@onekeyfe/react-native-native-list 3.0.114 → 3.0.115

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.
@@ -3,6 +3,8 @@ import type {
3
3
  CheckboxState,
4
4
  ImageSource,
5
5
  LeadingVisual,
6
+ MarketRow,
7
+ MarketTextStyle,
6
8
  NativeListSnapshot,
7
9
  NativeListActionAnchor,
8
10
  NativeListActionSource,
@@ -47,7 +49,7 @@ import {
47
49
  const SECTION_INDEX_CONTENT_INSET = 16;
48
50
  const SECTION_INDEX_RAIL_WIDTH = 32;
49
51
  const SECTION_INDEX_EDGE_PADDING = 8;
50
- const SECTION_INDEX_MIN_LABEL_SPACING = 14;
52
+ const SECTION_INDEX_LABEL_SPACING = 16;
51
53
  const SECTION_INDEX_MIN_HEIGHT = 120;
52
54
  const DEFAULT_VIEWPORT_WIDTH = 320;
53
55
  const DEFAULT_VIEWPORT_HEIGHT = 640;
@@ -60,6 +62,10 @@ export const WEB_REORDER_ANIMATION = {
60
62
  } as const;
61
63
 
62
64
  const REORDER_TOUCH_LONG_PRESS_MS = 120;
65
+ const MARKET_LONG_PRESS_MS = 800;
66
+ const MARKET_MOVE_CANCEL_PX = 10;
67
+ export const WEB_MARKET_VERIFIED_PATH =
68
+ 'M9.483 11.458v3.5h-1v-3.5z M10.467 2.698a2.03 2.03 0 0 1 3.065 0l1.358 1.564a.03.03 0 0 0 .028.01l2.046-.325a2.03 2.03 0 0 1 2.347 1.971l.037 2.07q0 .016.014.026l1.776 1.066a2.03 2.03 0 0 1 .532 3.019l-1.304 1.609a.03.03 0 0 0-.005.03l.675 1.956a2.03 2.03 0 0 1-1.533 2.656l-2.033.394a.03.03 0 0 0-.023.019l-.741 1.933a2.03 2.03 0 0 1-2.88 1.05l-1.811-1.006a.03.03 0 0 0-.03 0l-1.811 1.005a2.03 2.03 0 0 1-2.88-1.049l-.742-1.933a.03.03 0 0 0-.023-.019l-2.033-.394a2.03 2.03 0 0 1-1.532-2.656l.675-1.957a.03.03 0 0 0-.005-.029l-1.304-1.61a2.03 2.03 0 0 1 .532-3.018l1.776-1.066a.03.03 0 0 0 .014-.026l.035-2.07a2.03 2.03 0 0 1 2.349-1.97l2.045.324a.03.03 0 0 0 .028-.01zm1.516 3.76a.5.5 0 0 0-.447.276l-1.861 3.724H8.483a1 1 0 0 0-1 1v3.5a1 1 0 0 0 1 1h6.692a2 2 0 0 0 1.981-1.73l.341-2.5a2 2 0 0 0-1.982-2.27h-1.939l.197-1.269a1.5 1.5 0 0 0-1.481-1.731z';
63
69
  const REORDER_MOUSE_MOVE_THRESHOLD_PX = 5;
64
70
  const REORDER_EDGE_RATIO = 0.25;
65
71
  const REORDER_MAX_SPEED_ZONE_RATIO = 0.05;
@@ -108,6 +114,50 @@ export type WebListLayout = Readonly<{
108
114
  horizontal: boolean;
109
115
  }>;
110
116
 
117
+ export type WebMarketLayoutStyle = Readonly<{
118
+ horizontalPadding: number;
119
+ verticalPadding: number;
120
+ leadingGap: number;
121
+ titleBadgeGap: number;
122
+ trailingGap: number;
123
+ imageWidth: number;
124
+ imageHeight: number;
125
+ imageCornerRadius: number;
126
+ changeWidth: number;
127
+ changeHeight: number;
128
+ changeCornerRadius: number;
129
+ }>;
130
+
131
+ /** Resolves every reusable Market geometry field, including legacy defaults. */
132
+ export function resolveWebMarketLayoutStyle(
133
+ row: MarketRow
134
+ ): WebMarketLayoutStyle {
135
+ const style = row.style;
136
+ const imageWidth = style?.image?.width ?? (row.variant === 'stock' ? 40 : 32);
137
+ const imageHeight =
138
+ style?.image?.height ?? (row.variant === 'stock' ? 40 : 32);
139
+ return {
140
+ horizontalPadding:
141
+ style?.horizontalPadding ?? (row.variant === 'perp' ? 16 : 20),
142
+ verticalPadding: style?.verticalPadding ?? 12,
143
+ leadingGap: style?.leadingGap ?? (row.variant === 'perp' ? 8 : 14),
144
+ titleBadgeGap: style?.titleBadgeGap ?? 4,
145
+ trailingGap: style?.trailingGap ?? 8,
146
+ imageWidth,
147
+ imageHeight,
148
+ imageCornerRadius:
149
+ style?.image?.cornerRadius ??
150
+ (style?.image?.shape === 'square'
151
+ ? 0
152
+ : style?.image?.shape === 'rounded'
153
+ ? 8
154
+ : Math.min(imageWidth, imageHeight) / 2),
155
+ changeWidth: style?.changeWidth ?? 80,
156
+ changeHeight: style?.changeHeight ?? 32,
157
+ changeCornerRadius: style?.changeCornerRadius ?? 8,
158
+ };
159
+ }
160
+
111
161
  export type NativeListWebCallbacks = Readonly<{
112
162
  onRowAction?: (event: RowActionEvent) => void;
113
163
  onActionAnchorInvalidated?: (event: ActionAnchorInvalidatedEvent) => void;
@@ -450,7 +500,15 @@ export function estimateWebRowHeight(
450
500
  }
451
501
  case 'system':
452
502
  base =
453
- row.variant === 'noMatch' || row.variant === 'end'
503
+ row.variant === 'loading' && row.loadingStyle === 'skeleton'
504
+ ? 56
505
+ : row.variant === 'loading' && row.loadingStyle === 'spinner'
506
+ ? 52
507
+ : 'presentation' in row && row.presentation === 'market'
508
+ ? row.variant === 'loading'
509
+ ? 68
510
+ : 44
511
+ : row.variant === 'noMatch' || row.variant === 'end'
454
512
  ? 36
455
513
  : row.variant === 'retry'
456
514
  ? 44
@@ -462,6 +520,14 @@ export function estimateWebRowHeight(
462
520
  case 'dataRow':
463
521
  base = row.columns.some((column) => column.secondaryText) ? 60 : 56;
464
522
  break;
523
+ case 'market': {
524
+ const marketStyle = resolveWebMarketLayoutStyle(row);
525
+ base = Math.max(
526
+ row.variant === 'stock' ? 72 : 68,
527
+ marketStyle.imageHeight + marketStyle.verticalPadding * 2
528
+ );
529
+ break;
530
+ }
465
531
  case 'identity':
466
532
  base = row.tertiary ? 72 : row.subtitle ? 60 : 56;
467
533
  break;
@@ -706,6 +772,32 @@ export function visibleWebLayoutItems(
706
772
  return visible;
707
773
  }
708
774
 
775
+ export type WebCollapsiblePagerScrollMetrics = Readonly<{
776
+ offset: number;
777
+ viewportLength: number;
778
+ }>;
779
+
780
+ export function resolveWebCollapsiblePagerScrollMetrics(
781
+ rawOffset: number,
782
+ rawViewportLength: number,
783
+ headerInset: number,
784
+ stickyInset: number
785
+ ): WebCollapsiblePagerScrollMetrics {
786
+ const header = Math.max(0, headerInset);
787
+ const sticky = Math.max(0, stickyInset);
788
+ return {
789
+ offset: Math.max(0, rawOffset - header),
790
+ viewportLength: Math.max(0, rawViewportLength - sticky),
791
+ };
792
+ }
793
+
794
+ export function resolveWebCollapsiblePagerRawOffset(
795
+ logicalOffset: number,
796
+ headerInset: number
797
+ ): number {
798
+ return Math.max(0, logicalOffset) + Math.max(0, headerInset);
799
+ }
800
+
709
801
  export function webLayoutItemsForMount(
710
802
  layout: WebListLayout,
711
803
  offset: number,
@@ -742,6 +834,30 @@ export function webRowRenderSignature(row: RowModel): string {
742
834
  return JSON.stringify(rowWithoutSelectionState(row));
743
835
  }
744
836
 
837
+ const MARKET_QUOTE_PATCH_FIELDS = new Set([
838
+ 'revision',
839
+ 'price',
840
+ 'priceSegments',
841
+ 'change',
842
+ 'accessibilityLabel',
843
+ ]);
844
+
845
+ export function isWebMarketQuotePatch(patch: RowPatch): boolean {
846
+ return (
847
+ patch.type === 'market' &&
848
+ Object.keys(patch.changes).every((key) =>
849
+ MARKET_QUOTE_PATCH_FIELDS.has(key)
850
+ )
851
+ );
852
+ }
853
+
854
+ /** Number of Market image-binding passes caused by one Web patch transaction. */
855
+ export function webMarketImageBindDeltaForPatches(
856
+ patches: readonly RowPatch[]
857
+ ): 0 | 1 {
858
+ return patches.length > 0 && patches.every(isWebMarketQuotePatch) ? 0 : 1;
859
+ }
860
+
745
861
  // OneKey patch: selector names must shrink before the sidebar clips their contents.
746
862
  export const WEB_LIST_CSS = `
747
863
  [data-native-list-selector="walletSidebar"] .ok-native-list-title{max-width:100%;min-width:0}
@@ -798,11 +914,19 @@ export const WEB_LIST_CSS = `
798
914
  .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}
799
915
  .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)}
800
916
  .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}
917
+ .ok-native-list-market{padding:12px 20px;gap:14px}.ok-native-list-market>.ok-native-list-visual{width:32px;height:32px;flex-basis:32px}.ok-native-list-market[data-variant="stock"]>.ok-native-list-visual{width:40px;height:40px;flex-basis:40px}.ok-native-list-market>.ok-native-list-visual>.ok-native-list-visual-main{width:100%;height:100%}.ok-native-list-market-main{display:flex;flex:1;min-width:0;flex-direction:column;justify-content:center}.ok-native-list-market-title-line{display:flex;align-items:center;min-width:0;gap:4px}.ok-native-list-market-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--nl-primary);font-size:16px;line-height:24px;font-weight:500}.ok-native-list-market-subtitle{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--nl-secondary);font-size:14px;line-height:20px;font-weight:400}.ok-native-list-market-badge{display:inline-flex;flex:0 0 auto;align-items:center;justify-content:center;box-sizing:border-box;max-width:72px;height:18px;padding:0 5px;border:0;border-radius:4px;background:var(--nl-strong);color:var(--nl-secondary);font:500 11px/18px inherit;overflow:hidden;white-space:nowrap}.ok-native-list-market-badge img{width:14px;height:14px;object-fit:contain}.ok-native-list-market-trailing{display:flex;flex:0 0 auto;align-items:center;gap:8px}.ok-native-list-market-price{max-width:112px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:right;color:var(--nl-primary);font-size:16px;line-height:24px;font-weight:500;font-variant-numeric:tabular-nums}.ok-native-list-market-change{display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:80px;height:32px;border-radius:8px;color:#fff;background:#8d8d8d;font-size:14px;line-height:20px;font-weight:500;font-variant-numeric:tabular-nums;white-space:nowrap}.ok-native-list-market-change[data-tone="positive"]{background:var(--nl-positive)}.ok-native-list-market-change[data-tone="negative"]{background:var(--nl-negative)}
918
+ .ok-native-list-market-badge>svg,.ok-native-list-market-badge>.ok-native-list-market-badge-icon>svg{display:block;width:16px;height:16px;flex:0 0 16px}.ok-native-list-system[data-native-list-presentation="market"]{box-sizing:border-box;padding:12px 20px}.ok-native-list-system[data-native-list-presentation="market"]>.ok-native-list-spinner{width:32px;height:32px}
801
919
  .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)}
