@onekeyfe/react-native-native-list 6.7.0-alpha.403 → 6.7.0-alpha.404

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.
@@ -31,8 +31,6 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
31
31
  private val titleLine = PackedTitleLineLayout(context)
32
32
  private val title = NativeListTextView(context)
33
33
  private val titleIcon = OneKeyIconView(context)
34
- private val titleLeadingIcon = OneKeyIconView(context)
35
- private val titleSegments = NativeListTitleSegmentsView(context)
36
34
  private var titleImage: NativeListImageSlot? = null
37
35
  private val subtitle = NativeListTextView(context)
38
36
  private val tertiary = NativeListTextView(context)
@@ -111,13 +109,8 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
111
109
  mainColumn.gravity = Gravity.CENTER_VERTICAL
112
110
  titleLine.orientation = HORIZONTAL
113
111
  titleLine.gravity = Gravity.CENTER_VERTICAL
114
- titleLine.addView(titleLeadingIcon, LayoutParams(dp(16), dp(16)).apply { marginEnd = dp(6) })
115
112
  titleLine.addView(title, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
116
- titleLine.addView(titleSegments, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
117
113
  titleLine.addView(titleIcon, LayoutParams(dp(20), dp(20)).apply { marginStart = dp(4) })
118
- titleLine.titleChild = title
119
- titleLeadingIcon.visibility = GONE
120
- titleSegments.visibility = GONE
121
114
  titleLine.addView(badgeLine)
122
115
  trailingColumn.onAction = { key, view, slot, target ->
123
116
  emitAction(key, view, "trailingAccessory", slot, target, trailingColumn.anchorInset(view))
@@ -230,12 +223,6 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
230
223
  titleIcon.visibility = GONE
231
224
  titleIcon.contentDescription = null
232
225
  titleIcon.setOnClickListener(null)
233
- titleLeadingIcon.visibility = GONE
234
- titleLeadingIcon.contentDescription = null
235
- titleSegments.visibility = GONE
236
- titleSegments.clearRuns()
237
- titleSegments.layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
238
- titleLine.titleChild = title
239
226
  for (view in listOf(title, subtitle, tertiary, badgeLine)) {
240
227
  view.text = ""
241
228
  view.visibility = GONE
@@ -356,46 +343,6 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
356
343
  }
357
344
  // Legacy: overhanging selector icons/checkboxes must not be clipped by row padding.
358
345
  if (trailingColumn.parent === this && trailingColumn.requestsUnclippedHost) clipToPadding = false
359
- if (titleSegments.visibility == VISIBLE) bindTitleRuns(item, theme)
360
- }
361
-
362
- // Runs take the title's final text appearance and color unless they set
363
- // their own tone or weight.
364
- private fun bindTitleRuns(item: NativeListItem, theme: JSONObject?) {
365
- titleSegments.typeface = title.typeface
366
- titleSegments.setTextSize(android.util.TypedValue.COMPLEX_UNIT_PX, title.textSize)
367
- titleSegments.includeFontPadding = title.includeFontPadding
368
- titleSegments.letterSpacing = title.letterSpacing
369
- titleSegments.opticalOffsetY = title.opticalOffsetY
370
- titleSegments.maxLines = 1
371
- titleSegments.ellipsize = TextUtils.TruncateAt.END
372
- TextViewCompat.setLineHeight(titleSegments, title.lineHeight)
373
- val baseColor = title.currentTextColor
374
- titleSegments.setTextColor(baseColor)
375
- val segments = item.json.optJSONArray("titleSegments") ?: return
376
- val runs =
377
- (0 until segments.length()).mapNotNull { index ->
378
- val segment = segments.optJSONObject(index) ?: return@mapNotNull null
379
- NativeListTitleSegmentsView.Run(
380
- text = segment.optString("text"),
381
- typeface =
382
- when (segment.optString("fontWeight")) {
383
- "medium" -> fonts.medium(context)
384
- "regular" -> fonts.regular(context)
385
- else -> null
386
- },
387
- color =
388
- when (segment.optString("tone")) {
389
- "secondary" -> color(theme, "secondaryText", "#0000009B")
390
- "primary" -> color(theme, "primaryText", "#000000DF")
391
- else -> baseColor
392
- },
393
- shrink = segment.optBoolean("shrink", false),
394
- maxWidthRatio =
395
- if (segment.has("maxWidthRatio")) segment.optDouble("maxWidthRatio").toFloat() else null,
396
- )
397
- }
398
- titleSegments.setRuns(runs, item.json.optString("title"))
399
346
  }
400
347
 
401
348
  private fun styleGap(column: LinearLayout, gap: Int) {
@@ -529,8 +476,7 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
529
476
  val imageWidth = titleImage?.view?.takeIf { it.visibility == VISIBLE }?.let {
530
477
  it.layoutParams.width + dp(4)
531
478
  } ?: 0
532
- val icon = (if (titleIcon.visibility == VISIBLE) dp(24) else 0) + imageWidth +
533
- (if (titleLeadingIcon.visibility == VISIBLE) dp(22) else 0)
479
+ val icon = (if (titleIcon.visibility == VISIBLE) dp(24) else 0) + imageWidth
534
480
  (inner * ratio).roundToInt()
535
481
  .minus(leading.width + leading.marginEnd + icon)
536
482
  .coerceAtLeast(0)
@@ -541,10 +487,9 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
541
487
  super.onMeasure(widthMeasureSpec, heightMeasureSpec)
542
488
  if (titleSuffixLabels.isNotEmpty()) {
543
489
  val fixedWidth =
544
- (0 until titleLine.childCount).sumOf { index ->
490
+ (1 until titleLine.childCount).sumOf { index ->
545
491
  val view = titleLine.getChildAt(index)
546
- if (view.visibility == GONE || view === titleLine.titleChild ||
547
- titleSuffixLabels.any { it === view }) 0
492
+ if (view.visibility == GONE || titleSuffixLabels.any { it === view }) 0
548
493
  else {
549
494
  val margins = view.layoutParams as MarginLayoutParams
550
495
  view.measuredWidth + margins.leftMargin + margins.rightMargin
@@ -808,30 +753,6 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
808
753
  titleLine.packsChildrenAtStart = true
809
754
  title.ellipsize = TextUtils.TruncateAt.END
810
755
  showText(title, item.json.optString("title"), item.json.optInt("titleLines", 1))
811
- // Runs replace the drawn title except where `title` itself is highlighted;
812
- // their content is bound once the title style is final.
813
- if ((item.json.optJSONArray("titleSegments")?.length() ?: 0) > 0 &&
814
- (item.json.optJSONArray("titleMatch")?.length() ?: 0) == 0
815
- ) {
816
- title.visibility = GONE
817
- titleSegments.visibility = VISIBLE
818
- titleLine.titleChild = titleSegments
819
- }
820
- item.json.optJSONObject("titleLeadingIcon")?.let { icon ->
821
- titleLeadingIcon.iconName = icon.optString("name")
822
- titleLeadingIcon.glyphSizeDp = 16
823
- titleLeadingIcon.useSourceScale = sourceScale
824
- titleLeadingIcon.tintColor = safeColor(
825
- icon.optString("tintColor").takeIf(String::isNotEmpty),
826
- color(theme, "iconSubdued", "#00000072"),
827
- )
828
- titleLeadingIcon.contentDescription =
829
- icon.optString("accessibilityLabel").takeIf(String::isNotEmpty)
830
- titleLeadingIcon.importantForAccessibility =
831
- if (titleLeadingIcon.contentDescription == null) View.IMPORTANT_FOR_ACCESSIBILITY_NO
832
- else View.IMPORTANT_FOR_ACCESSIBILITY_YES
833
- titleLeadingIcon.visibility = VISIBLE
834
- }
835
756
  item.json.optJSONArray("titleSuffixes")?.let { suffixes ->
836
757
  for (index in 0 until suffixes.length()) {
837
758
  val suffix = suffixes.optJSONObject(index) ?: continue
@@ -111,9 +111,6 @@ internal class PackedTitleLineLayout(context: android.content.Context) : LinearL
111
111
  var packsChildrenAtStart = false
112
112
  // OneKey patch: optional cap for the Market subtitle's localized name.
113
113
  var leadingTextMaxWidth = Int.MAX_VALUE
114
- // The text view that takes what the other children leave; the first child
115
- // when unset, so a glyph may precede the title once this names it.
116
- var titleChild: TextView? = null
117
114
 
118
115
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
119
116
  if (!packsChildrenAtStart || childCount < 2) {
@@ -121,13 +118,13 @@ internal class PackedTitleLineLayout(context: android.content.Context) : LinearL
121
118
  return
122
119
  }
123
120
 
124
- val title = titleChild?.takeIf { it.parent === this } ?: getChildAt(0)
121
+ val title = getChildAt(0)
125
122
  val widthMode = MeasureSpec.getMode(widthMeasureSpec)
126
123
  val widthSize = MeasureSpec.getSize(widthMeasureSpec)
127
124
  var accessoryWidth = 0
128
- for (index in 0 until childCount) {
125
+ for (index in 1 until childCount) {
129
126
  val child = getChildAt(index)
130
- if (child === title || child.visibility == GONE) continue
127
+ if (child.visibility == GONE) continue
131
128
  measureChildWithMargins(
132
129
  child,
133
130
  widthMeasureSpec,
@@ -54,14 +54,7 @@ separately from source implementation in STYLE_SPEC §9.
54
54
 
55
55
  - **Use:** an identifiable asset, account, wallet, network or setting.
56
56
  - **Required data:** `leading`, `title`; optional `subtitle`, `tertiary`, `badges`,
57
- `trailing`, `leadingAction`, `titleIcon`, Web-only `secondaryTitleIcon`, `titleSegments`,
58
- `titleLeadingIcon`, match ranges and structured subtitle segments.
59
- - **Title runs:** optional `titleSegments` draw 1...4 runs with their own tone and
60
- weight in place of `title`, on one line. One run may shrink; the others keep
61
- their width under an optional `maxWidthRatio` cap. `title` stays the spoken
62
- text. See SPEC.md.
63
- - **Leading title icon:** optional `titleLeadingIcon` draws a 16-point glyph 6
64
- points before the title; it is decorative unless labelled.
57
+ `trailing`, `leadingAction`, `titleIcon`, Web-only `secondaryTitleIcon`, match ranges and structured subtitle segments.
65
58
  - **Static title icon:** optional 20-point icon immediately after the title with
66
59
  a 4-point gap. It has no click action; an optional Web `hoverActionKey`
67
60
  emits an icon-anchored tooltip request on hover or touch. A trailing `button` is the fixed small
package/docs/SPEC.md CHANGED
@@ -101,90 +101,6 @@ and Web acceptance must cover narrow addresses, dynamic type/fonts, long press
101
101
  versus tap, keyboard interaction, and the horizontal warning border. Source or
102
102
  build success alone is not runtime evidence.
103
103
 
104
- ### Identity title segments and leading title icon
105
-
106
- Status: implemented in source for iOS, Android and Web; device and Web runtime
107
- acceptance is pending. These fields travel in snapshot/patch JSON and do not
108
- change the Nitro ABI. They serve recipient rows whose title is a wallet and an
109
- account name in two styles, and address book rows marked by an icon before the
110
- name.
111
-
112
- `identity.titleSegments` is an optional list of 1...4 runs drawn on the title
113
- line in place of `title`:
114
-
115
- ```ts
116
- titleSegments?: readonly {
117
- text: string;
118
- tone?: 'primary' | 'secondary';
119
- fontWeight?: 'regular' | 'medium';
120
- shrink?: boolean;
121
- maxWidthRatio?: number; // (0, 1]
122
- }[];
123
- ```
124
-
125
- - `title` stays required. It is the spoken label and the fallback text, so it
126
- carries the same words as the runs; every renderer exposes `title`, not the
127
- runs, to accessibility.
128
- - Each run uses the title's font family, size and line height, including
129
- `style.title` and the configured fonts. `tone` picks the primary or secondary
130
- text color and defaults to the title color; `fontWeight` picks the weight and
131
- defaults to the title weight.
132
- - The runs share one line. `titleLines` and `style.title.lines` do not apply.
133
- - Layout. The runs take the whole width the title line leaves after the
134
- leading title icon and the title accessories (title icon, title image,
135
- suffixes, badges), so those accessories sit at the line's end rather than
136
- right after the text. W is that width:
137
- 1. A run's base width is its natural width, capped at `maxWidthRatio x W`
138
- when given. A run longer than its cap truncates at the cap with a tail
139
- ellipsis.
140
- 2. At most one run sets `shrink: true`. Every other run keeps its base width.
141
- 3. The shrinking run gets `min(base, W - sum of the other runs)` and
142
- truncates with a tail ellipsis; it may shrink to zero width.
143
- 4. When the runs that do not shrink exceed W on their own, the line is cut at
144
- its end. Native renderers end the last visible run with an ellipsis; Web
145
- clips it. This divergence is accepted: callers avoid the case with
146
- `maxWidthRatio`.
147
- - Development validation rejects an empty or oversized list, blank or
148
- oversized text (the 4096-code-unit text bound), unknown keys, tones or
149
- weights, a ratio outside `(0, 1]`, more than one shrinking run, `titleMatch`
150
- on the same row (its ranges index `title`), and
151
- `presentation: 'walletSidebar'`. Production treats the runs as decoration:
152
- an invalid list is dropped and the row shows `title`. Renderers draw `title`
153
- instead of runs on a row that also has `titleMatch`, and ignore runs and the
154
- leading icon with `presentation: 'walletSidebar'`.
155
-
156
- `identity.titleLeadingIcon` is an optional `{ name, tintColor?,
157
- accessibilityLabel? }`: a 16-point glyph before the title text or its runs,
158
- with a 6-point gap, vertically centered on the title line. The name comes from
159
- the same closed icon set as `titleIcon`; an unmapped name leaves an empty
160
- 16-point slot. `tintColor` is a hex `#RRGGBB` or `#RRGGBBAA` color and defaults
161
- to the theme's `iconSubdued`. Without `accessibilityLabel` the glyph is hidden
162
- from accessibility. It has no action of its own. Development validation
163
- rejects unknown keys, a blank name, a malformed color and
164
- `presentation: 'walletSidebar'`; production drops an invalid icon.
165
-
166
- Both fields are identity patch fields. Omission in a full snapshot removes
167
- them; omission in a patch keeps the current value. Rebind and recycle remove the
168
- previous runs and glyph, so a row without them never shows another row's.
169
-
170
- | Platform | Title runs | Leading icon |
171
- | --- | --- | --- |
172
- | iOS | `NativeListTitleSegmentsLabel` draws the runs at the widths that `NativeListTitleSegmentLayout.swift` allocates for its bounds; it fills the title row in place of the row's filler. | `UIImageView`, first in the title row stack. |
173
- | Android | `NativeListTitleSegmentsView`, a `TextView` whose text is rebuilt for each measured width from `NativeListTitleSegmentLayout.kt` and which measures to the width it is offered. `PackedTitleLineLayout.titleChild` names the flexible title view, so a view may precede it. | `OneKeyIconView`, first in the title line. |
174
- | Web | A flex line that fills the title line: the shrinking run is `flex: 0 1 auto; min-width: 0`, the others `flex: 0 0 auto` with a `max-width` percentage for a cap. | An icon element before the title text. |
175
-
176
- Conformance: `src/validation.ts` (`assertTitleSegments`,
177
- `assertTitleLeadingIcon`, production sanitizing) with `validation.test.ts`;
178
- `src/web/templates/IdentityRowRenderer.ts` with `IdentityTitleRunsWeb.test.ts`
179
- (run styles, flex behavior, accessibility, rebind);
180
- `ios/NativeListTitleSegmentLayout.swift` with the SwiftPM
181
- `NativeListTitleSegmentLayoutTests`; `NativeListTitleSegmentLayout.kt` with the
182
- JVM `NativeListTitleSegmentLayoutTest`. iOS, Android and Web runtime acceptance is pending: recipient rows
183
- with long wallet and account names in both truncation directions, narrow rows,
184
- dark mode, dynamic type and configured fonts, and recycling between rows with
185
- and without runs or a leading icon. Source and build success alone are not
186
- runtime evidence.
187
-
188
104
  ### DeFi position row controls
189
105
 
190
106
  Status: implemented in local source, unreleased. Focused validation and Web
@@ -81,8 +81,7 @@ final class NativeListIdentityCell: NativeListRendererCell {
81
81
  let title = NativeListResolvedText(
82
82
  item.data.string("title"), style: style.dictionary("title"), size: 16,
83
83
  weight: .medium, color: .black, lineHeight: 24,
84
- lines: item.data.dictionaries("titleSegments").isEmpty
85
- ? item.data.int("titleLines", default: 1) : 1, fonts: fonts)
84
+ lines: item.data.int("titleLines", default: 1), fonts: fonts)
86
85
  let subtitle = NativeListResolvedText(
87
86
  item.data.string("subtitle"), style: style.dictionary("subtitle"), size: 14,
88
87
  color: .black, lineHeight: 20,
@@ -111,12 +110,6 @@ final class NativeListIdentityCell: NativeListRendererCell {
111
110
  private let titleRowStack = UIStackView()
112
111
  private let titleLabel = NativeListTextLabel()
113
112
  private let titleIconView = UIImageView()
114
- private let titleLeadingIconView = UIImageView()
115
- private let titleSegmentsLabel = NativeListTitleSegmentsLabel()
116
- // Runs, when shown, take the title's place in the title row.
117
- private var visibleTitleView: UIView {
118
- titleSegmentsLabel.isHidden ? titleLabel : titleSegmentsLabel
119
- }
120
113
  private var titleImageSlot: NativeListImageSlot?
121
114
  private var titleImageConstraints: [NSLayoutConstraint] = []
122
115
  // Absorbs the title row's slack so the title keeps its intrinsic width and the
@@ -177,9 +170,7 @@ final class NativeListIdentityCell: NativeListRendererCell {
177
170
  mainStack.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
178
171
  titleRowStack.axis = .horizontal
179
172
  titleRowStack.alignment = .center
180
- titleRowStack.addArrangedSubview(titleLeadingIconView)
181
173
  titleRowStack.addArrangedSubview(titleLabel)
182
- titleRowStack.addArrangedSubview(titleSegmentsLabel)
183
174
  titleRowStack.addArrangedSubview(titleIconView)
184
175
  titleRowStack.addArrangedSubview(badgeLabel)
185
176
  titleRowStack.addArrangedSubview(titleRowFiller)
@@ -194,18 +185,6 @@ final class NativeListIdentityCell: NativeListRendererCell {
194
185
  ])
195
186
  titleIconView.contentMode = .scaleAspectFit
196
187
  titleIconView.setContentHuggingPriority(.required, for: .horizontal)
197
- titleLeadingIconView.translatesAutoresizingMaskIntoConstraints = false
198
- let leadingIconWidth = titleLeadingIconView.widthAnchor.constraint(equalToConstant: 16)
199
- leadingIconWidth.priority = UILayoutPriority(999)
200
- NSLayoutConstraint.activate([
201
- leadingIconWidth,
202
- titleLeadingIconView.heightAnchor.constraint(equalToConstant: 16),
203
- ])
204
- titleLeadingIconView.contentMode = .scaleAspectFit
205
- titleLeadingIconView.setContentHuggingPriority(.required, for: .horizontal)
206
- titleLeadingIconView.setContentCompressionResistancePriority(.required, for: .horizontal)
207
- titleLeadingIconView.isHidden = true
208
- titleSegmentsLabel.isHidden = true
209
188
  [titleRowStack, subtitleLabel, tertiaryLabel].forEach(mainStack.addArrangedSubview)
210
189
  visual.translatesAutoresizingMaskIntoConstraints = false
211
190
  leadingWidth = visual.widthAnchor.constraint(equalToConstant: 40)
@@ -267,15 +246,6 @@ final class NativeListIdentityCell: NativeListRendererCell {
267
246
  titleIconView.isHidden = true
268
247
  titleIconView.accessibilityLabel = nil
269
248
  titleIconView.isAccessibilityElement = false
270
- titleLeadingIconView.image = nil
271
- titleLeadingIconView.isHidden = true
272
- titleLeadingIconView.accessibilityLabel = nil
273
- titleLeadingIconView.isAccessibilityElement = false
274
- titleSegmentsLabel.isHidden = true
275
- titleSegmentsLabel.reset()
276
- titleRowFiller.isHidden = false
277
- titleSegmentsLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
278
- titleSegmentsLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
279
249
  titleImageSlot?.view.isHidden = true
280
250
  titleImageSlot?.view.accessibilityLabel = nil
281
251
  titleImageSlot?.view.isAccessibilityElement = false
@@ -337,7 +307,7 @@ final class NativeListIdentityCell: NativeListRendererCell {
337
307
  let label = titleIcon.string("accessibilityLabel")
338
308
  titleIconView.accessibilityLabel = label.isEmpty ? nil : label
339
309
  titleIconView.isAccessibilityElement = !label.isEmpty
340
- titleRowStack.setCustomSpacing(4, after: visibleTitleView)
310
+ titleRowStack.setCustomSpacing(4, after: titleLabel)
341
311
  // A long title truncates ahead of the 20pt glyph (Android caps the title
342
312
  // at row width minus accessories, Web shrinks the title first); with the
343
313
  // title at `.required` the icon's 999 width constraint collapsed to 0.
@@ -369,7 +339,7 @@ final class NativeListIdentityCell: NativeListRendererCell {
369
339
  ]
370
340
  titleImageConstraints.forEach { $0.priority = UILayoutPriority(999) }
371
341
  NSLayoutConstraint.activate(titleImageConstraints)
372
- titleRowStack.setCustomSpacing(4, after: titleIconView.isHidden ? visibleTitleView : titleIconView)
342
+ titleRowStack.setCustomSpacing(4, after: titleIconView.isHidden ? titleLabel : titleIconView)
373
343
  titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
374
344
  slot.bind(image, key: "\(item.key):titleImage", placeholder: "#00000000")
375
345
  } else {
@@ -412,7 +382,7 @@ final class NativeListIdentityCell: NativeListRendererCell {
412
382
  titleRowStack.setCustomSpacing(
413
383
  CGFloat(style.double("titleBadgeGap")),
414
384
  after: titleImageSlot?.view.isHidden == false ? titleImageSlot!.view
415
- : titleIconView.isHidden ? visibleTitleView : titleIconView)
385
+ : titleIconView.isHidden ? titleLabel : titleIconView)
416
386
  }
417
387
  }
418
388
  if style["trailingGap"] != nil { trailingStack.spacing = CGFloat(style.double("trailingGap")) }
@@ -426,7 +396,6 @@ final class NativeListIdentityCell: NativeListRendererCell {
426
396
  labels.forEach { NativeListTextStyles.applyStyledText($0, text, fonts: fonts) }
427
397
  }
428
398
  }
429
- if !titleSegmentsLabel.isHidden { bindTitleRuns(item, theme: theme, style: style) }
430
399
  contentInsets = UIEdgeInsets(
431
400
  top: topInset, left: leftInset, bottom: -bottomInset, right: -rightInset)
432
401
  if root.axis == .horizontal {
@@ -487,8 +456,6 @@ final class NativeListIdentityCell: NativeListRendererCell {
487
456
  }
488
457
  override func recycleContent() {
489
458
  visual.recycle()
490
- titleSegmentsLabel.reset()
491
- titleLeadingIconView.image = nil
492
459
  titleImageSlot?.recycle()
493
460
  titleImageSlot?.view.isHidden = true
494
461
  titleImageSlot?.view.accessibilityLabel = nil
@@ -537,30 +504,6 @@ final class NativeListIdentityCell: NativeListRendererCell {
537
504
  visual.bind(
538
505
  descriptor, style: image, key: key, theme: currentTheme, isUnread: false, fonts: fonts)
539
506
  }
540
- // Runs take the title's font, size and color unless they set their own tone
541
- // or weight; the title label holds the final style by now.
542
- private func bindTitleRuns(_ item: NativeListItem, theme: [String: Any]?, style: [String: Any]) {
543
- let baseFont = titleLabel.font ?? nativeListFont(ofSize: 16, weight: .medium, fonts: fonts)
544
- let baseColor = titleLabel.textColor ?? nativeListColor(theme, "primaryText", "#202020")
545
- let runs = item.data.dictionaries("titleSegments").map { segment in
546
- let weight = segment.string("fontWeight")
547
- let tone = segment.string("tone")
548
- return NativeListTitleSegmentsLabel.Run(
549
- text: segment.string("text"),
550
- font: weight.isEmpty
551
- ? baseFont
552
- : nativeListFont(
553
- ofSize: baseFont.pointSize, weight: weight == "medium" ? .medium : .regular, fonts: fonts),
554
- color: tone == "secondary"
555
- ? nativeListColor(theme, "secondaryText", "#646464")
556
- : tone == "primary" ? nativeListColor(theme, "primaryText", "#202020") : baseColor,
557
- shrink: segment.bool("shrink"),
558
- maxWidthRatio: (segment["maxWidthRatio"] as? NSNumber).map { CGFloat(truncating: $0) })
559
- }
560
- titleSegmentsLabel.lineHeight = CGFloat(
561
- style.dictionary("title")?.double("lineHeight", default: 24) ?? 24)
562
- titleSegmentsLabel.configure(runs, spokenText: item.data.string("title"))
563
- }
564
507
  private func bindIdentity(
565
508
  _ item: NativeListItem,
566
509
  theme: [String: Any]?,
@@ -698,33 +641,10 @@ final class NativeListIdentityCell: NativeListRendererCell {
698
641
  }
699
642
  root.addArrangedSubview(mainStack)
700
643
  show(titleLabel, item.data.string("title"), lines: item.data.int("titleLines", default: 1))
701
- // Runs replace the drawn title except where `title` itself is highlighted;
702
- // their content is bound once the title style is final.
703
- if !item.data.dictionaries("titleSegments").isEmpty,
704
- item.data.dictionaries("titleMatch").isEmpty
705
- {
706
- titleLabel.isHidden = true
707
- titleSegmentsLabel.isHidden = false
708
- // The runs fill what the title row leaves, so a ratio is a share of it.
709
- titleRowFiller.isHidden = true
710
- titleSegmentsLabel.setContentHuggingPriority(UILayoutPriority(1), for: .horizontal)
711
- }
712
- if let icon = item.data.dictionary("titleLeadingIcon") {
713
- titleLeadingIconView.image = nativeListIcon(named: icon.string("name"))
714
- titleLeadingIconView.tintColor = UIColor(
715
- nativeListHex: icon.string("tintColor",
716
- default: theme?["iconSubdued"] as? String ?? "#8D8D8D"),
717
- fallback: .gray)
718
- titleLeadingIconView.isHidden = false
719
- let label = icon.string("accessibilityLabel")
720
- titleLeadingIconView.accessibilityLabel = label.isEmpty ? nil : label
721
- titleLeadingIconView.isAccessibilityElement = !label.isEmpty
722
- titleRowStack.setCustomSpacing(6, after: titleLeadingIconView)
723
- }
724
644
  let titleSuffixes = item.data.dictionaries("titleSuffixes")
725
645
  if !titleSuffixes.isEmpty {
726
- visibleTitleView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
727
- let minimumTitleWidth = visibleTitleView.widthAnchor.constraint(greaterThanOrEqualToConstant: 80)
646
+ titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
647
+ let minimumTitleWidth = titleLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 80)
728
648
  minimumTitleWidth.priority = UILayoutPriority(751)
729
649
  minimumTitleWidth.isActive = true
730
650
  selectorConstraints.append(minimumTitleWidth)
@@ -39,8 +39,6 @@ export const ROW_BOX_STYLE_KEYS_BY_TYPE = {
39
39
 
40
40
  // OneKey patch: preserve compact very-small-balance digit runs.
41
41
 
42
- /** One run of an identity title line; see docs/SPEC.md. */
43
-
44
42
  /** Native Market quote row. All values are already localized/formatted strings. */
45
43
 
46
44
  /** Compact KPI card for dashboards and grids. */
@@ -653,66 +653,6 @@ function assertButtonGroup(row, path) {
653
653
  }
654
654
  });
655
655
  }
656
- const MAX_TITLE_SEGMENTS = 4;
657
- const HEX_COLOR_PATTERN = /^#[0-9a-f]{6}([0-9a-f]{2})?$/i;
658
- function assertTitleSegments(value, path) {
659
- if (!Array.isArray(value) || value.length === 0 || value.length > MAX_TITLE_SEGMENTS) fail(path, `must hold 1...${MAX_TITLE_SEGMENTS} segments`);
660
- let shrinking = 0;
661
- value.forEach((item, index) => {
662
- const segmentPath = `${path}[${index}]`;
663
- const segment = protocolObject(item, segmentPath);
664
- assertUnknownKeys(segment, ['text', 'tone', 'fontWeight', 'shrink', 'maxWidthRatio'], segmentPath, 'title segment key');
665
- const {
666
- text,
667
- tone,
668
- fontWeight,
669
- shrink,
670
- maxWidthRatio
671
- } = segment;
672
- if (typeof text !== 'string' || !text.trim()) fail(`${segmentPath}.text`, 'must be non-blank');
673
- assertText(text, `${segmentPath}.text`);
674
- if (tone !== undefined && tone !== 'primary' && tone !== 'secondary') fail(`${segmentPath}.tone`, 'must be primary or secondary');
675
- if (fontWeight !== undefined && fontWeight !== 'regular' && fontWeight !== 'medium') fail(`${segmentPath}.fontWeight`, 'must be regular or medium');
676
- if (shrink !== undefined && typeof shrink !== 'boolean') fail(`${segmentPath}.shrink`, 'must be a boolean');
677
- if (shrink) shrinking += 1;
678
- if (maxWidthRatio !== undefined && (typeof maxWidthRatio !== 'number' || !Number.isFinite(maxWidthRatio) || maxWidthRatio <= 0 || maxWidthRatio > 1)) fail(`${segmentPath}.maxWidthRatio`, 'must be finite within (0, 1]');
679
- });
680
- if (shrinking > 1) fail(path, 'allows at most one shrinking segment');
681
- }
682
- function assertTitleLeadingIcon(value, path) {
683
- const icon = protocolObject(value, path);
684
- assertUnknownKeys(icon, ['name', 'tintColor', 'accessibilityLabel'], path, 'title leading icon key');
685
- const {
686
- name,
687
- tintColor,
688
- accessibilityLabel
689
- } = icon;
690
- if (typeof name !== 'string' || !name.trim()) fail(`${path}.name`, 'must be non-blank');
691
- if (tintColor !== undefined && (typeof tintColor !== 'string' || !HEX_COLOR_PATTERN.test(tintColor))) fail(`${path}.tintColor`, 'must be #RRGGBB or #RRGGBBAA');
692
- if (accessibilityLabel !== undefined && typeof accessibilityLabel !== 'string') fail(`${path}.accessibilityLabel`, 'must be a string');
693
- }
694
- function assertIdentityTitleRuns(row, path) {
695
- if (row.titleSegments !== undefined) {
696
- assertTitleSegments(row.titleSegments, `${path}.titleSegments`);
697
- // titleMatch ranges index `title`, which the runs replace on screen.
698
- if (row.titleMatch?.length) fail(`${path}.titleMatch`, 'cannot be combined with titleSegments');
699
- if (row.presentation === 'walletSidebar') fail(`${path}.titleSegments`, 'is not supported with walletSidebar');
700
- }
701
- if (row.titleLeadingIcon !== undefined) {
702
- assertTitleLeadingIcon(row.titleLeadingIcon, `${path}.titleLeadingIcon`);
703
- if (row.presentation === 'walletSidebar') fail(`${path}.titleLeadingIcon`, 'is not supported with walletSidebar');
704
- }
705
- }
706
-
707
- /** Production keeps decorative title runs only when they pass the rules. */
708
- function passesTitleRunRules(check) {
709
- try {
710
- check();
711
- return true;
712
- } catch {
713
- return false;
714
- }
715
- }
716
656
  function assertIdentityExtras(row, path) {
717
657
  for (const field of ['titleIcon', 'secondaryTitleIcon']) {
718
658
  const titleIcon = row[field];
@@ -944,7 +884,6 @@ function assertRow(row, index, path = `rows[${index}]`) {
944
884
  if (suffix.maxWidth !== undefined && (!Number.isFinite(suffix.maxWidth) || suffix.maxWidth < 0 || suffix.maxWidth > 4096)) fail(`${path}.titleSuffixes[${index}].maxWidth`, 'must be finite within 0...4096');
945
885
  });
946
886
  assertIdentityExtras(row, path);
947
- assertIdentityTitleRuns(row, path);
948
887
  assertText(row.subtitle, `${path}.subtitle`);
949
888
  // OneKey patch: selector text segments truncate independently.
950
889
  row.subtitleSegments?.forEach((segment, segmentIndex) => {
@@ -1238,7 +1177,7 @@ function assertSnapshotContract(snapshot) {
1238
1177
  }
1239
1178
  const COMMON_ROW_FIELDS = ['height', 'heightRounding', 'testID', 'opacity', 'backgroundColor', 'backgroundFullWidth', 'pressDisabled', 'key', 'revision', 'sectionKey', 'groupId', 'groupPosition', 'size', 'density', 'disabled', 'selected', 'accessibilityLabel', 'separator', 'type', 'style'];
1240
1179
  const ROW_FIELDS = {
1241
- identity: [...COMMON_ROW_FIELDS, 'contentHeight', 'longPressActionKey', 'titleMatch', 'titleActionKey', 'titleActionOnHover', 'subtitleSegments', 'presentation', 'leading', 'leadingAction', 'title', 'titleSuffixes', 'titleIcon', 'secondaryTitleIcon', 'titleImage', 'titleSegments', 'titleLeadingIcon', 'subtitle', 'tertiary', 'tertiaryTone', 'titleLines', 'subtitleLines', 'badges', 'trailing', 'draggable'],
1180
+ identity: [...COMMON_ROW_FIELDS, 'contentHeight', 'longPressActionKey', 'titleMatch', 'titleActionKey', 'titleActionOnHover', 'subtitleSegments', 'presentation', 'leading', 'leadingAction', 'title', 'titleSuffixes', 'titleIcon', 'secondaryTitleIcon', 'titleImage', 'subtitle', 'tertiary', 'tertiaryTone', 'titleLines', 'subtitleLines', 'badges', 'trailing', 'draggable'],
1242
1181
  walletGroup: [...COMMON_ROW_FIELDS, 'parent', 'children', 'draggable'],
1243
1182
  rail: [...COMMON_ROW_FIELDS, 'visual', 'title', 'status', 'badge', 'draggable'],
1244
1183
  activity: [...COMMON_ROW_FIELDS, 'leading', 'hideLeading', 'secondaryLeading', 'title', 'description', 'status', 'primaryAmount', 'secondaryAmount', 'amounts', 'badges', 'presentation', 'fee', 'descriptionActionKey', 'footerActions'],
@@ -1254,7 +1193,7 @@ const ROW_FIELDS = {
1254
1193
  };
1255
1194
  const COMMON_PATCH_FIELDS = ['height', 'heightRounding', 'opacity', 'pressDisabled', 'revision', 'disabled', 'selected', 'accessibilityLabel', 'separator', 'style'];
1256
1195
  const PATCH_FIELDS = {
1257
- identity: [...COMMON_PATCH_FIELDS, 'contentHeight', 'longPressActionKey', 'titleMatch', 'titleActionKey', 'titleActionOnHover', 'subtitleSegments', 'leading', 'leadingAction', 'title', 'titleSuffixes', 'titleIcon', 'secondaryTitleIcon', 'titleImage', 'titleSegments', 'titleLeadingIcon', 'subtitle', 'tertiary', 'tertiaryTone', 'subtitleLines', 'badges', 'trailing'],
1196
+ identity: [...COMMON_PATCH_FIELDS, 'contentHeight', 'longPressActionKey', 'titleMatch', 'titleActionKey', 'titleActionOnHover', 'subtitleSegments', 'leading', 'leadingAction', 'title', 'titleSuffixes', 'titleIcon', 'secondaryTitleIcon', 'titleImage', 'subtitle', 'tertiary', 'tertiaryTone', 'subtitleLines', 'badges', 'trailing'],
1258
1197
  rail: [...COMMON_PATCH_FIELDS, 'title', 'visual', 'status', 'badge'],
1259
1198
  activity: [...COMMON_PATCH_FIELDS, 'presentation', 'leading', 'title', 'badges', 'status', 'hideLeading', 'secondaryLeading', 'description', 'primaryAmount', 'secondaryAmount', 'amounts', 'fee', 'descriptionActionKey', 'footerActions'],
1260
1199
  message: [...COMMON_PATCH_FIELDS, 'presentation', 'title', 'unread', 'body', 'bodyLines', 'time', 'thumbnail'],
@@ -1274,7 +1213,7 @@ function protocolObject(value, path) {
1274
1213
  function protocolKey(value, path) {
1275
1214
  if (typeof value !== 'string' || value.trim().length === 0) fail(path, 'must be a non-blank string');
1276
1215
  }
1277
- const PROTOCOL_ARRAY_FIELDS = new Set(['columns', 'titleSuffixes', 'titleSegments', 'cells', 'points', 'images', 'overlays', 'badges', 'trailing', 'footerActions', 'buttons', 'amounts', 'metrics', 'children', 'subtitleSegments', 'titleMatch', 'selectedKeys', 'segments', 'hoverSecondary', 'hoverImages', 'secondaryBadges', 'fallbackUris', 'probeOrder', 'priceSegments', 'textSegments', 'secondaryTextSegments', 'primaryTextSegments']);
1216
+ const PROTOCOL_ARRAY_FIELDS = new Set(['columns', 'titleSuffixes', 'cells', 'points', 'images', 'overlays', 'badges', 'trailing', 'footerActions', 'buttons', 'amounts', 'metrics', 'children', 'subtitleSegments', 'titleMatch', 'selectedKeys', 'segments', 'hoverSecondary', 'hoverImages', 'secondaryBadges', 'fallbackUris', 'probeOrder', 'priceSegments', 'textSegments', 'secondaryTextSegments', 'primaryTextSegments']);
1278
1217
  const PROTOCOL_NUMBER_FIELDS = new Set(['leadingContentMaxWidthRatio', 'height', 'width', 'fontSize', 'lineHeight', 'opacity', 'offsetY', 'offsetX', 'cornerRadius', 'borderWidth', 'horizontalPadding', 'verticalPadding', 'leadingGap', 'lineGap', 'titleBadgeGap', 'trailingGap', 'start', 'end', 'padding', 'offset', 'overscan', 'minWidth', 'flex', 'paddingStart', 'paddingEnd', 'changeWidth', 'changeHeight', 'changeCornerRadius', 'contentTrailingGap', 'subtitleTrailingPadding', 'partGap', 'contentPadding', 'contentPaddingHorizontal', 'contentPaddingTop', 'contentPaddingBottom', 'itemSpacing', 'gridColumns']);
1279
1218
 
1280
1219
  /** Validate only shapes that renderers dereference, not display budgets. */
@@ -1345,10 +1284,7 @@ function assertRowProtocol(row, path) {
1345
1284
  }
1346
1285
  if (row.height !== undefined && row.height < 0) fail(`${path}.height`, 'must be non-negative');
1347
1286
  if (row.type === 'identity') {
1348
- if (isDevelopment()) {
1349
- assertIdentityExtras(row, path);
1350
- assertIdentityTitleRuns(row, path);
1351
- }
1287
+ if (isDevelopment()) assertIdentityExtras(row, path);
1352
1288
  if (row.titleSuffixes !== undefined && !Array.isArray(row.titleSuffixes)) fail(`${path}.titleSuffixes`, 'must be an array');
1353
1289
  row.titleSuffixes?.forEach((suffix, index) => {
1354
1290
  protocolObject(suffix, `${path}.titleSuffixes[${index}]`);
@@ -1597,8 +1533,6 @@ const NESTED_DECORATION_FIELDS = {
1597
1533
  };
1598
1534
  function sanitizeDecoration(value, field) {
1599
1535
  if (value === undefined || value === null) return undefined;
1600
- if (field === 'titleSegments') return passesTitleRunRules(() => assertTitleSegments(value, field)) ? value : undefined;
1601
- if (field === 'titleLeadingIcon') return passesTitleRunRules(() => assertTitleLeadingIcon(value, field)) ? value : undefined;
1602
1536
  if (!Array.isArray(value) && ['image', 'networkImage', 'thumbnail', 'source', 'images', 'hoverImages'].includes(field)) return sanitizeImage(value);
1603
1537
  if (['titleImage', 'titleIcon', 'secondaryTitleIcon', 'fallbackIcon', 'cornerIcon', 'media'].includes(field) && !isRecord(value)) return undefined;
1604
1538
  if (field === 'style' && typeof value !== 'string' && !isRecord(value)) return undefined;
@@ -466,7 +466,7 @@ export const WEB_LIST_CSS = `
466
466
  .ok-native-list-trailing-button{appearance:none;box-sizing:border-box;flex:0 0 auto;padding:4px 10px;border:1px solid transparent;border-radius:9999px;background:var(--nl-button-bg,var(--nl-strong));color:var(--nl-button-text,var(--nl-primary));font:500 14px/20px var(--nl-font-medium);white-space:nowrap;cursor:pointer}.ok-native-list-trailing-button:hover:not(:disabled){background:var(--nl-button-hover,var(--nl-strong))}.ok-native-list-trailing-button:active:not(:disabled){background:var(--nl-button-pressed,var(--nl-pressed))}.ok-native-list-trailing-button:focus-visible{outline:2px solid var(--nl-button-focus,var(--nl-accent))}.ok-native-list-trailing-button:disabled{opacity:.4;cursor:default}
467
467
  .ok-native-list-icon-button.ok-native-list-tertiary-icon{box-sizing:border-box;flex:0 0 38px;width:38px;height:38px;margin:-7px;padding:6px;border:1px solid transparent;border-radius:9999px;color:var(--nl-icon-subdued);background:transparent}.ok-native-list-icon-button.ok-native-list-tertiary-icon:hover:not(:disabled){background:var(--nl-hover)}.ok-native-list-icon-button.ok-native-list-tertiary-icon:active:not(:disabled){background:var(--nl-pressed)}.ok-native-list-icon-button.ok-native-list-tertiary-icon:disabled{opacity:.4;cursor:default}
468
468
  .ok-native-list-icon-button.ok-native-list-tertiary-icon:focus-visible:not(:disabled){outline:2px solid var(--nl-accent)}
469
- .ok-native-list-identity-title-icon{flex:0 0 20px;width:20px;height:20px;display:inline-flex;align-items:center;justify-content:center;color:var(--nl-icon);font-size:20px}.ok-native-list-identity-title-leading-icon{flex:0 0 16px;width:16px;height:16px;display:inline-flex;align-items:center;justify-content:center;color:var(--nl-icon-subdued);font-size:16px}.ok-native-list-identity-title-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
469
+ .ok-native-list-identity-title-icon{flex:0 0 20px;width:20px;height:20px;display:inline-flex;align-items:center;justify-content:center;color:var(--nl-icon);font-size:20px}.ok-native-list-identity-title-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
470
470
  .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;font-family:var(--nl-font-regular)}.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;font-family:var(--nl-font-regular)}.ok-native-list-account-row .ok-native-list-secondary{font-size:14px;line-height:20px;font-weight:400;font-family:var(--nl-font-regular)}
471
471
  .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;font-family:var(--nl-font-semibold)}.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)}
472
472
  .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}