@onekeyfe/react-native-native-list 3.0.105

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/README.md +342 -0
  2. package/android/CMakeLists.txt +20 -0
  3. package/android/build.gradle +105 -0
  4. package/android/gradle.properties +4 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  7. package/android/src/main/java/com/onekey/nativelist/HybridNativeList.kt +125 -0
  8. package/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +165 -0
  9. package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +147 -0
  10. package/android/src/main/java/com/onekey/nativelist/NativeListPackage.kt +25 -0
  11. package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +2939 -0
  12. package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +1794 -0
  13. package/common/fonts/Roobert-Bold.ttf +0 -0
  14. package/common/fonts/Roobert-Medium.ttf +0 -0
  15. package/common/fonts/Roobert-Regular.ttf +0 -0
  16. package/common/fonts/Roobert-SemiBold.ttf +0 -0
  17. package/docs/DESIGN.md +129 -0
  18. package/ios/HybridNativeList.swift +131 -0
  19. package/ios/NativeListCell.swift +2858 -0
  20. package/ios/NativeListDesignAssets.swift +110 -0
  21. package/ios/NativeListModels.swift +215 -0
  22. package/ios/RNCNativeListView.swift +1737 -0
  23. package/ios/Resources/NativeListIcons.xcassets/Contents.json +1 -0
  24. package/ios/Resources/NativeListIcons.xcassets/onekey_arrow_bottom.imageset/Contents.json +1 -0
  25. package/ios/Resources/NativeListIcons.xcassets/onekey_arrow_bottom.imageset/icon.svg +1 -0
  26. package/ios/Resources/NativeListIcons.xcassets/onekey_arrow_top.imageset/Contents.json +1 -0
  27. package/ios/Resources/NativeListIcons.xcassets/onekey_arrow_top.imageset/icon.svg +1 -0
  28. package/ios/Resources/NativeListIcons.xcassets/onekey_chart_trending_up.imageset/Contents.json +1 -0
  29. package/ios/Resources/NativeListIcons.xcassets/onekey_chart_trending_up.imageset/icon.svg +1 -0
  30. package/ios/Resources/NativeListIcons.xcassets/onekey_checkbox_checked.imageset/Contents.json +1 -0
  31. package/ios/Resources/NativeListIcons.xcassets/onekey_checkbox_checked.imageset/icon.svg +1 -0
  32. package/ios/Resources/NativeListIcons.xcassets/onekey_checkbox_indeterminate.imageset/Contents.json +1 -0
  33. package/ios/Resources/NativeListIcons.xcassets/onekey_checkbox_indeterminate.imageset/icon.svg +1 -0
  34. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_bottom.imageset/Contents.json +1 -0
  35. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_bottom.imageset/icon.svg +1 -0
  36. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_grabber_ver.imageset/Contents.json +1 -0
  37. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_grabber_ver.imageset/icon.svg +1 -0
  38. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_right_small.imageset/Contents.json +1 -0
  39. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_right_small.imageset/icon.svg +1 -0
  40. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_top.imageset/Contents.json +1 -0
  41. package/ios/Resources/NativeListIcons.xcassets/onekey_chevron_top.imageset/icon.svg +1 -0
  42. package/ios/Resources/NativeListIcons.xcassets/onekey_dot_hor.imageset/Contents.json +1 -0
  43. package/ios/Resources/NativeListIcons.xcassets/onekey_dot_hor.imageset/icon.svg +1 -0
  44. package/ios/Resources/NativeListIcons.xcassets/onekey_drag.imageset/Contents.json +1 -0
  45. package/ios/Resources/NativeListIcons.xcassets/onekey_drag.imageset/icon.svg +1 -0
  46. package/ios/Resources/NativeListIcons.xcassets/onekey_error_solid.imageset/Contents.json +1 -0
  47. package/ios/Resources/NativeListIcons.xcassets/onekey_error_solid.imageset/icon.svg +1 -0
  48. package/ios/Resources/NativeListIcons.xcassets/onekey_image_square_waves.imageset/Contents.json +1 -0
  49. package/ios/Resources/NativeListIcons.xcassets/onekey_image_square_waves.imageset/icon.svg +1 -0
  50. package/ios/Resources/NativeListIcons.xcassets/onekey_info_circle.imageset/Contents.json +1 -0
  51. package/ios/Resources/NativeListIcons.xcassets/onekey_info_circle.imageset/icon.svg +1 -0
  52. package/ios/Resources/NativeListIcons.xcassets/onekey_minus_circle.imageset/Contents.json +1 -0
  53. package/ios/Resources/NativeListIcons.xcassets/onekey_minus_circle.imageset/icon.svg +1 -0
  54. package/ios/Resources/NativeListIcons.xcassets/onekey_minus_circle_solid.imageset/Contents.json +1 -0
  55. package/ios/Resources/NativeListIcons.xcassets/onekey_minus_circle_solid.imageset/icon.svg +1 -0
  56. package/ios/Resources/NativeListIcons.xcassets/onekey_pencil.imageset/Contents.json +1 -0
  57. package/ios/Resources/NativeListIcons.xcassets/onekey_pencil.imageset/icon.svg +1 -0
  58. package/ios/Resources/NativeListIcons.xcassets/onekey_plus_circle.imageset/Contents.json +1 -0
  59. package/ios/Resources/NativeListIcons.xcassets/onekey_plus_circle.imageset/icon.svg +1 -0
  60. package/ios/Resources/NativeListIcons.xcassets/onekey_plus_small.imageset/Contents.json +1 -0
  61. package/ios/Resources/NativeListIcons.xcassets/onekey_plus_small.imageset/icon.svg +1 -0
  62. package/ios/Resources/NativeListIcons.xcassets/onekey_questionmark_solid.imageset/Contents.json +1 -0
  63. package/ios/Resources/NativeListIcons.xcassets/onekey_questionmark_solid.imageset/icon.svg +1 -0
  64. package/ios/Resources/NativeListIcons.xcassets/onekey_shield_exclamation.imageset/Contents.json +1 -0
  65. package/ios/Resources/NativeListIcons.xcassets/onekey_shield_exclamation.imageset/icon.svg +1 -0
  66. package/ios/Resources/NativeListIcons.xcassets/onekey_speaker_promote.imageset/Contents.json +1 -0
  67. package/ios/Resources/NativeListIcons.xcassets/onekey_speaker_promote.imageset/icon.svg +1 -0
  68. package/ios/Resources/NativeListIcons.xcassets/onekey_star.imageset/Contents.json +1 -0
  69. package/ios/Resources/NativeListIcons.xcassets/onekey_star.imageset/icon.svg +1 -0
  70. package/ios/Resources/NativeListIcons.xcassets/onekey_star_solid.imageset/Contents.json +1 -0
  71. package/ios/Resources/NativeListIcons.xcassets/onekey_star_solid.imageset/icon.svg +1 -0
  72. package/ios/Resources/NativeListIcons.xcassets/onekey_swap_hor.imageset/Contents.json +1 -0
  73. package/ios/Resources/NativeListIcons.xcassets/onekey_swap_hor.imageset/icon.svg +1 -0
  74. package/lib/module/NativeList.js +183 -0
  75. package/lib/module/NativeList.nitro.js +4 -0
  76. package/lib/module/NativeList.types.js +4 -0
  77. package/lib/module/NativeList.web.js +167 -0
  78. package/lib/module/index.js +6 -0
  79. package/lib/module/models.js +2 -0
  80. package/lib/module/package.json +1 -0
  81. package/lib/module/scrolling.js +121 -0
  82. package/lib/module/selection.js +85 -0
  83. package/lib/module/validation.js +562 -0
  84. package/lib/module/web/NativeListWebEngine.js +2238 -0
  85. package/lib/nitrogen/generated/.gitattributes +1 -0
  86. package/lib/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
  87. package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +201 -0
  88. package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +84 -0
  89. package/lib/nitrogen/generated/android/c++/JNativeListScrollAlignment.hpp +64 -0
  90. package/lib/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +104 -0
  91. package/lib/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.hpp +54 -0
  92. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/Func_void_std__string.kt +78 -0
  93. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +177 -0
  94. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/NativeListScrollAlignment.kt +25 -0
  95. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/nativelistOnLoad.kt +35 -0
  96. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/views/HybridNativeListManager.kt +96 -0
  97. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/views/HybridNativeListStateUpdater.kt +23 -0
  98. package/lib/nitrogen/generated/android/nativelist+autolinking.cmake +83 -0
  99. package/lib/nitrogen/generated/android/nativelist+autolinking.gradle +27 -0
  100. package/lib/nitrogen/generated/android/nativelistOnLoad.cpp +58 -0
  101. package/lib/nitrogen/generated/android/nativelistOnLoad.hpp +34 -0
  102. package/lib/nitrogen/generated/ios/NativeListModule+autolinking.rb +62 -0
  103. package/lib/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Bridge.cpp +41 -0
  104. package/lib/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Bridge.hpp +91 -0
  105. package/lib/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Umbrella.hpp +49 -0
  106. package/lib/nitrogen/generated/ios/NativeListModuleAutolinking.mm +33 -0
  107. package/lib/nitrogen/generated/ios/NativeListModuleAutolinking.swift +26 -0
  108. package/lib/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.cpp +11 -0
  109. package/lib/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +180 -0
  110. package/lib/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +180 -0
  111. package/lib/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
  112. package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +69 -0
  113. package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +447 -0
  114. package/lib/nitrogen/generated/ios/swift/NativeListScrollAlignment.swift +48 -0
  115. package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +43 -0
  116. package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +87 -0
  117. package/lib/nitrogen/generated/shared/c++/NativeListScrollAlignment.hpp +84 -0
  118. package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +46 -0
  119. package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +103 -0
  120. package/lib/nitrogen/generated/shared/json/NativeListConfig.json +16 -0
  121. package/lib/typescript/package.json +1 -0
  122. package/lib/typescript/src/NativeList.d.ts +18 -0
  123. package/lib/typescript/src/NativeList.nitro.d.ts +29 -0
  124. package/lib/typescript/src/NativeList.types.d.ts +87 -0
  125. package/lib/typescript/src/NativeList.web.d.ts +19 -0
  126. package/lib/typescript/src/index.d.ts +7 -0
  127. package/lib/typescript/src/models.d.ts +456 -0
  128. package/lib/typescript/src/scrolling.d.ts +32 -0
  129. package/lib/typescript/src/selection.d.ts +27 -0
  130. package/lib/typescript/src/validation.d.ts +7 -0
  131. package/lib/typescript/src/web/NativeListWebEngine.d.ts +203 -0
  132. package/nitro.json +23 -0
  133. package/nitrogen/generated/.gitattributes +1 -0
  134. package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
  135. package/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +201 -0
  136. package/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +84 -0
  137. package/nitrogen/generated/android/c++/JNativeListScrollAlignment.hpp +64 -0
  138. package/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +104 -0
  139. package/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.hpp +54 -0
  140. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/Func_void_std__string.kt +78 -0
  141. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +177 -0
  142. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/NativeListScrollAlignment.kt +25 -0
  143. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/nativelistOnLoad.kt +35 -0
  144. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/views/HybridNativeListManager.kt +96 -0
  145. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/views/HybridNativeListStateUpdater.kt +23 -0
  146. package/nitrogen/generated/android/nativelist+autolinking.cmake +83 -0
  147. package/nitrogen/generated/android/nativelist+autolinking.gradle +27 -0
  148. package/nitrogen/generated/android/nativelistOnLoad.cpp +58 -0
  149. package/nitrogen/generated/android/nativelistOnLoad.hpp +34 -0
  150. package/nitrogen/generated/ios/NativeListModule+autolinking.rb +62 -0
  151. package/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Bridge.cpp +41 -0
  152. package/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Bridge.hpp +91 -0
  153. package/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Umbrella.hpp +49 -0
  154. package/nitrogen/generated/ios/NativeListModuleAutolinking.mm +33 -0
  155. package/nitrogen/generated/ios/NativeListModuleAutolinking.swift +26 -0
  156. package/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.cpp +11 -0
  157. package/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +180 -0
  158. package/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +180 -0
  159. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
  160. package/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +69 -0
  161. package/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +447 -0
  162. package/nitrogen/generated/ios/swift/NativeListScrollAlignment.swift +48 -0
  163. package/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +43 -0
  164. package/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +87 -0
  165. package/nitrogen/generated/shared/c++/NativeListScrollAlignment.hpp +84 -0
  166. package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +46 -0
  167. package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +103 -0
  168. package/nitrogen/generated/shared/json/NativeListConfig.json +16 -0
  169. package/package.json +126 -0
  170. package/react-native-native-list.podspec +38 -0
  171. package/react-native.config.js +19 -0
  172. package/src/NativeList.nitro.ts +48 -0
  173. package/src/NativeList.tsx +298 -0
  174. package/src/NativeList.types.ts +125 -0
  175. package/src/NativeList.web.tsx +258 -0
  176. package/src/index.ts +35 -0
  177. package/src/models.ts +604 -0
  178. package/src/scrolling.ts +189 -0
  179. package/src/selection.ts +159 -0
  180. package/src/validation.ts +877 -0
  181. package/src/web/NativeListWebEngine.ts +3767 -0
