Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file.
- **native-list**: Add `docs/STYLE_SPEC.md`, the shared style vocabulary for rows, section headers, fixed footers, and empty states. It records the design tokens (aliased to the application's own token names), the per-template style surface keyed by model field, list chrome, the template isolation rules, and a review checklist. Cross-platform divergences — row-height tables, typography, the Android sticky-header renderer, list-wide source scale — are registered rather than changed.

### Chores
- Add the "Native List Row Style" example page. Every template is rendered twice, plain and styled, behind a toggle; with the toggle off the page must match the build from before the row style existed, which makes it the cross-platform regression check. It also covers the cases the mapping is most likely to get wrong: `metricCard` styling `value` versus `title`, and `message` styling `body` and `time`.
- **native-list (Web)**: Remove dead duplicated rules from `WEB_LIST_CSS`. Four blocks (`.ok-native-list-footer`/`-sticky`/`-index-rail`/`-index-button`, `-refresh`, `-warning`, `-subtitle-segments`) were emitted twice and a `prefers-reduced-motion` block three times; every property of the earlier copies was redeclared by the later ones, so removing them changes no rendering.

## [3.0.136] - 2026-09-15
Expand Down
247 changes: 247 additions & 0 deletions example/react-native/pages/NativeListRowStylePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import { useMemo, useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import {
NativeList,
type NativeListSnapshot,
type NativeListTheme,
type RowModel,
} from '@onekeyfe/react-native-native-list';

// docs/STYLE_SPEC.md section 3.1 names; the list keys are the legacy aliases.
const THEME: NativeListTheme = {
background: '#F5F5F5',
rowBackground: '#FFFFFF',
rowSelectedBackground: '#F0F0F0',
rowPressedBackground: '#E8E8E8',
subduedBackground: '#F9F9F9',
strongBackground: '#F0F0F0',
primaryText: '#202020',
secondaryText: '#646464',
disabledText: '#8D8D8D',
icon: '#646464',
iconSubdued: '#8D8D8D',
separator: '#E0E0E0',
accent: '#108303',
positive: '#218358',
negative: '#CE2C31',
inverseBackground: '#202020',
inverseText: '#FCFCFC',
info: '#0D74CE',
};

const header = (key: string, title: string, note: string): RowModel => ({
type: 'sectionHeader',
key: `header-${key}`,
sectionKey: key,
title,
value: note,
});

/**
* Each template appears twice: once untouched, once styled. With the toggle off
* every row is untouched, so the whole page should be pixel-identical to the
* build before the row style existed - that is the regression check.
*/
function buildRows(styled: boolean): RowModel[] {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: [The regression page does not cover every styled row template]

When this page is used as the documented cross-platform regression check, buildRows only creates styled counterparts for identity, metricCard, message, rail, and action. The NativeList model also exposes style surfaces for activity, dataRow, market, mediaTile, sectionHeader, system, and walletGroup, so regressions in those binders are never rendered or exercised by the toggle.

Please add representative plain/styled pairs for the missing templates (including a styled section header) and have the toggle remove each counterpart’s style object, so the page actually covers the promised all-template comparison.

const rows: RowModel[] = [];

rows.push(header('identity', 'identity', 'plain / styled'));
const identity = {
type: 'identity',
sectionKey: 'identity',
leading: { kind: 'icon', name: 'StarOutline' },
title: 'Bitcoin',
subtitle: 'BTC · Bitcoin',
badges: [{ key: 'tag', text: 'Native' }],
trailing: [{ kind: 'value' as const, text: '$64,230' }],
} as const;
rows.push({ ...identity, key: 'identity-plain' });
rows.push({
...identity,
key: 'identity-styled',
...(styled
? {
style: {
// A named step from the application's scale, resolved to numbers in
// JavaScript before the snapshot is serialized.
title: { token: '$bodyMd' as const },
subtitle: { fontSize: 12, lineHeight: 16, color: '#8D8D8D' },
badge: { fontSize: 10 },
value: { token: '$bodySm' as const },
horizontalPadding: 20,
lineGap: 2,
},
}
: {}),
});

// The mapping is not one to one: metricCard draws `value` through the view
// identity uses for `title`. Styling `value` must hit the large number.
rows.push(header('metric', 'metricCard', 'value vs title'));
const metric = {
type: 'metricCard',
sectionKey: 'metric',
title: 'Volume',
value: '$1.24B',
subtitle: '24h',
trend: '+2.4%',
trendTone: 'positive',
} as const;
rows.push({ ...metric, key: 'metric-plain' });
rows.push({
...metric,
key: 'metric-styled',
...(styled
? {
style: {
title: { fontSize: 10, color: '#8D8D8D' },
value: { token: '$headingLg' as const, color: '#108303' },
trend: { fontSize: 11 },
},
}
: {}),
});

rows.push(header('message', 'message', 'body / time'));
const message = {
type: 'message',
sectionKey: 'message',
title: 'Transfer confirmed',
body: 'Your transfer of 0.5 BTC has been confirmed on-chain.',
bodyLines: 2,
time: '2m',
} as const;
rows.push({ ...message, key: 'message-plain' });
rows.push({
...message,
key: 'message-styled',
...(styled
? {
style: {
title: { token: '$bodyMd' as const },
body: { fontSize: 12, lineHeight: 16 },
time: { fontSize: 10, color: '#8D8D8D' },
},
}
: {}),
});

rows.push(header('rail', 'rail', 'title / status / badge'));
const rail = {
type: 'rail',
sectionKey: 'rail',
visual: { kind: 'icon', name: 'StarOutline' },
title: 'ETH',
status: 'online',
badge: { key: 'change', text: '+1.2%', tone: 'success' },
} as const;
rows.push({ ...rail, key: 'rail-plain' });
rows.push({
...rail,
key: 'rail-styled',
...(styled
? { style: { title: { fontSize: 11 }, status: { fontSize: 10 } } }
: {}),
});

rows.push(header('action', 'action', 'title'));
const action = {
type: 'action',
sectionKey: 'action',
title: 'Add custom token',
actionKey: 'demo.add',
tone: 'primary',
} as const;
rows.push({ ...action, key: 'action-plain' });
rows.push({
...action,
key: 'action-styled',
...(styled
? { style: { title: { token: '$bodyMd' as const, color: '#0D74CE' } } }
: {}),
});

// Growing text needs an explicit height: row heights are not derived from the
// style. See docs/STYLE_SPEC.md section 6.1.
rows.push(header('height', 'explicit height', 'larger text'));
rows.push({
type: 'identity',
key: 'identity-grown',
sectionKey: 'height',
leading: { kind: 'icon', name: 'StarOutline' },
title: 'Larger title',
subtitle: 'Row height is given, not inferred',
...(styled
? {
height: 76,
style: {
title: { token: '$headingMd' as const },
subtitle: { token: '$bodyMd' as const },
verticalPadding: 12,
},
}
: {}),
});

return rows;
}

export function NativeListRowStylePage() {
const [styled, setStyled] = useState(true);
const snapshot = useMemo<NativeListSnapshot>(
() => ({
schemaVersion: 1,
generation: styled ? 2 : 1,
layout: { kind: 'sectioned', stickyHeaders: true, contentPadding: 8 },
theme: THEME,
rows: buildRows(styled),
}),
[styled],
);

return (
<View style={styles.container}>
<View style={styles.bar}>
<Text style={styles.caption}>
Every template appears twice: plain, then styled. Turn the style off
and the page should match the build before the row style existed.
</Text>
<Pressable
accessibilityRole="button"
testID="native-list-row-style-toggle"
onPress={() => setStyled(current => !current)}
style={({ pressed }) => [styles.button, pressed && styles.pressed]}
>
<Text style={styles.buttonLabel}>
{styled ? 'Style: on' : 'Style: off'}
</Text>
</Pressable>
</View>
<NativeList style={styles.list} snapshot={snapshot} />
</View>
);
}

const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#F5F5F5' },
bar: {
paddingHorizontal: 16,
paddingTop: 12,
paddingBottom: 10,
gap: 10,
backgroundColor: '#FFFFFF',
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#E0E0E0',
},
caption: { fontSize: 13, lineHeight: 18, color: '#646464' },
button: {
alignSelf: 'flex-start',
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 8,
backgroundColor: '#202020',
},
pressed: { opacity: 0.8 },
buttonLabel: { color: '#FCFCFC', fontSize: 14, fontWeight: '600' },
list: { flex: 1 },
});
14 changes: 14 additions & 0 deletions example/react-native/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type { AccountSelectorInitialTargetInput } from './pages/nativeListAccoun
import { NativeListNetworkSelectorPage } from './pages/NativeListNetworkSelectorPage';
import { NativeListTokenSelectorPage } from './pages/NativeListTokenSelectorPage';
import { NativeListWalletSidebarReorderPage } from './pages/NativeListWalletSidebarReorderPage';
import { NativeListRowStylePage } from './pages/NativeListRowStylePage';
import type { MarketSearchParams } from './pages/MarketNativePagerExamplePage';
import {
NativeListExamplePage,
Expand Down Expand Up @@ -87,6 +88,7 @@ export type RootStackParamList = {
NativeListNetworkSelector: undefined;
NativeListTokenSelector: undefined;
NativeListWalletSidebarReorder: undefined;
NativeListRowStyle: undefined;
MarketNativePager: undefined;
MarketSearch: MarketSearchParams;
OtaPipeline: undefined;
Expand Down Expand Up @@ -289,6 +291,13 @@ const modules: {
'OK-62492 recorded wallet sidebar for iOS wallet group drag reorder',
icon: '🗂️',
},
{
screen: 'NativeListRowStyle',
name: 'Native List Row Style',
description:
'Every template plain and styled, with a toggle for the unstyled regression check',
icon: '🎚️',
},
{
screen: 'PagerView',
name: 'Pager View',
Expand Down Expand Up @@ -686,6 +695,11 @@ export function AppNavigator() {
// The sidebar sits on the left edge; edge-swipe back would steal slow drags.
options={{ headerShown: false, gestureEnabled: false }}
/>
<Stack.Screen
name="NativeListRowStyle"
component={NativeListRowStylePage}
options={{ title: 'Native List Row Style' }}
/>
<Stack.Screen
name="PagerView"
component={PagerViewTestPage}
Expand Down
6 changes: 6 additions & 0 deletions native-views/react-native-native-list/docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ Business concepts such as asset, account, network, wallet, and settings map to
the same presentation templates instead of introducing business-specific row
types.

Each row also accepts a bounded `style`, whose keys name model fields rather than
views. [STYLE_SPEC.md](STYLE_SPEC.md) is the shared vocabulary: the design tokens,
the per-template style surface with each platform's current values, the template
isolation rules, and the cross-platform divergences that are registered rather
than fixed.

The wrapper validates and normalizes data before serializing it. A structural
change is one snapshot payload. Frequently changing fields use one batch patch
payload keyed by row key. Neither API performs one native call per row. The
Expand Down
4 changes: 4 additions & 0 deletions native-views/react-native-native-list/docs/STYLE_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ in `NativeListModels.kt` and `styleSlot` in `NativeListCell.swift`. §4 is the s
truth for both; the Kotlin copy is unit-tested, including a check that no template maps
two style keys onto one view.

The example application's **Native List Row Style** page renders every template twice,
plain and styled, behind a toggle. With the toggle off the page must match the build
from before the style existed — that is the regression check for all three platforms.

## 3. T1 — Design tokens

### 3.1 Color tokens
Expand Down