@onekeyfe/react-native-native-list 6.7.0-alpha.426 → 6.7.0-alpha.428
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.
- package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +82 -3
- package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +6 -3
- package/android/src/main/java/com/onekey/nativelist/NativeListTitleSegmentLayout.kt +38 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListTitleSegmentsView.kt +105 -0
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListTitleSegmentLayoutTest.kt +65 -0
- package/docs/ROW_TEMPLATES.md +8 -1
- package/docs/SPEC.md +84 -0
- package/ios/NativeListIdentityRenderer.swift +86 -6
- package/ios/NativeListTitleSegmentLayout.swift +32 -0
- package/ios/NativeListTitleSegmentsLabel.swift +80 -0
- package/lib/module/models.js +2 -0
- package/lib/module/validation.js +70 -4
- package/lib/module/web/NativeListWebEngine.js +1 -1
- package/lib/module/web/templates/IdentityRowRenderer.js +91 -0
- package/lib/typescript/src/models.d.ts +21 -1
- package/package.json +3 -3
- package/src/models.ts +23 -0
- package/src/validation.ts +120 -1
- package/src/web/NativeListWebEngine.ts +1 -1
- package/src/web/templates/IdentityRowRenderer.ts +138 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Widths for identity title runs (docs/SPEC.md, "Identity title segments
|
|
4
|
+
/// and leading title icon"). A run's base width is its natural width under
|
|
5
|
+
/// its cap; the one shrinking run takes what the others leave; runs that do
|
|
6
|
+
/// not shrink and still overflow are cut from the end of the line.
|
|
7
|
+
func nativeListTitleSegmentWidths(
|
|
8
|
+
natural: [CGFloat],
|
|
9
|
+
shrink: [Bool],
|
|
10
|
+
maxWidthRatio: [CGFloat?],
|
|
11
|
+
available: CGFloat
|
|
12
|
+
) -> [CGFloat] {
|
|
13
|
+
let line = max(0, available)
|
|
14
|
+
var widths = natural.indices.map { index -> CGFloat in
|
|
15
|
+
let width = max(0, natural[index])
|
|
16
|
+
guard index < maxWidthRatio.count, let ratio = maxWidthRatio[index] else { return width }
|
|
17
|
+
return min(width, ratio * line)
|
|
18
|
+
}
|
|
19
|
+
if let shrinking = shrink.firstIndex(of: true), shrinking < widths.count {
|
|
20
|
+
let others = widths.indices.reduce(CGFloat(0)) { $1 == shrinking ? $0 : $0 + widths[$1] }
|
|
21
|
+
widths[shrinking] = max(0, min(widths[shrinking], line - others))
|
|
22
|
+
}
|
|
23
|
+
var overflow = widths.reduce(0, +) - line
|
|
24
|
+
var index = widths.count - 1
|
|
25
|
+
while overflow > 0, index >= 0 {
|
|
26
|
+
let cut = min(widths[index], overflow)
|
|
27
|
+
widths[index] -= cut
|
|
28
|
+
overflow -= cut
|
|
29
|
+
index -= 1
|
|
30
|
+
}
|
|
31
|
+
return widths
|
|
32
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// Draws identity title runs on one line, each at the width that
|
|
4
|
+
/// `nativeListTitleSegmentWidths` allocates for the current bounds. Its
|
|
5
|
+
/// intrinsic width is the runs' natural width, so the title row stack can
|
|
6
|
+
/// give it less, and accessibility reads the row's `title` instead of runs.
|
|
7
|
+
final class NativeListTitleSegmentsLabel: UIView {
|
|
8
|
+
struct Run {
|
|
9
|
+
let text: String
|
|
10
|
+
let font: UIFont
|
|
11
|
+
let color: UIColor
|
|
12
|
+
let shrink: Bool
|
|
13
|
+
let maxWidthRatio: CGFloat?
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private var runs: [Run] = []
|
|
17
|
+
private var naturalWidths: [CGFloat] = []
|
|
18
|
+
var lineHeight: CGFloat = 24 {
|
|
19
|
+
didSet {
|
|
20
|
+
invalidateIntrinsicContentSize()
|
|
21
|
+
setNeedsDisplay()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
override init(frame: CGRect) {
|
|
26
|
+
super.init(frame: frame)
|
|
27
|
+
isOpaque = false
|
|
28
|
+
backgroundColor = .clear
|
|
29
|
+
contentMode = .redraw
|
|
30
|
+
isAccessibilityElement = true
|
|
31
|
+
accessibilityTraits = .staticText
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
35
|
+
|
|
36
|
+
func configure(_ runs: [Run], spokenText: String) {
|
|
37
|
+
self.runs = runs
|
|
38
|
+
naturalWidths = runs.map {
|
|
39
|
+
ceil(($0.text as NSString).size(withAttributes: [.font: $0.font]).width)
|
|
40
|
+
}
|
|
41
|
+
accessibilityLabel = spokenText.isEmpty ? nil : spokenText
|
|
42
|
+
invalidateIntrinsicContentSize()
|
|
43
|
+
setNeedsDisplay()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func reset() {
|
|
47
|
+
configure([], spokenText: "")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override var intrinsicContentSize: CGSize {
|
|
51
|
+
CGSize(width: naturalWidths.reduce(0, +), height: lineHeight)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override func layoutSubviews() {
|
|
55
|
+
super.layoutSubviews()
|
|
56
|
+
setNeedsDisplay()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
override func draw(_ rect: CGRect) {
|
|
60
|
+
guard !runs.isEmpty else { return }
|
|
61
|
+
let widths = nativeListTitleSegmentWidths(
|
|
62
|
+
natural: naturalWidths, shrink: runs.map(\.shrink),
|
|
63
|
+
maxWidthRatio: runs.map(\.maxWidthRatio), available: bounds.width)
|
|
64
|
+
let isRightToLeft = effectiveUserInterfaceLayoutDirection == .rightToLeft
|
|
65
|
+
var x: CGFloat = isRightToLeft ? bounds.width : 0
|
|
66
|
+
for (run, width) in zip(runs, widths) where width > 0 {
|
|
67
|
+
let paragraph = NSMutableParagraphStyle()
|
|
68
|
+
paragraph.lineBreakMode = .byTruncatingTail
|
|
69
|
+
let text = NSAttributedString(
|
|
70
|
+
string: run.text,
|
|
71
|
+
attributes: [.font: run.font, .foregroundColor: run.color, .paragraphStyle: paragraph])
|
|
72
|
+
let height = ceil(run.font.lineHeight)
|
|
73
|
+
let originX = isRightToLeft ? x - width : x
|
|
74
|
+
text.draw(
|
|
75
|
+
with: CGRect(x: originX, y: (bounds.height - height) / 2, width: width, height: height),
|
|
76
|
+
options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine], context: nil)
|
|
77
|
+
x += isRightToLeft ? -width : width
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
package/lib/module/models.js
CHANGED
|
@@ -39,6 +39,8 @@ 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
|
+
|
|
42
44
|
/** Native Market quote row. All values are already localized/formatted strings. */
|
|
43
45
|
|
|
44
46
|
/** Compact KPI card for dashboards and grids. */
|
package/lib/module/validation.js
CHANGED
|
@@ -702,6 +702,66 @@ function assertButtonGroup(row, path) {
|
|
|
702
702
|
}
|
|
703
703
|
});
|
|
704
704
|
}
|
|
705
|
+
const MAX_TITLE_SEGMENTS = 4;
|
|
706
|
+
const HEX_COLOR_PATTERN = /^#[0-9a-f]{6}([0-9a-f]{2})?$/i;
|
|
707
|
+
function assertTitleSegments(value, path) {
|
|
708
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > MAX_TITLE_SEGMENTS) fail(path, `must hold 1...${MAX_TITLE_SEGMENTS} segments`);
|
|
709
|
+
let shrinking = 0;
|
|
710
|
+
value.forEach((item, index) => {
|
|
711
|
+
const segmentPath = `${path}[${index}]`;
|
|
712
|
+
const segment = protocolObject(item, segmentPath);
|
|
713
|
+
assertUnknownKeys(segment, ['text', 'tone', 'fontWeight', 'shrink', 'maxWidthRatio'], segmentPath, 'title segment key');
|
|
714
|
+
const {
|
|
715
|
+
text,
|
|
716
|
+
tone,
|
|
717
|
+
fontWeight,
|
|
718
|
+
shrink,
|
|
719
|
+
maxWidthRatio
|
|
720
|
+
} = segment;
|
|
721
|
+
if (typeof text !== 'string' || !text.trim()) fail(`${segmentPath}.text`, 'must be non-blank');
|
|
722
|
+
assertText(text, `${segmentPath}.text`);
|
|
723
|
+
if (tone !== undefined && tone !== 'primary' && tone !== 'secondary') fail(`${segmentPath}.tone`, 'must be primary or secondary');
|
|
724
|
+
if (fontWeight !== undefined && fontWeight !== 'regular' && fontWeight !== 'medium') fail(`${segmentPath}.fontWeight`, 'must be regular or medium');
|
|
725
|
+
if (shrink !== undefined && typeof shrink !== 'boolean') fail(`${segmentPath}.shrink`, 'must be a boolean');
|
|
726
|
+
if (shrink) shrinking += 1;
|
|
727
|
+
if (maxWidthRatio !== undefined && (typeof maxWidthRatio !== 'number' || !Number.isFinite(maxWidthRatio) || maxWidthRatio <= 0 || maxWidthRatio > 1)) fail(`${segmentPath}.maxWidthRatio`, 'must be finite within (0, 1]');
|
|
728
|
+
});
|
|
729
|
+
if (shrinking > 1) fail(path, 'allows at most one shrinking segment');
|
|
730
|
+
}
|
|
731
|
+
function assertTitleLeadingIcon(value, path) {
|
|
732
|
+
const icon = protocolObject(value, path);
|
|
733
|
+
assertUnknownKeys(icon, ['name', 'tintColor', 'accessibilityLabel'], path, 'title leading icon key');
|
|
734
|
+
const {
|
|
735
|
+
name,
|
|
736
|
+
tintColor,
|
|
737
|
+
accessibilityLabel
|
|
738
|
+
} = icon;
|
|
739
|
+
if (typeof name !== 'string' || !name.trim()) fail(`${path}.name`, 'must be non-blank');
|
|
740
|
+
if (tintColor !== undefined && (typeof tintColor !== 'string' || !HEX_COLOR_PATTERN.test(tintColor))) fail(`${path}.tintColor`, 'must be #RRGGBB or #RRGGBBAA');
|
|
741
|
+
if (accessibilityLabel !== undefined && typeof accessibilityLabel !== 'string') fail(`${path}.accessibilityLabel`, 'must be a string');
|
|
742
|
+
}
|
|
743
|
+
function assertIdentityTitleRuns(row, path) {
|
|
744
|
+
if (row.titleSegments !== undefined) {
|
|
745
|
+
assertTitleSegments(row.titleSegments, `${path}.titleSegments`);
|
|
746
|
+
// titleMatch ranges index `title`, which the runs replace on screen.
|
|
747
|
+
if (row.titleMatch?.length) fail(`${path}.titleMatch`, 'cannot be combined with titleSegments');
|
|
748
|
+
if (row.presentation === 'walletSidebar') fail(`${path}.titleSegments`, 'is not supported with walletSidebar');
|
|
749
|
+
}
|
|
750
|
+
if (row.titleLeadingIcon !== undefined) {
|
|
751
|
+
assertTitleLeadingIcon(row.titleLeadingIcon, `${path}.titleLeadingIcon`);
|
|
752
|
+
if (row.presentation === 'walletSidebar') fail(`${path}.titleLeadingIcon`, 'is not supported with walletSidebar');
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/** Production keeps decorative title runs only when they pass the rules. */
|
|
757
|
+
function passesTitleRunRules(check) {
|
|
758
|
+
try {
|
|
759
|
+
check();
|
|
760
|
+
return true;
|
|
761
|
+
} catch {
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
705
765
|
function assertIdentityExtras(row, path) {
|
|
706
766
|
for (const field of ['titleIcon', 'secondaryTitleIcon']) {
|
|
707
767
|
const titleIcon = row[field];
|
|
@@ -933,6 +993,7 @@ function assertRow(row, index, path = `rows[${index}]`) {
|
|
|
933
993
|
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');
|
|
934
994
|
});
|
|
935
995
|
assertIdentityExtras(row, path);
|
|
996
|
+
assertIdentityTitleRuns(row, path);
|
|
936
997
|
assertText(row.subtitle, `${path}.subtitle`);
|
|
937
998
|
// OneKey patch: selector text segments truncate independently.
|
|
938
999
|
row.subtitleSegments?.forEach((segment, segmentIndex) => {
|
|
@@ -1226,7 +1287,7 @@ function assertSnapshotContract(snapshot) {
|
|
|
1226
1287
|
}
|
|
1227
1288
|
const COMMON_ROW_FIELDS = ['height', 'heightRounding', 'testID', 'opacity', 'backgroundColor', 'backgroundFullWidth', 'pressDisabled', 'key', 'revision', 'sectionKey', 'groupId', 'groupPosition', 'size', 'density', 'disabled', 'selected', 'accessibilityLabel', 'separator', 'type', 'style'];
|
|
1228
1289
|
const ROW_FIELDS = {
|
|
1229
|
-
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'],
|
|
1290
|
+
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'],
|
|
1230
1291
|
walletGroup: [...COMMON_ROW_FIELDS, 'parent', 'children', 'draggable'],
|
|
1231
1292
|
rail: [...COMMON_ROW_FIELDS, 'visual', 'title', 'status', 'badge', 'draggable'],
|
|
1232
1293
|
activity: [...COMMON_ROW_FIELDS, 'leading', 'hideLeading', 'secondaryLeading', 'title', 'description', 'status', 'primaryAmount', 'secondaryAmount', 'amounts', 'badges', 'presentation', 'fee', 'descriptionActionKey', 'footerActions'],
|
|
@@ -1242,7 +1303,7 @@ const ROW_FIELDS = {
|
|
|
1242
1303
|
};
|
|
1243
1304
|
const COMMON_PATCH_FIELDS = ['height', 'heightRounding', 'opacity', 'pressDisabled', 'revision', 'disabled', 'selected', 'accessibilityLabel', 'separator', 'style'];
|
|
1244
1305
|
const PATCH_FIELDS = {
|
|
1245
|
-
identity: [...COMMON_PATCH_FIELDS, 'contentHeight', 'longPressActionKey', 'titleMatch', 'titleActionKey', 'titleActionOnHover', 'subtitleSegments', 'leading', 'leadingAction', 'title', 'titleSuffixes', 'titleIcon', 'secondaryTitleIcon', 'titleImage', 'subtitle', 'tertiary', 'tertiaryTone', 'subtitleLines', 'badges', 'trailing'],
|
|
1306
|
+
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'],
|
|
1246
1307
|
rail: [...COMMON_PATCH_FIELDS, 'title', 'visual', 'status', 'badge'],
|
|
1247
1308
|
activity: [...COMMON_PATCH_FIELDS, 'presentation', 'leading', 'title', 'badges', 'status', 'hideLeading', 'secondaryLeading', 'description', 'primaryAmount', 'secondaryAmount', 'amounts', 'fee', 'descriptionActionKey', 'footerActions'],
|
|
1248
1309
|
message: [...COMMON_PATCH_FIELDS, 'presentation', 'title', 'unread', 'body', 'bodyLines', 'time', 'thumbnail'],
|
|
@@ -1262,7 +1323,7 @@ function protocolObject(value, path) {
|
|
|
1262
1323
|
function protocolKey(value, path) {
|
|
1263
1324
|
if (typeof value !== 'string' || value.trim().length === 0) fail(path, 'must be a non-blank string');
|
|
1264
1325
|
}
|
|
1265
|
-
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']);
|
|
1326
|
+
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']);
|
|
1266
1327
|
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']);
|
|
1267
1328
|
|
|
1268
1329
|
/** Validate only shapes that renderers dereference, not display budgets. */
|
|
@@ -1333,7 +1394,10 @@ function assertRowProtocol(row, path) {
|
|
|
1333
1394
|
}
|
|
1334
1395
|
if (row.height !== undefined && row.height < 0) fail(`${path}.height`, 'must be non-negative');
|
|
1335
1396
|
if (row.type === 'identity') {
|
|
1336
|
-
if (isDevelopment())
|
|
1397
|
+
if (isDevelopment()) {
|
|
1398
|
+
assertIdentityExtras(row, path);
|
|
1399
|
+
assertIdentityTitleRuns(row, path);
|
|
1400
|
+
}
|
|
1337
1401
|
if (row.titleSuffixes !== undefined && !Array.isArray(row.titleSuffixes)) fail(`${path}.titleSuffixes`, 'must be an array');
|
|
1338
1402
|
row.titleSuffixes?.forEach((suffix, index) => {
|
|
1339
1403
|
protocolObject(suffix, `${path}.titleSuffixes[${index}]`);
|
|
@@ -1583,6 +1647,8 @@ const NESTED_DECORATION_FIELDS = {
|
|
|
1583
1647
|
};
|
|
1584
1648
|
function sanitizeDecoration(value, field) {
|
|
1585
1649
|
if (value === undefined || value === null) return undefined;
|
|
1650
|
+
if (field === 'titleSegments') return passesTitleRunRules(() => assertTitleSegments(value, field)) ? value : undefined;
|
|
1651
|
+
if (field === 'titleLeadingIcon') return passesTitleRunRules(() => assertTitleLeadingIcon(value, field)) ? value : undefined;
|
|
1586
1652
|
if (!Array.isArray(value) && ['image', 'networkImage', 'thumbnail', 'source', 'images', 'hoverImages'].includes(field)) return sanitizeImage(value);
|
|
1587
1653
|
if (['titleImage', 'titleIcon', 'secondaryTitleIcon', 'fallbackIcon', 'cornerIcon', 'media'].includes(field) && !isRecord(value)) return undefined;
|
|
1588
1654
|
if (field === 'style' && typeof value !== 'string' && !isRecord(value)) return undefined;
|
|
@@ -467,7 +467,7 @@ export const WEB_LIST_CSS = `
|
|
|
467
467
|
.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}
|
|
468
468
|
.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}
|
|
469
469
|
.ok-native-list-icon-button.ok-native-list-tertiary-icon:focus-visible:not(:disabled){outline:2px solid var(--nl-accent)}
|
|
470
|
-
.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
|
+
.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}
|
|
471
471
|
.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)}
|
|
472
472
|
.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)}
|
|
473
473
|
.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}
|
|
@@ -11,6 +11,54 @@ function toneColor(tone, fallback) {
|
|
|
11
11
|
if (tone === 'secondary') return 'var(--nl-secondary)';
|
|
12
12
|
return fallback === 'secondary' ? 'var(--nl-secondary)' : 'var(--nl-primary)';
|
|
13
13
|
}
|
|
14
|
+
// Hidden from sight, still read: the runs themselves are hidden from
|
|
15
|
+
// assistive technology in favor of `title`.
|
|
16
|
+
const VISUALLY_HIDDEN = {
|
|
17
|
+
position: 'absolute',
|
|
18
|
+
width: '1px',
|
|
19
|
+
height: '1px',
|
|
20
|
+
overflow: 'hidden',
|
|
21
|
+
clip: 'rect(0 0 0 0)',
|
|
22
|
+
whiteSpace: 'nowrap'
|
|
23
|
+
};
|
|
24
|
+
function appendTitleRuns(document, target, row) {
|
|
25
|
+
const runs = createElement(document, 'span', 'ok-native-list-identity-title-runs');
|
|
26
|
+
runs.setAttribute('aria-hidden', 'true');
|
|
27
|
+
Object.assign(runs.style, {
|
|
28
|
+
display: 'flex',
|
|
29
|
+
flex: '1 1 auto',
|
|
30
|
+
minWidth: '0',
|
|
31
|
+
maxWidth: '100%',
|
|
32
|
+
overflow: 'hidden'
|
|
33
|
+
});
|
|
34
|
+
row.titleSegments?.forEach(segment => {
|
|
35
|
+
const run = createElement(document, 'span', 'ok-native-list-identity-title-run', segment.text);
|
|
36
|
+
// Only the shrinking run gives up width; a cap is a share of the line.
|
|
37
|
+
Object.assign(run.style, {
|
|
38
|
+
flex: segment.shrink ? '0 1 auto' : '0 0 auto',
|
|
39
|
+
minWidth: segment.shrink ? '0' : '',
|
|
40
|
+
maxWidth: segment.maxWidthRatio === undefined ? '' : `${segment.maxWidthRatio * 100}%`,
|
|
41
|
+
overflow: 'hidden',
|
|
42
|
+
textOverflow: 'ellipsis',
|
|
43
|
+
whiteSpace: 'pre'
|
|
44
|
+
});
|
|
45
|
+
if (segment.tone) run.style.color = segment.tone === 'secondary' ? 'var(--nl-secondary)' : 'var(--nl-primary)';
|
|
46
|
+
if (segment.fontWeight === 'medium') {
|
|
47
|
+
run.style.fontWeight = '500';
|
|
48
|
+
run.style.fontFamily = 'var(--nl-font-medium)';
|
|
49
|
+
} else if (segment.fontWeight === 'regular') {
|
|
50
|
+
run.style.fontWeight = '400';
|
|
51
|
+
run.style.fontFamily = 'var(--nl-font-regular)';
|
|
52
|
+
}
|
|
53
|
+
setData(run, 'tone', segment.tone);
|
|
54
|
+
runs.appendChild(run);
|
|
55
|
+
});
|
|
56
|
+
const spoken = createElement(document, 'span', 'ok-native-list-identity-title-spoken', row.title);
|
|
57
|
+
Object.assign(spoken.style, VISUALLY_HIDDEN);
|
|
58
|
+
target.replaceChildren(runs, spoken);
|
|
59
|
+
target.style.display = 'flex';
|
|
60
|
+
target.style.minWidth = '0';
|
|
61
|
+
}
|
|
14
62
|
function appendAccessories(parent, document, primitives, row, accessories) {
|
|
15
63
|
if (!accessories?.length) return;
|
|
16
64
|
const container = createElement(document, 'span', 'ok-native-list-accessories');
|
|
@@ -86,6 +134,13 @@ function createIdentityRow(document, primitives, row) {
|
|
|
86
134
|
fragment.appendChild(document.createTextNode(row.title.slice(offset)));
|
|
87
135
|
textTarget.prepend(fragment);
|
|
88
136
|
}
|
|
137
|
+
// The runs replace the plain title, except where `title` itself is
|
|
138
|
+
// highlighted or shown in the wallet sidebar.
|
|
139
|
+
const hasTitleRuns = !!row.titleSegments?.length && presentation !== 'walletSidebar' && !row.titleMatch?.length;
|
|
140
|
+
if (hasTitleRuns) {
|
|
141
|
+
const textTarget = titleElement.querySelector('[data-nl-slot="title"]') ?? titleElement;
|
|
142
|
+
appendTitleRuns(document, textTarget, row);
|
|
143
|
+
}
|
|
89
144
|
if (row.subtitleSegments?.length) {
|
|
90
145
|
column.querySelector('.ok-native-list-secondary')?.remove();
|
|
91
146
|
const segments = createElement(document, 'span', 'ok-native-list-subtitle-segments');
|
|
@@ -211,12 +266,48 @@ function createIdentityRow(document, primitives, row) {
|
|
|
211
266
|
badges.style.minWidth = '0';
|
|
212
267
|
}
|
|
213
268
|
}
|
|
269
|
+
if (hasTitleRuns) {
|
|
270
|
+
// The runs fill what the title line leaves, so a ratio is a share of
|
|
271
|
+
// that width and title accessories sit at the line's end.
|
|
272
|
+
const runs = titleElement.querySelector('.ok-native-list-identity-title-runs');
|
|
273
|
+
for (let node = runs?.parentElement; node && node !== titleElement; node = node.parentElement) {
|
|
274
|
+
node.style.display = 'flex';
|
|
275
|
+
node.style.flex = '1 1 auto';
|
|
276
|
+
node.style.minWidth = '0';
|
|
277
|
+
}
|
|
278
|
+
titleElement.style.display = 'flex';
|
|
279
|
+
titleElement.style.alignItems = 'center';
|
|
280
|
+
}
|
|
281
|
+
if (row.titleLeadingIcon && presentation !== 'walletSidebar') {
|
|
282
|
+
const descriptor = row.titleLeadingIcon;
|
|
283
|
+
if (titleElement.hasAttribute('data-nl-slot')) {
|
|
284
|
+
// Keep the glyph out of the title slot that text styles apply to.
|
|
285
|
+
const titleText = tagSlot(createElement(document, 'span', 'ok-native-list-identity-title-text'), 'title');
|
|
286
|
+
while (titleElement.firstChild) titleText.appendChild(titleElement.firstChild);
|
|
287
|
+
if (hasTitleRuns) {
|
|
288
|
+
titleText.style.display = 'flex';
|
|
289
|
+
titleText.style.minWidth = '0';
|
|
290
|
+
}
|
|
291
|
+
titleElement.removeAttribute('data-nl-slot');
|
|
292
|
+
titleElement.appendChild(titleText);
|
|
293
|
+
}
|
|
294
|
+
const icon = primitives.iconAction(descriptor.name, undefined, undefined, descriptor.tintColor);
|
|
295
|
+
icon.className = 'ok-native-list-identity-title-leading-icon';
|
|
296
|
+
if (descriptor.accessibilityLabel) icon.setAttribute('aria-label', descriptor.accessibilityLabel);else icon.setAttribute('aria-hidden', 'true');
|
|
297
|
+
titleElement.style.display = 'flex';
|
|
298
|
+
titleElement.style.alignItems = 'center';
|
|
299
|
+
// 6px to the title, whatever gap the title line already has.
|
|
300
|
+
const lineGap = Number.parseFloat(titleElement.style.gap || '0') || 0;
|
|
301
|
+
icon.style.marginInlineEnd = `${Math.max(0, 6 - lineGap)}px`;
|
|
302
|
+
titleElement.prepend(icon);
|
|
303
|
+
}
|
|
214
304
|
if (presentation === 'walletSidebar' && row.badges?.length) {
|
|
215
305
|
const badges = createElement(document, 'span', 'ok-native-list-wallet-badges');
|
|
216
306
|
row.badges.forEach(badge => badges.appendChild(createBadge(document, badge)));
|
|
217
307
|
column.appendChild(badges);
|
|
218
308
|
}
|
|
219
309
|
for (const key of ['title', 'subtitle']) {
|
|
310
|
+
if (key === 'title' && hasTitleRuns) continue;
|
|
220
311
|
const lines = row.style?.[key]?.lines ?? row[`${key}Lines`];
|
|
221
312
|
if (lines !== undefined) column.querySelectorAll(`[data-nl-slot="${key}"]`).forEach(text => applyTextLayout(text, lines === 0 ? {} : {
|
|
222
313
|
lines
|
|
@@ -394,6 +394,18 @@ export type RowBase = Readonly<{
|
|
|
394
394
|
accessibilityLabel?: string;
|
|
395
395
|
separator?: boolean;
|
|
396
396
|
}>;
|
|
397
|
+
/** One run of an identity title line; see docs/SPEC.md. */
|
|
398
|
+
export type IdentityTitleSegment = Readonly<{
|
|
399
|
+
text: string;
|
|
400
|
+
/** Omitted: the title color. */
|
|
401
|
+
tone?: 'primary' | 'secondary';
|
|
402
|
+
/** Omitted: the title weight. */
|
|
403
|
+
fontWeight?: 'regular' | 'medium';
|
|
404
|
+
/** The one run that gives up width when the line is too narrow. */
|
|
405
|
+
shrink?: boolean;
|
|
406
|
+
/** Caps the run at this fraction of the title line, within (0, 1]. */
|
|
407
|
+
maxWidthRatio?: number;
|
|
408
|
+
}>;
|
|
397
409
|
export type IdentityRow = RowBase & Readonly<{
|
|
398
410
|
type: 'identity';
|
|
399
411
|
/** Opt in to content-based height; explicit row/container height still wins. */
|
|
@@ -428,6 +440,14 @@ export type IdentityRow = RowBase & Readonly<{
|
|
|
428
440
|
}>;
|
|
429
441
|
/** Web-only additional help icon after titleIcon. */
|
|
430
442
|
secondaryTitleIcon?: IdentityRow['titleIcon'];
|
|
443
|
+
/** Runs drawn in place of `title`, which stays the spoken label. */
|
|
444
|
+
titleSegments?: readonly IdentityTitleSegment[];
|
|
445
|
+
/** 16-point glyph before the title; decorative unless labelled. */
|
|
446
|
+
titleLeadingIcon?: Readonly<{
|
|
447
|
+
name: string;
|
|
448
|
+
tintColor?: string;
|
|
449
|
+
accessibilityLabel?: string;
|
|
450
|
+
}>;
|
|
431
451
|
/** Circular image after title icons; image dimensions must be square, 1...40. */
|
|
432
452
|
titleImage?: Readonly<{
|
|
433
453
|
image: ImageSource;
|
|
@@ -880,7 +900,7 @@ export type PatchClearableChanges<Row, Keys extends keyof Row> = Readonly<{
|
|
|
880
900
|
export type RowPatch = Readonly<{
|
|
881
901
|
type: 'identity';
|
|
882
902
|
key: string;
|
|
883
|
-
changes: PatchClearableChanges<IdentityRow, CommonPatchFields | 'title' | 'titleSuffixes' | 'titleIcon' | 'secondaryTitleIcon' | 'titleImage' | 'subtitle' | 'subtitleLines' | 'contentHeight' | 'longPressActionKey' | 'tertiary' | 'tertiaryTone' | 'badges' | 'trailing' | 'leading' | 'leadingAction' | 'titleMatch' | 'titleActionKey' | 'titleActionOnHover' | 'subtitleSegments'>;
|
|
903
|
+
changes: PatchClearableChanges<IdentityRow, CommonPatchFields | 'title' | 'titleSuffixes' | 'titleIcon' | 'secondaryTitleIcon' | 'titleImage' | 'titleSegments' | 'titleLeadingIcon' | 'subtitle' | 'subtitleLines' | 'contentHeight' | 'longPressActionKey' | 'tertiary' | 'tertiaryTone' | 'badges' | 'trailing' | 'leading' | 'leadingAction' | 'titleMatch' | 'titleActionKey' | 'titleActionOnHover' | 'subtitleSegments'>;
|
|
884
904
|
}> | Readonly<{
|
|
885
905
|
type: 'rail';
|
|
886
906
|
key: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/react-native-native-list",
|
|
3
|
-
"version": "6.7.0-alpha.
|
|
3
|
+
"version": "6.7.0-alpha.428",
|
|
4
4
|
"description": "Template-driven native RecyclerView and UICollectionView for React Native",
|
|
5
5
|
"source": "./src/index.ts",
|
|
6
6
|
"main": "./lib/module/index.js",
|
|
@@ -89,8 +89,8 @@
|
|
|
89
89
|
"typescript": "^5.9.2"
|
|
90
90
|
},
|
|
91
91
|
"peerDependencies": {
|
|
92
|
-
"@onekeyfe/react-native-image": "6.7.0-alpha.
|
|
93
|
-
"@onekeyfe/react-native-native-logger": "6.7.0-alpha.
|
|
92
|
+
"@onekeyfe/react-native-image": "6.7.0-alpha.428",
|
|
93
|
+
"@onekeyfe/react-native-native-logger": "6.7.0-alpha.428",
|
|
94
94
|
"react": "*",
|
|
95
95
|
"react-native": "*",
|
|
96
96
|
"react-native-nitro-modules": "0.37.0"
|
package/src/models.ts
CHANGED
|
@@ -513,6 +513,19 @@ export type RowBase = Readonly<{
|
|
|
513
513
|
separator?: boolean;
|
|
514
514
|
}>;
|
|
515
515
|
|
|
516
|
+
/** One run of an identity title line; see docs/SPEC.md. */
|
|
517
|
+
export type IdentityTitleSegment = Readonly<{
|
|
518
|
+
text: string;
|
|
519
|
+
/** Omitted: the title color. */
|
|
520
|
+
tone?: 'primary' | 'secondary';
|
|
521
|
+
/** Omitted: the title weight. */
|
|
522
|
+
fontWeight?: 'regular' | 'medium';
|
|
523
|
+
/** The one run that gives up width when the line is too narrow. */
|
|
524
|
+
shrink?: boolean;
|
|
525
|
+
/** Caps the run at this fraction of the title line, within (0, 1]. */
|
|
526
|
+
maxWidthRatio?: number;
|
|
527
|
+
}>;
|
|
528
|
+
|
|
516
529
|
export type IdentityRow = RowBase &
|
|
517
530
|
Readonly<{
|
|
518
531
|
type: 'identity';
|
|
@@ -544,6 +557,14 @@ export type IdentityRow = RowBase &
|
|
|
544
557
|
}>;
|
|
545
558
|
/** Web-only additional help icon after titleIcon. */
|
|
546
559
|
secondaryTitleIcon?: IdentityRow['titleIcon'];
|
|
560
|
+
/** Runs drawn in place of `title`, which stays the spoken label. */
|
|
561
|
+
titleSegments?: readonly IdentityTitleSegment[];
|
|
562
|
+
/** 16-point glyph before the title; decorative unless labelled. */
|
|
563
|
+
titleLeadingIcon?: Readonly<{
|
|
564
|
+
name: string;
|
|
565
|
+
tintColor?: string;
|
|
566
|
+
accessibilityLabel?: string;
|
|
567
|
+
}>;
|
|
547
568
|
/** Circular image after title icons; image dimensions must be square, 1...40. */
|
|
548
569
|
titleImage?: Readonly<{
|
|
549
570
|
image: ImageSource;
|
|
@@ -1093,6 +1114,8 @@ export type RowPatch =
|
|
|
1093
1114
|
| 'titleIcon'
|
|
1094
1115
|
| 'secondaryTitleIcon'
|
|
1095
1116
|
| 'titleImage'
|
|
1117
|
+
| 'titleSegments'
|
|
1118
|
+
| 'titleLeadingIcon'
|
|
1096
1119
|
| 'subtitle'
|
|
1097
1120
|
| 'subtitleLines'
|
|
1098
1121
|
| 'contentHeight'
|
package/src/validation.ts
CHANGED
|
@@ -1344,6 +1344,108 @@ function assertButtonGroup(
|
|
|
1344
1344
|
});
|
|
1345
1345
|
}
|
|
1346
1346
|
|
|
1347
|
+
const MAX_TITLE_SEGMENTS = 4;
|
|
1348
|
+
const HEX_COLOR_PATTERN = /^#[0-9a-f]{6}([0-9a-f]{2})?$/i;
|
|
1349
|
+
|
|
1350
|
+
function assertTitleSegments(value: unknown, path: string): void {
|
|
1351
|
+
if (
|
|
1352
|
+
!Array.isArray(value) ||
|
|
1353
|
+
value.length === 0 ||
|
|
1354
|
+
value.length > MAX_TITLE_SEGMENTS
|
|
1355
|
+
)
|
|
1356
|
+
fail(path, `must hold 1...${MAX_TITLE_SEGMENTS} segments`);
|
|
1357
|
+
let shrinking = 0;
|
|
1358
|
+
value.forEach((item: unknown, index) => {
|
|
1359
|
+
const segmentPath = `${path}[${index}]`;
|
|
1360
|
+
const segment = protocolObject(item, segmentPath);
|
|
1361
|
+
assertUnknownKeys(
|
|
1362
|
+
segment,
|
|
1363
|
+
['text', 'tone', 'fontWeight', 'shrink', 'maxWidthRatio'],
|
|
1364
|
+
segmentPath,
|
|
1365
|
+
'title segment key'
|
|
1366
|
+
);
|
|
1367
|
+
const { text, tone, fontWeight, shrink, maxWidthRatio } = segment;
|
|
1368
|
+
if (typeof text !== 'string' || !text.trim())
|
|
1369
|
+
fail(`${segmentPath}.text`, 'must be non-blank');
|
|
1370
|
+
assertText(text, `${segmentPath}.text`);
|
|
1371
|
+
if (tone !== undefined && tone !== 'primary' && tone !== 'secondary')
|
|
1372
|
+
fail(`${segmentPath}.tone`, 'must be primary or secondary');
|
|
1373
|
+
if (
|
|
1374
|
+
fontWeight !== undefined &&
|
|
1375
|
+
fontWeight !== 'regular' &&
|
|
1376
|
+
fontWeight !== 'medium'
|
|
1377
|
+
)
|
|
1378
|
+
fail(`${segmentPath}.fontWeight`, 'must be regular or medium');
|
|
1379
|
+
if (shrink !== undefined && typeof shrink !== 'boolean')
|
|
1380
|
+
fail(`${segmentPath}.shrink`, 'must be a boolean');
|
|
1381
|
+
if (shrink) shrinking += 1;
|
|
1382
|
+
if (
|
|
1383
|
+
maxWidthRatio !== undefined &&
|
|
1384
|
+
(typeof maxWidthRatio !== 'number' ||
|
|
1385
|
+
!Number.isFinite(maxWidthRatio) ||
|
|
1386
|
+
maxWidthRatio <= 0 ||
|
|
1387
|
+
maxWidthRatio > 1)
|
|
1388
|
+
)
|
|
1389
|
+
fail(`${segmentPath}.maxWidthRatio`, 'must be finite within (0, 1]');
|
|
1390
|
+
});
|
|
1391
|
+
if (shrinking > 1) fail(path, 'allows at most one shrinking segment');
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
function assertTitleLeadingIcon(value: unknown, path: string): void {
|
|
1395
|
+
const icon = protocolObject(value, path);
|
|
1396
|
+
assertUnknownKeys(
|
|
1397
|
+
icon,
|
|
1398
|
+
['name', 'tintColor', 'accessibilityLabel'],
|
|
1399
|
+
path,
|
|
1400
|
+
'title leading icon key'
|
|
1401
|
+
);
|
|
1402
|
+
const { name, tintColor, accessibilityLabel } = icon;
|
|
1403
|
+
if (typeof name !== 'string' || !name.trim())
|
|
1404
|
+
fail(`${path}.name`, 'must be non-blank');
|
|
1405
|
+
if (
|
|
1406
|
+
tintColor !== undefined &&
|
|
1407
|
+
(typeof tintColor !== 'string' || !HEX_COLOR_PATTERN.test(tintColor))
|
|
1408
|
+
)
|
|
1409
|
+
fail(`${path}.tintColor`, 'must be #RRGGBB or #RRGGBBAA');
|
|
1410
|
+
if (
|
|
1411
|
+
accessibilityLabel !== undefined &&
|
|
1412
|
+
typeof accessibilityLabel !== 'string'
|
|
1413
|
+
)
|
|
1414
|
+
fail(`${path}.accessibilityLabel`, 'must be a string');
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function assertIdentityTitleRuns(
|
|
1418
|
+
row: Pick<
|
|
1419
|
+
IdentityRow,
|
|
1420
|
+
'titleSegments' | 'titleLeadingIcon' | 'titleMatch' | 'presentation'
|
|
1421
|
+
>,
|
|
1422
|
+
path: string
|
|
1423
|
+
): void {
|
|
1424
|
+
if (row.titleSegments !== undefined) {
|
|
1425
|
+
assertTitleSegments(row.titleSegments, `${path}.titleSegments`);
|
|
1426
|
+
// titleMatch ranges index `title`, which the runs replace on screen.
|
|
1427
|
+
if (row.titleMatch?.length)
|
|
1428
|
+
fail(`${path}.titleMatch`, 'cannot be combined with titleSegments');
|
|
1429
|
+
if (row.presentation === 'walletSidebar')
|
|
1430
|
+
fail(`${path}.titleSegments`, 'is not supported with walletSidebar');
|
|
1431
|
+
}
|
|
1432
|
+
if (row.titleLeadingIcon !== undefined) {
|
|
1433
|
+
assertTitleLeadingIcon(row.titleLeadingIcon, `${path}.titleLeadingIcon`);
|
|
1434
|
+
if (row.presentation === 'walletSidebar')
|
|
1435
|
+
fail(`${path}.titleLeadingIcon`, 'is not supported with walletSidebar');
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/** Production keeps decorative title runs only when they pass the rules. */
|
|
1440
|
+
function passesTitleRunRules(check: () => void): boolean {
|
|
1441
|
+
try {
|
|
1442
|
+
check();
|
|
1443
|
+
return true;
|
|
1444
|
+
} catch {
|
|
1445
|
+
return false;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1347
1449
|
function assertIdentityExtras(
|
|
1348
1450
|
row: Pick<
|
|
1349
1451
|
IdentityRow,
|
|
@@ -1834,6 +1936,7 @@ function assertRow(
|
|
|
1834
1936
|
);
|
|
1835
1937
|
});
|
|
1836
1938
|
assertIdentityExtras(row, path);
|
|
1939
|
+
assertIdentityTitleRuns(row, path);
|
|
1837
1940
|
assertText(row.subtitle, `${path}.subtitle`);
|
|
1838
1941
|
// OneKey patch: selector text segments truncate independently.
|
|
1839
1942
|
row.subtitleSegments?.forEach((segment, segmentIndex) => {
|
|
@@ -2416,6 +2519,8 @@ const ROW_FIELDS: Readonly<Record<string, readonly string[]>> = {
|
|
|
2416
2519
|
'titleIcon',
|
|
2417
2520
|
'secondaryTitleIcon',
|
|
2418
2521
|
'titleImage',
|
|
2522
|
+
'titleSegments',
|
|
2523
|
+
'titleLeadingIcon',
|
|
2419
2524
|
'subtitle',
|
|
2420
2525
|
'tertiary',
|
|
2421
2526
|
'tertiaryTone',
|
|
@@ -2593,6 +2698,8 @@ const PATCH_FIELDS: Readonly<Record<string, readonly string[]>> = {
|
|
|
2593
2698
|
'titleIcon',
|
|
2594
2699
|
'secondaryTitleIcon',
|
|
2595
2700
|
'titleImage',
|
|
2701
|
+
'titleSegments',
|
|
2702
|
+
'titleLeadingIcon',
|
|
2596
2703
|
'subtitle',
|
|
2597
2704
|
'tertiary',
|
|
2598
2705
|
'tertiaryTone',
|
|
@@ -2724,6 +2831,7 @@ function protocolKey(value: unknown, path: string): void {
|
|
|
2724
2831
|
const PROTOCOL_ARRAY_FIELDS = new Set([
|
|
2725
2832
|
'columns',
|
|
2726
2833
|
'titleSuffixes',
|
|
2834
|
+
'titleSegments',
|
|
2727
2835
|
'cells',
|
|
2728
2836
|
'points',
|
|
2729
2837
|
'images',
|
|
@@ -2965,7 +3073,10 @@ function assertRowProtocol(row: RowModel, path: string): void {
|
|
|
2965
3073
|
if (row.height !== undefined && row.height < 0)
|
|
2966
3074
|
fail(`${path}.height`, 'must be non-negative');
|
|
2967
3075
|
if (row.type === 'identity') {
|
|
2968
|
-
if (isDevelopment())
|
|
3076
|
+
if (isDevelopment()) {
|
|
3077
|
+
assertIdentityExtras(row, path);
|
|
3078
|
+
assertIdentityTitleRuns(row, path);
|
|
3079
|
+
}
|
|
2969
3080
|
if (row.titleSuffixes !== undefined && !Array.isArray(row.titleSuffixes))
|
|
2970
3081
|
fail(`${path}.titleSuffixes`, 'must be an array');
|
|
2971
3082
|
row.titleSuffixes?.forEach((suffix, index) => {
|
|
@@ -3560,6 +3671,14 @@ const NESTED_DECORATION_FIELDS: Readonly<Record<string, readonly string[]>> = {
|
|
|
3560
3671
|
|
|
3561
3672
|
function sanitizeDecoration(value: unknown, field: string): unknown {
|
|
3562
3673
|
if (value === undefined || value === null) return undefined;
|
|
3674
|
+
if (field === 'titleSegments')
|
|
3675
|
+
return passesTitleRunRules(() => assertTitleSegments(value, field))
|
|
3676
|
+
? value
|
|
3677
|
+
: undefined;
|
|
3678
|
+
if (field === 'titleLeadingIcon')
|
|
3679
|
+
return passesTitleRunRules(() => assertTitleLeadingIcon(value, field))
|
|
3680
|
+
? value
|
|
3681
|
+
: undefined;
|
|
3563
3682
|
if (
|
|
3564
3683
|
!Array.isArray(value) &&
|
|
3565
3684
|
[
|