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

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.
@@ -8,7 +8,7 @@ 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_MIN_LABEL_SPACING = 14;
11
+ const SECTION_INDEX_LABEL_SPACING = 16;
12
12
  const SECTION_INDEX_MIN_HEIGHT = 120;
13
13
  const DEFAULT_VIEWPORT_WIDTH = 320;
14
14
  const DEFAULT_VIEWPORT_HEIGHT = 640;
@@ -20,6 +20,9 @@ export const WEB_REORDER_ANIMATION = {
20
20
  dropTimingFunction: 'ease'
21
21
  };
22
22
  const REORDER_TOUCH_LONG_PRESS_MS = 120;
23
+ const MARKET_LONG_PRESS_MS = 800;
24
+ const MARKET_MOVE_CANCEL_PX = 10;
25
+ export const WEB_MARKET_VERIFIED_PATH = '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';
23
26
  const REORDER_MOUSE_MOVE_THRESHOLD_PX = 5;
24
27
  const REORDER_EDGE_RATIO = 0.25;
25
28
  const REORDER_MAX_SPEED_ZONE_RATIO = 0.05;
@@ -50,6 +53,25 @@ const defaultTheme = {
50
53
  inverseText: '#FCFCFC',
51
54
  info: '#0D74CE'
52
55
  };
56
+ /** Resolves every reusable Market geometry field, including legacy defaults. */
57
+ export function resolveWebMarketLayoutStyle(row) {
58
+ const style = row.style;
59
+ const imageWidth = style?.image?.width ?? (row.variant === 'stock' ? 40 : 32);
60
+ const imageHeight = style?.image?.height ?? (row.variant === 'stock' ? 40 : 32);
61
+ return {
62
+ horizontalPadding: style?.horizontalPadding ?? (row.variant === 'perp' ? 16 : 20),
63
+ verticalPadding: style?.verticalPadding ?? 12,
64
+ leadingGap: style?.leadingGap ?? (row.variant === 'perp' ? 8 : 14),
65
+ titleBadgeGap: style?.titleBadgeGap ?? 4,
66
+ trailingGap: style?.trailingGap ?? 8,
67
+ imageWidth,
68
+ imageHeight,
69
+ imageCornerRadius: style?.image?.cornerRadius ?? (style?.image?.shape === 'square' ? 0 : style?.image?.shape === 'rounded' ? 8 : Math.min(imageWidth, imageHeight) / 2),
70
+ changeWidth: style?.changeWidth ?? 80,
71
+ changeHeight: style?.changeHeight ?? 32,
72
+ changeCornerRadius: style?.changeCornerRadius ?? 8
73
+ };
74
+ }
53
75
  export function webActionAnchorPayload(token, rect, source, generation, layoutDirection, slot) {
54
76
  return {
55
77
  token,
@@ -195,7 +217,7 @@ export function estimateWebRowHeight(row, snapshot, availableWidth) {
195
217
  break;
196
218
  }
197
219
  case 'system':
198
- base = row.variant === 'noMatch' || row.variant === 'end' ? 36 : row.variant === 'retry' ? 44 : 56;
220
+ base = row.variant === 'loading' && row.loadingStyle === 'skeleton' ? 56 : row.variant === 'loading' && row.loadingStyle === 'spinner' ? 52 : 'presentation' in row && row.presentation === 'market' ? row.variant === 'loading' ? 68 : 44 : row.variant === 'noMatch' || row.variant === 'end' ? 36 : row.variant === 'retry' ? 44 : 56;
199
221
  break;
200
222
  case 'action':
201
223
  base = row.presentation === 'accountSelector' ? 48 : row.icon ? 60 : 44;
@@ -203,6 +225,12 @@ export function estimateWebRowHeight(row, snapshot, availableWidth) {
203
225
  case 'dataRow':
204
226
  base = row.columns.some(column => column.secondaryText) ? 60 : 56;
205
227
  break;
228
+ case 'market':
229
+ {
230
+ const marketStyle = resolveWebMarketLayoutStyle(row);
231
+ base = Math.max(row.variant === 'stock' ? 72 : 68, marketStyle.imageHeight + marketStyle.verticalPadding * 2);
232
+ break;
233
+ }
206
234
  case 'identity':
207
235
  base = row.tertiary ? 72 : row.subtitle ? 60 : 56;
208
236
  break;
@@ -366,6 +394,17 @@ export function visibleWebLayoutItems(layout, offset, viewportLength, overscan =
366
394
  }
367
395
  return visible;
368
396
  }
397
+ export function resolveWebCollapsiblePagerScrollMetrics(rawOffset, rawViewportLength, headerInset, stickyInset) {
398
+ const header = Math.max(0, headerInset);
399
+ const sticky = Math.max(0, stickyInset);
400
+ return {
401
+ offset: Math.max(0, rawOffset - header),
402
+ viewportLength: Math.max(0, rawViewportLength - sticky)
403
+ };
404
+ }
405
+ export function resolveWebCollapsiblePagerRawOffset(logicalOffset, headerInset) {
406
+ return Math.max(0, logicalOffset) + Math.max(0, headerInset);
407
+ }
369
408
  export function webLayoutItemsForMount(layout, offset, viewportLength, virtualizationEnabled, overscan = 0) {
370
409
  return virtualizationEnabled ? visibleWebLayoutItems(layout, offset, viewportLength, overscan) : layout.items;
371
410
  }
@@ -391,6 +430,15 @@ function rowWithoutSelectionState(row) {
391
430
  export function webRowRenderSignature(row) {
392
431
  return JSON.stringify(rowWithoutSelectionState(row));
393
432
  }
433
+ const MARKET_QUOTE_PATCH_FIELDS = new Set(['revision', 'price', 'priceSegments', 'change', 'accessibilityLabel']);
434
+ export function isWebMarketQuotePatch(patch) {
435
+ return patch.type === 'market' && Object.keys(patch.changes).every(key => MARKET_QUOTE_PATCH_FIELDS.has(key));
436
+ }
437
+
438
+ /** Number of Market image-binding passes caused by one Web patch transaction. */
439
+ export function webMarketImageBindDeltaForPatches(patches) {
440
+ return patches.length > 0 && patches.every(isWebMarketQuotePatch) ? 0 : 1;
441
+ }
394
442
 
395
443
  // OneKey patch: selector names must shrink before the sidebar clips their contents.
396
444
  export const WEB_LIST_CSS = `
@@ -448,11 +496,19 @@ export const WEB_LIST_CSS = `
448
496
  .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}
449
497
  .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)}
450
498
  .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}
499
+ .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)}
500
+ .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}
451
501
  .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)}