@@ -0,0 +1,3767 @@
1
+ import type {
2
+ ActionAnchorInvalidatedEvent,
3
+ CheckboxState,
4
+ ImageSource,
5
+ LeadingVisual,
6
+ NativeListSnapshot,
7
+ NativeListActionAnchor,
8
+ NativeListActionSource,
9
+ NativeListTheme,
10
+ ReorderEvent,
11
+ RowActionEvent,
12
+ RowModel,
13
+ RowPatch,
14
+ SelectionDeltaEvent,
15
+ SelectionTarget,
16
+ TextTone,
17
+ TrailingAccessory,
18
+ VisibleRangeChangedEvent,
19
+ } from '../models';
20
+ import type {
21
+ ActionAnchorState,
22
+ ScrollToIndexFailedInfo,
23
+ ScrollToLocationParams,
24
+ } from '../NativeList.types';
25
+ import {
26
+ checkboxStateForKeys,
27
+ checkboxStateForSection,
28
+ isSelectableRow,
29
+ reduceSelection,
30
+ selectionStateFromSnapshot,
31
+ } from '../selection';
32
+ import {
33
+ calculateAlignedScrollOffset,
34
+ resolveLocationIndex,
35
+ scrollFailure,
36
+ validateOffset,
37
+ type NormalizedPositionScroll,
38
+ } from '../scrolling';
39
+ import { applyRowPatches, validateSnapshot } from '../validation';
40
+
41
+ const SECTION_INDEX_GUTTER = 44;
42
+ const DEFAULT_VIEWPORT_WIDTH = 320;
43
+ const DEFAULT_VIEWPORT_HEIGHT = 640;
44
+ const OVERSCAN_VIEWPORTS = 1;
45
+ export const WEB_REORDER_ANIMATION = {
46
+ outOfWayDurationMs: 200,
47
+ outOfWayTimingFunction: 'cubic-bezier(0.2, 0, 0, 1)',
48
+ dropDurationMs: 80,
49
+ dropTimingFunction: 'ease',
50
+ } as const;
51
+
52
+ const REORDER_TOUCH_LONG_PRESS_MS = 120;
53
+ const REORDER_MOUSE_MOVE_THRESHOLD_PX = 5;
54
+ const REORDER_EDGE_RATIO = 0.25;
55
+ const REORDER_MAX_SPEED_ZONE_RATIO = 0.05;
56
+ const REORDER_MAX_SCROLL_PX = 28;
57
+ const REORDER_SCROLL_ACCELERATE_AT_MS = 360;
58
+ const REORDER_SCROLL_DAMPENING_MS = 1_200;
59
+ const REORDER_DROP_TRANSITION_MS = WEB_REORDER_ANIMATION.dropDurationMs;
60
+ const WALLET_REORDER_COMPACT_HEIGHT = 68;
61
+ let webNativeListInstanceCounter = 0;
62
+
63
+ const defaultTheme: NativeListTheme = {
64
+ background: '#F7F7F7',
65
+ rowBackground: '#FFFFFF',
66
+ rowSelectedBackground: '#EAF2FF',
67
+ rowPressedBackground: '#E8E8E8',
68
+ subduedBackground: '#F9F9F9',
69
+ strongBackground: '#0000000F',
70
+ primaryText: '#111111',
71
+ secondaryText: '#6B7280',
72
+ disabledText: '#8D8D8D',
73
+ icon: '#111111',
74
+ iconSubdued: '#8D8D8D',
75
+ separator: '#E5E7EB',
76
+ accent: '#2F6BFF',
77
+ positive: '#15803D',
78
+ negative: '#DC2626',
79
+ criticalBackground: '#FEECEC',
80
+ inverseBackground: '#202020',
81
+ inverseText: '#FCFCFC',
82
+ info: '#0D74CE',
83
+ };
84
+
85
+ export type WebLayoutItem = Readonly<{
86
+ index: number;
87
+ key: string;
88
+ x: number;
89
+ y: number;
90
+ width: number;
91
+ height: number;
92
+ }>;
93
+
94
+ export type WebListLayout = Readonly<{
95
+ items: readonly WebLayoutItem[];
96
+ contentWidth: number;
97
+ contentHeight: number;
98
+ horizontal: boolean;
99
+ }>;
100
+
101
+ export type NativeListWebCallbacks = Readonly<{
102
+ onRowAction?: (event: RowActionEvent) => void;
103
+ onActionAnchorInvalidated?: (event: ActionAnchorInvalidatedEvent) => void;
104
+ onSelectionDelta?: (event: SelectionDeltaEvent) => void;
105
+ onReorder?: (event: ReorderEvent) => void;
106
+ onEndReached?: (event: { generation: number; lastKey?: string }) => void;
107
+ onVisibleRangeChanged?: (event: VisibleRangeChangedEvent) => void;
108
+ onRefresh?: () => void;
109
+ onScrollToIndexFailed?: (info: ScrollToIndexFailedInfo) => void;
110
+ }>;
111
+
112
+ type WebActionAnchorRecord = {
113
+ token: string;
114
+ actionElement: HTMLElement;
115
+ rowElement: HTMLElement;
116
+ bindingEpoch: string;
117
+ open: boolean;
118
+ invalidatedReason?: ActionAnchorInvalidatedEvent['reason'];
119
+ };
120
+
121
+ export function webActionAnchorPayload(
122
+ token: string,
123
+ rect: Readonly<{
124
+ left: number;
125
+ top: number;
126
+ width: number;
127
+ height: number;
128
+ }>,
129
+ source: NativeListActionSource,
130
+ generation: number,
131
+ layoutDirection: 'ltr' | 'rtl',
132
+ slot?: number
133
+ ): NativeListActionAnchor {
134
+ return {
135
+ token,
136
+ windowRect: {
137
+ x: rect.left,
138
+ y: rect.top,
139
+ width: rect.width,
140
+ height: rect.height,
141
+ },
142
+ source,
143
+ slot,
144
+ generation,
145
+ layoutDirection,
146
+ };
147
+ }
148
+
149
+ type RenderContext = Readonly<{
150
+ document: Document;
151
+ snapshot: NativeListSnapshot;
152
+ selectedKeys: ReadonlySet<string>;
153
+ itemIndex: number;
154
+ }>;
155
+
156
+ type PendingScroll =
157
+ | Readonly<{
158
+ kind: 'index';
159
+ index: number;
160
+ scroll: NormalizedPositionScroll;
161
+ }>
162
+ | Readonly<{ kind: 'offset'; offset: number; animated: boolean }>
163
+ | Readonly<{ kind: 'end'; animated: boolean }>;
164
+
165
+ type PointerReorderState = {
166
+ pointerId: number;
167
+ pointerType: string;
168
+ sourceKey: string;
169
+ currentIndex: number;
170
+ startX: number;
171
+ startY: number;
172
+ clientX: number;
173
+ clientY: number;
174
+ originalRows: readonly RowModel[];
175
+ workingRows: RowModel[];
176
+ viewportRect?: Readonly<{
177
+ left: number;
178
+ top: number;
179
+ right: number;
180
+ bottom: number;
181
+ width: number;
182
+ height: number;
183
+ }>;
184
+ previewOffsetX?: number;
185
+ previewOffsetY?: number;
186
+ longPressTimer?: number;
187
+ activatedAt?: number;
188
+ active: boolean;
189
+ };
190
+
191
+ type KeyboardReorderState = {
192
+ sourceKey: string;
193
+ originalRows: readonly RowModel[];
194
+ };
195
+
196
+ export function isWebRowReorderable(
197
+ snapshot: NativeListSnapshot,
198
+ row: RowModel
199
+ ): boolean {
200
+ if (!snapshot.capabilities?.reorderable || row.disabled) return false;
201
+ if (row.type === 'rail' && row.draggable) return true;
202
+ if (row.type === 'identity' && row.draggable) return true;
203
+ if (row.type === 'walletGroup' && row.draggable) return true;
204
+ return (
205
+ (row.type === 'identity' || row.type === 'action') &&
206
+ Boolean(row.trailing?.some((accessory) => accessory.kind === 'drag'))
207
+ );
208
+ }
209
+
210
+ export function moveWebReorderRow(
211
+ rows: readonly RowModel[],
212
+ fromIndex: number,
213
+ toIndex: number
214
+ ): readonly RowModel[] {
215
+ if (
216
+ fromIndex === toIndex ||
217
+ fromIndex < 0 ||
218
+ fromIndex >= rows.length ||
219
+ toIndex < 0 ||
220
+ toIndex >= rows.length
221
+ ) {
222
+ return rows;
223
+ }
224
+ const next = [...rows];
225
+ const [moved] = next.splice(fromIndex, 1);
226
+ if (!moved) return rows;
227
+ next.splice(toIndex, 0, moved);
228
+ return next;
229
+ }
230
+
231
+ export function cancelWebReorderRows(
232
+ originalRows: readonly RowModel[]
233
+ ): readonly RowModel[] {
234
+ return originalRows;
235
+ }
236
+
237
+ export function webReorderEventForRows(
238
+ originalRows: readonly RowModel[],
239
+ finalRows: readonly RowModel[],
240
+ key: string
241
+ ): ReorderEvent | undefined {
242
+ const fromIndex = originalRows.findIndex((row) => row.key === key);
243
+ const toIndex = finalRows.findIndex((row) => row.key === key);
244
+ if (fromIndex < 0 || toIndex < 0 || fromIndex === toIndex) return undefined;
245
+ return {
246
+ key,
247
+ fromIndex,
248
+ toIndex,
249
+ beforeKey: finalRows[toIndex - 1]?.key,
250
+ afterKey: finalRows[toIndex + 1]?.key,
251
+ };
252
+ }
253
+
254
+ export function webReorderAutoScrollVelocity(
255
+ point: number,
256
+ start: number,
257
+ end: number,
258
+ elapsedMs: number
259
+ ): number {
260
+ const viewportLength = Math.max(1, end - start);
261
+ const edge = viewportLength * REORDER_EDGE_RATIO;
262
+ const maxZone = viewportLength * REORDER_MAX_SPEED_ZONE_RATIO;
263
+ const speedForDistance = (distance: number) => {
264
+ if (distance > edge) return 0;
265
+ const proposed =
266
+ distance <= maxZone
267
+ ? REORDER_MAX_SCROLL_PX
268
+ : distance === edge
269
+ ? 1
270
+ : Math.ceil(
271
+ REORDER_MAX_SCROLL_PX *
272
+ Math.pow((edge - distance) / Math.max(1, edge - maxZone), 2)
273
+ );
274
+ if (elapsedMs < REORDER_SCROLL_ACCELERATE_AT_MS) return 1;
275
+ if (elapsedMs >= REORDER_SCROLL_DAMPENING_MS) return proposed;
276
+ const timeProgress =
277
+ (elapsedMs - REORDER_SCROLL_ACCELERATE_AT_MS) /
278
+ (REORDER_SCROLL_DAMPENING_MS - REORDER_SCROLL_ACCELERATE_AT_MS);
279
+ return Math.ceil(proposed * timeProgress * timeProgress);
280
+ };
281
+ if (point <= start + edge) return -speedForDistance(point - start);
282
+ if (point >= end - edge) return speedForDistance(end - point);
283
+ return 0;
284
+ }
285
+
286
+ export function hasExceededWebReorderMouseThreshold(
287
+ deltaX: number,
288
+ deltaY: number
289
+ ): boolean {
290
+ return (
291
+ Math.abs(deltaX) >= REORDER_MOUSE_MOVE_THRESHOLD_PX ||
292
+ Math.abs(deltaY) >= REORDER_MOUSE_MOVE_THRESHOLD_PX
293
+ );
294
+ }
295
+
296
+ function effectiveRows(snapshot: NativeListSnapshot): readonly RowModel[] {
297
+ if (snapshot.rows.length > 0) return snapshot.rows;
298
+ return snapshot.emptyState ? [snapshot.emptyState] : [];
299
+ }
300
+
301
+ function resolvedTheme(snapshot: NativeListSnapshot): NativeListTheme {
302
+ return { ...defaultTheme, ...snapshot.theme };
303
+ }
304
+
305
+ function paddingValues(snapshot: NativeListSnapshot) {
306
+ const fallback = snapshot.layout.contentPadding ?? 0;
307
+ return {
308
+ horizontal: snapshot.layout.contentPaddingHorizontal ?? fallback,
309
+ top: snapshot.layout.contentPaddingTop ?? fallback,
310
+ bottom: snapshot.layout.contentPaddingBottom ?? fallback,
311
+ };
312
+ }
313
+
314
+ function isStructuralRow(row: RowModel): boolean {
315
+ return (
316
+ row.type === 'sectionHeader' ||
317
+ row.type === 'system' ||
318
+ row.type === 'action'
319
+ );
320
+ }
321
+
322
+ function sizeModifier(row: RowModel): number {
323
+ if (
324
+ row.type === 'sectionHeader' &&
325
+ (row.variant === 'summary' || row.variant === 'gallery')
326
+ )
327
+ return 0;
328
+ if (row.size === 'small') return -8;
329
+ if (row.size === 'large') return 12;
330
+ return 0;
331
+ }
332
+
333
+ function approximateMessageHeight(row: RowModel, availableWidth: number) {
334
+ if (row.type !== 'message') return 0;
335
+ const leadingWidth = row.leading ? 52 : 0;
336
+ const thumbnailWidth = row.thumbnail ? 88 : 0;
337
+ const charactersPerLine = Math.max(
338
+ 18,
339
+ Math.floor((availableWidth - leadingWidth - thumbnailWidth - 40) / 7)
340
+ );
341
+ const titleLines = Math.min(
342
+ 2,
343
+ Math.max(1, Math.ceil(row.title.length / charactersPerLine))
344
+ );
345
+ const bodyLines = Math.min(
346
+ row.bodyLines ?? 3,
347
+ Math.max(1, Math.ceil(row.body.length / charactersPerLine))
348
+ );
349
+ return 32 + titleLines * 20 + bodyLines * 20 + 22;
350
+ }
351
+
352
+ export function estimateWebRowHeight(
353
+ row: RowModel,
354
+ snapshot: NativeListSnapshot,
355
+ availableWidth: number
356
+ ): number {
357
+ if (row.type === 'system' && row.variant === 'spacer') return row.height;
358
+ if (row.type === 'walletGroup')
359
+ return (row.children.length + 1) * 68 + row.children.length * 12;
360
+ if (row.type === 'identity' && row.presentation === 'walletSidebar')
361
+ return 68;
362
+ if (row.type === 'identity' && row.presentation === 'networkSelector')
363
+ return 47;
364
+ if (row.type === 'identity' && row.presentation === 'accountSelector')
365
+ return 58;
366
+
367
+ let base: number;
368
+ switch (row.type) {
369
+ case 'rail':
370
+ base = 40;
371
+ break;
372
+ case 'activity':
373
+ base = row.footerActions?.length ? 100 : 60;
374
+ break;
375
+ case 'message':
376
+ base = approximateMessageHeight(row, availableWidth);
377
+ break;
378
+ case 'mediaTile':
379
+ base = 244;
380
+ break;
381
+ case 'metricCard':
382
+ base =
383
+ row.variant === 'activity'
384
+ ? 161
385
+ : row.variant === 'performance'
386
+ ? 178
387
+ : 132;
388
+ break;
389
+ case 'sectionHeader': {
390
+ const isHistory =
391
+ row.variant === 'history' ||
392
+ row.key.startsWith('history-') ||
393
+ row.sectionKey.startsWith('history-');
394
+ base =
395
+ snapshot.layout.kind === 'table'
396
+ ? 28
397
+ : row.presentation === 'networkSelector'
398
+ ? 47
399
+ : isHistory
400
+ ? 16
401
+ : row.variant === 'summary'
402
+ ? 68
403
+ : row.variant === 'gallery'
404
+ ? 32
405
+ : row.checkbox
406
+ ? 56
407
+ : snapshot.layout.kind === 'linear'
408
+ ? 30
409
+ : 36;
410
+ break;
411
+ }
412
+ case 'system':
413
+ base =
414
+ row.variant === 'noMatch' || row.variant === 'end'
415
+ ? 36
416
+ : row.variant === 'retry'
417
+ ? 44
418
+ : 56;
419
+ break;
420
+ case 'action':
421
+ base = row.presentation === 'accountSelector' ? 48 : row.icon ? 60 : 44;
422
+ break;
423
+ case 'dataRow':
424
+ base = row.columns.some((column) => column.secondaryText) ? 60 : 56;
425
+ break;
426
+ case 'identity':
427
+ base = row.tertiary ? 72 : row.subtitle ? 60 : 56;
428
+ break;
429
+ default:
430
+ base = 56;
431
+ }
432
+ const tableAdjustment =
433
+ snapshot.layout.kind === 'table' &&
434
+ row.type === 'dataRow' &&
435
+ !row.columns.some((column) => column.secondaryText)
436
+ ? -8
437
+ : 0;
438
+ return Math.max(0, base + sizeModifier(row) + tableAdjustment);
439
+ }
440
+
441
+ function estimateHorizontalWidth(row: RowModel): number {
442
+ if (row.type === 'mediaTile') return 200;
443
+ if (row.type !== 'rail') return 280;
444
+ const badgeLength = row.badge?.text.length ?? 0;
445
+ const statusLength =
446
+ row.status && row.status !== 'none' ? row.status.length : 0;
447
+ return Math.min(
448
+ 288,
449
+ Math.max(72, 50 + (row.title.length + badgeLength + statusLength) * 7)
450
+ );
451
+ }
452
+
453
+ function sectionIndexEnabled(snapshot: NativeListSnapshot): boolean {
454
+ return Boolean(
455
+ snapshot.capabilities?.sectionIndex?.enabled &&
456
+ snapshot.layout.kind === 'sectioned' &&
457
+ snapshot.layout.orientation !== 'horizontal' &&
458
+ snapshot.rows.some(
459
+ (row) => row.type === 'sectionHeader' && row.indexTitle
460
+ )
461
+ );
462
+ }
463
+
464
+ export function computeWebListLayout(
465
+ snapshot: NativeListSnapshot,
466
+ viewportWidth: number,
467
+ viewportHeight: number,
468
+ compactRowKey?: string
469
+ ): WebListLayout {
470
+ const rows = effectiveRows(snapshot);
471
+ const horizontal = snapshot.layout.orientation === 'horizontal';
472
+ const spacing = snapshot.layout.itemSpacing ?? 0;
473
+ const padding = paddingValues(snapshot);
474
+ const indexGutter = sectionIndexEnabled(snapshot) ? SECTION_INDEX_GUTTER : 0;
475
+ const width = Math.max(1, viewportWidth || DEFAULT_VIEWPORT_WIDTH);
476
+ const height = Math.max(1, viewportHeight || DEFAULT_VIEWPORT_HEIGHT);
477
+ const availableWidth = Math.max(
478
+ 1,
479
+ width - padding.horizontal * 2 - indexGutter
480
+ );
481
+ const availableHeight = Math.max(1, height - padding.top - padding.bottom);
482
+ const items: WebLayoutItem[] = [];
483
+
484
+ if (horizontal) {
485
+ let x = padding.horizontal;
486
+ rows.forEach((row, index) => {
487
+ const itemWidth = estimateHorizontalWidth(row);
488
+ const rowHeight =
489
+ row.type === 'walletGroup' && row.key === compactRowKey
490
+ ? WALLET_REORDER_COMPACT_HEIGHT
491
+ : estimateWebRowHeight(row, snapshot, availableWidth);
492
+ items.push({
493
+ index,
494
+ key: row.key,
495
+ x,
496
+ y: padding.top,
497
+ width: itemWidth,
498
+ height:
499
+ row.type === 'rail'
500
+ ? Math.min(rowHeight, availableHeight)
501
+ : availableHeight,
502
+ });
503
+ x += itemWidth + spacing;
504
+ });
505
+ const contentWidth =
506
+ rows.length > 0
507
+ ? x - spacing + padding.horizontal
508
+ : padding.horizontal * 2;
509
+ return {
510
+ items,
511
+ horizontal: true,
512
+ contentWidth: Math.max(width, contentWidth),
513
+ contentHeight: height,
514
+ };
515
+ }
516
+
517
+ if (snapshot.layout.kind !== 'grid') {
518
+ let y = padding.top;
519
+ rows.forEach((row, index) => {
520
+ const itemHeight =
521
+ row.type === 'walletGroup' && row.key === compactRowKey
522
+ ? WALLET_REORDER_COMPACT_HEIGHT
523
+ : estimateWebRowHeight(row, snapshot, availableWidth);
524
+ items.push({
525
+ index,
526
+ key: row.key,
527
+ x: padding.horizontal,
528
+ y,
529
+ width: availableWidth,
530
+ height: itemHeight,
531
+ });
532
+ y += itemHeight + spacing;
533
+ });
534
+ const contentHeight =
535
+ rows.length > 0
536
+ ? y - spacing + padding.bottom
537
+ : padding.top + padding.bottom;
538
+ return {
539
+ items,
540
+ horizontal: false,
541
+ contentWidth: width,
542
+ contentHeight: Math.max(height, contentHeight),
543
+ };
544
+ }
545
+
546
+ const columns = snapshot.layout.gridColumns ?? 2;
547
+ const itemWidth = Math.floor(
548
+ (availableWidth - spacing * (columns - 1)) / columns
549
+ );
550
+ let y = padding.top;
551
+ let column = 0;
552
+ let bandStart = y;
553
+ let bandHeight = 0;
554
+ const flushBand = () => {
555
+ if (column === 0) return;
556
+ y = bandStart + bandHeight + spacing;
557
+ column = 0;
558
+ bandHeight = 0;
559
+ bandStart = y;
560
+ };
561
+
562
+ rows.forEach((row, index) => {
563
+ if (isStructuralRow(row)) {
564
+ flushBand();
565
+ const itemHeight = estimateWebRowHeight(row, snapshot, availableWidth);
566
+ items.push({
567
+ index,
568
+ key: row.key,
569
+ x: padding.horizontal,
570
+ y,
571
+ width: availableWidth,
572
+ height: itemHeight,
573
+ });
574
+ y += itemHeight + spacing;
575
+ bandStart = y;
576
+ return;
577
+ }
578
+ const itemHeight =
579
+ row.type === 'mediaTile'
580
+ ? itemWidth + 48
581
+ : estimateWebRowHeight(row, snapshot, itemWidth);
582
+ items.push({
583
+ index,
584
+ key: row.key,
585
+ x: padding.horizontal + column * (itemWidth + spacing),
586
+ y: bandStart,
587
+ width: itemWidth,
588
+ height: itemHeight,
589
+ });
590
+ bandHeight = Math.max(bandHeight, itemHeight);
591
+ column += 1;
592
+ if (column === columns) flushBand();
593
+ });
594
+ flushBand();
595
+ const contentHeight =
596
+ rows.length > 0
597
+ ? Math.max(padding.top, y - spacing) + padding.bottom
598
+ : padding.top + padding.bottom;
599
+ return {
600
+ items,
601
+ horizontal: false,
602
+ contentWidth: width,
603
+ contentHeight: Math.max(height, contentHeight),
604
+ };
605
+ }
606
+
607
+ export function webWalletGroupReorderBadge(row: RowModel): string | undefined {
608
+ return row.type === 'walletGroup' && row.children.length > 0
609
+ ? '+' + String(row.children.length)
610
+ : undefined;
611
+ }
612
+
613
+ function itemStart(item: WebLayoutItem, horizontal: boolean): number {
614
+ return horizontal ? item.x : item.y;
615
+ }
616
+
617
+ function itemEnd(item: WebLayoutItem, horizontal: boolean): number {
618
+ return itemStart(item, horizontal) + (horizontal ? item.width : item.height);
619
+ }
620
+
621
+ export function visibleWebLayoutItems(
622
+ layout: WebListLayout,
623
+ offset: number,
624
+ viewportLength: number,
625
+ overscan = 0
626
+ ): readonly WebLayoutItem[] {
627
+ if (layout.items.length === 0) return [];
628
+ const start = Math.max(0, offset - overscan);
629
+ const end = offset + viewportLength + overscan;
630
+ let low = 0;
631
+ let high = layout.items.length;
632
+ while (low < high) {
633
+ const middle = Math.floor((low + high) / 2);
634
+ if (itemStart(layout.items[middle], layout.horizontal) < start)
635
+ low = middle + 1;
636
+ else high = middle;
637
+ }
638
+ // Grid items in a band share a start coordinate but can have different
639
+ // heights. Include the preceding band when any of its items overlaps the
640
+ // viewport; every earlier band is guaranteed to have ended before it.
641
+ if (low > 0) {
642
+ const previousStart = itemStart(layout.items[low - 1], layout.horizontal);
643
+ let bandStart = low - 1;
644
+ while (
645
+ bandStart > 0 &&
646
+ itemStart(layout.items[bandStart - 1], layout.horizontal) ===
647
+ previousStart
648
+ )
649
+ bandStart -= 1;
650
+ if (
651
+ layout.items
652
+ .slice(bandStart, low)
653
+ .some((item) => itemEnd(item, layout.horizontal) >= start)
654
+ )
655
+ low = bandStart;
656
+ }
657
+ const visible: WebLayoutItem[] = [];
658
+ for (let index = low; index < layout.items.length; index += 1) {
659
+ const item = layout.items[index];
660
+ if (itemStart(item, layout.horizontal) > end) break;
661
+ if (itemEnd(item, layout.horizontal) >= start) visible.push(item);
662
+ }
663
+ return visible;
664
+ }
665
+
666
+ export function webLayoutItemsForMount(
667
+ layout: WebListLayout,
668
+ offset: number,
669
+ viewportLength: number,
670
+ virtualizationEnabled: boolean,
671
+ overscan = 0
672
+ ): readonly WebLayoutItem[] {
673
+ return virtualizationEnabled
674
+ ? visibleWebLayoutItems(layout, offset, viewportLength, overscan)
675
+ : layout.items;
676
+ }
677
+
678
+ function rowWithoutSelectionState(row: RowModel): unknown {
679
+ const copy = { ...row } as Record<string, unknown>;
680
+ delete copy.selected;
681
+ if (
682
+ row.type === 'sectionHeader' ||
683
+ row.type === 'action' ||
684
+ row.type === 'dataRow'
685
+ ) {
686
+ if (row.checkbox) copy.checkbox = { ...row.checkbox, state: undefined };
687
+ }
688
+ if (row.type === 'identity' || row.type === 'action') {
689
+ copy.trailing = row.trailing?.map((accessory) =>
690
+ accessory.kind === 'checkbox'
691
+ ? { ...accessory, state: undefined }
692
+ : accessory
693
+ );
694
+ }
695
+ return copy;
696
+ }
697
+
698
+ export function webRowRenderSignature(row: RowModel): string {
699
+ return JSON.stringify(rowWithoutSelectionState(row));
700
+ }
701
+
702
+ export const WEB_LIST_CSS = `
703
+ .ok-native-list-root{--nl-bg:#f7f7f7;--nl-row:#fff;--nl-selected:#eaf2ff;--nl-pressed:#e8e8e8;--nl-subdued:#f9f9f9;--nl-strong:#0000000f;--nl-primary:#111;--nl-secondary:#6b7280;--nl-disabled:#8d8d8d;--nl-icon:#111;--nl-icon-subdued:#8d8d8d;--nl-separator:#e5e7eb;--nl-accent:#2f6bff;--nl-positive:#15803d;--nl-negative:#dc2626;--nl-critical:#feecec;--nl-inverse:#202020;--nl-inverse-text:#fcfcfc;--nl-info:#0d74ce;position:absolute;inset:0;display:flex;min-width:0;min-height:0;overflow:hidden;background:var(--nl-bg);color:var(--nl-primary);font-family:Roobert,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-synthesis:none}
704
+ .ok-native-list-viewport-frame{position:relative;flex:1;min-width:0;min-height:0;overflow:hidden}
705
+ .ok-native-list-viewport{position:absolute;inset:0;overflow:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;scrollbar-gutter:stable}
706
+ .ok-native-list-content{position:relative;min-width:100%;min-height:100%}
707
+ .ok-native-list-item{position:absolute;box-sizing:border-box;contain:layout paint style;outline:none}
708
+ .ok-native-list-row{width:100%;height:100%;box-sizing:border-box;display:flex;align-items:center;gap:12px;overflow:hidden;background:var(--nl-row);color:var(--nl-primary);cursor:pointer;user-select:none;-webkit-user-select:none}
709
+ .ok-native-list-item[data-table-alternate="true"]>.ok-native-list-row{background:var(--nl-bg)}
710
+ .ok-native-list-item[data-native-list-selected="true"]>.ok-native-list-row{background:var(--nl-selected)}
711
+ .ok-native-list-item[data-native-list-disabled="true"]>.ok-native-list-row{opacity:.5;cursor:default}
712
+ .ok-native-list-item:not([data-native-list-disabled="true"]):hover>.ok-native-list-row{background:var(--nl-pressed)}
713
+ .ok-native-list-item:not([data-native-list-disabled="true"]):not([data-native-list-selected="true"]):hover>.ok-native-list-wallet-row{background:var(--nl-strong)}
714
+ .ok-native-list-item:not([data-native-list-disabled="true"]):not([data-native-list-selected="true"]):active>.ok-native-list-wallet-row{background:var(--nl-pressed)}
715
+ .ok-native-list-item[data-native-list-selected="true"]:hover>.ok-native-list-wallet-row{background:var(--nl-selected)}
716
+ .ok-native-list-wallet-group{display:flex;width:100%;height:100%;box-sizing:border-box;flex-direction:column;gap:12px;overflow:hidden;border:1px solid var(--nl-separator);border-radius:12px;background:var(--nl-subdued);user-select:none;-webkit-user-select:none}
717
+ .ok-native-list-wallet-member{flex:0 0 68px;height:68px;overflow:hidden;border-radius:12px;background:transparent;cursor:pointer}
718
+ .ok-native-list-wallet-member>.ok-native-list-wallet-row{height:100%;background:transparent}
719
+ .ok-native-list-wallet-member[data-native-list-selected="true"]>.ok-native-list-wallet-row{background:var(--nl-selected)}
720
+ .ok-native-list-wallet-member:not([data-native-list-selected="true"]):hover>.ok-native-list-wallet-row{background:var(--nl-strong)}
721
+ .ok-native-list-wallet-member:not([data-native-list-selected="true"]):active>.ok-native-list-wallet-row{background:var(--nl-pressed)}
722
+ .ok-native-list-item:focus-visible>.ok-native-list-row{outline:2px solid var(--nl-accent);outline-offset:-2px}
723
+ .ok-native-list-item[data-native-list-reorderable="true"]>.ok-native-list-row{cursor:grab}
724
+ .ok-native-list-root[data-native-list-dragging="true"] .ok-native-list-row{cursor:grabbing}
725
+ .ok-native-list-item[data-native-list-animate-reorder="true"]{transition:transform ${WEB_REORDER_ANIMATION.outOfWayDurationMs}ms ${WEB_REORDER_ANIMATION.outOfWayTimingFunction},height ${WEB_REORDER_ANIMATION.outOfWayDurationMs}ms ${WEB_REORDER_ANIMATION.outOfWayTimingFunction}}
726
+ .ok-native-list-item[data-native-list-dragging="true"]>.ok-native-list-row{overflow:hidden;border-radius:12px;background:var(--nl-row)}
727
+ .ok-native-list-item[data-native-list-dragging="true"]>.ok-native-list-row>*{visibility:hidden}
728
+ .ok-native-list-item[data-native-list-dragging="true"]>.ok-native-list-wallet-group{border-color:transparent;background:var(--nl-row)}
729
+ .ok-native-list-item[data-native-list-dragging="true"]>.ok-native-list-wallet-group>*{visibility:hidden}
730
+ .ok-native-list-reorder-preview{position:fixed;left:0;top:0;z-index:100001;pointer-events:none;overflow:hidden;border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,.12);transform-origin:center;will-change:transform;font-family:Roobert,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
731
+ .ok-native-list-reorder-preview>.ok-native-list-row{background:var(--nl-row);cursor:grabbing}
732
+ .ok-native-list-reorder-preview[data-native-list-selected="true"]>.ok-native-list-row{background:var(--nl-selected)}
733
+ .ok-native-list-reorder-count{position:absolute;right:4px;bottom:4px;display:flex;align-items:center;justify-content:center;box-sizing:border-box;min-width:24px;height:24px;padding:0 6px;border:1px solid var(--nl-row);border-radius:12px;background:var(--nl-inverse);color:var(--nl-inverse-text);font-size:12px;line-height:22px;font-weight:600}
734
+ .ok-native-list-item[data-separator="true"]>.ok-native-list-row{border-bottom:1px solid var(--nl-separator)}
735
+ .ok-native-list-item[data-group-position="first"]>.ok-native-list-row{border-radius:12px 12px 0 0}
736
+ .ok-native-list-item[data-group-position="last"]>.ok-native-list-row{border-radius:0 0 12px 12px}
737
+ .ok-native-list-item[data-group-position="single"]>.ok-native-list-row{border-radius:12px}
738
+ .ok-native-list-standard{padding:8px 12px}.ok-native-list-network-row{padding:0 12px}.ok-native-list-wallet-row{padding:4px 8px;flex-direction:column;justify-content:center;gap:4px}.ok-native-list-account-row{padding:4px 12px;gap:8px}
739
+ .ok-native-list-account-row .ok-native-list-visual,.ok-native-list-account-action-row .ok-native-list-visual{width:32px;height:32px;flex-basis:32px}.ok-native-list-account-row .ok-native-list-visual>img,.ok-native-list-account-row .ok-native-list-visual-main,.ok-native-list-account-action-row .ok-native-list-visual>img,.ok-native-list-account-action-row .ok-native-list-visual-main{width:32px;height:32px}.ok-native-list-account-row .ok-native-list-visual>.ok-native-list-visual-corner{width:20px;height:20px;padding:2px}.ok-native-list-account-action-row .ok-native-list-visual{border-radius:8px!important}.ok-native-list-wallet-row .ok-native-list-title{color:var(--nl-secondary);font-size:12px;line-height:16px;font-weight:400}.ok-native-list-item[data-native-list-selected="true"]>.ok-native-list-wallet-row .ok-native-list-title{color:var(--nl-primary)}.ok-native-list-account-row .ok-native-list-title{font-size:16px;line-height:20px;font-weight:400}.ok-native-list-account-row .ok-native-list-secondary{font-size:14px;line-height:20px;font-weight:400}
740
+ .ok-native-list-flex{display:flex;flex:1;min-width:0;flex-direction:column;justify-content:center}.ok-native-list-title{font-size:15px;line-height:20px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ok-native-list-secondary{font-size:13px;line-height:18px;color:var(--nl-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ok-native-list-tertiary{color:var(--nl-secondary)}.ok-native-list-info{color:var(--nl-info)}
741
+ .ok-native-list-identity-row .ok-native-list-title,.ok-native-list-identity-row .ok-native-list-amounts>span:first-child{font-size:16px;line-height:20px}.ok-native-list-identity-row .ok-native-list-secondary{font-size:14px;line-height:20px}
742
+ .ok-native-list-value{font-size:14px;line-height:20px;font-weight:500;white-space:nowrap}.ok-native-list-time{font-size:11px;line-height:16px;color:var(--nl-secondary);align-self:flex-start}.ok-native-list-amounts{display:flex;flex-direction:column;align-items:flex-end;min-width:0}.ok-native-list-actions{display:flex;gap:16px;margin-top:6px}
743
+ .ok-native-list-action-button{appearance:none;border:0;background:transparent;padding:2px;color:var(--nl-accent);font:600 12px/16px inherit;cursor:pointer}.ok-native-list-action-button[data-tone="danger"]{color:var(--nl-negative)}.ok-native-list-action-button:disabled{opacity:.5;cursor:default}
744
+ .ok-native-list-visual{position:relative;flex:0 0 40px;width:40px;height:40px;border-radius:20px;overflow:visible;background:var(--nl-strong)}.ok-native-list-visual>img,.ok-native-list-visual-main{display:block;width:40px;height:40px;border-radius:inherit;object-fit:cover}.ok-native-list-visual-fallback{display:flex;width:100%;height:100%;align-items:center;justify-content:center;border-radius:inherit;color:var(--nl-primary);font-size:15px;font-weight:600;overflow:hidden}.ok-native-list-visual>.ok-native-list-visual-corner{position:absolute;right:-4px;bottom:-4px;width:18px;height:18px;padding:2px;border-radius:50%;background:var(--nl-row);object-fit:cover}.ok-native-list-stacked{display:flex;align-items:center;width:48px;flex-basis:48px}.ok-native-list-stacked img{width:28px;height:28px;border:2px solid var(--nl-row);border-radius:50%;object-fit:cover;margin-left:-10px}.ok-native-list-stacked img:first-child{margin-left:0}
745
+ .ok-native-list-accessories{display:flex;align-items:center;gap:10px;min-width:0}.ok-native-list-accessory{font-size:14px;color:var(--nl-primary);white-space:nowrap}.ok-native-list-accessory-secondary{color:var(--nl-secondary)}.ok-native-list-icon-button{display:inline-flex;align-items:center;justify-content:center;min-width:24px;height:24px;border:0;background:transparent;color:var(--nl-icon);font-size:20px;line-height:1;font-weight:500;font-family:inherit;cursor:pointer}.ok-native-list-checkbox{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;width:20px;height:20px;border:2px solid color-mix(in srgb,var(--nl-primary) 19%,transparent);border-radius:5px;background:var(--nl-row);cursor:pointer}.ok-native-list-checkbox[data-state="checked"],.ok-native-list-checkbox[data-state="indeterminate"]{border-color:transparent;background:var(--nl-primary)}.ok-native-list-checkbox[data-state="checked"]::after{content:"";width:6px;height:10px;margin-top:-2px;border-right:2px solid var(--nl-row);border-bottom:2px solid var(--nl-row);transform:rotate(45deg)}.ok-native-list-checkbox[data-state="indeterminate"]::after{content:"";width:8px;height:2px;border-radius:1px;background:var(--nl-row)}.ok-native-list-spinner{width:16px;height:16px;border:2px solid var(--nl-separator);border-top-color:var(--nl-primary);border-radius:50%;animation:ok-native-list-spin .8s linear infinite}@keyframes ok-native-list-spin{to{transform:rotate(360deg)}}
746
+ .ok-native-list-badge{display:inline-flex;align-items:center;max-width:100%;height:18px;padding:0 5px;border-radius:5px;background:color-mix(in srgb,var(--nl-accent) 12%,transparent);color:var(--nl-accent);font-size:11px;line-height:18px;white-space:nowrap}.ok-native-list-badge[data-tone="danger"]{background:var(--nl-critical);color:var(--nl-negative)}.ok-native-list-badges{display:inline-flex;gap:4px;margin-left:6px;vertical-align:middle}
747
+ .ok-native-list-section{padding:0 8px;background:var(--nl-bg);gap:8px}.ok-native-list-section[data-variant="summary"]{padding:0 20px}.ok-native-list-section[data-variant="gallery"]{padding:0 12px}.ok-native-list-section[data-variant="history"]{padding:0 8px}.ok-native-list-section-title{font-size:13px;line-height:18px;font-weight:700;color:var(--nl-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ok-native-list-section[data-variant="summary"] .ok-native-list-section-title{font-size:16px;font-weight:500;color:var(--nl-primary)}.ok-native-list-section[data-variant="gallery"] .ok-native-list-section-title{font-size:16px;color:var(--nl-primary)}.ok-native-list-section[data-variant="history"] .ok-native-list-section-title{font-size:12px;line-height:16px}.ok-native-list-section-value{margin-left:auto}
748
+ .ok-native-list-action-row{justify-content:space-between;padding:0 16px}.ok-native-list-action-title{font-size:15px;font-weight:600;color:var(--nl-accent)}.ok-native-list-action-title[data-tone="danger"]{color:var(--nl-negative)}
749
+ .ok-native-list-account-action-row{justify-content:flex-start;gap:12px;padding:0 12px}.ok-native-list-account-action-row .ok-native-list-action-title{color:var(--nl-secondary);font-size:16px;line-height:24px;font-weight:400}
750
+ .ok-native-list-system{justify-content:center;padding:8px 12px;background:var(--nl-bg);color:var(--nl-secondary);cursor:default}.ok-native-list-system[data-variant="retry"],.ok-native-list-system[data-variant="noMatch"],.ok-native-list-system[data-variant="end"]{justify-content:flex-start}.ok-native-list-system[data-variant="retry"]{cursor:pointer}
751
+ .ok-native-list-rail{padding:4px;gap:6px;border-radius:8px}.ok-native-list-rail .ok-native-list-visual{width:20px;height:20px;flex-basis:20px}.ok-native-list-rail .ok-native-list-visual>img,.ok-native-list-rail .ok-native-list-visual-main{width:20px;height:20px}.ok-native-list-rail-title{font-size:12px;font-weight:500;white-space:nowrap}
752
+ .ok-native-list-media{display:block;padding:0 5px;background:transparent;border-radius:16px}.ok-native-list-media-image{display:block;width:100%;aspect-ratio:1;border-radius:10px;background:var(--nl-strong);object-fit:cover}.ok-native-list-media-image[data-state="empty"]{background:transparent}.ok-native-list-media-image[data-state="error"]{display:flex;align-items:center;justify-content:center;color:var(--nl-icon-subdued);font-size:24px}.ok-native-list-media-meta{padding-top:7px}.ok-native-list-media-subtitle-row{display:flex;align-items:center;gap:6px}.ok-native-list-media-subtitle{flex:1;min-width:0;font-size:12px;color:var(--nl-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ok-native-list-media-network{width:14px;height:14px;border-radius:50%}.ok-native-list-media-title{font-size:16px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ok-native-list-media-close{position:absolute;right:9px;top:4px;border:0;background:color-mix(in srgb,var(--nl-inverse) 72%,transparent);color:var(--nl-inverse-text);width:24px;height:24px;border-radius:50%;font:18px/20px inherit;cursor:pointer}
753
+ .ok-native-list-metric{display:flex;flex-direction:column;align-items:flex-start;padding:12px;border-radius:12px;gap:5px;background:var(--nl-row)}.ok-native-list-metric-value{font-size:22px;line-height:28px;font-weight:700}.ok-native-list-composite{display:flex;flex-direction:column;align-items:stretch;padding:14px;border-radius:12px;gap:12px;background:var(--nl-subdued)}.ok-native-list-composite-heading{font-size:14px;letter-spacing:1px;color:var(--nl-secondary)}.ok-native-list-composite-row{display:flex;gap:12px}.ok-native-list-composite-cell{flex:1;min-width:0}.ok-native-list-composite-cell[data-shaded="true"]{padding:10px;border-radius:10px;background:color-mix(in srgb,var(--nl-primary) 5%,transparent)}.ok-native-list-composite-value{font-size:18px;font-weight:600}.ok-native-list-divider{height:1px;background:var(--nl-separator)}.ok-native-list-progress{height:4px;border-radius:2px;overflow:hidden;background:var(--nl-negative)}.ok-native-list-progress>span{display:block;height:100%;border-radius:2px;background:var(--nl-positive)}
754
+ .ok-native-list-data{padding:6px 12px}.ok-native-list-index{flex:0 0 28px;color:var(--nl-secondary);font-size:13px}.ok-native-list-favorite{flex:0 0 24px;color:var(--nl-icon-subdued);font-size:22px}.ok-native-list-favorite[data-active="true"]{color:var(--nl-accent)}.ok-native-list-data-cell{display:flex;flex-direction:column;min-width:0}.ok-native-list-data-cell[data-align="center"]{align-items:center}.ok-native-list-data-cell[data-align="end"]{align-items:flex-end}.ok-native-list-data-primary{display:flex;align-items:center;gap:5px;max-width:100%;font-size:16px;font-weight:500;white-space:nowrap}.ok-native-list-unread{width:7px;height:7px;flex:0 0 7px;border-radius:50%;background:var(--nl-accent)}.ok-native-list-thumbnail{width:64px;height:64px;border-radius:10px;object-fit:cover}
755
+ .ok-native-list-footer{flex:0 0 auto;min-height:0}.ok-native-list-sticky{position:absolute;z-index:4;left:0;right:0;top:0;pointer-events:auto;box-shadow:0 1px 0 var(--nl-separator)}.ok-native-list-index-rail{position:absolute;z-index:6;top:0;right:0;bottom:0;width:44px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0;touch-action:none}.ok-native-list-index-rail[hidden]{display:none}.ok-native-list-index-button{appearance:none;border:0;background:transparent;display:flex;flex:1;max-height:22px;min-height:12px;width:100%;align-items:center;justify-content:center;padding:0;color:var(--nl-secondary);font:600 11px/1 inherit;cursor:pointer}.ok-native-list-index-button[data-active="true"]{color:var(--nl-accent)}.ok-native-list-index-preview{position:absolute;z-index:8;left:50%;top:50%;display:flex;width:72px;height:72px;align-items:center;justify-content:center;transform:translate(-50%,-50%) scale(.92);border-radius:16px;background:var(--nl-inverse);color:var(--nl-inverse-text);font-size:28px;font-weight:600;opacity:0;pointer-events:none;transition:opacity .15s ease,transform .15s ease}.ok-native-list-index-preview[data-visible="true"]{opacity:1;transform:translate(-50%,-50%) scale(1)}
756
+ .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)}
757
+ @media (prefers-reduced-motion:reduce){.ok-native-list-index-preview,.ok-native-list-refresh{transition:none}.ok-native-list-spinner{animation:none}}
758
+ `;
759
+
760
+ function createElement(
761
+ document: Document,
762
+ tag: string,
763
+ className?: string,
764
+ text?: string
765
+ ): HTMLElement {
766
+ const element = document.createElement(tag);
767
+ if (className) element.className = className;
768
+ if (text !== undefined) element.textContent = text;
769
+ return element;
770
+ }
771
+
772
+ function setData(
773
+ element: HTMLElement,
774
+ name: string,
775
+ value: string | number | boolean | undefined
776
+ ) {
777
+ if (value === undefined) delete element.dataset[name];
778
+ else element.dataset[name] = String(value);
779
+ }
780
+
781
+ function safeImageUri(uri: string): string | undefined {
782
+ const trimmed = uri.trim();
783
+ if (
784
+ /^(https?:|data:image\/|blob:|file:)/i.test(trimmed) ||
785
+ trimmed.startsWith('/')
786
+ )
787
+ return trimmed;
788
+ return undefined;
789
+ }
790
+
791
+ function createImage(
792
+ context: RenderContext,
793
+ source: ImageSource,
794
+ className?: string
795
+ ): HTMLImageElement | undefined {
796
+ const uri = safeImageUri(source.uri);
797
+ if (!uri) return undefined;
798
+ const image = context.document.createElement('img');
799
+ if (className) image.className = className;
800
+ image.src = uri;
801
+ image.alt = '';
802
+ image.draggable = false;
803
+ image.loading = 'lazy';
804
+ image.decoding = 'async';
805
+ image.style.objectFit =
806
+ source.contentFit === 'fill' ? 'fill' : source.contentFit ?? 'cover';
807
+ return image;
808
+ }
809
+
810
+ function iconGlyph(name: string): string {
811
+ const normalized = name.toLocaleLowerCase();
812
+ if (normalized.includes('chevron')) {
813
+ if (normalized.includes('top')) return '⌃';
814
+ if (normalized.includes('bottom')) return '⌄';
815
+ return '›';
816
+ }
817
+ if (normalized.includes('plus') || normalized.includes('add')) return '+';
818
+ if (normalized.includes('minus')) return '−';
819
+ if (normalized.includes('star')) return '☆';
820
+ if (normalized.includes('drag') || normalized.includes('grabber')) return '≡';
821
+ if (normalized.includes('dot') || normalized.includes('more')) return '•••';
822
+ if (normalized.includes('pencil') || normalized.includes('edit')) return '✎';
823
+ if (normalized.includes('error') || normalized.includes('exclamation'))
824
+ return '!';
825
+ if (normalized.includes('info')) return 'i';
826
+ if (normalized.includes('question')) return '?';
827
+ if (normalized.includes('swap')) return '⇄';
828
+ if (normalized.includes('trend') || normalized.includes('arrow')) return '↗';
829
+ return name.slice(0, 1).toLocaleUpperCase();
830
+ }
831
+
832
+ function visualFromRow(row: RowModel): LeadingVisual | undefined {
833
+ if (row.type === 'identity') return row.leading;
834
+ if (row.type === 'rail') return row.visual;
835
+ if (row.type === 'activity') return row.leading;
836
+ if (row.type === 'message') return row.leading;
837
+ if (row.type === 'dataRow') return row.leading;
838
+ if (row.type === 'metricCard') return row.visual;
839
+ return undefined;
840
+ }
841
+
842
+ function createVisual(
843
+ context: RenderContext,
844
+ visual: LeadingVisual | undefined
845
+ ): HTMLElement | undefined {
846
+ if (!visual) return undefined;
847
+ if (visual.kind === 'stackedImages') {
848
+ const stack = createElement(
849
+ context.document,
850
+ 'span',
851
+ 'ok-native-list-stacked'
852
+ );
853
+ visual.images.forEach((source) => {
854
+ const image = createImage(context, source);
855
+ if (image) stack.appendChild(image);
856
+ });
857
+ return stack;
858
+ }
859
+
860
+ const frame = createElement(
861
+ context.document,
862
+ 'span',
863
+ 'ok-native-list-visual'
864
+ );
865
+ const shape = visual.kind === 'icon' ? 'circle' : visual.shape ?? 'circle';
866
+ frame.style.borderRadius =
867
+ shape === 'square' ? '0' : shape === 'rounded' ? '10px' : '50%';
868
+ if (visual.kind === 'icon') {
869
+ frame.style.background = visual.backgroundColor ?? 'var(--nl-strong)';
870
+ const fallback = createElement(
871
+ context.document,
872
+ 'span',
873
+ 'ok-native-list-visual-fallback',
874
+ iconGlyph(visual.name)
875
+ );
876
+ if (visual.tintColor) fallback.style.color = visual.tintColor;
877
+ frame.appendChild(fallback);
878
+ return frame;
879
+ }
880
+
881
+ if ('backgroundColor' in visual && visual.backgroundColor)
882
+ frame.style.background = visual.backgroundColor;
883
+ const source = visual.kind === 'image' ? visual.image : visual.image;
884
+ const image = source ? createImage(context, source) : undefined;
885
+ if (image) {
886
+ image.className = 'ok-native-list-visual-main';
887
+ frame.appendChild(image);
888
+ } else {
889
+ frame.appendChild(
890
+ createElement(
891
+ context.document,
892
+ 'span',
893
+ 'ok-native-list-visual-fallback',
894
+ 'fallbackText' in visual ? visual.fallbackText ?? '' : ''
895
+ )
896
+ );
897
+ }
898
+ if (visual.kind === 'token' && visual.networkImage) {
899
+ const corner = createImage(
900
+ context,
901
+ visual.networkImage,
902
+ 'ok-native-list-visual-corner'
903
+ );
904
+ if (corner) frame.appendChild(corner);
905
+ } else if ('cornerIcon' in visual && visual.cornerIcon) {
906
+ const corner = createElement(
907
+ context.document,
908
+ 'span',
909
+ 'ok-native-list-visual-corner ok-native-list-visual-fallback',
910
+ iconGlyph(visual.cornerIcon.name)
911
+ );
912
+ if (visual.cornerIcon.tintColor)
913
+ corner.style.color = visual.cornerIcon.tintColor;
914
+ if (visual.cornerIcon.backgroundColor)
915
+ corner.style.background = visual.cornerIcon.backgroundColor;
916
+ frame.appendChild(corner);
917
+ }
918
+ return frame;
919
+ }
920
+
921
+ function toneColor(
922
+ tone: TextTone | undefined,
923
+ fallback: 'primary' | 'secondary'
924
+ ): string {
925
+ if (tone === 'positive') return 'var(--nl-positive)';
926
+ if (tone === 'negative') return 'var(--nl-negative)';
927
+ if (tone === 'secondary') return 'var(--nl-secondary)';
928
+ return fallback === 'secondary' ? 'var(--nl-secondary)' : 'var(--nl-primary)';
929
+ }
930
+
931
+ function createBadge(
932
+ context: RenderContext,
933
+ badge: Readonly<{ text: string; tone?: string }>
934
+ ): HTMLElement {
935
+ const element = createElement(
936
+ context.document,
937
+ 'span',
938
+ 'ok-native-list-badge',
939
+ badge.text
940
+ );
941
+ setData(element, 'tone', badge.tone);
942
+ return element;
943
+ }
944
+
945
+ function selectionKeysForTarget(
946
+ target: SelectionTarget | undefined,
947
+ rowKey: string,
948
+ snapshot: NativeListSnapshot
949
+ ): readonly string[] {
950
+ if (!target || target.scope === 'row') {
951
+ return snapshot.rows.some(
952
+ (row) => row.key === rowKey && isSelectableRow(row)
953
+ )
954
+ ? [rowKey]
955
+ : [];
956
+ }
957
+ if (target.scope === 'section') {
958
+ return snapshot.rows
959
+ .filter(
960
+ (row) => row.sectionKey === target.sectionKey && isSelectableRow(row)
961
+ )
962
+ .map((row) => row.key);
963
+ }
964
+ return snapshot.rows.filter(isSelectableRow).map((row) => row.key);
965
+ }
966
+
967
+ function checkboxState(
968
+ target: SelectionTarget | undefined,
969
+ fallback: CheckboxState,
970
+ rowKey: string,
971
+ snapshot: NativeListSnapshot,
972
+ selectedKeys: ReadonlySet<string>
973
+ ): CheckboxState {
974
+ if (target?.scope === 'section') {
975
+ return checkboxStateForSection(
976
+ target.sectionKey,
977
+ snapshot.rows,
978
+ selectedKeys
979
+ );
980
+ }
981
+ return checkboxStateForKeys(
982
+ selectionKeysForTarget(target, rowKey, snapshot),
983
+ selectedKeys
984
+ );
985
+ }
986
+
987
+ function createCheckbox(
988
+ context: RenderContext,
989
+ rowKey: string,
990
+ accessory: Extract<TrailingAccessory, { kind: 'checkbox' }>
991
+ ): HTMLElement {
992
+ if (accessory.loading) {
993
+ const spinner = createElement(
994
+ context.document,
995
+ 'span',
996
+ 'ok-native-list-spinner'
997
+ );
998
+ spinner.setAttribute('aria-label', 'Loading');
999
+ return spinner;
1000
+ }
1001
+ const state = checkboxState(
1002
+ accessory.target,
1003
+ accessory.state,
1004
+ rowKey,
1005
+ context.snapshot,
1006
+ context.selectedKeys
1007
+ );
1008
+ const element = createElement(
1009
+ context.document,
1010
+ 'button',
1011
+ 'ok-native-list-checkbox'
1012
+ );
1013
+ element.setAttribute('type', 'button');
1014
+ element.setAttribute('role', 'checkbox');
1015
+ element.setAttribute(
1016
+ 'aria-checked',
1017
+ state === 'indeterminate' ? 'mixed' : String(state === 'checked')
1018
+ );
1019
+ element.setAttribute('aria-label', 'Select');
1020
+ element.toggleAttribute('disabled', Boolean(accessory.disabled));
1021
+ setData(element, 'state', state);
1022
+ setData(element, 'checkboxFallback', accessory.state);
1023
+ setData(element, 'nativeListAction', accessory.actionKey ?? 'selection');
1024
+ setData(element, 'selectionScope', accessory.target?.scope ?? 'row');
1025
+ if (accessory.target?.scope === 'section')
1026
+ setData(element, 'selectionKey', accessory.target.sectionKey);
1027
+ else if (accessory.target?.scope === 'row')
1028
+ setData(element, 'selectionKey', rowKey);
1029
+ return element;
1030
+ }
1031
+
1032
+ function createIconAction(
1033
+ context: RenderContext,
1034
+ name: string,
1035
+ actionKey: string | undefined,
1036
+ disabled?: boolean,
1037
+ tintColor?: string
1038
+ ): HTMLElement {
1039
+ const element = createElement(
1040
+ context.document,
1041
+ actionKey ? 'button' : 'span',
1042
+ 'ok-native-list-icon-button',
1043
+ iconGlyph(name)
1044
+ );
1045
+ if (element.tagName === 'BUTTON') {
1046
+ element.setAttribute('type', 'button');
1047
+ element.toggleAttribute('disabled', Boolean(disabled));
1048
+ }
1049
+ if (actionKey) setData(element, 'nativeListAction', actionKey);
1050
+ if (tintColor) element.style.color = tintColor;
1051
+ return element;
1052
+ }
1053
+
1054
+ function markActionAnchorSource(
1055
+ element: HTMLElement,
1056
+ source: NativeListActionSource,
1057
+ slot?: number
1058
+ ) {
1059
+ setData(element, 'nativeListAnchorSource', source);
1060
+ if (slot !== undefined) setData(element, 'nativeListAnchorSlot', slot);
1061
+ }
1062
+
1063
+ function createAccessory(
1064
+ context: RenderContext,
1065
+ rowKey: string,
1066
+ accessory: TrailingAccessory,
1067
+ slot: number
1068
+ ): HTMLElement {
1069
+ if (accessory.kind === 'checkbox') {
1070
+ const element = createCheckbox(context, rowKey, accessory);
1071
+ markActionAnchorSource(element, 'trailingAccessory', slot);
1072
+ return element;
1073
+ }
1074
+ if (accessory.kind === 'icon') {
1075
+ const element = createIconAction(
1076
+ context,
1077
+ accessory.name,
1078
+ accessory.actionKey,
1079
+ accessory.disabled,
1080
+ accessory.tintColor
1081
+ );
1082
+ markActionAnchorSource(element, 'trailingAccessory', slot);
1083
+ return element;
1084
+ }
1085
+ if (accessory.kind === 'spinner') {
1086
+ return createElement(context.document, 'span', 'ok-native-list-spinner');
1087
+ }
1088
+ const actionKey = 'actionKey' in accessory ? accessory.actionKey : undefined;
1089
+ const element = createElement(
1090
+ context.document,
1091
+ actionKey ? 'button' : 'span',
1092
+ 'ok-native-list-accessory'
1093
+ );
1094
+ if (element.tagName === 'BUTTON') {
1095
+ element.setAttribute('type', 'button');
1096
+ element.classList.add('ok-native-list-icon-button');
1097
+ setData(element, 'nativeListAction', actionKey);
1098
+ }
1099
+ switch (accessory.kind) {
1100
+ case 'value':
1101
+ element.textContent = accessory.text;
1102
+ if (accessory.secondary)
1103
+ element.classList.add('ok-native-list-accessory-secondary');
1104
+ break;
1105
+ case 'valuePair': {
1106
+ const primary = createElement(
1107
+ context.document,
1108
+ 'span',
1109
+ undefined,
1110
+ accessory.primary
1111
+ );
1112
+ primary.style.color = toneColor(accessory.primaryTone, 'primary');
1113
+ const secondary = createElement(
1114
+ context.document,
1115
+ 'span',
1116
+ 'ok-native-list-secondary',
1117
+ accessory.secondary
1118
+ );
1119
+ secondary.style.color = toneColor(accessory.secondaryTone, 'secondary');
1120
+ element.replaceChildren(primary, secondary);
1121
+ element.classList.add('ok-native-list-amounts');
1122
+ break;
1123
+ }
1124
+ case 'radio':
1125
+ element.textContent = accessory.checked ? '●' : '○';
1126
+ break;
1127
+ case 'switch':
1128
+ element.textContent = accessory.value ? 'ON' : 'OFF';
1129
+ setData(element, 'nativeListAction', accessory.actionKey);
1130
+ break;
1131
+ case 'chevron':
1132
+ element.textContent = '›';
1133
+ break;
1134
+ case 'menu':
1135
+ element.textContent = '⋮';
1136
+ break;
1137
+ case 'drag':
1138
+ element.textContent = '≡';
1139
+ break;
1140
+ case 'progress':
1141
+ element.textContent = String(Math.round(accessory.value * 100)) + '%';
1142
+ break;
1143
+ }
1144
+ if (actionKey) markActionAnchorSource(element, 'trailingAccessory', slot);
1145
+ return element;
1146
+ }
1147
+
1148
+ function appendAccessories(
1149
+ parent: HTMLElement,
1150
+ context: RenderContext,
1151
+ rowKey: string,
1152
+ accessories: readonly TrailingAccessory[] | undefined
1153
+ ) {
1154
+ if (!accessories?.length) return;
1155
+ const container = createElement(
1156
+ context.document,
1157
+ 'span',
1158
+ 'ok-native-list-accessories'
1159
+ );
1160
+ accessories.forEach((accessory, slot) =>
1161
+ container.appendChild(createAccessory(context, rowKey, accessory, slot))
1162
+ );
1163
+ parent.appendChild(container);
1164
+ }
1165
+
1166
+ function createTextColumn(
1167
+ context: RenderContext,
1168
+ title: string,
1169
+ subtitle?: string,
1170
+ tertiary?: string,
1171
+ tertiaryTone?: 'secondary' | 'info',
1172
+ badges?: readonly Readonly<{ text: string; tone?: string }>[]
1173
+ ): HTMLElement {
1174
+ const column = createElement(context.document, 'span', 'ok-native-list-flex');
1175
+ const titleLine = createElement(
1176
+ context.document,
1177
+ 'span',
1178
+ 'ok-native-list-title',
1179
+ title
1180
+ );
1181
+ if (badges?.length) {
1182
+ const badgeLine = createElement(
1183
+ context.document,
1184
+ 'span',
1185
+ 'ok-native-list-badges'
1186
+ );
1187
+ badges.forEach((badge) =>
1188
+ badgeLine.appendChild(createBadge(context, badge))
1189
+ );
1190
+ titleLine.appendChild(badgeLine);
1191
+ }
1192
+ column.appendChild(titleLine);
1193
+ if (subtitle)
1194
+ column.appendChild(
1195
+ createElement(
1196
+ context.document,
1197
+ 'span',
1198
+ 'ok-native-list-secondary',
1199
+ subtitle
1200
+ )
1201
+ );
1202
+ if (tertiary) {
1203
+ const element = createElement(
1204
+ context.document,
1205
+ 'span',
1206
+ tertiaryTone === 'info'
1207
+ ? 'ok-native-list-secondary ok-native-list-info'
1208
+ : 'ok-native-list-secondary ok-native-list-tertiary',
1209
+ tertiary
1210
+ );
1211
+ column.appendChild(element);
1212
+ }
1213
+ return column;
1214
+ }
1215
+
1216
+ function createSectionHeader(
1217
+ context: RenderContext,
1218
+ row: Extract<RowModel, { type: 'sectionHeader' }>
1219
+ ): HTMLElement {
1220
+ const body = createElement(
1221
+ context.document,
1222
+ 'div',
1223
+ 'ok-native-list-row ok-native-list-section'
1224
+ );
1225
+ setData(body, 'variant', row.variant);
1226
+ if (row.titleIcon) {
1227
+ const titleIcon = createIconAction(
1228
+ context,
1229
+ row.titleIcon.name,
1230
+ row.titleIcon.actionKey,
1231
+ row.titleIcon.disabled,
1232
+ row.titleIcon.tintColor
1233
+ );
1234
+ markActionAnchorSource(titleIcon, 'leadingAction');
1235
+ body.appendChild(titleIcon);
1236
+ }
1237
+ body.appendChild(createTextColumn(context, row.title, row.subtitle));
1238
+ if (row.value) {
1239
+ const value = createElement(
1240
+ context.document,
1241
+ row.valueActionKey ? 'button' : 'span',
1242
+ row.valueActionKey
1243
+ ? 'ok-native-list-action-button ok-native-list-section-value'
1244
+ : 'ok-native-list-value ok-native-list-section-value',
1245
+ row.value
1246
+ );
1247
+ if (row.valueActionKey) {
1248
+ value.setAttribute('type', 'button');
1249
+ setData(value, 'nativeListAction', row.valueActionKey);
1250
+ markActionAnchorSource(value, 'trailingAccessory', 0);
1251
+ }
1252
+ body.appendChild(value);
1253
+ }
1254
+ if (row.valueIcon) {
1255
+ const valueIcon = createIconAction(
1256
+ context,
1257
+ row.valueIcon.name,
1258
+ row.valueIcon.actionKey,
1259
+ row.valueIcon.disabled,
1260
+ row.valueIcon.tintColor
1261
+ );
1262
+ markActionAnchorSource(valueIcon, 'trailingAccessory', 1);
1263
+ body.appendChild(valueIcon);
1264
+ }
1265
+ if (row.checkbox)
1266
+ body.appendChild(createCheckbox(context, row.key, row.checkbox));
1267
+ return body;
1268
+ }
1269
+
1270
+ function createActionRow(
1271
+ context: RenderContext,
1272
+ row: Extract<RowModel, { type: 'action' }>
1273
+ ): HTMLElement {
1274
+ const body = createElement(
1275
+ context.document,
1276
+ 'div',
1277
+ [
1278
+ 'ok-native-list-row',
1279
+ 'ok-native-list-action-row',
1280
+ row.presentation === 'accountSelector'
1281
+ ? 'ok-native-list-account-action-row'
1282
+ : '',
1283
+ ]
1284
+ .filter(Boolean)
1285
+ .join(' ')
1286
+ );
1287
+ if (row.icon) body.appendChild(createVisual(context, row.icon)!);
1288
+ const title = createElement(
1289
+ context.document,
1290
+ 'span',
1291
+ 'ok-native-list-action-title',
1292
+ row.title
1293
+ );
1294
+ setData(title, 'tone', row.tone);
1295
+ body.appendChild(title);
1296
+ if (row.checkbox)
1297
+ body.appendChild(createCheckbox(context, row.key, row.checkbox));
1298
+ appendAccessories(body, context, row.key, row.trailing);
1299
+ return body;
1300
+ }
1301
+
1302
+ function createSystemRow(
1303
+ context: RenderContext,
1304
+ row: Extract<RowModel, { type: 'system' }>
1305
+ ): HTMLElement {
1306
+ const body = createElement(
1307
+ context.document,
1308
+ 'div',
1309
+ 'ok-native-list-row ok-native-list-system'
1310
+ );
1311
+ setData(body, 'variant', row.variant);
1312
+ if (row.variant === 'loading')
1313
+ body.appendChild(
1314
+ createElement(context.document, 'span', 'ok-native-list-spinner')
1315
+ );
1316
+ const message =
1317
+ row.variant === 'spacer'
1318
+ ? ''
1319
+ : row.message ?? (row.variant === 'end' ? 'End' : '');
1320
+ if (message)
1321
+ body.appendChild(
1322
+ createElement(
1323
+ context.document,
1324
+ 'span',
1325
+ 'ok-native-list-secondary',
1326
+ message
1327
+ )
1328
+ );
1329
+ return body;
1330
+ }
1331
+
1332
+ function createRailRow(
1333
+ context: RenderContext,
1334
+ row: Extract<RowModel, { type: 'rail' }>
1335
+ ): HTMLElement {
1336
+ const body = createElement(
1337
+ context.document,
1338
+ 'div',
1339
+ 'ok-native-list-row ok-native-list-rail'
1340
+ );
1341
+ const visual = createVisual(context, row.visual);
1342
+ if (visual) body.appendChild(visual);
1343
+ body.appendChild(
1344
+ createElement(
1345
+ context.document,
1346
+ 'span',
1347
+ 'ok-native-list-rail-title',
1348
+ row.title
1349
+ )
1350
+ );
1351
+ if (row.status && row.status !== 'none')
1352
+ body.appendChild(
1353
+ createElement(
1354
+ context.document,
1355
+ 'span',
1356
+ 'ok-native-list-secondary',
1357
+ row.status
1358
+ )
1359
+ );
1360
+ if (row.badge) body.appendChild(createBadge(context, row.badge));
1361
+ return body;
1362
+ }
1363
+
1364
+ function createMediaRow(
1365
+ context: RenderContext,
1366
+ row: Extract<RowModel, { type: 'mediaTile' }>
1367
+ ): HTMLElement {
1368
+ const body = createElement(
1369
+ context.document,
1370
+ 'div',
1371
+ 'ok-native-list-row ok-native-list-media'
1372
+ );
1373
+ if (row.image && !row.imageState) {
1374
+ const image = createImage(context, row.image, 'ok-native-list-media-image');
1375
+ if (image) body.appendChild(image);
1376
+ } else {
1377
+ const placeholder = createElement(
1378
+ context.document,
1379
+ 'span',
1380
+ 'ok-native-list-media-image',
1381
+ row.imageState === 'error' ? '▧' : ''
1382
+ );
1383
+ setData(placeholder, 'state', row.imageState ?? 'empty');
1384
+ body.appendChild(placeholder);
1385
+ }
1386
+ const metadata = createElement(
1387
+ context.document,
1388
+ 'div',
1389
+ 'ok-native-list-media-meta'
1390
+ );
1391
+ const subtitleLine = createElement(
1392
+ context.document,
1393
+ 'div',
1394
+ 'ok-native-list-media-subtitle-row'
1395
+ );
1396
+ subtitleLine.appendChild(
1397
+ createElement(
1398
+ context.document,
1399
+ 'span',
1400
+ 'ok-native-list-media-subtitle',
1401
+ row.subtitle || '-'
1402
+ )
1403
+ );
1404
+ if (row.networkImage) {
1405
+ const network = createImage(
1406
+ context,
1407
+ row.networkImage,
1408
+ 'ok-native-list-media-network'
1409
+ );
1410
+ if (network) subtitleLine.appendChild(network);
1411
+ }
1412
+ metadata.appendChild(subtitleLine);
1413
+ metadata.appendChild(
1414
+ createElement(
1415
+ context.document,
1416
+ 'div',
1417
+ 'ok-native-list-media-title',
1418
+ row.title
1419
+ )
1420
+ );
1421
+ if (row.badge) metadata.appendChild(createBadge(context, row.badge));
1422
+ body.appendChild(metadata);
1423
+ if (row.closeActionKey) {
1424
+ const close = createElement(
1425
+ context.document,
1426
+ 'button',
1427
+ 'ok-native-list-media-close',
1428
+ '×'
1429
+ );
1430
+ close.setAttribute('type', 'button');
1431
+ close.setAttribute('aria-label', 'Close');
1432
+ setData(close, 'nativeListAction', row.closeActionKey);
1433
+ markActionAnchorSource(close, 'mediaClose');
1434
+ body.appendChild(close);
1435
+ }
1436
+ return body;
1437
+ }
1438
+
1439
+ function createMetricCell(
1440
+ context: RenderContext,
1441
+ metric: NonNullable<
1442
+ Extract<RowModel, { type: 'metricCard' }>['metrics']
1443
+ >[number],
1444
+ shaded: boolean
1445
+ ): HTMLElement {
1446
+ const cell = createElement(
1447
+ context.document,
1448
+ 'div',
1449
+ 'ok-native-list-composite-cell'
1450
+ );
1451
+ setData(cell, 'shaded', shaded);
1452
+ cell.appendChild(
1453
+ createElement(
1454
+ context.document,
1455
+ 'div',
1456
+ 'ok-native-list-secondary',
1457
+ metric.label
1458
+ )
1459
+ );
1460
+ const valueLine = createElement(context.document, 'div');
1461
+ valueLine.style.display = 'flex';
1462
+ valueLine.style.alignItems = 'center';
1463
+ valueLine.style.gap = '6px';
1464
+ if (metric.visual) {
1465
+ const visual = createVisual(context, metric.visual);
1466
+ if (visual) {
1467
+ visual.style.width = '16px';
1468
+ visual.style.height = '16px';
1469
+ visual.style.flexBasis = '16px';
1470
+ const image = visual.querySelector('img');
1471
+ if (image) {
1472
+ image.style.width = '16px';
1473
+ image.style.height = '16px';
1474
+ }
1475
+ valueLine.appendChild(visual);
1476
+ }
1477
+ }
1478
+ const value = createElement(
1479
+ context.document,
1480
+ 'span',
1481
+ 'ok-native-list-composite-value',
1482
+ metric.value
1483
+ );
1484
+ value.style.color = toneColor(metric.tone, 'primary');
1485
+ valueLine.appendChild(value);
1486
+ cell.appendChild(valueLine);
1487
+ return cell;
1488
+ }
1489
+
1490
+ function createMetricRow(
1491
+ context: RenderContext,
1492
+ row: Extract<RowModel, { type: 'metricCard' }>
1493
+ ): HTMLElement {
1494
+ if (row.variant === 'activity' || row.variant === 'performance') {
1495
+ const body = createElement(
1496
+ context.document,
1497
+ 'div',
1498
+ 'ok-native-list-row ok-native-list-composite'
1499
+ );
1500
+ body.appendChild(
1501
+ createElement(
1502
+ context.document,
1503
+ 'div',
1504
+ 'ok-native-list-composite-heading',
1505
+ row.title
1506
+ )
1507
+ );
1508
+ const metrics = row.metrics ?? [];
1509
+ const firstLine = createElement(
1510
+ context.document,
1511
+ 'div',
1512
+ 'ok-native-list-composite-row'
1513
+ );
1514
+ metrics
1515
+ .slice(0, 2)
1516
+ .forEach((metric) =>
1517
+ firstLine.appendChild(createMetricCell(context, metric, false))
1518
+ );
1519
+ body.appendChild(firstLine);
1520
+ if (row.variant === 'activity') {
1521
+ body.appendChild(
1522
+ createElement(context.document, 'div', 'ok-native-list-divider')
1523
+ );
1524
+ } else {
1525
+ const progress = createElement(
1526
+ context.document,
1527
+ 'div',
1528
+ 'ok-native-list-progress'
1529
+ );
1530
+ const fill = createElement(context.document, 'span');
1531
+ fill.style.width = String(Math.round((row.progress ?? 0) * 100)) + '%';
1532
+ progress.appendChild(fill);
1533
+ body.appendChild(progress);
1534
+ }
1535
+ const secondLine = createElement(
1536
+ context.document,
1537
+ 'div',
1538
+ 'ok-native-list-composite-row'
1539
+ );
1540
+ metrics
1541
+ .slice(2)
1542
+ .forEach((metric) =>
1543
+ secondLine.appendChild(
1544
+ createMetricCell(context, metric, row.variant === 'performance')
1545
+ )
1546
+ );
1547
+ body.appendChild(secondLine);
1548
+ return body;
1549
+ }
1550
+
1551
+ const body = createElement(
1552
+ context.document,
1553
+ 'div',
1554
+ 'ok-native-list-row ok-native-list-metric'
1555
+ );
1556
+ if (row.visual) {
1557
+ const visual = createVisual(context, row.visual);
1558
+ if (visual) body.appendChild(visual);
1559
+ }
1560
+ body.appendChild(
1561
+ createElement(
1562
+ context.document,
1563
+ 'div',
1564
+ 'ok-native-list-secondary',
1565
+ row.title
1566
+ )
1567
+ );
1568
+ body.appendChild(
1569
+ createElement(
1570
+ context.document,
1571
+ 'div',
1572
+ 'ok-native-list-metric-value',
1573
+ row.value
1574
+ )
1575
+ );
1576
+ if (row.trend) {
1577
+ const trend = createElement(
1578
+ context.document,
1579
+ 'div',
1580
+ 'ok-native-list-secondary',
1581
+ row.trend
1582
+ );
1583
+ trend.style.color =
1584
+ row.trendTone === 'positive'
1585
+ ? 'var(--nl-positive)'
1586
+ : row.trendTone === 'negative'
1587
+ ? 'var(--nl-negative)'
1588
+ : 'var(--nl-secondary)';
1589
+ body.appendChild(trend);
1590
+ }
1591
+ if (row.subtitle)
1592
+ body.appendChild(
1593
+ createElement(
1594
+ context.document,
1595
+ 'div',
1596
+ 'ok-native-list-secondary',
1597
+ row.subtitle
1598
+ )
1599
+ );
1600
+ if (row.badge) body.appendChild(createBadge(context, row.badge));
1601
+ return body;
1602
+ }
1603
+
1604
+ function createDataRow(
1605
+ context: RenderContext,
1606
+ row: Extract<RowModel, { type: 'dataRow' }>
1607
+ ): HTMLElement {
1608
+ const body = createElement(
1609
+ context.document,
1610
+ 'div',
1611
+ 'ok-native-list-row ok-native-list-data'
1612
+ );
1613
+ if (row.checkbox)
1614
+ body.appendChild(createCheckbox(context, row.key, row.checkbox));
1615
+ if (row.index !== undefined)
1616
+ body.appendChild(
1617
+ createElement(
1618
+ context.document,
1619
+ 'span',
1620
+ 'ok-native-list-index',
1621
+ String(row.index)
1622
+ )
1623
+ );
1624
+ if (row.favorite) {
1625
+ const favorite = createElement(
1626
+ context.document,
1627
+ 'span',
1628
+ 'ok-native-list-favorite',
1629
+ row.favoriteActive ? '★' : '☆'
1630
+ );
1631
+ setData(favorite, 'active', row.favoriteActive);
1632
+ body.appendChild(favorite);
1633
+ }
1634
+ if (row.leading) {
1635
+ const visual = createVisual(context, row.leading);
1636
+ if (visual) body.appendChild(visual);
1637
+ }
1638
+ row.columns.forEach((column) => {
1639
+ const cell = createElement(
1640
+ context.document,
1641
+ 'span',
1642
+ 'ok-native-list-data-cell'
1643
+ );
1644
+ cell.style.flex = String(column.weight ?? 1);
1645
+ setData(cell, 'align', column.alignment ?? 'start');
1646
+ const primary = createElement(
1647
+ context.document,
1648
+ 'span',
1649
+ 'ok-native-list-data-primary'
1650
+ );
1651
+ primary.style.color = toneColor(column.tone, 'primary');
1652
+ if (column.secondaryLeadingText)
1653
+ primary.appendChild(
1654
+ createElement(
1655
+ context.document,
1656
+ 'span',
1657
+ 'ok-native-list-secondary',
1658
+ column.secondaryLeadingText
1659
+ )
1660
+ );
1661
+ primary.appendChild(
1662
+ createElement(context.document, 'span', undefined, column.text)
1663
+ );
1664
+ if (column.key === 'asset' && row.badges?.length) {
1665
+ row.badges.forEach((badge) =>
1666
+ primary.appendChild(createBadge(context, badge))
1667
+ );
1668
+ }
1669
+ cell.appendChild(primary);
1670
+ if (column.secondaryText) {
1671
+ const secondary = createElement(
1672
+ context.document,
1673
+ 'span',
1674
+ 'ok-native-list-secondary',
1675
+ column.secondaryText
1676
+ );
1677
+ secondary.style.color = toneColor(column.secondaryTone, 'secondary');
1678
+ cell.appendChild(secondary);
1679
+ }
1680
+ body.appendChild(cell);
1681
+ });
1682
+ return body;
1683
+ }
1684
+
1685
+ function createIdentityActivityOrMessageRow(
1686
+ context: RenderContext,
1687
+ row: Extract<RowModel, { type: 'identity' | 'activity' | 'message' }>
1688
+ ): HTMLElement {
1689
+ const presentation = row.type === 'identity' ? row.presentation : undefined;
1690
+ const body = createElement(
1691
+ context.document,
1692
+ 'div',
1693
+ [
1694
+ 'ok-native-list-row',
1695
+ 'ok-native-list-standard',
1696
+ row.type === 'identity' && !presentation
1697
+ ? 'ok-native-list-identity-row'
1698
+ : '',
1699
+ presentation === 'networkSelector' ? 'ok-native-list-network-row' : '',
1700
+ presentation === 'walletSidebar' ? 'ok-native-list-wallet-row' : '',
1701
+ presentation === 'accountSelector' ? 'ok-native-list-account-row' : '',
1702
+ ]
1703
+ .filter(Boolean)
1704
+ .join(' ')
1705
+ );
1706
+ if (row.type === 'identity' && row.leadingAction) {
1707
+ const action = createIconAction(
1708
+ context,
1709
+ row.leadingAction.name,
1710
+ row.leadingAction.actionKey,
1711
+ row.leadingAction.disabled,
1712
+ row.leadingAction.tintColor
1713
+ );
1714
+ markActionAnchorSource(action, 'leadingAction');
1715
+ body.appendChild(action);
1716
+ }
1717
+ const visual = createVisual(context, visualFromRow(row));
1718
+ if (visual) body.appendChild(visual);
1719
+ if (row.type === 'activity' && row.secondaryLeading) {
1720
+ const secondVisual = createVisual(context, row.secondaryLeading);
1721
+ if (secondVisual) body.appendChild(secondVisual);
1722
+ }
1723
+ if (row.type === 'message' && row.unread)
1724
+ body.appendChild(
1725
+ createElement(context.document, 'span', 'ok-native-list-unread')
1726
+ );
1727
+ const title = row.title;
1728
+ const subtitle =
1729
+ row.type === 'identity'
1730
+ ? row.subtitle
1731
+ : row.type === 'activity'
1732
+ ? row.description
1733
+ : row.body;
1734
+ const column = createTextColumn(
1735
+ context,
1736
+ title,
1737
+ subtitle,
1738
+ row.type === 'identity' ? row.tertiary : undefined,
1739
+ row.type === 'identity' ? row.tertiaryTone : undefined,
1740
+ row.type === 'identity' ? row.badges : undefined
1741
+ );
1742
+ if (row.type === 'activity' && row.status)
1743
+ column.appendChild(
1744
+ createElement(
1745
+ context.document,
1746
+ 'span',
1747
+ 'ok-native-list-secondary',
1748
+ row.status
1749
+ )
1750
+ );
1751
+ if (row.type === 'activity' && row.footerActions?.length) {
1752
+ const actions = createElement(
1753
+ context.document,
1754
+ 'span',
1755
+ 'ok-native-list-actions'
1756
+ );
1757
+ row.footerActions.forEach((action, slot) => {
1758
+ const button = createElement(
1759
+ context.document,
1760
+ 'button',
1761
+ 'ok-native-list-action-button',
1762
+ action.label
1763
+ );
1764
+ button.setAttribute('type', 'button');
1765
+ button.toggleAttribute('disabled', Boolean(action.disabled));
1766
+ setData(button, 'tone', action.tone);
1767
+ setData(button, 'nativeListAction', action.key);
1768
+ markActionAnchorSource(button, 'footerAction', slot);
1769
+ actions.appendChild(button);
1770
+ });
1771
+ column.appendChild(actions);
1772
+ }
1773
+ body.appendChild(column);
1774
+ if (row.type === 'activity') {
1775
+ const amounts = createElement(
1776
+ context.document,
1777
+ 'span',
1778
+ 'ok-native-list-amounts'
1779
+ );
1780
+ if (row.primaryAmount)
1781
+ amounts.appendChild(
1782
+ createElement(
1783
+ context.document,
1784
+ 'span',
1785
+ 'ok-native-list-value',
1786
+ row.primaryAmount
1787
+ )
1788
+ );
1789
+ if (row.secondaryAmount)
1790
+ amounts.appendChild(
1791
+ createElement(
1792
+ context.document,
1793
+ 'span',
1794
+ 'ok-native-list-secondary',
1795
+ row.secondaryAmount
1796
+ )
1797
+ );
1798
+ body.appendChild(amounts);
1799
+ } else if (row.type === 'message') {
1800
+ body.appendChild(
1801
+ createElement(context.document, 'span', 'ok-native-list-time', row.time)
1802
+ );
1803
+ if (row.thumbnail) {
1804
+ const thumbnail = createImage(
1805
+ context,
1806
+ row.thumbnail,
1807
+ 'ok-native-list-thumbnail'
1808
+ );
1809
+ if (thumbnail) body.appendChild(thumbnail);
1810
+ }
1811
+ } else {
1812
+ appendAccessories(body, context, row.key, row.trailing);
1813
+ }
1814
+ return body;
1815
+ }
1816
+
1817
+ function createWalletGroupRow(
1818
+ context: RenderContext,
1819
+ row: Extract<RowModel, { type: 'walletGroup' }>
1820
+ ): HTMLElement {
1821
+ const body = createElement(
1822
+ context.document,
1823
+ 'div',
1824
+ 'ok-native-list-wallet-group'
1825
+ );
1826
+ [row.parent, ...row.children].forEach((member, memberIndex) => {
1827
+ const memberElement = createElement(
1828
+ context.document,
1829
+ 'div',
1830
+ 'ok-native-list-wallet-member'
1831
+ );
1832
+ setData(memberElement, 'nativeListGroupMemberKey', member.key);
1833
+ setData(memberElement, 'nativeListGroupParent', memberIndex === 0);
1834
+ setData(memberElement, 'nativeListSelected', member.selected);
1835
+ memberElement.appendChild(
1836
+ createIdentityActivityOrMessageRow(context, member)
1837
+ );
1838
+ body.appendChild(memberElement);
1839
+ });
1840
+ return body;
1841
+ }
1842
+
1843
+ function createRowBody(context: RenderContext, row: RowModel): HTMLElement {
1844
+ switch (row.type) {
1845
+ case 'walletGroup':
1846
+ return createWalletGroupRow(context, row);
1847
+ case 'sectionHeader':
1848
+ return createSectionHeader(context, row);
1849
+ case 'action':
1850
+ return createActionRow(context, row);
1851
+ case 'system':
1852
+ return createSystemRow(context, row);
1853
+ case 'rail':
1854
+ return createRailRow(context, row);
1855
+ case 'mediaTile':
1856
+ return createMediaRow(context, row);
1857
+ case 'metricCard':
1858
+ return createMetricRow(context, row);
1859
+ case 'dataRow':
1860
+ return createDataRow(context, row);
1861
+ case 'identity':
1862
+ case 'activity':
1863
+ case 'message':
1864
+ return createIdentityActivityOrMessageRow(context, row);
1865
+ }
1866
+ }
1867
+
1868
+ export class NativeListWebEngine {
1869
+ private readonly document: Document;
1870
+ private readonly root: HTMLElement;
1871
+ private readonly viewportFrame: HTMLElement;
1872
+ private readonly viewport: HTMLElement;
1873
+ private readonly content: HTMLElement;
1874
+ private readonly footer: HTMLElement;
1875
+ private readonly sticky: HTMLElement;
1876
+ private readonly indexRail: HTMLElement;
1877
+ private readonly indexPreview: HTMLElement;
1878
+ private readonly refreshIndicator: HTMLElement;
1879
+ private readonly reorderPreview: HTMLElement;
1880
+ private readonly previousHostPosition: string;
1881
+ private snapshot: NativeListSnapshot;
1882
+ private rows: readonly RowModel[] = [];
1883
+ private selectedKeys: ReadonlySet<string> = new Set();
1884
+ private callbacks: NativeListWebCallbacks;
1885
+ private layout: WebListLayout = {
1886
+ items: [],
1887
+ contentWidth: 0,
1888
+ contentHeight: 0,
1889
+ horizontal: false,
1890
+ };
1891
+ private readonly mounted = new Map<number, HTMLElement>();
1892
+ private readonly pool: HTMLElement[] = [];
1893
+ private frameHandle: number | undefined;
1894
+ private resizeObserver: ResizeObserver | undefined;
1895
+ private pendingScroll: PendingScroll | undefined;
1896
+ private lastVisibleSignature: string | undefined;
1897
+ private reachedGeneration: number | undefined;
1898
+ private stickyKey: string | undefined;
1899
+ private previewTimer: number | undefined;
1900
+ private pointerReorder: PointerReorderState | undefined;
1901
+ private keyboardReorder: KeyboardReorderState | undefined;
1902
+ private reorderMoveFrame: number | undefined;
1903
+ private reorderAutoScrollFrame: number | undefined;
1904
+ private reorderDropTimer: number | undefined;
1905
+ private reorderMovementTimer: number | undefined;
1906
+ private reorderSettlingKey: string | undefined;
1907
+ private reorderCompactKey: string | undefined;
1908
+ private reorderExpandingKey: string | undefined;
1909
+ private suppressClickUntil = 0;
1910
+ private pullStartY: number | undefined;
1911
+ private pullDistance = 0;
1912
+ private virtualizationEnabled: boolean;
1913
+ private actionAnchor: WebActionAnchorRecord | undefined;
1914
+ private readonly actionAnchorInstanceId = String(
1915
+ ++webNativeListInstanceCounter
1916
+ );
1917
+ private actionAnchorCounter = 0;
1918
+ private bindingEpochCounter = 0;
1919
+ private lastViewportWidth = -1;
1920
+ private lastViewportHeight = -1;
1921
+ private destroyed = false;
1922
+
1923
+ constructor(
1924
+ host: HTMLElement,
1925
+ snapshot: NativeListSnapshot,
1926
+ callbacks: NativeListWebCallbacks,
1927
+ virtualizationEnabled = true
1928
+ ) {
1929
+ this.document = host.ownerDocument;
1930
+ this.snapshot = validateSnapshot(snapshot);
1931
+ this.callbacks = callbacks;
1932
+ this.virtualizationEnabled = virtualizationEnabled;
1933
+ this.previousHostPosition = host.style.position;
1934
+ if (!host.style.position) host.style.position = 'relative';
1935
+
1936
+ this.root = createElement(this.document, 'div', 'ok-native-list-root');
1937
+ const style = this.document.createElement('style');
1938
+ style.textContent = WEB_LIST_CSS;
1939
+ this.viewportFrame = createElement(
1940
+ this.document,
1941
+ 'div',
1942
+ 'ok-native-list-viewport-frame'
1943
+ );
1944
+ this.viewport = createElement(
1945
+ this.document,
1946
+ 'div',
1947
+ 'ok-native-list-viewport'
1948
+ );
1949
+ this.viewport.setAttribute('role', 'list');
1950
+ this.content = createElement(
1951
+ this.document,
1952
+ 'div',
1953
+ 'ok-native-list-content'
1954
+ );
1955
+ this.footer = createElement(this.document, 'div', 'ok-native-list-footer');
1956
+ this.sticky = createElement(
1957
+ this.document,
1958
+ 'div',
1959
+ 'ok-native-list-item ok-native-list-sticky'
1960
+ );
1961
+ this.sticky.setAttribute('aria-hidden', 'true');
1962
+ this.sticky.hidden = true;
1963
+ this.indexRail = createElement(
1964
+ this.document,
1965
+ 'div',
1966
+ 'ok-native-list-index-rail'
1967
+ );
1968
+ this.indexRail.setAttribute('role', 'navigation');
1969
+ this.indexRail.setAttribute('aria-label', 'Section index');
1970
+ this.indexRail.hidden = true;
1971
+ this.indexPreview = createElement(
1972
+ this.document,
1973
+ 'div',
1974
+ 'ok-native-list-index-preview'
1975
+ );
1976
+ this.indexPreview.setAttribute('aria-live', 'polite');
1977
+ this.refreshIndicator = createElement(
1978
+ this.document,
1979
+ 'div',
1980
+ 'ok-native-list-refresh',
1981
+ 'Refreshing'
1982
+ );
1983
+ this.refreshIndicator.setAttribute('role', 'status');
1984
+ this.reorderPreview = createElement(
1985
+ this.document,
1986
+ 'div',
1987
+ 'ok-native-list-reorder-preview'
1988
+ );
1989
+ this.reorderPreview.hidden = true;
1990
+ this.reorderPreview.setAttribute('aria-hidden', 'true');
1991
+ this.document.body.appendChild(this.reorderPreview);
1992
+ this.viewport.append(this.content);
1993
+ this.viewportFrame.append(
1994
+ this.viewport,
1995
+ this.sticky,
1996
+ this.indexRail,
1997
+ this.indexPreview,
1998
+ this.refreshIndicator
1999
+ );
2000
+ this.root.append(style, this.viewportFrame, this.footer);
2001
+ host.appendChild(this.root);
2002
+
2003
+ this.viewport.addEventListener('scroll', this.handleScroll, {
2004
+ passive: true,
2005
+ });
2006
+ this.root.addEventListener('click', this.handleClick);
2007
+ this.root.addEventListener('keydown', this.handleKeyDown);
2008
+ this.viewport.addEventListener(
2009
+ 'pointerdown',
2010
+ this.handleReorderPointerDown,
2011
+ { passive: false }
2012
+ );
2013
+ this.viewport.addEventListener(
2014
+ 'pointermove',
2015
+ this.handleReorderPointerMove,
2016
+ { passive: false }
2017
+ );
2018
+ this.viewport.addEventListener('pointerup', this.handleReorderPointerEnd);
2019
+ this.viewport.addEventListener(
2020
+ 'pointercancel',
2021
+ this.handleReorderPointerCancel
2022
+ );
2023
+ this.viewport.addEventListener('touchmove', this.handleReorderTouchMove, {
2024
+ passive: false,
2025
+ });
2026
+ this.viewport.addEventListener('touchend', this.handleReorderTouchEnd, {
2027
+ passive: false,
2028
+ });
2029
+ this.viewport.addEventListener(
2030
+ 'touchcancel',
2031
+ this.handleReorderTouchCancel
2032
+ );
2033
+ this.indexRail.addEventListener('pointerdown', this.handleIndexPointer);
2034
+ this.indexRail.addEventListener('pointermove', this.handleIndexPointer);
2035
+ this.indexRail.addEventListener('click', this.handleIndexClick);
2036
+ this.viewport.addEventListener('pointerdown', this.handlePullStart, {
2037
+ passive: true,
2038
+ });
2039
+ this.viewport.addEventListener('pointermove', this.handlePullMove, {
2040
+ passive: false,
2041
+ });
2042
+ this.viewport.addEventListener('pointerup', this.handlePullEnd);
2043
+ this.viewport.addEventListener('pointercancel', this.handlePullEnd);
2044
+
2045
+ const ResizeObserverConstructor = this.document.defaultView?.ResizeObserver;
2046
+ if (ResizeObserverConstructor) {
2047
+ this.resizeObserver = new ResizeObserverConstructor(() =>
2048
+ this.recomputeLayout()
2049
+ );
2050
+ this.resizeObserver.observe(this.viewport);
2051
+ } else {
2052
+ this.document.defaultView?.addEventListener(
2053
+ 'resize',
2054
+ this.handleWindowResize
2055
+ );
2056
+ }
2057
+ this.applySnapshot(this.snapshot);
2058
+ }
2059
+
2060
+ updateCallbacks(callbacks: NativeListWebCallbacks) {
2061
+ this.callbacks = callbacks;
2062
+ }
2063
+
2064
+ setVirtualizationEnabled(enabled: boolean) {
2065
+ if (this.virtualizationEnabled === enabled) return;
2066
+ this.virtualizationEnabled = enabled;
2067
+ this.renderWindow();
2068
+ }
2069
+
2070
+ applySnapshot(snapshot: NativeListSnapshot) {
2071
+ const validated = validateSnapshot(snapshot);
2072
+ this.invalidateActionAnchor('snapshot');
2073
+ this.setSnapshot(validated);
2074
+ }
2075
+
2076
+ applyPatches(patches: readonly RowPatch[]) {
2077
+ if (patches.length === 0) return;
2078
+ const next = applyRowPatches(this.snapshot, patches);
2079
+ this.invalidateActionAnchor('snapshot');
2080
+ const selection = new Set(this.selectedKeys);
2081
+ patches.forEach((patch) => {
2082
+ const selected =
2083
+ 'selected' in patch.changes ? patch.changes.selected : undefined;
2084
+ if (selected === true) selection.add(patch.key);
2085
+ else if (selected === false) selection.delete(patch.key);
2086
+ });
2087
+ this.setSnapshot(next, selection);
2088
+ }
2089
+
2090
+ reconcileSelection(keys: readonly string[]) {
2091
+ const selectable = new Set(
2092
+ this.snapshot.rows.filter(isSelectableRow).map((row) => row.key)
2093
+ );
2094
+ if (keys.some((key) => !selectable.has(key))) return;
2095
+ if (this.snapshot.selection?.mode === 'single' && keys.length > 1) return;
2096
+ this.selectedKeys = new Set(keys);
2097
+ this.updateVisibleSelection();
2098
+ this.renderFooter();
2099
+ }
2100
+
2101
+ scrollToIndex(index: number, scroll: NormalizedPositionScroll) {
2102
+ if (index < 0 || index >= this.rows.length) {
2103
+ this.emitScrollFailure(index, 'index-out-of-range');
2104
+ return;
2105
+ }
2106
+ if (!this.canScroll()) {
2107
+ this.pendingScroll = { kind: 'index', index, scroll };
2108
+ return;
2109
+ }
2110
+ const item = this.layout.items[index];
2111
+ if (!item) {
2112
+ this.pendingScroll = { kind: 'index', index, scroll };
2113
+ return;
2114
+ }
2115
+ const viewportLength = this.viewportLength();
2116
+ const contentLength = this.contentLength();
2117
+ const currentOffset = this.currentOffset();
2118
+ const offset = calculateAlignedScrollOffset({
2119
+ itemOffset: itemStart(item, this.layout.horizontal),
2120
+ itemLength: this.layout.horizontal ? item.width : item.height,
2121
+ viewportLength,
2122
+ contentLength,
2123
+ currentOffset,
2124
+ alignment: scroll.alignment,
2125
+ viewPosition: scroll.viewPosition,
2126
+ viewOffset: scroll.viewOffset,
2127
+ });
2128
+ this.scrollToAbsoluteOffset(offset, scroll.animated);
2129
+ }
2130
+
2131
+ scrollToKey(key: string, scroll: NormalizedPositionScroll) {
2132
+ const index = this.rows.findIndex((row) => row.key === key);
2133
+ if (index >= 0) this.scrollToIndex(index, scroll);
2134
+ }
2135
+
2136
+ scrollToOffset(offset: number, animated: boolean) {
2137
+ validateOffset(offset);
2138
+ if (!this.canScroll()) {
2139
+ this.pendingScroll = { kind: 'offset', offset, animated };
2140
+ return;
2141
+ }
2142
+ this.scrollToAbsoluteOffset(offset, animated);
2143
+ }
2144
+
2145
+ scrollToEnd(animated: boolean) {
2146
+ if (!this.canScroll()) {
2147
+ this.pendingScroll = { kind: 'end', animated };
2148
+ return;
2149
+ }
2150
+ this.scrollToAbsoluteOffset(
2151
+ Math.max(0, this.contentLength() - this.viewportLength()),
2152
+ animated
2153
+ );
2154
+ }
2155
+
2156
+ scrollToLocation(
2157
+ params: ScrollToLocationParams,
2158
+ scroll: NormalizedPositionScroll
2159
+ ) {
2160
+ const index = resolveLocationIndex(this.snapshot.rows, params);
2161
+ if (index === undefined) {
2162
+ const sectionCount = this.snapshot.rows.filter(
2163
+ (row) => row.type === 'sectionHeader' && row.variant !== 'summary'
2164
+ ).length;
2165
+ this.emitScrollFailure(
2166
+ params.itemIndex,
2167
+ params.sectionIndex >= sectionCount
2168
+ ? 'section-out-of-range'
2169
+ : 'item-out-of-range'
2170
+ );
2171
+ return;
2172
+ }
2173
+ this.scrollToIndex(index, scroll);
2174
+ }
2175
+
2176
+ setRefreshing(refreshing: boolean) {
2177
+ this.snapshot = {
2178
+ ...this.snapshot,
2179
+ capabilities: { ...this.snapshot.capabilities, refreshing },
2180
+ };
2181
+ this.updateRefreshIndicator();
2182
+ }
2183
+
2184
+ setActionAnchorState(state: ActionAnchorState) {
2185
+ const anchor = this.actionAnchor;
2186
+ if (!anchor || anchor.token !== state.token) return;
2187
+ if (!state.open) {
2188
+ if (state.restoreFocus && this.isActionAnchorValid(anchor)) {
2189
+ anchor.actionElement.focus({ preventScroll: true });
2190
+ }
2191
+ anchor.actionElement.removeAttribute('aria-expanded');
2192
+ this.actionAnchor = undefined;
2193
+ return;
2194
+ }
2195
+ if (anchor.invalidatedReason || !this.isActionAnchorValid(anchor)) {
2196
+ this.emitActionAnchorInvalidated(
2197
+ anchor,
2198
+ anchor.invalidatedReason ?? 'rebind'
2199
+ );
2200
+ return;
2201
+ }
2202
+ anchor.open = true;
2203
+ anchor.actionElement.setAttribute('aria-expanded', 'true');
2204
+ }
2205
+
2206
+ destroy() {
2207
+ if (this.destroyed) return;
2208
+ this.invalidateActionAnchor('destroy');
2209
+ this.destroyed = true;
2210
+ if (this.frameHandle !== undefined) this.cancelFrame(this.frameHandle);
2211
+ if (this.previewTimer !== undefined)
2212
+ this.document.defaultView?.clearTimeout(this.previewTimer);
2213
+ if (this.reorderMovementTimer !== undefined)
2214
+ this.document.defaultView?.clearTimeout(this.reorderMovementTimer);
2215
+ this.resizeObserver?.disconnect();
2216
+ this.document.defaultView?.removeEventListener(
2217
+ 'resize',
2218
+ this.handleWindowResize
2219
+ );
2220
+ this.viewport.removeEventListener('scroll', this.handleScroll);
2221
+ this.root.removeEventListener('click', this.handleClick);
2222
+ this.root.removeEventListener('keydown', this.handleKeyDown);
2223
+ this.cancelPointerReorder(true);
2224
+ this.viewport.removeEventListener(
2225
+ 'pointerdown',
2226
+ this.handleReorderPointerDown
2227
+ );
2228
+ this.viewport.removeEventListener(
2229
+ 'pointermove',
2230
+ this.handleReorderPointerMove
2231
+ );
2232
+ this.viewport.removeEventListener(
2233
+ 'pointerup',
2234
+ this.handleReorderPointerEnd
2235
+ );
2236
+ this.viewport.removeEventListener(
2237
+ 'pointercancel',
2238
+ this.handleReorderPointerCancel
2239
+ );
2240
+ this.viewport.removeEventListener('touchmove', this.handleReorderTouchMove);
2241
+ this.viewport.removeEventListener('touchend', this.handleReorderTouchEnd);
2242
+ this.viewport.removeEventListener(
2243
+ 'touchcancel',
2244
+ this.handleReorderTouchCancel
2245
+ );
2246
+ this.indexRail.removeEventListener('pointerdown', this.handleIndexPointer);
2247
+ this.indexRail.removeEventListener('pointermove', this.handleIndexPointer);
2248
+ this.indexRail.removeEventListener('click', this.handleIndexClick);
2249
+ this.viewport.removeEventListener('pointerdown', this.handlePullStart);
2250
+ this.viewport.removeEventListener('pointermove', this.handlePullMove);
2251
+ this.viewport.removeEventListener('pointerup', this.handlePullEnd);
2252
+ this.viewport.removeEventListener('pointercancel', this.handlePullEnd);
2253
+ const host = this.root.parentElement;
2254
+ this.root.remove();
2255
+ this.hideReorderPreview();
2256
+ this.reorderPreview.remove();
2257
+ if (host) host.style.position = this.previousHostPosition;
2258
+ this.mounted.clear();
2259
+ this.pool.length = 0;
2260
+ }
2261
+
2262
+ private setSnapshot(
2263
+ snapshot: NativeListSnapshot,
2264
+ selectedKeys?: ReadonlySet<string>
2265
+ ) {
2266
+ this.snapshot = snapshot;
2267
+ this.rows = effectiveRows(snapshot);
2268
+ this.selectedKeys =
2269
+ selectedKeys ?? selectionStateFromSnapshot(snapshot).selectedKeys;
2270
+ if (this.reachedGeneration !== snapshot.generation)
2271
+ this.reachedGeneration = undefined;
2272
+ this.renderSectionIndex();
2273
+ this.applyTheme();
2274
+ this.recomputeLayout();
2275
+ this.renderFooter();
2276
+ this.updateRefreshIndicator();
2277
+ }
2278
+
2279
+ private applyTheme() {
2280
+ const theme = resolvedTheme(this.snapshot);
2281
+ const values: Readonly<Record<string, string | undefined>> = {
2282
+ '--nl-bg': theme.background,
2283
+ '--nl-row': theme.rowBackground,
2284
+ '--nl-selected': theme.rowSelectedBackground,
2285
+ '--nl-pressed': theme.rowPressedBackground,
2286
+ '--nl-subdued': theme.subduedBackground,
2287
+ '--nl-strong': theme.strongBackground,
2288
+ '--nl-primary': theme.primaryText,
2289
+ '--nl-secondary': theme.secondaryText,
2290
+ '--nl-disabled': theme.disabledText,
2291
+ '--nl-icon': theme.icon,
2292
+ '--nl-icon-subdued': theme.iconSubdued,
2293
+ '--nl-separator': theme.separator,
2294
+ '--nl-accent': theme.accent,
2295
+ '--nl-positive': theme.positive,
2296
+ '--nl-negative': theme.negative,
2297
+ '--nl-critical': theme.criticalBackground,
2298
+ '--nl-inverse': theme.inverseBackground,
2299
+ '--nl-inverse-text': theme.inverseText,
2300
+ '--nl-info': theme.info,
2301
+ };
2302
+ Object.entries(values).forEach(([name, value]) => {
2303
+ if (!value) return;
2304
+ this.root.style.setProperty(name, value);
2305
+ this.reorderPreview.style.setProperty(name, value);
2306
+ });
2307
+ }
2308
+
2309
+ private recomputeLayout = () => {
2310
+ if (this.destroyed) return;
2311
+ const viewportWidth = this.viewport.clientWidth;
2312
+ const viewportHeight = this.viewport.clientHeight;
2313
+ if (
2314
+ this.lastViewportWidth >= 0 &&
2315
+ (viewportWidth !== this.lastViewportWidth ||
2316
+ viewportHeight !== this.lastViewportHeight)
2317
+ ) {
2318
+ this.invalidateActionAnchor('layout');
2319
+ }
2320
+ this.lastViewportWidth = viewportWidth;
2321
+ this.lastViewportHeight = viewportHeight;
2322
+ const previousHorizontal = this.layout.horizontal;
2323
+ this.layout = computeWebListLayout(
2324
+ this.snapshot,
2325
+ viewportWidth,
2326
+ viewportHeight,
2327
+ this.reorderCompactKey
2328
+ );
2329
+ this.content.style.width = String(this.layout.contentWidth) + 'px';
2330
+ this.content.style.height = String(this.layout.contentHeight) + 'px';
2331
+ if (previousHorizontal !== this.layout.horizontal) {
2332
+ this.viewport.scrollLeft = 0;
2333
+ this.viewport.scrollTop = 0;
2334
+ }
2335
+ this.renderWindow();
2336
+ this.performPendingScroll();
2337
+ };
2338
+
2339
+ private renderWindow() {
2340
+ const viewportLength = this.viewportLength();
2341
+ const visible = webLayoutItemsForMount(
2342
+ this.layout,
2343
+ this.currentOffset(),
2344
+ viewportLength,
2345
+ this.virtualizationEnabled,
2346
+ viewportLength * OVERSCAN_VIEWPORTS
2347
+ );
2348
+ const desired = new Set(visible.map((item) => item.index));
2349
+ this.mounted.forEach((element, index) => {
2350
+ if (!desired.has(index)) {
2351
+ this.invalidateActionAnchorForElement(element);
2352
+ this.mounted.delete(index);
2353
+ element.remove();
2354
+ this.pool.push(element);
2355
+ }
2356
+ });
2357
+
2358
+ visible.forEach((layoutItem) => {
2359
+ const row = this.rows[layoutItem.index];
2360
+ if (!row) return;
2361
+ let element = this.mounted.get(layoutItem.index);
2362
+ if (!element) {
2363
+ element =
2364
+ this.pool.pop() ??
2365
+ createElement(this.document, 'div', 'ok-native-list-item');
2366
+ setData(element, 'nativeListAnimateReorder', false);
2367
+ this.mounted.set(layoutItem.index, element);
2368
+ this.content.appendChild(element);
2369
+ }
2370
+ this.positionElement(element, layoutItem);
2371
+ const signature = webRowRenderSignature(row);
2372
+ if (
2373
+ element.dataset.nativeListRowKey !== row.key ||
2374
+ element.dataset.renderSignature !== signature
2375
+ ) {
2376
+ this.renderElement(element, layoutItem.index, row);
2377
+ element.dataset.renderSignature = signature;
2378
+ }
2379
+ });
2380
+ this.updateVisibleSelection();
2381
+ this.updateVisibleState();
2382
+ }
2383
+
2384
+ private positionElement(element: HTMLElement, item: WebLayoutItem) {
2385
+ element.style.transform =
2386
+ 'translate3d(' + String(item.x) + 'px,' + String(item.y) + 'px,0)';
2387
+ element.style.width = String(item.width) + 'px';
2388
+ element.style.height = String(item.height) + 'px';
2389
+ }
2390
+
2391
+ private renderElement(
2392
+ element: HTMLElement,
2393
+ index: number,
2394
+ row: RowModel,
2395
+ overlay = false
2396
+ ) {
2397
+ this.invalidateActionAnchorForElement(element);
2398
+ const bindingEpoch = String(++this.bindingEpochCounter);
2399
+ element.className = overlay
2400
+ ? 'ok-native-list-item ok-native-list-sticky'
2401
+ : 'ok-native-list-item';
2402
+ setData(element, 'nativeListRowKey', row.key);
2403
+ setData(element, 'nativeListBindingEpoch', bindingEpoch);
2404
+ setData(element, 'nativeListRowIndex', index);
2405
+ setData(element, 'nativeListDisabled', Boolean(row.disabled));
2406
+ setData(element, 'nativeListReorderable', this.isReorderable(row));
2407
+ setData(
2408
+ element,
2409
+ 'nativeListDragging',
2410
+ this.pointerReorder?.active && this.pointerReorder.sourceKey === row.key
2411
+ );
2412
+ setData(element, 'separator', row.separator);
2413
+ setData(element, 'groupPosition', row.groupPosition);
2414
+ setData(
2415
+ element,
2416
+ 'tableAlternate',
2417
+ this.snapshot.layout.kind === 'table' &&
2418
+ row.type === 'dataRow' &&
2419
+ (row.index ?? index) % 2 === 0
2420
+ );
2421
+ element.setAttribute('role', 'listitem');
2422
+ element.setAttribute(
2423
+ 'aria-label',
2424
+ row.accessibilityLabel ?? this.rowLabel(row)
2425
+ );
2426
+ if (!row.disabled && !(row.type === 'system' && row.variant === 'spacer')) {
2427
+ element.tabIndex = 0;
2428
+ } else {
2429
+ element.removeAttribute('tabindex');
2430
+ }
2431
+ element.draggable = false;
2432
+ const context = {
2433
+ document: this.document,
2434
+ snapshot: this.snapshot,
2435
+ selectedKeys: this.selectedKeys,
2436
+ itemIndex: index,
2437
+ };
2438
+ element.replaceChildren(createRowBody(context, row));
2439
+ }
2440
+
2441
+ private renderFooter() {
2442
+ const row = this.snapshot.fixedFooter;
2443
+ this.invalidateActionAnchorForElement(this.footer);
2444
+ this.footer.replaceChildren();
2445
+ if (!row) return;
2446
+ const element = createElement(this.document, 'div', 'ok-native-list-item');
2447
+ this.renderElement(element, -1, row);
2448
+ element.style.position = 'relative';
2449
+ element.style.transform = 'none';
2450
+ element.style.width = '100%';
2451
+ element.style.height =
2452
+ String(
2453
+ estimateWebRowHeight(
2454
+ row,
2455
+ this.snapshot,
2456
+ this.viewport.clientWidth || DEFAULT_VIEWPORT_WIDTH
2457
+ )
2458
+ ) + 'px';
2459
+ this.footer.appendChild(element);
2460
+ }
2461
+
2462
+ private renderSectionIndex() {
2463
+ this.indexRail.replaceChildren();
2464
+ if (!sectionIndexEnabled(this.snapshot)) {
2465
+ this.indexRail.hidden = true;
2466
+ return;
2467
+ }
2468
+ const fragment = this.document.createDocumentFragment();
2469
+ this.snapshot.rows.forEach((row, index) => {
2470
+ if (row.type !== 'sectionHeader' || !row.indexTitle) return;
2471
+ const button = createElement(
2472
+ this.document,
2473
+ 'button',
2474
+ 'ok-native-list-index-button',
2475
+ row.indexTitle
2476
+ );
2477
+ button.setAttribute('type', 'button');
2478
+ button.setAttribute('aria-label', 'Jump to ' + row.indexTitle);
2479
+ setData(button, 'sectionPosition', index);
2480
+ setData(button, 'sectionKey', row.key);
2481
+ fragment.appendChild(button);
2482
+ });
2483
+ this.indexRail.appendChild(fragment);
2484
+ this.indexRail.hidden = this.indexRail.childElementCount === 0;
2485
+ }
2486
+
2487
+ private updateVisibleSelection() {
2488
+ const update = (element: HTMLElement, row: RowModel | undefined) => {
2489
+ if (!row) return;
2490
+ const selected = this.selectedKeys.has(row.key);
2491
+ setData(element, 'nativeListSelected', selected);
2492
+ element.setAttribute('aria-selected', String(selected));
2493
+ element
2494
+ .querySelectorAll<HTMLElement>('.ok-native-list-checkbox')
2495
+ .forEach((checkbox) => {
2496
+ const scope = checkbox.dataset.selectionScope;
2497
+ const key = checkbox.dataset.selectionKey;
2498
+ const target: SelectionTarget | undefined =
2499
+ scope === 'section' && key
2500
+ ? { scope: 'section', sectionKey: key }
2501
+ : scope === 'list'
2502
+ ? { scope: 'list' }
2503
+ : { scope: 'row' };
2504
+ const state = checkboxState(
2505
+ target,
2506
+ (checkbox.dataset.checkboxFallback as CheckboxState | undefined) ??
2507
+ 'unchecked',
2508
+ row.key,
2509
+ this.snapshot,
2510
+ this.selectedKeys
2511
+ );
2512
+ setData(checkbox, 'state', state);
2513
+ checkbox.setAttribute(
2514
+ 'aria-checked',
2515
+ state === 'indeterminate' ? 'mixed' : String(state === 'checked')
2516
+ );
2517
+ });
2518
+ };
2519
+ this.mounted.forEach((element, index) => update(element, this.rows[index]));
2520
+ const stickyIndex = Number(this.sticky.dataset.nativeListRowIndex);
2521
+ if (!Number.isNaN(stickyIndex)) update(this.sticky, this.rows[stickyIndex]);
2522
+ }
2523
+
2524
+ private updateVisibleState() {
2525
+ const visible = visibleWebLayoutItems(
2526
+ this.layout,
2527
+ this.currentOffset(),
2528
+ this.viewportLength()
2529
+ );
2530
+ const first = visible[0];
2531
+ const last = visible.at(-1);
2532
+ const signature =
2533
+ String(first?.index ?? -1) +
2534
+ ':' +
2535
+ String(last?.index ?? -1) +
2536
+ ':' +
2537
+ (first?.key ?? '') +
2538
+ ':' +
2539
+ (last?.key ?? '');
2540
+ if (signature !== this.lastVisibleSignature) {
2541
+ this.lastVisibleSignature = signature;
2542
+ this.callbacks.onVisibleRangeChanged?.({
2543
+ firstKey: first?.key,
2544
+ lastKey: last?.key,
2545
+ firstIndex: first?.index ?? -1,
2546
+ lastIndex: last?.index ?? -1,
2547
+ });
2548
+ }
2549
+ this.checkEndReached(last?.index ?? -1);
2550
+ this.updateStickyHeader(first?.index ?? -1);
2551
+ this.updateSectionIndex(first?.index ?? -1);
2552
+ }
2553
+
2554
+ private updateStickyHeader(firstVisibleIndex: number) {
2555
+ if (
2556
+ !this.snapshot.layout.stickyHeaders ||
2557
+ this.layout.horizontal ||
2558
+ firstVisibleIndex < 0
2559
+ ) {
2560
+ this.sticky.hidden = true;
2561
+ this.stickyKey = undefined;
2562
+ return;
2563
+ }
2564
+ let index = -1;
2565
+ for (let cursor = firstVisibleIndex; cursor >= 0; cursor -= 1) {
2566
+ const row = this.rows[cursor];
2567
+ if (row?.type === 'sectionHeader' && row.variant !== 'summary') {
2568
+ index = cursor;
2569
+ break;
2570
+ }
2571
+ }
2572
+ const row = this.rows[index];
2573
+ const item = this.layout.items[index];
2574
+ if (!row || row.type !== 'sectionHeader' || !item) {
2575
+ this.sticky.hidden = true;
2576
+ this.stickyKey = undefined;
2577
+ return;
2578
+ }
2579
+ const signature = webRowRenderSignature(row);
2580
+ if (
2581
+ this.stickyKey !== row.key ||
2582
+ this.sticky.dataset.renderSignature !== signature
2583
+ ) {
2584
+ this.renderElement(this.sticky, index, row, true);
2585
+ this.sticky.dataset.renderSignature = signature;
2586
+ this.stickyKey = row.key;
2587
+ }
2588
+ this.sticky.hidden = false;
2589
+ this.sticky.style.left = String(item.x) + 'px';
2590
+ this.sticky.style.right = 'auto';
2591
+ this.sticky.style.width = String(item.width) + 'px';
2592
+ this.sticky.style.height = String(item.height) + 'px';
2593
+ let nextIndex = -1;
2594
+ for (let cursor = index + 1; cursor < this.rows.length; cursor += 1) {
2595
+ const candidate = this.rows[cursor];
2596
+ if (
2597
+ candidate?.type === 'sectionHeader' &&
2598
+ candidate.variant !== 'summary'
2599
+ ) {
2600
+ nextIndex = cursor;
2601
+ break;
2602
+ }
2603
+ }
2604
+ const next = this.layout.items[nextIndex];
2605
+ const translate = next
2606
+ ? Math.min(0, next.y - this.viewport.scrollTop - item.height)
2607
+ : 0;
2608
+ this.sticky.style.transform =
2609
+ 'translate3d(0,' + String(translate) + 'px,0)';
2610
+ this.updateVisibleSelection();
2611
+ }
2612
+
2613
+ private updateSectionIndex(firstVisibleIndex: number) {
2614
+ let activeKey: string | undefined;
2615
+ this.snapshot.rows.forEach((row, index) => {
2616
+ if (
2617
+ index <= firstVisibleIndex &&
2618
+ row.type === 'sectionHeader' &&
2619
+ row.indexTitle
2620
+ )
2621
+ activeKey = row.key;
2622
+ });
2623
+ this.indexRail
2624
+ .querySelectorAll<HTMLElement>('[data-section-key]')
2625
+ .forEach((button) =>
2626
+ setData(button, 'active', button.dataset.sectionKey === activeKey)
2627
+ );
2628
+ }
2629
+
2630
+ private checkEndReached(lastVisibleIndex: number) {
2631
+ if (
2632
+ !this.snapshot.capabilities?.loadMore ||
2633
+ this.reachedGeneration === this.snapshot.generation ||
2634
+ this.rows.length === 0
2635
+ )
2636
+ return;
2637
+ const threshold = Math.max(
2638
+ 1,
2639
+ Math.ceil(
2640
+ this.rows.length *
2641
+ (this.snapshot.capabilities.endReachedThreshold ?? 0.2)
2642
+ )
2643
+ );
2644
+ if (lastVisibleIndex < this.rows.length - threshold) return;
2645
+ this.reachedGeneration = this.snapshot.generation;
2646
+ this.callbacks.onEndReached?.({
2647
+ generation: this.snapshot.generation,
2648
+ lastKey: this.rows.at(-1)?.key,
2649
+ });
2650
+ }
2651
+
2652
+ private emitScrollFailure(
2653
+ index: number,
2654
+ reason: ScrollToIndexFailedInfo['reason']
2655
+ ) {
2656
+ const average =
2657
+ this.layout.items.length === 0
2658
+ ? 0
2659
+ : this.layout.items.reduce(
2660
+ (sum, item) =>
2661
+ sum + (this.layout.horizontal ? item.width : item.height),
2662
+ 0
2663
+ ) / this.layout.items.length;
2664
+ this.callbacks.onScrollToIndexFailed?.(
2665
+ scrollFailure(this.rows, index, reason, average)
2666
+ );
2667
+ }
2668
+
2669
+ private canScroll(): boolean {
2670
+ const viewportLength = this.layout.horizontal
2671
+ ? this.viewport.clientWidth
2672
+ : this.viewport.clientHeight;
2673
+ return viewportLength > 0 && this.layout.items.length > 0;
2674
+ }
2675
+
2676
+ private viewportLength(): number {
2677
+ return this.layout.horizontal
2678
+ ? this.viewport.clientWidth || DEFAULT_VIEWPORT_WIDTH
2679
+ : this.viewport.clientHeight || DEFAULT_VIEWPORT_HEIGHT;
2680
+ }
2681
+
2682
+ private contentLength(): number {
2683
+ return this.layout.horizontal
2684
+ ? this.layout.contentWidth
2685
+ : this.layout.contentHeight;
2686
+ }
2687
+
2688
+ private currentOffset(): number {
2689
+ return this.layout.horizontal
2690
+ ? this.viewport.scrollLeft
2691
+ : this.viewport.scrollTop;
2692
+ }
2693
+
2694
+ private scrollToAbsoluteOffset(offset: number, animated: boolean) {
2695
+ const resolved = Math.min(
2696
+ Math.max(0, offset),
2697
+ Math.max(0, this.contentLength() - this.viewportLength())
2698
+ );
2699
+ this.viewport.scrollTo(
2700
+ this.layout.horizontal
2701
+ ? { left: resolved, top: 0, behavior: animated ? 'smooth' : 'auto' }
2702
+ : { left: 0, top: resolved, behavior: animated ? 'smooth' : 'auto' }
2703
+ );
2704
+ this.scheduleFrame();
2705
+ }
2706
+
2707
+ private performPendingScroll() {
2708
+ const pending = this.pendingScroll;
2709
+ if (!pending || !this.canScroll()) return;
2710
+ this.pendingScroll = undefined;
2711
+ if (pending.kind === 'index')
2712
+ this.scrollToIndex(pending.index, pending.scroll);
2713
+ else if (pending.kind === 'offset')
2714
+ this.scrollToOffset(pending.offset, pending.animated);
2715
+ else this.scrollToEnd(pending.animated);
2716
+ }
2717
+
2718
+ private rowAtElement(element: Element | null): RowModel | undefined {
2719
+ if (!element) return undefined;
2720
+ const key = (element as HTMLElement).dataset.nativeListRowKey;
2721
+ return (
2722
+ this.rows.find((row) => row.key === key) ??
2723
+ (this.snapshot.fixedFooter?.key === key
2724
+ ? this.snapshot.fixedFooter
2725
+ : undefined)
2726
+ );
2727
+ }
2728
+
2729
+ private rowLabel(row: RowModel): string {
2730
+ if (row.type === 'walletGroup') return row.parent.title;
2731
+ if ('title' in row) return row.title;
2732
+ if ('message' in row && row.message) return row.message;
2733
+ return row.type;
2734
+ }
2735
+
2736
+ private isReorderable(row: RowModel): boolean {
2737
+ return isWebRowReorderable(this.snapshot, row);
2738
+ }
2739
+
2740
+ private activateSelection(target: SelectionTarget, sourceRow: RowModel) {
2741
+ const action =
2742
+ target.scope === 'row'
2743
+ ? { scope: 'row' as const, key: sourceRow.key }
2744
+ : target;
2745
+ const result = reduceSelection(
2746
+ {
2747
+ mode: this.snapshot.selection?.mode ?? 'none',
2748
+ selectedKeys: this.selectedKeys,
2749
+ },
2750
+ action,
2751
+ this.snapshot.rows
2752
+ );
2753
+ if (
2754
+ result.delta.addedKeys.length === 0 &&
2755
+ result.delta.removedKeys.length === 0
2756
+ )
2757
+ return;
2758
+ this.selectedKeys = result.state.selectedKeys;
2759
+ this.updateVisibleSelection();
2760
+ this.renderFooter();
2761
+ this.callbacks.onSelectionDelta?.(result.delta);
2762
+ }
2763
+
2764
+ private emitRowAction(
2765
+ row: RowModel,
2766
+ actionKey: string,
2767
+ actionElement?: HTMLElement,
2768
+ rowElement?: HTMLElement
2769
+ ) {
2770
+ const anchor =
2771
+ actionElement && rowElement
2772
+ ? this.createActionAnchor(actionElement, rowElement)
2773
+ : undefined;
2774
+ this.callbacks.onRowAction?.({
2775
+ rowKey: row.key,
2776
+ actionKey,
2777
+ sectionKey: row.sectionKey,
2778
+ anchor,
2779
+ });
2780
+ }
2781
+
2782
+ private createActionAnchor(
2783
+ actionElement: HTMLElement,
2784
+ rowElement: HTMLElement,
2785
+ sourceOverride?: NativeListActionSource
2786
+ ): NativeListActionAnchor | undefined {
2787
+ const source =
2788
+ sourceOverride ??
2789
+ (actionElement.dataset.nativeListAnchorSource as
2790
+ | NativeListActionSource
2791
+ | undefined);
2792
+ const bindingEpoch = rowElement.dataset.nativeListBindingEpoch;
2793
+ if (!source || !bindingEpoch || !rowElement.contains(actionElement))
2794
+ return undefined;
2795
+ this.invalidateActionAnchor('rebind');
2796
+ const rect = actionElement.getBoundingClientRect();
2797
+ const token = [
2798
+ this.actionAnchorInstanceId,
2799
+ this.snapshot.generation,
2800
+ ++this.actionAnchorCounter,
2801
+ bindingEpoch,
2802
+ ].join(':');
2803
+ const slotValue = actionElement.dataset.nativeListAnchorSlot;
2804
+ const direction =
2805
+ this.document.defaultView?.getComputedStyle(actionElement).direction ===
2806
+ 'rtl' || actionElement.closest('[dir="rtl"]')
2807
+ ? 'rtl'
2808
+ : 'ltr';
2809
+ this.actionAnchor = {
2810
+ token,
2811
+ actionElement,
2812
+ rowElement,
2813
+ bindingEpoch,
2814
+ open: false,
2815
+ };
2816
+ return webActionAnchorPayload(
2817
+ token,
2818
+ rect,
2819
+ source,
2820
+ this.snapshot.generation,
2821
+ direction,
2822
+ slotValue === undefined ? undefined : Number(slotValue)
2823
+ );
2824
+ }
2825
+
2826
+ private isActionAnchorValid(anchor: WebActionAnchorRecord): boolean {
2827
+ return (
2828
+ anchor.actionElement.isConnected &&
2829
+ anchor.rowElement.isConnected &&
2830
+ anchor.rowElement.contains(anchor.actionElement) &&
2831
+ anchor.rowElement.dataset.nativeListBindingEpoch === anchor.bindingEpoch
2832
+ );
2833
+ }
2834
+
2835
+ private invalidateActionAnchorForElement(element: HTMLElement) {
2836
+ const anchor = this.actionAnchor;
2837
+ if (
2838
+ anchor &&
2839
+ (element === anchor.rowElement || element.contains(anchor.actionElement))
2840
+ ) {
2841
+ this.invalidateActionAnchor('rebind');
2842
+ }
2843
+ }
2844
+
2845
+ private invalidateActionAnchor(
2846
+ reason: ActionAnchorInvalidatedEvent['reason']
2847
+ ) {
2848
+ const anchor = this.actionAnchor;
2849
+ if (!anchor || anchor.invalidatedReason) return;
2850
+ anchor.invalidatedReason = reason;
2851
+ anchor.actionElement.removeAttribute('aria-expanded');
2852
+ if (anchor.open) this.emitActionAnchorInvalidated(anchor, reason);
2853
+ }
2854
+
2855
+ private emitActionAnchorInvalidated(
2856
+ anchor: WebActionAnchorRecord,
2857
+ reason: ActionAnchorInvalidatedEvent['reason']
2858
+ ) {
2859
+ if (this.actionAnchor !== anchor) return;
2860
+ this.actionAnchor = undefined;
2861
+ this.callbacks.onActionAnchorInvalidated?.({ token: anchor.token, reason });
2862
+ }
2863
+
2864
+ private handleRowPress(
2865
+ row: RowModel,
2866
+ rowElement?: HTMLElement,
2867
+ sourceElement = rowElement
2868
+ ) {
2869
+ if (row.disabled) return;
2870
+ if (
2871
+ this.snapshot.selection?.rowPressToggles &&
2872
+ this.snapshot.selection.mode !== 'none' &&
2873
+ isSelectableRow(row)
2874
+ ) {
2875
+ this.activateSelection({ scope: 'row' }, row);
2876
+ return;
2877
+ }
2878
+ const actionKey =
2879
+ row.type === 'action'
2880
+ ? row.actionKey
2881
+ : row.type === 'system' && row.variant === 'retry'
2882
+ ? row.actionKey
2883
+ : 'press';
2884
+ if (rowElement && sourceElement) {
2885
+ const anchor = this.createActionAnchor(sourceElement, rowElement, 'row');
2886
+ this.callbacks.onRowAction?.({
2887
+ rowKey: row.key,
2888
+ actionKey,
2889
+ sectionKey: row.sectionKey,
2890
+ anchor,
2891
+ });
2892
+ } else {
2893
+ this.emitRowAction(row, actionKey);
2894
+ }
2895
+ }
2896
+
2897
+ private handleClick = (event: Event) => {
2898
+ if (Date.now() < this.suppressClickUntil) {
2899
+ event.preventDefault();
2900
+ event.stopPropagation();
2901
+ return;
2902
+ }
2903
+ const target = event.target;
2904
+ if (!(target instanceof Element)) return;
2905
+ const rowElement = target.closest<HTMLElement>(
2906
+ '[data-native-list-row-key]'
2907
+ );
2908
+ const row = this.rowAtElement(rowElement);
2909
+ if (!row || row.disabled) return;
2910
+ const memberElement = target.closest<HTMLElement>(
2911
+ '[data-native-list-group-member-key]'
2912
+ );
2913
+ const memberKey = memberElement?.dataset.nativeListGroupMemberKey;
2914
+ const sourceRow =
2915
+ row.type === 'walletGroup' && memberKey
2916
+ ? [row.parent, ...row.children].find(
2917
+ (member) => member.key === memberKey
2918
+ ) ?? row
2919
+ : row;
2920
+ const action = target.closest<HTMLElement>('[data-native-list-action]');
2921
+ if (action) {
2922
+ const scope = action.dataset.selectionScope;
2923
+ if (scope === 'row') this.activateSelection({ scope: 'row' }, row);
2924
+ else if (scope === 'section' && action.dataset.selectionKey)
2925
+ this.activateSelection(
2926
+ { scope: 'section', sectionKey: action.dataset.selectionKey },
2927
+ row
2928
+ );
2929
+ else if (scope === 'list') this.activateSelection({ scope: 'list' }, row);
2930
+ else if (action.dataset.nativeListAction)
2931
+ this.emitRowAction(
2932
+ sourceRow,
2933
+ action.dataset.nativeListAction,
2934
+ action,
2935
+ rowElement ?? undefined
2936
+ );
2937
+ return;
2938
+ }
2939
+ this.handleRowPress(
2940
+ sourceRow,
2941
+ rowElement ?? undefined,
2942
+ memberElement ?? rowElement ?? undefined
2943
+ );
2944
+ };
2945
+
2946
+ private handleKeyDown = (event: KeyboardEvent) => {
2947
+ if (event.key === 'Escape') {
2948
+ if (this.pointerReorder?.active) {
2949
+ event.preventDefault();
2950
+ this.cancelPointerReorder(true);
2951
+ return;
2952
+ }
2953
+ if (this.keyboardReorder) {
2954
+ event.preventDefault();
2955
+ this.cancelKeyboardReorder();
2956
+ return;
2957
+ }
2958
+ }
2959
+ if (
2960
+ this.keyboardReorder &&
2961
+ (event.key === 'ArrowUp' || event.key === 'ArrowLeft')
2962
+ ) {
2963
+ event.preventDefault();
2964
+ this.moveKeyboardReorder(-1);
2965
+ return;
2966
+ }
2967
+ if (
2968
+ this.keyboardReorder &&
2969
+ (event.key === 'ArrowDown' || event.key === 'ArrowRight')
2970
+ ) {
2971
+ event.preventDefault();
2972
+ this.moveKeyboardReorder(1);
2973
+ return;
2974
+ }
2975
+ if (event.key !== 'Enter' && event.key !== ' ') return;
2976
+ const target = event.target;
2977
+ if (!(target instanceof HTMLElement)) return;
2978
+ if (target.matches('button')) return;
2979
+ const row = this.rowAtElement(target.closest('[data-native-list-row-key]'));
2980
+ if (!row) return;
2981
+ event.preventDefault();
2982
+ if (event.key === ' ' && this.isReorderable(row)) {
2983
+ if (this.keyboardReorder) this.finishKeyboardReorder();
2984
+ else this.startKeyboardReorder(row);
2985
+ return;
2986
+ }
2987
+ this.handleRowPress(
2988
+ row,
2989
+ target.closest<HTMLElement>('[data-native-list-row-key]') ?? undefined
2990
+ );
2991
+ };
2992
+
2993
+ private startKeyboardReorder(row: RowModel) {
2994
+ this.keyboardReorder = {
2995
+ sourceKey: row.key,
2996
+ originalRows: this.snapshot.rows,
2997
+ };
2998
+ this.updateReorderVisualState();
2999
+ }
3000
+
3001
+ private moveKeyboardReorder(delta: -1 | 1) {
3002
+ const state = this.keyboardReorder;
3003
+ if (!state) return;
3004
+ const fromIndex = this.snapshot.rows.findIndex(
3005
+ (row) => row.key === state.sourceKey
3006
+ );
3007
+ const toIndex = fromIndex + delta;
3008
+ const from = this.snapshot.rows[fromIndex];
3009
+ const to = this.snapshot.rows[toIndex];
3010
+ if (
3011
+ !from ||
3012
+ !to ||
3013
+ !this.isReorderable(to) ||
3014
+ from.sectionKey !== to.sectionKey
3015
+ )
3016
+ return;
3017
+ const rows = moveWebReorderRow(this.snapshot.rows, fromIndex, toIndex);
3018
+ this.remapMountedRows(rows);
3019
+ this.setSnapshot({ ...this.snapshot, rows }, this.selectedKeys);
3020
+ this.updateReorderVisualState();
3021
+ this.scrollToIndex(toIndex, {
3022
+ animated: false,
3023
+ alignment: 'nearest',
3024
+ viewPosition: 0,
3025
+ viewOffset: 0,
3026
+ });
3027
+ [...this.mounted.values()]
3028
+ .find((element) => element.dataset.nativeListRowKey === state.sourceKey)
3029
+ ?.focus({ preventScroll: true });
3030
+ }
3031
+
3032
+ private finishKeyboardReorder() {
3033
+ const state = this.keyboardReorder;
3034
+ if (!state) return;
3035
+ const reorderEvent = webReorderEventForRows(
3036
+ state.originalRows,
3037
+ this.snapshot.rows,
3038
+ state.sourceKey
3039
+ );
3040
+ this.keyboardReorder = undefined;
3041
+ this.updateReorderVisualState();
3042
+ if (reorderEvent) this.callbacks.onReorder?.(reorderEvent);
3043
+ }
3044
+
3045
+ private cancelKeyboardReorder() {
3046
+ const state = this.keyboardReorder;
3047
+ if (!state) return;
3048
+ this.keyboardReorder = undefined;
3049
+ this.remapMountedRows(state.originalRows);
3050
+ this.setSnapshot(
3051
+ { ...this.snapshot, rows: cancelWebReorderRows(state.originalRows) },
3052
+ this.selectedKeys
3053
+ );
3054
+ this.updateReorderVisualState();
3055
+ [...this.mounted.values()]
3056
+ .find((element) => element.dataset.nativeListRowKey === state.sourceKey)
3057
+ ?.focus({ preventScroll: true });
3058
+ }
3059
+
3060
+ private handleScroll = () => {
3061
+ this.invalidateActionAnchor('scroll');
3062
+ this.scheduleFrame();
3063
+ };
3064
+
3065
+ private handleWindowResize = () => {
3066
+ const state = this.pointerReorder;
3067
+ if (state?.active) state.viewportRect = this.reorderViewportRect();
3068
+ this.recomputeLayout();
3069
+ };
3070
+
3071
+ private scheduleFrame() {
3072
+ if (this.frameHandle !== undefined || this.destroyed) return;
3073
+ this.frameHandle = this.requestFrame(() => {
3074
+ this.frameHandle = undefined;
3075
+ if (this.virtualizationEnabled) this.renderWindow();
3076
+ else this.updateVisibleState();
3077
+ });
3078
+ }
3079
+
3080
+ private requestFrame(callback: FrameRequestCallback): number {
3081
+ const view = this.document.defaultView;
3082
+ return view?.requestAnimationFrame
3083
+ ? view.requestAnimationFrame(callback)
3084
+ : view?.setTimeout(() => callback(Date.now()), 16) ?? 0;
3085
+ }
3086
+
3087
+ private cancelFrame(handle: number) {
3088
+ const view = this.document.defaultView;
3089
+ if (view?.cancelAnimationFrame) view.cancelAnimationFrame(handle);
3090
+ else view?.clearTimeout(handle);
3091
+ }
3092
+
3093
+ private selectIndexPosition(position: number, title: string) {
3094
+ this.scrollToIndex(position, {
3095
+ animated: false,
3096
+ alignment: 'start',
3097
+ viewPosition: 0,
3098
+ viewOffset: 0,
3099
+ });
3100
+ this.indexPreview.textContent = title;
3101
+ setData(this.indexPreview, 'visible', true);
3102
+ if (this.previewTimer !== undefined)
3103
+ this.document.defaultView?.clearTimeout(this.previewTimer);
3104
+ this.previewTimer = this.document.defaultView?.setTimeout(() => {
3105
+ setData(this.indexPreview, 'visible', false);
3106
+ }, 180);
3107
+ }
3108
+
3109
+ private indexButtonAtEvent(event: PointerEvent): HTMLElement | undefined {
3110
+ const direct = (event.target as Element | null)?.closest<HTMLElement>(
3111
+ '[data-section-position]'
3112
+ );
3113
+ if (direct) return direct;
3114
+ return (
3115
+ this.document
3116
+ .elementFromPoint(event.clientX, event.clientY)
3117
+ ?.closest<HTMLElement>('[data-section-position]') ?? undefined
3118
+ );
3119
+ }
3120
+
3121
+ private handleIndexPointer = (event: PointerEvent) => {
3122
+ if (event.type === 'pointermove' && event.buttons === 0) return;
3123
+ const button = this.indexButtonAtEvent(event);
3124
+ if (!button) return;
3125
+ event.preventDefault();
3126
+ this.selectIndexPosition(
3127
+ Number(button.dataset.sectionPosition),
3128
+ button.textContent ?? ''
3129
+ );
3130
+ };
3131
+
3132
+ private handleIndexClick = (event: Event) => {
3133
+ const target = event.target;
3134
+ if (!(target instanceof Element)) return;
3135
+ const button = target.closest<HTMLElement>('[data-section-position]');
3136
+ if (!button) return;
3137
+ this.selectIndexPosition(
3138
+ Number(button.dataset.sectionPosition),
3139
+ button.textContent ?? ''
3140
+ );
3141
+ };
3142
+
3143
+ private handlePullStart = (event: PointerEvent) => {
3144
+ if (
3145
+ this.pointerReorder?.pointerId === event.pointerId ||
3146
+ !this.snapshot.capabilities?.pullToRefresh ||
3147
+ this.viewport.scrollTop > 0 ||
3148
+ (event.pointerType !== 'touch' && event.pointerType !== 'pen')
3149
+ )
3150
+ return;
3151
+ this.pullStartY = event.clientY;
3152
+ this.pullDistance = 0;
3153
+ };
3154
+
3155
+ private handlePullMove = (event: PointerEvent) => {
3156
+ if (this.pullStartY === undefined) return;
3157
+ this.pullDistance = Math.max(
3158
+ 0,
3159
+ Math.min(96, event.clientY - this.pullStartY)
3160
+ );
3161
+ if (this.pullDistance <= 0) return;
3162
+ event.preventDefault();
3163
+ this.refreshIndicator.textContent =
3164
+ this.pullDistance >= 64 ? 'Release to refresh' : 'Pull to refresh';
3165
+ setData(this.refreshIndicator, 'visible', true);
3166
+ this.refreshIndicator.style.transform =
3167
+ 'translate(-50%,' + String(Math.min(32, this.pullDistance / 2)) + 'px)';
3168
+ };
3169
+
3170
+ private handlePullEnd = () => {
3171
+ if (this.pullStartY === undefined) return;
3172
+ const shouldRefresh = this.pullDistance >= 64;
3173
+ this.pullStartY = undefined;
3174
+ this.pullDistance = 0;
3175
+ this.refreshIndicator.style.removeProperty('transform');
3176
+ this.updateRefreshIndicator();
3177
+ if (!shouldRefresh) return;
3178
+ this.callbacks.onRefresh?.();
3179
+ this.callbacks.onRowAction?.({ actionKey: 'nativeList.refresh' });
3180
+ };
3181
+
3182
+ private updateRefreshIndicator() {
3183
+ const refreshing = Boolean(this.snapshot.capabilities?.refreshing);
3184
+ this.refreshIndicator.textContent = refreshing ? 'Refreshing' : '';
3185
+ setData(this.refreshIndicator, 'visible', refreshing);
3186
+ }
3187
+
3188
+ private handleReorderPointerDown = (event: PointerEvent) => {
3189
+ if (
3190
+ this.pointerReorder ||
3191
+ this.keyboardReorder ||
3192
+ event.isPrimary === false ||
3193
+ (event.pointerType === 'mouse' && event.button !== 0)
3194
+ ) {
3195
+ return;
3196
+ }
3197
+ const target = event.target;
3198
+ if (!(target instanceof Element)) return;
3199
+ if (target.closest('button,input,textarea,select,a')) return;
3200
+ const rowElement = target.closest<HTMLElement>(
3201
+ '[data-native-list-row-index]'
3202
+ );
3203
+ const index = Number(rowElement?.dataset.nativeListRowIndex);
3204
+ const row = this.rows[index];
3205
+ if (!row || !this.isReorderable(row)) return;
3206
+ if (
3207
+ row.type === 'walletGroup' &&
3208
+ target.closest<HTMLElement>('[data-native-list-group-parent]')?.dataset
3209
+ .nativeListGroupParent !== 'true'
3210
+ )
3211
+ return;
3212
+
3213
+ const view = this.document.defaultView;
3214
+ const state: PointerReorderState = {
3215
+ pointerId: event.pointerId,
3216
+ pointerType: event.pointerType || 'mouse',
3217
+ sourceKey: row.key,
3218
+ currentIndex: index,
3219
+ startX: event.clientX,
3220
+ startY: event.clientY,
3221
+ clientX: event.clientX,
3222
+ clientY: event.clientY,
3223
+ originalRows: this.snapshot.rows,
3224
+ workingRows: [...this.snapshot.rows],
3225
+ active: false,
3226
+ };
3227
+ this.pointerReorder = state;
3228
+ if (state.pointerType === 'mouse') {
3229
+ this.captureReorderPointer(state);
3230
+ } else {
3231
+ state.longPressTimer = view?.setTimeout(
3232
+ () => this.activatePointerReorder(state),
3233
+ REORDER_TOUCH_LONG_PRESS_MS
3234
+ );
3235
+ }
3236
+ };
3237
+
3238
+ private activatePointerReorder(state: PointerReorderState) {
3239
+ if (this.pointerReorder !== state) return;
3240
+ this.clearReorderLongPress(state);
3241
+ state.active = true;
3242
+ state.activatedAt = Date.now();
3243
+ state.viewportRect = this.reorderViewportRect();
3244
+ this.captureReorderPointer(state);
3245
+ this.showReorderPreview(state);
3246
+ if (
3247
+ state.workingRows[state.currentIndex]?.type === 'walletGroup' &&
3248
+ this.reorderCompactKey !== state.sourceKey
3249
+ ) {
3250
+ this.reorderCompactKey = state.sourceKey;
3251
+ this.mounted.forEach((element) =>
3252
+ setData(element, 'nativeListAnimateReorder', true)
3253
+ );
3254
+ this.recomputeLayout();
3255
+ }
3256
+ this.updateReorderVisualState();
3257
+ this.scheduleReorderAutoScroll();
3258
+ }
3259
+
3260
+ private captureReorderPointer(state: PointerReorderState) {
3261
+ try {
3262
+ this.viewport.setPointerCapture?.(state.pointerId);
3263
+ } catch {
3264
+ // A canceled pointer can disappear before the long-press timer fires.
3265
+ }
3266
+ }
3267
+
3268
+ private releaseReorderPointer(state: PointerReorderState) {
3269
+ try {
3270
+ if (this.viewport.hasPointerCapture?.(state.pointerId))
3271
+ this.viewport.releasePointerCapture?.(state.pointerId);
3272
+ } catch {
3273
+ // The browser may have released capture while dispatching pointercancel.
3274
+ }
3275
+ }
3276
+
3277
+ private clearReorderLongPress(state: PointerReorderState) {
3278
+ if (state.longPressTimer === undefined) return;
3279
+ this.document.defaultView?.clearTimeout(state.longPressTimer);
3280
+ state.longPressTimer = undefined;
3281
+ }
3282
+
3283
+ private handleReorderPointerMove = (event: PointerEvent) => {
3284
+ const state = this.pointerReorder;
3285
+ if (!state || state.pointerId !== event.pointerId) return;
3286
+ state.clientX = event.clientX;
3287
+ state.clientY = event.clientY;
3288
+
3289
+ if (!state.active) {
3290
+ if (state.pointerType !== 'mouse') {
3291
+ this.cancelPointerReorder(false);
3292
+ return;
3293
+ }
3294
+ if (
3295
+ !hasExceededWebReorderMouseThreshold(
3296
+ event.clientX - state.startX,
3297
+ event.clientY - state.startY
3298
+ )
3299
+ )
3300
+ return;
3301
+ this.activatePointerReorder(state);
3302
+ if (!state.active) return;
3303
+ event.preventDefault();
3304
+ this.scheduleReorderMove();
3305
+ this.scheduleReorderAutoScroll();
3306
+ return;
3307
+ }
3308
+
3309
+ event.preventDefault();
3310
+ this.scheduleReorderMove();
3311
+ this.scheduleReorderAutoScroll();
3312
+ };
3313
+
3314
+ private handleReorderTouchMove = (event: TouchEvent) => {
3315
+ const state = this.pointerReorder;
3316
+ if (!state?.active || state.pointerType === 'mouse') return;
3317
+ const touch = event.touches[0];
3318
+ if (!touch) return;
3319
+ event.preventDefault();
3320
+ state.clientX = touch.clientX;
3321
+ state.clientY = touch.clientY;
3322
+ this.scheduleReorderMove();
3323
+ this.scheduleReorderAutoScroll();
3324
+ };
3325
+
3326
+ private handleReorderTouchEnd = (event: TouchEvent) => {
3327
+ const state = this.pointerReorder;
3328
+ if (!state?.active || state.pointerType === 'mouse') return;
3329
+ event.preventDefault();
3330
+ this.completePointerReorder(state);
3331
+ };
3332
+
3333
+ private handleReorderTouchCancel = () => {
3334
+ const state = this.pointerReorder;
3335
+ if (!state?.active || state.pointerType === 'mouse') return;
3336
+ this.cancelPointerReorder(true);
3337
+ };
3338
+
3339
+ private setCurrentOffset(offset: number) {
3340
+ const maximum = Math.max(0, this.contentLength() - this.viewportLength());
3341
+ const next = Math.max(0, Math.min(maximum, offset));
3342
+ if (this.layout.horizontal) this.viewport.scrollLeft = next;
3343
+ else this.viewport.scrollTop = next;
3344
+ this.scheduleFrame();
3345
+ }
3346
+
3347
+ private showReorderPreview(state: PointerReorderState) {
3348
+ this.hideReorderPreview();
3349
+ const source = [...this.mounted.values()].find(
3350
+ (element) => element.dataset.nativeListRowKey === state.sourceKey
3351
+ );
3352
+ const row = source?.firstElementChild;
3353
+ if (!source || !row) return;
3354
+ const rect = source.getBoundingClientRect();
3355
+ const groupParent = row.querySelector<HTMLElement>(
3356
+ '[data-native-list-group-parent="true"]>.ok-native-list-wallet-row'
3357
+ );
3358
+ const previewRow = groupParent ?? row;
3359
+ const previewHeight = groupParent ? 68 : rect.height;
3360
+ state.previewOffsetX = state.startX - rect.left;
3361
+ state.previewOffsetY = Math.min(
3362
+ previewHeight,
3363
+ Math.max(0, state.startY - rect.top)
3364
+ );
3365
+ this.reorderPreview.replaceChildren(previewRow.cloneNode(true));
3366
+ const sourceRow = state.workingRows[state.currentIndex];
3367
+ const badgeText = sourceRow
3368
+ ? webWalletGroupReorderBadge(sourceRow)
3369
+ : undefined;
3370
+ if (badgeText) {
3371
+ this.reorderPreview.appendChild(
3372
+ createElement(
3373
+ this.document,
3374
+ 'span',
3375
+ 'ok-native-list-reorder-count',
3376
+ badgeText
3377
+ )
3378
+ );
3379
+ }
3380
+ setData(
3381
+ this.reorderPreview,
3382
+ 'nativeListSelected',
3383
+ source.dataset.nativeListSelected === 'true'
3384
+ );
3385
+ this.reorderPreview.style.width = String(rect.width) + 'px';
3386
+ this.reorderPreview.style.height = String(previewHeight) + 'px';
3387
+ this.reorderPreview.style.transition = 'none';
3388
+ this.reorderPreview.hidden = false;
3389
+ this.updateReorderPreview(state);
3390
+ }
3391
+
3392
+ private animateReorderPreviewToCurrent(state: PointerReorderState) {
3393
+ const destination = [...this.mounted.values()].find(
3394
+ (element) => element.dataset.nativeListRowKey === state.sourceKey
3395
+ );
3396
+ const destinationIndex = this.rows.findIndex(
3397
+ (row) => row.key === state.sourceKey
3398
+ );
3399
+ const destinationLayout = this.layout.items.find(
3400
+ (item) => item.index === destinationIndex
3401
+ );
3402
+ if (this.reorderPreview.hidden || !destination || !destinationLayout) {
3403
+ this.hideReorderPreview();
3404
+ return;
3405
+ }
3406
+ this.reorderSettlingKey = state.sourceKey;
3407
+ const viewportRect = this.viewport.getBoundingClientRect();
3408
+ const left =
3409
+ viewportRect.left + destinationLayout.x - this.viewport.scrollLeft;
3410
+ const top =
3411
+ viewportRect.top + destinationLayout.y - this.viewport.scrollTop;
3412
+ this.reorderPreview.getBoundingClientRect();
3413
+ this.reorderPreview.style.transition =
3414
+ 'transform ' +
3415
+ String(REORDER_DROP_TRANSITION_MS) +
3416
+ 'ms ' +
3417
+ WEB_REORDER_ANIMATION.dropTimingFunction +
3418
+ ', box-shadow ' +
3419
+ String(REORDER_DROP_TRANSITION_MS) +
3420
+ 'ms ' +
3421
+ WEB_REORDER_ANIMATION.dropTimingFunction;
3422
+ this.reorderPreview.style.boxShadow = 'none';
3423
+ this.reorderPreview.style.transform =
3424
+ 'translate3d(' + String(left) + 'px,' + String(top) + 'px,0) scale(1)';
3425
+ this.reorderDropTimer = this.document.defaultView?.setTimeout(() => {
3426
+ this.reorderDropTimer = undefined;
3427
+ this.finishReorderDrop();
3428
+ }, REORDER_DROP_TRANSITION_MS);
3429
+ }
3430
+
3431
+ private clearReorderPreviewVisual() {
3432
+ this.reorderPreview.hidden = true;
3433
+ this.reorderPreview.replaceChildren();
3434
+ this.reorderPreview.style.removeProperty('transform');
3435
+ this.reorderPreview.style.removeProperty('transition');
3436
+ this.reorderPreview.style.removeProperty('box-shadow');
3437
+ this.reorderPreview.removeAttribute('data-native-list-selected');
3438
+ }
3439
+
3440
+ private finishReorderDrop() {
3441
+ this.clearReorderPreviewVisual();
3442
+ this.reorderSettlingKey = undefined;
3443
+ if (this.reorderCompactKey) {
3444
+ this.reorderExpandingKey = this.reorderCompactKey;
3445
+ this.reorderCompactKey = undefined;
3446
+ this.mounted.forEach((element) =>
3447
+ setData(element, 'nativeListAnimateReorder', true)
3448
+ );
3449
+ this.recomputeLayout();
3450
+ }
3451
+ this.updateReorderVisualState();
3452
+ }
3453
+
3454
+ private hideReorderPreview() {
3455
+ if (this.reorderDropTimer !== undefined) {
3456
+ this.document.defaultView?.clearTimeout(this.reorderDropTimer);
3457
+ this.reorderDropTimer = undefined;
3458
+ }
3459
+ this.clearReorderPreviewVisual();
3460
+ this.reorderSettlingKey = undefined;
3461
+ this.reorderExpandingKey = undefined;
3462
+ if (this.reorderCompactKey) {
3463
+ this.reorderCompactKey = undefined;
3464
+ this.recomputeLayout();
3465
+ }
3466
+ }
3467
+
3468
+ private updateReorderPreview(state: PointerReorderState) {
3469
+ if (
3470
+ this.reorderPreview.hidden ||
3471
+ state.previewOffsetX === undefined ||
3472
+ state.previewOffsetY === undefined
3473
+ )
3474
+ return;
3475
+ const x = state.clientX - state.previewOffsetX;
3476
+ const y = state.clientY - state.previewOffsetY;
3477
+ this.reorderPreview.style.transform =
3478
+ 'translate3d(' + String(x) + 'px,' + String(y) + 'px,0)';
3479
+ }
3480
+
3481
+ private reorderViewportRect() {
3482
+ const { left, top, right, bottom, width, height } =
3483
+ this.viewport.getBoundingClientRect();
3484
+ return { left, top, right, bottom, width, height };
3485
+ }
3486
+
3487
+ private scheduleReorderMove() {
3488
+ if (this.reorderMoveFrame !== undefined || !this.pointerReorder?.active)
3489
+ return;
3490
+ this.reorderMoveFrame = this.requestFrame(this.runReorderMove);
3491
+ }
3492
+
3493
+ private runReorderMove = () => {
3494
+ this.reorderMoveFrame = undefined;
3495
+ const state = this.pointerReorder;
3496
+ if (!state?.active) return;
3497
+ this.updateReorderPreview(state);
3498
+ this.movePointerReorderTo(state.clientX, state.clientY);
3499
+ };
3500
+
3501
+ private flushReorderMove(state: PointerReorderState) {
3502
+ if (this.reorderMoveFrame !== undefined) {
3503
+ this.cancelFrame(this.reorderMoveFrame);
3504
+ this.reorderMoveFrame = undefined;
3505
+ }
3506
+ if (this.pointerReorder !== state || !state.active) return;
3507
+ this.updateReorderPreview(state);
3508
+ this.movePointerReorderTo(state.clientX, state.clientY);
3509
+ }
3510
+
3511
+ private stopReorderMove() {
3512
+ if (this.reorderMoveFrame === undefined) return;
3513
+ this.cancelFrame(this.reorderMoveFrame);
3514
+ this.reorderMoveFrame = undefined;
3515
+ }
3516
+
3517
+ private reorderIndexAtPointer(
3518
+ state: PointerReorderState,
3519
+ clientX: number,
3520
+ clientY: number
3521
+ ): number {
3522
+ const rect = state.viewportRect ?? this.reorderViewportRect();
3523
+ const local = this.layout.horizontal
3524
+ ? Math.max(0, Math.min(Math.max(0, rect.width - 1), clientX - rect.left))
3525
+ : Math.max(0, Math.min(Math.max(0, rect.height - 1), clientY - rect.top));
3526
+ const coordinate = this.currentOffset() + local;
3527
+ const centerAt = (index: number) => {
3528
+ const item = this.layout.items[index];
3529
+ return item
3530
+ ? (itemStart(item, this.layout.horizontal) +
3531
+ itemEnd(item, this.layout.horizontal)) /
3532
+ 2
3533
+ : Number.POSITIVE_INFINITY;
3534
+ };
3535
+ let low = 0;
3536
+ let high = this.layout.items.length;
3537
+ while (low < high) {
3538
+ const middle = Math.floor((low + high) / 2);
3539
+ if (centerAt(middle) < coordinate) low = middle + 1;
3540
+ else high = middle;
3541
+ }
3542
+ const previous = Math.max(0, low - 1);
3543
+ const next = Math.min(this.layout.items.length - 1, low);
3544
+ const nearest =
3545
+ Math.abs(centerAt(previous) - coordinate) <=
3546
+ Math.abs(centerAt(next) - coordinate)
3547
+ ? previous
3548
+ : next;
3549
+ return this.layout.items[nearest]?.index ?? -1;
3550
+ }
3551
+
3552
+ private movePointerReorderTo(clientX: number, clientY: number) {
3553
+ const state = this.pointerReorder;
3554
+ if (!state?.active) return;
3555
+ const fromIndex = state.currentIndex;
3556
+ const toIndex = this.reorderIndexAtPointer(state, clientX, clientY);
3557
+ const from = state.workingRows[fromIndex];
3558
+ const to = state.workingRows[toIndex];
3559
+ if (
3560
+ !from ||
3561
+ !to ||
3562
+ fromIndex === toIndex ||
3563
+ !this.isReorderable(from) ||
3564
+ !this.isReorderable(to) ||
3565
+ from.sectionKey !== to.sectionKey
3566
+ )
3567
+ return;
3568
+ const [moved] = state.workingRows.splice(fromIndex, 1);
3569
+ if (!moved) return;
3570
+ state.workingRows.splice(toIndex, 0, moved);
3571
+ state.currentIndex = toIndex;
3572
+ const canReuseLayout = this.canReuseReorderLayout(fromIndex, toIndex);
3573
+ this.snapshot = { ...this.snapshot, rows: state.workingRows };
3574
+ this.rows = state.workingRows;
3575
+ this.remapMountedRowsForMove(fromIndex, toIndex);
3576
+ if (canReuseLayout) this.renderWindow();
3577
+ else this.recomputeLayout();
3578
+ this.updateReorderVisualState();
3579
+ }
3580
+
3581
+ private canReuseReorderLayout(fromIndex: number, toIndex: number): boolean {
3582
+ const start = Math.min(fromIndex, toIndex);
3583
+ const end = Math.max(fromIndex, toIndex);
3584
+ const first = this.layout.items[start];
3585
+ if (!first || this.layout.items.length !== this.rows.length) return false;
3586
+ for (let index = start + 1; index <= end; index += 1) {
3587
+ const item = this.layout.items[index];
3588
+ if (!item || item.width !== first.width || item.height !== first.height)
3589
+ return false;
3590
+ }
3591
+ return true;
3592
+ }
3593
+
3594
+ private remapMountedRowsForMove(fromIndex: number, toIndex: number) {
3595
+ const remapped: Array<readonly [number, HTMLElement]> = [];
3596
+ this.mounted.forEach((element, index) => {
3597
+ let nextIndex = index;
3598
+ if (index === fromIndex) nextIndex = toIndex;
3599
+ else if (fromIndex < toIndex && index > fromIndex && index <= toIndex)
3600
+ nextIndex = index - 1;
3601
+ else if (fromIndex > toIndex && index >= toIndex && index < fromIndex)
3602
+ nextIndex = index + 1;
3603
+ setData(element, 'nativeListAnimateReorder', true);
3604
+ setData(element, 'nativeListRowIndex', nextIndex);
3605
+ remapped.push([nextIndex, element]);
3606
+ });
3607
+ this.mounted.clear();
3608
+ remapped.forEach(([index, element]) => this.mounted.set(index, element));
3609
+ }
3610
+
3611
+ private reorderAutoScrollVelocity(): number {
3612
+ const state = this.pointerReorder;
3613
+ if (!state?.active) return 0;
3614
+ const rect = state.viewportRect ?? this.reorderViewportRect();
3615
+ const point = this.layout.horizontal ? state.clientX : state.clientY;
3616
+ const start = this.layout.horizontal ? rect.left : rect.top;
3617
+ const end = this.layout.horizontal ? rect.right : rect.bottom;
3618
+ return webReorderAutoScrollVelocity(
3619
+ point,
3620
+ start,
3621
+ end,
3622
+ Date.now() - (state.activatedAt ?? 0)
3623
+ );
3624
+ }
3625
+
3626
+ private scheduleReorderAutoScroll() {
3627
+ if (
3628
+ this.reorderAutoScrollFrame !== undefined ||
3629
+ !this.pointerReorder?.active
3630
+ )
3631
+ return;
3632
+ this.reorderAutoScrollFrame = this.requestFrame(this.runReorderAutoScroll);
3633
+ }
3634
+
3635
+ private runReorderAutoScroll = () => {
3636
+ this.reorderAutoScrollFrame = undefined;
3637
+ const state = this.pointerReorder;
3638
+ if (!state?.active) return;
3639
+ const velocity = this.reorderAutoScrollVelocity();
3640
+ if (velocity === 0) return;
3641
+ const before = this.currentOffset();
3642
+ this.setCurrentOffset(before + velocity);
3643
+ if (this.currentOffset() !== before) {
3644
+ this.movePointerReorderTo(state.clientX, state.clientY);
3645
+ this.scheduleReorderAutoScroll();
3646
+ }
3647
+ };
3648
+
3649
+ private handleReorderPointerEnd = (event: PointerEvent) => {
3650
+ const state = this.pointerReorder;
3651
+ if (!state || state.pointerId !== event.pointerId) return;
3652
+ this.clearReorderLongPress(state);
3653
+ this.releaseReorderPointer(state);
3654
+ if (!state.active) {
3655
+ this.pointerReorder = undefined;
3656
+ return;
3657
+ }
3658
+
3659
+ event.preventDefault();
3660
+ state.clientX = event.clientX;
3661
+ state.clientY = event.clientY;
3662
+ this.completePointerReorder(state);
3663
+ };
3664
+
3665
+ private completePointerReorder(state: PointerReorderState) {
3666
+ this.flushReorderMove(state);
3667
+ const finalRows = this.snapshot.rows;
3668
+ const reorderEvent = webReorderEventForRows(
3669
+ state.originalRows,
3670
+ finalRows,
3671
+ state.sourceKey
3672
+ );
3673
+ this.animateReorderPreviewToCurrent(state);
3674
+ this.pointerReorder = undefined;
3675
+ this.stopReorderMove();
3676
+ this.stopReorderAutoScroll();
3677
+ this.suppressClickUntil = Date.now() + 300;
3678
+ this.updateReorderVisualState();
3679
+ if (reorderEvent) this.callbacks.onReorder?.(reorderEvent);
3680
+ }
3681
+
3682
+ private handleReorderPointerCancel = (event: PointerEvent) => {
3683
+ if (this.pointerReorder?.pointerId !== event.pointerId) return;
3684
+ if (
3685
+ this.pointerReorder.active &&
3686
+ this.pointerReorder.pointerType !== 'mouse'
3687
+ )
3688
+ return;
3689
+ this.cancelPointerReorder(true);
3690
+ };
3691
+
3692
+ private stopReorderAutoScroll() {
3693
+ if (this.reorderAutoScrollFrame === undefined) return;
3694
+ this.cancelFrame(this.reorderAutoScrollFrame);
3695
+ this.reorderAutoScrollFrame = undefined;
3696
+ }
3697
+
3698
+ private cancelPointerReorder(restore: boolean) {
3699
+ const state = this.pointerReorder;
3700
+ if (!state) return;
3701
+ this.clearReorderLongPress(state);
3702
+ this.releaseReorderPointer(state);
3703
+ this.pointerReorder = undefined;
3704
+ this.stopReorderMove();
3705
+ this.stopReorderAutoScroll();
3706
+ if (state.active && restore) {
3707
+ this.remapMountedRows(state.originalRows);
3708
+ this.setSnapshot(
3709
+ { ...this.snapshot, rows: cancelWebReorderRows(state.originalRows) },
3710
+ this.selectedKeys
3711
+ );
3712
+ this.suppressClickUntil = Date.now() + 300;
3713
+ }
3714
+ if (state.active && restore) this.animateReorderPreviewToCurrent(state);
3715
+ else this.hideReorderPreview();
3716
+ this.updateReorderVisualState();
3717
+ }
3718
+
3719
+ private remapMountedRows(rows: readonly RowModel[]) {
3720
+ const elementByKey = new Map<string, HTMLElement>();
3721
+ this.mounted.forEach((element) => {
3722
+ const key = element.dataset.nativeListRowKey;
3723
+ if (key) elementByKey.set(key, element);
3724
+ setData(element, 'nativeListAnimateReorder', true);
3725
+ });
3726
+ this.mounted.clear();
3727
+ rows.forEach((row, index) => {
3728
+ const element = elementByKey.get(row.key);
3729
+ if (element) {
3730
+ setData(element, 'nativeListRowIndex', index);
3731
+ this.mounted.set(index, element);
3732
+ }
3733
+ });
3734
+ }
3735
+
3736
+ private updateReorderVisualState() {
3737
+ const sourceKey = this.pointerReorder?.active
3738
+ ? this.pointerReorder.sourceKey
3739
+ : this.keyboardReorder?.sourceKey ?? this.reorderSettlingKey;
3740
+ setData(this.root, 'nativeListDragging', Boolean(sourceKey));
3741
+ if (this.reorderMovementTimer !== undefined) {
3742
+ this.document.defaultView?.clearTimeout(this.reorderMovementTimer);
3743
+ this.reorderMovementTimer = undefined;
3744
+ }
3745
+ if (!sourceKey) {
3746
+ this.reorderMovementTimer = this.document.defaultView?.setTimeout(() => {
3747
+ this.reorderMovementTimer = undefined;
3748
+ this.reorderExpandingKey = undefined;
3749
+ this.mounted.forEach((element) => {
3750
+ setData(element, 'nativeListAnimateReorder', false);
3751
+ setData(element, 'nativeListReorderExpanding', false);
3752
+ });
3753
+ }, WEB_REORDER_ANIMATION.outOfWayDurationMs);
3754
+ }
3755
+ this.mounted.forEach((element) => {
3756
+ const dragging = element.dataset.nativeListRowKey === sourceKey;
3757
+ setData(
3758
+ element,
3759
+ 'nativeListReorderExpanding',
3760
+ element.dataset.nativeListRowKey === this.reorderExpandingKey
3761
+ );
3762
+ setData(element, 'nativeListDragging', dragging);
3763
+ if (dragging) element.setAttribute('aria-grabbed', 'true');
3764
+ else element.removeAttribute('aria-grabbed');
3765
+ });
3766
+ }
3767
+ }