Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion doc/layout/grid.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

<a name="responsive"></a>
## Responsive
Expand Down
12 changes: 7 additions & 5 deletions lib/src/widgets/w_div.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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));
}

Expand Down
46 changes: 40 additions & 6 deletions lib/src/widgets/wind_equal_height_row.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Comment on lines +123 to 126
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);
Expand Down
4 changes: 2 additions & 2 deletions skills/wind-ui/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
---

<!-- fluttersdk_wind 1.1.x | Skill v2.8.1 (2026-07-01) -->
<!-- fluttersdk_wind 1.1.x | Skill v2.8.2 (2026-07-01) -->

# Wind UI 1.1

Expand Down
2 changes: 1 addition & 1 deletion skills/wind-ui/references/tokens.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]`) |
Expand Down
32 changes: 32 additions & 0 deletions test/widgets/w_div/grid_stretch_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
}