feat(native-list): add listStyle for separator and group chrome - #112
huhuanming wants to merge 1 commit into
Conversation
snapshot.listStyle carries the list chrome that sits outside a row: separator.inset, separator.color and groupCornerRadius, applied on Web, iOS and Android. The surface is deliberately small. It carries only what all three platforms can honour, and validateSnapshot rejects any other key rather than accepting one that some platform would quietly ignore. Pull to refresh is a system control on both native platforms and only Web draws its own indicator; the section index rail belongs to capabilities.sectionIndex and is three independent constant sets today; the reorder preview and count badge are drawn with platform-specific primitives, including a Canvas badge inside Android's dispatchDraw; and content padding and item spacing already live on layout, so repeating them here would give one value two homes. docs/STYLE_SPEC.md section 5 records all of that. Every value is absent by default and each platform keeps its own number as the fallback, so an untouched list renders exactly as before. Web needed care for that: an inset separator keeps the transparent border-bottom so the row's height does not change, and paints the visible line with an inset-inline-start overlay so it also follows RTL, which the border could not. Chrome lives outside the row payload, so a changed listStyle has to reach rows whose own content is unchanged. Android routes it through the existing theme rebind flag and iOS folds it into the themeChanged comparison and into both snapshot fast-path guards. The example page gains a listStyle section: a row with an inset separator and a two-row grouped card, both driven by the snapshot rather than by the rows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| usesSelectorSourceScale = next.items.any { it.usesSelectorSourceScale } | ||
| adapter.usesSelectorSourceScale = usesSelectorSourceScale | ||
| adapter.theme = next.theme | ||
| adapter.listStyle = next.listStyle |
There was a problem hiding this comment.
P2: [Android initial snapshots never pass listStyle to rows]
On the normal snapshot path, including first mount, the adapter receives theme, layout, and orientation but not next.listStyle. This assignment exists only in the stable-content fast path, so regular rows bind with a null style and Android does not render the configured separator inset/color or grouped-card radius; only the footer receives the style.
Assign adapter.listStyle = next.listStyle alongside adapter.theme = next.theme in the normal path before submitList, and add Android coverage for an initial snapshot with listStyle.
| private fun groupedBackground(position: String, color: Int) = GradientDrawable().apply { | ||
| setColor(color) | ||
| val radius = scaledDp(12f) | ||
| val radius = listStyle?.takeIf { it.has("groupCornerRadius") } |
There was a problem hiding this comment.
P2: [Android applies groupCornerRadius outside groupId cards]
When a snapshot sets groupCornerRadius, applySelectionState synthesizes single for wallet-sidebar, sized selector, and metric-card rows even when they have no groupId. groupedBackground then applies the configured radius to those rows. iOS and Web consume the serialized groupPosition, so this creates Android-only geometry changes outside the documented grouped-card surface.
Gate the custom radius on actual groupId membership (or pass an explicit grouped-card flag), while preserving each non-group template's existing radius, and cover these selector and metric-card variants.
Stacked on #111 → #110 → #109 → #107. This PR's diff is the chrome surface only.
Important
The iOS and Android halves are not compiled here (same constraint as #110). Swift parses cleanly; the Kotlin integration is review-only. Please build both apps before merging.
What
snapshot.listStyle— the list chrome that sits outside a row:Applied on Web, iOS and Android.
Why the surface is this small
I originally deferred
listStylebecause most chrome cannot be honoured on every platform. Rather than drop it or declare knobs that silently do nothing, the surface carries only what all three can actually do, andvalidateSnapshotrejects anything else:Excluded, with the reason recorded in
STYLE_SPEC.md§5:UIRefreshControl/SwipeRefreshLayoutare system controls; only Web draws its owncapabilities.sectionIndex, and is three independent constant sets todayCanvasinsidedispatchDraw, iOS has neitherlayout.contentPadding*/layout.itemSpacing; duplicating gives one value two homesDefaults are untouched
Every value is absent by default and each platform keeps its own number as the fallback — including the ones that disagree (separator inset is 60/12 on the native sides and 0 on Web; now registered in §6.2).
Web needed care to keep that true. An inset separator:
border-bottom, so the row's height does not move by a pixel;inset-inline-startoverlay, so it follows RTL — which the border-based separator never could.Chrome must reach unchanged rows
listStylelives outside the row payload, so a changed value has to rebind rows whose own content is identical. Android routes it through the existing theme-rebind flag; iOS folds it into thethemeChangedcomparison and into both snapshot fast-path guards, otherwise a chrome-only change would be swallowed by the controlled-selection path.Threading is by property (
cell.listStyle/rowView.listStyle) rather than a newbindparameter, so no existing call site changed shape. Wallet-group members and the fixed footer get it propagated.Example page
Gains a
listStylesection — a row with an inset separator and a two-row grouped card, both driven by the snapshot rather than by the rows. The existing toggle covers it, so with the style off the chrome falls back to today's rendering.Verification
jest— 5 suites, 80 tests (was 78), passtsc -b— cleaneslint— cleanswiftc -parseon all three modified Swift files — cleanWhat to check on device
🤖 Generated with Claude Code