452
502
  .ok-native-list-refresh{position:absolute;z-index:7;left:50%;top:8px;display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:999px;background:var(--nl-inverse);color:var(--nl-inverse-text);font-size:12px;opacity:0;transform:translate(-50%,-16px);transition:opacity .15s ease,transform .15s ease;pointer-events:none}.ok-native-list-refresh[data-visible="true"]{opacity:1;transform:translate(-50%,0)}
453
503
  .ok-native-list-warning{height:auto;display:flex;flex-direction:column;align-items:stretch;gap:4px;padding:14px 12px;border-top:1px solid;border-bottom:1px solid;box-sizing:border-box;cursor:default}.ok-native-list-warning-title,.ok-native-list-warning-message{font-size:14px;line-height:20px;white-space:normal;overflow-wrap:anywhere}.ok-native-list-warning-title{font-weight:500;color:var(--nl-primary)}.ok-native-list-warning-message{font-weight:400;color:var(--nl-secondary)}
454
504
  .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}
455
- @media (prefers-reduced-motion:reduce){.ok-native-list-index-preview,.ok-native-list-refresh{transition:none}.ok-native-list-spinner{animation:none}}
505
+ .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}
506
+ @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)}}
507
+ .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}
508
+ .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)}
509
+ .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)}
510
+ .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}
511
+ @media (prefers-reduced-motion:reduce){.ok-native-list-refresh{transition:none}.ok-native-list-spinner{animation:none}}
456
512
  /* OneKey patch: selector controls follow their original semantic colors and geometry. */
457
513
  .ok-native-list-checkbox[data-selector="networkSelector"]{padding:0;border-radius:4px;border-color:var(--nl-checkbox-border,var(--nl-separator));background:var(--nl-checkbox-icon,var(--nl-inverse-text))}
458
514
  .ok-native-list-checkbox[data-selector="networkSelector"]::after{display:none}
@@ -812,6 +868,15 @@ const selectorIcons = {
812
868
  fillRule: 'nonzero',
813
869
  opacity: 1
814
870
  }]
871
+ },
872
+ BadgeVerifiedSolid: {
873
+ viewBox: '0 0 24 24',
874
+ paths: [{
875
+ d: WEB_MARKET_VERIFIED_PATH,
876
+ fill: 'currentColor',
877
+ fillRule: 'evenodd',
878
+ opacity: 1
879
+ }]
815
880
  }
816
881
  };
