Responsive breakpoints: md:/lg:/xl: classes resolved against the live window width - #467
Conversation
sm:/md:/lg:/xl:/2xl: (plus medium:/expanded: for the Apple and Material window size classes) parse like Tailwind, mobile-first, and compose with ios:/android:/dark: in either order. PHP can't resolve them — it doesn't know the window width, and on iPad it changes under a running screen — so each breakpoint's classes ship as a layout/style/prop delta in one _variants JSON prop (the packed node struct is fixed; props are the only extensible slot). The native NodeView on both platforms folds the deltas over the base at decode time and picks the widest fit against the live window width, resolving one level of children too so containers see the same layout their child will render with. Re-flows on rotation and Split View with no PHP round-trip; nodes without variants pay one branch. grid-cols-N drives the lazy grid's column count from a class so md:grid-cols-2 lg:grid-cols-3 re-tracks it per breakpoint. Thresholds are configurable via nativephp.breakpoints. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNpmttr74cEGMszwgHfR61
A breakpoint's classes were built into layout / style / props on their own, so any value the builders assemble from sibling keys came out wrong: `p-4 md:px-8` shipped [0, 32, 0, 32] and lost the vertical padding, `rounded-xl md:rounded-t-3xl` squared the bottom corners, and `md:border-2` vanished without a colour. Each entry is now the difference between two complete builds of the node (everything narrower, then that plus this breakpoint) through a throwaway element of the node's type, so a prefixed class reaches the wire exactly as the unprefixed one does. That also carries the element props the old hand-kept map dropped: tracking, leading, text transform, italic, underline, font family, object fit. `dark:md:` now parses to the same shape as `md:dark:` instead of burying the breakpoint under `dark`, where nothing read it. `hidden` and the display utilities map to display, so `hidden md:flex` works, and `text-7xl` to `text-9xl` join the font scale. On iOS, rotating or resizing the window left text wrapped at the old width. FlexContainer's size memo outlives a layout pass (a resize never triggers updateCache), and a hit leaves `idealSize` at whatever the last real measurement set, so a row measured `.unspecified` after a resize hit the cache and was placed at its old shrunk width. The root renderer now bumps a measurement generation when the window width changes and the memo empties on the next call. This predates breakpoints; they just make resizing matter. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`<native:column hidden>` is documented under Visibility but never reached the wire — nothing read the attribute. It now sets display none in both layout paths, winning over any display class the way the HTML attribute does, so `:hidden="$collapsed"` works alongside `hidden md:flex`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Apple's size classes are compact and regular; medium and expanded are Material's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
|
We tested the FlexContainer counter on its own on an iPad Pro 13" sim: main, this branch, and this branch with only the counter removed. After rotating, every screen matched a fresh launch pixel for pixel in all three builds, with and without breakpoints. We couldn't reproduce the bug on main and found no value in the counter. |
A child with xl:hidden inside a row showed up on iOS as an empty bar in the hidden child's background colour, covering its visible sibling. Plain `hidden` did the same with no breakpoint involved, so this is not a variant resolution problem. FlexContainer already reads the resolved layout and rebuilds its cache when the width crosses a breakpoint. The cause is how display none was drawn. FlexContainer leaves a hidden child out of flow and never places it, so SwiftUI centres it at the container's size. The only thing hiding it was an opacity(0) inside NodeLayoutModifier. NodeStyleModifier adds the background and border after that, outside the opacity, so they stayed visible and painted over the siblings. The opacity now sits at the end of ResolvedNodeView so it covers the style layer too. Android skipped hidden flow children already, so the row case never happened there. Hidden absolute children were still composed, and their background showed at their fixed size because nodeLayout's size(0) and alpha(0) sit inside nodeStyle. The absolute loop in FlexContainer now skips them like the flow loops do. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # resources/xcode/NativePHP/NativeRender/FlexContainer.swift
|
Pushed two commits to this branch.
|
Why
EDGE screens had no way to adapt to window width. On an iPad, and on Android tablets and foldables, a screen laid out for a phone stayed a phone layout, and "responsive" meant branching in PHP, which can't see the window and misses rotation and Split View.
What
Breakpoint prefixes
Tailwind's mobile-first prefixes, plus Material's window size classes:
sm:medium:md:expanded:lg:xl:2xl:_variantsJSON prop (the packed node struct is fixed, so props are the only extensible slot).NodeViewon both platforms picks the widest breakpoint that fits the live window width, so a screen re-flows on rotation and Split View with no PHP round trip. Nodes without breakpoints pay one branch.p-4 md:px-8keeps its vertical padding,rounded-xl md:rounded-t-3xlkeeps its bottom corners, and element props (tracking, leading, font family, object fit, grid columns) behave exactly as they do unprefixed. The throwaway build uses its own callback registry, so screen callback ids don't shift.md:dark:,dark:md:,ios:md:.nativephp.breakpointstable, which also allows custom names ('tablet' => 700).max-*:andmin-[…]:are not supported.Also in this PR
hidden, plusflex/inline-flex/block/inline-block/gridto show an element again, for thehidden md:flexpattern.hiddenattribute now works.<native:column hidden>was documented but never reached the wire.grid-cols-Nsets a lazy grid's column count. An explicit:columnsstill wins.text-7xltotext-9xl.FlexContainer: re-measure after a resize (affects every app)Worth a close look, because it changes behaviour outside breakpoints. Rotating or resizing the window on iOS left text wrapped at the old width until the next launch.
FlexContainer'ssizeCacheoutlives a layout pass (a resize doesn't triggerupdateCache), and a cache hit leaves each child'sidealSizeat whatever the last real measurement set. After a resize, a row measured.unspecifiedhits the cache, andplaceSubviewslays its text out at the shrunk width from before.NativeTreeRenderernow bumps a staticmeasurementGenerationwhen the window width changes, and each container empties its memo on its next call. This means one extra measurement pass per resize and nothing otherwise.This bug is on
mainalready. Breakpoints just make resizing common. Android doesn't have it.Testing
pint,phpstan, and the full Pest suite: 1018 passing.tests/Unit/Edge/ResponsiveVariantsTest.phpcovers parsing, the wire format, building on the base, prefix order, display, element props, callback isolation and thehiddenattribute.lg) and landscape at 1376pt (xl, includingxl:text-7xl), rotated portrait → landscape → portrait with no relaunch. The round trip matches a fresh launch in each orientation, which is how theFlexContainerfix was confirmed.sm) and landscape at 960dp (smandmd), rotated both ways in one process.hidden md:flex, per-corner radius, border width andgrid-colsbehind a breakpoint.Notes
NativeUINode.kt,SwiftUINodeRenderer.swift,NativeElementCollector.php), Shared-element view transitions (ref+@navigate.viewTransition) #372 (NodeView) and fix: Android crashes when a positional node ID changes type #436 (NodeView.kt). I'll rebase against whichever lands first.NodeView, which resolves breakpoints before any renderer sees the node.🤖 Generated with Claude Code