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 }} /> +