@onekeyfe/react-native-native-list 3.0.157-alpha.249 → 3.0.157-alpha.251

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 (130) hide show
  1. package/README.md +21 -5
  2. package/android/src/main/java/com/onekey/nativelist/NativeListAccessoryStack.kt +711 -0
  3. package/android/src/main/java/com/onekey/nativelist/NativeListActionRenderer.kt +169 -0
  4. package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +243 -0
  5. package/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +20 -6
  6. package/android/src/main/java/com/onekey/nativelist/NativeListDataRowRenderer.kt +487 -0
  7. package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +820 -0
  8. package/android/src/main/java/com/onekey/nativelist/NativeListImageSlot.kt +147 -0
  9. package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +453 -0
  10. package/android/src/main/java/com/onekey/nativelist/NativeListMarketRenderer.kt +997 -0
  11. package/android/src/main/java/com/onekey/nativelist/NativeListMediaTileRenderer.kt +234 -0
  12. package/android/src/main/java/com/onekey/nativelist/NativeListMessageRenderer.kt +232 -0
  13. package/android/src/main/java/com/onekey/nativelist/NativeListMetricCardRenderer.kt +551 -0
  14. package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +24 -0
  15. package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +475 -0
  16. package/android/src/main/java/com/onekey/nativelist/NativeListRailRenderer.kt +143 -0
  17. package/android/src/main/java/com/onekey/nativelist/NativeListRendererRegistry.kt +42 -0
  18. package/android/src/main/java/com/onekey/nativelist/NativeListRendererRowView.kt +415 -0
  19. package/android/src/main/java/com/onekey/nativelist/NativeListResolvedText.kt +163 -0
  20. package/android/src/main/java/com/onekey/nativelist/NativeListRowHost.kt +70 -0
  21. package/android/src/main/java/com/onekey/nativelist/NativeListSectionHeaderRenderer.kt +748 -0
  22. package/android/src/main/java/com/onekey/nativelist/NativeListSystemRenderer.kt +464 -0
  23. package/android/src/main/java/com/onekey/nativelist/NativeListTextView.kt +25 -0
  24. package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +174 -104
  25. package/android/src/main/java/com/onekey/nativelist/NativeListWalletGroupRenderer.kt +407 -0
  26. package/android/src/test/java/com/margelo/nitro/nativelist/NativeListSelectorBackgroundColorTest.kt +34 -0
  27. package/docs/DESIGN.md +518 -9
  28. package/docs/MIGRATION_BASELINE.md +117 -0
  29. package/docs/ROW_TEMPLATES.md +359 -0
  30. package/docs/SPEC.md +358 -0
  31. package/docs/STYLE_SPEC.md +978 -0
  32. package/docs/images/list-capabilities.svg +39 -0
  33. package/docs/images/row-action.svg +17 -0
  34. package/docs/images/row-activity.svg +24 -0
  35. package/docs/images/row-data.svg +24 -0
  36. package/docs/images/row-identity.svg +25 -0
  37. package/docs/images/row-market.svg +23 -0
  38. package/docs/images/row-media-tile.svg +26 -0
  39. package/docs/images/row-message.svg +20 -0
  40. package/docs/images/row-metric-card.svg +33 -0
  41. package/docs/images/row-rail.svg +18 -0
  42. package/docs/images/row-section-header.svg +19 -0
  43. package/docs/images/row-style-regions.svg +30 -0
  44. package/docs/images/row-system.svg +30 -0
  45. package/docs/images/row-wallet-group.svg +26 -0
  46. package/ios/NativeListAccessoryStack.swift +680 -0
  47. package/ios/NativeListActionRenderer.swift +108 -0
  48. package/ios/NativeListActivityRenderer.swift +179 -0
  49. package/ios/NativeListDataRowRenderer.swift +444 -0
  50. package/ios/NativeListIdentityRenderer.swift +685 -0
  51. package/ios/NativeListImageSlot.swift +140 -0
  52. package/ios/NativeListLeadingVisual.swift +372 -0
  53. package/ios/NativeListMarketRenderer.swift +698 -0
  54. package/ios/NativeListMediaTileRenderer.swift +184 -0
  55. package/ios/NativeListMessageRenderer.swift +168 -0
  56. package/ios/NativeListMetricCardRenderer.swift +420 -0
  57. package/ios/NativeListModels.swift +32 -0
  58. package/ios/NativeListRailRenderer.swift +152 -0
  59. package/ios/NativeListRendererCell.swift +311 -0
  60. package/ios/NativeListRendererRegistry.swift +50 -0
  61. package/ios/NativeListResolvedText.swift +122 -0
  62. package/ios/NativeListRowHost.swift +63 -0
  63. package/ios/NativeListSectionHeaderRenderer.swift +708 -0
  64. package/ios/NativeListSystemRenderer.swift +568 -0
  65. package/ios/NativeListTextLabel.swift +42 -0
  66. package/ios/NativeListTextStyles.swift +236 -0
  67. package/ios/NativeListWalletGroupRenderer.swift +255 -0
  68. package/ios/RNCNativeListView.swift +111 -180
  69. package/lib/module/models.js +54 -0
  70. package/lib/module/validation.js +324 -26
  71. package/lib/module/web/NativeListWebEngine.js +148 -936
  72. package/lib/module/web/templates/ActionRowRenderer.js +97 -0
  73. package/lib/module/web/templates/ActivityRowRenderer.js +95 -0
  74. package/lib/module/web/templates/DataRowRenderer.js +145 -0
  75. package/lib/module/web/templates/IdentityRowRenderer.js +289 -0
  76. package/lib/module/web/templates/MarketRowRenderer.js +310 -0
  77. package/lib/module/web/templates/MediaTileRowRenderer.js +132 -0
  78. package/lib/module/web/templates/MessageRowRenderer.js +165 -0
  79. package/lib/module/web/templates/MetricCardRowRenderer.js +172 -0
  80. package/lib/module/web/templates/RailRowRenderer.js +98 -0
  81. package/lib/module/web/templates/RowContainerStyle.js +100 -0
  82. package/lib/module/web/templates/RowElements.js +93 -0
  83. package/lib/module/web/templates/RowRendererRegistry.js +70 -0
  84. package/lib/module/web/templates/RowText.js +121 -0
  85. package/lib/module/web/templates/RowVisual.js +43 -0
  86. package/lib/module/web/templates/SectionHeaderRowRenderer.js +146 -0
  87. package/lib/module/web/templates/SystemRowRenderer.js +161 -0
  88. package/lib/module/web/templates/WalletGroupRowRenderer.js +96 -0
  89. package/lib/typescript/src/models.d.ts +166 -13
  90. package/lib/typescript/src/web/NativeListWebEngine.d.ts +18 -16
  91. package/lib/typescript/src/web/templates/ActionRowRenderer.d.ts +15 -0
  92. package/lib/typescript/src/web/templates/ActivityRowRenderer.d.ts +15 -0
  93. package/lib/typescript/src/web/templates/DataRowRenderer.d.ts +15 -0
  94. package/lib/typescript/src/web/templates/IdentityRowRenderer.d.ts +16 -0
  95. package/lib/typescript/src/web/templates/MarketRowRenderer.d.ts +30 -0
  96. package/lib/typescript/src/web/templates/MediaTileRowRenderer.d.ts +15 -0
  97. package/lib/typescript/src/web/templates/MessageRowRenderer.d.ts +44 -0
  98. package/lib/typescript/src/web/templates/MetricCardRowRenderer.d.ts +15 -0
  99. package/lib/typescript/src/web/templates/RailRowRenderer.d.ts +28 -0
  100. package/lib/typescript/src/web/templates/RowContainerStyle.d.ts +9 -0
  101. package/lib/typescript/src/web/templates/RowElements.d.ts +38 -0
  102. package/lib/typescript/src/web/templates/RowRendererRegistry.d.ts +83 -0
  103. package/lib/typescript/src/web/templates/RowText.d.ts +9 -0
  104. package/lib/typescript/src/web/templates/RowVisual.d.ts +20 -0
  105. package/lib/typescript/src/web/templates/SectionHeaderRowRenderer.d.ts +15 -0
  106. package/lib/typescript/src/web/templates/SystemRowRenderer.d.ts +16 -0
  107. package/lib/typescript/src/web/templates/WalletGroupRowRenderer.d.ts +20 -0
  108. package/package.json +3 -3
  109. package/src/models.ts +281 -19
  110. package/src/validation.ts +501 -46
  111. package/src/web/NativeListWebEngine.ts +361 -1932
  112. package/src/web/templates/ActionRowRenderer.ts +123 -0
  113. package/src/web/templates/ActivityRowRenderer.ts +159 -0
  114. package/src/web/templates/DataRowRenderer.ts +212 -0
  115. package/src/web/templates/IdentityRowRenderer.ts +527 -0
  116. package/src/web/templates/MarketRowRenderer.ts +467 -0
  117. package/src/web/templates/MediaTileRowRenderer.ts +182 -0
  118. package/src/web/templates/MessageRowRenderer.ts +306 -0
  119. package/src/web/templates/MetricCardRowRenderer.ts +337 -0
  120. package/src/web/templates/RailRowRenderer.ts +136 -0
  121. package/src/web/templates/RowContainerStyle.ts +131 -0
  122. package/src/web/templates/RowElements.ts +160 -0
  123. package/src/web/templates/RowRendererRegistry.ts +111 -0
  124. package/src/web/templates/RowText.ts +181 -0
  125. package/src/web/templates/RowVisual.ts +115 -0
  126. package/src/web/templates/SectionHeaderRowRenderer.ts +243 -0
  127. package/src/web/templates/SystemRowRenderer.ts +282 -0
  128. package/src/web/templates/WalletGroupRowRenderer.ts +131 -0
  129. package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +0 -4538
  130. package/ios/NativeListCell.swift +0 -4300
