From 8d04d9132bc333211ab11d9780b7360ae69b1187 Mon Sep 17 00:00:00 2001 From: huhuanming Date: Wed, 16 Sep 2026 09:45:43 +0800 Subject: [PATCH] feat(example): add the Native List Row Style page The row style now exists on all three platforms but only the Web half has automated coverage, so the native halves need something to check against. This page is that artifact. Every template is rendered twice - plain, then styled - behind a toggle. With the toggle off the whole page must match the build from before the row style existed, which is the regression check that matters most: unstyled rows must not move by a pixel. The rows deliberately cover what the field-to-view mapping is most likely to get wrong. metricCard styles `value` and `title` separately, and the two must land on the large number and the small label respectively even though they share views with identity's title and subtitle. message styles `body` and `time`, which are carried by the subtitle and status views. One identity row grows its text and gives an explicit height, because heights are not derived from the style. Also link STYLE_SPEC.md from DESIGN.md, so the architecture document points at the style vocabulary. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 1 + .../pages/NativeListRowStylePage.tsx | 247 ++++++++++++++++++ example/react-native/route.tsx | 14 + .../react-native-native-list/docs/DESIGN.md | 6 + .../docs/STYLE_SPEC.md | 4 + 5 files changed, 272 insertions(+) create mode 100644 example/react-native/pages/NativeListRowStylePage.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 8204f275..32779e44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/example/react-native/pages/NativeListRowStylePage.tsx b/example/react-native/pages/NativeListRowStylePage.tsx new file mode 100644 index 00000000..a21966fa --- /dev/null +++ b/example/react-native/pages/NativeListRowStylePage.tsx @@ -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[] { + 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( + () => ({ + schemaVersion: 1, + generation: styled ? 2 : 1, + layout: { kind: 'sectioned', stickyHeaders: true, contentPadding: 8 }, + theme: THEME, + rows: buildRows(styled), + }), + [styled], + ); + + return ( + + + + Every template appears twice: plain, then styled. Turn the style off + and the page should match the build before the row style existed. + + setStyled(current => !current)} + style={({ pressed }) => [styles.button, pressed && styles.pressed]} + > + + {styled ? 'Style: on' : 'Style: off'} + + + + + + ); +} + +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 }, +}); diff --git a/example/react-native/route.tsx b/example/react-native/route.tsx index d1a0b8f3..6556159c 100644 --- a/example/react-native/route.tsx +++ b/example/react-native/route.tsx @@ -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, @@ -87,6 +88,7 @@ export type RootStackParamList = { NativeListNetworkSelector: undefined; NativeListTokenSelector: undefined; NativeListWalletSidebarReorder: undefined; + NativeListRowStyle: undefined; MarketNativePager: undefined; MarketSearch: MarketSearchParams; OtaPipeline: undefined; @@ -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', @@ -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 }} /> +