@onekeyfe/react-native-native-list 3.0.152 → 3.0.154-alpha.246

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.
@@ -27,6 +27,7 @@ function parsePayload(payloadJson) {
27
27
  export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
28
28
  snapshot,
29
29
  webVirtualizationEnabled: _webVirtualizationEnabled,
30
+ webSectionIndexContainerRef: _webSectionIndexContainerRef,
30
31
  keyboardDismissMode = 'none',
31
32
  keyboardShouldPersistTaps = 'never',
32
33
  onRowAction,
@@ -23,6 +23,7 @@ export function preloadNativeListAvatarImages(sources) {
23
23
  export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
24
24
  snapshot,
25
25
  webVirtualizationEnabled = true,
26
+ webSectionIndexContainerRef,
26
27
  keyboardDismissMode: _keyboardDismissMode,
27
28
  keyboardShouldPersistTaps: _keyboardShouldPersistTaps,
28
29
  onRowAction,
@@ -81,7 +82,7 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
81
82
  const didApplyInitialScroll = useRef(false);
82
83
  useEffect(() => {
83
84
  if (!host) return;
84
- const engine = new NativeListWebEngine(host, snapshotRef.current, callbacksRef.current, webVirtualizationEnabledRef.current);
85
+ const engine = new NativeListWebEngine(host, snapshotRef.current, callbacksRef.current, webVirtualizationEnabledRef.current, webSectionIndexContainerRef?.current);
85
86
  engineRef.current = engine;
86
87
  appliedSnapshotRef.current = snapshotRef.current;
87
88
  const pending = pendingPatchesRef.current;
@@ -96,7 +97,7 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
96
97
  engine.destroy();
97
98
  if (engineRef.current === engine) engineRef.current = undefined;
98
99
  };
99
- }, [host]);
100
+ }, [host, webSectionIndexContainerRef]);
100
101
  useEffect(() => {
101
102
  engineRef.current?.setVirtualizationEnabled(webVirtualizationEnabled);
102
103
  }, [webVirtualizationEnabled]);
@@ -8,9 +8,14 @@ import { acquireNativeListAvatar, canonicalNativeListAvatarUri } from "./NativeL
8
8
  const SECTION_INDEX_CONTENT_INSET = 16;
9
9
  const SECTION_INDEX_RAIL_WIDTH = 32;
10
10
  const SECTION_INDEX_EDGE_PADDING = 8;
11
- const SECTION_INDEX_LABEL_SPACING = 16;
11
+ const SECTION_INDEX_MIN_LABEL_SPACING = 16;
12
+ const SECTION_INDEX_CONTAINER_LABEL_SPACING = 22;
12
13
  const SECTION_INDEX_MIN_HEIGHT = 120;
13
14
  const SECTION_INDEX_WINDOW_Z_INDEX = 100_000;
15
+ const SECTION_INDEX_TRANSFORM_MIN_TRACKING_FRAMES = 12;
16
+ const SECTION_INDEX_TRANSFORM_MAX_TRACKING_FRAMES = 90;
17
+ const SECTION_INDEX_TRANSFORM_SETTLED_FRAMES = 4;
18
+ const SECTION_INDEX_TRANSFORM_SETTLED_EPSILON = 0.0001;
14
19
  const DEFAULT_VIEWPORT_WIDTH = 320;
15
20
  const DEFAULT_VIEWPORT_HEIGHT = 640;
16
21
  const OVERSCAN_VIEWPORTS = 1;
@@ -54,6 +59,46 @@ const defaultTheme = {
54
59
  inverseText: '#FCFCFC',
55
60
  info: '#0D74CE'
56
61
  };
62
+ export function webSectionIndexMetrics(entryCount, viewportHeight, alignStart = false) {
63
+ const availableHeight = Math.max(0, viewportHeight - SECTION_INDEX_EDGE_PADDING * 2);
64
+ const labelSpacing = alignStart ? SECTION_INDEX_CONTAINER_LABEL_SPACING : SECTION_INDEX_MIN_LABEL_SPACING;
65
+ const trackHeight = Math.min(availableHeight, labelSpacing * entryCount);
66
+ return {
67
+ originY: alignStart ? SECTION_INDEX_EDGE_PADDING : (viewportHeight - trackHeight) / 2,
68
+ trackHeight
69
+ };
70
+ }
71
+ export function webSectionIndexContainerLayout(entryCount, containerHeight) {
72
+ const height = Math.min(Math.max(0, containerHeight), SECTION_INDEX_EDGE_PADDING * 2 + SECTION_INDEX_CONTAINER_LABEL_SPACING * entryCount);
73
+ return {
74
+ top: Math.max(0, (containerHeight - height) / 2),
75
+ height
76
+ };
77
+ }
78
+ export function webSectionIndexInverseScale(layoutHeight, renderedHeight) {
79
+ if (layoutHeight <= 0 || renderedHeight <= 0) return 1;
80
+ return layoutHeight / renderedHeight;
81
+ }
82
+ export function webSectionIndexActiveKey(rows, layout, offset, viewportLength) {
83
+ const indexedRows = rows.flatMap((row, index) => row.type === 'sectionHeader' && row.sticky !== false && row.indexTitle ? [{
84
+ key: row.key,
85
+ index
86
+ }] : []);
87
+ const maxOffset = Math.max(0, layout.contentHeight - viewportLength);
88
+ if (maxOffset > 0 && offset >= maxOffset - 1) {
89
+ return indexedRows.at(-1)?.key;
90
+ }
91
+ let activeKey;
92
+ indexedRows.forEach(({
93
+ key,
94
+ index
95
+ }) => {
96
+ if (itemStart(layout.items[index], layout.horizontal) <= offset) {
97
+ activeKey = key;
98
+ }
99
+ });
100
+ return activeKey;
101
+ }
57
102
  /** Resolves every reusable Market geometry field, including legacy defaults. */
