ReviewOS

also looking at this

stacks/stx

fix(native): preserve explicit grid row tracks

#2092
Merged chris wants to merge codex/native-explicit-grid-rows into main
5 files +42 -4

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

packages/stx/src/native/compiler/inline-style.tsmodified+2-2
Changes to packages/stx/src/native/compiler/inline-style.ts
@@ -2,7 +2,7 @@ import type { STXStyle } from './ir'
22
33/** Every property `STXStyle` declares, for inline `style=""` translation. */
44const STYLE_KEYS = new Set([
5 'display', 'gridColumns', 'gridTemplateColumns', 'gridAutoRows',
5 'display', 'gridColumns', 'gridTemplateColumns', 'gridTemplateRows', 'gridAutoRows',
66 'flex', 'flexGrow', 'flexShrink', 'flexBasis', 'flexDirection', 'flexWrap',
77 'justifyContent', 'alignItems', 'alignSelf', 'alignContent', 'position', 'top', 'right',
88 'bottom', 'left', 'zIndex', 'width', 'height', 'minWidth', 'maxWidth', 'minHeight',
@@ -161,7 +161,7 @@ export function parseNativeStyle(input: unknown, report: (diagnostic: NativeStyl
161161 translated = undefined
162162 }
163163 else if (property === 'shadowOffset' || property === 'transform') translated = structuredStyle(property, value)
164 else if (property === 'gridTemplateColumns') translated = finite(value) || typeof value === 'string' ? value : undefined
164 else if (property === 'gridTemplateColumns' || property === 'gridTemplateRows') translated = finite(value) || typeof value === 'string' ? value : undefined
165165 // Font weights are strings in the IR, even when authored as object numbers.
166166 else if (property === 'fontWeight' && finite(value)) translated = String(value)
167167 else if (typeof value === 'string') translated = value
packages/stx/src/native/compiler/ir.tsmodified+2-0
Changes to packages/stx/src/native/compiler/ir.ts
@@ -149,6 +149,8 @@ export interface STXStyle {
149149 gridColumns?: number
150150 /** CSS-shaped grid tracks understood by the maintained Craft hosts. */
151151 gridTemplateColumns?: string | number
152 /** Explicit equal rows within a definite native grid height. */
153 gridTemplateRows?: string | number
152154 /** Fixed native row height for grid layouts. */
153155 gridAutoRows?: number
154156 flex?: number
packages/stx/test/native/compiler-ir.test.tsmodified+2-2
Changes to packages/stx/test/native/compiler-ir.test.ts
@@ -407,7 +407,7 @@ describe('style vocabulary coverage', () => {
407407
408408 /**
409409 * Emitted by the compiler and absent from the iOS mirror, so dropped in
410 * silence. This list must only shrink.
410 * silence. Non-grid gaps must only shrink; grid features target Craft.
411411 */
412412 const IOS_DROPS = [
413413 'alignContent', 'aspectRatio', 'backgroundImage', 'borderBottomColor',
@@ -416,7 +416,7 @@ describe('style vocabulary coverage', () => {
416416 'columnGap', 'elevation', 'flexBasis', 'rowGap', 'transform',
417417 // Craft's native host lays out `display: grid` with `gridColumns`; this
418418 // older renderer has no grid (added with grid-cols-N, 2026-10).
419 'gridAutoRows', 'gridColumns', 'gridTemplateColumns',
419 'gridAutoRows', 'gridColumns', 'gridTemplateColumns', 'gridTemplateRows',
420420 ]
421421
422422 it('iOS drops exactly the properties on the known list, and no more', () => {
packages/stx/test/native/inline-style.test.tsmodified+15-0
Changes to packages/stx/test/native/inline-style.test.ts
@@ -16,6 +16,21 @@ describe('shared native inline-style normalization', () => {
1616 expect(parseNativeStyle({ fontWeight: 700 })).toEqual({ fontWeight: '700' })
1717 })
1818
19 it('preserves explicit grid row tracks in object, serialized and CSS styles', async () => {
20 for (const rows of ['repeat(2, 1fr)', '1fr 1fr', 2]) {
21 const expected = { display: 'grid', gridTemplateRows: rows }
22 for (const input of [expected, JSON.stringify(expected)]) {
23 const diagnostics: NativeStyleDiagnostic[] = []
24 expect(parseNativeStyle(input, d => diagnostics.push(d))).toEqual(expected)
25 expect(diagnostics).toEqual([])
26 }
27 }
28 const css = 'display:grid;grid-template-rows:repeat(2, 1fr)'
29 expect(parseNativeStyle(css)).toEqual({ display: 'grid', gridTemplateRows: 'repeat(2, 1fr)' })
30 expect((await translateHtmlToIR(`<div style="${css}"><span>row</span></div>`)).root.style)
31 .toEqual({ display: 'grid', gridTemplateRows: 'repeat(2, 1fr)' })
32 })
33
1934 it('does not corrupt quoted font names or semicolons in CSS function values', () => {
2035 const image = 'url("data:image/svg+xml;base64,abc")'
2136 expect(parseNativeStyle(`background-image:${image};font-family:"A;B";padding:8px`))
packages/stx/test/native/reactive-inline-style.test.tsmodified+21-0
Changes to packages/stx/test/native/reactive-inline-style.test.ts
@@ -2,6 +2,27 @@ import { describe, expect, it } from 'bun:test'
22import { compileNativeScreen, mountNativeScreen } from '../../src/native/testing'
33
44describe.each([false, true])('reactive native inline styles (minify=%s)', (minify) => {
5 it('delivers explicit grid rows through the compiled native bundle and clears changed tracks', async () => {
6 const { code } = await compileNativeScreen(`<script client>
7const tracks = state({ gridTemplateRows: 'repeat(2, 1fr)' })
8</script><View testID="grid" style="display:grid;height:84px" :style="tracks">
9 <View testID="static-grid" style={{"display":"grid","gridTemplateRows":"repeat(2, 1fr)","height":84}} />
10 <Button testID="change" @click="tracks.set({gridTemplateRows:3})">Change rows</Button>
11 <Button testID="clear" @click="tracks.set({})">Clear rows</Button>
12</View>`, { minify })
13 const phone = mountNativeScreen(code, 'Screen')
14 await phone.settle()
15 expect(phone.byTestID('static-grid')!.style).toMatchObject({ display: 'grid', height: 84, gridTemplateRows: 'repeat(2, 1fr)' })
16 expect(phone.byTestID('grid')!.style).toMatchObject({ display: 'grid', height: 84, gridTemplateRows: 'repeat(2, 1fr)' })
17 phone.press(phone.byTestID('change')!)
18 await phone.settle()
19 expect(phone.byTestID('grid')!.style.gridTemplateRows).toBe(3)
20 phone.press(phone.byTestID('clear')!)
21 await phone.settle()
22 expect(phone.byTestID('grid')!.style.gridTemplateRows).toBeNull()
23 phone.unmount()
24 }, 30_000)
25
526 it('normalizes CSS and objects, clears previous values, and protects hidden nodes', async () => {
627 const { code } = await compileNativeScreen(`<script client>
728const styles = state('padding-left:12px;font-size:1.5rem;width:50%;font-weight:700')