817
882
  function applySelectorIcon(element, name) {
@@ -859,6 +924,7 @@ function visualFromRow(row) {
859
924
  if (row.type === 'activity') return row.leading;
860
925
  if (row.type === 'message') return row.leading;
861
926
  if (row.type === 'dataRow') return row.leading;
927
+ if (row.type === 'market') return row.leading;
862
928
  if (row.type === 'metricCard') return row.visual;
863
929
  return undefined;
864
930
  }
@@ -1266,6 +1332,69 @@ function createActionRow(context, row) {
1266
1332
  function createSystemRow(context, row) {
1267
1333
  const body = createElement(context.document, 'div', 'ok-native-list-row ok-native-list-system');
1268
1334
  setData(body, 'variant', row.variant);
1335
+ if ('presentation' in row) setData(body, 'nativeListPresentation', row.presentation);
1336
+ if (row.variant === 'loading' && row.loadingStyle === 'skeleton') {
1337
+ body.classList.add('ok-native-list-market-skeleton');
1338
+ const background = resolvedTheme(context.snapshot).background;
1339
+ const rgb = Number.parseInt(background.slice(1, 7), 16);
1340
+ const dark = (rgb >> 16 & 255) * 0.299 + (rgb >> 8 & 255) * 0.587 + (rgb & 255) * 0.114 < 128;
1341
+ body.style.setProperty('--nl-skeleton-base', dark ? '#111111' : '#fafafa');
1342
+ body.style.setProperty('--nl-skeleton-highlight', dark ? '#333333' : '#cdcdcd');
1343
+ const left = createElement(context.document, 'div', 'ok-native-list-skeleton-left');
1344
+ const mark = (width, height, circle = false) => {
1345
+ const element = createElement(context.document, 'span', 'ok-native-list-skeleton-mark');
1346
+ element.style.width = `${width}px`;
1347
+ element.style.height = `${height}px`;
1348
+ if (circle) element.style.borderRadius = '50%';
1349
+ return element;
1350
+ };
1351
+ left.appendChild(mark(32, 32, true));
1352
+ const text = createElement(context.document, 'div', 'ok-native-list-skeleton-text');
1353
+ text.appendChild(mark(80, 16));
1354
+ text.appendChild(mark(60, 12));
1355
+ left.appendChild(text);
1356
+ body.appendChild(left);
1357
+ const right = createElement(context.document, 'div', 'ok-native-list-skeleton-right');
1358
+ right.appendChild(mark(80, 18));
1359
+ right.appendChild(mark(80, 18));
1360
+ body.appendChild(right);
1361
+ return body;
1362
+ }
1363
+ if (row.variant === 'loading' && row.loadingStyle === 'spinner') {
1364
+ body.style.justifyContent = 'center';
1365
+ body.style.padding = '16px';
1366
+ const spinner = createElement(context.document, 'span', 'ok-native-list-market-spinner');
1367
+ spinner.setAttribute('role', 'progressbar');
1368
+ // Same 20px SVG and 750ms rotation as react-native-web ActivityIndicator.
1369
+ spinner.innerHTML = '<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>';
1370
+ body.appendChild(spinner);
1371
+ return body;
1372
+ }
1373
+ if ('presentation' in row && row.presentation === 'market') {
1374
+ if (row.variant === 'noMatch') {
1375
+ body.style.justifyContent = 'center';
1376
+ body.style.padding = '32px';
1377
+ const message = createElement(context.document, 'span', '', row.message);
1378
+ message.style.fontSize = '16px';
1379
+ message.style.lineHeight = '24px';
1380
+ body.appendChild(message);
1381
+ return body;
1382
+ }
1383
+ if (row.variant === 'end') {
1384
+ body.style.justifyContent = 'center';
1385
+ body.style.padding = '16px';
1386
+ body.style.gap = '8px';
1387
+ for (const width of [80, 4, 80]) {
1388
+ const mark = createElement(context.document, 'span', '');
1389
+ mark.style.width = String(width) + 'px';
1390
+ mark.style.height = width === 4 ? '4px' : '1px';
1391
+ mark.style.borderRadius = width === 4 ? '2px' : '0';
1392
+ mark.style.background = 'var(--nl-separator)';
1393
+ body.appendChild(mark);
1394
+ }
1395
+ return body;
1396
+ }
1397
+ }
1269
1398
  if (row.variant === 'warning') {
1270
1399
  body.classList.add('ok-native-list-warning');
1271
1400
  body.style.borderColor = row.borderColor ?? 'var(--nl-separator)';
@@ -1555,6 +1684,163 @@ function createWalletGroupRow(context, row) {
1555
1684
  });
1556
1685
  return body;
1557
1686
  }
1687
+ function marketFontWeight(value, fallback) {
1688
+ if (value === 'bold') return 700;
1689
+ if (value === 'semibold') return 600;
1690
+ if (value === 'medium') return 500;
1691
+ if (value === 'regular') return 400;
1692
+ return fallback;
1693
+ }
1694
+ function applyMarketTextStyle(element, style, defaults) {
1695
+ element.style.fontSize = String(style?.fontSize ?? defaults.fontSize) + 'px';
1696
+ element.style.lineHeight = String(style?.lineHeight ?? defaults.lineHeight) + 'px';
1697
+ element.style.fontWeight = String(marketFontWeight(style?.fontWeight, defaults.weight));
1698
+ element.style.color = style?.color ?? '';
1699
+ element.style.textAlign = style?.alignment ?? defaults.alignment;
1700
+ element.style.whiteSpace = style?.lines === 2 ? 'normal' : 'nowrap';
1701
+ element.style.display = '';
1702
+ element.style.removeProperty('-webkit-line-clamp');
1703
+ element.style.removeProperty('-webkit-box-orient');
1704
+ if (style?.lines === 2) {
1705
+ element.style.display = '-webkit-box';
1706
+ element.style.setProperty('-webkit-line-clamp', '2');
1707
+ element.style.setProperty('-webkit-box-orient', 'vertical');
1708
+ }
1709
+ }
1710
+ function setMarketQuoteContent(element, row) {
1711
+ const style = row.style;
1712
+ const layoutStyle = resolveWebMarketLayoutStyle(row);
1713
+ const price = element.querySelector('.ok-native-list-market-price');
1714
+ if (price) {
1715
+ price.textContent = row.price;
1716
+ applyMarketTextStyle(price, style?.price, {
1717
+ fontSize: 16,
1718
+ lineHeight: 24,
1719
+ weight: 500,
1720
+ alignment: 'end'
1721
+ });
1722
+ applyValueSegments(price, row.priceSegments, style?.price?.fontSize ?? 16, style?.price?.lineHeight ?? 24, marketFontWeight(style?.price?.fontWeight, 500));
1723
+ }
1724
+ const change = element.querySelector('.ok-native-list-market-change');
1725
+ if (change) {
1726
+ change.textContent = row.change.text;
1727
+ setData(change, 'tone', row.change.tone);
1728
+ applyMarketTextStyle(change, style?.change, {
1729
+ fontSize: 14,
1730
+ lineHeight: 20,
1731
+ weight: 500,
1732
+ alignment: 'center'
1733
+ });
1734
+ applyValueSegments(change, row.change.textSegments, style?.change?.fontSize ?? 14, style?.change?.lineHeight ?? 20, marketFontWeight(style?.change?.fontWeight, 500));
1735
+ change.style.width = String(layoutStyle.changeWidth) + 'px';
1736
+ change.style.height = String(layoutStyle.changeHeight) + 'px';
1737
+ change.style.borderRadius = String(layoutStyle.changeCornerRadius) + 'px';
1738
+ change.style.color = row.change.textColor ?? style?.change?.color ?? '';
1739
+ change.style.background = row.change.backgroundColor ?? '';
1740
+ }
1741
+ element.setAttribute('aria-label', row.accessibilityLabel ?? [row.title, row.subtitle, row.price, row.change.text].filter(Boolean).join(', '));
1742
+ }
1743
+ function createMarketRow(context, row) {
1744
+ const body = createElement(context.document, 'div', 'ok-native-list-row ok-native-list-market');
1745
+ setData(body, 'variant', row.variant);
1746
+ const style = row.style;
1747
+ const layoutStyle = resolveWebMarketLayoutStyle(row);
1748
+ body.style.padding = String(layoutStyle.verticalPadding) + 'px ' + String(layoutStyle.horizontalPadding) + 'px';
1749
+ body.style.gap = '0px';
1750
+ const visual = createVisual(context, row.leading);
1751
+ if (visual) {
1752
+ const width = layoutStyle.imageWidth;
1753
+ const height = layoutStyle.imageHeight;
1754
+ visual.style.width = String(width) + 'px';
1755
+ visual.style.height = String(height) + 'px';
1756
+ visual.style.flexBasis = String(width) + 'px';
1757
+ visual.style.borderRadius = String(layoutStyle.imageCornerRadius) + 'px';
1758
+ const borderColor = 'borderColor' in row.leading ? row.leading.borderColor : undefined;
1759
+ if (borderColor) {
1760
+ visual.style.border = '1px solid ' + borderColor;
1761
+ visual.style.boxSizing = 'border-box';
1762
+ }
1763
+ visual.querySelectorAll('img').forEach(image => {
1764
+ if (!image.classList.contains('ok-native-list-visual-corner')) {
1765
+ image.style.width = '100%';
1766
+ image.style.height = '100%';
1767
+ if (borderColor) {
1768
+ image.style.borderRadius = '0';
1769
+ image.style.clipPath = 'inset(-1px round ' + String(layoutStyle.imageCornerRadius) + 'px)';
1770
+ }
1771
+ image.style.objectFit = style?.image?.contentFit ?? image.style.objectFit;
1772
+ } else {
1773
+ image.style.width = '20px';
1774
+ image.style.height = '20px';
1775
+ if (borderColor) {
1776
+ image.style.right = '-5px';
1777
+ image.style.bottom = '-5px';
1778
+ }
1779
+ }
1780
+ });
1781
+ visual.style.marginRight = String(layoutStyle.leadingGap) + 'px';
1782
+ body.appendChild(visual);
1783
+ }
1784
+ const main = createElement(context.document, 'span', 'ok-native-list-market-main');
1785
+ main.style.gap = String(style?.lineGap ?? 0) + 'px';
1786
+ const titleLine = createElement(context.document, 'span', 'ok-native-list-market-title-line');
1787
+ titleLine.style.gap = String(layoutStyle.titleBadgeGap) + 'px';
1788
+ const title = createElement(context.document, 'span', 'ok-native-list-market-title', row.title);
1789
+ applyMarketTextStyle(title, style?.title, {
1790
+ fontSize: 16,
1791
+ lineHeight: 24,
1792
+ weight: 500,
1793
+ alignment: 'start'
1794
+ });
1795
+ titleLine.appendChild(title);
1796
+ row.badges?.forEach((badge, slot) => {
1797
+ const element = createElement(context.document, badge.actionKey ? 'button' : 'span', 'ok-native-list-market-badge', badge.text);
1798
+ if (badge.actionKey) {
1799
+ element.setAttribute('type', 'button');
1800
+ setData(element, 'nativeListAction', badge.actionKey);
1801
+ markActionAnchorSource(element, 'marketBadge', slot);
1802
+ }
1803
+ setData(element, 'tone', badge.tone);
1804
+ element.style.color = badge.textColor ?? (badge.tone === 'success' ? 'var(--nl-positive)' : badge.tone === 'danger' ? 'var(--nl-negative)' : badge.tone === 'info' ? 'var(--nl-info)' : badge.tone === 'warning' ? 'var(--nl-primary)' : '');
1805
+ element.style.background = badge.backgroundColor ?? ((badge.icon || badge.iconName) && !badge.text ? 'transparent' : '');
1806
+ if ((badge.icon || badge.iconName) && !badge.text) {
1807
+ element.style.padding = '0';
1808
+ }
1809
+ if (badge.accessibilityLabel) element.setAttribute('aria-label', badge.accessibilityLabel);
1810
+ if (badge.icon) {
1811
+ const icon = createImage(context, badge.icon);
1812
+ if (icon) {
1813
+ icon.style.borderRadius = '50%';
1814
+ element.prepend(icon);
1815
+ }
1816
+ }
1817
+ if (badge.iconName === 'verified') {
1818
+ const icon = createElement(context.document, 'span', 'ok-native-list-market-badge-icon');
1819
+ applySelectorIcon(icon, 'BadgeVerifiedSolid');
1820
+ element.prepend(icon);
1821
+ }
1822
+ titleLine.appendChild(element);
1823
+ });
1824
+ main.appendChild(titleLine);
1825
+ if (row.subtitle || row.subtitleSegments?.length) {
1826
+ const subtitle = createElement(context.document, 'span', 'ok-native-list-market-subtitle', row.subtitle);
1827
+ applyMarketTextStyle(subtitle, style?.subtitle, {
1828
+ fontSize: 14,
1829
+ lineHeight: 20,
1830
+ weight: 400,
1831
+ alignment: 'start'
1832
+ });
1833
+ applyValueSegments(subtitle, row.subtitleSegments, style?.subtitle?.fontSize ?? 14, style?.subtitle?.lineHeight ?? 20, marketFontWeight(style?.subtitle?.fontWeight, 400));
1834
+ main.appendChild(subtitle);
1835
+ }
1836
+ body.appendChild(main);
1837
+ const trailing = createElement(context.document, 'span', 'ok-native-list-market-trailing');
1838
+ trailing.style.gap = String(layoutStyle.trailingGap) + 'px';
1839
+ trailing.append(createElement(context.document, 'span', 'ok-native-list-market-price'), createElement(context.document, 'span', 'ok-native-list-market-change'));
1840
+ body.appendChild(trailing);
1841
+ setMarketQuoteContent(body, row);
1842
+ return body;
1843
+ }
1558
1844
  function createRowBody(context, row) {
1559
1845
  switch (row.type) {
1560
1846
  case 'walletGroup':
@@ -1573,6 +1859,8 @@ function createRowBody(context, row) {
1573
1859
  return createMetricRow(context, row);
1574
1860
  case 'dataRow':
1575
1861
  return createDataRow(context, row);
1862
+ case 'market':
1863
+ return createMarketRow(context, row);
1576
1864
  case 'identity':
1577
1865
  case 'activity':
1578
1866
  case 'message':
@@ -1600,6 +1888,7 @@ export class NativeListWebEngine {
1600
1888
  actionAnchorInstanceId = String(++webNativeListInstanceCounter);
1601
1889
  actionAnchorCounter = 0;
1602
1890
  bindingEpochCounter = 0;
1891
+ marketLongPressFired = false;
1603
1892
  lastViewportWidth = -1;
1604
1893
  lastViewportHeight = -1;
1605
1894
  destroyed = false;
@@ -1627,8 +1916,6 @@ export class NativeListWebEngine {
1627
1916
  this.indexRail.setAttribute('role', 'navigation');
1628
1917
  this.indexRail.setAttribute('aria-label', 'Section index');
1629
1918
  this.indexRail.hidden = true;
1630
- this.indexPreview = createElement(this.document, 'div', 'ok-native-list-index-preview');
1631
- this.indexPreview.setAttribute('aria-live', 'polite');
1632
1919
  this.refreshIndicator = createElement(this.document, 'div', 'ok-native-list-refresh', 'Refreshing');
1633
1920
  this.refreshIndicator.setAttribute('role', 'status');
1634
1921
  this.reorderPreview = createElement(this.document, 'div', 'ok-native-list-reorder-preview');
@@ -1636,13 +1923,18 @@ export class NativeListWebEngine {
1636
1923
  this.reorderPreview.setAttribute('aria-hidden', 'true');
1637
1924
  this.document.body.appendChild(this.reorderPreview);
1638
1925
  this.viewport.append(this.content);
1639
- this.viewportFrame.append(this.viewport, this.sticky, this.indexRail, this.indexPreview, this.refreshIndicator);
1926
+ this.viewportFrame.append(this.viewport, this.sticky, this.indexRail, this.refreshIndicator);
1640
1927
  this.root.append(style, this.viewportFrame, this.footer);
1641
1928
  host.appendChild(this.root);
1642
1929
  this.viewport.addEventListener('scroll', this.handleScroll, {
1643
1930
  passive: true
1644
1931
  });
1932
+ this.viewport.addEventListener('ok-collapsible-pager-insets-changed', this.handleCollapsiblePagerInsetsChanged);
1645
1933
  this.root.addEventListener('click', this.handleClick);
1934
+ this.root.addEventListener('pointerdown', this.handleMarketPointerDown);
1935
+ this.root.addEventListener('pointermove', this.handleMarketPointerMove);
1936
+ this.root.addEventListener('pointerup', this.handleMarketPointerEnd);
1937
+ this.root.addEventListener('pointercancel', this.handleMarketPointerEnd);
1646
1938
  // OneKey patch: preserve web tooltip hover for section titles.
1647
1939
  this.root.addEventListener('pointerover', this.handleTitlePointerOver);
1648
1940
  this.root.addEventListener('pointerout', this.handleTitlePointerOut);
@@ -1664,6 +1956,8 @@ export class NativeListWebEngine {
1664
1956
  this.viewport.addEventListener('touchcancel', this.handleReorderTouchCancel);
1665
1957
  this.indexRail.addEventListener('pointerdown', this.handleIndexPointer);
1666
1958
  this.indexRail.addEventListener('pointermove', this.handleIndexPointer);
1959
+ this.indexRail.addEventListener('pointerup', this.handleIndexPointerEnd);
1960
+ this.indexRail.addEventListener('pointercancel', this.handleIndexPointerEnd);
1667
1961
  this.indexRail.addEventListener('click', this.handleIndexClick);
1668
1962
  this.viewport.addEventListener('pointerdown', this.handlePullStart, {
1669
1963
  passive: true
@@ -1698,12 +1992,31 @@ export class NativeListWebEngine {
1698
1992
  applyPatches(patches) {
1699
1993
  if (patches.length === 0) return;
1700
1994
  const next = applyRowPatches(this.snapshot, patches);
1701
- this.invalidateActionAnchor('snapshot');
1702
1995
  const selection = new Set(this.selectedKeys);
1703
1996
  patches.forEach(patch => {
1704
1997
  const selected = 'selected' in patch.changes ? patch.changes.selected : undefined;
1705
1998
  if (selected === true) selection.add(patch.key);else if (selected === false) selection.delete(patch.key);
1706
1999
  });
2000
+ if (webMarketImageBindDeltaForPatches(patches) === 0) {
2001
+ this.snapshot = next;
2002
+ this.rows = effectiveRows(next);
2003
+ this.selectedKeys = selection;
2004
+ patches.forEach(patch => {
2005
+ const index = this.rows.findIndex(row => row.key === patch.key);
2006
+ const row = this.rows[index];
2007
+ const element = this.mounted.get(index);
2008
+ if (row?.type === 'market' && element) {
2009
+ setMarketQuoteContent(element, row);
2010
+ element.dataset.renderSignature = webRowRenderSignature(row);
2011
+ }
2012
+ if (row?.type === 'market' && this.sticky.dataset.nativeListRowKey === row.key) {
2013
+ setMarketQuoteContent(this.sticky, row);
2014
+ this.sticky.dataset.renderSignature = webRowRenderSignature(row);
2015
+ }
2016
+ });
2017
+ return;
2018
+ }
2019
+ this.invalidateActionAnchor('snapshot');
1707
2020
  this.setSnapshot(next, selection);
1708
2021
  }
1709
2022
  reconcileSelection(keys) {
@@ -1821,12 +2134,17 @@ export class NativeListWebEngine {
1821
2134
  this.invalidateActionAnchor('destroy');
1822
2135
  this.destroyed = true;
1823
2136
  if (this.frameHandle !== undefined) this.cancelFrame(this.frameHandle);
1824
- if (this.previewTimer !== undefined) this.document.defaultView?.clearTimeout(this.previewTimer);
1825
2137
  if (this.reorderMovementTimer !== undefined) this.document.defaultView?.clearTimeout(this.reorderMovementTimer);
1826
2138
  this.resizeObserver?.disconnect();
1827
2139
  this.document.defaultView?.removeEventListener('resize', this.handleWindowResize);
1828
2140
  this.viewport.removeEventListener('scroll', this.handleScroll);
2141
+ this.viewport.removeEventListener('ok-collapsible-pager-insets-changed', this.handleCollapsiblePagerInsetsChanged);
1829
2142
  this.root.removeEventListener('click', this.handleClick);
2143
+ this.cancelMarketPointer();
2144
+ this.root.removeEventListener('pointerdown', this.handleMarketPointerDown);
2145
+ this.root.removeEventListener('pointermove', this.handleMarketPointerMove);
2146
+ this.root.removeEventListener('pointerup', this.handleMarketPointerEnd);
2147
+ this.root.removeEventListener('pointercancel', this.handleMarketPointerEnd);
1830
2148
  this.root.removeEventListener('pointerover', this.handleTitlePointerOver);
1831
2149
  this.root.removeEventListener('pointerout', this.handleTitlePointerOut);
1832
2150
  this.root.removeEventListener('keydown', this.handleKeyDown);
@@ -1840,6 +2158,8 @@ export class NativeListWebEngine {
1840
2158
  this.viewport.removeEventListener('touchcancel', this.handleReorderTouchCancel);
1841
2159
  this.indexRail.removeEventListener('pointerdown', this.handleIndexPointer);
1842
2160
  this.indexRail.removeEventListener('pointermove', this.handleIndexPointer);
2161
+ this.indexRail.removeEventListener('pointerup', this.handleIndexPointerEnd);
2162
+ this.indexRail.removeEventListener('pointercancel', this.handleIndexPointerEnd);
1843
2163
  this.indexRail.removeEventListener('click', this.handleIndexClick);
1844
2164
  this.viewport.removeEventListener('pointerdown', this.handlePullStart);
1845
2165
  this.viewport.removeEventListener('pointermove', this.handlePullMove);
@@ -1858,6 +2178,7 @@ export class NativeListWebEngine {
1858
2178
  this.avatarLeases.clear();
1859
2179
  }
1860
2180
  setSnapshot(snapshot, selectedKeys) {
2181
+ this.cancelMarketPointer();
1861
2182
  this.measuredWarningHeights.clear();
1862
2183
  this.snapshot = snapshot;
1863
2184
  this.rows = effectiveRows(snapshot);
@@ -1905,7 +2226,17 @@ export class NativeListWebEngine {
1905
2226
  recomputeLayout = () => {
1906
2227
  if (this.destroyed) return;
1907
2228
  const viewportWidth = this.viewport.clientWidth;
1908
- const viewportHeight = this.viewport.clientHeight;
2229
+ const viewportHeight = this.snapshot.layout.orientation === 'horizontal' ? this.viewport.clientHeight : this.verticalScrollMetrics().viewportLength;
2230
+ if (this.snapshot.layout.orientation !== 'horizontal') {
2231
+ const stickyInset = this.collapsiblePagerInsets().sticky;
2232
+ this.sticky.style.top = String(stickyInset) + 'px';
2233
+ this.indexRail.style.top = String(stickyInset) + 'px';
2234
+ this.refreshIndicator.style.top = String(stickyInset + 8) + 'px';
2235
+ } else {
2236
+ this.sticky.style.top = '0px';
2237
+ this.indexRail.style.top = '0px';
2238
+ this.refreshIndicator.style.top = '8px';
2239
+ }
1909
2240
  if (this.lastViewportWidth >= 0 && (viewportWidth !== this.lastViewportWidth || viewportHeight !== this.lastViewportHeight)) {
1910
2241
  this.invalidateActionAnchor('layout');
1911
2242
  this.measuredWarningHeights.clear();
@@ -1923,7 +2254,7 @@ export class NativeListWebEngine {
1923
2254
  this.layout = computeWebListLayout(measuredSnapshot, viewportWidth, viewportHeight, this.reorderCompactKey);
1924
2255
  this.content.style.width = String(this.layout.contentWidth) + 'px';
1925
2256
  this.content.style.height = String(this.layout.contentHeight) + 'px';
1926
- this.renderSectionIndex(viewportHeight || DEFAULT_VIEWPORT_HEIGHT);
2257
+ this.renderSectionIndex(this.viewport.clientHeight <= 0 ? DEFAULT_VIEWPORT_HEIGHT : Math.max(1, viewportHeight));
1927
2258
  if (previousHorizontal !== this.layout.horizontal) {
1928
2259
  this.viewport.scrollLeft = 0;
1929
2260
  this.viewport.scrollTop = 0;
@@ -2107,6 +2438,13 @@ export class NativeListWebEngine {
2107
2438
  }
2108
2439
  }
2109
2440
  element.replaceChildren(body);
2441
+ if (row.type === 'market' && row.diagnostics?.imageBindActionKey && body.querySelector('img')) {
2442
+ this.callbacks.onRowAction?.({
2443
+ rowKey: row.key,
2444
+ actionKey: row.diagnostics.imageBindActionKey,
2445
+ sectionKey: row.sectionKey
2446
+ });
2447
+ }
2110
2448
  }
2111
2449
  renderFooter() {
2112
2450
  const row = this.snapshot.fixedFooter;
@@ -2125,19 +2463,30 @@ export class NativeListWebEngine {
2125
2463
  sectionIndexVisibleEntryIndices(viewportHeight) {
2126
2464
  const entryCount = this.sectionIndexEntries.length;
2127
2465
  if (entryCount <= 1) return entryCount ? [0] : [];
2128
- const availableHeight = Math.max(0, viewportHeight - SECTION_INDEX_EDGE_PADDING * 2);
2129
- const maxVisible = Math.max(2, Math.floor(availableHeight / SECTION_INDEX_MIN_LABEL_SPACING) + 1);
2466
+ const {
2467
+ trackHeight
2468
+ } = this.sectionIndexMetrics(viewportHeight);
2469
+ const maxVisible = Math.max(1, Math.floor(trackHeight / SECTION_INDEX_LABEL_SPACING));
2130
2470
  if (entryCount <= maxVisible) {
2131
2471
  return Array.from({
2132
2472
  length: entryCount
2133
2473
  }, (_, index) => index);
2134
2474
  }
2475
+ if (maxVisible === 1) return [0];
2135
2476
  const result = new Set();
2136
2477
  for (let slot = 0; slot < maxVisible; slot += 1) {
2137
2478
  result.add(Math.round(slot * (entryCount - 1) / (maxVisible - 1)));
2138
2479
  }
2139
2480
  return [...result].sort((left, right) => left - right);
2140
2481
  }
2482
+ sectionIndexMetrics(viewportHeight) {
2483
+ const availableHeight = Math.max(0, viewportHeight - SECTION_INDEX_EDGE_PADDING * 2);
2484
+ const trackHeight = Math.min(availableHeight, SECTION_INDEX_LABEL_SPACING * this.sectionIndexEntries.length);
2485
+ return {
2486
+ originY: (viewportHeight - trackHeight) / 2,
2487
+ trackHeight
2488
+ };
2489
+ }
2141
2490
  renderSectionIndex(viewportHeight) {
2142
2491
  this.indexRail.replaceChildren();
2143
2492
  if (!sectionIndexEnabled(this.snapshot)) {
@@ -2156,8 +2505,11 @@ export class NativeListWebEngine {
2156
2505
  }
2157
2506
  const visibleEntryIndices = this.sectionIndexVisibleEntryIndices(viewportHeight);
2158
2507
  setData(this.indexRail, 'compact', visibleEntryIndices.length < this.sectionIndexEntries.length);
2508
+ const metrics = this.sectionIndexMetrics(viewportHeight);
2509
+ const visibleTrackHeight = Math.min(metrics.trackHeight, SECTION_INDEX_LABEL_SPACING * visibleEntryIndices.length);
2510
+ const visibleOriginY = metrics.originY + (metrics.trackHeight - visibleTrackHeight) / 2;
2159
2511
  const fragment = this.document.createDocumentFragment();
2160
- visibleEntryIndices.forEach(entryIndex => {
2512
+ visibleEntryIndices.forEach((entryIndex, visibleIndex) => {
2161
2513
  const entry = this.sectionIndexEntries[entryIndex];
2162
2514
  if (!entry) return;
2163
2515
  const button = createElement(this.document, 'button', 'ok-native-list-index-button', entry.title);
@@ -2166,8 +2518,7 @@ export class NativeListWebEngine {
2166
2518
  setData(button, 'sectionEntryIndex', entryIndex);
2167
2519
  setData(button, 'sectionPosition', entry.position);
2168
2520
  setData(button, 'sectionKey', entry.key);
2169
- const progress = this.sectionIndexEntries.length === 1 ? 0.5 : entryIndex / (this.sectionIndexEntries.length - 1);
2170
- button.style.top = String(SECTION_INDEX_EDGE_PADDING + progress * (viewportHeight - SECTION_INDEX_EDGE_PADDING * 2)) + 'px';
2521
+ button.style.top = String(visibleOriginY + visibleTrackHeight * (visibleIndex + 0.5) / visibleEntryIndices.length) + 'px';
2171
2522
  fragment.appendChild(button);
2172
2523
  });
2173
2524
  this.indexRail.appendChild(fragment);
@@ -2262,7 +2613,7 @@ export class NativeListWebEngine {
2262
2613
  }
2263
2614
  }
2264
2615
  const next = this.layout.items[nextIndex];
2265
- const translate = next ? Math.min(0, next.y - this.viewport.scrollTop - item.height) : 0;
2616
+ const translate = next ? Math.min(0, next.y - this.currentOffset() - item.height) : 0;
2266
2617
  this.sticky.style.transform = 'translate3d(0,' + String(translate) + 'px,0)';
2267
2618
  this.updateVisibleSelection();
2268
2619
  }
@@ -2297,23 +2648,45 @@ export class NativeListWebEngine {
2297
2648
  return viewportLength > 0 && this.layout.items.length > 0;
2298
2649
  }
2299
2650
  viewportLength() {
2300
- return this.layout.horizontal ? this.viewport.clientWidth || DEFAULT_VIEWPORT_WIDTH : this.viewport.clientHeight || DEFAULT_VIEWPORT_HEIGHT;
2651
+ if (this.layout.horizontal) {
2652
+ return this.viewport.clientWidth || DEFAULT_VIEWPORT_WIDTH;
2653
+ }
2654
+ if (this.viewport.clientHeight <= 0) return DEFAULT_VIEWPORT_HEIGHT;
2655
+ return Math.max(1, this.verticalScrollMetrics().viewportLength);
2301
2656
  }
2302
2657
  contentLength() {
2303
2658
  return this.layout.horizontal ? this.layout.contentWidth : this.layout.contentHeight;
2304
2659
  }
2305
2660
  currentOffset() {
2306
- return this.layout.horizontal ? this.viewport.scrollLeft : this.viewport.scrollTop;
2661
+ return this.layout.horizontal ? this.viewport.scrollLeft : this.verticalScrollMetrics().offset;
2307
2662
  }
2663
+ collapsiblePagerInsets() {
2664
+ const readInset = name => {
2665
+ const parsed = Number.parseFloat(this.viewport.style.getPropertyValue(name));
2666
+ return Number.isFinite(parsed) ? Math.max(0, parsed) : 0;
2667
+ };
2668
+ return {
2669
+ header: readInset('--ok-collapsible-pager-header-inset'),
2670
+ sticky: readInset('--ok-collapsible-pager-sticky-inset')
2671
+ };
2672
+ }
2673
+ verticalScrollMetrics() {
2674
+ const insets = this.collapsiblePagerInsets();
2675
+ return resolveWebCollapsiblePagerScrollMetrics(this.viewport.scrollTop, this.viewport.clientHeight, insets.header, insets.sticky);
2676
+ }
2677
+ handleCollapsiblePagerInsetsChanged = () => {
2678
+ this.recomputeLayout();
2679
+ };
2308
2680
  scrollToAbsoluteOffset(offset, animated) {
2309
2681
  const resolved = Math.min(Math.max(0, offset), Math.max(0, this.contentLength() - this.viewportLength()));
2682
+ const rawOffset = this.layout.horizontal ? resolved : resolveWebCollapsiblePagerRawOffset(resolved, this.collapsiblePagerInsets().header);
2310
2683
  this.viewport.scrollTo(this.layout.horizontal ? {
2311
- left: resolved,
2684
+ left: rawOffset,
2312
2685
  top: 0,
2313
2686
  behavior: animated ? 'smooth' : 'auto'
2314
2687
  } : {
2315
2688
  left: 0,
2316
- top: resolved,
2689
+ top: rawOffset,
2317
2690
  behavior: animated ? 'smooth' : 'auto'
2318
2691
  });
2319
2692
  this.scheduleFrame();
@@ -2421,7 +2794,7 @@ export class NativeListWebEngine {
2421
2794
  }, row);
2422
2795
  return;
2423
2796
  }
2424
- const actionKey = row.type === 'action' ? row.actionKey : row.type === 'system' && row.variant === 'retry' ? row.actionKey : 'press';
2797
+ const actionKey = row.type === 'action' ? row.actionKey : row.type === 'system' && row.variant === 'retry' ? row.actionKey : row.type === 'market' && row.pressActionKey ? row.pressActionKey : 'press';
2425
2798
  if (rowElement && sourceElement) {
2426
2799
  const anchor = this.createActionAnchor(sourceElement, rowElement, 'row');
2427
2800
  this.callbacks.onRowAction?.({
@@ -2434,6 +2807,47 @@ export class NativeListWebEngine {
2434
2807
  this.emitRowAction(row, actionKey);
2435
2808
  }
2436
2809
  }
2810
+ cancelMarketPointer() {
2811
+ const state = this.marketPointer;
2812
+ if (!state) return;
2813
+ this.document.defaultView?.clearTimeout(state.timer);
2814
+ this.marketPointer = undefined;
2815
+ }
2816
+ handleMarketPointerDown = event => {
2817
+ if (event.button !== 0 || !(event.target instanceof Element)) return;
2818
+ this.marketLongPressFired = false;
2819
+ if (event.target.closest('[data-native-list-action]')) return;
2820
+ const rowElement = event.target.closest('[data-native-list-row-key]');
2821
+ const row = this.rowAtElement(rowElement);
2822
+ if (row?.type !== 'market' || row.disabled) return;
2823
+ this.cancelMarketPointer();
2824
+ if (row.pressInActionKey) this.emitRowAction(row, row.pressInActionKey, rowElement ?? undefined, rowElement ?? undefined);
2825
+ if (!row.longPressActionKey || !rowElement) return;
2826
+ markActionAnchorSource(rowElement, 'row');
2827
+ const timer = this.document.defaultView?.setTimeout(() => {
2828
+ const current = this.marketPointer;
2829
+ if (!current || current.pointerId !== event.pointerId) return;
2830
+ this.marketPointer = undefined;
2831
+ this.marketLongPressFired = true;
2832
+ this.emitRowAction(row, row.longPressActionKey, rowElement, rowElement);
2833
+ }, MARKET_LONG_PRESS_MS);
2834
+ if (timer === undefined) return;
2835
+ this.marketPointer = {
2836
+ pointerId: event.pointerId,
2837
+ rowKey: row.key,
2838
+ startX: event.clientX,
2839
+ startY: event.clientY,
2840
+ timer
2841
+ };
2842
+ };
2843
+ handleMarketPointerMove = event => {
2844
+ const state = this.marketPointer;
2845
+ if (!state || state.pointerId !== event.pointerId) return;
2846
+ if (Math.abs(event.clientX - state.startX) > MARKET_MOVE_CANCEL_PX || Math.abs(event.clientY - state.startY) > MARKET_MOVE_CANCEL_PX) this.cancelMarketPointer();
2847
+ };
2848
+ handleMarketPointerEnd = event => {
2849
+ if (this.marketPointer?.pointerId === event.pointerId) this.cancelMarketPointer();
2850
+ };
2437
2851
 
2438
2852
  // OneKey patch: hover opens the same anchored help action as native taps.
2439
2853
  handleTitlePointerOver = event => {
@@ -2456,7 +2870,8 @@ export class NativeListWebEngine {
2456
2870
  if (this.actionAnchor?.actionElement === action) this.invalidateActionAnchor('pointerLeave');
2457
2871
  };
2458
2872
  handleClick = event => {
2459
- if (Date.now() < this.suppressClickUntil) {
2873
+ if (this.marketLongPressFired || Date.now() < this.suppressClickUntil) {
2874
+ this.marketLongPressFired = false;
2460
2875
  event.preventDefault();
2461
2876
  event.stopPropagation();
2462
2877
  return;
@@ -2581,6 +2996,7 @@ export class NativeListWebEngine {
2581
2996
  });
2582
2997
  }
2583
2998
  handleScroll = () => {
2999
+ this.cancelMarketPointer();
2584
3000
  this.invalidateActionAnchor('scroll');
2585
3001
  this.scheduleFrame();
2586
3002
  };
@@ -2607,50 +3023,42 @@ export class NativeListWebEngine {
2607
3023
  const view = this.document.defaultView;
2608
3024
  if (view?.cancelAnimationFrame) view.cancelAnimationFrame(handle);else view?.clearTimeout(handle);
2609
3025
  }
2610
- selectIndexPosition(position, title, previewClientY) {
3026
+ selectIndexPosition(position) {
3027
+ const activeKey = this.sectionIndexEntries.find(entry => entry.position === position)?.key;
3028
+ this.indexRail.querySelectorAll('[data-section-key]').forEach(button => setData(button, 'active', button.dataset.sectionKey === activeKey));
2611
3029
  this.scrollToIndex(position, {
2612
3030
  animated: false,
2613
3031
  alignment: 'start',
2614
3032
  viewPosition: 0,
2615
3033
  viewOffset: 0
2616
3034
  });
2617
- const frame = this.viewportFrame.getBoundingClientRect();
2618
- if (previewClientY !== undefined && frame.height > 0) {
2619
- const previewY = Math.min(frame.height - 24, Math.max(24, previewClientY - frame.top));
2620
- this.indexPreview.style.top = String(previewY) + 'px';
2621
- } else {
2622
- this.indexPreview.style.top = '50%';
2623
- }
2624
- this.indexPreview.textContent = title;
2625
- setData(this.indexPreview, 'visible', true);
2626
- if (this.previewTimer !== undefined) this.document.defaultView?.clearTimeout(this.previewTimer);
2627
- this.previewTimer = this.document.defaultView?.setTimeout(() => {
2628
- setData(this.indexPreview, 'visible', false);
2629
- }, 180);
2630
3035
  }
2631
3036
  sectionIndexEntryAtEvent(event) {
2632
3037
  const rail = this.indexRail.getBoundingClientRect();
2633
3038
  if (this.sectionIndexEntries.length === 0 || rail.height <= 0) {
2634
3039
  return undefined;
2635
3040
  }
2636
- const availableHeight = Math.max(1, rail.height - SECTION_INDEX_EDGE_PADDING * 2);
2637
- const progress = Math.min(1, Math.max(0, (event.clientY - rail.top - SECTION_INDEX_EDGE_PADDING) / availableHeight));
2638
- const entryIndex = Math.round(progress * (this.sectionIndexEntries.length - 1));
2639
- const entry = this.sectionIndexEntries[entryIndex];
2640
- return entry ? {
2641
- entry,
2642
- previewClientY: event.clientY
2643
- } : undefined;
3041
+ const metrics = this.sectionIndexMetrics(rail.height);
3042
+ if (metrics.trackHeight <= 0) return undefined;
3043
+ const progress = Math.min(1, Math.max(0, (event.clientY - rail.top - metrics.originY) / metrics.trackHeight));
3044
+ const entryIndex = Math.min(this.sectionIndexEntries.length - 1, Math.floor(progress * this.sectionIndexEntries.length));
3045
+ return this.sectionIndexEntries[entryIndex];
2644
3046
  }
2645
3047
  handleIndexPointer = event => {
2646
3048
  if (event.type === 'pointermove' && event.buttons === 0) return;
2647
- const selection = this.sectionIndexEntryAtEvent(event);
2648
- if (!selection) return;
3049
+ const entry = this.sectionIndexEntryAtEvent(event);
3050
+ if (!entry) return;
2649
3051
  event.preventDefault();
2650
3052
  if (event.type === 'pointerdown') {
2651
3053
  this.indexRail.setPointerCapture?.(event.pointerId);
2652
3054
  }
2653
- this.selectIndexPosition(selection.entry.position, selection.entry.title, selection.previewClientY);
3055
+ this.selectIndexPosition(entry.position);
3056
+ };
3057
+ handleIndexPointerEnd = event => {
3058
+ if (event.type === 'pointerup') {
3059
+ const entry = this.sectionIndexEntryAtEvent(event);
3060
+ if (entry) this.selectIndexPosition(entry.position);
3061
+ }
2654
3062
  };
2655
3063
  handleIndexClick = event => {
2656
3064
  if ('detail' in event && typeof event.detail === 'number' && event.detail > 0) return;
@@ -2660,8 +3068,7 @@ export class NativeListWebEngine {
2660
3068
  if (!button) return;
2661
3069
  const entry = this.sectionIndexEntries[Number(button.dataset.sectionEntryIndex)];
2662
3070
  if (!entry) return;
2663
- const rect = button.getBoundingClientRect();
2664
- this.selectIndexPosition(entry.position, entry.title, rect.top + rect.height / 2);
3071
+ this.selectIndexPosition(entry.position);
2665
3072
  };
2666
3073
  handlePullStart = event => {
2667
3074
  if (this.pointerReorder?.pointerId === event.pointerId || !this.snapshot.capabilities?.pullToRefresh || this.viewport.scrollTop > 0 || event.pointerType !== 'touch' && event.pointerType !== 'pen') return;
@@ -2816,7 +3223,9 @@ export class NativeListWebEngine {
2816
3223
  setCurrentOffset(offset) {
2817
3224
  const maximum = Math.max(0, this.contentLength() - this.viewportLength());
2818
3225
  const next = Math.max(0, Math.min(maximum, offset));
2819
- if (this.layout.horizontal) this.viewport.scrollLeft = next;else this.viewport.scrollTop = next;
3226
+ if (this.layout.horizontal) this.viewport.scrollLeft = next;else {
3227
+ this.viewport.scrollTop = resolveWebCollapsiblePagerRawOffset(next, this.collapsiblePagerInsets().header);
3228
+ }
2820
3229
  this.scheduleFrame();
2821
3230
  }
2822
3231
  showReorderPreview(state) {
@@ -2867,8 +3276,9 @@ export class NativeListWebEngine {
2867
3276
  }
2868
3277
  this.reorderSettlingKey = state.sourceKey;
2869
3278
  const viewportRect = this.viewport.getBoundingClientRect();
3279
+ const insets = this.collapsiblePagerInsets();
2870
3280
  const left = viewportRect.left + destinationLayout.x - this.viewport.scrollLeft;
2871
- const top = viewportRect.top + destinationLayout.y - this.viewport.scrollTop;
3281
+ const top = viewportRect.top + destinationLayout.y + insets.header + insets.sticky - this.viewport.scrollTop;
2872
3282
  this.reorderPreview.getBoundingClientRect();
2873
3283
  this.reorderPreview.style.transition = 'transform ' + String(REORDER_DROP_TRANSITION_MS) + 'ms ' + WEB_REORDER_ANIMATION.dropTimingFunction + ', box-shadow ' + String(REORDER_DROP_TRANSITION_MS) + 'ms ' + WEB_REORDER_ANIMATION.dropTimingFunction;
2874
3284
  this.reorderPreview.style.boxShadow = 'none';