58
103
  export function resolveWebMarketLayoutStyle(row) {
59
104
  const style = row.style;
@@ -512,7 +557,7 @@ export const WEB_LIST_CSS = `
512
557
  .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}
513
558
  .ok-native-list-row.ok-native-list-market-skeleton{padding:12px 20px;gap:0}.ok-native-list-skeleton-left{display:flex;align-items:center;gap:12px;flex:1}.ok-native-list-skeleton-text{display:flex;flex-direction:column;gap:4px}.ok-native-list-skeleton-right{display:flex;align-items:center;gap:8px}.ok-native-list-skeleton-mark{display:block;flex-shrink:0;border-radius:8px;animation:ok-native-list-skeleton 1.5s linear infinite alternate}@keyframes ok-native-list-skeleton{from{background-color:var(--nl-skeleton-base)}to{background-color:var(--nl-skeleton-highlight)}}.ok-native-list-market-spinner{display:block;width:20px;height:20px;flex-shrink:0;color:var(--nl-icon);animation:ok-native-list-spin .75s linear infinite}
514
559
  @media (prefers-reduced-motion:reduce){.ok-native-list-index-preview,.ok-native-list-refresh{transition:none}.ok-native-list-spinner,.ok-native-list-market-spinner{animation:none}.ok-native-list-skeleton-mark{animation:none;background:var(--nl-skeleton-base)}}
515
- .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-viewport-frame:has(>.ok-native-list-index-rail:not([hidden]))>.ok-native-list-viewport,.ok-native-list-viewport-frame[data-section-index-visible="true"]>.ok-native-list-viewport{scrollbar-width:none}.ok-native-list-viewport-frame:has(>.ok-native-list-index-rail:not([hidden]))>.ok-native-list-viewport::-webkit-scrollbar,.ok-native-list-viewport-frame[data-section-index-visible="true"]>.ok-native-list-viewport::-webkit-scrollbar{display:none}.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;font-family:Roobert,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.ok-native-list-index-rail[hidden]{display:none}.ok-native-list-index-button{appearance:none;position:absolute;left:15px;display:flex;width:14px;height:14px;align-items:center;justify-content:center;padding:0;transform:translateY(-50%);border:0;border-radius:7px;background:transparent;color:var(--nl-disabled);font-family:inherit;font-size:10px;font-weight:400;line-height:1;cursor:pointer}.ok-native-list-index-button[data-active="true"]{background:var(--nl-positive);color:var(--nl-inverse-text);font-weight:500}.ok-native-list-index-button:focus-visible{outline:2px solid var(--nl-positive);outline-offset:1px}
560
+ .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-viewport-frame:has(>.ok-native-list-index-rail:not([hidden]))>.ok-native-list-viewport,.ok-native-list-viewport-frame[data-section-index-visible="true"]>.ok-native-list-viewport{scrollbar-width:none}.ok-native-list-viewport-frame:has(>.ok-native-list-index-rail:not([hidden]))>.ok-native-list-viewport::-webkit-scrollbar,.ok-native-list-viewport-frame[data-section-index-visible="true"]>.ok-native-list-viewport::-webkit-scrollbar{display:none}.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;font-family:Roobert,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.ok-native-list-index-rail[hidden]{display:none}.ok-native-list-index-button{appearance:none;position:absolute;left:15px;display:flex;width:14px;height:14px;align-items:center;justify-content:center;padding:0;transform:translateY(-50%);border:0;border-radius:7px;background:transparent;color:var(--nl-disabled);font-family:inherit;font-size:10px;font-weight:400;line-height:1;cursor:pointer;opacity:.5;transition:opacity .15s ease}.ok-native-list-index-rail:hover .ok-native-list-index-button,.ok-native-list-index-rail:focus-within .ok-native-list-index-button,.ok-native-list-index-button[data-active="true"]{opacity:1}.ok-native-list-index-button[data-active="true"]{background:var(--nl-positive);color:var(--nl-inverse-text);font-weight:500}.ok-native-list-index-button:focus-visible{outline:2px solid var(--nl-positive);outline-offset:1px}
516
561
  .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)}
517
562
  .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)}
518
563
  .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}
@@ -1982,6 +2027,8 @@ export class NativeListWebEngine {
1982
2027
  };
1983
2028
  mounted = new Map();
1984
2029
  pool = [];
2030
+ sectionIndexTransformTrackingFrames = 0;
2031
+ sectionIndexTransformSettledFrames = 0;
1985
2032
  // OneKey patch: URI leases outlive DOM overscan only inside this bounded window.
1986
2033
  avatarLeases = new Map();
1987
2034
  avatarOffset = 0;
@@ -1996,13 +2043,15 @@ export class NativeListWebEngine {
1996
2043
  lastViewportWidth = -1;
1997
2044
  lastViewportHeight = -1;
1998
2045
  destroyed = false;
2046
+ sectionIndexAlignStart = false;
1999
2047
  // OneKey patch: warning banners are measured after normal browser text wrapping.
2000
2048
  measuredWarningHeights = new Map();
2001
- constructor(host, snapshot, callbacks, virtualizationEnabled = true) {
2049
+ constructor(host, snapshot, callbacks, virtualizationEnabled = true, sectionIndexContainer) {
2002
2050
  this.document = host.ownerDocument;
2003
2051
  this.snapshot = validateSnapshot(snapshot);
2004
2052
  this.callbacks = callbacks;
2005
2053
  this.virtualizationEnabled = virtualizationEnabled;
2054
+ this.sectionIndexContainer = sectionIndexContainer ?? undefined;
2006
2055
  this.previousHostPosition = host.style.position;
2007
2056
  if (!host.style.position) host.style.position = 'relative';
2008
2057
  this.root = createElement(this.document, 'div', 'ok-native-list-root');
@@ -2078,6 +2127,7 @@ export class NativeListWebEngine {
2078
2127
  } else {
2079
2128
  this.document.defaultView?.addEventListener('resize', this.handleWindowResize);
2080
2129
  }
2130
+ this.observeSectionIndexTransforms();
2081
2131
  this.applySnapshot(this.snapshot);
2082
2132
  }
2083
2133
  updateCallbacks(callbacks) {
@@ -2238,8 +2288,10 @@ export class NativeListWebEngine {
2238
2288
  this.invalidateActionAnchor('destroy');
2239
2289
  this.destroyed = true;
2240
2290
  if (this.frameHandle !== undefined) this.cancelFrame(this.frameHandle);
2291
+ if (this.sectionIndexTransformFrame !== undefined) this.cancelFrame(this.sectionIndexTransformFrame);
2241
2292
  if (this.reorderMovementTimer !== undefined) this.document.defaultView?.clearTimeout(this.reorderMovementTimer);
2242
2293
  this.resizeObserver?.disconnect();
2294
+ this.sectionIndexTransformObserver?.disconnect();
2243
2295
  this.document.defaultView?.removeEventListener('resize', this.handleWindowResize);
2244
2296
  this.viewport.removeEventListener('scroll', this.handleScroll);
2245
2297
  this.viewport.removeEventListener('ok-collapsible-pager-insets-changed', this.handleCollapsiblePagerInsetsChanged);
@@ -2571,7 +2623,7 @@ export class NativeListWebEngine {
2571
2623
  const {
2572
2624
  trackHeight
2573
2625
  } = this.sectionIndexMetrics(viewportHeight);
2574
- const maxVisible = Math.max(1, Math.floor(trackHeight / SECTION_INDEX_LABEL_SPACING));
2626
+ const maxVisible = Math.max(1, Math.floor(trackHeight / SECTION_INDEX_MIN_LABEL_SPACING));
2575
2627
  if (entryCount <= maxVisible) {
2576
2628
  return Array.from({
2577
2629
  length: entryCount
@@ -2585,18 +2637,15 @@ export class NativeListWebEngine {
2585
2637
  return [...result].sort((left, right) => left - right);
2586
2638
  }
2587
2639
  sectionIndexMetrics(viewportHeight) {
2588
- const availableHeight = Math.max(0, viewportHeight - SECTION_INDEX_EDGE_PADDING * 2);
2589
- const trackHeight = Math.min(availableHeight, SECTION_INDEX_LABEL_SPACING * this.sectionIndexEntries.length);
2590
- return {
2591
- originY: (viewportHeight - trackHeight) / 2,
2592
- trackHeight
2593
- };
2640
+ return webSectionIndexMetrics(this.sectionIndexEntries.length, viewportHeight, this.sectionIndexAlignStart);
2594
2641
  }
2595
2642
  configureSectionIndexRail(viewportHeight, stickyInset, windowCentered) {
2596
2643
  const view = this.document.defaultView;
2597
2644
  const direction = view?.getComputedStyle(this.viewportFrame).direction === 'rtl' ? 'rtl' : 'ltr';
2598
2645
  this.indexRail.style.direction = direction;
2599
2646
  if (!windowCentered) {
2647
+ this.sectionIndexAlignStart = false;
2648
+ this.resetSectionIndexTransformCompensation();
2600
2649
  if (this.indexRail.parentElement !== this.viewportFrame) {
2601
2650
  this.viewportFrame.appendChild(this.indexRail);
2602
2651
  }
@@ -2611,11 +2660,32 @@ export class NativeListWebEngine {
2611
2660
  return viewportHeight;
2612
2661
  }
2613
2662
  if (!view) return viewportHeight;
2663
+ const sectionIndexContainer = this.sectionIndexContainer?.isConnected ? this.sectionIndexContainer : undefined;
2664
+ const frame = this.viewportFrame.getBoundingClientRect();
2665
+ if (sectionIndexContainer) {
2666
+ const container = sectionIndexContainer.getBoundingClientRect();
2667
+ const railLayout = webSectionIndexContainerLayout(this.sectionIndexEntries.length, sectionIndexContainer.clientHeight || container.height);
2668
+ if (this.indexRail.parentElement !== sectionIndexContainer) {
2669
+ sectionIndexContainer.appendChild(this.indexRail);
2670
+ }
2671
+ this.sectionIndexAlignStart = true;
2672
+ this.indexRail.style.position = 'absolute';
2673
+ this.indexRail.style.removeProperty('left');
2674
+ this.indexRail.style.removeProperty('right');
2675
+ this.indexRail.style.insetInlineEnd = String(Math.max(0, direction === 'rtl' ? frame.left - container.left : container.right - frame.right)) + 'px';
2676
+ this.indexRail.style.top = String(railLayout.top) + 'px';
2677
+ this.indexRail.style.bottom = 'auto';
2678
+ this.indexRail.style.height = String(railLayout.height) + 'px';
2679
+ this.indexRail.style.zIndex = String(SECTION_INDEX_WINDOW_Z_INDEX);
2680
+ this.startSectionIndexTransformCompensation();
2681
+ return railLayout.height;
2682
+ }
2683
+ this.sectionIndexAlignStart = false;
2684
+ this.resetSectionIndexTransformCompensation();
2614
2685
  const visualViewport = view.visualViewport;
2615
2686
  const windowTop = visualViewport?.offsetTop ?? 0;
2616
2687
  const windowHeight = visualViewport?.height ?? view.innerHeight;
2617
- const railHeight = Math.min(windowHeight, SECTION_INDEX_EDGE_PADDING * 2 + SECTION_INDEX_LABEL_SPACING * this.sectionIndexEntries.length);
2618
- const frame = this.viewportFrame.getBoundingClientRect();
2688
+ const railHeight = Math.min(windowHeight, SECTION_INDEX_EDGE_PADDING * 2 + SECTION_INDEX_MIN_LABEL_SPACING * this.sectionIndexEntries.length);
2619
2689
  if (this.indexRail.parentElement !== this.document.body) {
2620
2690
  this.document.body.appendChild(this.indexRail);
2621
2691
  }
@@ -2629,6 +2699,59 @@ export class NativeListWebEngine {
2629
2699
  this.indexRail.style.zIndex = String(SECTION_INDEX_WINDOW_Z_INDEX);
2630
2700
  return railHeight;
2631
2701
  }
2702
+ observeSectionIndexTransforms() {
2703
+ const container = this.sectionIndexContainer;
2704
+ const MutationObserverConstructor = this.document.defaultView?.MutationObserver;
2705
+ if (!container || !MutationObserverConstructor) return;
2706
+ this.sectionIndexTransformObserver = new MutationObserverConstructor(() => this.startSectionIndexTransformCompensation());
2707
+ let element = container;
2708
+ while (element) {
2709
+ this.sectionIndexTransformObserver.observe(element, {
2710
+ attributes: true,
2711
+ attributeFilter: ['style']
2712
+ });
2713
+ if (element === this.document.body) break;
2714
+ element = element.parentElement;
2715
+ }
2716
+ }
2717
+ startSectionIndexTransformCompensation() {
2718
+ this.sectionIndexTransformTrackingFrames = 0;
2719
+ this.sectionIndexTransformSettledFrames = 0;
2720
+ this.updateSectionIndexTransformCompensation();
2721
+ this.scheduleSectionIndexTransformCompensation();
2722
+ }
2723
+ scheduleSectionIndexTransformCompensation() {
2724
+ if (this.destroyed || this.sectionIndexTransformFrame !== undefined || this.indexRail.parentElement !== this.sectionIndexContainer) {
2725
+ return;
2726
+ }
2727
+ this.sectionIndexTransformFrame = this.requestFrame(() => {
2728
+ this.sectionIndexTransformFrame = undefined;
2729
+ const settled = this.updateSectionIndexTransformCompensation();
2730
+ this.sectionIndexTransformTrackingFrames += 1;
2731
+ this.sectionIndexTransformSettledFrames = settled ? this.sectionIndexTransformSettledFrames + 1 : 0;
2732
+ if (this.sectionIndexTransformTrackingFrames < SECTION_INDEX_TRANSFORM_MAX_TRACKING_FRAMES && (this.sectionIndexTransformTrackingFrames < SECTION_INDEX_TRANSFORM_MIN_TRACKING_FRAMES || this.sectionIndexTransformSettledFrames < SECTION_INDEX_TRANSFORM_SETTLED_FRAMES)) {
2733
+ this.scheduleSectionIndexTransformCompensation();
2734
+ }
2735
+ });
2736
+ }
2737
+ updateSectionIndexTransformCompensation() {
2738
+ const container = this.sectionIndexContainer;
2739
+ if (!container || this.indexRail.parentElement !== container) return true;
2740
+ const inverseScale = webSectionIndexInverseScale(container.clientHeight, container.getBoundingClientRect().height);
2741
+ this.indexRail.style.transformOrigin = 'center center';
2742
+ this.indexRail.style.transform = Math.abs(inverseScale - 1) < SECTION_INDEX_TRANSFORM_SETTLED_EPSILON ? 'none' : `scale(${String(inverseScale)})`;
2743
+ return Math.abs(inverseScale - 1) < SECTION_INDEX_TRANSFORM_SETTLED_EPSILON;
2744
+ }
2745
+ resetSectionIndexTransformCompensation() {
2746
+ if (this.sectionIndexTransformFrame !== undefined) {
2747
+ this.cancelFrame(this.sectionIndexTransformFrame);
2748
+ this.sectionIndexTransformFrame = undefined;
2749
+ }
2750
+ this.sectionIndexTransformTrackingFrames = 0;
2751
+ this.sectionIndexTransformSettledFrames = 0;
2752
+ this.indexRail.style.removeProperty('transform');
2753
+ this.indexRail.style.removeProperty('transform-origin');
2754
+ }
2632
2755
  renderSectionIndex(viewportHeight, stickyInset) {
2633
2756
  this.indexRail.replaceChildren();
2634
2757
  if (!sectionIndexEnabled(this.snapshot)) {
@@ -2654,8 +2777,10 @@ export class NativeListWebEngine {
2654
2777
  const visibleEntryIndices = this.sectionIndexVisibleEntryIndices(indexLayoutHeight);
2655
2778
  setData(this.indexRail, 'compact', visibleEntryIndices.length < this.sectionIndexEntries.length);
2656
2779
  const metrics = this.sectionIndexMetrics(indexLayoutHeight);
2657
- const visibleTrackHeight = Math.min(metrics.trackHeight, SECTION_INDEX_LABEL_SPACING * visibleEntryIndices.length);
2658
- const visibleOriginY = metrics.originY + (metrics.trackHeight - visibleTrackHeight) / 2;
2780
+ const visibleTrackHeight = Math.min(metrics.trackHeight, SECTION_INDEX_MIN_LABEL_SPACING * visibleEntryIndices.length);
2781
+ const allEntriesVisible = visibleEntryIndices.length === this.sectionIndexEntries.length;
2782
+ const resolvedVisibleTrackHeight = allEntriesVisible ? metrics.trackHeight : visibleTrackHeight;
2783
+ const visibleOriginY = metrics.originY + (metrics.trackHeight - resolvedVisibleTrackHeight) / 2;
2659
2784
  const fragment = this.document.createDocumentFragment();
2660
2785
  visibleEntryIndices.forEach((entryIndex, visibleIndex) => {
2661
2786
  const entry = this.sectionIndexEntries[entryIndex];
@@ -2666,7 +2791,7 @@ export class NativeListWebEngine {
2666
2791
  setData(button, 'sectionEntryIndex', entryIndex);
2667
2792
  setData(button, 'sectionPosition', entry.position);
2668
2793
  setData(button, 'sectionKey', entry.key);
2669
- button.style.top = String(visibleOriginY + visibleTrackHeight * (visibleIndex + 0.5) / visibleEntryIndices.length) + 'px';
2794
+ button.style.top = String(visibleOriginY + resolvedVisibleTrackHeight * (visibleIndex + 0.5) / visibleEntryIndices.length) + 'px';
2670
2795
  fragment.appendChild(button);
2671
2796
  });
2672
2797
  this.indexRail.appendChild(fragment);
@@ -2769,13 +2894,7 @@ export class NativeListWebEngine {
2769
2894
 
2770
2895
  // private updateSectionIndex(firstVisibleIndex: number) {
2771
2896
  updateSectionIndex() {
2772
- let activeKey;
2773
- this.snapshot.rows.forEach((row, index) => {
2774
- if (
2775
- // OneKey patch: a spacer ending exactly at the viewport is not the active section.
2776
- // index <= firstVisibleIndex &&
2777
- itemStart(this.layout.items[index], this.layout.horizontal) <= this.currentOffset() && row.type === 'sectionHeader' && row.sticky !== false && row.indexTitle) activeKey = row.key;
2778
- });
2897
+ const activeKey = webSectionIndexActiveKey(this.snapshot.rows, this.layout, this.currentOffset(), this.viewportLength());
2779
2898
  this.indexRail.querySelectorAll('[data-section-key]').forEach(button => setData(button, 'active', button.dataset.sectionKey === activeKey));
2780
2899
  }
2781
2900
  checkEndReached(lastVisibleIndex) {
@@ -1,4 +1,5 @@
1
- import type { ViewProps } from 'react-native';
1
+ import type { RefObject } from 'react';
2
+ import type { View, ViewProps } from 'react-native';
2
3
  import type { ActionAnchorInvalidatedEvent, EndReachedEvent, NativeListSnapshot, ReorderEvent, RowActionEvent, RowModel, RowPatch, SelectionDeltaEvent, VisibleRangeChangedEvent } from './models';
3
4
  export type ActionAnchorState = Readonly<{
4
5
  token: string;
@@ -76,6 +77,8 @@ export type NativeListProps = Omit<ViewProps, 'children'> & Readonly<{
76
77
  snapshot: NativeListSnapshot;
77
78
  /** Web only. Defaults to true and is ignored by the native host. */
78
79
  webVirtualizationEnabled?: boolean;
80
+ /** Web only. Hosts the section index inside this container. */
81
+ webSectionIndexContainerRef?: RefObject<View | null>;
79
82
  /** Defaults to `none`. `interactive` behaves as `none` on Android. */
80
83
  keyboardDismissMode?: NativeListKeyboardDismissMode;
81
84
  /**
@@ -22,6 +22,16 @@ export type WebListLayout = Readonly<{
22
22
  contentHeight: number;
23
23
  horizontal: boolean;
24
24
  }>;
25
+ export declare function webSectionIndexMetrics(entryCount: number, viewportHeight: number, alignStart?: boolean): {
26
+ originY: number;
27
+ trackHeight: number;
28
+ };
29
+ export declare function webSectionIndexContainerLayout(entryCount: number, containerHeight: number): {
30
+ top: number;
31
+ height: number;
32
+ };
33
+ export declare function webSectionIndexInverseScale(layoutHeight: number, renderedHeight: number): number;
34
+ export declare function webSectionIndexActiveKey(rows: readonly RowModel[], layout: WebListLayout, offset: number, viewportLength: number): string | undefined;
25
35
  export type WebMarketLayoutStyle = Readonly<{
26
36
  horizontalPadding: number;
27
37
  verticalPadding: number;
@@ -91,6 +101,7 @@ export declare class NativeListWebEngine {
91
101
  private readonly refreshIndicator;
92
102
  private readonly reorderPreview;
93
103
  private readonly previousHostPosition;
104
+ private readonly sectionIndexContainer;
94
105
  private snapshot;
95
106
  private rows;
96
107
  private selectedKeys;
@@ -100,6 +111,10 @@ export declare class NativeListWebEngine {
100
111
  private readonly pool;
101
112
  private frameHandle;
102
113
  private resizeObserver;
114
+ private sectionIndexTransformObserver;
115
+ private sectionIndexTransformFrame;
116
+ private sectionIndexTransformTrackingFrames;
117
+ private sectionIndexTransformSettledFrames;
103
118
  private pendingScroll;
104
119
  private lastVisibleSignature;
105
120
  private readonly avatarLeases;
@@ -130,8 +145,9 @@ export declare class NativeListWebEngine {
130
145
  private lastViewportWidth;
131
146
  private lastViewportHeight;
132
147
  private destroyed;
148
+ private sectionIndexAlignStart;
133
149
  private measuredWarningHeights;
134
- constructor(host: HTMLElement, snapshot: NativeListSnapshot, callbacks: NativeListWebCallbacks, virtualizationEnabled?: boolean);
150
+ constructor(host: HTMLElement, snapshot: NativeListSnapshot, callbacks: NativeListWebCallbacks, virtualizationEnabled?: boolean, sectionIndexContainer?: HTMLElement | null);
135
151
  updateCallbacks(callbacks: NativeListWebCallbacks): void;
136
152
  setVirtualizationEnabled(enabled: boolean): void;
137
153
  applySnapshot(snapshot: NativeListSnapshot): void;
@@ -156,6 +172,11 @@ export declare class NativeListWebEngine {
156
172
  private sectionIndexVisibleEntryIndices;
157
173
  private sectionIndexMetrics;
158
174
  private configureSectionIndexRail;
175
+ private observeSectionIndexTransforms;
176
+ private startSectionIndexTransformCompensation;
177
+ private scheduleSectionIndexTransformCompensation;
178
+ private updateSectionIndexTransformCompensation;
179
+ private resetSectionIndexTransformCompensation;
159
180
  private renderSectionIndex;
160
181
  private updateVisibleSelection;
161
182
  private updateVisibleState;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-native-list",
3
- "version": "3.0.152",
3
+ "version": "3.0.154-alpha.246",
4
4
  "description": "Template-driven native RecyclerView and UICollectionView for React Native",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./lib/module/index.js",
@@ -83,8 +83,8 @@
83
83
  "typescript": "^5.9.2"
84
84
  },
85
85
  "peerDependencies": {
86
- "@onekeyfe/react-native-image": "3.0.152",
87
- "@onekeyfe/react-native-native-logger": "3.0.152",
86
+ "@onekeyfe/react-native-image": "3.0.154-alpha.246",
87
+ "@onekeyfe/react-native-native-logger": "3.0.154-alpha.246",
88
88
  "react": "*",
89
89
  "react-native": "*",
90
90
  "react-native-nitro-modules": "0.37.0"
@@ -97,6 +97,7 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
97
97
  {
98
98
  snapshot,
99
99
  webVirtualizationEnabled: _webVirtualizationEnabled,
100
+ webSectionIndexContainerRef: _webSectionIndexContainerRef,
100
101
  keyboardDismissMode = 'none',
101
102
  keyboardShouldPersistTaps = 'never',
102
103
  onRowAction,
@@ -1,4 +1,5 @@
1
- import type { ViewProps } from 'react-native';
1
+ import type { RefObject } from 'react';
2
+ import type { View, ViewProps } from 'react-native';
2
3
  import type {
3
4
  ActionAnchorInvalidatedEvent,
4
5
  EndReachedEvent,
@@ -120,6 +121,8 @@ export type NativeListProps = Omit<ViewProps, 'children'> &
120
121
  snapshot: NativeListSnapshot;
121
122
  /** Web only. Defaults to true and is ignored by the native host. */
122
123
  webVirtualizationEnabled?: boolean;
124
+ /** Web only. Hosts the section index inside this container. */
125
+ webSectionIndexContainerRef?: RefObject<View | null>;
123
126
  /** Defaults to `none`. `interactive` behaves as `none` on Android. */
124
127
  keyboardDismissMode?: NativeListKeyboardDismissMode;
125
128
  /**
@@ -83,6 +83,7 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
83
83
  {
84
84
  snapshot,
85
85
  webVirtualizationEnabled = true,
86
+ webSectionIndexContainerRef,
86
87
  keyboardDismissMode: _keyboardDismissMode,
87
88
  keyboardShouldPersistTaps: _keyboardShouldPersistTaps,
88
89
  onRowAction,
@@ -175,7 +176,8 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
175
176
  host,
176
177
  snapshotRef.current,
177
178
  callbacksRef.current,
178
- webVirtualizationEnabledRef.current
179
+ webVirtualizationEnabledRef.current,
180
+ webSectionIndexContainerRef?.current as unknown as HTMLElement | null
179
181
  );
180
182
  engineRef.current = engine;
181
183
  appliedSnapshotRef.current = snapshotRef.current;
@@ -195,7 +197,7 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
195
197
  engine.destroy();
196
198
  if (engineRef.current === engine) engineRef.current = undefined;
197
199
  };
198
- }, [host]);
200
+ }, [host, webSectionIndexContainerRef]);
199
201
 
200
202
  useEffect(() => {
201
203
  engineRef.current?.setVirtualizationEnabled(webVirtualizationEnabled);
@@ -50,9 +50,14 @@ import {
50
50
  const SECTION_INDEX_CONTENT_INSET = 16;
51
51
  const SECTION_INDEX_RAIL_WIDTH = 32;
52
52
  const SECTION_INDEX_EDGE_PADDING = 8;
53
- const SECTION_INDEX_LABEL_SPACING = 16;
53
+ const SECTION_INDEX_MIN_LABEL_SPACING = 16;
54
+ const SECTION_INDEX_CONTAINER_LABEL_SPACING = 22;
54
55
  const SECTION_INDEX_MIN_HEIGHT = 120;
55
56
  const SECTION_INDEX_WINDOW_Z_INDEX = 100_000;
57
+ const SECTION_INDEX_TRANSFORM_MIN_TRACKING_FRAMES = 12;
58
+ const SECTION_INDEX_TRANSFORM_MAX_TRACKING_FRAMES = 90;
59
+ const SECTION_INDEX_TRANSFORM_SETTLED_FRAMES = 4;
60
+ const SECTION_INDEX_TRANSFORM_SETTLED_EPSILON = 0.0001;
56
61
  const DEFAULT_VIEWPORT_WIDTH = 320;
57
62
  const DEFAULT_VIEWPORT_HEIGHT = 640;
58
63
  const OVERSCAN_VIEWPORTS = 1;
@@ -116,6 +121,74 @@ export type WebListLayout = Readonly<{
116
121
  horizontal: boolean;
117
122
  }>;
118
123
 
124
+ export function webSectionIndexMetrics(
125
+ entryCount: number,
126
+ viewportHeight: number,
127
+ alignStart = false
128
+ ) {
129
+ const availableHeight = Math.max(
130
+ 0,
131
+ viewportHeight - SECTION_INDEX_EDGE_PADDING * 2
132
+ );
133
+ const labelSpacing = alignStart
134
+ ? SECTION_INDEX_CONTAINER_LABEL_SPACING
135
+ : SECTION_INDEX_MIN_LABEL_SPACING;
136
+ const trackHeight = Math.min(availableHeight, labelSpacing * entryCount);
137
+ return {
138
+ originY: alignStart
139
+ ? SECTION_INDEX_EDGE_PADDING
140
+ : (viewportHeight - trackHeight) / 2,
141
+ trackHeight,
142
+ };
143
+ }
144
+
145
+ export function webSectionIndexContainerLayout(
146
+ entryCount: number,
147
+ containerHeight: number
148
+ ) {
149
+ const height = Math.min(
150
+ Math.max(0, containerHeight),
151
+ SECTION_INDEX_EDGE_PADDING * 2 +
152
+ SECTION_INDEX_CONTAINER_LABEL_SPACING * entryCount
153
+ );
154
+ return {
155
+ top: Math.max(0, (containerHeight - height) / 2),
156
+ height,
157
+ };
158
+ }
159
+
160
+ export function webSectionIndexInverseScale(
161
+ layoutHeight: number,
162
+ renderedHeight: number
163
+ ) {
164
+ if (layoutHeight <= 0 || renderedHeight <= 0) return 1;
165
+ return layoutHeight / renderedHeight;
166
+ }
167
+
168
+ export function webSectionIndexActiveKey(
169
+ rows: readonly RowModel[],
170
+ layout: WebListLayout,
171
+ offset: number,
172
+ viewportLength: number
173
+ ): string | undefined {
174
+ const indexedRows = rows.flatMap((row, index) =>
175
+ row.type === 'sectionHeader' && row.sticky !== false && row.indexTitle
176
+ ? [{ key: row.key, index }]
177
+ : []
178
+ );
179
+ const maxOffset = Math.max(0, layout.contentHeight - viewportLength);
180
+ if (maxOffset > 0 && offset >= maxOffset - 1) {
181
+ return indexedRows.at(-1)?.key;
182
+ }
183
+ let activeKey: string | undefined;
184
+ indexedRows.forEach(({ key, index }) => {
185
+ if (itemStart(layout.items[index], layout.horizontal) <= offset) {
186
+ activeKey = key;
187
+ }
188
+ });
189
+ return activeKey;
190
+ }
191
+
119
192
  export type WebMarketLayoutStyle = Readonly<{
120
193
  horizontalPadding: number;
121
194
  verticalPadding: number;
@@ -940,7 +1013,7 @@ export const WEB_LIST_CSS = `
940
1013
  .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}