802
920
  .ok-native-list-refresh{position:absolute;z-index:7;left:50%;top:8px;display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:999px;background:var(--nl-inverse);color:var(--nl-inverse-text);font-size:12px;opacity:0;transform:translate(-50%,-16px);transition:opacity .15s ease,transform .15s ease;pointer-events:none}.ok-native-list-refresh[data-visible="true"]{opacity:1;transform:translate(-50%,0)}
803
921
  .ok-native-list-warning{height:auto;display:flex;flex-direction:column;align-items:stretch;gap:4px;padding:14px 12px;border-top:1px solid;border-bottom:1px solid;box-sizing:border-box;cursor:default}.ok-native-list-warning-title,.ok-native-list-warning-message{font-size:14px;line-height:20px;white-space:normal;overflow-wrap:anywhere}.ok-native-list-warning-title{font-weight:500;color:var(--nl-primary)}.ok-native-list-warning-message{font-weight:400;color:var(--nl-secondary)}
804
922
  .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}
805
- @media (prefers-reduced-motion:reduce){.ok-native-list-index-preview,.ok-native-list-refresh{transition:none}.ok-native-list-spinner{animation:none}}
923
+ .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}
924
+ @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)}}
925
+ .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{scrollbar-width:none}.ok-native-list-viewport-frame:has(>.ok-native-list-index-rail:not([hidden]))>.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}.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}
926
+ .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)}
927
+ .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)}
928
+ .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}
929
+ @media (prefers-reduced-motion:reduce){.ok-native-list-refresh{transition:none}.ok-native-list-spinner{animation:none}}
806
930
  /* OneKey patch: selector controls follow their original semantic colors and geometry. */
807
931
  .ok-native-list-checkbox[data-selector="networkSelector"]{padding:0;border-radius:4px;border-color:var(--nl-checkbox-border,var(--nl-separator));background:var(--nl-checkbox-icon,var(--nl-inverse-text))}
808
932
  .ok-native-list-checkbox[data-selector="networkSelector"]::after{display:none}
@@ -1280,6 +1404,17 @@ const selectorIcons: Readonly<
1280
1404
  },
1281
1405
  ],
1282
1406
  },
1407
+ BadgeVerifiedSolid: {
1408
+ viewBox: '0 0 24 24',
1409
+ paths: [
1410
+ {
1411
+ d: WEB_MARKET_VERIFIED_PATH,
1412
+ fill: 'currentColor',
1413
+ fillRule: 'evenodd',
1414
+ opacity: 1,
1415
+ },
1416
+ ],
1417
+ },
1283
1418
  };
