@onekeyfe/react-native-native-list 6.7.0-alpha.401 → 6.7.0-alpha.403
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
|
@@ -653,6 +653,66 @@ 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
|
+
}
|
|
656
716
|
function assertIdentityExtras(row, path) {
|
|
657
717
|
for (const field of ['titleIcon', 'secondaryTitleIcon']) {
|
|
658
718
|
const titleIcon = row[field];
|
|
@@ -884,6 +944,7 @@ function assertRow(row, index, path = `rows[${index}]`) {
|
|
|
884
944
|
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');
|
|
885
945
|
});
|
|
886
946
|
assertIdentityExtras(row, path);
|
|
947
|
+
assertIdentityTitleRuns(row, path);
|
|
887
948
|
assertText(row.subtitle, `${path}.subtitle`);
|
|
888
949
|
// OneKey patch: selector text segments truncate independently.
|
|
889
950
|
row.subtitleSegments?.forEach((segment, segmentIndex) => {
|
|
@@ -1177,7 +1238,7 @@ function assertSnapshotContract(snapshot) {
|
|
|
1177
1238
|
}
|
|
1178
1239
|
const COMMON_ROW_FIELDS = ['height', 'heightRounding', 'testID', 'opacity', 'backgroundColor', 'backgroundFullWidth', 'pressDisabled', 'key', 'revision', 'sectionKey', 'groupId', 'groupPosition', 'size', 'density', 'disabled', 'selected', 'accessibilityLabel', 'separator', 'type', 'style'];
|
|
1179
1240
|
const ROW_FIELDS = {
|
|
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'],
|
|
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'],
|
|
1181
1242
|
walletGroup: [...COMMON_ROW_FIELDS, 'parent', 'children', 'draggable'],
|
|
1182
1243
|
rail: [...COMMON_ROW_FIELDS, 'visual', 'title', 'status', 'badge', 'draggable'],
|
|
1183
1244
|
activity: [...COMMON_ROW_FIELDS, 'leading', 'hideLeading', 'secondaryLeading', 'title', 'description', 'status', 'primaryAmount', 'secondaryAmount', 'amounts', 'badges', 'presentation', 'fee', 'descriptionActionKey', 'footerActions'],
|
|
@@ -1193,7 +1254,7 @@ const ROW_FIELDS = {
|
|
|
1193
1254
|
};
|
|
1194
1255
|
const COMMON_PATCH_FIELDS = ['height', 'heightRounding', 'opacity', 'pressDisabled', 'revision', 'disabled', 'selected', 'accessibilityLabel', 'separator', 'style'];
|
|
1195
1256
|
const PATCH_FIELDS = {
|
|
1196
|
-
identity: [...COMMON_PATCH_FIELDS, 'contentHeight', 'longPressActionKey', 'titleMatch', 'titleActionKey', 'titleActionOnHover', 'subtitleSegments', 'leading', 'leadingAction', 'title', 'titleSuffixes', 'titleIcon', 'secondaryTitleIcon', 'titleImage', 'subtitle', 'tertiary', 'tertiaryTone', 'subtitleLines', 'badges', 'trailing'],
|
|
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'],
|
|
1197
1258
|
rail: [...COMMON_PATCH_FIELDS, 'title', 'visual', 'status', 'badge'],
|
|
1198
1259
|
activity: [...COMMON_PATCH_FIELDS, 'presentation', 'leading', 'title', 'badges', 'status', 'hideLeading', 'secondaryLeading', 'description', 'primaryAmount', 'secondaryAmount', 'amounts', 'fee', 'descriptionActionKey', 'footerActions'],
|
|
1199
1260
|
message: [...COMMON_PATCH_FIELDS, 'presentation', 'title', 'unread', 'body', 'bodyLines', 'time', 'thumbnail'],
|
|
@@ -1213,7 +1274,7 @@ function protocolObject(value, path) {
|
|
|
1213
1274
|
function protocolKey(value, path) {
|
|
1214
1275
|
if (typeof value !== 'string' || value.trim().length === 0) fail(path, 'must be a non-blank string');
|
|
1215
1276
|
}
|
|
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']);
|
|
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']);
|
|
1217
1278
|
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']);
|
|
1218
1279
|
|
|
1219
1280
|
/** Validate only shapes that renderers dereference, not display budgets. */
|
|
@@ -1284,7 +1345,10 @@ function assertRowProtocol(row, path) {
|
|
|
1284
1345
|
}
|
|
1285
1346
|
if (row.height !== undefined && row.height < 0) fail(`${path}.height`, 'must be non-negative');
|
|
1286
1347
|
if (row.type === 'identity') {
|
|
1287
|
-
if (isDevelopment())
|
|
1348
|
+
if (isDevelopment()) {
|
|
1349
|
+
assertIdentityExtras(row, path);
|
|
1350
|
+
assertIdentityTitleRuns(row, path);
|
|
1351
|
+
}
|
|
1288
1352
|
if (row.titleSuffixes !== undefined && !Array.isArray(row.titleSuffixes)) fail(`${path}.titleSuffixes`, 'must be an array');
|
|
1289
1353
|
row.titleSuffixes?.forEach((suffix, index) => {
|
|
1290
1354
|
protocolObject(suffix, `${path}.titleSuffixes[${index}]`);
|
|
@@ -1533,6 +1597,8 @@ const NESTED_DECORATION_FIELDS = {
|
|
|
1533
1597
|
};
|
|
1534
1598
|
function sanitizeDecoration(value, field) {
|
|
1535
1599
|
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;
|
|
1536
1602
|
if (!Array.isArray(value) && ['image', 'networkImage', 'thumbnail', 'source', 'images', 'hoverImages'].includes(field)) return sanitizeImage(value);
|
|
1537
1603
|
if (['titleImage', 'titleIcon', 'secondaryTitleIcon', 'fallbackIcon', 'cornerIcon', 'media'].includes(field) && !isRecord(value)) return undefined;
|
|
1538
1604
|
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-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-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}
|
|
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}
|
|
@@ -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');
|
|
@@ -203,12 +258,48 @@ function createIdentityRow(document, primitives, row) {
|
|
|
203
258
|
badges.style.minWidth = '0';
|
|
204
259
|
}
|
|
205
260
|
}
|
|
261
|
+
if (hasTitleRuns) {
|
|
262
|
+
// The runs fill what the title line leaves, so a ratio is a share of
|
|
263
|
+
// that width and title accessories sit at the line's end.
|
|
264
|
+
const runs = titleElement.querySelector('.ok-native-list-identity-title-runs');
|
|
265
|
+
for (let node = runs?.parentElement; node && node !== titleElement; node = node.parentElement) {
|
|
266
|
+
node.style.display = 'flex';
|
|
267
|
+
node.style.flex = '1 1 auto';
|
|
268
|
+
node.style.minWidth = '0';
|
|
269
|
+
}
|
|
270
|
+
titleElement.style.display = 'flex';
|
|
271
|
+
titleElement.style.alignItems = 'center';
|
|
272
|
+
}
|
|
273
|
+
if (row.titleLeadingIcon && presentation !== 'walletSidebar') {
|
|
274
|
+
const descriptor = row.titleLeadingIcon;
|
|
275
|
+
if (titleElement.hasAttribute('data-nl-slot')) {
|
|
276
|
+
// Keep the glyph out of the title slot that text styles apply to.
|
|
277
|
+
const titleText = tagSlot(createElement(document, 'span', 'ok-native-list-identity-title-text'), 'title');
|
|
278
|
+
while (titleElement.firstChild) titleText.appendChild(titleElement.firstChild);
|
|
279
|
+
if (hasTitleRuns) {
|
|
280
|
+
titleText.style.display = 'flex';
|
|
281
|
+
titleText.style.minWidth = '0';
|
|
282
|
+
}
|
|
283
|
+
titleElement.removeAttribute('data-nl-slot');
|
|
284
|
+
titleElement.appendChild(titleText);
|
|
285
|
+
}
|
|
286
|
+
const icon = primitives.iconAction(descriptor.name, undefined, undefined, descriptor.tintColor);
|
|
287
|
+
icon.className = 'ok-native-list-identity-title-leading-icon';
|
|
288
|
+
if (descriptor.accessibilityLabel) icon.setAttribute('aria-label', descriptor.accessibilityLabel);else icon.setAttribute('aria-hidden', 'true');
|
|
289
|
+
titleElement.style.display = 'flex';
|
|
290
|
+
titleElement.style.alignItems = 'center';
|
|
291
|
+
// 6px to the title, whatever gap the title line already has.
|
|
292
|
+
const lineGap = Number.parseFloat(titleElement.style.gap || '0') || 0;
|
|
293
|
+
icon.style.marginInlineEnd = `${Math.max(0, 6 - lineGap)}px`;
|
|
294
|
+
titleElement.prepend(icon);
|
|
295
|
+
}
|
|
206
296
|
if (presentation === 'walletSidebar' && row.badges?.length) {
|
|
207
297
|
const badges = createElement(document, 'span', 'ok-native-list-wallet-badges');
|
|
208
298
|
row.badges.forEach(badge => badges.appendChild(createBadge(document, badge)));
|
|
209
299
|
column.appendChild(badges);
|
|
210
300
|
}
|
|
211
301
|
for (const key of ['title', 'subtitle']) {
|
|
302
|
+
if (key === 'title' && hasTitleRuns) continue;
|
|
212
303
|
const lines = row.style?.[key]?.lines ?? row[`${key}Lines`];
|
|
213
304
|
if (lines !== undefined) column.querySelectorAll(`[data-nl-slot="${key}"]`).forEach(text => applyTextLayout(text, lines === 0 ? {} : {
|
|
214
305
|
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;
|
|
@@ -878,7 +898,7 @@ export type PatchClearableChanges<Row, Keys extends keyof Row> = Readonly<{
|
|
|
878
898
|
export type RowPatch = Readonly<{
|
|
879
899
|
type: 'identity';
|
|
880
900
|
key: string;
|
|
881
|
-
changes: PatchClearableChanges<IdentityRow, CommonPatchFields | 'title' | 'titleSuffixes' | 'titleIcon' | 'secondaryTitleIcon' | 'titleImage' | 'subtitle' | 'subtitleLines' | 'contentHeight' | 'longPressActionKey' | 'tertiary' | 'tertiaryTone' | 'badges' | 'trailing' | 'leading' | 'leadingAction' | 'titleMatch' | 'titleActionKey' | 'titleActionOnHover' | 'subtitleSegments'>;
|
|
901
|
+
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'>;
|
|
882
902
|
}> | Readonly<{
|
|
883
903
|
type: 'rail';
|
|
884
904
|
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.403",
|
|
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",
|
|
@@ -86,8 +86,8 @@
|
|
|
86
86
|
"typescript": "^5.9.2"
|
|
87
87
|
},
|
|
88
88
|
"peerDependencies": {
|
|
89
|
-
"@onekeyfe/react-native-image": "6.7.0-alpha.
|
|
90
|
-
"@onekeyfe/react-native-native-logger": "6.7.0-alpha.
|
|
89
|
+
"@onekeyfe/react-native-image": "6.7.0-alpha.403",
|
|
90
|
+
"@onekeyfe/react-native-native-logger": "6.7.0-alpha.403",
|
|
91
91
|
"react": "*",
|
|
92
92
|
"react-native": "*",
|
|
93
93
|
"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;
|
|
@@ -1080,6 +1101,8 @@ export type RowPatch =
|
|
|
1080
1101
|
| 'titleIcon'
|
|
1081
1102
|
| 'secondaryTitleIcon'
|
|
1082
1103
|
| 'titleImage'
|
|
1104
|
+
| 'titleSegments'
|
|
1105
|
+
| 'titleLeadingIcon'
|
|
1083
1106
|
| 'subtitle'
|
|
1084
1107
|
| 'subtitleLines'
|
|
1085
1108
|
| 'contentHeight'
|
package/src/validation.ts
CHANGED
|
@@ -1252,6 +1252,108 @@ function assertButtonGroup(
|
|
|
1252
1252
|
});
|
|
1253
1253
|
}
|
|
1254
1254
|
|
|
1255
|
+
const MAX_TITLE_SEGMENTS = 4;
|
|
1256
|
+
const HEX_COLOR_PATTERN = /^#[0-9a-f]{6}([0-9a-f]{2})?$/i;
|
|
1257
|
+
|
|
1258
|
+
function assertTitleSegments(value: unknown, path: string): void {
|
|
1259
|
+
if (
|
|
1260
|
+
!Array.isArray(value) ||
|
|
1261
|
+
value.length === 0 ||
|
|
1262
|
+
value.length > MAX_TITLE_SEGMENTS
|
|
1263
|
+
)
|
|
1264
|
+
fail(path, `must hold 1...${MAX_TITLE_SEGMENTS} segments`);
|
|
1265
|
+
let shrinking = 0;
|
|
1266
|
+
value.forEach((item: unknown, index) => {
|
|
1267
|
+
const segmentPath = `${path}[${index}]`;
|
|
1268
|
+
const segment = protocolObject(item, segmentPath);
|
|
1269
|
+
assertUnknownKeys(
|
|
1270
|
+
segment,
|
|
1271
|
+
['text', 'tone', 'fontWeight', 'shrink', 'maxWidthRatio'],
|
|
1272
|
+
segmentPath,
|
|
1273
|
+
'title segment key'
|
|
1274
|
+
);
|
|
1275
|
+
const { text, tone, fontWeight, shrink, maxWidthRatio } = segment;
|
|
1276
|
+
if (typeof text !== 'string' || !text.trim())
|
|
1277
|
+
fail(`${segmentPath}.text`, 'must be non-blank');
|
|
1278
|
+
assertText(text, `${segmentPath}.text`);
|
|
1279
|
+
if (tone !== undefined && tone !== 'primary' && tone !== 'secondary')
|
|
1280
|
+
fail(`${segmentPath}.tone`, 'must be primary or secondary');
|
|
1281
|
+
if (
|
|
1282
|
+
fontWeight !== undefined &&
|
|
1283
|
+
fontWeight !== 'regular' &&
|
|
1284
|
+
fontWeight !== 'medium'
|
|
1285
|
+
)
|
|
1286
|
+
fail(`${segmentPath}.fontWeight`, 'must be regular or medium');
|
|
1287
|
+
if (shrink !== undefined && typeof shrink !== 'boolean')
|
|
1288
|
+
fail(`${segmentPath}.shrink`, 'must be a boolean');
|
|
1289
|
+
if (shrink) shrinking += 1;
|
|
1290
|
+
if (
|
|
1291
|
+
maxWidthRatio !== undefined &&
|
|
1292
|
+
(typeof maxWidthRatio !== 'number' ||
|
|
1293
|
+
!Number.isFinite(maxWidthRatio) ||
|
|
1294
|
+
maxWidthRatio <= 0 ||
|
|
1295
|
+
maxWidthRatio > 1)
|
|
1296
|
+
)
|
|
1297
|
+
fail(`${segmentPath}.maxWidthRatio`, 'must be finite within (0, 1]');
|
|
1298
|
+
});
|
|
1299
|
+
if (shrinking > 1) fail(path, 'allows at most one shrinking segment');
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
function assertTitleLeadingIcon(value: unknown, path: string): void {
|
|
1303
|
+
const icon = protocolObject(value, path);
|
|
1304
|
+
assertUnknownKeys(
|
|
1305
|
+
icon,
|
|
1306
|
+
['name', 'tintColor', 'accessibilityLabel'],
|
|
1307
|
+
path,
|
|
1308
|
+
'title leading icon key'
|
|
1309
|
+
);
|
|
1310
|
+
const { name, tintColor, accessibilityLabel } = icon;
|
|
1311
|
+
if (typeof name !== 'string' || !name.trim())
|
|
1312
|
+
fail(`${path}.name`, 'must be non-blank');
|
|
1313
|
+
if (
|
|
1314
|
+
tintColor !== undefined &&
|
|
1315
|
+
(typeof tintColor !== 'string' || !HEX_COLOR_PATTERN.test(tintColor))
|
|
1316
|
+
)
|
|
1317
|
+
fail(`${path}.tintColor`, 'must be #RRGGBB or #RRGGBBAA');
|
|
1318
|
+
if (
|
|
1319
|
+
accessibilityLabel !== undefined &&
|
|
1320
|
+
typeof accessibilityLabel !== 'string'
|
|
1321
|
+
)
|
|
1322
|
+
fail(`${path}.accessibilityLabel`, 'must be a string');
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function assertIdentityTitleRuns(
|
|
1326
|
+
row: Pick<
|
|
1327
|
+
IdentityRow,
|
|
1328
|
+
'titleSegments' | 'titleLeadingIcon' | 'titleMatch' | 'presentation'
|
|
1329
|
+
>,
|
|
1330
|
+
path: string
|
|
1331
|
+
): void {
|
|
1332
|
+
if (row.titleSegments !== undefined) {
|
|
1333
|
+
assertTitleSegments(row.titleSegments, `${path}.titleSegments`);
|
|
1334
|
+
// titleMatch ranges index `title`, which the runs replace on screen.
|
|
1335
|
+
if (row.titleMatch?.length)
|
|
1336
|
+
fail(`${path}.titleMatch`, 'cannot be combined with titleSegments');
|
|
1337
|
+
if (row.presentation === 'walletSidebar')
|
|
1338
|
+
fail(`${path}.titleSegments`, 'is not supported with walletSidebar');
|
|
1339
|
+
}
|
|
1340
|
+
if (row.titleLeadingIcon !== undefined) {
|
|
1341
|
+
assertTitleLeadingIcon(row.titleLeadingIcon, `${path}.titleLeadingIcon`);
|
|
1342
|
+
if (row.presentation === 'walletSidebar')
|
|
1343
|
+
fail(`${path}.titleLeadingIcon`, 'is not supported with walletSidebar');
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
/** Production keeps decorative title runs only when they pass the rules. */
|
|
1348
|
+
function passesTitleRunRules(check: () => void): boolean {
|
|
1349
|
+
try {
|
|
1350
|
+
check();
|
|
1351
|
+
return true;
|
|
1352
|
+
} catch {
|
|
1353
|
+
return false;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1255
1357
|
function assertIdentityExtras(
|
|
1256
1358
|
row: Pick<
|
|
1257
1359
|
IdentityRow,
|
|
@@ -1742,6 +1844,7 @@ function assertRow(
|
|
|
1742
1844
|
);
|
|
1743
1845
|
});
|
|
1744
1846
|
assertIdentityExtras(row, path);
|
|
1847
|
+
assertIdentityTitleRuns(row, path);
|
|
1745
1848
|
assertText(row.subtitle, `${path}.subtitle`);
|
|
1746
1849
|
// OneKey patch: selector text segments truncate independently.
|
|
1747
1850
|
row.subtitleSegments?.forEach((segment, segmentIndex) => {
|
|
@@ -2324,6 +2427,8 @@ const ROW_FIELDS: Readonly<Record<string, readonly string[]>> = {
|
|
|
2324
2427
|
'titleIcon',
|
|
2325
2428
|
'secondaryTitleIcon',
|
|
2326
2429
|
'titleImage',
|
|
2430
|
+
'titleSegments',
|
|
2431
|
+
'titleLeadingIcon',
|
|
2327
2432
|
'subtitle',
|
|
2328
2433
|
'tertiary',
|
|
2329
2434
|
'tertiaryTone',
|
|
@@ -2500,6 +2605,8 @@ const PATCH_FIELDS: Readonly<Record<string, readonly string[]>> = {
|
|
|
2500
2605
|
'titleIcon',
|
|
2501
2606
|
'secondaryTitleIcon',
|
|
2502
2607
|
'titleImage',
|
|
2608
|
+
'titleSegments',
|
|
2609
|
+
'titleLeadingIcon',
|
|
2503
2610
|
'subtitle',
|
|
2504
2611
|
'tertiary',
|
|
2505
2612
|
'tertiaryTone',
|
|
@@ -2630,6 +2737,7 @@ function protocolKey(value: unknown, path: string): void {
|
|
|
2630
2737
|
const PROTOCOL_ARRAY_FIELDS = new Set([
|
|
2631
2738
|
'columns',
|
|
2632
2739
|
'titleSuffixes',
|
|
2740
|
+
'titleSegments',
|
|
2633
2741
|
'cells',
|
|
2634
2742
|
'points',
|
|
2635
2743
|
'images',
|
|
@@ -2871,7 +2979,10 @@ function assertRowProtocol(row: RowModel, path: string): void {
|
|
|
2871
2979
|
if (row.height !== undefined && row.height < 0)
|
|
2872
2980
|
fail(`${path}.height`, 'must be non-negative');
|
|
2873
2981
|
if (row.type === 'identity') {
|
|
2874
|
-
if (isDevelopment())
|
|
2982
|
+
if (isDevelopment()) {
|
|
2983
|
+
assertIdentityExtras(row, path);
|
|
2984
|
+
assertIdentityTitleRuns(row, path);
|
|
2985
|
+
}
|
|
2875
2986
|
if (row.titleSuffixes !== undefined && !Array.isArray(row.titleSuffixes))
|
|
2876
2987
|
fail(`${path}.titleSuffixes`, 'must be an array');
|
|
2877
2988
|
row.titleSuffixes?.forEach((suffix, index) => {
|
|
@@ -3463,6 +3574,14 @@ const NESTED_DECORATION_FIELDS: Readonly<Record<string, readonly string[]>> = {
|
|
|
3463
3574
|
|
|
3464
3575
|
function sanitizeDecoration(value: unknown, field: string): unknown {
|
|
3465
3576
|
if (value === undefined || value === null) return undefined;
|
|
3577
|
+
if (field === 'titleSegments')
|
|
3578
|
+
return passesTitleRunRules(() => assertTitleSegments(value, field))
|
|
3579
|
+
? value
|
|
3580
|
+
: undefined;
|
|
3581
|
+
if (field === 'titleLeadingIcon')
|
|
3582
|
+
return passesTitleRunRules(() => assertTitleLeadingIcon(value, field))
|
|
3583
|
+
? value
|
|
3584
|
+
: undefined;
|
|
3466
3585
|
if (
|
|
3467
3586
|
!Array.isArray(value) &&
|
|
3468
3587
|
[
|