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

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,136 @@
1
+ import type { NativeListTextStyle, RailRow } from '../../models';
2
+ import type { RowPrimitives } from './RowVisual';
3
+ import { RowVisualStyle } from './RowVisual';
4
+
5
+ export function resolveRailRow(row: RailRow) {
6
+ const style = row.style;
7
+ return {
8
+ horizontalPadding: style?.horizontalPadding ?? 4,
9
+ verticalPadding: style?.verticalPadding ?? 4,
10
+ leadingGap: style?.leadingGap ?? 6,
11
+ badgeGap: style?.titleBadgeGap ?? 6,
12
+ trailingGap: style?.trailingGap ?? 6,
13
+ width: style?.image?.width ?? 20,
14
+ height: style?.image?.height ?? 20,
15
+ title: row.title,
16
+ badge: row.badge?.text ?? '',
17
+ status: row.status === 'none' ? '' : row.status ?? '',
18
+ };
19
+ }
20
+
21
+ type Views = {
22
+ title: HTMLElement;
23
+ badge: HTMLElement;
24
+ status: HTMLElement;
25
+ visual?: HTMLElement;
26
+ visualStyle?: RowVisualStyle;
27
+ identity: string;
28
+ primitives: RowPrimitives;
29
+ };
30
+ const views = new WeakMap<HTMLElement, Views>();
31
+
32
+ function bind(body: HTMLElement, row: RailRow, primitives: RowPrimitives) {
33
+ const v = views.get(body)!;
34
+ v.primitives = primitives;
35
+ const r = resolveRailRow(row);
36
+ body.style.cssText = '';
37
+ body.removeAttribute('data-nl-container-background');
38
+ body.removeAttribute('data-nl-container-border');
39
+ body.style.padding = `${r.verticalPadding}px ${r.horizontalPadding}px`;
40
+ const text = (
41
+ view: HTMLElement,
42
+ value: string,
43
+ style?: NativeListTextStyle
44
+ ) => {
45
+ view.style.cssText = '';
46
+ view.textContent = value;
47
+ if (style) primitives.textStyle(view, style);
48
+ if (value) body.appendChild(view);
49
+ else view.remove();
50
+ };
51
+ text(v.title, r.title, row.style?.title);
52
+ text(v.badge, r.badge, row.style?.badge);
53
+ if (row.badge?.tone) v.badge.dataset.tone = row.badge.tone;
54
+ else delete v.badge.dataset.tone;
55
+ v.badge.style.marginInlineStart = `${r.badgeGap - 6}px`;
56
+ text(v.status, r.status, row.style?.status);
57
+ v.status.style.marginInlineStart = `${r.trailingGap - 6}px`;
58
+ const identity = JSON.stringify([row.key, row.visual]);
59
+ if (identity !== v.identity) {
60
+ if (v.visual) {
61
+ primitives.dispose(v.visual);
62
+ v.visual.remove();
63
+ }
64
+ v.visual = primitives.visual(row.visual);
65
+ v.visualStyle = v.visual ? new RowVisualStyle(v.visual) : undefined;
66
+ v.identity = identity;
67
+ }
68
+ if (v.visual) body.insertBefore(v.visual, body.firstChild);
69
+ v.visualStyle?.apply(row.style?.image, r.width, r.height, r.leadingGap - 6);
70
+ }
71
+ function create(document: Document, row: RailRow, primitives: RowPrimitives) {
72
+ const span = (className: string, slot: string) => {
73
+ const view = document.createElement('span');
74
+ view.className = className;
75
+ view.dataset.nlSlot = slot;
76
+ return view;
77
+ };
78
+ const body = document.createElement('div');
79
+ body.className = 'ok-native-list-row ok-native-list-rail';
80
+ body.dataset.nlRenderer = 'rail';
81
+ views.set(body, {
82
+ title: span('ok-native-list-rail-title', 'title'),
83
+ badge: span('ok-native-list-badge', 'badge'),
84
+ status: span('ok-native-list-secondary', 'status'),
85
+ identity: '',
86
+ primitives,
87
+ });
88
+ bind(body, row, primitives);
89
+ return body;
90
+ }
91
+ function recycle(body: HTMLElement) {
92
+ const v = views.get(body);
93
+ if (!v) return;
94
+ if (v.visual) {
95
+ v.primitives.dispose(v.visual);
96
+ v.visual.remove();
97
+ }
98
+ v.visual = undefined;
99
+ v.visualStyle = undefined;
100
+ v.identity = '';
101
+ }
102
+ export function measureRailWidth(row: RailRow) {
103
+ const r = resolveRailRow(row);
104
+ const width = (
105
+ value: string,
106
+ style: NativeListTextStyle | undefined,
107
+ size: number
108
+ ) => value.length * 7 * ((style?.fontSize ?? size) / size);
109
+ // Retain the historical 50px allowance and viewport limits at default values.
110
+ return Math.min(
111
+ 288,
112
+ Math.max(
113
+ 72,
114
+ 50 +
115
+ width(r.title, row.style?.title, 12) +
116
+ width(r.badge, row.style?.badge, 11) +
117
+ width(r.status, row.style?.status, 13) +
118
+ (r.horizontalPadding - 4) * 2 +
119
+ r.width -
120
+ 20 +
121
+ r.leadingGap -
122
+ 6 +
123
+ (r.badge ? r.badgeGap - 6 : 0) +
124
+ (r.status ? r.trailingGap - 6 : 0)
125
+ )
126
+ );
127
+ }
128
+ export const railRowRenderer = {
129
+ key: 'rail' as const,
130
+ create,
131
+ bind,
132
+ recycle,
133
+ measure: (_row: RailRow, _width: number) => 40,
134
+ measureRendered: (_body: HTMLElement, _row: RailRow): number | undefined =>
135
+ undefined,
136
+ };
@@ -0,0 +1,131 @@
1
+ import type { RowModel } from '../../models';
2
+
3
+ type Write = {
4
+ element: HTMLElement;
5
+ property: string;
6
+ previous: string;
7
+ previousPriority: string;
8
+ written: string;
9
+ };
10
+
11
+ // Inline properties written by the container pass, per row body. Renderers only
12
+ // reset the nodes they own, so the container undoes its own writes on rebind.
13
+ const writesByBody = new WeakMap<HTMLElement, Write[]>();
14
+
15
+ function restoreContainerWrites(body: HTMLElement, force: boolean) {
16
+ body.removeAttribute('data-nl-container-background');
17
+ body.removeAttribute('data-nl-container-border');
18
+ const writes = writesByBody.get(body);
19
+ if (!writes) return;
20
+ writesByBody.delete(body);
21
+ // Undo in reverse so repeated writes restore the oldest value.
22
+ for (let index = writes.length - 1; index >= 0; index -= 1) {
23
+ const write = writes[index]!;
24
+ const { style } = write.element;
25
+ // Fallback path only (no reset before the renderer bind): a renderer that
26
+ // already rewrote this property owns its current value. It cannot tell a
27
+ // renderer rewrite of the same value apart, which is why the engine and
28
+ // WalletGroup reset before binding instead.
29
+ if (!force && style.getPropertyValue(write.property) !== write.written)
30
+ continue;
31
+ style.removeProperty(write.property);
32
+ if (write.previous)
33
+ style.setProperty(write.property, write.previous, write.previousPriority);
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Undo the previous container pass before a renderer rebinds `body`, so the
39
+ * renderer always writes over its own baseline and the next
40
+ * `applyRowContainerStyle` records that fresh value as the value to restore.
41
+ */
42
+ export function resetRowContainerStyle(body: HTMLElement): void {
43
+ restoreContainerWrites(body, true);
44
+ }
45
+
46
+ function writeProperty(
47
+ body: HTMLElement,
48
+ element: HTMLElement,
49
+ property: string,
50
+ value: string,
51
+ priority = ''
52
+ ) {
53
+ const { style } = element;
54
+ const write: Write = {
55
+ element,
56
+ property,
57
+ previous: style.getPropertyValue(property),
58
+ previousPriority: style.getPropertyPriority(property),
59
+ written: '',
60
+ };
61
+ style.setProperty(property, value, priority);
62
+ write.written = style.getPropertyValue(property);
63
+ let writes = writesByBody.get(body);
64
+ if (!writes) writesByBody.set(body, (writes = []));
65
+ writes.push(write);
66
+ }
67
+
68
+ export function applyRowContainerStyle(body: HTMLElement, row: RowModel): void {
69
+ restoreContainerWrites(body, false);
70
+ const set = (element: HTMLElement, property: string, value: string) =>
71
+ writeProperty(body, element, property, value);
72
+ if (
73
+ row.type === 'system' &&
74
+ row.variant === 'warning' &&
75
+ (row.style?.container?.height ?? row.height) !== undefined
76
+ )
77
+ set(body, 'height', '100%');
78
+ const container = row.style?.container;
79
+ if (!container) return;
80
+ if (container.backgroundColor !== undefined) {
81
+ set(body, '--nl-container-background', container.backgroundColor);
82
+ body.setAttribute('data-nl-container-background', 'true');
83
+ // A CSS rule allows existing hover/press feedback to override the resting fill.
84
+ // The template/engine re-applies its own fill on every bind, so this
85
+ // removal is not tracked for restoration.
86
+ body.style.removeProperty('background');
87
+ body.style.removeProperty('background-color');
88
+ }
89
+ if (container.cornerRadius !== undefined)
90
+ set(body, 'border-radius', `${container.cornerRadius}px`);
91
+ if (container.borderWidth !== undefined) {
92
+ // Paint above member backgrounds without changing the content box or hit targets.
93
+ body.setAttribute('data-nl-container-border', 'true');
94
+ set(body, '--nl-container-border-width', `${container.borderWidth}px`);
95
+ set(
96
+ body,
97
+ '--nl-container-border-color',
98
+ container.borderColor ?? 'transparent'
99
+ );
100
+ set(body, 'border-color', 'transparent');
101
+ } else if (container.borderColor !== undefined)
102
+ set(body, 'border-color', container.borderColor);
103
+ if (container.contentVerticalAlignment !== undefined) {
104
+ const alignment = {
105
+ top: 'flex-start',
106
+ center: 'center',
107
+ bottom: 'flex-end',
108
+ }[container.contentVerticalAlignment];
109
+ const vertical =
110
+ row.type === 'walletGroup' ||
111
+ row.type === 'mediaTile' ||
112
+ row.type === 'metricCard' ||
113
+ (row.type === 'identity' && row.presentation === 'walletSidebar') ||
114
+ (row.type === 'system' && row.variant === 'warning');
115
+ if (vertical) {
116
+ set(body, 'display', 'flex');
117
+ set(body, 'flex-direction', 'column');
118
+ if (row.type === 'mediaTile') set(body, 'gap', '0px');
119
+ set(body, 'justify-content', alignment);
120
+ Array.from(body.children).forEach((child) => {
121
+ set(child as HTMLElement, 'flex-shrink', '0');
122
+ });
123
+ } else {
124
+ set(body, 'align-items', alignment);
125
+ // Timestamp/accessory defaults may have their own align-self.
126
+ Array.from(body.children).forEach((child) => {
127
+ set(child as HTMLElement, 'align-self', alignment);
128
+ });
129
+ }
130
+ }
131
+ }
@@ -0,0 +1,160 @@
1
+ import type { NativeListActionSource } from '../../models';
2
+ export function createElement(
3
+ document: Document,
4
+ tag: string,
5
+ className?: string,
6
+ text?: string
7
+ ): HTMLElement {
8
+ const element = document.createElement(tag);
9
+ if (className) element.className = className;
10
+ if (text !== undefined) element.textContent = text;
11
+ return element;
12
+ }
13
+
14
+ export function setData(
15
+ element: HTMLElement,
16
+ name: string,
17
+ value: string | number | boolean | undefined
18
+ ) {
19
+ if (value === undefined) delete element.dataset[name];
20
+ else element.dataset[name] = String(value);
21
+ }
22
+
23
+ export function tagSlot<T extends HTMLElement>(element: T, slot: string): T {
24
+ element.dataset.nlSlot = slot;
25
+ return element;
26
+ }
27
+
28
+ export function markActionAnchorSource(
29
+ element: HTMLElement,
30
+ source: NativeListActionSource,
31
+ slot?: number
32
+ ) {
33
+ setData(element, 'nativeListAnchorSource', source);
34
+ if (slot !== undefined) setData(element, 'nativeListAnchorSlot', slot);
35
+ }
36
+
37
+ export function createTextColumn(
38
+ document: Document,
39
+ title: string,
40
+ subtitle?: string,
41
+ tertiary?: string,
42
+ tertiaryTone?: 'secondary' | 'info',
43
+ badges?: readonly Readonly<{ text: string; tone?: string }>[],
44
+ slots: Readonly<{ title: string; subtitle: string; tertiary: string }> = {
45
+ title: 'title',
46
+ subtitle: 'subtitle',
47
+ tertiary: 'tertiary',
48
+ }
49
+ ): HTMLElement {
50
+ const column = createElement(document, 'span', 'ok-native-list-flex');
51
+ const titleLine = tagSlot(
52
+ createElement(document, 'span', 'ok-native-list-title', title),
53
+ slots.title
54
+ );
55
+ if (badges?.length) {
56
+ titleLine.removeAttribute('data-nl-slot');
57
+ titleLine.replaceChildren(
58
+ tagSlot(createElement(document, 'span', undefined, title), slots.title)
59
+ );
60
+ const badgeLine = createElement(document, 'span', 'ok-native-list-badges');
61
+ badges.forEach((badge) =>
62
+ badgeLine.appendChild(createBadge(document, badge))
63
+ );
64
+ titleLine.appendChild(badgeLine);
65
+ }
66
+ column.appendChild(titleLine);
67
+ if (subtitle)
68
+ column.appendChild(
69
+ tagSlot(
70
+ createElement(document, 'span', 'ok-native-list-secondary', subtitle),
71
+ slots.subtitle
72
+ )
73
+ );
74
+ if (tertiary) {
75
+ const element = tagSlot(
76
+ createElement(
77
+ document,
78
+ 'span',
79
+ tertiaryTone === 'info'
80
+ ? 'ok-native-list-secondary ok-native-list-info'
81
+ : 'ok-native-list-secondary ok-native-list-tertiary',
82
+ tertiary
83
+ ),
84
+ slots.tertiary
85
+ );
86
+ column.appendChild(element);
87
+ }
88
+ return column;
89
+ }
90
+ export function createBadge(
91
+ document: Document,
92
+ badge: Readonly<{ text: string; tone?: string }>
93
+ ): HTMLElement {
94
+ const element = tagSlot(
95
+ createElement(document, 'span', 'ok-native-list-badge', badge.text),
96
+ 'badge'
97
+ );
98
+ setData(element, 'tone', badge.tone);
99
+ return element;
100
+ }
101
+
102
+ // Selector digits retain their font family, including explicit button fonts.
103
+ export function applyTabularNumbers(body: HTMLElement): void {
104
+ body.style.fontVariantNumeric = 'tabular-nums';
105
+ body.querySelectorAll<HTMLElement>('span,button').forEach((text) => {
106
+ text.style.fontVariantNumeric = 'tabular-nums';
107
+ });
108
+ }
109
+
110
+ /**
111
+ * Selector presentations use their explicit-height geometry whenever the row
112
+ * has a resolved explicit height: the legacy `row.height` or the preferred
113
+ * `style.container.height`.
114
+ */
115
+ export function hasExplicitRowHeight(
116
+ row: Readonly<{
117
+ height?: number;
118
+ style?: Readonly<{ container?: Readonly<{ height?: number }> }>;
119
+ }>
120
+ ): boolean {
121
+ return (row.style?.container?.height ?? row.height) !== undefined;
122
+ }
123
+
124
+ /** Legacy `size` preset adjustment, applied by templates that opt in. */
125
+ export function rowSizeModifier(size: string | undefined): number {
126
+ if (size === 'small') return -8;
127
+ if (size === 'large') return 12;
128
+ return 0;
129
+ }
130
+
131
+ // Async image reveal state is owned by the image lifecycle, not by template
132
+ // defaults: a reused image that already fired `load` never fires it again.
133
+ const isImageLoadingNode = (node: HTMLElement) =>
134
+ node.tagName === 'IMG' ||
135
+ node.classList.contains('ok-native-list-selector-image-background');
136
+
137
+ /** Captures factory inline styles of a template subtree. */
138
+ export function captureInlineStyles(
139
+ root: HTMLElement
140
+ ): Map<HTMLElement, string> {
141
+ return new Map(
142
+ [root, ...root.querySelectorAll<HTMLElement>('*')].map((node) => [
143
+ node,
144
+ node.style.cssText,
145
+ ])
146
+ );
147
+ }
148
+
149
+ /** Restores captured factory styles while keeping image loading visibility. */
150
+ export function restoreInlineStyles(defaults: Map<HTMLElement, string>) {
151
+ defaults.forEach((css, node) => {
152
+ if (!isImageLoadingNode(node)) {
153
+ node.style.cssText = css;
154
+ return;
155
+ }
156
+ const opacity = node.style.opacity;
157
+ node.style.cssText = css;
158
+ node.style.opacity = opacity;
159
+ });
160
+ }
@@ -0,0 +1,111 @@
1
+ import type { WalletGroupRow } from '../../models';
2
+ import { walletGroupRowRenderer } from './WalletGroupRowRenderer';
3
+ import type { IdentityRow, SectionHeaderRow } from '../../models';
4
+ import { identityRowRenderer } from './IdentityRowRenderer';
5
+ import { sectionHeaderRowRenderer } from './SectionHeaderRowRenderer';
6
+ import type { MarketRow } from '../../models';
7
+ import { marketRowRenderer } from './MarketRowRenderer';
8
+ import type { MetricCardRow } from '../../models';
9
+ import { metricCardRowRenderer } from './MetricCardRowRenderer';
10
+ import type { DataRow } from '../../models';
11
+ import { dataRowRenderer } from './DataRowRenderer';
12
+ import type { ActivityRow } from '../../models';
13
+ import { activityRowRenderer } from './ActivityRowRenderer';
14
+ import type { SystemRow } from '../../models';
15
+ import { systemRowRenderer } from './SystemRowRenderer';
16
+ import type { ActionRow } from '../../models';
17
+ import { actionRowRenderer } from './ActionRowRenderer';
18
+ import { mediaTileRowRenderer } from './MediaTileRowRenderer';
19
+ import type { MessageRow, RailRow, MediaTileRow, RowModel } from '../../models';
20
+ import { messageRowRenderer } from './MessageRowRenderer';
21
+ import type { RowPrimitives } from './RowVisual';
22
+ import { railRowRenderer } from './RailRowRenderer';
23
+
24
+ type Renderer<R extends RowModel> = {
25
+ key: string;
26
+ appliesSizePreset?: (row: R) => boolean;
27
+ create: (
28
+ document: Document,
29
+ row: R,
30
+ primitives: RowPrimitives
31
+ ) => HTMLElement;
32
+ bind: (body: HTMLElement, row: R, primitives: RowPrimitives) => void;
33
+ measure: (row: R, width: number, layout?: string) => number;
34
+ measureRendered: (body: HTMLElement, row: R) => number | undefined;
35
+ recycle: (body: HTMLElement) => void;
36
+ reorderPreview?: (
37
+ body: HTMLElement,
38
+ row: R
39
+ ) => { element: HTMLElement; height: number } | undefined;
40
+ };
41
+ function registration<R extends RowModel>(renderer: Renderer<R>, row: R) {
42
+ return {
43
+ key: renderer.key,
44
+ appliesSizePreset: renderer.appliesSizePreset?.(row) ?? true,
45
+ create: (document: Document, primitives: RowPrimitives) =>
46
+ renderer.create(document, row, primitives),
47
+ bind: (body: HTMLElement, primitives: RowPrimitives) =>
48
+ renderer.bind(body, row, primitives),
49
+ measure: (width: number, layout?: string) =>
50
+ renderer.measure(row, width, layout),
51
+ measureRendered: (body: HTMLElement) => renderer.measureRendered(body, row),
52
+ reorderPreview: (body: HTMLElement) => renderer.reorderPreview?.(body, row),
53
+ };
54
+ }
55
+ const factories = {
56
+ walletGroup: (row: RowModel) =>
57
+ registration(walletGroupRowRenderer, row as WalletGroupRow),
58
+ identity: (row: RowModel) =>
59
+ registration(identityRowRenderer, row as IdentityRow),
60
+ sectionHeader: (row: RowModel) =>
61
+ registration(sectionHeaderRowRenderer, row as SectionHeaderRow),
62
+ market: (row: RowModel) => registration(marketRowRenderer, row as MarketRow),
63
+ metricCard: (row: RowModel) =>
64
+ registration(metricCardRowRenderer, row as MetricCardRow),
65
+ dataRow: (row: RowModel) => registration(dataRowRenderer, row as DataRow),
66
+ activity: (row: RowModel) =>
67
+ registration(activityRowRenderer, row as ActivityRow),
68
+ system: (row: RowModel) => registration(systemRowRenderer, row as SystemRow),
69
+ action: (row: RowModel) => registration(actionRowRenderer, row as ActionRow),
70
+ message: (row: RowModel) =>
71
+ registration(messageRowRenderer, row as MessageRow),
72
+ rail: (row: RowModel) => registration(railRowRenderer, row as RailRow),
73
+ mediaTile: (row: RowModel) =>
74
+ registration(mediaTileRowRenderer, row as MediaTileRow),
75
+ } satisfies Record<
76
+ RowModel['type'],
77
+ (row: RowModel) => ReturnType<typeof registration>
78
+ >;
79
+ const renderers = {
80
+ walletGroup: walletGroupRowRenderer,
81
+ identity: identityRowRenderer,
82
+ sectionHeader: sectionHeaderRowRenderer,
83
+ market: marketRowRenderer,
84
+ metricCard: metricCardRowRenderer,
85
+ dataRow: dataRowRenderer,
86
+ activity: activityRowRenderer,
87
+ system: systemRowRenderer,
88
+ action: actionRowRenderer,
89
+ message: messageRowRenderer,
90
+ rail: railRowRenderer,
91
+ mediaTile: mediaTileRowRenderer,
92
+ };
93
+ export type RowRendererKey = RowModel['type'];
94
+ const isRegistered = (key: string): key is keyof typeof factories =>
95
+ Object.prototype.hasOwnProperty.call(factories, key);
96
+
97
+ // Closed registration; a row's data key, styles and placement never affect reuse.
98
+ export function rowRenderer(row: RowModel) {
99
+ if (!isRegistered(row.type))
100
+ throw new Error('No renderer registered for ' + row.type);
101
+ return factories[row.type](row);
102
+ }
103
+ export function rowRendererKey(row: RowModel): RowRendererKey {
104
+ return row.type;
105
+ }
106
+ export function recycleRowBody(body: HTMLElement): boolean {
107
+ const key = body.dataset.nlRenderer ?? '';
108
+ if (!isRegistered(key)) return false;
109
+ renderers[key].recycle(body);
110
+ return true;
111
+ }