feat(native-list): unify template spec and cross-platform row styles - #128
Draft
huhuanming wants to merge 42 commits into
Draft
huhuanming wants to merge 42 commits into
huhuanming wants to merge 42 commits into
Conversation
Add docs/STYLE_SPEC.md as the style vocabulary for rows, section headers, fixed footers, and empty states. The three renderers cannot constrain each other, so the spec is authoritative by review plus the one boundary they share: the serialized snapshot. It records the design tokens aliased to the application's own token names, the per-template style surface keyed by model field rather than by view (metricCard renders its value through the title label, so view names mis-target), list chrome, the template isolation rules, and a review checklist. Cross-platform divergences are registered, not changed: the three row-height tables disagree (rail 40/28/40, sectionHeader summary 68/80/68), Android draws sticky headers through a second renderer with its own typography, section headers carrying a value or a checkbox silently lose stickiness on Android, and source scale is computed list-wide. Remove dead duplicated rules from WEB_LIST_CSS: four blocks were emitted twice and a prefers-reduced-motion block three times, and every property of the earlier copies was redeclared by the later ones, so removing them changes no rendering. The two oddities that look like duplication are load-bearing and are documented instead - the !important on the account action row overrides an inline radius, and the market change chip's literal colors do not equal their nearest tokens. Also apply prettier to NativeList.web.test.ts, which was left unformatted by d91262e and failed lint before this change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every template except market hard-codes its typography in three renderers, so
tuning one meant adding a presentation variant plus a branch per platform. Give
each template a bounded style instead.
A style key names the model field it modifies, never the view that carries it.
The view pool is shared and the mapping is not one to one: metricCard renders
its value through the label identity uses for its title, and one status view
carries rail.status, activity.status, message.time and metricCard.trend. So
metricCard.style.value reaches the large number, and identity.style.title the
title, through the same physical view.
Typography can be a named step from the application's scale
({ token: '$bodyLg' }). Tokens resolve to numbers in validation.ts before the
snapshot is serialized, so no native renderer learns the vocabulary and none can
drift from it; an explicit fontSize alongside a token still wins. Resolution is
idempotent and a snapshot with nothing to resolve is returned unchanged, so the
common path keeps object identity.
Isolation follows docs/STYLE_SPEC.md §7: style types are per template, and
validateSnapshot and validatePatches reject a key the row type does not declare,
so a style written for one template cannot reach a shared view through another.
Styles carried by a patch are validated and resolved the same way, because
patches reach the native side without passing through a snapshot.
Web applies it in renderElement after createRowBody. The element rendering a
field carries data-nl-slot, because .ok-native-list-secondary alone is the
identity subtitle, the rail status, a metric label, a data column's secondary
text and a system message. Text slots work for every template, along with
horizontalPadding, verticalPadding and lineGap. leadingGap, trailingGap,
titleBadgeGap and image stay validated but applied only on market: the others
need a per-template default gap that cannot be read back from the DOM.
Row heights are untouched. A styled row that grows still needs an explicit
height, as recorded in docs/STYLE_SPEC.md §6.1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both platforms gain applyRowStyle, running after the per-template binder so it is the last writer. Android must run after applySize, which re-dispatches font size and typeface by row type and would otherwise overwrite the style. A style key names a model field, and the view pool is shared, so the same field lands in a different view per template: metricCard draws its value through the view identity uses for its title, and one status view carries rail.status, activity.status, message.time and metricCard.trend. The mapping is nativeListStyleSlot in NativeListModels.kt and styleSlot in NativeListCell.swift, both following docs/STYLE_SPEC.md section 4. The Kotlin copy is unit-tested, including a check that no template maps two style keys onto one view - a collision would make one of them silently win. Both platforms also gain resetRowStyle, running before the binder, because neither reset path is complete (STYLE_SPEC section 7 rule 2). Android's resetViews restores visibility, gravity, maxLines, layout params, background and padding but not textSize, typeface or lineHeight. iOS's reset restores the fonts of the shared labels but not those of the data, metric and media labels, nor any text alignment. Without the reset a style would leak into the next row that reuses the view. Both resets are guarded by a dirty flag, so an unstyled list pays nothing. On iOS the pass rebuilds the attributed string rather than assigning font and textColor: the binders install an attributed string through setLineHeight, so a plain font assignment would not take effect. The rebuilt line box centers font metrics inside an explicit lineHeight, matching React Native and the Web engine. Deliberately unchanged, and recorded in the spec's status table: - Row heights. A styled row that grows still needs an explicit height. - The list-wide source scale on Android. Making it per-row would move every selector list's metrics and needs device verification. - dataRow columns in the table layout, whose text lives inside NativeListTableColumnView; the linear layout is covered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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>
… boundaries Document all 12 RowModel types with structural SVG diagrams, local style layout constraints, and shared container ownership for headers, footers, sections, scrolling and the indexed bar. Record source-verified implementation gaps from the consolidated PR stack. Validation: 83 NativeList tests; package and focused example/document TypeScript checks; package and focused example lint; Swift syntax parsing; document link and SVG checks plus offline visual inspection. Native SDK builds and device acceptance remain pending.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
NativeList accepted local styles that some renderers ignored or applied to the wrong fields. This PR consolidates the five specification PRs, implements a shared configurable row-style contract, completes the Message pilot, and starts stage 3 with a dedicated Rail renderer on all three platforms.
row.stylecustomizes existing fields. Header/footer placement, sections, scrolling and indexed bar remain common list capabilities.style.container.height→row.height→ template/layout measurement. Clearing styles restores the original model/default behavior.docs/MIGRATION_BASELINE.md, including each rule's platform scope and migration disposition. Keep existing compatibility policies explicit until their callers migrate.offsetYproducing NaN and invisible text.Consolidates the now-closed #107, #109, #110, #111 and #112. Main is merged through
52849b86f.Boundaries
Integrators own content fitting and overflow; there is no automatic shrinking or layout correction. Styles cannot replace template structure. No arbitrary React header/footer slot or public renderer/plugin API is added. Existing unstyled platform differences are inventoried separately from configurable-property parity. Image geometry/content-fit changes may require the image module to decode/request again; text-only updates retain unchanged effective requests.
The six-stage plan in
docs/SPEC.mdmarks stages 1 and 2 complete. Stage 3 is in progress: Rail is complete (1/7); mediaTile, action, system, activity, dataRow and metricCard remain. Stages 4–6 are not started. Those templates still use legacy hosts. No allocation/scrolling-performance benchmark is claimed.Validation
compileDebugKotlinandtestDebugUnitTestpass (seven tests, three suites). The isolated app runs the real NativeList/Image/Logger/Skeleton/Nitro packages on a dedicated external-drive API 36 emulator.git diff --checkpasses.Full native acceptance of later templates (including long/CJK/RTL content, pinned-header interactions and WalletGroup reorder) remains part of their migration. Earlier sampled gallery checks do not replace that acceptance. This PR remains draft.