941
1014
  .ok-native-list-row.ok-native-list-market-skeleton{padding:12px 20px;gap:0}.ok-native-list-skeleton-left{display:flex;align-items:center;gap:12px;flex:1}.ok-native-list-skeleton-text{display:flex;flex-direction:column;gap:4px}.ok-native-list-skeleton-right{display:flex;align-items:center;gap:8px}.ok-native-list-skeleton-mark{display:block;flex-shrink:0;border-radius:8px;animation:ok-native-list-skeleton 1.5s linear infinite alternate}@keyframes ok-native-list-skeleton{from{background-color:var(--nl-skeleton-base)}to{background-color:var(--nl-skeleton-highlight)}}.ok-native-list-market-spinner{display:block;width:20px;height:20px;flex-shrink:0;color:var(--nl-icon);animation:ok-native-list-spin .75s linear infinite}
942
1015
  @media (prefers-reduced-motion:reduce){.ok-native-list-index-preview,.ok-native-list-refresh{transition:none}.ok-native-list-spinner,.ok-native-list-market-spinner{animation:none}.ok-native-list-skeleton-mark{animation:none;background:var(--nl-skeleton-base)}}
943
- .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-viewport-frame:has(>.ok-native-list-index-rail:not([hidden]))>.ok-native-list-viewport,.ok-native-list-viewport-frame[data-section-index-visible="true"]>.ok-native-list-viewport{scrollbar-width:none}.ok-native-list-viewport-frame:has(>.ok-native-list-index-rail:not([hidden]))>.ok-native-list-viewport::-webkit-scrollbar,.ok-native-list-viewport-frame[data-section-index-visible="true"]>.ok-native-list-viewport::-webkit-scrollbar{display:none}.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;font-family:Roobert,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.ok-native-list-index-rail[hidden]{display:none}.ok-native-list-index-button{appearance:none;position:absolute;left:15px;display:flex;width:14px;height:14px;align-items:center;justify-content:center;padding:0;transform:translateY(-50%);border:0;border-radius:7px;background:transparent;color:var(--nl-disabled);font-family:inherit;font-size:10px;font-weight:400;line-height:1;cursor:pointer}.ok-native-list-index-button[data-active="true"]{background:var(--nl-positive);color:var(--nl-inverse-text);font-weight:500}.ok-native-list-index-button:focus-visible{outline:2px solid var(--nl-positive);outline-offset:1px}
1016
+ .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-viewport-frame:has(>.ok-native-list-index-rail:not([hidden]))>.ok-native-list-viewport,.ok-native-list-viewport-frame[data-section-index-visible="true"]>.ok-native-list-viewport{scrollbar-width:none}.ok-native-list-viewport-frame:has(>.ok-native-list-index-rail:not([hidden]))>.ok-native-list-viewport::-webkit-scrollbar,.ok-native-list-viewport-frame[data-section-index-visible="true"]>.ok-native-list-viewport::-webkit-scrollbar{display:none}.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;font-family:Roobert,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.ok-native-list-index-rail[hidden]{display:none}.ok-native-list-index-button{appearance:none;position:absolute;left:15px;display:flex;width:14px;height:14px;align-items:center;justify-content:center;padding:0;transform:translateY(-50%);border:0;border-radius:7px;background:transparent;color:var(--nl-disabled);font-family:inherit;font-size:10px;font-weight:400;line-height:1;cursor:pointer;opacity:.5;transition:opacity .15s ease}.ok-native-list-index-rail:hover .ok-native-list-index-button,.ok-native-list-index-rail:focus-within .ok-native-list-index-button,.ok-native-list-index-button[data-active="true"]{opacity:1}.ok-native-list-index-button[data-active="true"]{background:var(--nl-positive);color:var(--nl-inverse-text);font-weight:500}.ok-native-list-index-button:focus-visible{outline:2px solid var(--nl-positive);outline-offset:1px}
944
1017
  .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)}