@@ -0,0 +1,467 @@
1
+ import type { MarketRow, MarketTextStyle } from '../../models';
2
+ import { createElement, setData, markActionAnchorSource } from './RowElements';
3
+ import {
4
+ applyTextStyleToSlot,
5
+ applyValueSegments,
6
+ fontWeight,
7
+ } from './RowText';
8
+ import { RowVisualStyle, type RowPrimitives } from './RowVisual';
9
+ export type WebMarketLayoutStyle = Readonly<{
10
+ horizontalPadding: number;
11
+ verticalPadding: number;
12
+ leadingGap: number;
13
+ titleBadgeGap: number;
14
+ trailingGap: number;
15
+ imageWidth: number;
16
+ imageHeight: number;
17
+ imageCornerRadius: number;
18
+ changeWidth: number;
19
+ changeHeight: number;
20
+ changeCornerRadius: number;
21
+ }>;
22
+ export function resolveWebMarketLayoutStyle(
23
+ row: MarketRow
24
+ ): WebMarketLayoutStyle {
25
+ const style = row.style;
26
+ const imageWidth = style?.image?.width ?? (row.variant === 'stock' ? 40 : 32);
27
+ const imageHeight =
28
+ style?.image?.height ?? (row.variant === 'stock' ? 40 : 32);
29
+ return {
30
+ horizontalPadding:
31
+ style?.horizontalPadding ?? (row.variant === 'perp' ? 16 : 20),
32
+ verticalPadding: style?.verticalPadding ?? 12,
33
+ leadingGap: style?.leadingGap ?? (row.variant === 'perp' ? 8 : 14),
34
+ titleBadgeGap: style?.titleBadgeGap ?? 4,
35
+ trailingGap: style?.trailingGap ?? 8,
36
+ imageWidth,
37
+ imageHeight,
38
+ imageCornerRadius:
39
+ style?.image?.cornerRadius ??
40
+ (style?.image?.shape === 'square'
41
+ ? 0
42
+ : style?.image?.shape === 'rounded'
43
+ ? 8
44
+ : Math.min(imageWidth, imageHeight) / 2),
45
+ changeWidth: style?.changeWidth ?? 80,
46
+ changeHeight: style?.changeHeight ?? 32,
47
+ changeCornerRadius: style?.changeCornerRadius ?? 8,
48
+ };
49
+ }
50
+ function applyMarketTextStyle(
51
+ element: HTMLElement,
52
+ style: MarketTextStyle | undefined,
53
+ defaults: Readonly<{
54
+ fontSize: number;
55
+ lineHeight: number;
56
+ weight: number;
57
+ alignment: 'start' | 'center' | 'end';
58
+ }>
59
+ ) {
60
+ element.style.fontSize = String(style?.fontSize ?? defaults.fontSize) + 'px';
61
+ element.style.lineHeight =
62
+ String(style?.lineHeight ?? defaults.lineHeight) + 'px';
63
+ element.style.fontWeight = String(
64
+ fontWeight(style?.fontWeight, defaults.weight)
65
+ );
66
+ element.style.color = style?.color ?? '';
67
+ element.style.textAlign = style?.alignment ?? defaults.alignment;
68
+ element.style.whiteSpace = (style?.lines ?? 1) > 1 ? 'normal' : 'nowrap';
69
+ element.style.display = '';
70
+ element.style.removeProperty('-webkit-line-clamp');
71
+ element.style.removeProperty('-webkit-box-orient');
72
+ if ((style?.lines ?? 1) > 1) {
73
+ element.style.display = '-webkit-box';
74
+ element.style.setProperty('-webkit-line-clamp', String(style?.lines ?? 2));
75
+ element.style.setProperty('-webkit-box-orient', 'vertical');
76
+ }
77
+ }
78
+ export function setMarketQuoteContent(element: HTMLElement, row: MarketRow) {
79
+ const style = row.style;
80
+ const layoutStyle = resolveWebMarketLayoutStyle(row);
81
+ const price = element.querySelector<HTMLElement>(
82
+ '.ok-native-list-market-price'
83
+ );
84
+ if (price) {
85
+ price.textContent = row.price;
86
+ applyMarketTextStyle(price, style?.price, {
87
+ fontSize: 16,
88
+ lineHeight: 24,
89
+ weight: 500,
90
+ alignment: 'end',
91
+ });
92
+ applyValueSegments(
93
+ price,
94
+ row.priceSegments,
95
+ style?.price?.fontSize ?? 16,
96
+ style?.price?.lineHeight ?? 24,
97
+ fontWeight(style?.price?.fontWeight, 500)
98
+ );
99
+ }
100
+ const change = element.querySelector<HTMLElement>(
101
+ '.ok-native-list-market-change'
102
+ );
103
+ if (change) {
104
+ change.textContent = row.change.text;
105
+ setData(change, 'tone', row.change.tone);
106
+ applyMarketTextStyle(change, style?.change, {
107
+ fontSize: 14,
108
+ lineHeight: 20,
109
+ weight: 500,
110
+ alignment: 'center',
111
+ });
112
+ applyValueSegments(
113
+ change,
114
+ row.change.textSegments,
115
+ style?.change?.fontSize ?? 14,
116
+ style?.change?.lineHeight ?? 20,
117
+ fontWeight(style?.change?.fontWeight, 500)
118
+ );
119
+ change.style.width = String(layoutStyle.changeWidth) + 'px';
120
+ change.style.height = String(layoutStyle.changeHeight) + 'px';
121
+ change.style.borderRadius = String(layoutStyle.changeCornerRadius) + 'px';
122
+ change.style.color = style?.change?.color ?? row.change.textColor ?? '';
123
+ change.style.background = row.change.backgroundColor ?? '';
124
+ }
125
+ if (price) applyTextStyleToSlot(price, style?.price ?? {}, 1);
126
+ if (change) applyTextStyleToSlot(change, style?.change ?? {}, 1);
127
+ element.setAttribute(
128
+ 'aria-label',
129
+ row.accessibilityLabel ??
130
+ [row.title, row.subtitle, row.price, row.change.text]
131
+ .filter(Boolean)
132
+ .join(', ')
133
+ );
134
+ }
135
+ function createMarketRow(
136
+ document: Document,
137
+ primitives: RowPrimitives,
138
+ row: MarketRow
139
+ ): HTMLElement {
140
+ const body = createElement(
141
+ document,
142
+ 'div',
143
+ 'ok-native-list-row ok-native-list-market'
144
+ );
145
+ setData(body, 'variant', row.variant);
146
+ const style = row.style;
147
+ const layoutStyle = resolveWebMarketLayoutStyle(row);
148
+ body.style.padding =
149
+ String(layoutStyle.verticalPadding) +
150
+ 'px ' +
151
+ String(layoutStyle.horizontalPadding) +
152
+ 'px';
153
+ body.style.gap = '0px';
154
+ if (row.leadingAction) {
155
+ const action = primitives.iconAction(
156
+ row.leadingAction.name,
157
+ row.leadingAction.actionKey,
158
+ row.leadingAction.disabled,
159
+ row.leadingAction.tintColor
160
+ );
161
+ action.style.flex = '0 0 36px';
162
+ action.style.width = '36px';
163
+ action.style.height = '36px';
164
+ action.style.marginRight = '5px';
165
+ setData(action, 'testid', row.leadingAction.testID);
166
+ if (row.leadingAction.accessibilityLabel)
167
+ action.setAttribute('aria-label', row.leadingAction.accessibilityLabel);
168
+ markActionAnchorSource(action, 'leadingAction');
169
+ body.appendChild(action);
170
+ }
171
+ const visual = primitives.visual(row.leading);
172
+ if (visual) {
173
+ const width = layoutStyle.imageWidth;
174
+ const height = layoutStyle.imageHeight;
175
+ visual.style.width = String(width) + 'px';
176
+ visual.style.height = String(height) + 'px';
177
+ visual.style.flexBasis = String(width) + 'px';
178
+ visual.style.borderRadius = String(layoutStyle.imageCornerRadius) + 'px';
179
+ const borderColor =
180
+ 'borderColor' in row.leading ? row.leading.borderColor : undefined;
181
+ if (borderColor) {
182
+ visual.style.border = '1px solid ' + borderColor;
183
+ visual.style.boxSizing = 'border-box';
184
+ }
185
+ visual.querySelectorAll<HTMLElement>('img').forEach((image) => {
186
+ if (!image.classList.contains('ok-native-list-visual-corner')) {
187
+ image.style.width = '100%';
188
+ image.style.height = '100%';
189
+ if (borderColor) {
190
+ image.style.borderRadius = '0';
191
+ image.style.clipPath =
192
+ 'inset(-1px round ' + String(layoutStyle.imageCornerRadius) + 'px)';
193
+ }
194
+ image.style.objectFit =
195
+ style?.image?.contentFit === 'center'
196
+ ? 'none'
197
+ : style?.image?.contentFit ?? image.style.objectFit;
198
+ } else {
199
+ image.style.width = '20px';
200
+ image.style.height = '20px';
201
+ if (borderColor) {
202
+ image.style.right = '-5px';
203
+ image.style.bottom = '-5px';
204
+ }
205
+ }
206
+ });
207
+ visual.style.marginInlineEnd = String(layoutStyle.leadingGap) + 'px';
208
+ body.appendChild(visual);
209
+ }
210
+ const main = createElement(document, 'span', 'ok-native-list-market-main');
211
+ main.style.gap = String(style?.lineGap ?? 0) + 'px';
212
+ const titleLine = createElement(
213
+ document,
214
+ 'span',
215
+ 'ok-native-list-market-title-line'
216
+ );
217
+ titleLine.style.gap = String(layoutStyle.titleBadgeGap) + 'px';
218
+ const title = createElement(
219
+ document,
220
+ 'span',
221
+ 'ok-native-list-market-title',
222
+ row.title
223
+ );
224
+ applyMarketTextStyle(title, style?.title, {
225
+ fontSize: 16,
226
+ lineHeight: 24,
227
+ weight: 500,
228
+ alignment: 'start',
229
+ });
230
+ titleLine.appendChild(title);
231
+ row.badges?.forEach((badge, slot) => {
232
+ const element = createElement(
233
+ document,
234
+ badge.actionKey ? 'button' : 'span',
235
+ 'ok-native-list-market-badge',
236
+ badge.text
237
+ );
238
+ if (badge.actionKey) {
239
+ element.setAttribute('type', 'button');
240
+ setData(element, 'nativeListAction', badge.actionKey);
241
+ markActionAnchorSource(element, 'marketBadge', slot);
242
+ }
243
+ setData(element, 'tone', badge.tone);
244
+ element.style.color =
245
+ badge.textColor ??
246
+ (badge.tone === 'success'
247
+ ? 'var(--nl-positive)'
248
+ : badge.tone === 'danger'
249
+ ? 'var(--nl-negative)'
250
+ : badge.tone === 'info'
251
+ ? 'var(--nl-info)'
252
+ : badge.tone === 'warning'
253
+ ? 'var(--nl-primary)'
254
+ : '');
255
+ element.style.background =
256
+ badge.backgroundColor ??
257
+ ((badge.icon || badge.iconName) && !badge.text ? 'transparent' : '');
258
+ if ((badge.icon || badge.iconName) && !badge.text) {
259
+ element.style.padding = '0';
260
+ }
261
+ if (badge.style) {
262
+ applyTextStyleToSlot(element, badge.style);
263
+ if (badge.style.height !== undefined)
264
+ element.style.height = `${badge.style.height}px`;
265
+ if (badge.style.horizontalPadding !== undefined && badge.text)
266
+ element.style.paddingInline = `${badge.style.horizontalPadding}px`;
267
+ }
268
+ if (badge.accessibilityLabel)
269
+ element.setAttribute('aria-label', badge.accessibilityLabel);
270
+ if (badge.icon) {
271
+ const icon = primitives.thumbnail(badge.icon);
272
+ if (icon) {
273
+ icon.className = '';
274
+ icon.style.borderRadius = '50%';
275
+ element.prepend(icon);
276
+ }
277
+ }
278
+ if (badge.iconName === 'verified') {
279
+ const icon = createElement(
280
+ document,
281
+ 'span',
282
+ 'ok-native-list-market-badge-icon'
283
+ );
284
+ primitives.icon(icon, 'BadgeVerifiedSolid');
285
+ element.prepend(icon);
286
+ }
287
+ titleLine.appendChild(element);
288
+ });
289
+ main.appendChild(titleLine);
290
+ if (row.subtitlePrefix || row.subtitle || row.subtitleSegments?.length) {
291
+ const subtitleLine = createElement(
292
+ document,
293
+ 'span',
294
+ 'ok-native-list-market-subtitle-line'
295
+ );
296
+ subtitleLine.style.display = 'flex';
297
+ subtitleLine.style.alignItems = 'center';
298
+ subtitleLine.style.minWidth = '0';
299
+ subtitleLine.style.overflow = 'hidden';
300
+ subtitleLine.style.gap =
301
+ String(row.subtitlePrefix ? row.subtitlePrefix.gap ?? 4 : 0) + 'px';
302
+ if (row.subtitlePrefix) {
303
+ const prefix = createElement(
304
+ document,
305
+ 'span',
306
+ 'ok-native-list-market-subtitle-prefix',
307
+ row.subtitlePrefix.text
308
+ );
309
+ applyMarketTextStyle(prefix, row.subtitlePrefix.style, {
310
+ fontSize: 12,
311
+ lineHeight: 16,
312
+ weight: 400,
313
+ alignment: 'start',
314
+ });
315
+ prefix.style.display = 'block';
316
+ prefix.style.flex = '1 1 auto';
317
+ prefix.style.minWidth = '0';
318
+ prefix.style.overflow = 'hidden';
319
+ prefix.style.textOverflow = 'ellipsis';
320
+ prefix.style.color =
321
+ row.subtitlePrefix.style?.color ?? 'var(--nl-secondary)';
322
+ if (row.subtitlePrefix.maxWidth !== undefined) {
323
+ prefix.style.maxWidth = String(row.subtitlePrefix.maxWidth) + 'px';
324
+ }
325
+ subtitleLine.appendChild(prefix);
326
+ }
327
+ if (row.subtitle || row.subtitleSegments?.length) {
328
+ const subtitle = createElement(
329
+ document,
330
+ 'span',
331
+ 'ok-native-list-market-subtitle',
332
+ row.subtitle
333
+ );
334
+ applyMarketTextStyle(subtitle, style?.subtitle, {
335
+ fontSize: 14,
336
+ lineHeight: 20,
337
+ weight: 400,
338
+ alignment: 'start',
339
+ });
340
+ applyValueSegments(
341
+ subtitle,
342
+ row.subtitleSegments,
343
+ style?.subtitle?.fontSize ?? 14,
344
+ style?.subtitle?.lineHeight ?? 20,
345
+ fontWeight(style?.subtitle?.fontWeight, 400)
346
+ );
347
+ subtitle.style.flex = row.subtitlePrefix ? '0 0 auto' : '1 1 auto';
348
+ subtitleLine.appendChild(subtitle);
349
+ }
350
+ main.appendChild(subtitleLine);
351
+ }
352
+ body.appendChild(main);
353
+ const trailing = createElement(
354
+ document,
355
+ 'span',
356
+ 'ok-native-list-market-trailing'
357
+ );
358
+ trailing.style.gap = String(layoutStyle.trailingGap) + 'px';
359
+ trailing.append(
360
+ createElement(document, 'span', 'ok-native-list-market-price'),
361
+ createElement(document, 'span', 'ok-native-list-market-change')
362
+ );
363
+ body.appendChild(trailing);
364
+ setMarketQuoteContent(body, row);
365
+ for (const key of ['title', 'subtitle'] as const) {
366
+ const text = body.querySelector<HTMLElement>(
367
+ `.ok-native-list-market-${key}`
368
+ );
369
+ if (text) applyTextStyleToSlot(text, style?.[key] ?? {}, 1);
370
+ }
371
+ const prefix = body.querySelector<HTMLElement>(
372
+ '.ok-native-list-market-subtitle-prefix'
373
+ );
374
+ if (prefix) applyTextStyleToSlot(prefix, row.subtitlePrefix?.style ?? {}, 1);
375
+ return body;
376
+ }
377
+ // A quote payload changes only the quote nodes; full binds retain equal image slots.
378
+ type Asset = {
379
+ key: string;
380
+ node: HTMLElement;
381
+ defaults: RowVisualStyle;
382
+ };
383
+ type Views = { assets: Asset[]; content: string; primitives: RowPrimitives };
384
+ const views = new WeakMap<HTMLElement, Views>();
385
+ function bind(body: HTMLElement, row: MarketRow, primitives: RowPrimitives) {
386
+ const v = views.get(body)!;
387
+ v.primitives = primitives;
388
+ const content = JSON.stringify({
389
+ ...row,
390
+ price: undefined,
391
+ priceSegments: undefined,
392
+ change: undefined,
393
+ accessibilityLabel: undefined,
394
+ });
395
+ if (v.content === content) {
396
+ setMarketQuoteContent(body, row);
397
+ return;
398
+ }
399
+ let index = 0;
400
+ function asset(source: unknown, createAsset: () => HTMLElement | undefined) {
401
+ const slot = index++,
402
+ key = JSON.stringify([row.key, source]);
403
+ let entry = v.assets[slot];
404
+ if (entry?.key !== key) {
405
+ if (entry) primitives.dispose(entry.node);
406
+ const node = createAsset();
407
+ if (!node) return;
408
+ entry = {
409
+ key,
410
+ node,
411
+ defaults: new RowVisualStyle(node),
412
+ };
413
+ v.assets[slot] = entry;
414
+ }
415
+ entry.defaults.restore();
416
+ return entry.node;
417
+ }
418
+ const next = createMarketRow(
419
+ body.ownerDocument,
420
+ {
421
+ ...primitives,
422
+ visual: (source) => asset(source, () => primitives.visual(source)),
423
+ thumbnail: (source) => asset(source, () => primitives.thumbnail(source)),
424
+ },
425
+ row
426
+ );
427
+ v.assets.slice(index).forEach((slot) => primitives.dispose(slot.node));
428
+ v.assets.length = index;
429
+ body.replaceChildren(...Array.from(next.childNodes));
430
+ body.className = next.className;
431
+ body.style.cssText = next.style.cssText;
432
+ body.dataset.variant = row.variant;
433
+ body.removeAttribute('data-nl-container-background');
434
+ body.removeAttribute('data-nl-container-border');
435
+ body.setAttribute('aria-label', next.getAttribute('aria-label') ?? '');
436
+ v.content = content;
437
+ }
438
+ function create(document: Document, row: MarketRow, primitives: RowPrimitives) {
439
+ const body = document.createElement('div');
440
+ body.dataset.nlRenderer = 'market';
441
+ views.set(body, { assets: [], content: '', primitives });
442
+ bind(body, row, primitives);
443
+ return body;
444
+ }
445
+ function recycle(body: HTMLElement) {
446
+ const v = views.get(body);
447
+ if (!v) return;
448
+ v.assets.forEach((slot) => v.primitives.dispose(slot.node));
449
+ v.assets = [];
450
+ v.content = '';
451
+ body.replaceChildren();
452
+ }
453
+ export const marketRowRenderer = {
454
+ key: 'market' as const,
455
+ create,
456
+ bind,
457
+ recycle,
458
+ measure: (row: MarketRow, _width: number) => {
459
+ const style = resolveWebMarketLayoutStyle(row);
460
+ return Math.max(
461
+ row.variant === 'stock' ? 72 : 68,
462
+ style.imageHeight + 2 * style.verticalPadding
463
+ );
464
+ },
465
+ measureRendered: (_body: HTMLElement, _row: MarketRow): number | undefined =>
466
+ undefined,
467
+ };
@@ -0,0 +1,182 @@
1
+ import type { MediaTileRow, NativeListTextStyle } from '../../models';
2
+ import type { RowPrimitives } from './RowVisual';
3
+
4
+ type Views = {
5
+ title: HTMLElement;
6
+ subtitle: HTMLElement;
7
+ badge: HTMLElement;
8
+ metadata: HTMLElement;
9
+ subtitleLine: HTMLElement;
10
+ close: HTMLButtonElement;
11
+ image?: HTMLElement;
12
+ network?: HTMLElement;
13
+ imageKey: string;
14
+ networkKey: string;
15
+ primitives: RowPrimitives;
16
+ };
17
+ const views = new WeakMap<HTMLElement, Views>();
18
+ function bind(body: HTMLElement, row: MediaTileRow, primitives: RowPrimitives) {
19
+ const v = views.get(body)!;
20
+ v.primitives = primitives;
21
+ const style = row.style;
22
+ body.style.cssText = '';
23
+ body.removeAttribute('data-nl-container-background');
24
+ body.removeAttribute('data-nl-container-border');
25
+ if (style?.horizontalPadding !== undefined)
26
+ body.style.paddingInline = `${style.horizontalPadding}px`;
27
+ if (style?.verticalPadding !== undefined)
28
+ body.style.paddingBlock = `${style.verticalPadding}px`;
29
+ const text = (
30
+ node: HTMLElement,
31
+ value: string,
32
+ roleStyle?: NativeListTextStyle
33
+ ) => {
34
+ node.style.cssText = '';
35
+ node.textContent = value;
36
+ if (roleStyle) primitives.textStyle(node, roleStyle);
37
+ };
38
+ text(v.title, row.title, style?.title);
39
+ text(v.subtitle, row.subtitle || '-', style?.subtitle);
40
+ text(v.badge, row.badge?.text ?? '', style?.badge);
41
+ if (row.badge) {
42
+ v.metadata.appendChild(v.badge);
43
+ v.badge.dataset.tone = row.badge.tone ?? '';
44
+ } else v.badge.remove();
45
+ v.metadata.style.cssText = '';
46
+ if (style?.leadingGap !== undefined)
47
+ v.metadata.style.paddingTop = `${style.leadingGap}px`;
48
+ if (style?.lineGap !== undefined)
49
+ Object.assign(v.metadata.style, {
50
+ display: 'flex',
51
+ flexDirection: 'column',
52
+ rowGap: `${style.lineGap}px`,
53
+ });
54
+ const imageKey = JSON.stringify([row.key, row.image, row.imageState]);
55
+ if (v.imageKey !== imageKey) {
56
+ if (v.image) {
57
+ primitives.dispose(v.image);
58
+ v.image.remove();
59
+ }
60
+ v.image =
61
+ row.image && !row.imageState
62
+ ? primitives.thumbnail(row.image)
63
+ : undefined;
64
+ if (!v.image) {
65
+ v.image = body.ownerDocument.createElement('span');
66
+ v.image.dataset.state = row.imageState ?? 'empty';
67
+ v.image.textContent = row.imageState === 'error' ? '▧' : '';
68
+ }
69
+ v.image.className = 'ok-native-list-media-image';
70
+ body.insertBefore(v.image, v.metadata);
71
+ v.imageKey = imageKey;
72
+ }
73
+ const image = v.image!;
74
+ // Only restore geometry; loading opacity is owned by the image primitive.
75
+ ['width', 'height', 'border-radius', 'overflow', 'object-fit'].forEach(
76
+ (key) => image.style.removeProperty(key)
77
+ );
78
+ if (style?.image?.width !== undefined)
79
+ image.style.width = `${style.image.width}px`;
80
+ if (style?.image?.height !== undefined)
81
+ image.style.height = `${style.image.height}px`;
82
+ const shape = style?.image?.shape;
83
+ const radius =
84
+ style?.image?.cornerRadius !== undefined
85
+ ? `${style.image.cornerRadius}px`
86
+ : shape === 'circle'
87
+ ? '50%'
88
+ : shape === 'square'
89
+ ? '0px'
90
+ : shape === 'rounded'
91
+ ? '10px'
92
+ : undefined;
93
+ if (radius !== undefined)
94
+ image.style.setProperty('border-radius', radius, 'important');
95
+ const fit = style?.image?.contentFit ?? row.image?.contentFit;
96
+ if (fit) image.style.objectFit = fit === 'center' ? 'none' : fit;
97
+ const networkKey = JSON.stringify([row.key, row.networkImage]);
98
+ if (v.networkKey !== networkKey) {
99
+ if (v.network) {
100
+ primitives.dispose(v.network);
101
+ v.network.remove();
102
+ }
103
+ v.network = row.networkImage
104
+ ? primitives.thumbnail(row.networkImage)
105
+ : undefined;
106
+ if (v.network) {
107
+ v.network.className = 'ok-native-list-media-network';
108
+ v.subtitleLine.appendChild(v.network);
109
+ }
110
+ v.networkKey = networkKey;
111
+ }
112
+ if (row.closeActionKey) {
113
+ v.close.dataset.nativeListAction = row.closeActionKey;
114
+ body.appendChild(v.close);
115
+ } else v.close.remove();
116
+ }
117
+ function create(
118
+ document: Document,
119
+ row: MediaTileRow,
120
+ primitives: RowPrimitives
121
+ ) {
122
+ const node = (tag: string, className: string, slot?: string) => {
123
+ const n = document.createElement(tag);
124
+ n.className = className;
125
+ if (slot) n.dataset.nlSlot = slot;
126
+ return n;
127
+ };
128
+ const body = node('div', 'ok-native-list-row ok-native-list-media');
129
+ body.dataset.nlRenderer = 'mediaTile';
130
+ const metadata = node('div', 'ok-native-list-media-meta');
131
+ const subtitleLine = node('div', 'ok-native-list-media-subtitle-row');
132
+ const title = node('div', 'ok-native-list-media-title', 'title');
133
+ const subtitle = node('span', 'ok-native-list-media-subtitle', 'subtitle');
134
+ const badge = node('span', 'ok-native-list-badge', 'badge');
135
+ const close = node(
136
+ 'button',
137
+ 'ok-native-list-media-close'
138
+ ) as HTMLButtonElement;
139
+ close.type = 'button';
140
+ close.textContent = '×';
141
+ close.setAttribute('aria-label', 'Close');
142
+ close.dataset.nativeListAnchorSource = 'mediaClose';
143
+ subtitleLine.appendChild(subtitle);
144
+ metadata.append(subtitleLine, title);
145
+ body.appendChild(metadata);
146
+ views.set(body, {
147
+ title,
148
+ subtitle,
149
+ badge,
150
+ metadata,
151
+ subtitleLine,
152
+ close,
153
+ primitives,
154
+ imageKey: '',
155
+ networkKey: '',
156
+ });
157
+ bind(body, row, primitives);
158
+ return body;
159
+ }
160
+ function recycle(body: HTMLElement) {
161
+ const v = views.get(body);
162
+ if (!v) return;
163
+ [v.image, v.network].forEach((n) => {
164
+ if (n) {
165
+ v.primitives.dispose(n);
166
+ n.remove();
167
+ }
168
+ });
169
+ v.image = v.network = undefined;
170
+ v.imageKey = v.networkKey = '';
171
+ }
172
+ export const mediaTileRowRenderer = {
173
+ key: 'mediaTile' as const,
174
+ create,
175
+ bind,
176
+ recycle,
177
+ measure: (_row: MediaTileRow, _width: number) => 244,
178
+ measureRendered: (
179
+ _body: HTMLElement,
180
+ _row: MediaTileRow
181
+ ): number | undefined => undefined,
182
+ };