@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
@@ -3,7 +3,13 @@
3
3
  import { checkboxStateForKeys, checkboxStateForSection, isSelectableRow, reduceSelection, selectionStateFromSnapshot } from "../selection.js";
4
4
  import { calculateAlignedScrollOffset, resolveLocationIndex, scrollFailure, validateOffset } from "../scrolling.js";
5
5
  import { applyRowPatches, validateSnapshot } from "../validation.js";
6
- const SECTION_INDEX_GUTTER = 44;
6
+ import { avatarPrefetchWindow } from "../avatarPrefetch.js";
7
+ import { acquireNativeListAvatar, canonicalNativeListAvatarUri } from "./NativeListWebAvatarCache.js";
8
+ const SECTION_INDEX_CONTENT_INSET = 16;
9
+ const SECTION_INDEX_RAIL_WIDTH = 32;
10
+ const SECTION_INDEX_EDGE_PADDING = 8;
11
+ const SECTION_INDEX_MIN_LABEL_SPACING = 14;
12
+ const SECTION_INDEX_MIN_HEIGHT = 120;
7
13
  const DEFAULT_VIEWPORT_WIDTH = 320;
8
14
  const DEFAULT_VIEWPORT_HEIGHT = 640;
9
15
  const OVERSCAN_VIEWPORTS = 1;
@@ -147,9 +153,22 @@ function approximateMessageHeight(row, availableWidth) {
147
153
  return 32 + titleLines * 20 + bodyLines * 20 + 22;
148
154
  }
149
155
  export function estimateWebRowHeight(row, snapshot, availableWidth) {
150
- if (row.type === 'system' && row.variant === 'spacer') return row.height;
151
- if (row.type === 'walletGroup') return (row.children.length + 1) * 68 + row.children.length * 12;
152
- if (row.type === 'identity' && row.presentation === 'walletSidebar') return 68;
156
+ // OneKey patch: explicit selector height takes precedence over presets.
157
+ // if (row.type === 'system' && row.variant === 'spacer') return row.height;
158
+ if (row.height !== undefined) return row.height;
159
+ if (row.type === 'system' && row.variant === 'warning') {
160
+ const width = Math.max(1, availableWidth - 24);
161
+ const lines = text => Math.max(1, Math.ceil(Array.from(text).reduce((length, char) => length + (char.charCodeAt(0) > 255 ? 14 : 7), 0) / width));
162
+ return 32 + 20 * (lines(row.title) + lines(row.message));
163
+ }
164
+ if (row.type === 'walletGroup')
165
+ // OneKey patch: wallet badges participate in the outer group height.
166
+ // return (row.children.length + 1) * 68 + row.children.length * 12;
167
+ return [row.parent, ...row.children].reduce((height, member) => height + estimateWebRowHeight(member, snapshot, availableWidth), 0) + row.children.length * 12 + (row.parent.height !== undefined ? 2 : 0);
168
+ // OneKey patch: reserve the source badge line below wallet names.
169
+ // if (row.type === 'identity' && row.presentation === 'walletSidebar')
170
+ // return 68;
171
+ if (row.type === 'identity' && row.presentation === 'walletSidebar') return 68 + (row.badges?.length ? 24 : 0);
153
172
  if (row.type === 'identity' && row.presentation === 'networkSelector') return 47;
154
173
  if (row.type === 'identity' && row.presentation === 'accountSelector') return 58;
155
174
  let base;
@@ -208,9 +227,10 @@ export function computeWebListLayout(snapshot, viewportWidth, viewportHeight, co
208
227
  const horizontal = snapshot.layout.orientation === 'horizontal';
209
228
  const spacing = snapshot.layout.itemSpacing ?? 0;
210
229
  const padding = paddingValues(snapshot);
211
- const indexGutter = sectionIndexEnabled(snapshot) ? SECTION_INDEX_GUTTER : 0;
212
230
  const width = Math.max(1, viewportWidth || DEFAULT_VIEWPORT_WIDTH);
213
231
  const height = Math.max(1, viewportHeight || DEFAULT_VIEWPORT_HEIGHT);
232
+ // OneKey patch: the index overlays the content and only keeps a small accessory-safe inset.
233
+ const indexGutter = sectionIndexEnabled(snapshot) && height >= SECTION_INDEX_MIN_HEIGHT ? SECTION_INDEX_CONTENT_INSET : 0;
214
234
  const availableWidth = Math.max(1, width - padding.horizontal * 2 - indexGutter);
215
235
  const availableHeight = Math.max(1, height - padding.top - padding.bottom);
216
236
  const items = [];
@@ -371,7 +391,10 @@ function rowWithoutSelectionState(row) {
371
391
  export function webRowRenderSignature(row) {
372
392
  return JSON.stringify(rowWithoutSelectionState(row));
373
393
  }
394
+
395
+ // OneKey patch: selector names must shrink before the sidebar clips their contents.
374
396
  export const WEB_LIST_CSS = `
397
+ [data-native-list-selector="walletSidebar"] .ok-native-list-title{max-width:100%;min-width:0}
375
398
  .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}
376
399
  .ok-native-list-viewport-frame{position:relative;flex:1;min-width:0;min-height:0;overflow:hidden}
377
400
  .ok-native-list-viewport{position:absolute;inset:0;overflow:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;scrollbar-gutter:stable}
@@ -382,6 +405,7 @@ export const WEB_LIST_CSS = `
382
405
  .ok-native-list-item[data-native-list-selected="true"]>.ok-native-list-row{background:var(--nl-selected)}
383
406
  .ok-native-list-item[data-native-list-disabled="true"]>.ok-native-list-row{opacity:.5;cursor:default}
384
407
  .ok-native-list-item:not([data-native-list-disabled="true"]):hover>.ok-native-list-row{background:var(--nl-pressed)}
408
+ .ok-native-list-item:not([data-native-list-disabled="true"]):active>.ok-native-list-row{background:var(--nl-pressed)}
385
409
  .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)}
386
410
  .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)}
387
411
  .ok-native-list-item[data-native-list-selected="true"]:hover>.ok-native-list-wallet-row{background:var(--nl-selected)}
@@ -424,9 +448,29 @@ export const WEB_LIST_CSS = `
424
448
  .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}
425
449
  .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)}
426
450
  .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}
427
- .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)}
451
+ .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)}
428
452
  .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)}
453
+ .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)}
454
+ .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}
429
455
  @media (prefers-reduced-motion:reduce){.ok-native-list-index-preview,.ok-native-list-refresh{transition:none}.ok-native-list-spinner{animation:none}}
456
+ /* OneKey patch: selector controls follow their original semantic colors and geometry. */
457
+ .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))}
458
+ .ok-native-list-checkbox[data-selector="networkSelector"]::after{display:none}
459
+ .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}
460
+ .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))}
461
+ .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}
462
+ /* OneKey patch: source WalletListItem uses four-point padding on every side. */
463
+ .ok-native-list-wallet-row[data-native-list-selector="walletSidebar"]{border-radius:12px;padding:4px}
464
+ /* OneKey patch: selected group members use the same primary title color as standalone wallets. */
465
+ .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)}
466
+ .ok-native-list-wallet-row[data-native-list-selector="walletSidebar"] .ok-native-list-wallet-badges{height:18px}
467
+ .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)}
468
+ .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)}
469
+ .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}
470
+ /* OneKey patch: AccountSelectorAccountListItem fixes the borderless Plus slot at top18/right20. */
471
+ .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}
472
+ .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}
473
+ .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)}
430
474
  `;