1284
1419
  function applySelectorIcon(element: HTMLElement, name: string) {
1285
1420
  const icon = selectorIcons[name];
@@ -1335,6 +1470,7 @@ function visualFromRow(row: RowModel): LeadingVisual | undefined {
1335
1470
  if (row.type === 'activity') return row.leading;
1336
1471
  if (row.type === 'message') return row.leading;
1337
1472
  if (row.type === 'dataRow') return row.leading;
1473
+ if (row.type === 'market') return row.leading;
1338
1474
  if (row.type === 'metricCard') return row.visual;
1339
1475
  return undefined;
1340
1476
  }
@@ -2089,6 +2225,98 @@ function createSystemRow(
2089
2225
  'ok-native-list-row ok-native-list-system'
2090
2226
  );
2091
2227
  setData(body, 'variant', row.variant);
2228
+ if ('presentation' in row)
2229
+ setData(body, 'nativeListPresentation', row.presentation);
2230
+ if (row.variant === 'loading' && row.loadingStyle === 'skeleton') {
2231
+ body.classList.add('ok-native-list-market-skeleton');
2232
+ const background = resolvedTheme(context.snapshot).background;
2233
+ const rgb = Number.parseInt(background.slice(1, 7), 16);
2234
+ const dark =
2235
+ ((rgb >> 16) & 255) * 0.299 +
2236
+ ((rgb >> 8) & 255) * 0.587 +
2237
+ (rgb & 255) * 0.114 <
2238
+ 128;
2239
+ body.style.setProperty('--nl-skeleton-base', dark ? '#111111' : '#fafafa');
2240
+ body.style.setProperty(
2241
+ '--nl-skeleton-highlight',
2242
+ dark ? '#333333' : '#cdcdcd'
2243
+ );
2244
+ const left = createElement(
2245
+ context.document,
2246
+ 'div',
2247
+ 'ok-native-list-skeleton-left'
2248
+ );
2249
+ const mark = (width: number, height: number, circle = false) => {
2250
+ const element = createElement(
2251
+ context.document,
2252
+ 'span',
2253
+ 'ok-native-list-skeleton-mark'
2254
+ );
2255
+ element.style.width = `${width}px`;
2256
+ element.style.height = `${height}px`;
2257
+ if (circle) element.style.borderRadius = '50%';
2258
+ return element;
2259
+ };
2260
+ left.appendChild(mark(32, 32, true));
2261
+ const text = createElement(
2262
+ context.document,
2263
+ 'div',
2264
+ 'ok-native-list-skeleton-text'
2265
+ );
2266
+ text.appendChild(mark(80, 16));
2267
+ text.appendChild(mark(60, 12));
2268
+ left.appendChild(text);
2269
+ body.appendChild(left);
2270
+ const right = createElement(
2271
+ context.document,
2272
+ 'div',
2273
+ 'ok-native-list-skeleton-right'
2274
+ );
2275
+ right.appendChild(mark(80, 18));
2276
+ right.appendChild(mark(80, 18));
2277
+ body.appendChild(right);
2278
+ return body;
2279
+ }
2280
+ if (row.variant === 'loading' && row.loadingStyle === 'spinner') {
2281
+ body.style.justifyContent = 'center';
2282
+ body.style.padding = '16px';
2283
+ const spinner = createElement(
2284
+ context.document,
2285
+ 'span',
2286
+ 'ok-native-list-market-spinner'
2287
+ );
2288
+ spinner.setAttribute('role', 'progressbar');
2289
+ // Same 20px SVG and 750ms rotation as react-native-web ActivityIndicator.
2290
+ spinner.innerHTML =
2291
+ '<svg viewBox="0 0 32 32" width="20" height="20"><circle cx="16" cy="16" r="14" fill="none" stroke="currentColor" stroke-width="4" opacity="0.2"/><circle cx="16" cy="16" r="14" fill="none" stroke="currentColor" stroke-width="4" stroke-dasharray="80" stroke-dashoffset="60"/></svg>';
2292
+ body.appendChild(spinner);
2293
+ return body;
2294
+ }
2295
+ if ('presentation' in row && row.presentation === 'market') {
2296
+ if (row.variant === 'noMatch') {
2297
+ body.style.justifyContent = 'center';
2298
+ body.style.padding = '32px';
2299
+ const message = createElement(context.document, 'span', '', row.message);
2300
+ message.style.fontSize = '16px';
2301
+ message.style.lineHeight = '24px';
2302
+ body.appendChild(message);
2303
+ return body;
2304
+ }
2305
+ if (row.variant === 'end') {
2306
+ body.style.justifyContent = 'center';
2307
+ body.style.padding = '16px';
2308
+ body.style.gap = '8px';
2309
+ for (const width of [80, 4, 80]) {
2310
+ const mark = createElement(context.document, 'span', '');
2311
+ mark.style.width = String(width) + 'px';
2312
+ mark.style.height = width === 4 ? '4px' : '1px';
2313
+ mark.style.borderRadius = width === 4 ? '2px' : '0';
2314
+ mark.style.background = 'var(--nl-separator)';
2315
+ body.appendChild(mark);
2316
+ }
2317
+ return body;
2318
+ }
2319
+ }
2092
2320
  if (row.variant === 'warning') {
2093
2321
  body.classList.add('ok-native-list-warning');
2094
2322
  body.style.borderColor = row.borderColor ?? 'var(--nl-separator)';
@@ -2784,6 +3012,268 @@ function createWalletGroupRow(
2784
3012
  return body;
2785
3013
  }
2786
3014
 
3015
+ function marketFontWeight(
3016
+ value: MarketTextStyle['fontWeight'] | undefined,
3017
+ fallback: number
3018
+ ): number {
3019
+ if (value === 'bold') return 700;
3020
+ if (value === 'semibold') return 600;
3021
+ if (value === 'medium') return 500;
3022
+ if (value === 'regular') return 400;
3023
+ return fallback;
3024
+ }
3025
+
3026
+ function applyMarketTextStyle(
3027
+ element: HTMLElement,
3028
+ style: MarketTextStyle | undefined,
3029
+ defaults: Readonly<{
3030
+ fontSize: number;
3031
+ lineHeight: number;
3032
+ weight: number;
3033
+ alignment: 'start' | 'center' | 'end';
3034
+ }>
3035
+ ) {
3036
+ element.style.fontSize = String(style?.fontSize ?? defaults.fontSize) + 'px';
3037
+ element.style.lineHeight =
3038
+ String(style?.lineHeight ?? defaults.lineHeight) + 'px';
3039
+ element.style.fontWeight = String(
3040
+ marketFontWeight(style?.fontWeight, defaults.weight)
3041
+ );
3042
+ element.style.color = style?.color ?? '';
3043
+ element.style.textAlign = style?.alignment ?? defaults.alignment;
3044
+ element.style.whiteSpace = style?.lines === 2 ? 'normal' : 'nowrap';
3045
+ element.style.display = '';
3046
+ element.style.removeProperty('-webkit-line-clamp');
3047
+ element.style.removeProperty('-webkit-box-orient');
3048
+ if (style?.lines === 2) {
3049
+ element.style.display = '-webkit-box';
3050
+ element.style.setProperty('-webkit-line-clamp', '2');
3051
+ element.style.setProperty('-webkit-box-orient', 'vertical');
3052
+ }
3053
+ }
3054
+
3055
+ function setMarketQuoteContent(element: HTMLElement, row: MarketRow) {
3056
+ const style = row.style;
3057
+ const layoutStyle = resolveWebMarketLayoutStyle(row);
3058
+ const price = element.querySelector<HTMLElement>(
3059
+ '.ok-native-list-market-price'
3060
+ );
3061
+ if (price) {
3062
+ price.textContent = row.price;
3063
+ applyMarketTextStyle(price, style?.price, {
3064
+ fontSize: 16,
3065
+ lineHeight: 24,
3066
+ weight: 500,
3067
+ alignment: 'end',
3068
+ });
3069
+ applyValueSegments(
3070
+ price,
3071
+ row.priceSegments,
3072
+ style?.price?.fontSize ?? 16,
3073
+ style?.price?.lineHeight ?? 24,
3074
+ marketFontWeight(style?.price?.fontWeight, 500)
3075
+ );
3076
+ }
3077
+ const change = element.querySelector<HTMLElement>(
3078
+ '.ok-native-list-market-change'
3079
+ );
3080
+ if (change) {
3081
+ change.textContent = row.change.text;
3082
+ setData(change, 'tone', row.change.tone);
3083
+ applyMarketTextStyle(change, style?.change, {
3084
+ fontSize: 14,
3085
+ lineHeight: 20,
3086
+ weight: 500,
3087
+ alignment: 'center',
3088
+ });
3089
+ applyValueSegments(
3090
+ change,
3091
+ row.change.textSegments,
3092
+ style?.change?.fontSize ?? 14,
3093
+ style?.change?.lineHeight ?? 20,
3094
+ marketFontWeight(style?.change?.fontWeight, 500)
3095
+ );
3096
+ change.style.width = String(layoutStyle.changeWidth) + 'px';
3097
+ change.style.height = String(layoutStyle.changeHeight) + 'px';
3098
+ change.style.borderRadius = String(layoutStyle.changeCornerRadius) + 'px';
3099
+ change.style.color = row.change.textColor ?? style?.change?.color ?? '';
3100
+ change.style.background = row.change.backgroundColor ?? '';
3101
+ }
3102
+ element.setAttribute(
3103
+ 'aria-label',
3104
+ row.accessibilityLabel ??
3105
+ [row.title, row.subtitle, row.price, row.change.text]
3106
+ .filter(Boolean)
3107
+ .join(', ')
3108
+ );
3109
+ }
3110
+
3111
+ function createMarketRow(context: RenderContext, row: MarketRow): HTMLElement {
3112
+ const body = createElement(
3113
+ context.document,
3114
+ 'div',
3115
+ 'ok-native-list-row ok-native-list-market'
3116
+ );
3117
+ setData(body, 'variant', row.variant);
3118
+ const style = row.style;
3119
+ const layoutStyle = resolveWebMarketLayoutStyle(row);
3120
+ body.style.padding =
3121
+ String(layoutStyle.verticalPadding) +
3122
+ 'px ' +
3123
+ String(layoutStyle.horizontalPadding) +
3124
+ 'px';
3125
+ body.style.gap = '0px';
3126
+ const visual = createVisual(context, row.leading);
3127
+ if (visual) {
3128
+ const width = layoutStyle.imageWidth;
3129
+ const height = layoutStyle.imageHeight;
3130
+ visual.style.width = String(width) + 'px';
3131
+ visual.style.height = String(height) + 'px';
3132
+ visual.style.flexBasis = String(width) + 'px';
3133
+ visual.style.borderRadius = String(layoutStyle.imageCornerRadius) + 'px';
3134
+ const borderColor =
3135
+ 'borderColor' in row.leading ? row.leading.borderColor : undefined;
3136
+ if (borderColor) {
3137
+ visual.style.border = '1px solid ' + borderColor;
3138
+ visual.style.boxSizing = 'border-box';
3139
+ }
3140
+ visual.querySelectorAll<HTMLElement>('img').forEach((image) => {
3141
+ if (!image.classList.contains('ok-native-list-visual-corner')) {
3142
+ image.style.width = '100%';
3143
+ image.style.height = '100%';
3144
+ if (borderColor) {
3145
+ image.style.borderRadius = '0';
3146
+ image.style.clipPath =
3147
+ 'inset(-1px round ' + String(layoutStyle.imageCornerRadius) + 'px)';
3148
+ }
3149
+ image.style.objectFit =
3150
+ style?.image?.contentFit ?? image.style.objectFit;
3151
+ } else {
3152
+ image.style.width = '20px';
3153
+ image.style.height = '20px';
3154
+ if (borderColor) {
3155
+ image.style.right = '-5px';
3156
+ image.style.bottom = '-5px';
3157
+ }
3158
+ }
3159
+ });
3160
+ visual.style.marginRight = String(layoutStyle.leadingGap) + 'px';
3161
+ body.appendChild(visual);
3162
+ }
3163
+ const main = createElement(
3164
+ context.document,
3165
+ 'span',
3166
+ 'ok-native-list-market-main'
3167
+ );
3168
+ main.style.gap = String(style?.lineGap ?? 0) + 'px';
3169
+ const titleLine = createElement(
3170
+ context.document,
3171
+ 'span',
3172
+ 'ok-native-list-market-title-line'
3173
+ );
3174
+ titleLine.style.gap = String(layoutStyle.titleBadgeGap) + 'px';
3175
+ const title = createElement(
3176
+ context.document,
3177
+ 'span',
3178
+ 'ok-native-list-market-title',
3179
+ row.title
3180
+ );
3181
+ applyMarketTextStyle(title, style?.title, {
3182
+ fontSize: 16,
3183
+ lineHeight: 24,
3184
+ weight: 500,
3185
+ alignment: 'start',
3186
+ });
3187
+ titleLine.appendChild(title);
3188
+ row.badges?.forEach((badge, slot) => {
3189
+ const element = createElement(
3190
+ context.document,
3191
+ badge.actionKey ? 'button' : 'span',
3192
+ 'ok-native-list-market-badge',
3193
+ badge.text
3194
+ );
3195
+ if (badge.actionKey) {
3196
+ element.setAttribute('type', 'button');
3197
+ setData(element, 'nativeListAction', badge.actionKey);
3198
+ markActionAnchorSource(element, 'marketBadge', slot);
3199
+ }
3200
+ setData(element, 'tone', badge.tone);
3201
+ element.style.color =
3202
+ badge.textColor ??
3203
+ (badge.tone === 'success'
3204
+ ? 'var(--nl-positive)'
3205
+ : badge.tone === 'danger'
3206
+ ? 'var(--nl-negative)'
3207
+ : badge.tone === 'info'
3208
+ ? 'var(--nl-info)'
3209
+ : badge.tone === 'warning'
3210
+ ? 'var(--nl-primary)'
3211
+ : '');
3212
+ element.style.background =
3213
+ badge.backgroundColor ??
3214
+ ((badge.icon || badge.iconName) && !badge.text ? 'transparent' : '');
3215
+ if ((badge.icon || badge.iconName) && !badge.text) {
3216
+ element.style.padding = '0';
3217
+ }
3218
+ if (badge.accessibilityLabel)
3219
+ element.setAttribute('aria-label', badge.accessibilityLabel);
3220
+ if (badge.icon) {
3221
+ const icon = createImage(context, badge.icon);
3222
+ if (icon) {
3223
+ icon.style.borderRadius = '50%';
3224
+ element.prepend(icon);
3225
+ }
3226
+ }
3227
+ if (badge.iconName === 'verified') {
3228
+ const icon = createElement(
3229
+ context.document,
3230
+ 'span',
3231
+ 'ok-native-list-market-badge-icon'
3232
+ );
3233
+ applySelectorIcon(icon, 'BadgeVerifiedSolid');
3234
+ element.prepend(icon);
3235
+ }
3236
+ titleLine.appendChild(element);
3237
+ });
3238
+ main.appendChild(titleLine);
3239
+ if (row.subtitle || row.subtitleSegments?.length) {
3240
+ const subtitle = createElement(
3241
+ context.document,
3242
+ 'span',
3243
+ 'ok-native-list-market-subtitle',
3244
+ row.subtitle
3245
+ );
3246
+ applyMarketTextStyle(subtitle, style?.subtitle, {
3247
+ fontSize: 14,
3248
+ lineHeight: 20,
3249
+ weight: 400,
3250
+ alignment: 'start',
3251
+ });
3252
+ applyValueSegments(
3253
+ subtitle,
3254
+ row.subtitleSegments,
3255
+ style?.subtitle?.fontSize ?? 14,
3256
+ style?.subtitle?.lineHeight ?? 20,
3257
+ marketFontWeight(style?.subtitle?.fontWeight, 400)
3258
+ );
3259
+ main.appendChild(subtitle);
3260
+ }
3261
+ body.appendChild(main);
3262
+ const trailing = createElement(
3263
+ context.document,
3264
+ 'span',
3265
+ 'ok-native-list-market-trailing'
3266
+ );
3267
+ trailing.style.gap = String(layoutStyle.trailingGap) + 'px';
3268
+ trailing.append(
3269
+ createElement(context.document, 'span', 'ok-native-list-market-price'),
3270
+ createElement(context.document, 'span', 'ok-native-list-market-change')
3271
+ );
3272
+ body.appendChild(trailing);
3273
+ setMarketQuoteContent(body, row);
3274
+ return body;
3275
+ }
3276
+
2787
3277
  function createRowBody(context: RenderContext, row: RowModel): HTMLElement {
2788
3278
  switch (row.type) {
2789
3279
  case 'walletGroup':
@@ -2802,6 +3292,8 @@ function createRowBody(context: RenderContext, row: RowModel): HTMLElement {
2802
3292
  return createMetricRow(context, row);
2803
3293
  case 'dataRow':
2804
3294
  return createDataRow(context, row);
3295
+ case 'market':
3296
+ return createMarketRow(context, row);
2805
3297
  case 'identity':
2806
3298
  case 'activity':
2807
3299
  case 'message':
@@ -2818,7 +3310,6 @@ export class NativeListWebEngine {
2818
3310
  private readonly footer: HTMLElement;
2819
3311
  private readonly sticky: HTMLElement;
2820
3312
  private readonly indexRail: HTMLElement;
2821
- private readonly indexPreview: HTMLElement;
2822
3313
  private readonly refreshIndicator: HTMLElement;
2823
3314
  private readonly reorderPreview: HTMLElement;
2824
3315
  private readonly previousHostPosition: string;
@@ -2844,7 +3335,6 @@ export class NativeListWebEngine {
2844
3335
  private avatarDirection = 1;
2845
3336
  private reachedGeneration: number | undefined;
2846
3337
  private stickyKey: string | undefined;
2847
- private previewTimer: number | undefined;
2848
3338
  private sectionIndexEntries: readonly Readonly<{
2849
3339
  key: string;
2850
3340
  title: string;
@@ -2869,6 +3359,16 @@ export class NativeListWebEngine {
2869
3359
  );
2870
3360
  private actionAnchorCounter = 0;
2871
3361
  private bindingEpochCounter = 0;
3362
+ private marketPointer:
3363
+ | Readonly<{
3364
+ pointerId: number;
3365
+ rowKey: string;
3366
+ startX: number;
3367
+ startY: number;
3368
+ timer: number;
3369
+ }>
3370
+ | undefined;
3371
+ private marketLongPressFired = false;
2872
3372
  private lastViewportWidth = -1;
2873
3373
  private lastViewportHeight = -1;
2874
3374
  private destroyed = false;
@@ -2923,12 +3423,6 @@ export class NativeListWebEngine {
2923
3423
  this.indexRail.setAttribute('role', 'navigation');
2924
3424
  this.indexRail.setAttribute('aria-label', 'Section index');
2925
3425
  this.indexRail.hidden = true;
2926
- this.indexPreview = createElement(
2927
- this.document,
2928
- 'div',
2929
- 'ok-native-list-index-preview'
2930
- );
2931
- this.indexPreview.setAttribute('aria-live', 'polite');
2932
3426
  this.refreshIndicator = createElement(
2933
3427
  this.document,
2934
3428
  'div',
@@ -2949,7 +3443,6 @@ export class NativeListWebEngine {
2949
3443
  this.viewport,
2950
3444
  this.sticky,
2951
3445
  this.indexRail,
2952
- this.indexPreview,
2953
3446
  this.refreshIndicator
2954
3447
  );
2955
3448
  this.root.append(style, this.viewportFrame, this.footer);
@@ -2958,7 +3451,15 @@ export class NativeListWebEngine {
2958
3451
  this.viewport.addEventListener('scroll', this.handleScroll, {
2959
3452
  passive: true,
2960
3453
  });
3454
+ this.viewport.addEventListener(
3455
+ 'ok-collapsible-pager-insets-changed',
3456
+ this.handleCollapsiblePagerInsetsChanged
3457
+ );
2961
3458
  this.root.addEventListener('click', this.handleClick);
3459
+ this.root.addEventListener('pointerdown', this.handleMarketPointerDown);
3460
+ this.root.addEventListener('pointermove', this.handleMarketPointerMove);
3461
+ this.root.addEventListener('pointerup', this.handleMarketPointerEnd);
3462
+ this.root.addEventListener('pointercancel', this.handleMarketPointerEnd);
2962
3463
  // OneKey patch: preserve web tooltip hover for section titles.
2963
3464
  this.root.addEventListener('pointerover', this.handleTitlePointerOver);
2964
3465
  this.root.addEventListener('pointerout', this.handleTitlePointerOut);
@@ -2990,6 +3491,11 @@ export class NativeListWebEngine {
2990
3491
  );
2991
3492
  this.indexRail.addEventListener('pointerdown', this.handleIndexPointer);
2992
3493
  this.indexRail.addEventListener('pointermove', this.handleIndexPointer);
3494
+ this.indexRail.addEventListener('pointerup', this.handleIndexPointerEnd);
3495
+ this.indexRail.addEventListener(
3496
+ 'pointercancel',
3497
+ this.handleIndexPointerEnd
3498
+ );
2993
3499
  this.indexRail.addEventListener('click', this.handleIndexClick);
2994
3500
  this.viewport.addEventListener('pointerdown', this.handlePullStart, {
2995
3501
  passive: true,
@@ -3034,7 +3540,6 @@ export class NativeListWebEngine {
3034
3540
  applyPatches(patches: readonly RowPatch[]) {
3035
3541
  if (patches.length === 0) return;
3036
3542
  const next = applyRowPatches(this.snapshot, patches);
3037
- this.invalidateActionAnchor('snapshot');
3038
3543
  const selection = new Set(this.selectedKeys);
3039
3544
  patches.forEach((patch) => {
3040
3545
  const selected =
@@ -3042,6 +3547,29 @@ export class NativeListWebEngine {
3042
3547
  if (selected === true) selection.add(patch.key);
3043
3548
  else if (selected === false) selection.delete(patch.key);
3044
3549
  });
3550
+ if (webMarketImageBindDeltaForPatches(patches) === 0) {
3551
+ this.snapshot = next;
3552
+ this.rows = effectiveRows(next);
3553
+ this.selectedKeys = selection;
3554
+ patches.forEach((patch) => {
3555
+ const index = this.rows.findIndex((row) => row.key === patch.key);
3556
+ const row = this.rows[index];
3557
+ const element = this.mounted.get(index);
3558
+ if (row?.type === 'market' && element) {
3559
+ setMarketQuoteContent(element, row);
3560
+ element.dataset.renderSignature = webRowRenderSignature(row);
3561
+ }
3562
+ if (
3563
+ row?.type === 'market' &&
3564
+ this.sticky.dataset.nativeListRowKey === row.key
3565
+ ) {
3566
+ setMarketQuoteContent(this.sticky, row);
3567
+ this.sticky.dataset.renderSignature = webRowRenderSignature(row);
3568
+ }
3569
+ });
3570
+ return;
3571
+ }
3572
+ this.invalidateActionAnchor('snapshot');
3045
3573
  this.setSnapshot(next, selection);
3046
3574
  }
3047
3575
 
@@ -3169,8 +3697,6 @@ export class NativeListWebEngine {
3169
3697
  this.invalidateActionAnchor('destroy');
3170
3698
  this.destroyed = true;
3171
3699
  if (this.frameHandle !== undefined) this.cancelFrame(this.frameHandle);
3172
- if (this.previewTimer !== undefined)
3173
- this.document.defaultView?.clearTimeout(this.previewTimer);
3174
3700
  if (this.reorderMovementTimer !== undefined)
3175
3701
  this.document.defaultView?.clearTimeout(this.reorderMovementTimer);
3176
3702
  this.resizeObserver?.disconnect();
@@ -3179,7 +3705,16 @@ export class NativeListWebEngine {
3179
3705
  this.handleWindowResize
3180
3706
  );
3181
3707
  this.viewport.removeEventListener('scroll', this.handleScroll);
3708
+ this.viewport.removeEventListener(
3709
+ 'ok-collapsible-pager-insets-changed',
3710
+ this.handleCollapsiblePagerInsetsChanged
3711
+ );
3182
3712
  this.root.removeEventListener('click', this.handleClick);
3713
+ this.cancelMarketPointer();
3714
+ this.root.removeEventListener('pointerdown', this.handleMarketPointerDown);
3715
+ this.root.removeEventListener('pointermove', this.handleMarketPointerMove);
3716
+ this.root.removeEventListener('pointerup', this.handleMarketPointerEnd);
3717
+ this.root.removeEventListener('pointercancel', this.handleMarketPointerEnd);
3183
3718
  this.root.removeEventListener('pointerover', this.handleTitlePointerOver);
3184
3719
  this.root.removeEventListener('pointerout', this.handleTitlePointerOut);
3185
3720
  this.root.removeEventListener('keydown', this.handleKeyDown);
@@ -3208,6 +3743,11 @@ export class NativeListWebEngine {
3208
3743
  );
3209
3744
  this.indexRail.removeEventListener('pointerdown', this.handleIndexPointer);
3210
3745
  this.indexRail.removeEventListener('pointermove', this.handleIndexPointer);
3746
+ this.indexRail.removeEventListener('pointerup', this.handleIndexPointerEnd);
3747
+ this.indexRail.removeEventListener(
3748
+ 'pointercancel',
3749
+ this.handleIndexPointerEnd
3750
+ );
3211
3751
  this.indexRail.removeEventListener('click', this.handleIndexClick);
3212
3752
  this.viewport.removeEventListener('pointerdown', this.handlePullStart);
3213
3753
  this.viewport.removeEventListener('pointermove', this.handlePullMove);
@@ -3230,6 +3770,7 @@ export class NativeListWebEngine {
3230
3770
  snapshot: NativeListSnapshot,
3231
3771
  selectedKeys?: ReadonlySet<string>
3232
3772
  ) {
3773
+ this.cancelMarketPointer();
3233
3774
  this.measuredWarningHeights.clear();
3234
3775
  this.snapshot = snapshot;
3235
3776
  this.rows = effectiveRows(snapshot);
@@ -3281,7 +3822,20 @@ export class NativeListWebEngine {
3281
3822
  private recomputeLayout = () => {
3282
3823
  if (this.destroyed) return;
3283
3824
  const viewportWidth = this.viewport.clientWidth;
3284
- const viewportHeight = this.viewport.clientHeight;
3825
+ const viewportHeight =
3826
+ this.snapshot.layout.orientation === 'horizontal'
3827
+ ? this.viewport.clientHeight
3828
+ : this.verticalScrollMetrics().viewportLength;
3829
+ if (this.snapshot.layout.orientation !== 'horizontal') {
3830
+ const stickyInset = this.collapsiblePagerInsets().sticky;
3831
+ this.sticky.style.top = String(stickyInset) + 'px';
3832
+ this.indexRail.style.top = String(stickyInset) + 'px';
3833
+ this.refreshIndicator.style.top = String(stickyInset + 8) + 'px';
3834
+ } else {
3835
+ this.sticky.style.top = '0px';
3836
+ this.indexRail.style.top = '0px';
3837
+ this.refreshIndicator.style.top = '8px';
3838
+ }
3285
3839
  if (
3286
3840
  this.lastViewportWidth >= 0 &&
3287
3841
  (viewportWidth !== this.lastViewportWidth ||
@@ -3312,7 +3866,11 @@ export class NativeListWebEngine {
3312
3866
  );
3313
3867
  this.content.style.width = String(this.layout.contentWidth) + 'px';
3314
3868
  this.content.style.height = String(this.layout.contentHeight) + 'px';
3315
- this.renderSectionIndex(viewportHeight || DEFAULT_VIEWPORT_HEIGHT);
3869
+ this.renderSectionIndex(
3870
+ this.viewport.clientHeight <= 0
3871
+ ? DEFAULT_VIEWPORT_HEIGHT
3872
+ : Math.max(1, viewportHeight)
3873
+ );
3316
3874
  if (previousHorizontal !== this.layout.horizontal) {
3317
3875
  this.viewport.scrollLeft = 0;
3318
3876
  this.viewport.scrollTop = 0;
@@ -3580,6 +4138,17 @@ export class NativeListWebEngine {
3580
4138
  }
3581
4139
  }
3582
4140
  element.replaceChildren(body);
4141
+ if (
4142
+ row.type === 'market' &&
4143
+ row.diagnostics?.imageBindActionKey &&
4144
+ body.querySelector('img')
4145
+ ) {
4146
+ this.callbacks.onRowAction?.({
4147
+ rowKey: row.key,
4148
+ actionKey: row.diagnostics.imageBindActionKey,
4149
+ sectionKey: row.sectionKey,
4150
+ });
4151
+ }
3583
4152
  }
3584
4153
 
3585
4154
  private renderFooter() {
@@ -3609,17 +4178,15 @@ export class NativeListWebEngine {
3609
4178
  ): readonly number[] {
3610
4179
  const entryCount = this.sectionIndexEntries.length;
3611
4180
  if (entryCount <= 1) return entryCount ? [0] : [];
3612
- const availableHeight = Math.max(
3613
- 0,
3614
- viewportHeight - SECTION_INDEX_EDGE_PADDING * 2
3615
- );
4181
+ const { trackHeight } = this.sectionIndexMetrics(viewportHeight);
3616
4182
  const maxVisible = Math.max(
3617
- 2,
3618
- Math.floor(availableHeight / SECTION_INDEX_MIN_LABEL_SPACING) + 1
4183
+ 1,
4184
+ Math.floor(trackHeight / SECTION_INDEX_LABEL_SPACING)
3619
4185
  );
3620
4186
  if (entryCount <= maxVisible) {
3621
4187
  return Array.from({ length: entryCount }, (_, index) => index);
3622
4188
  }
4189
+ if (maxVisible === 1) return [0];
3623
4190
  const result = new Set<number>();
3624
4191
  for (let slot = 0; slot < maxVisible; slot += 1) {
3625
4192
  result.add(Math.round((slot * (entryCount - 1)) / (maxVisible - 1)));
@@ -3627,6 +4194,21 @@ export class NativeListWebEngine {
3627
4194
  return [...result].sort((left, right) => left - right);
3628
4195
  }
3629
4196
 
4197
+ private sectionIndexMetrics(viewportHeight: number) {
4198
+ const availableHeight = Math.max(
4199
+ 0,
4200
+ viewportHeight - SECTION_INDEX_EDGE_PADDING * 2
4201
+ );
4202
+ const trackHeight = Math.min(
4203
+ availableHeight,
4204
+ SECTION_INDEX_LABEL_SPACING * this.sectionIndexEntries.length
4205
+ );
4206
+ return {
4207
+ originY: (viewportHeight - trackHeight) / 2,
4208
+ trackHeight,
4209
+ };
4210
+ }
4211
+
3630
4212
  private renderSectionIndex(viewportHeight: number) {
3631
4213
  this.indexRail.replaceChildren();
3632
4214
  if (!sectionIndexEnabled(this.snapshot)) {
@@ -3653,8 +4235,15 @@ export class NativeListWebEngine {
3653
4235
  'compact',
3654
4236
  visibleEntryIndices.length < this.sectionIndexEntries.length
3655
4237
  );
4238
+ const metrics = this.sectionIndexMetrics(viewportHeight);
4239
+ const visibleTrackHeight = Math.min(
4240
+ metrics.trackHeight,
4241
+ SECTION_INDEX_LABEL_SPACING * visibleEntryIndices.length
4242
+ );
4243
+ const visibleOriginY =
4244
+ metrics.originY + (metrics.trackHeight - visibleTrackHeight) / 2;
3656
4245
  const fragment = this.document.createDocumentFragment();
3657
- visibleEntryIndices.forEach((entryIndex) => {
4246
+ visibleEntryIndices.forEach((entryIndex, visibleIndex) => {
3658
4247
  const entry = this.sectionIndexEntries[entryIndex];
3659
4248
  if (!entry) return;
3660
4249
  const button = createElement(
@@ -3668,14 +4257,11 @@ export class NativeListWebEngine {
3668
4257
  setData(button, 'sectionEntryIndex', entryIndex);
3669
4258
  setData(button, 'sectionPosition', entry.position);
3670
4259
  setData(button, 'sectionKey', entry.key);
3671
- const progress =
3672
- this.sectionIndexEntries.length === 1
3673
- ? 0.5
3674
- : entryIndex / (this.sectionIndexEntries.length - 1);
3675
4260
  button.style.top =
3676
4261
  String(
3677
- SECTION_INDEX_EDGE_PADDING +
3678
- progress * (viewportHeight - SECTION_INDEX_EDGE_PADDING * 2)
4262
+ visibleOriginY +
4263
+ (visibleTrackHeight * (visibleIndex + 0.5)) /
4264
+ visibleEntryIndices.length
3679
4265
  ) + 'px';
3680
4266
  fragment.appendChild(button);
3681
4267
  });
@@ -3811,7 +4397,7 @@ export class NativeListWebEngine {
3811
4397
  }
3812
4398
  const next = this.layout.items[nextIndex];
3813
4399
  const translate = next
3814
- ? Math.min(0, next.y - this.viewport.scrollTop - item.height)
4400
+ ? Math.min(0, next.y - this.currentOffset() - item.height)
3815
4401
  : 0;
3816
4402
  this.sticky.style.transform =
3817
4403
  'translate3d(0,' + String(translate) + 'px,0)';
@@ -3887,9 +4473,11 @@ export class NativeListWebEngine {
3887
4473
  }
3888
4474
 
3889
4475
  private viewportLength(): number {
3890
- return this.layout.horizontal
3891
- ? this.viewport.clientWidth || DEFAULT_VIEWPORT_WIDTH
3892
- : this.viewport.clientHeight || DEFAULT_VIEWPORT_HEIGHT;
4476
+ if (this.layout.horizontal) {
4477
+ return this.viewport.clientWidth || DEFAULT_VIEWPORT_WIDTH;
4478
+ }
4479
+ if (this.viewport.clientHeight <= 0) return DEFAULT_VIEWPORT_HEIGHT;
4480
+ return Math.max(1, this.verticalScrollMetrics().viewportLength);
3893
4481
  }
3894
4482
 
3895
4483
  private contentLength(): number {
@@ -3901,18 +4489,54 @@ export class NativeListWebEngine {
3901
4489
  private currentOffset(): number {
3902
4490
  return this.layout.horizontal
3903
4491
  ? this.viewport.scrollLeft
3904
- : this.viewport.scrollTop;
4492
+ : this.verticalScrollMetrics().offset;
4493
+ }
4494
+
4495
+ private collapsiblePagerInsets(): Readonly<{
4496
+ header: number;
4497
+ sticky: number;
4498
+ }> {
4499
+ const readInset = (name: string) => {
4500
+ const parsed = Number.parseFloat(
4501
+ this.viewport.style.getPropertyValue(name)
4502
+ );
4503
+ return Number.isFinite(parsed) ? Math.max(0, parsed) : 0;
4504
+ };
4505
+ return {
4506
+ header: readInset('--ok-collapsible-pager-header-inset'),
4507
+ sticky: readInset('--ok-collapsible-pager-sticky-inset'),
4508
+ };
4509
+ }
4510
+
4511
+ private verticalScrollMetrics(): WebCollapsiblePagerScrollMetrics {
4512
+ const insets = this.collapsiblePagerInsets();
4513
+ return resolveWebCollapsiblePagerScrollMetrics(
4514
+ this.viewport.scrollTop,
4515
+ this.viewport.clientHeight,
4516
+ insets.header,
4517
+ insets.sticky
4518
+ );
3905
4519
  }
3906
4520
 
4521
+ private handleCollapsiblePagerInsetsChanged = () => {
4522
+ this.recomputeLayout();
4523
+ };
4524
+
3907
4525
  private scrollToAbsoluteOffset(offset: number, animated: boolean) {
3908
4526
  const resolved = Math.min(
3909
4527
  Math.max(0, offset),
3910
4528
  Math.max(0, this.contentLength() - this.viewportLength())
3911
4529
  );
4530
+ const rawOffset = this.layout.horizontal
4531
+ ? resolved
4532
+ : resolveWebCollapsiblePagerRawOffset(
4533
+ resolved,
4534
+ this.collapsiblePagerInsets().header
4535
+ );
3912
4536
  this.viewport.scrollTo(
3913
4537
  this.layout.horizontal
3914
- ? { left: resolved, top: 0, behavior: animated ? 'smooth' : 'auto' }
3915
- : { left: 0, top: resolved, behavior: animated ? 'smooth' : 'auto' }
4538
+ ? { left: rawOffset, top: 0, behavior: animated ? 'smooth' : 'auto' }
4539
+ : { left: 0, top: rawOffset, behavior: animated ? 'smooth' : 'auto' }
3916
4540
  );
3917
4541
  this.scheduleFrame();
3918
4542
  }
@@ -4102,6 +4726,8 @@ export class NativeListWebEngine {
4102
4726
  ? row.actionKey
4103
4727
  : row.type === 'system' && row.variant === 'retry'
4104
4728
  ? row.actionKey
4729
+ : row.type === 'market' && row.pressActionKey
4730
+ ? row.pressActionKey
4105
4731
  : 'press';
4106
4732
  if (rowElement && sourceElement) {
4107
4733
  const anchor = this.createActionAnchor(sourceElement, rowElement, 'row');
@@ -4116,6 +4742,64 @@ export class NativeListWebEngine {
4116
4742
  }
4117
4743
  }
4118
4744
 
4745
+ private cancelMarketPointer() {
4746
+ const state = this.marketPointer;
4747
+ if (!state) return;
4748
+ this.document.defaultView?.clearTimeout(state.timer);
4749
+ this.marketPointer = undefined;
4750
+ }
4751
+
4752
+ private handleMarketPointerDown = (event: PointerEvent) => {
4753
+ if (event.button !== 0 || !(event.target instanceof Element)) return;
4754
+ this.marketLongPressFired = false;
4755
+ if (event.target.closest('[data-native-list-action]')) return;
4756
+ const rowElement = event.target.closest<HTMLElement>(
4757
+ '[data-native-list-row-key]'
4758
+ );
4759
+ const row = this.rowAtElement(rowElement);
4760
+ if (row?.type !== 'market' || row.disabled) return;
4761
+ this.cancelMarketPointer();
4762
+ if (row.pressInActionKey)
4763
+ this.emitRowAction(
4764
+ row,
4765
+ row.pressInActionKey,
4766
+ rowElement ?? undefined,
4767
+ rowElement ?? undefined
4768
+ );
4769
+ if (!row.longPressActionKey || !rowElement) return;
4770
+ markActionAnchorSource(rowElement, 'row');
4771
+ const timer = this.document.defaultView?.setTimeout(() => {
4772
+ const current = this.marketPointer;
4773
+ if (!current || current.pointerId !== event.pointerId) return;
4774
+ this.marketPointer = undefined;
4775
+ this.marketLongPressFired = true;
4776
+ this.emitRowAction(row, row.longPressActionKey!, rowElement, rowElement);
4777
+ }, MARKET_LONG_PRESS_MS);
4778
+ if (timer === undefined) return;
4779
+ this.marketPointer = {
4780
+ pointerId: event.pointerId,
4781
+ rowKey: row.key,
4782
+ startX: event.clientX,
4783
+ startY: event.clientY,
4784
+ timer,
4785
+ };
4786
+ };
4787
+
4788
+ private handleMarketPointerMove = (event: PointerEvent) => {
4789
+ const state = this.marketPointer;
4790
+ if (!state || state.pointerId !== event.pointerId) return;
4791
+ if (
4792
+ Math.abs(event.clientX - state.startX) > MARKET_MOVE_CANCEL_PX ||
4793
+ Math.abs(event.clientY - state.startY) > MARKET_MOVE_CANCEL_PX
4794
+ )
4795
+ this.cancelMarketPointer();
4796
+ };
4797
+
4798
+ private handleMarketPointerEnd = (event: PointerEvent) => {
4799
+ if (this.marketPointer?.pointerId === event.pointerId)
4800
+ this.cancelMarketPointer();
4801
+ };
4802
+
4119
4803
  // OneKey patch: hover opens the same anchored help action as native taps.
4120
4804
  private handleTitlePointerOver = (event: PointerEvent) => {
4121
4805
  const target = event.target;
@@ -4167,7 +4851,8 @@ export class NativeListWebEngine {
4167
4851
  };
4168
4852
 
4169
4853
  private handleClick = (event: Event) => {
4170
- if (Date.now() < this.suppressClickUntil) {
4854
+ if (this.marketLongPressFired || Date.now() < this.suppressClickUntil) {
4855
+ this.marketLongPressFired = false;
4171
4856
  event.preventDefault();
4172
4857
  event.stopPropagation();
4173
4858
  return;
@@ -4340,6 +5025,7 @@ export class NativeListWebEngine {
4340
5025
  }
4341
5026
 
4342
5027
  private handleScroll = () => {
5028
+ this.cancelMarketPointer();
4343
5029
  this.invalidateActionAnchor('scroll');
4344
5030
  this.scheduleFrame();
4345
5031
  };
@@ -4375,78 +5061,62 @@ export class NativeListWebEngine {
4375
5061
  else view?.clearTimeout(handle);
4376
5062
  }
4377
5063
 
4378
- private selectIndexPosition(
4379
- position: number,
4380
- title: string,
4381
- previewClientY?: number
4382
- ) {
5064
+ private selectIndexPosition(position: number) {
5065
+ const activeKey = this.sectionIndexEntries.find(
5066
+ (entry) => entry.position === position
5067
+ )?.key;
5068
+ this.indexRail
5069
+ .querySelectorAll<HTMLElement>('[data-section-key]')
5070
+ .forEach((button) =>
5071
+ setData(button, 'active', button.dataset.sectionKey === activeKey)
5072
+ );
4383
5073
  this.scrollToIndex(position, {
4384
5074
  animated: false,
4385
5075
  alignment: 'start',
4386
5076
  viewPosition: 0,
4387
5077
  viewOffset: 0,
4388
5078
  });
4389
- const frame = this.viewportFrame.getBoundingClientRect();
4390
- if (previewClientY !== undefined && frame.height > 0) {
4391
- const previewY = Math.min(
4392
- frame.height - 24,
4393
- Math.max(24, previewClientY - frame.top)
4394
- );
4395
- this.indexPreview.style.top = String(previewY) + 'px';
4396
- } else {
4397
- this.indexPreview.style.top = '50%';
4398
- }
4399
- this.indexPreview.textContent = title;
4400
- setData(this.indexPreview, 'visible', true);
4401
- if (this.previewTimer !== undefined)
4402
- this.document.defaultView?.clearTimeout(this.previewTimer);
4403
- this.previewTimer = this.document.defaultView?.setTimeout(() => {
4404
- setData(this.indexPreview, 'visible', false);
4405
- }, 180);
4406
5079
  }
4407
5080
 
4408
- private sectionIndexEntryAtEvent(event: PointerEvent):
4409
- | Readonly<{
4410
- entry: NativeListWebEngine['sectionIndexEntries'][number];
4411
- previewClientY: number;
4412
- }>
4413
- | undefined {
5081
+ private sectionIndexEntryAtEvent(
5082
+ event: PointerEvent
5083
+ ): NativeListWebEngine['sectionIndexEntries'][number] | undefined {
4414
5084
  const rail = this.indexRail.getBoundingClientRect();
4415
5085
  if (this.sectionIndexEntries.length === 0 || rail.height <= 0) {
4416
5086
  return undefined;
4417
5087
  }
4418
- const availableHeight = Math.max(
4419
- 1,
4420
- rail.height - SECTION_INDEX_EDGE_PADDING * 2
4421
- );
5088
+ const metrics = this.sectionIndexMetrics(rail.height);
5089
+ if (metrics.trackHeight <= 0) return undefined;
4422
5090
  const progress = Math.min(
4423
5091
  1,
4424
5092
  Math.max(
4425
5093
  0,
4426
- (event.clientY - rail.top - SECTION_INDEX_EDGE_PADDING) /
4427
- availableHeight
5094
+ (event.clientY - rail.top - metrics.originY) / metrics.trackHeight
4428
5095
  )
4429
5096
  );
4430
- const entryIndex = Math.round(
4431
- progress * (this.sectionIndexEntries.length - 1)
5097
+ const entryIndex = Math.min(
5098
+ this.sectionIndexEntries.length - 1,
5099
+ Math.floor(progress * this.sectionIndexEntries.length)
4432
5100
  );
4433
- const entry = this.sectionIndexEntries[entryIndex];
4434
- return entry ? { entry, previewClientY: event.clientY } : undefined;
5101
+ return this.sectionIndexEntries[entryIndex];
4435
5102
  }
4436
5103
 
4437
5104
  private handleIndexPointer = (event: PointerEvent) => {
4438
5105
  if (event.type === 'pointermove' && event.buttons === 0) return;
4439
- const selection = this.sectionIndexEntryAtEvent(event);
4440
- if (!selection) return;
5106
+ const entry = this.sectionIndexEntryAtEvent(event);
5107
+ if (!entry) return;
4441
5108
  event.preventDefault();
4442
5109
  if (event.type === 'pointerdown') {
4443
5110
  this.indexRail.setPointerCapture?.(event.pointerId);
4444
5111
  }
4445
- this.selectIndexPosition(
4446
- selection.entry.position,
4447
- selection.entry.title,
4448
- selection.previewClientY
4449
- );
5112
+ this.selectIndexPosition(entry.position);
5113
+ };
5114
+
5115
+ private handleIndexPointerEnd = (event: PointerEvent) => {
5116
+ if (event.type === 'pointerup') {
5117
+ const entry = this.sectionIndexEntryAtEvent(event);
5118
+ if (entry) this.selectIndexPosition(entry.position);
5119
+ }
4450
5120
  };
4451
5121
 
4452
5122
  private handleIndexClick = (event: Event) => {
@@ -4463,12 +5133,7 @@ export class NativeListWebEngine {
4463
5133
  const entry =
4464
5134
  this.sectionIndexEntries[Number(button.dataset.sectionEntryIndex)];
4465
5135
  if (!entry) return;
4466
- const rect = button.getBoundingClientRect();
4467
- this.selectIndexPosition(
4468
- entry.position,
4469
- entry.title,
4470
- rect.top + rect.height / 2
4471
- );
5136
+ this.selectIndexPosition(entry.position);
4472
5137
  };
4473
5138
 
4474
5139
  private handlePullStart = (event: PointerEvent) => {
@@ -4671,7 +5336,12 @@ export class NativeListWebEngine {
4671
5336
  const maximum = Math.max(0, this.contentLength() - this.viewportLength());
4672
5337
  const next = Math.max(0, Math.min(maximum, offset));
4673
5338
  if (this.layout.horizontal) this.viewport.scrollLeft = next;
4674
- else this.viewport.scrollTop = next;
5339
+ else {
5340
+ this.viewport.scrollTop = resolveWebCollapsiblePagerRawOffset(
5341
+ next,
5342
+ this.collapsiblePagerInsets().header
5343
+ );
5344
+ }
4675
5345
  this.scheduleFrame();
4676
5346
  }
4677
5347
 
@@ -4753,10 +5423,15 @@ export class NativeListWebEngine {
4753
5423
  }
4754
5424
  this.reorderSettlingKey = state.sourceKey;
4755
5425
  const viewportRect = this.viewport.getBoundingClientRect();
5426
+ const insets = this.collapsiblePagerInsets();
4756
5427
  const left =
4757
5428
  viewportRect.left + destinationLayout.x - this.viewport.scrollLeft;
4758
5429
  const top =
4759
- viewportRect.top + destinationLayout.y - this.viewport.scrollTop;
5430
+ viewportRect.top +
5431
+ destinationLayout.y +
5432
+ insets.header +
5433
+ insets.sticky -
5434
+ this.viewport.scrollTop;
4760
5435
  this.reorderPreview.getBoundingClientRect();
4761
5436
  this.reorderPreview.style.transition =
4762
5437
  'transform ' +