diff --git a/CHANGELOG.md b/CHANGELOG.md index 54957ae..3f28a0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ This project follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0. ### Fixed +- `grid ... items-stretch` no longer emits a residual `RenderFlex overflowed by ~2px on the bottom` when it stretches an unequal cell (follow-up to #139). `WindEqualHeightRow` previously re-laid each cell to a TIGHT height equal to the loose-measured row max; a cell whose `flex flex-col` content needs a hair more under the tight re-lay (sub-pixel text/flex rounding on real rendering, e.g. CanvasKit) overflowed by a couple of pixels. It now stretches with a MIN height instead (never a tight squeeze), so a cell is never forced below its own content and the row takes the tallest resulting height — overflow-free by construction. (`lib/src/widgets/wind_equal_height_row.dart`) (#141) + - `grid ... items-stretch` no longer asserts `RenderBox was not laid out` (rooted at `RenderIntrinsicHeight` / `LayoutBuilder does not support returning intrinsic dimensions`) when it stretches an unequal `flex flex-col` cell. The equal-height mechanism from #126 used `IntrinsicHeight`, which queries child intrinsics; a `flex flex-col` cell (or one using `h-full` / `basis-*`) carries a `LayoutBuilder` that cannot answer intrinsic queries, so stretching a shorter cell crashed the exact case the feature exists for. Replaced it with `WindEqualHeightRow`, a `RenderObject` that measures each cell with a real loose-height layout and re-lays it tight to the row max, so LayoutBuilder-bearing cells stretch correctly. As a bonus, `h-full` on a stretched cell now resolves too. (`lib/src/widgets/wind_equal_height_row.dart`, `lib/src/widgets/w_div.dart`) (#139) - Alias expansion now resolves state/breakpoint-prefixed alias tokens instead of silently dropping them. A prefixed token (`hover:bg-surface-container`, `md:row`, `dark:hover:row`) had its whole form rejected before the alias lookup, so `prefix:` + a known alias key was a silent no-op while plain `bg-surface-container` worked. The expander now peels the prefix chain off, matches the bare body against the (still bare-only) alias map, and re-applies the prefix to every produced token (`md:row` -> `md:flex md:flex-row`; `hover:bg-surface` carries `hover:` onto each token including the value's own `dark:` peer, yielding `hover:dark:...`, which the parser resolves regardless of prefix order). Cycle/depth/output guards are unchanged and now key on the bare body. (`lib/src/parser/alias_expander.dart`) (#124) diff --git a/doc/layout/grid.md b/doc/layout/grid.md index bd47933..edb86ff 100644 --- a/doc/layout/grid.md +++ b/doc/layout/grid.md @@ -79,7 +79,7 @@ WDiv( ) ``` -The equal-height rows are laid out for real (each cell is measured with a loose height, then re-laid tight to the row's tallest), NOT via `IntrinsicHeight`, so cells whose content is itself a `flex flex-col`, or that use `h-full` / `basis-*` (which carry a `LayoutBuilder`), stretch correctly instead of asserting `LayoutBuilder does not support returning intrinsic dimensions`. Cells simply fill the row height. +The equal-height rows are laid out for real (each cell is measured with a loose height, then re-laid to at least the row's tallest via a **min** height, never a tight squeeze), NOT via `IntrinsicHeight`, so cells whose content is itself a `flex flex-col`, or that use `h-full` / `basis-*` (which carry a `LayoutBuilder`), stretch correctly instead of asserting `LayoutBuilder does not support returning intrinsic dimensions`. Because a cell is never forced below its own content height, a stretched cell also produces no residual `RenderFlex overflowed` warning (#141). ## Responsive diff --git a/lib/src/widgets/w_div.dart b/lib/src/widgets/w_div.dart index 472cf88..ee9d1b9 100644 --- a/lib/src/widgets/w_div.dart +++ b/lib/src/widgets/w_div.dart @@ -1160,7 +1160,8 @@ class WDiv extends StatelessWidget { /// /// A `Column` of [WindEqualHeightRow]s, `cols` cells per row. Each row lays /// its cells out for real (a loose-height pass to measure the tallest, then a - /// tight re-layout to that height) and gives each an equal width share, so + /// re-layout to at least that height via a min constraint) and gives each an + /// equal width share, so /// every cell matches the row's tallest. The last row is padded with blank /// cells so columns stay aligned. /// @@ -1196,10 +1197,11 @@ class WDiv extends StatelessWidget { rows.add(SizedBox(height: gapY)); } // WindEqualHeightRow measures each cell with a real (loose-height) layout - // and re-lays it tight to the row max, instead of IntrinsicHeight's - // intrinsic query. A `flex flex-col` cell (which carries a LayoutBuilder) - // can then be stretched without the "LayoutBuilder does not support - // returning intrinsic dimensions" assert (#139). + // and re-lays it to at least the row max via a MIN height, instead of the + // intrinsic query IntrinsicHeight would run. A `flex flex-col` cell (which + // carries a LayoutBuilder) can then be stretched without the "LayoutBuilder + // does not support returning intrinsic dimensions" assert (#139), and the + // min (never tight) height leaves no residual RenderFlex overflow (#141). rows.add(WindEqualHeightRow(spacing: gapX, children: rowChildren)); } diff --git a/lib/src/widgets/wind_equal_height_row.dart b/lib/src/widgets/wind_equal_height_row.dart index 84e1151..275c121 100644 --- a/lib/src/widgets/wind_equal_height_row.dart +++ b/lib/src/widgets/wind_equal_height_row.dart @@ -11,9 +11,12 @@ import 'package:flutter/widgets.dart'; /// dry-layout queries, so `IntrinsicHeight` asserts `LayoutBuilder does not /// support returning intrinsic dimensions` the moment it has to stretch an /// unequal cell (issue #139). This widget instead lays each child out for real -/// with a loose height to measure it, then lays it out again tight to the row's -/// max height. Real layout is exactly what `LayoutBuilder` supports, so a -/// `flex flex-col` cell stretches without asserting. +/// with a loose height to measure it, then lays it out again to the row's max +/// height via a MIN constraint (never a tight one). Real layout is exactly what +/// `LayoutBuilder` supports, so a `flex flex-col` cell stretches without +/// asserting; and because a cell is never forced BELOW its own content height, a +/// stretched cell leaves no residual `RenderFlex overflowed` warning the way a +/// tight re-lay could on fractional (sub-pixel) content (issue #141). /// /// Every child is given an equal share of the incoming width (`(maxWidth - /// spacing * (n - 1)) / n`), matching the grid's fixed column count, so callers @@ -107,15 +110,46 @@ class _RenderEqualHeightRow extends RenderBox // Honor the incoming height constraint: if the parent forces a taller (or // caps at a shorter) row, cells stretch to that height, not just the // measured tallest, so the row and its cells stay consistent. - final double rowHeight = + final double target = maxHeight.clamp(constraints.minHeight, constraints.maxHeight); - // Pass 2: stretch every cell to the row height and position left to right. + // Pass 2: re-lay each cell with a MIN height of `target` (never a tight + // height) to settle the true row height. A tight height would squeeze a + // cell whose content, laid out for real, needs a hair more than the + // loose-measured max (sub-pixel text/flex rounding), producing a + // "RenderFlex overflowed by ~2px" warning (#141). A min constraint instead + // lets such a cell keep its own (larger) height, so `rowHeight` ends up >= + // every cell's content and no cell is ever squeezed. + double rowHeight = target; + child = firstChild; + while (child != null) { + child.layout( + BoxConstraints( + minWidth: cellWidth, + maxWidth: cellWidth, + minHeight: target, + maxHeight: constraints.maxHeight, + ), + parentUsesSize: true, + ); + if (child.size.height > rowHeight) rowHeight = child.size.height; + child = childAfter(child); + } + rowHeight = rowHeight.clamp(constraints.minHeight, constraints.maxHeight); + + // Pass 3: lay every cell to the settled `rowHeight` (still a MIN height, so + // no squeeze) and position it. This equalizes any cell that measured shorter + // than a sibling which grew in pass 2, so all cells share the row height. double dx = 0; child = firstChild; while (child != null) { child.layout( - BoxConstraints.tightFor(width: cellWidth, height: rowHeight), + BoxConstraints( + minWidth: cellWidth, + maxWidth: cellWidth, + minHeight: rowHeight, + maxHeight: constraints.maxHeight, + ), parentUsesSize: true, ); (child.parentData as _EqualHeightParentData).offset = Offset(dx, 0); diff --git a/skills/wind-ui/SKILL.md b/skills/wind-ui/SKILL.md index 61eaf5b..9eea9e7 100644 --- a/skills/wind-ui/SKILL.md +++ b/skills/wind-ui/SKILL.md @@ -3,10 +3,10 @@ name: wind-ui description: "fluttersdk_wind 1.1: utility-first Flutter styling with Tailwind-syntax className strings. 27 public widgets (WDiv, WText, WButton, WInput, WSelect, WCheckbox, WDatePicker, WPopover, WAnchor, WIcon, WImage, WSvg, WSpacer, WBreakpoint, WDynamic, WKeyboardActions, WindAnimationWrapper, WBadge, WCard, WSwitch, WRadio, WTabs + 5 WForm* wrappers) consume className through a 20-parser pipeline (20 implementation files organized into 12 token families for teaching) that emits a cached immutable WindStyle. WindRecipe / WindSlotRecipe compose className variants (base + axes + compoundVariants + caller) in strict emission order, no dedupe/sort/twMerge. Prefixes stack freely (dark: / hover: / focus: / md: / lg: / ios: / android: / web: / mobile: / selected: / loading: / disabled: / readonly: / error: / checked: / custom). Last class wins; unknown tokens fail silently. Every color token (bg-, text-, border-, ring-, shadow-, fill-) needs a dark: pair in the same className. TRIGGER when: writing or editing any UI in a Flutter app that depends on `fluttersdk_wind`; any className string; any W-prefix widget; any WindTheme / WindThemeData reference; the user mentions Tailwind for Flutter, utility-first, className, or wind-ui. DO NOT TRIGGER when: backend / API / state-management work that does not touch a widget tree; Flutter projects that do not have fluttersdk_wind in pubspec.yaml; Material-only widgets (Scaffold, AppBar, Dialog) without Wind content inside them." when_to_use: | Any task that produces, modifies, or audits Wind-styled Flutter UI: composing a className string, picking the right W-widget for a use case, integrating with a Form / FormField, customizing WindThemeData, wiring dark-mode pairs, debugging an unexpected layout, recovering from RenderFlex overflow, building a popover or dropdown, rendering a JSON tree via WDynamic, wiring Wind.installDebugResolver for kDebugMode tooling, migrating a Tailwind className from web, or composing a WindRecipe / WindSlotRecipe for a variant-driven component. Apply BEFORE writing the first line of UI in a Wind-using file, not as an audit pass. -version: 2.8.1 +version: 2.8.2 --- - + # Wind UI 1.1 diff --git a/skills/wind-ui/references/tokens.md b/skills/wind-ui/references/tokens.md index 35a9967..78bcbef 100644 --- a/skills/wind-ui/references/tokens.md +++ b/skills/wind-ui/references/tokens.md @@ -79,7 +79,7 @@ Inline color escape hatches that bypass the cache key: | `align-content-start` / `-end` / `-center` / `-between` / `-around` / `-evenly` / `-stretch` | Wrap-only, `WrapAlignment` for runs | | `align-self-start` / `-end` / `-center` / `-stretch` / `-auto` (or the `self-*` shorthand) | Per-child cross-axis override | | `axis-min` / `axis-max` | Wind-only: `MainAxisSize.min` / `.max` on the parent flex | -| `grid-cols-N` | N columns (any integer); renders as `Wrap` with computed column widths. Add `items-stretch` for equal-height rows: each row is a real-layout equal-height row (measures each cell, re-lays tight to the tallest), NOT `IntrinsicHeight`, so cells containing a `flex flex-col`, `h-full`, or `basis-*` (LayoutBuilder-bearing) stretch without asserting (#139) | +| `grid-cols-N` | N columns (any integer); renders as `Wrap` with computed column widths. Add `items-stretch` for equal-height rows: each row is a real-layout equal-height row (measures each cell, re-lays to at least the tallest via a MIN height, never a tight squeeze), NOT `IntrinsicHeight`, so cells containing a `flex flex-col`, `h-full`, or `basis-*` (LayoutBuilder-bearing) stretch without asserting or overflowing (#139, #141) | | `order-0` through `order-12` | Child order index | | `order-first` / `order-last` / `order-none` | Sentinel order (first=-9999, last=9999, none=0) | | `order-[N]` | Arbitrary signed integer (e.g. `order-[-5]`) | diff --git a/test/widgets/w_div/grid_stretch_test.dart b/test/widgets/w_div/grid_stretch_test.dart index 003f891..d1f9701 100644 --- a/test/widgets/w_div/grid_stretch_test.dart +++ b/test/widgets/w_div/grid_stretch_test.dart @@ -164,5 +164,37 @@ void main() { cellOf(tester, 'Revenue').height, cellOf(tester, 'Signups').height); expect(cellOf(tester, 'Revenue').height, greaterThan(0)); }); + + testWidgets( + 'stretching an unequal flex-col cell emits no overflow warning (#141)', + (tester) async { + // Follow-up to #139: the stretch must not leave a residual RenderFlex + // overflow on the stretched row. WindEqualHeightRow uses a min-height + // (never a tight squeeze), so a stretched cell's content is never forced + // below its own height. + await tester.pumpWidget( + wrapWithTheme( + WDiv( + className: 'grid grid-cols-2 gap-4 items-stretch', + children: [ + // taller (3 rows) + WDiv( + className: 'flex flex-col gap-1', + children: const [WText('A'), WText('1'), WText('note')], + ), + // shorter (2 rows) -> stretched to the taller + WDiv( + className: 'flex flex-col gap-1', + children: const [WText('B'), WText('2')], + ), + ], + ), + ), + ); + + // No "RenderFlex overflowed" (or any) exception, and heights match. + expect(tester.takeException(), isNull); + expect(cellOf(tester, 'A').height, cellOf(tester, 'B').height); + }); }); }