431
475
  function createElement(document, tag, className, text) {
432
476
  const element = document.createElement(tag);
@@ -442,12 +486,105 @@ function safeImageUri(uri) {
442
486
  if (/^(https?:|data:image\/|blob:|file:)/i.test(trimmed) || trimmed.startsWith('/')) return trimmed;
443
487
  return undefined;
444
488
  }
489
+
490
+ // OneKey patch: retries belong to an image binding and must not outlive recycled rows.
491
+ const webImageRetryCleanup = new WeakMap();
492
+ const webAvatarCleanup = new WeakMap();
493
+ const webAvatarSources = new WeakMap();
494
+ function disposeWebImageRetries(element) {
495
+ let disposed = false;
496
+ const images = element.matches('img') ? [element] : element.querySelectorAll('img');
497
+ images.forEach(image => {
498
+ const avatarCleanup = webAvatarCleanup.get(image);
499
+ const cleanup = webImageRetryCleanup.get(image);
500
+ if (!cleanup && !avatarCleanup) return;
501
+ avatarCleanup?.();
502
+ webAvatarCleanup.delete(image);
503
+ webAvatarSources.delete(image);
504
+ cleanup?.();
505
+ webImageRetryCleanup.delete(image);
506
+ disposed = true;
507
+ });
508
+ return disposed;
509
+ }
510
+ function configureWebImageRetry(image, source, initialUri) {
511
+ const fallbackUri = source.fallbackUri ? safeImageUri(source.fallbackUri) : undefined;
512
+ const retryLimit = Number.isFinite(source.retryTimes) ? Math.max(0, Math.floor(source.retryTimes ?? 0)) : 0;
513
+ if (!fallbackUri && retryLimit === 0) return;
514
+ const view = image.ownerDocument.defaultView;
515
+ let currentUri = initialUri;
516
+ let usedFallback = false;
517
+ let retryCount = 0;
518
+ let retryTimer;
519
+ let disposed = false;
520
+ const clearRetry = () => {
521
+ if (retryTimer !== undefined) view?.clearTimeout(retryTimer);
522
+ retryTimer = undefined;
523
+ };
524
+ const handleError = event => {
525
+ if (disposed || !image.isConnected || retryTimer !== undefined) {
526
+ event.stopImmediatePropagation();
527
+ return;
528
+ }
529
+ if (fallbackUri && !usedFallback && fallbackUri !== currentUri) {
530
+ event.stopImmediatePropagation();
531
+ usedFallback = true;
532
+ currentUri = fallbackUri;
533
+ image.src = currentUri;
534
+ return;
535
+ }
536
+ if (retryCount >= retryLimit || !view) return;
537
+ event.stopImmediatePropagation();
538
+ retryCount += 1;
539
+ retryTimer = view.setTimeout(() => {
540
+ retryTimer = undefined;
541
+ if (disposed || !image.isConnected) return;
542
+ image.removeAttribute('src');
543
+ image.src = currentUri;
544
+ }, Math.floor(Math.random() * 3) * 1000);
545
+ };
546
+ image.addEventListener('error', handleError);
547
+ image.addEventListener('load', clearRetry);
548
+ webImageRetryCleanup.set(image, () => {
549
+ disposed = true;
550
+ clearRetry();
551
+ image.removeEventListener('error', handleError);
552
+ image.removeEventListener('load', clearRetry);
553
+ });
554
+ }
555
+ function configureWebAvatar(image, source, uri) {
556
+ const dispose = acquireNativeListAvatar(image.ownerDocument, uri, resolvedUri => {
557
+ configureWebImageRetry(image, source, resolvedUri);
558
+ image.src = resolvedUri;
559
+ }, () => {
560
+ const fallbackUri = source.fallbackUri ? safeImageUri(source.fallbackUri) : undefined;
561
+ if (fallbackUri) {
562
+ configureWebImageRetry(image, source, fallbackUri);
563
+ image.src = fallbackUri;
564
+ return;
565
+ }
566
+ const ImageEvent = image.ownerDocument.defaultView?.Event;
567
+ if (ImageEvent) image.dispatchEvent(new ImageEvent('error'));
568
+ });
569
+ webAvatarSources.set(image, {
570
+ uri,
571
+ source
572
+ });
573
+ webAvatarCleanup.set(image, dispose);
574
+ }
445
575
  function createImage(context, source, className) {
446
- const uri = safeImageUri(source.uri);
576
+ const avatarUri = canonicalNativeListAvatarUri(source.uri);
577
+ const uri = avatarUri ?? safeImageUri(source.uri);
447
578
  if (!uri) return undefined;
448
579
  const image = context.document.createElement('img');
449
580
  if (className) image.className = className;
450
- image.src = uri;
581
+ // OneKey patch: consume recoverable errors before the visual's final fallback listener.
582
+ if (avatarUri) {
583
+ configureWebAvatar(image, source, avatarUri);
584
+ } else {
585
+ configureWebImageRetry(image, source, uri);
586
+ image.src = uri;
587
+ }
451
588
  image.alt = '';
452
589
  image.draggable = false;
453
590
  image.loading = 'lazy';
@@ -455,6 +592,247 @@ function createImage(context, source, className) {
455
592
  image.style.objectFit = source.contentFit === 'fill' ? 'fill' : source.contentFit ?? 'cover';
456
593
  return image;
457
594
  }
595
+
596
+ // OneKey patch: React Native Web paints selector images as centered CSS backgrounds.
597
+ // The image keeps its loading/error lifecycle; only its replaced-element pixels are hidden.
598
+ function paintSelectorImageBackground(image, frame, inset = 0) {
599
+ const paint = createElement(image.ownerDocument, 'span', 'ok-native-list-selector-image-background');
600
+ paint.style.cssText = 'position:absolute;pointer-events:none;border-radius:inherit;background-position:center;background-repeat:no-repeat';
601
+ paint.style.inset = String(inset) + 'px';
602
+ paint.style.backgroundSize = image.style.objectFit === 'fill' ? '100% 100%' : image.style.objectFit === 'center' ? 'auto' : image.style.objectFit;
603
+ image.style.opacity = '0';
604
+ const update = () => {
605
+ paint.style.backgroundImage = 'url(' + JSON.stringify(image.currentSrc || image.src) + ')';
606
+ };
607
+ image.addEventListener('load', update);
608
+ image.addEventListener('error', () => {
609
+ paint.style.backgroundImage = 'none';
610
+ });
611
+ frame.insertBefore(paint, image);
612
+ if (image.complete && image.naturalWidth > 0) update();
613
+ }
614
+
615
+ // OneKey patch: use source SVG paths for selector actions and wallet provider marks.
616
+ const selectorIcons = {
617
+ GlobusOutline: {
618
+ viewBox: '0 0 24 24',
619
+ paths: [{
620
+ 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',
621
+ fill: 'currentColor',
622
+ fillRule: 'evenodd',
623
+ opacity: 1.0
624
+ }]
625
+ },
626
+ LockSolid: {
627
+ viewBox: '0 0 24 24',
628
+ paths: [{
629
+ 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',
630
+ fill: 'currentColor',
631
+ fillRule: 'evenodd',
632
+ opacity: 1.0
633
+ }]
634
+ },
635
+ GoogleIllus: {
636
+ viewBox: '0 0 24 24',
637
+ paths: [{
638
+ 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',
639
+ fill: '#4285F4',
640
+ fillRule: 'nonzero',
641
+ opacity: 1.0
642
+ }, {
643
+ 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',
644
+ fill: '#34A853',
645
+ fillRule: 'nonzero',
646
+ opacity: 1.0
647
+ }, {
648
+ 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',
649
+ fill: '#FBBC05',
650
+ fillRule: 'nonzero',
651
+ opacity: 1.0
652
+ }, {
653
+ 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',
654
+ fill: '#EA4335',
655
+ fillRule: 'nonzero',
656
+ opacity: 1.0
657
+ }]
658
+ },
659
+ AppleBrand: {
660
+ viewBox: '0 0 16 20',
661
+ paths: [{
662
+ 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',
663
+ fill: 'currentColor',
664
+ fillRule: 'nonzero',
665
+ opacity: 1.0
666
+ }]
667
+ },
668
+ BotIllus: {
669
+ viewBox: '0 0 24 24',
670
+ paths: [{
671
+ 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',
672
+ fill: '#8897A5',
673
+ fillRule: 'nonzero',
674
+ opacity: 1.0
675
+ }, {
676
+ 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',
677
+ fill: '#3FA9F5',
678
+ fillRule: 'nonzero',
679
+ opacity: 1.0
680
+ }, {
681
+ 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',
682
+ fill: '#8897A5',
683
+ fillRule: 'nonzero',
684
+ opacity: 1.0
685
+ }, {
686
+ 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',
687
+ fill: '#8897A5',
688
+ fillRule: 'nonzero',
689
+ opacity: 1.0
690
+ }, {
691
+ d: 'M7.5 12.0a1.5 1.5 0 1 0 3.0 0a1.5 1.5 0 1 0 -3.0 0',
692
+ fill: '#10243E',
693
+ fillRule: 'nonzero',
694
+ opacity: 1.0
695
+ }, {
696
+ d: 'M13.5 12.0a1.5 1.5 0 1 0 3.0 0a1.5 1.5 0 1 0 -3.0 0',
697
+ fill: '#10243E',
698
+ fillRule: 'nonzero',
699
+ opacity: 1.0
700
+ }, {
701
+ 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',
702
+ fill: '#10243E',
703
+ fillRule: 'nonzero',
704
+ opacity: 1.0
705
+ }]
706
+ },
707
+ AllNetworksSolid: {
708
+ viewBox: '0 0 24 24',
709
+ paths: [{
710
+ d: 'M15.333 13.998a1.335 1.335 0 1 1 0 2.67 1.335 1.335 0 0 1 0-2.67',
711
+ fill: 'currentColor',
712
+ fillRule: 'nonzero',
713
+ opacity: 1.0
714
+ }, {
715
+ 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',
716
+ fill: 'currentColor',
717
+ fillRule: 'evenodd',
718
+ opacity: 1.0
719
+ }]
720
+ },
721
+ CrossedSmallSolid: {
722
+ viewBox: '0 0 24 24',
723
+ paths: [{
724
+ 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',
725
+ fill: 'currentColor',
726
+ fillRule: 'nonzero',
727
+ opacity: 1.0
728
+ }]
729
+ },
730
+ AccountErrorCustom: {
731
+ viewBox: '0 0 18 18',
732
+ paths: [{
733
+ d: 'M12.5 12.75a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5',
734
+ fill: '#000',
735
+ fillRule: 'nonzero',
736
+ opacity: 0.447
737
+ }, {
738
+ 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',
739
+ fill: '#000',
740
+ fillRule: 'evenodd',
741
+ opacity: 0.447
742
+ }]
743
+ },
744
+ PlusSmallOutline: {
745
+ viewBox: '0 0 24 24',
746
+ paths: [{
747
+ d: 'M13 11h5v2h-5v5h-2v-5H6v-2h5V6h2z',
748
+ fill: 'currentColor',
749
+ fillRule: 'nonzero',
750
+ opacity: 1.0
751
+ }]
752
+ },
753
+ DotHorOutline: {
754
+ viewBox: '0 0 24 24',
755
+ paths: [{
756
+ d: 'M6 14H2v-4h4zm8 0h-4v-4h4zm8 0h-4v-4h4z',
757
+ fill: 'currentColor',
758
+ fillRule: 'nonzero',
759
+ opacity: 1.0
760
+ }]
761
+ },
762
+ ChevronRightSmallOutline: {
763
+ viewBox: '0 0 24 24',
764
+ paths: [{
765
+ d: 'M15.414 12 10 17.414 8.586 16l4-4-4-4L10 6.586z',
766
+ fill: 'currentColor',
767
+ fillRule: 'nonzero',
768
+ opacity: 1.0
769
+ }]
770
+ },
771
+ DragOutline: {
772
+ viewBox: '0 0 24 24',
773
+ paths: [{
774
+ d: 'M11 21H7v-4h4zm6 0h-4v-4h4zm-6-7H7v-4h4zm6 0h-4v-4h4zm-6-7H7V3h4zm6 0h-4V3h4z',
775
+ fill: 'currentColor',
776
+ fillRule: 'nonzero',
777
+ opacity: 1.0
778
+ }]
779
+ },
780
+ PencilOutline: {
781
+ viewBox: '0 0 24 24',
782
+ paths: [{
783
+ 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',
784
+ fill: 'currentColor',
785
+ fillRule: 'evenodd',
786
+ opacity: 1.0
787
+ }]
788
+ },
789
+ CheckboxCheckedCustom: {
790
+ viewBox: '0 0 16 16',
791
+ paths: [{
792
+ 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',
793
+ fill: 'currentColor',
794
+ fillRule: 'evenodd',
795
+ opacity: 1.0
796
+ }]
797
+ },
798
+ CheckboxIndeterminateCustom: {
799
+ viewBox: '0 0 16 16',
800
+ paths: [{
801
+ d: 'M4 8a1 1 0 0 1 1-1h6a1 1 0 0 1 0 2H5a1 1 0 0 1-1-1',
802
+ fill: 'currentColor',
803
+ fillRule: 'evenodd',
804
+ opacity: 1.0
805
+ }]
806
+ },
807
+ Circle: {
808
+ viewBox: '0 0 24 24',
809
+ paths: [{
810
+ d: 'M0 12a12 12 0 1 0 24 0a12 12 0 1 0 -24 0',
811
+ fill: 'currentColor',
812
+ fillRule: 'nonzero',
813
+ opacity: 1
814
+ }]
815
+ }
816
+ };
817
+ function applySelectorIcon(element, name) {
818
+ const icon = selectorIcons[name];
819
+ if (!icon) return;
820
+ element.textContent = '';
821
+ const svg = element.ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'svg');
822
+ svg.setAttribute('viewBox', icon.viewBox);
823
+ svg.setAttribute('width', '24');
824
+ svg.setAttribute('height', '24');
825
+ svg.setAttribute('aria-hidden', 'true');
826
+ icon.paths.forEach(path => {
827
+ const child = element.ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'path');
828
+ child.setAttribute('d', path.d);
829
+ child.setAttribute('fill', path.fill);
830
+ child.setAttribute('fill-rule', path.fillRule);
831
+ child.setAttribute('fill-opacity', String(path.opacity));
832
+ svg.appendChild(child);
833
+ });
834
+ element.appendChild(svg);
835
+ }
458
836
  function iconGlyph(name) {
459
837
  const normalized = name.toLocaleLowerCase();
460
838
  if (normalized.includes('chevron')) {
@@ -484,7 +862,7 @@ function visualFromRow(row) {
484
862
  if (row.type === 'metricCard') return row.visual;
485
863
  return undefined;
486
864
  }
487
- function createVisual(context, visual) {
865
+ function createVisual(context, visual, selectorPresentation) {
488
866
  if (!visual) return undefined;
489
867
  if (visual.kind === 'stackedImages') {
490
868
  const stack = createElement(context.document, 'span', 'ok-native-list-stacked');
@@ -500,6 +878,7 @@ function createVisual(context, visual) {
500
878
  if (visual.kind === 'icon') {
501
879
  frame.style.background = visual.backgroundColor ?? 'var(--nl-strong)';
502
880
  const fallback = createElement(context.document, 'span', 'ok-native-list-visual-fallback', iconGlyph(visual.name));
881
+ applySelectorIcon(fallback, visual.name);
503
882
  if (visual.tintColor) fallback.style.color = visual.tintColor;
504
883
  frame.appendChild(fallback);
505
884
  return frame;
@@ -510,6 +889,7 @@ function createVisual(context, visual) {
510
889
  if (image) {
511
890
  image.className = 'ok-native-list-visual-main';
512
891
  frame.appendChild(image);
892
+ if (selectorPresentation) paintSelectorImageBackground(image, frame);
513
893
  } else {
514
894
  frame.appendChild(createElement(context.document, 'span', 'ok-native-list-visual-fallback', 'fallbackText' in visual ? visual.fallbackText ?? '' : ''));
515
895
  }
@@ -520,8 +900,64 @@ function createVisual(context, visual) {
520
900
  const corner = createElement(context.document, 'span', 'ok-native-list-visual-corner ok-native-list-visual-fallback', iconGlyph(visual.cornerIcon.name));
521
901
  if (visual.cornerIcon.tintColor) corner.style.color = visual.cornerIcon.tintColor;
522
902
  if (visual.cornerIcon.backgroundColor) corner.style.background = visual.cornerIcon.backgroundColor;
903
+ applySelectorIcon(corner, visual.cornerIcon.name);
523
904
  frame.appendChild(corner);
524
905
  }
906
+ // OneKey patch: image failure uses the same source-derived fallback as v1.
907
+ if ('fallbackIcon' in visual && visual.fallbackIcon) {
908
+ const icon = visual.fallbackIcon;
909
+ const showFallback = () => {
910
+ if (image) {
911
+ disposeWebImageRetries(image);
912
+ image.remove();
913
+ }
914
+ frame.querySelector('.ok-native-list-visual-fallback:not(.ok-native-list-visual-corner)')?.remove();
915
+ const fallback = createElement(context.document, 'span', 'ok-native-list-visual-fallback');
916
+ applySelectorIcon(fallback, icon.name);
917
+ if (icon.tintColor) fallback.style.color = icon.tintColor;
918
+ frame.prepend(fallback);
919
+ };
920
+ if (image) image.addEventListener('error', showFallback, {
921
+ once: true
922
+ });else showFallback();
923
+ }
924
+ if ('borderStyle' in visual && visual.borderStyle === 'dashed') {
925
+ frame.style.border = '2px dashed ' + (visual.borderColor ?? 'var(--nl-disabled)');
926
+ frame.style.boxSizing = 'border-box';
927
+ }
928
+ if ('overlays' in visual) visual.overlays?.forEach(overlay => {
929
+ const corner = createElement(context.document, 'span', 'ok-native-list-visual-overlay', overlay.text);
930
+ const size = overlay.size ?? 20;
931
+ const isWalletText = selectorPresentation === 'walletSidebar' && !!overlay.text && !overlay.image && !overlay.name;
932
+ corner.style.width = isWalletText && overlay.width === undefined ? 'auto' : String(overlay.width ?? size) + 'px';
933
+ corner.style.height = String(overlay.height ?? (isWalletText ? 16 : size)) + 'px';
934
+ corner.style.padding = isWalletText ? '0 2px' : String(overlay.padding ?? 0) + 'px';
935
+ if (isWalletText) {
936
+ corner.style.fontSize = '12px';
937
+ corner.style.lineHeight = '16px';
938
+ corner.style.fontWeight = '400';
939
+ }
940
+ if (isWalletText || overlay.width !== undefined || overlay.height !== undefined) corner.style.borderRadius = '9999px';
941
+ const offsetX = String(-(overlay.offsetX ?? overlay.offset ?? 2)) + 'px';
942
+ const offsetY = String(-(overlay.offsetY ?? overlay.offset ?? 2)) + 'px';
943
+ corner.style.background = overlay.backgroundColor ?? 'transparent';
944
+ corner.style.color = overlay.tintColor ?? 'var(--nl-secondary)';
945
+ if (overlay.position === 'topLeft') {
946
+ corner.style.left = offsetX;
947
+ corner.style.top = offsetY;
948
+ } else {
949
+ corner.style.right = offsetX;
950
+ corner.style.bottom = offsetY;
951
+ }
952
+ if (overlay.image) {
953
+ const overlayImage = createImage(context, overlay.image);
954
+ if (overlayImage) {
955
+ corner.appendChild(overlayImage);
956
+ if (selectorPresentation) paintSelectorImageBackground(overlayImage, corner, overlay.padding ?? 0);
957
+ }
958
+ } else if (overlay.name) applySelectorIcon(corner, overlay.name);
959
+ frame.appendChild(corner);
960
+ });
525
961
  return frame;
526
962
  }
527
963
  function toneColor(tone, fallback) {
@@ -567,6 +1003,19 @@ function createCheckbox(context, rowKey, accessory) {
567
1003
  setData(element, 'checkboxFallback', accessory.state);
568
1004
  setData(element, 'nativeListAction', accessory.actionKey ?? 'selection');
569
1005
  setData(element, 'selectionScope', accessory.target?.scope ?? 'row');
1006
+ const row = context.snapshot.rows[context.itemIndex];
1007
+ if (row && 'presentation' in row && row.presentation === 'networkSelector') {
1008
+ setData(element, 'selector', 'networkSelector');
1009
+ for (const [state, name] of [['checked', 'CheckboxCheckedCustom'], ['indeterminate', 'CheckboxIndeterminateCustom']]) {
1010
+ const holder = createElement(context.document, 'span');
1011
+ applySelectorIcon(holder, name);
1012
+ const svg = holder.firstElementChild;
1013
+ if (svg) {
1014
+ svg.setAttribute('data-state', state);
1015
+ element.appendChild(svg);
1016
+ }
1017
+ }
1018
+ }
570
1019
  if (accessory.target?.scope === 'section') setData(element, 'selectionKey', accessory.target.sectionKey);else if (accessory.target?.scope === 'row') setData(element, 'selectionKey', rowKey);
571
1020
  return element;
572
1021
  }
@@ -576,6 +1025,7 @@ function createIconAction(context, name, actionKey, disabled, tintColor) {
576
1025
  element.setAttribute('type', 'button');
577
1026
  element.toggleAttribute('disabled', Boolean(disabled));
578
1027
  }
1028
+ applySelectorIcon(element, name);
579
1029
  if (actionKey) setData(element, 'nativeListAction', actionKey);
580
1030
  if (tintColor) element.style.color = tintColor;
581
1031
  return element;
@@ -584,6 +1034,23 @@ function markActionAnchorSource(element, source, slot) {
584
1034
  setData(element, 'nativeListAnchorSource', source);
585
1035
  if (slot !== undefined) setData(element, 'nativeListAnchorSlot', slot);
586
1036
  }
1037
+
1038
+ // OneKey patch: the compact zero-count digits share the amount baseline.
1039
+ function applyValueSegments(element, segments, fontSize = 16, lineHeight = 24, weight = 500) {
1040
+ if (!segments?.length) return;
1041
+ element.textContent = '';
1042
+ element.style.fontSize = String(fontSize) + 'px';
1043
+ element.style.lineHeight = String(lineHeight) + 'px';
1044
+ element.style.fontWeight = String(weight);
1045
+ segments.forEach(segment => {
1046
+ const span = createElement(element.ownerDocument, 'span', undefined, segment.text);
1047
+ if (segment.style === 'subscript') {
1048
+ span.style.fontSize = String(Math.ceil(fontSize * 0.6)) + 'px';
1049
+ span.style.lineHeight = String(fontSize) + 'px';
1050
+ }
1051
+ element.appendChild(span);
1052
+ });
1053
+ }
587
1054
  function createAccessory(context, rowKey, accessory, slot) {
588
1055
  if (accessory.kind === 'checkbox') {
589
1056
  const element = createCheckbox(context, rowKey, accessory);
@@ -593,6 +1060,18 @@ function createAccessory(context, rowKey, accessory, slot) {
593
1060
  if (accessory.kind === 'icon') {
594
1061
  const element = createIconAction(context, accessory.name, accessory.actionKey, accessory.disabled, accessory.tintColor);
595
1062
  markActionAnchorSource(element, 'trailingAccessory', slot);
1063
+ setData(element, 'testid', accessory.testID);
1064
+ if (accessory.hoverActionKey) setData(element, 'nativeListHoverAction', accessory.hoverActionKey);
1065
+ if (accessory.accessibilityLabel) element.setAttribute('aria-label', accessory.accessibilityLabel);
1066
+ const row = context.snapshot.rows[context.itemIndex];
1067
+ if (row && 'presentation' in row && row.presentation === 'accountSelector') {
1068
+ setData(element, 'nativeListAnchorInset', 7);
1069
+ // OneKey patch: the create-address button omits IconButton's one-point border.
1070
+ if (row.height !== undefined && accessory.name === 'PlusSmallOutline') {
1071
+ setData(element, 'nativeListAccountControl', 'createAddress');
1072
+ if (!accessory.tintColor) element.style.color = 'var(--nl-icon-subdued)';
1073
+ }
1074
+ }
596
1075
  return element;
597
1076
  }
598
1077
  if (accessory.kind === 'spinner') {
@@ -608,6 +1087,7 @@ function createAccessory(context, rowKey, accessory, slot) {
608
1087
  switch (accessory.kind) {
609
1088
  case 'value':
610
1089
  element.textContent = accessory.text;
1090
+ applyValueSegments(element, accessory.textSegments);
611
1091
  if (accessory.secondary) element.classList.add('ok-native-list-accessory-secondary');
612
1092
  break;
613
1093
  case 'valuePair':
@@ -646,6 +1126,13 @@ function createAccessory(context, rowKey, accessory, slot) {
646
1126
  function appendAccessories(parent, context, rowKey, accessories) {
647
1127
  if (!accessories?.length) return;
648
1128
  const container = createElement(context.document, 'span', 'ok-native-list-accessories');
1129
+ const row = context.snapshot.rows[context.itemIndex];
1130
+ if (row && row.height !== undefined && 'presentation' in row && row.presentation === 'networkSelector') {
1131
+ container.style.gap = accessories.some(accessory => accessory.kind === 'checkbox') ? '12px' : '20px';
1132
+ }
1133
+ if (row && row.height !== undefined && 'presentation' in row && row.presentation === 'accountSelector' && accessories.length === 1 && accessories[0]?.kind === 'icon' && accessories[0].name === 'PlusSmallOutline') {
1134
+ setData(container, 'nativeListAccountControl', 'createAddress');
1135
+ }
649
1136
  accessories.forEach((accessory, slot) => container.appendChild(createAccessory(context, rowKey, accessory, slot)));
650
1137
  parent.appendChild(container);
651
1138
  }
@@ -673,9 +1160,83 @@ function createSectionHeader(context, row) {
673
1160
  markActionAnchorSource(titleIcon, 'leadingAction');
674
1161
  body.appendChild(titleIcon);
675
1162
  }
676
- body.appendChild(createTextColumn(context, row.title, row.subtitle));
1163
+ // OneKey patch: section title help has its own measurable action target.
1164
+ // body.appendChild(createTextColumn(context, row.title, row.subtitle));
1165
+ const column = createTextColumn(context, row.title, row.subtitle);
1166
+ const title = column.firstElementChild;
1167
+ title.classList.add('ok-native-list-section-title');
1168
+ if (row.titleActionKey) {
1169
+ setData(title, 'nativeListAction', row.titleActionKey);
1170
+ markActionAnchorSource(title, 'leadingAction');
1171
+ title.setAttribute('role', 'button');
1172
+ title.tabIndex = 0;
1173
+ title.style.alignSelf = 'flex-start';
1174
+ title.style.maxWidth = '100%';
1175
+ // OneKey patch: explicit network headers reserve a separate 3-point underline area.
1176
+ if (row.presentation !== 'networkSelector' || row.height === undefined) {
1177
+ title.style.textDecoration = 'underline dotted';
1178
+ title.style.textUnderlineOffset = '6px';
1179
+ }
1180
+ if (row.titleActionOnHover) setData(title, 'nativeListHoverAction', true);
1181
+ }
1182
+ if (row.presentation === 'networkSelector' && row.height !== undefined) {
1183
+ body.style.padding = row.variant === 'summary' ? '24px 12px 20px' : '0 12px';
1184
+ body.style.backgroundColor = 'var(--nl-bg)';
1185
+ body.style.gap = row.checkbox ? '12px' : '8px';
1186
+ title.style.fontSize = row.variant === 'summary' ? '16px' : '14px';
1187
+ title.style.lineHeight = row.variant === 'summary' ? '24px' : '20px';
1188
+ title.style.fontWeight = row.variant === 'summary' || row.titleActionKey && !row.checkbox ? '500' : '600';
1189
+ if (row.titleActionKey) {
1190
+ const text = createElement(context.document, 'span', 'ok-native-list-section-title-text', row.title);
1191
+ text.style.overflow = 'hidden';
1192
+ text.style.textOverflow = 'ellipsis';
1193
+ text.style.maxWidth = '100%';
1194
+ const dotted = context.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
1195
+ dotted.setAttribute('height', '2');
1196
+ dotted.style.cssText = 'display:block;position:absolute;left:0;bottom:0;width:100%;height:2px;color:var(--nl-secondary)';
1197
+ const line = context.document.createElementNS('http://www.w3.org/2000/svg', 'line');
1198
+ for (const [key, value] of Object.entries({
1199
+ x1: '1',
1200
+ y1: '1',
1201
+ x2: '100%',
1202
+ y2: '1',
1203
+ stroke: 'currentColor',
1204
+ 'stroke-width': '1.5',
1205
+ 'stroke-dasharray': '0,4',
1206
+ 'stroke-linecap': 'round'
1207
+ })) line.setAttribute(key, value);
1208
+ // OneKey patch: keep both round caps inside the original full-width viewport.
1209
+ const lineViewport = context.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
1210
+ lineViewport.setAttribute('width', 'calc(100% - 1px)');
1211
+ lineViewport.setAttribute('height', '2');
1212
+ lineViewport.setAttribute('overflow', 'visible');
1213
+ lineViewport.appendChild(line);
1214
+ dotted.appendChild(lineViewport);
1215
+ // OneKey patch: SVG intrinsic width must not expand the title action beyond its text.
1216
+ title.style.display = 'block';
1217
+ title.style.position = 'relative';
1218
+ title.style.width = 'fit-content';
1219
+ title.style.paddingBottom = '3px';
1220
+ text.style.display = 'block';
1221
+ title.replaceChildren(text, dotted);
1222
+ }
1223
+ }
1224
+ body.appendChild(column);
677
1225
  if (row.value) {
678
1226
  const value = createElement(context.document, row.valueActionKey ? 'button' : 'span', row.valueActionKey ? 'ok-native-list-action-button ok-native-list-section-value' : 'ok-native-list-value ok-native-list-section-value', row.value);
1227
+ applyValueSegments(value, row.valueSegments);
1228
+ if (row.presentation === 'networkSelector' && row.height !== undefined) {
1229
+ value.style.fontFamily = 'inherit';
1230
+ value.style.fontSize = '16px';
1231
+ value.style.lineHeight = '24px';
1232
+ value.style.fontWeight = '500';
1233
+ if (row.valueActionKey) {
1234
+ value.style.color = 'var(--nl-secondary)';
1235
+ value.style.padding = '0';
1236
+ value.style.flexShrink = '0';
1237
+ }
1238
+ }
1239
+ if (row.valueActionTestID) setData(value, 'testid', row.valueActionTestID);
679
1240
  if (row.valueActionKey) {
680
1241
  value.setAttribute('type', 'button');
681
1242
  setData(value, 'nativeListAction', row.valueActionKey);
@@ -696,6 +1257,7 @@ function createActionRow(context, row) {
696
1257
  if (row.icon) body.appendChild(createVisual(context, row.icon));
697
1258
  const title = createElement(context.document, 'span', 'ok-native-list-action-title', row.title);
698
1259
  setData(title, 'tone', row.tone);
1260
+ if (row.presentation === 'accountSelector' && row.icon) title.style.fontWeight = '500';
699
1261
  body.appendChild(title);
700
1262
  if (row.checkbox) body.appendChild(createCheckbox(context, row.key, row.checkbox));
701
1263
  appendAccessories(body, context, row.key, row.trailing);
@@ -704,6 +1266,13 @@ function createActionRow(context, row) {
704
1266
  function createSystemRow(context, row) {
705
1267
  const body = createElement(context.document, 'div', 'ok-native-list-row ok-native-list-system');
706
1268
  setData(body, 'variant', row.variant);
1269
+ if (row.variant === 'warning') {
1270
+ body.classList.add('ok-native-list-warning');
1271
+ body.style.borderColor = row.borderColor ?? 'var(--nl-separator)';
1272
+ body.appendChild(createElement(context.document, 'span', 'ok-native-list-warning-title', row.title));
1273
+ body.appendChild(createElement(context.document, 'span', 'ok-native-list-warning-message', row.message));
1274
+ return body;
1275
+ }
707
1276
  if (row.variant === 'loading') body.appendChild(createElement(context.document, 'span', 'ok-native-list-spinner'));
708
1277
  const message = row.variant === 'spacer' ? '' : row.message ?? (row.variant === 'end' ? 'End' : '');
709
1278
  if (message) body.appendChild(createElement(context.document, 'span', 'ok-native-list-secondary', message));
@@ -852,12 +1421,31 @@ function createDataRow(context, row) {
852
1421
  function createIdentityActivityOrMessageRow(context, row) {
853
1422
  const presentation = row.type === 'identity' ? row.presentation : undefined;
854
1423
  const body = createElement(context.document, 'div', ['ok-native-list-row', 'ok-native-list-standard', row.type === 'identity' && !presentation ? 'ok-native-list-identity-row' : '', presentation === 'networkSelector' ? 'ok-native-list-network-row' : '', presentation === 'walletSidebar' ? 'ok-native-list-wallet-row' : '', presentation === 'accountSelector' ? 'ok-native-list-account-row' : ''].filter(Boolean).join(' '));
1424
+ setData(body, 'nativeListSelector', row.height !== undefined ? presentation : undefined);
1425
+ if (row.type === 'identity' && row.titleActionKey && row.titleActionOnHover) {
1426
+ setData(body, 'nativeListHoverAction', row.titleActionKey);
1427
+ markActionAnchorSource(body, 'leadingAction');
1428
+ }
855
1429
  if (row.type === 'identity' && row.leadingAction) {
856
1430
  const action = createIconAction(context, row.leadingAction.name, row.leadingAction.actionKey, row.leadingAction.disabled, row.leadingAction.tintColor);
857
1431
  markActionAnchorSource(action, 'leadingAction');
858
1432
  body.appendChild(action);
859
1433
  }
860
- const visual = createVisual(context, visualFromRow(row));
1434
+ const visual = createVisual(context, visualFromRow(row), row.height !== undefined ? presentation : undefined);
1435
+ if (visual && row.type === 'identity' && row.height !== undefined && row.presentation === 'walletSidebar' && 'fallbackIcon' in row.leading && row.leading.fallbackIcon?.name === 'LockSolid') {
1436
+ // OneKey patch: hidden-wallet locks use WalletAvatar's full 40-point icon.
1437
+ const icon = visual.querySelector('.ok-native-list-visual-fallback svg');
1438
+ if (icon) {
1439
+ icon.style.width = '40px';
1440
+ icon.style.height = '40px';
1441
+ }
1442
+ const fallback = visual.querySelector('.ok-native-list-visual-fallback');
1443
+ if (fallback) {
1444
+ fallback.style.borderRadius = '0';
1445
+ fallback.style.overflow = 'visible';
1446
+ }
1447
+ }
1448
+ if (visual && row.type === 'identity' && row.height !== undefined && row.presentation === 'walletSidebar' && 'borderStyle' in row.leading && row.leading.borderStyle === 'dashed') visual.style.borderWidth = '1px';
861
1449
  if (visual) body.appendChild(visual);
862
1450
  if (row.type === 'activity' && row.secondaryLeading) {
863
1451
  const secondVisual = createVisual(context, row.secondaryLeading);
@@ -866,7 +1454,46 @@ function createIdentityActivityOrMessageRow(context, row) {
866
1454
  if (row.type === 'message' && row.unread) body.appendChild(createElement(context.document, 'span', 'ok-native-list-unread'));
867
1455
  const title = row.title;
868
1456
  const subtitle = row.type === 'identity' ? row.subtitle : row.type === 'activity' ? row.description : row.body;
869
- const column = createTextColumn(context, title, subtitle, row.type === 'identity' ? row.tertiary : undefined, row.type === 'identity' ? row.tertiaryTone : undefined, row.type === 'identity' ? row.badges : undefined);
1457
+ const column = createTextColumn(context, title, subtitle, row.type === 'identity' ? row.tertiary : undefined, row.type === 'identity' ? row.tertiaryTone : undefined, row.type === 'identity' && presentation !== 'walletSidebar' ? row.badges : undefined);
1458
+ // OneKey patch: match existing search, subtitle fragments, and sidebar badges.
1459
+ if (row.type === 'identity') {
1460
+ const titleElement = column.firstElementChild;
1461
+ if (row.titleMatch?.length) {
1462
+ const firstText = titleElement.firstChild;
1463
+ if (firstText) firstText.remove();
1464
+ const fragment = context.document.createDocumentFragment();
1465
+ let offset = 0;
1466
+ row.titleMatch.forEach(({
1467
+ start,
1468
+ end
1469
+ }) => {
1470
+ fragment.appendChild(context.document.createTextNode(row.title.slice(offset, start)));
1471
+ const match = createElement(context.document, 'span', 'ok-native-list-info', row.title.slice(start, end));
1472
+ fragment.appendChild(match);
1473
+ offset = end;
1474
+ });
1475
+ fragment.appendChild(context.document.createTextNode(row.title.slice(offset)));
1476
+ titleElement.prepend(fragment);
1477
+ }
1478
+ if (row.subtitleSegments?.length) {
1479
+ column.querySelector('.ok-native-list-secondary')?.remove();
1480
+ const segments = createElement(context.document, 'span', 'ok-native-list-subtitle-segments');
1481
+ row.subtitleSegments.forEach(segment => {
1482
+ if (segment.separatorBefore) segments.appendChild(createElement(context.document, 'span', 'ok-native-list-subtitle-dot'));
1483
+ const text = createElement(context.document, 'span', 'ok-native-list-secondary', segment.text);
1484
+ applyValueSegments(text, segment.textSegments, 14, 20, 400);
1485
+ setData(text, 'tone', segment.tone);
1486
+ text.style.color = segment.tone === 'disabled' ? 'var(--nl-disabled)' : segment.tone === 'caution' ? 'var(--nl-caution)' : toneColor(segment.tone, 'secondary');
1487
+ segments.appendChild(text);
1488
+ });
1489
+ column.insertBefore(segments, titleElement.nextSibling);
1490
+ }
1491
+ if (presentation === 'walletSidebar' && row.badges?.length) {
1492
+ const badges = createElement(context.document, 'span', 'ok-native-list-wallet-badges');
1493
+ row.badges.forEach(badge => badges.appendChild(createBadge(context, badge)));
1494
+ column.appendChild(badges);
1495
+ }
1496
+ }
870
1497
  if (row.type === 'activity' && row.status) column.appendChild(createElement(context.document, 'span', 'ok-native-list-secondary', row.status));
871
1498
  if (row.type === 'activity' && row.footerActions?.length) {
872
1499
  const actions = createElement(context.document, 'span', 'ok-native-list-actions');
@@ -898,14 +1525,32 @@ function createIdentityActivityOrMessageRow(context, row) {
898
1525
  }
899
1526
  return body;
900
1527
  }
1528
+
1529
+ // OneKey patch: SizableText enables tabular digits without replacing its font family.
1530
+ function applySelectorTabularNumbers(body, row) {
1531
+ if (!('presentation' in row) || !['accountSelector', 'networkSelector', 'walletSidebar'].includes(row.presentation ?? '')) return;
1532
+ body.style.fontVariantNumeric = 'tabular-nums';
1533
+ body.querySelectorAll('span,button').forEach(text => {
1534
+ text.style.fontVariantNumeric = 'tabular-nums';
1535
+ });
1536
+ }
901
1537
  function createWalletGroupRow(context, row) {
902
1538
  const body = createElement(context.document, 'div', 'ok-native-list-wallet-group');
903
1539
  [row.parent, ...row.children].forEach((member, memberIndex) => {
904
1540
  const memberElement = createElement(context.document, 'div', 'ok-native-list-wallet-member');
905
1541
  setData(memberElement, 'nativeListGroupMemberKey', member.key);
1542
+ setData(memberElement, 'testid', member.testID);
906
1543
  setData(memberElement, 'nativeListGroupParent', memberIndex === 0);
907
1544
  setData(memberElement, 'nativeListSelected', member.selected);
908
- memberElement.appendChild(createIdentityActivityOrMessageRow(context, member));
1545
+ // OneKey patch: grouped members have the same selector typography as standalone wallets.
1546
+ // memberElement.appendChild(createIdentityActivityOrMessageRow(context, member));
1547
+ const memberBody = createIdentityActivityOrMessageRow(context, member);
1548
+ applySelectorTabularNumbers(memberBody, member);
1549
+ memberElement.appendChild(memberBody);
1550
+ // OneKey patch: group children use their own measured badge height.
1551
+ memberElement.style.flexBasis = String(member.height ?? 68 + (member.badges?.length ? 24 : 0)) + 'px';
1552
+ memberElement.style.height = memberElement.style.flexBasis;
1553
+ memberElement.style.opacity = String(member.opacity ?? 1);
909
1554
  body.appendChild(memberElement);
910
1555
  });
911
1556
  return body;
@@ -945,6 +1590,11 @@ export class NativeListWebEngine {
945
1590
  };
946
1591
  mounted = new Map();
947
1592
  pool = [];
1593
+ // OneKey patch: URI leases outlive DOM overscan only inside this bounded window.
1594
+ avatarLeases = new Map();
1595
+ avatarOffset = 0;
1596
+ avatarDirection = 1;
1597
+ sectionIndexEntries = [];
948
1598
  suppressClickUntil = 0;
949
1599
  pullDistance = 0;
950
1600
  actionAnchorInstanceId = String(++webNativeListInstanceCounter);
@@ -953,6 +1603,8 @@ export class NativeListWebEngine {
953
1603
  lastViewportWidth = -1;
954
1604
  lastViewportHeight = -1;
955
1605
  destroyed = false;
1606
+ // OneKey patch: warning banners are measured after normal browser text wrapping.
1607
+ measuredWarningHeights = new Map();
956
1608
  constructor(host, snapshot, callbacks, virtualizationEnabled = true) {
957
1609
  this.document = host.ownerDocument;
958
1610
  this.snapshot = validateSnapshot(snapshot);
@@ -991,6 +1643,9 @@ export class NativeListWebEngine {
991
1643
  passive: true
992
1644
  });
993
1645
  this.root.addEventListener('click', this.handleClick);
1646
+ // OneKey patch: preserve web tooltip hover for section titles.
1647
+ this.root.addEventListener('pointerover', this.handleTitlePointerOver);
1648
+ this.root.addEventListener('pointerout', this.handleTitlePointerOut);
994
1649
  this.root.addEventListener('keydown', this.handleKeyDown);
995
1650
  this.viewport.addEventListener('pointerdown', this.handleReorderPointerDown, {
996
1651
  passive: false
@@ -1125,7 +1780,7 @@ export class NativeListWebEngine {
1125
1780
  scrollToLocation(params, scroll) {
1126
1781
  const index = resolveLocationIndex(this.snapshot.rows, params);
1127
1782
  if (index === undefined) {
1128
- const sectionCount = this.snapshot.rows.filter(row => row.type === 'sectionHeader' && row.variant !== 'summary').length;
1783
+ const sectionCount = this.snapshot.rows.filter(row => row.type === 'sectionHeader' && row.sticky !== false && row.variant !== 'summary').length;
1129
1784
  this.emitScrollFailure(params.itemIndex, params.sectionIndex >= sectionCount ? 'section-out-of-range' : 'item-out-of-range');
1130
1785
  return;
1131
1786
  }
@@ -1172,6 +1827,8 @@ export class NativeListWebEngine {
1172
1827
  this.document.defaultView?.removeEventListener('resize', this.handleWindowResize);
1173
1828
  this.viewport.removeEventListener('scroll', this.handleScroll);
1174
1829
  this.root.removeEventListener('click', this.handleClick);
1830
+ this.root.removeEventListener('pointerover', this.handleTitlePointerOver);
1831
+ this.root.removeEventListener('pointerout', this.handleTitlePointerOut);
1175
1832
  this.root.removeEventListener('keydown', this.handleKeyDown);
1176
1833
  this.cancelPointerReorder(true);
1177
1834
  this.viewport.removeEventListener('pointerdown', this.handleReorderPointerDown);
@@ -1189,19 +1846,23 @@ export class NativeListWebEngine {
1189
1846
  this.viewport.removeEventListener('pointerup', this.handlePullEnd);
1190
1847
  this.viewport.removeEventListener('pointercancel', this.handlePullEnd);
1191
1848
  const host = this.root.parentElement;
1849
+ disposeWebImageRetries(this.root);
1850
+ this.pool.forEach(disposeWebImageRetries);
1192
1851
  this.root.remove();
1193
1852
  this.hideReorderPreview();
1194
1853
  this.reorderPreview.remove();
1195
1854
  if (host) host.style.position = this.previousHostPosition;
1196
1855
  this.mounted.clear();
1197
1856
  this.pool.length = 0;
1857
+ this.avatarLeases.forEach(release => release());
1858
+ this.avatarLeases.clear();
1198
1859
  }
1199
1860
  setSnapshot(snapshot, selectedKeys) {
1861
+ this.measuredWarningHeights.clear();
1200
1862
  this.snapshot = snapshot;
1201
1863
  this.rows = effectiveRows(snapshot);
1202
1864
  this.selectedKeys = selectedKeys ?? selectionStateFromSnapshot(snapshot).selectedKeys;
1203
1865
  if (this.reachedGeneration !== snapshot.generation) this.reachedGeneration = undefined;
1204
- this.renderSectionIndex();
1205
1866
  this.applyTheme();
1206
1867
  this.recomputeLayout();
1207
1868
  this.renderFooter();
@@ -1219,6 +1880,11 @@ export class NativeListWebEngine {
1219
1880
  '--nl-primary': theme.primaryText,
1220
1881
  '--nl-secondary': theme.secondaryText,
1221
1882
  '--nl-disabled': theme.disabledText,
1883
+ '--nl-caution': theme.caution ?? '#AB6400',
1884
+ '--nl-caution-background': theme.cautionBackground,
1885
+ '--nl-checkbox-background': theme.checkboxBackground,
1886
+ '--nl-checkbox-border': theme.checkboxBorder,
1887
+ '--nl-checkbox-icon': theme.checkboxIcon,
1222
1888
  '--nl-icon': theme.icon,
1223
1889
  '--nl-icon-subdued': theme.iconSubdued,
1224
1890
  '--nl-separator': theme.separator,
@@ -1242,13 +1908,22 @@ export class NativeListWebEngine {
1242
1908
  const viewportHeight = this.viewport.clientHeight;
1243
1909
  if (this.lastViewportWidth >= 0 && (viewportWidth !== this.lastViewportWidth || viewportHeight !== this.lastViewportHeight)) {
1244
1910
  this.invalidateActionAnchor('layout');
1911
+ this.measuredWarningHeights.clear();
1245
1912
  }
1246
1913
  this.lastViewportWidth = viewportWidth;
1247
1914
  this.lastViewportHeight = viewportHeight;
1248
1915
  const previousHorizontal = this.layout.horizontal;
1249
- this.layout = computeWebListLayout(this.snapshot, viewportWidth, viewportHeight, this.reorderCompactKey);
1916
+ const measuredSnapshot = {
1917
+ ...this.snapshot,
1918
+ rows: this.snapshot.rows.map(row => row.type === 'system' && row.variant === 'warning' && row.height === undefined && this.measuredWarningHeights.has(row.key) ? {
1919
+ ...row,
1920
+ height: this.measuredWarningHeights.get(row.key)
1921
+ } : row)
1922
+ };
1923
+ this.layout = computeWebListLayout(measuredSnapshot, viewportWidth, viewportHeight, this.reorderCompactKey);
1250
1924
  this.content.style.width = String(this.layout.contentWidth) + 'px';
1251
1925
  this.content.style.height = String(this.layout.contentHeight) + 'px';
1926
+ this.renderSectionIndex(viewportHeight || DEFAULT_VIEWPORT_HEIGHT);
1252
1927
  if (previousHorizontal !== this.layout.horizontal) {
1253
1928
  this.viewport.scrollLeft = 0;
1254
1929
  this.viewport.scrollTop = 0;
@@ -1256,7 +1931,44 @@ export class NativeListWebEngine {
1256
1931
  this.renderWindow();
1257
1932
  this.performPendingScroll();
1258
1933
  };
1934
+ updateAvatarWindow() {
1935
+ const offset = this.currentOffset();
1936
+ if (offset !== this.avatarOffset) this.avatarDirection = Math.sign(offset - this.avatarOffset);
1937
+ this.avatarOffset = offset;
1938
+ const visible = visibleWebLayoutItems(this.layout, offset, this.viewportLength(), 0);
1939
+ const first = visible[0]?.index ?? -1;
1940
+ const last = visible[visible.length - 1]?.index ?? -1;
1941
+ const candidates = avatarPrefetchWindow(this.rows, first, last, this.avatarDirection);
1942
+ const desired = new Set(candidates.map(({
1943
+ source
1944
+ }) => canonicalNativeListAvatarUri(source.uri)).filter(uri => uri !== undefined));
1945
+ this.avatarLeases.forEach((release, uri) => {
1946
+ if (!desired.has(uri)) {
1947
+ release();
1948
+ this.avatarLeases.delete(uri);
1949
+ }
1950
+ });
1951
+ candidates.forEach(({
1952
+ source,
1953
+ priority
1954
+ }) => {
1955
+ const uri = canonicalNativeListAvatarUri(source.uri);
1956
+ if (!uri) return;
1957
+ const existing = this.avatarLeases.get(uri);
1958
+ if (existing) existing.setPriority(priority);else {
1959
+ let lease;
1960
+ lease = acquireNativeListAvatar(this.document, uri, () => {}, () => {
1961
+ if (this.avatarLeases.get(uri) === lease) {
1962
+ lease?.();
1963
+ this.avatarLeases.delete(uri);
1964
+ }
1965
+ }, priority);
1966
+ this.avatarLeases.set(uri, lease);
1967
+ }
1968
+ });
1969
+ }
1259
1970
  renderWindow() {
1971
+ this.updateAvatarWindow();
1260
1972
  const viewportLength = this.viewportLength();
1261
1973
  const visible = webLayoutItemsForMount(this.layout, this.currentOffset(), viewportLength, this.virtualizationEnabled, viewportLength * OVERSCAN_VIEWPORTS);
1262
1974
  const desired = new Set(visible.map(item => item.index));
@@ -1264,6 +1976,7 @@ export class NativeListWebEngine {
1264
1976
  if (!desired.has(index)) {
1265
1977
  this.invalidateActionAnchorForElement(element);
1266
1978
  this.mounted.delete(index);
1979
+ if (disposeWebImageRetries(element)) element.removeAttribute('data-render-signature');
1267
1980
  element.remove();
1268
1981
  this.pool.push(element);
1269
1982
  }
@@ -1285,6 +1998,20 @@ export class NativeListWebEngine {
1285
1998
  element.dataset.renderSignature = signature;
1286
1999
  }
1287
2000
  });
2001
+ let measuredWarningChanged = false;
2002
+ this.mounted.forEach((element, index) => {
2003
+ const row = this.rows[index];
2004
+ if (row?.type !== 'system' || row.variant !== 'warning' || row.height !== undefined) return;
2005
+ const height = element.querySelector('.ok-native-list-warning')?.offsetHeight ?? 0;
2006
+ if (height > 0 && height !== this.measuredWarningHeights.get(row.key)) {
2007
+ this.measuredWarningHeights.set(row.key, height);
2008
+ measuredWarningChanged = true;
2009
+ }
2010
+ });
2011
+ if (measuredWarningChanged) {
2012
+ this.recomputeLayout();
2013
+ return;
2014
+ }
1288
2015
  this.updateVisibleSelection();
1289
2016
  this.updateVisibleState();
1290
2017
  }
@@ -1295,12 +2022,16 @@ export class NativeListWebEngine {
1295
2022
  }
1296
2023
  renderElement(element, index, row, overlay = false) {
1297
2024
  this.invalidateActionAnchorForElement(element);
2025
+ disposeWebImageRetries(element);
1298
2026
  const bindingEpoch = String(++this.bindingEpochCounter);
1299
2027
  element.className = overlay ? 'ok-native-list-item ok-native-list-sticky' : 'ok-native-list-item';
1300
2028
  setData(element, 'nativeListRowKey', row.key);
1301
2029
  setData(element, 'nativeListBindingEpoch', bindingEpoch);
1302
2030
  setData(element, 'nativeListRowIndex', index);
1303
2031
  setData(element, 'nativeListDisabled', Boolean(row.disabled));
2032
+ setData(element, 'testid', row.testID);
2033
+ // OneKey patch: deprecation dims a row without disabling its actions.
2034
+ element.style.opacity = String(row.opacity ?? 1);
1304
2035
  setData(element, 'nativeListReorderable', this.isReorderable(row));
1305
2036
  setData(element, 'nativeListDragging', this.pointerReorder?.active && this.pointerReorder.sourceKey === row.key);
1306
2037
  setData(element, 'separator', row.separator);
@@ -1320,11 +2051,67 @@ export class NativeListWebEngine {
1320
2051
  selectedKeys: this.selectedKeys,
1321
2052
  itemIndex: index
1322
2053
  };
1323
- element.replaceChildren(createRowBody(context, row));
2054
+ const body = createRowBody(context, row);
2055
+ applySelectorTabularNumbers(body, row);
2056
+ // OneKey patch: explicit selector fields preserve original page geometry.
2057
+ element.style.contain = row.backgroundFullWidth ? 'layout style' : '';
2058
+ if (row.backgroundColor) body.style.backgroundColor = row.backgroundColor;
2059
+ if (row.backgroundFullWidth && row.backgroundColor) {
2060
+ const bleed = paddingValues(this.snapshot).horizontal;
2061
+ body.style.position = 'relative';
2062
+ body.style.overflow = 'visible';
2063
+ body.style.boxShadow = String(-bleed) + 'px 0 ' + row.backgroundColor + ',' + String(bleed) + 'px 0 ' + row.backgroundColor;
2064
+ }
2065
+ if (row.type === 'identity' && row.height !== undefined) {
2066
+ const title = body.querySelector('.ok-native-list-title');
2067
+ if (row.presentation === 'accountSelector') {
2068
+ body.style.gap = '12px';
2069
+ body.style.borderRadius = '12px';
2070
+ if ('shape' in row.leading && row.leading.shape === 'rounded') {
2071
+ const visual = body.querySelector('.ok-native-list-visual');
2072
+ if (visual) visual.style.borderRadius = '8px';
2073
+ }
2074
+ if (title) title.style.lineHeight = '24px';
2075
+ }
2076
+ if (row.presentation === 'networkSelector') {
2077
+ body.style.borderRadius = '12px';
2078
+ const visual = body.querySelector('.ok-native-list-visual');
2079
+ if (visual) {
2080
+ visual.style.width = '32px';
2081
+ visual.style.height = '32px';
2082
+ visual.style.flexBasis = '32px';
2083
+ }
2084
+ if (row.leading.kind === 'network' && !row.leading.image && !row.leading.fallbackIcon && row.leading.fallbackText) {
2085
+ const fallback = visual?.querySelector('.ok-native-list-visual-fallback');
2086
+ if (fallback) {
2087
+ fallback.style.fontSize = '19px';
2088
+ fallback.style.lineHeight = '27px';
2089
+ fallback.style.fontWeight = '600';
2090
+ fallback.style.color = 'var(--nl-inverse-text)';
2091
+ }
2092
+ }
2093
+ visual?.querySelectorAll('.ok-native-list-visual-main').forEach(image => {
2094
+ image.style.width = '32px';
2095
+ image.style.height = '32px';
2096
+ });
2097
+ if (title) {
2098
+ title.style.fontSize = '16px';
2099
+ title.style.lineHeight = '24px';
2100
+ title.style.fontWeight = '500';
2101
+ }
2102
+ body.querySelectorAll('.ok-native-list-accessory').forEach(value => {
2103
+ value.style.fontSize = '16px';
2104
+ value.style.lineHeight = '24px';
2105
+ value.style.fontWeight = '500';
2106
+ });
2107
+ }
2108
+ }
2109
+ element.replaceChildren(body);
1324
2110
  }
1325
2111
  renderFooter() {
1326
2112
  const row = this.snapshot.fixedFooter;
1327
2113
  this.invalidateActionAnchorForElement(this.footer);
2114
+ disposeWebImageRetries(this.footer);
1328
2115
  this.footer.replaceChildren();
1329
2116
  if (!row) return;
1330
2117
  const element = createElement(this.document, 'div', 'ok-native-list-item');
@@ -1335,20 +2122,52 @@ export class NativeListWebEngine {
1335
2122
  element.style.height = String(estimateWebRowHeight(row, this.snapshot, this.viewport.clientWidth || DEFAULT_VIEWPORT_WIDTH)) + 'px';
1336
2123
  this.footer.appendChild(element);
1337
2124
  }
1338
- renderSectionIndex() {
2125
+ sectionIndexVisibleEntryIndices(viewportHeight) {
2126
+ const entryCount = this.sectionIndexEntries.length;
2127
+ if (entryCount <= 1) return entryCount ? [0] : [];
2128
+ const availableHeight = Math.max(0, viewportHeight - SECTION_INDEX_EDGE_PADDING * 2);
2129
+ const maxVisible = Math.max(2, Math.floor(availableHeight / SECTION_INDEX_MIN_LABEL_SPACING) + 1);
2130
+ if (entryCount <= maxVisible) {
2131
+ return Array.from({
2132
+ length: entryCount
2133
+ }, (_, index) => index);
2134
+ }
2135
+ const result = new Set();
2136
+ for (let slot = 0; slot < maxVisible; slot += 1) {
2137
+ result.add(Math.round(slot * (entryCount - 1) / (maxVisible - 1)));
2138
+ }
2139
+ return [...result].sort((left, right) => left - right);
2140
+ }
2141
+ renderSectionIndex(viewportHeight) {
1339
2142
  this.indexRail.replaceChildren();
1340
2143
  if (!sectionIndexEnabled(this.snapshot)) {
2144
+ this.sectionIndexEntries = [];
2145
+ this.indexRail.hidden = true;
2146
+ return;
2147
+ }
2148
+ this.sectionIndexEntries = this.snapshot.rows.flatMap((row, position) => row.type === 'sectionHeader' && row.indexTitle ? [{
2149
+ key: row.key,
2150
+ title: row.indexTitle,
2151
+ position
2152
+ }] : []);
2153
+ if (this.sectionIndexEntries.length === 0 || viewportHeight < SECTION_INDEX_MIN_HEIGHT) {
1341
2154
  this.indexRail.hidden = true;
1342
2155
  return;
1343
2156
  }
2157
+ const visibleEntryIndices = this.sectionIndexVisibleEntryIndices(viewportHeight);
2158
+ setData(this.indexRail, 'compact', visibleEntryIndices.length < this.sectionIndexEntries.length);
1344
2159
  const fragment = this.document.createDocumentFragment();
1345
- this.snapshot.rows.forEach((row, index) => {
1346
- if (row.type !== 'sectionHeader' || !row.indexTitle) return;
1347
- const button = createElement(this.document, 'button', 'ok-native-list-index-button', row.indexTitle);
2160
+ visibleEntryIndices.forEach(entryIndex => {
2161
+ const entry = this.sectionIndexEntries[entryIndex];
2162
+ if (!entry) return;
2163
+ const button = createElement(this.document, 'button', 'ok-native-list-index-button', entry.title);
1348
2164
  button.setAttribute('type', 'button');
1349
- button.setAttribute('aria-label', 'Jump to ' + row.indexTitle);
1350
- setData(button, 'sectionPosition', index);
1351
- setData(button, 'sectionKey', row.key);
2165
+ button.setAttribute('aria-label', 'Jump to ' + entry.title);
2166
+ setData(button, 'sectionEntryIndex', entryIndex);
2167
+ setData(button, 'sectionPosition', entry.position);
2168
+ setData(button, 'sectionKey', entry.key);
2169
+ const progress = this.sectionIndexEntries.length === 1 ? 0.5 : entryIndex / (this.sectionIndexEntries.length - 1);
2170
+ button.style.top = String(SECTION_INDEX_EDGE_PADDING + progress * (viewportHeight - SECTION_INDEX_EDGE_PADDING * 2)) + 'px';
1352
2171
  fragment.appendChild(button);
1353
2172
  });
1354
2173
  this.indexRail.appendChild(fragment);
@@ -1357,7 +2176,9 @@ export class NativeListWebEngine {
1357
2176
  updateVisibleSelection() {
1358
2177
  const update = (element, row) => {
1359
2178
  if (!row) return;
1360
- const selected = this.selectedKeys.has(row.key);
2179
+ // OneKey patch: selector adapters mark active rows independently of checkbox selection.
2180
+ // const selected = this.selectedKeys.has(row.key);
2181
+ const selected = row.selected === true || this.selectedKeys.has(row.key);
1361
2182
  setData(element, 'nativeListSelected', selected);
1362
2183
  element.setAttribute('aria-selected', String(selected));
1363
2184
  element.querySelectorAll('.ok-native-list-checkbox').forEach(checkbox => {
@@ -1396,7 +2217,9 @@ export class NativeListWebEngine {
1396
2217
  }
1397
2218
  this.checkEndReached(last?.index ?? -1);
1398
2219
  this.updateStickyHeader(first?.index ?? -1);
1399
- this.updateSectionIndex(first?.index ?? -1);
2220
+ // OneKey patch: index highlighting follows header positions at scroll boundaries.
2221
+ // this.updateSectionIndex(first?.index ?? -1);
2222
+ this.updateSectionIndex();
1400
2223
  }
1401
2224
  updateStickyHeader(firstVisibleIndex) {
1402
2225
  if (!this.snapshot.layout.stickyHeaders || this.layout.horizontal || firstVisibleIndex < 0) {
@@ -1407,7 +2230,7 @@ export class NativeListWebEngine {
1407
2230
  let index = -1;
1408
2231
  for (let cursor = firstVisibleIndex; cursor >= 0; cursor -= 1) {
1409
2232
  const row = this.rows[cursor];
1410
- if (row?.type === 'sectionHeader' && row.variant !== 'summary') {
2233
+ if (row?.type === 'sectionHeader' && row.sticky !== false && row.variant !== 'summary') {
1411
2234
  index = cursor;
1412
2235
  break;
1413
2236
  }
@@ -1433,7 +2256,7 @@ export class NativeListWebEngine {
1433
2256
  let nextIndex = -1;
1434
2257
  for (let cursor = index + 1; cursor < this.rows.length; cursor += 1) {
1435
2258
  const candidate = this.rows[cursor];
1436
- if (candidate?.type === 'sectionHeader' && candidate.variant !== 'summary') {
2259
+ if (candidate?.type === 'sectionHeader' && candidate.sticky !== false && candidate.variant !== 'summary') {
1437
2260
  nextIndex = cursor;
1438
2261
  break;
1439
2262
  }
@@ -1443,10 +2266,15 @@ export class NativeListWebEngine {
1443
2266
  this.sticky.style.transform = 'translate3d(0,' + String(translate) + 'px,0)';
1444
2267
  this.updateVisibleSelection();
1445
2268
  }
1446
- updateSectionIndex(firstVisibleIndex) {
2269
+
2270
+ // private updateSectionIndex(firstVisibleIndex: number) {
2271
+ updateSectionIndex() {
1447
2272
  let activeKey;
1448
2273
  this.snapshot.rows.forEach((row, index) => {
1449
- if (index <= firstVisibleIndex && row.type === 'sectionHeader' && row.indexTitle) activeKey = row.key;
2274
+ if (
2275
+ // OneKey patch: a spacer ending exactly at the viewport is not the active section.
2276
+ // index <= firstVisibleIndex &&
2277
+ itemStart(this.layout.items[index], this.layout.horizontal) <= this.currentOffset() && row.type === 'sectionHeader' && row.sticky !== false && row.indexTitle) activeKey = row.key;
1450
2278
  });
1451
2279
  this.indexRail.querySelectorAll('[data-section-key]').forEach(button => setData(button, 'active', button.dataset.sectionKey === activeKey));
1452
2280
  }
@@ -1539,7 +2367,14 @@ export class NativeListWebEngine {
1539
2367
  const bindingEpoch = rowElement.dataset.nativeListBindingEpoch;
1540
2368
  if (!source || !bindingEpoch || !rowElement.contains(actionElement)) return undefined;
1541
2369
  this.invalidateActionAnchor('rebind');
1542
- const rect = actionElement.getBoundingClientRect();
2370
+ const actualRect = actionElement.getBoundingClientRect();
2371
+ const inset = Number(actionElement.dataset.nativeListAnchorInset ?? 0);
2372
+ const rect = {
2373
+ left: actualRect.left + inset,
2374
+ top: actualRect.top + inset,
2375
+ width: actualRect.width - inset * 2,
2376
+ height: actualRect.height - inset * 2
2377
+ };
1543
2378
  const token = [this.actionAnchorInstanceId, this.snapshot.generation, ++this.actionAnchorCounter, bindingEpoch].join(':');
1544
2379
  const slotValue = actionElement.dataset.nativeListAnchorSlot;
1545
2380
  const direction = this.document.defaultView?.getComputedStyle(actionElement).direction === 'rtl' || actionElement.closest('[dir="rtl"]') ? 'rtl' : 'ltr';
@@ -1577,7 +2412,9 @@ export class NativeListWebEngine {
1577
2412
  });
1578
2413
  }
1579
2414
  handleRowPress(row, rowElement, sourceElement = rowElement) {
1580
- if (row.disabled) return;
2415
+ // OneKey patch: missing-address rows keep their create-address accessory interactive.
2416
+ // if (row.disabled) return;
2417
+ if (row.disabled || row.pressDisabled) return;
1581
2418
  if (this.snapshot.selection?.rowPressToggles && this.snapshot.selection.mode !== 'none' && isSelectableRow(row)) {
1582
2419
  this.activateSelection({
1583
2420
  scope: 'row'
@@ -1597,6 +2434,27 @@ export class NativeListWebEngine {
1597
2434
  this.emitRowAction(row, actionKey);
1598
2435
  }
1599
2436
  }
2437
+
2438
+ // OneKey patch: hover opens the same anchored help action as native taps.
2439
+ handleTitlePointerOver = event => {
2440
+ const target = event.target;
2441
+ if (!(target instanceof Element)) return;
2442
+ const action = target.closest('[data-native-list-hover-action]');
2443
+ if (!action || event.relatedTarget instanceof Node && action.contains(event.relatedTarget)) return;
2444
+ const rowElement = action.closest('[data-native-list-row-key]');
2445
+ const row = this.rowAtElement(rowElement);
2446
+ const memberKey = action.closest('[data-native-list-group-member-key]')?.dataset.nativeListGroupMemberKey;
2447
+ const sourceRow = row?.type === 'walletGroup' ? [row.parent, ...row.children].find(member => member.key === memberKey) ?? row : row;
2448
+ const actionKey = action.dataset.nativeListHoverAction === 'true' ? action.dataset.nativeListAction : action.dataset.nativeListHoverAction;
2449
+ if (sourceRow && !sourceRow.disabled && actionKey) this.emitRowAction(sourceRow, actionKey, action, rowElement ?? undefined);
2450
+ };
2451
+ handleTitlePointerOut = event => {
2452
+ const target = event.target;
2453
+ if (!(target instanceof Element)) return;
2454
+ const action = target.closest('[data-native-list-hover-action]');
2455
+ if (!action || event.relatedTarget instanceof Node && action.contains(event.relatedTarget)) return;
2456
+ if (this.actionAnchor?.actionElement === action) this.invalidateActionAnchor('pointerLeave');
2457
+ };
1600
2458
  handleClick = event => {
1601
2459
  if (Date.now() < this.suppressClickUntil) {
1602
2460
  event.preventDefault();
@@ -1627,6 +2485,12 @@ export class NativeListWebEngine {
1627
2485
  this.handleRowPress(sourceRow, rowElement ?? undefined, memberElement ?? rowElement ?? undefined);
1628
2486
  };
1629
2487
  handleKeyDown = event => {
2488
+ // OneKey patch: non-button title help supports keyboard activation.
2489
+ if ((event.key === 'Enter' || event.key === ' ') && event.target instanceof HTMLElement && event.target.matches('[role="button"][data-native-list-action]')) {
2490
+ event.preventDefault();
2491
+ event.target.click();
2492
+ return;
2493
+ }
1630
2494
  if (event.key === 'Escape') {
1631
2495
  if (this.pointerReorder?.active) {
1632
2496
  event.preventDefault();
@@ -1729,7 +2593,10 @@ export class NativeListWebEngine {
1729
2593
  if (this.frameHandle !== undefined || this.destroyed) return;
1730
2594
  this.frameHandle = this.requestFrame(() => {
1731
2595
  this.frameHandle = undefined;
1732
- if (this.virtualizationEnabled) this.renderWindow();else this.updateVisibleState();
2596
+ if (this.virtualizationEnabled) this.renderWindow();else {
2597
+ this.updateAvatarWindow();
2598
+ this.updateVisibleState();
2599
+ }
1733
2600
  });
1734
2601
  }
1735
2602
  requestFrame(callback) {
@@ -1740,13 +2607,20 @@ export class NativeListWebEngine {
1740
2607
  const view = this.document.defaultView;
1741
2608
  if (view?.cancelAnimationFrame) view.cancelAnimationFrame(handle);else view?.clearTimeout(handle);
1742
2609
  }
1743
- selectIndexPosition(position, title) {
2610
+ selectIndexPosition(position, title, previewClientY) {
1744
2611
  this.scrollToIndex(position, {
1745
2612
  animated: false,
1746
2613
  alignment: 'start',
1747
2614
  viewPosition: 0,
1748
2615
  viewOffset: 0
1749
2616
  });
2617
+ const frame = this.viewportFrame.getBoundingClientRect();
2618
+ if (previewClientY !== undefined && frame.height > 0) {
2619
+ const previewY = Math.min(frame.height - 24, Math.max(24, previewClientY - frame.top));
2620
+ this.indexPreview.style.top = String(previewY) + 'px';
2621
+ } else {
2622
+ this.indexPreview.style.top = '50%';
2623
+ }
1750
2624
  this.indexPreview.textContent = title;
1751
2625
  setData(this.indexPreview, 'visible', true);
1752
2626
  if (this.previewTimer !== undefined) this.document.defaultView?.clearTimeout(this.previewTimer);
@@ -1754,24 +2628,40 @@ export class NativeListWebEngine {
1754
2628
  setData(this.indexPreview, 'visible', false);
1755
2629
  }, 180);
1756
2630
  }
1757
- indexButtonAtEvent(event) {
1758
- const direct = event.target?.closest('[data-section-position]');
1759
- if (direct) return direct;
1760
- return this.document.elementFromPoint(event.clientX, event.clientY)?.closest('[data-section-position]') ?? undefined;
2631
+ sectionIndexEntryAtEvent(event) {
2632
+ const rail = this.indexRail.getBoundingClientRect();
2633
+ if (this.sectionIndexEntries.length === 0 || rail.height <= 0) {
2634
+ return undefined;
2635
+ }
2636
+ const availableHeight = Math.max(1, rail.height - SECTION_INDEX_EDGE_PADDING * 2);
2637
+ const progress = Math.min(1, Math.max(0, (event.clientY - rail.top - SECTION_INDEX_EDGE_PADDING) / availableHeight));
2638
+ const entryIndex = Math.round(progress * (this.sectionIndexEntries.length - 1));
2639
+ const entry = this.sectionIndexEntries[entryIndex];
2640
+ return entry ? {
2641
+ entry,
2642
+ previewClientY: event.clientY
2643
+ } : undefined;
1761
2644
  }
1762
2645
  handleIndexPointer = event => {
1763
2646
  if (event.type === 'pointermove' && event.buttons === 0) return;
1764
- const button = this.indexButtonAtEvent(event);
1765
- if (!button) return;
2647
+ const selection = this.sectionIndexEntryAtEvent(event);
2648
+ if (!selection) return;
1766
2649
  event.preventDefault();
1767
- this.selectIndexPosition(Number(button.dataset.sectionPosition), button.textContent ?? '');
2650
+ if (event.type === 'pointerdown') {
2651
+ this.indexRail.setPointerCapture?.(event.pointerId);
2652
+ }
2653
+ this.selectIndexPosition(selection.entry.position, selection.entry.title, selection.previewClientY);
1768
2654
  };
1769
2655
  handleIndexClick = event => {
2656
+ if ('detail' in event && typeof event.detail === 'number' && event.detail > 0) return;
1770
2657
  const target = event.target;
1771
2658
  if (!(target instanceof Element)) return;
1772
- const button = target.closest('[data-section-position]');
2659
+ const button = target.closest('[data-section-entry-index]');
1773
2660
  if (!button) return;
1774
- this.selectIndexPosition(Number(button.dataset.sectionPosition), button.textContent ?? '');
2661
+ const entry = this.sectionIndexEntries[Number(button.dataset.sectionEntryIndex)];
2662
+ if (!entry) return;
2663
+ const rect = button.getBoundingClientRect();
2664
+ this.selectIndexPosition(entry.position, entry.title, rect.top + rect.height / 2);
1775
2665
  };
1776
2666
  handlePullStart = event => {
1777
2667
  if (this.pointerReorder?.pointerId === event.pointerId || !this.snapshot.capabilities?.pullToRefresh || this.viewport.scrollTop > 0 || event.pointerType !== 'touch' && event.pointerType !== 'pen') return;
@@ -1816,7 +2706,14 @@ export class NativeListWebEngine {
1816
2706
  const index = Number(rowElement?.dataset.nativeListRowIndex);
1817
2707
  const row = this.rows[index];
1818
2708
  if (!row || !this.isReorderable(row)) return;
1819
- if (row.type === 'walletGroup' && target.closest('[data-native-list-group-parent]')?.dataset.nativeListGroupParent !== 'true') return;
2709
+ // OneKey patch: a child drag reorders its parent wallet group as one item.
2710
+ // if (
2711
+ // row.type === 'walletGroup' &&
2712
+ // target.closest<HTMLElement>('[data-native-list-group-parent]')?.dataset
2713
+ // .nativeListGroupParent !== 'true'
2714
+ // )
2715
+ // return;
2716
+
1820
2717
  const view = this.document.defaultView;
1821
2718
  const state = {
1822
2719
  pointerId: event.pointerId,
@@ -1832,9 +2729,8 @@ export class NativeListWebEngine {
1832
2729
  active: false
1833
2730
  };
1834
2731
  this.pointerReorder = state;
1835
- if (state.pointerType === 'mouse') {
1836
- this.captureReorderPointer(state);
1837
- } else {
2732
+ // OneKey patch: normal wallet taps retain their target until a drag is activated.
2733
+ if (state.pointerType !== 'mouse') {
1838
2734
  state.longPressTimer = view?.setTimeout(() => this.activatePointerReorder(state), REORDER_TOUCH_LONG_PRESS_MS);
1839
2735
  }
1840
2736
  };
@@ -1935,6 +2831,20 @@ export class NativeListWebEngine {
1935
2831
  state.previewOffsetX = state.startX - rect.left;
1936
2832
  state.previewOffsetY = Math.min(previewHeight, Math.max(0, state.startY - rect.top));
1937
2833
  this.reorderPreview.replaceChildren(previewRow.cloneNode(true));
2834
+ // Cloned previews need their own lease when a source row is recycled during dragging.
2835
+ const originals = previewRow.querySelectorAll('img');
2836
+ this.reorderPreview.querySelectorAll('img').forEach((image, index) => {
2837
+ const original = originals.item(index);
2838
+ const avatar = original ? webAvatarSources.get(original) : undefined;
2839
+ if (!avatar) return;
2840
+ const paint = image.previousElementSibling;
2841
+ if (paint?.classList.contains('ok-native-list-selector-image-background')) {
2842
+ image.addEventListener('load', () => {
2843
+ paint.style.backgroundImage = 'url(' + JSON.stringify(image.currentSrc || image.src) + ')';
2844
+ });
2845
+ }
2846
+ configureWebAvatar(image, avatar.source, avatar.uri);
2847
+ });
1938
2848
  const sourceRow = state.workingRows[state.currentIndex];
1939
2849
  const badgeText = sourceRow ? webWalletGroupReorderBadge(sourceRow) : undefined;
1940
2850
  if (badgeText) {
@@ -1970,6 +2880,7 @@ export class NativeListWebEngine {
1970
2880
  }
1971
2881
  clearReorderPreviewVisual() {
1972
2882
  this.reorderPreview.hidden = true;
2883
+ disposeWebImageRetries(this.reorderPreview);
1973
2884
  this.reorderPreview.replaceChildren();
1974
2885
  this.reorderPreview.style.removeProperty('transform');
1975
2886
  this.reorderPreview.style.removeProperty('transition');