945
1018
  .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)}
946
1019
  .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}
@@ -3469,6 +3542,7 @@ export class NativeListWebEngine {
3469
3542
  private readonly refreshIndicator: HTMLElement;
3470
3543
  private readonly reorderPreview: HTMLElement;
3471
3544
  private readonly previousHostPosition: string;
3545
+ private readonly sectionIndexContainer: HTMLElement | undefined;
3472
3546
  private snapshot: NativeListSnapshot;
3473
3547
  private rows: readonly RowModel[] = [];
3474
3548
  private selectedKeys: ReadonlySet<string> = new Set();
@@ -3483,6 +3557,10 @@ export class NativeListWebEngine {
3483
3557
  private readonly pool: HTMLElement[] = [];
3484
3558
  private frameHandle: number | undefined;
3485
3559
  private resizeObserver: ResizeObserver | undefined;
3560
+ private sectionIndexTransformObserver: MutationObserver | undefined;
3561
+ private sectionIndexTransformFrame: number | undefined;
3562
+ private sectionIndexTransformTrackingFrames = 0;
3563
+ private sectionIndexTransformSettledFrames = 0;
3486
3564
  private pendingScroll: PendingScroll | undefined;
3487
3565
  private lastVisibleSignature: string | undefined;
3488
3566
  // OneKey patch: URI leases outlive DOM overscan only inside this bounded window.
@@ -3528,6 +3606,7 @@ export class NativeListWebEngine {
3528
3606
  private lastViewportWidth = -1;
3529
3607
  private lastViewportHeight = -1;
3530
3608
  private destroyed = false;
3609
+ private sectionIndexAlignStart = false;
3531
3610
  // OneKey patch: warning banners are measured after normal browser text wrapping.
3532
3611
  private measuredWarningHeights = new Map<string, number>();
3533
3612
 
@@ -3535,12 +3614,14 @@ export class NativeListWebEngine {
3535
3614
  host: HTMLElement,
3536
3615
  snapshot: NativeListSnapshot,
3537
3616
  callbacks: NativeListWebCallbacks,
3538
- virtualizationEnabled = true
3617
+ virtualizationEnabled = true,
3618
+ sectionIndexContainer?: HTMLElement | null
3539
3619
  ) {
3540
3620
  this.document = host.ownerDocument;
3541
3621
  this.snapshot = validateSnapshot(snapshot);
3542
3622
  this.callbacks = callbacks;
3543
3623
  this.virtualizationEnabled = virtualizationEnabled;
3624
+ this.sectionIndexContainer = sectionIndexContainer ?? undefined;
3544
3625
  this.previousHostPosition = host.style.position;
3545
3626
  if (!host.style.position) host.style.position = 'relative';
3546
3627
 
@@ -3674,6 +3755,7 @@ export class NativeListWebEngine {
3674
3755
  this.handleWindowResize
3675
3756
  );
3676
3757
  }
3758
+ this.observeSectionIndexTransforms();
3677
3759
  this.applySnapshot(this.snapshot);
3678
3760
  }
3679
3761
 
@@ -3853,9 +3935,12 @@ export class NativeListWebEngine {
3853
3935
  this.invalidateActionAnchor('destroy');
3854
3936
  this.destroyed = true;
3855
3937
  if (this.frameHandle !== undefined) this.cancelFrame(this.frameHandle);
3938
+ if (this.sectionIndexTransformFrame !== undefined)
3939
+ this.cancelFrame(this.sectionIndexTransformFrame);
3856
3940
  if (this.reorderMovementTimer !== undefined)
3857
3941
  this.document.defaultView?.clearTimeout(this.reorderMovementTimer);
3858
3942
  this.resizeObserver?.disconnect();
3943
+ this.sectionIndexTransformObserver?.disconnect();
3859
3944
  this.document.defaultView?.removeEventListener(
3860
3945
  'resize',
3861
3946
  this.handleWindowResize
@@ -4342,7 +4427,7 @@ export class NativeListWebEngine {
4342
4427
  const { trackHeight } = this.sectionIndexMetrics(viewportHeight);
4343
4428
  const maxVisible = Math.max(
4344
4429
  1,
4345
- Math.floor(trackHeight / SECTION_INDEX_LABEL_SPACING)
4430
+ Math.floor(trackHeight / SECTION_INDEX_MIN_LABEL_SPACING)
4346
4431
  );
4347
4432
  if (entryCount <= maxVisible) {
4348
4433
  return Array.from({ length: entryCount }, (_, index) => index);
@@ -4356,18 +4441,11 @@ export class NativeListWebEngine {
4356
4441
  }
4357
4442
 
4358
4443
  private sectionIndexMetrics(viewportHeight: number) {
4359
- const availableHeight = Math.max(
4360
- 0,
4361
- viewportHeight - SECTION_INDEX_EDGE_PADDING * 2
4362
- );
4363
- const trackHeight = Math.min(
4364
- availableHeight,
4365
- SECTION_INDEX_LABEL_SPACING * this.sectionIndexEntries.length
4444
+ return webSectionIndexMetrics(
4445
+ this.sectionIndexEntries.length,
4446
+ viewportHeight,
4447
+ this.sectionIndexAlignStart
4366
4448
  );
4367
- return {
4368
- originY: (viewportHeight - trackHeight) / 2,
4369
- trackHeight,
4370
- };
4371
4449
  }
4372
4450
 
4373
4451
  private configureSectionIndexRail(
@@ -4382,6 +4460,8 @@ export class NativeListWebEngine {
4382
4460
  : 'ltr';
4383
4461
  this.indexRail.style.direction = direction;
4384
4462
  if (!windowCentered) {
4463
+ this.sectionIndexAlignStart = false;
4464
+ this.resetSectionIndexTransformCompensation();
4385
4465
  if (this.indexRail.parentElement !== this.viewportFrame) {
4386
4466
  this.viewportFrame.appendChild(this.indexRail);
4387
4467
  }
@@ -4397,15 +4477,50 @@ export class NativeListWebEngine {
4397
4477
  }
4398
4478
 
4399
4479
  if (!view) return viewportHeight;
4480
+ const sectionIndexContainer = this.sectionIndexContainer?.isConnected
4481
+ ? this.sectionIndexContainer
4482
+ : undefined;
4483
+ const frame = this.viewportFrame.getBoundingClientRect();
4484
+ if (sectionIndexContainer) {
4485
+ const container = sectionIndexContainer.getBoundingClientRect();
4486
+ const railLayout = webSectionIndexContainerLayout(
4487
+ this.sectionIndexEntries.length,
4488
+ sectionIndexContainer.clientHeight || container.height
4489
+ );
4490
+ if (this.indexRail.parentElement !== sectionIndexContainer) {
4491
+ sectionIndexContainer.appendChild(this.indexRail);
4492
+ }
4493
+ this.sectionIndexAlignStart = true;
4494
+ this.indexRail.style.position = 'absolute';
4495
+ this.indexRail.style.removeProperty('left');
4496
+ this.indexRail.style.removeProperty('right');
4497
+ this.indexRail.style.insetInlineEnd =
4498
+ String(
4499
+ Math.max(
4500
+ 0,
4501
+ direction === 'rtl'
4502
+ ? frame.left - container.left
4503
+ : container.right - frame.right
4504
+ )
4505
+ ) + 'px';
4506
+ this.indexRail.style.top = String(railLayout.top) + 'px';
4507
+ this.indexRail.style.bottom = 'auto';
4508
+ this.indexRail.style.height = String(railLayout.height) + 'px';
4509
+ this.indexRail.style.zIndex = String(SECTION_INDEX_WINDOW_Z_INDEX);
4510
+ this.startSectionIndexTransformCompensation();
4511
+ return railLayout.height;
4512
+ }
4513
+
4514
+ this.sectionIndexAlignStart = false;
4515
+ this.resetSectionIndexTransformCompensation();
4400
4516
  const visualViewport = view.visualViewport;
4401
4517
  const windowTop = visualViewport?.offsetTop ?? 0;
4402
4518
  const windowHeight = visualViewport?.height ?? view.innerHeight;
4403
4519
  const railHeight = Math.min(
4404
4520
  windowHeight,
4405
4521
  SECTION_INDEX_EDGE_PADDING * 2 +
4406
- SECTION_INDEX_LABEL_SPACING * this.sectionIndexEntries.length
4522
+ SECTION_INDEX_MIN_LABEL_SPACING * this.sectionIndexEntries.length
4407
4523
  );
4408
- const frame = this.viewportFrame.getBoundingClientRect();
4409
4524
  if (this.indexRail.parentElement !== this.document.body) {
4410
4525
  this.document.body.appendChild(this.indexRail);
4411
4526
  }
@@ -4427,6 +4542,86 @@ export class NativeListWebEngine {
4427
4542
  return railHeight;
4428
4543
  }
4429
4544
 
4545
+ private observeSectionIndexTransforms() {
4546
+ const container = this.sectionIndexContainer;
4547
+ const MutationObserverConstructor =
4548
+ this.document.defaultView?.MutationObserver;
4549
+ if (!container || !MutationObserverConstructor) return;
4550
+ this.sectionIndexTransformObserver = new MutationObserverConstructor(() =>
4551
+ this.startSectionIndexTransformCompensation()
4552
+ );
4553
+ let element: HTMLElement | null = container;
4554
+ while (element) {
4555
+ this.sectionIndexTransformObserver.observe(element, {
4556
+ attributes: true,
4557
+ attributeFilter: ['style'],
4558
+ });
4559
+ if (element === this.document.body) break;
4560
+ element = element.parentElement;
4561
+ }
4562
+ }
4563
+
4564
+ private startSectionIndexTransformCompensation() {
4565
+ this.sectionIndexTransformTrackingFrames = 0;
4566
+ this.sectionIndexTransformSettledFrames = 0;
4567
+ this.updateSectionIndexTransformCompensation();
4568
+ this.scheduleSectionIndexTransformCompensation();
4569
+ }
4570
+
4571
+ private scheduleSectionIndexTransformCompensation() {
4572
+ if (
4573
+ this.destroyed ||
4574
+ this.sectionIndexTransformFrame !== undefined ||
4575
+ this.indexRail.parentElement !== this.sectionIndexContainer
4576
+ ) {
4577
+ return;
4578
+ }
4579
+ this.sectionIndexTransformFrame = this.requestFrame(() => {
4580
+ this.sectionIndexTransformFrame = undefined;
4581
+ const settled = this.updateSectionIndexTransformCompensation();
4582
+ this.sectionIndexTransformTrackingFrames += 1;
4583
+ this.sectionIndexTransformSettledFrames = settled
4584
+ ? this.sectionIndexTransformSettledFrames + 1
4585
+ : 0;
4586
+ if (
4587
+ this.sectionIndexTransformTrackingFrames <
4588
+ SECTION_INDEX_TRANSFORM_MAX_TRACKING_FRAMES &&
4589
+ (this.sectionIndexTransformTrackingFrames <
4590
+ SECTION_INDEX_TRANSFORM_MIN_TRACKING_FRAMES ||
4591
+ this.sectionIndexTransformSettledFrames <
4592
+ SECTION_INDEX_TRANSFORM_SETTLED_FRAMES)
4593
+ ) {
4594
+ this.scheduleSectionIndexTransformCompensation();
4595
+ }
4596
+ });
4597
+ }
4598
+
4599
+ private updateSectionIndexTransformCompensation() {
4600
+ const container = this.sectionIndexContainer;
4601
+ if (!container || this.indexRail.parentElement !== container) return true;
4602
+ const inverseScale = webSectionIndexInverseScale(
4603
+ container.clientHeight,
4604
+ container.getBoundingClientRect().height
4605
+ );
4606
+ this.indexRail.style.transformOrigin = 'center center';
4607
+ this.indexRail.style.transform =
4608
+ Math.abs(inverseScale - 1) < SECTION_INDEX_TRANSFORM_SETTLED_EPSILON
4609
+ ? 'none'
4610
+ : `scale(${String(inverseScale)})`;
4611
+ return Math.abs(inverseScale - 1) < SECTION_INDEX_TRANSFORM_SETTLED_EPSILON;
4612
+ }
4613
+
4614
+ private resetSectionIndexTransformCompensation() {
4615
+ if (this.sectionIndexTransformFrame !== undefined) {
4616
+ this.cancelFrame(this.sectionIndexTransformFrame);
4617
+ this.sectionIndexTransformFrame = undefined;
4618
+ }
4619
+ this.sectionIndexTransformTrackingFrames = 0;
4620
+ this.sectionIndexTransformSettledFrames = 0;
4621
+ this.indexRail.style.removeProperty('transform');
4622
+ this.indexRail.style.removeProperty('transform-origin');
4623
+ }
4624
+
4430
4625
  private renderSectionIndex(viewportHeight: number, stickyInset: number) {
4431
4626
  this.indexRail.replaceChildren();
4432
4627
  if (!sectionIndexEnabled(this.snapshot)) {
@@ -4468,10 +4663,15 @@ export class NativeListWebEngine {
4468
4663
  const metrics = this.sectionIndexMetrics(indexLayoutHeight);
4469
4664
  const visibleTrackHeight = Math.min(
4470
4665
  metrics.trackHeight,
4471
- SECTION_INDEX_LABEL_SPACING * visibleEntryIndices.length
4666
+ SECTION_INDEX_MIN_LABEL_SPACING * visibleEntryIndices.length
4472
4667
  );
4668
+ const allEntriesVisible =
4669
+ visibleEntryIndices.length === this.sectionIndexEntries.length;
4670
+ const resolvedVisibleTrackHeight = allEntriesVisible
4671
+ ? metrics.trackHeight
4672
+ : visibleTrackHeight;
4473
4673
  const visibleOriginY =
4474
- metrics.originY + (metrics.trackHeight - visibleTrackHeight) / 2;
4674
+ metrics.originY + (metrics.trackHeight - resolvedVisibleTrackHeight) / 2;
4475
4675
  const fragment = this.document.createDocumentFragment();
4476
4676
  visibleEntryIndices.forEach((entryIndex, visibleIndex) => {
4477
4677
  const entry = this.sectionIndexEntries[entryIndex];
@@ -4490,7 +4690,7 @@ export class NativeListWebEngine {
4490
4690
  button.style.top =
4491
4691
  String(
4492
4692
  visibleOriginY +
4493
- (visibleTrackHeight * (visibleIndex + 0.5)) /
4693
+ (resolvedVisibleTrackHeight * (visibleIndex + 0.5)) /
4494
4694
  visibleEntryIndices.length
4495
4695
  ) + 'px';
4496
4696
  fragment.appendChild(button);
@@ -4637,19 +4837,12 @@ export class NativeListWebEngine {
4637
4837
 
4638
4838
  // private updateSectionIndex(firstVisibleIndex: number) {
4639
4839
  private updateSectionIndex() {
4640
- let activeKey: string | undefined;
4641
- this.snapshot.rows.forEach((row, index) => {
4642
- if (
4643
- // OneKey patch: a spacer ending exactly at the viewport is not the active section.
4644
- // index <= firstVisibleIndex &&
4645
- itemStart(this.layout.items[index], this.layout.horizontal) <=
4646
- this.currentOffset() &&
4647
- row.type === 'sectionHeader' &&
4648
- row.sticky !== false &&
4649
- row.indexTitle
4650
- )
4651
- activeKey = row.key;
4652
- });
4840
+ const activeKey = webSectionIndexActiveKey(
4841
+ this.snapshot.rows,
4842
+ this.layout,
4843
+ this.currentOffset(),
4844
+ this.viewportLength()
4845
+ );
4653
4846
  this.indexRail
4654
4847
  .querySelectorAll<HTMLElement>('[data-section-key]')
4655
4848
  .forEach((button) =>