From 7dcf77cf39ac9756843b058081413f621724d28a Mon Sep 17 00:00:00 2001 From: Anilcan Cakir Date: Tue, 9 Jun 2026 03:10:35 +0300 Subject: [PATCH 1/5] feat(w-div): smart column stretch covers WAnchor and WButton A clickable nav row is authored as WAnchor(onTap) > WDiv (WDiv carries no onTap), and a WButton is the canonical button surface. Previously _shouldStretchColumnChild only stretched direct WDiv children, so those wrapped rows rendered at content width instead of filling the column, diverging from CSS align-items: stretch where element type is irrelevant. Extend the eligibility gate: WButton is gated by its own className, WAnchor delegates to its effective child (inner WDiv className decides; WText or raw child always stretches). The existing _selfWrapsInFlex / _hasExplicitCrossWidth / absolute checks are reused on the inner WDiv, so an explicit-width or self-flexing child is still left untouched. Stretch stays a SizedBox(width: infinity) under the bounded-width LayoutBuilder guard; no CrossAxisAlignment.stretch is introduced. hasStretchTarget delegates to the same predicate, so the LayoutBuilder gate and the wrap broaden together. --- lib/src/widgets/w_div.dart | 32 +++- test/widgets/w_div/flex_stretch_test.dart | 211 ++++++++++++++++++++++ 2 files changed, 242 insertions(+), 1 deletion(-) diff --git a/lib/src/widgets/w_div.dart b/lib/src/widgets/w_div.dart index 5ba860d..2f2a7ee 100644 --- a/lib/src/widgets/w_div.dart +++ b/lib/src/widgets/w_div.dart @@ -7,6 +7,7 @@ import 'wind_animation_wrapper.dart'; import '../state/wind_anchor_state_provider.dart'; import '../state/wind_flex_overflow_scope.dart'; import 'w_anchor.dart'; +import 'w_button.dart'; import 'w_text.dart'; /// **The Fundamental Building Block of Wind** @@ -837,12 +838,41 @@ class WDiv extends StatelessWidget { /// effect, while breaking trees that locate sibling `SizedBox`es positionally /// (e.g. a `WSpacer` next to bare `WText`). Container children (`WDiv`) are /// the meaningful stretch targets. + /// + /// Interaction wrappers count too. A clickable row is authored as + /// `WAnchor(onTap: ...) > WDiv(...)` (WDiv carries no `onTap`), and a + /// `WButton` is the canonical button surface; CSS would stretch both to the + /// column width. So: + /// - `WButton` is eligible, gated by its own `className` exactly like a `WDiv` + /// (an explicit width / self-flex / `absolute` opts it out). + /// - `WAnchor` delegates its eligibility to its effective child: when the + /// child is a `WDiv`, the inner WDiv's `className` decides (a `WAnchor` + /// carries no `className`), so a `WAnchor > WDiv(w-32)` keeps 128px and a + /// `WAnchor > WDiv(grow)` stays a self-flexing child that must never be + /// wrapped in a stretch `SizedBox`; when the child is anything else (a + /// `WText`, a raw widget) there is no width to honor, so it is eligible. + /// + /// The inner-WDiv delegation is load-bearing for crash safety: a + /// `WAnchor > WDiv(grow)` self-wraps in `Expanded` during its own build, so + /// adding a `SizedBox(width: infinity)` stretch around it would compound the + /// flex wrap. `_selfWrapsInFlex` on the inner className excludes it. static bool _shouldStretchColumnChild(Widget child) { final String? className; if (child is WDiv) { className = child.className; + } else if (child is WButton) { + className = child.className; + } else if (child is WAnchor) { + // A WAnchor has no className of its own; delegate to its effective child. + final Widget inner = child.child; + if (inner is WDiv) { + className = inner.className; + } else { + // WText / raw widget: no cross-axis width to honor, always eligible. + return true; + } } else { - // Non-WDiv children (raw Flutter widgets, WText leaves) are untouched. + // Non-Wind children (raw Flutter widgets, WText leaves) are untouched. return false; } diff --git a/test/widgets/w_div/flex_stretch_test.dart b/test/widgets/w_div/flex_stretch_test.dart index d6eb295..95b69f2 100644 --- a/test/widgets/w_div/flex_stretch_test.dart +++ b/test/widgets/w_div/flex_stretch_test.dart @@ -257,6 +257,217 @@ void main() { }); }); + group('smart column stretch (interaction wrappers)', () { + testWidgets('WAnchor wrapping a no-width WDiv stretches to parent width', + (tester) async { + await tester.pumpWidget( + wrapWithTheme( + SizedBox( + width: 300, + child: WDiv( + className: 'flex flex-col', + children: [ + WAnchor( + onTap: () {}, + child: const WDiv( + className: 'bg-red-500', + child: SizedBox(height: 10), + ), + ), + ], + ), + ), + ), + ); + + expect(tester.takeException(), isNull); + expect(tester.getSize(find.byType(WDiv).at(1)).width, 300); + }); + + testWidgets('WAnchor wrapping a WText stretches to parent width', + (tester) async { + await tester.pumpWidget( + wrapWithTheme( + SizedBox( + width: 300, + child: WDiv( + className: 'flex flex-col', + children: [ + WAnchor( + onTap: () {}, + child: const WText('x'), + ), + ], + ), + ), + ), + ); + + expect(tester.takeException(), isNull); + expect(tester.getSize(find.byType(WAnchor)).width, 300); + }); + + testWidgets('WButton stretches to parent width', (tester) async { + await tester.pumpWidget( + wrapWithTheme( + SizedBox( + width: 300, + child: WDiv( + className: 'flex flex-col', + children: [ + WButton( + onTap: () {}, + child: const WText('x'), + ), + ], + ), + ), + ), + ); + + expect(tester.takeException(), isNull); + expect(tester.getSize(find.byType(WButton)).width, 300); + }); + + testWidgets('WAnchor wrapping a w-32 WDiv keeps its width (no stretch)', + (tester) async { + await tester.pumpWidget( + wrapWithTheme( + SizedBox( + width: 300, + child: WDiv( + className: 'flex flex-col', + children: [ + WAnchor( + onTap: () {}, + child: const WDiv( + className: 'w-32 bg-red-500', + child: SizedBox(height: 10), + ), + ), + ], + ), + ), + ), + ); + + expect(tester.takeException(), isNull); + // w-32 = 32 * 4px = 128px; the inner explicit width disables stretch. + expect(tester.getSize(find.byType(WDiv).at(1)).width, 128); + }); + + testWidgets( + 'stretch-eligible WAnchor in an unbounded-width column does not crash ' + '(degrades to content size)', (tester) async { + // A flex-col in a bare Row main-axis slot has unbounded width. A + // stretch-eligible WAnchor would otherwise be wrapped in + // SizedBox(width: infinity), which throws under an unbounded constraint. + // The bounded-width LayoutBuilder guard must skip the wrap and let the + // WAnchor degrade to its content size instead of crashing. + await tester.pumpWidget( + wrapWithTheme( + Row( + mainAxisSize: MainAxisSize.min, + children: [ + WDiv( + className: 'flex flex-col', + children: [ + WAnchor( + onTap: () {}, + child: const WText('content'), + ), + ], + ), + ], + ), + ), + ); + + expect(tester.takeException(), isNull, + reason: 'WAnchor smart stretch must not throw on unbounded width'); + }); + + testWidgets('WButton with explicit width keeps its width (no stretch)', + (tester) async { + await tester.pumpWidget( + wrapWithTheme( + SizedBox( + width: 300, + child: WDiv( + className: 'flex flex-col', + children: [ + WButton( + onTap: () {}, + className: 'w-32', + child: const WText('x'), + ), + ], + ), + ), + ), + ); + + expect(tester.takeException(), isNull); + // w-32 = 128px; the button's own explicit width disables stretch. + expect(tester.getSize(find.byType(WButton)).width, 128); + }); + + testWidgets('self-flexing WButton (grow) is not stretch-wrapped', + (tester) async { + // A WButton whose own className self-wraps in Expanded must not be + // wrapped again in SizedBox(width: infinity); _selfWrapsInFlex on the + // button's className excludes it from the stretch pass. + await tester.pumpWidget( + wrapWithTheme( + SizedBox( + width: 300, + height: 200, + child: WDiv( + className: 'flex flex-col', + children: [ + WButton( + onTap: () {}, + className: 'grow', + child: const WText('x'), + ), + ], + ), + ), + ), + ); + + expect(tester.takeException(), isNull, + reason: 'self-flexing WButton must not be stretch-wrapped'); + }); + + testWidgets('bare WText direct column child is still NOT stretched', + (tester) async { + await tester.pumpWidget( + wrapWithTheme( + const SizedBox( + width: 300, + child: WDiv( + className: 'flex flex-col', + children: [ + WText('x'), + ], + ), + ), + ), + ); + + expect(tester.takeException(), isNull); + // The bare WText leaf must not be wrapped in a stretch SizedBox. + final column = tester.widget(find.byType(Column)); + final stretched = column.children + .whereType() + .where((box) => box.width == double.infinity) + .length; + expect(stretched, 0, + reason: 'bare WText leaf stays unstretched (existing exclusion)'); + }); + }); + group('smart column stretch — edge cases', () { testWidgets('bounded-width column in a vertical scroll still stretches', (tester) async { From 22ce8e1d4bed667b28bcb7221a28b93e900ccf31 Mon Sep 17 00:00:00 2001 From: Anilcan Cakir Date: Tue, 9 Jun 2026 03:10:44 +0300 Subject: [PATCH 2/5] docs: sync smart-stretch eligibility to WAnchor/WButton Update the column-stretch wording in doc/layout/flexbox.md, the layouts and tailwind-divergence skill references, and the CHANGELOG so the eligibility list reads WDiv, WAnchor (any child), and WButton instead of WDiv-only. Folds the broadening into the existing unreleased [1.0.0] Added entry rather than adding a redundant Changed entry for the same version. --- CHANGELOG.md | 2 +- doc/layout/flexbox.md | 2 +- skills/wind-ui/references/layouts.md | 2 +- skills/wind-ui/references/tailwind-divergence.md | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ac48ea..cbb322e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,7 @@ First stable release. wind is utility-first, Tailwind-syntax styling for Flutter - **Reverse flex direction**: `flex-row-reverse` and `flex-col-reverse` flip the main-axis direction so `justify-start` mirrors per CSS semantics. - **`self-*` align-self shorthand**: `self-start`, `self-end`, `self-center`, `self-stretch`, `self-auto`, `self-baseline` as aliases for the `align-self-*` long form, matching Tailwind's canonical class name. - **Flex grow / basis tokens**: `grow` (Tailwind shorthand for `flex-grow`, i.e. `flex: 1`), `grow-0` (no grow), and `basis-1/2` / `basis-1/3` / `basis-1/4` / `basis-full` / `basis-[Npx]`. Basis approximates CSS `flex-basis`: it sets the child's initial MAIN-axis size (width in a row, height in a column) and ignores grow/shrink interplay. The no-grow / no-shrink resets follow last-class-wins: `grow-0` cancels an earlier `grow`/`flex-grow`/`flex-N`, `shrink-0` cancels an earlier `shrink`/`flex-shrink`, and `flex-none` (`flex: 0 0 auto`) cancels both, within the same active class list. -- **Smart column cross-axis stretch**: a `flex flex-col` with no explicit `items-*` token now stretches each `WDiv` child that does not control its own width to the column width (CSS `align-items: stretch` default). Children that self-wrap in `Expanded`/`Flexible` (`grow`, `flex-grow`, `flex-auto`, `flex-initial`, `shrink`, `flex-shrink`, `flex-N`, in any state/breakpoint variant), children with an explicit `w-*` / `min-w-*` / `max-w-*` (including `w-full`), absolute children, and non-`WDiv` children are left untouched. `shrink-0` / `flex-none` children still stretch on the cross axis (their no-shrink effect is main-axis only, matching CSS). Rows are unaffected. +- **Smart column cross-axis stretch**: a `flex flex-col` with no explicit `items-*` token now stretches each `WDiv`, `WAnchor` (any child), and `WButton` child that does not control its own width to the column width (CSS `align-items: stretch` default), so a clickable nav row (`WAnchor(onTap) > WDiv`) fills the column width without an explicit `items-stretch` or `w-full`. For a `WAnchor`: when it wraps a `WDiv`, the inner `WDiv`'s className decides; when it wraps a `WText` or raw widget, it always stretches. Children that self-wrap in `Expanded`/`Flexible` (`grow`, `flex-grow`, `flex-auto`, `flex-initial`, `shrink`, `flex-shrink`, `flex-N`, in any state/breakpoint variant), children with an explicit `w-*` / `min-w-*` / `max-w-*` (including `w-full`), absolute children, bare `WText` leaves, and raw Flutter widgets are left untouched. `shrink-0` / `flex-none` children still stretch on the cross axis (their no-shrink effect is main-axis only, matching CSS). Rows are unaffected. - **Inline color props**: `WDiv.backgroundColor` and `WText.foregroundColor` for runtime-dynamic colors. Override any `bg-*` / `text-*` from className and stay out of the parser cache key. - **`WBreakpoint` widget**: declarative per-breakpoint widget tree builder. Reach for it when className prefixes are not enough because the widget structure itself changes between breakpoints. - **`WDynamic`**: JSON-driven widget tree renderer. 13 Wind types + 16 Flutter core types allowed by default, extensible via `builders:`, restrictable via `denyWidgets:`, with `customIcons:` for user-defined icon mappings (24 built-in glyphs). State binding by widget `id`, action dispatch via `WActionHandler`, max recursion depth 50. diff --git a/doc/layout/flexbox.md b/doc/layout/flexbox.md index 37e9242..a4291df 100644 --- a/doc/layout/flexbox.md +++ b/doc/layout/flexbox.md @@ -208,7 +208,7 @@ Controls how children are distributed along the **cross axis** (Vertical for `ro WDiv(className: 'flex items-center h-20') ``` -> **Column default — smart cross-axis stretch.** A `flex flex-col` with no explicit `items-*` token stretches each `WDiv` child that does not control its own width to the column width, matching CSS `align-items: stretch`. Left untouched: children with an explicit width (`w-*` / `min-w-*` / `max-w-*` / `w-full`, in any state/breakpoint variant), children that self-wrap in `Expanded`/`Flexible` (`grow`, `flex-grow`, `flex-auto`, `flex-initial`, `shrink`, `flex-shrink`, `flex-N`), absolute children, and non-`WDiv` children. `shrink-0` / `flex-none` children still stretch on the cross axis (`flex-shrink` is main-axis only, matching CSS). Add any `items-*` token (e.g. `items-start`) to turn this off and let children size to content. Rows are never auto-stretched on the cross axis. When the column itself sits in an unbounded-width context (a bare `Row` slot, `UnconstrainedBox`, horizontal scroll), the stretch safely falls back to content-sized children instead of forcing an infinite width. +> **Column default: smart cross-axis stretch.** A `flex flex-col` with no explicit `items-*` token stretches each `WDiv`, `WAnchor` (any child), and `WButton` child that does not control its own width to the column width, matching CSS `align-items: stretch`. For `WAnchor`: when the anchor wraps a `WDiv`, the inner `WDiv`'s className decides (so `WAnchor > WDiv(w-32)` keeps 128 px and `WAnchor > WDiv(grow)` stays self-flexing); when the anchor wraps a `WText` or raw widget, it always stretches. Left untouched: children with an explicit width (`w-*` / `min-w-*` / `max-w-*` / `w-full`, in any state/breakpoint variant), children that self-wrap in `Expanded`/`Flexible` (`grow`, `flex-grow`, `flex-auto`, `flex-initial`, `shrink`, `flex-shrink`, `flex-N`), absolute children, bare `WText` leaves, and raw Flutter widgets. `shrink-0` / `flex-none` children still stretch on the cross axis (`flex-shrink` is main-axis only, matching CSS). Add any `items-*` token (e.g. `items-start`) to turn this off and let children size to content. Rows are never auto-stretched on the cross axis. When the column itself sits in an unbounded-width context (a bare `Row` slot, `UnconstrainedBox`, horizontal scroll), the stretch safely falls back to content-sized children instead of forcing an infinite width. ## Align Content diff --git a/skills/wind-ui/references/layouts.md b/skills/wind-ui/references/layouts.md index 9983954..55b6d24 100644 --- a/skills/wind-ui/references/layouts.md +++ b/skills/wind-ui/references/layouts.md @@ -28,7 +28,7 @@ Two consequences drive almost every Wind layout footgun: 2. **Scrollables remove the max on their axis.** A `SingleChildScrollView` (Wind's `overflow-y-auto`) passes `maxHeight: double.infinity` to its child. A child that asserts on finite height (`Column` with `Expanded` children) throws "Vertical viewport was given unbounded height". -3. **`flex flex-col` stretches `WDiv` children to the column width by default.** With NO explicit `items-*` token, each direct `WDiv` child that does not control its own width is wrapped in `SizedBox(width: double.infinity)`, mirroring CSS `align-items: stretch`. Left untouched: children with an explicit width (`w-*` / `min-w-*` / `max-w-*` / `w-full`, in any state/breakpoint variant), children that self-wrap in `Expanded`/`Flexible` (`grow`, `flex-grow`, `flex-auto`, `flex-initial`, `shrink`, `flex-shrink`, `flex-N`), absolute children, and non-`WDiv` children (raw Flutter widgets, `WText`). `shrink-0` / `flex-none` children still stretch on the cross axis: `flex-shrink` is main-axis only, matching CSS. Add any `items-*` token (e.g. `items-start`) to disable the stretch and let children size to content. This is column-only; rows are never auto-stretched on the cross axis. +3. **`flex flex-col` stretches `WDiv`, `WAnchor` (any child), and `WButton` children to the column width by default.** With NO explicit `items-*` token, each such child that does not control its own width is wrapped in `SizedBox(width: double.infinity)`, mirroring CSS `align-items: stretch`. For `WAnchor`: when the anchor wraps a `WDiv`, the inner `WDiv`'s className decides; when it wraps a `WText` or raw widget, it always stretches. Left untouched: children with an explicit width (`w-*` / `min-w-*` / `max-w-*` / `w-full`, in any state/breakpoint variant), children that self-wrap in `Expanded`/`Flexible` (`grow`, `flex-grow`, `flex-auto`, `flex-initial`, `shrink`, `flex-shrink`, `flex-N`), absolute children, bare `WText` leaves, and raw Flutter widgets. `shrink-0` / `flex-none` children still stretch on the cross axis: `flex-shrink` is main-axis only, matching CSS. Add any `items-*` token (e.g. `items-start`) to disable the stretch and let children size to content. This is column-only; rows are never auto-stretched on the cross axis. Memorize these and the rest follows. diff --git a/skills/wind-ui/references/tailwind-divergence.md b/skills/wind-ui/references/tailwind-divergence.md index 5a278f5..88b7efd 100644 --- a/skills/wind-ui/references/tailwind-divergence.md +++ b/skills/wind-ui/references/tailwind-divergence.md @@ -340,4 +340,6 @@ These canonical Tailwind classes are silently ignored by wind (unknown tokens ar Note: `WText` is self-contained regarding its baseline rendering. When no Material/Scaffold ancestor supplies a `DefaultTextStyle` color, `WText` applies a brightness-aware fallback (`Colors.white` on dark platforms, `Colors.black` on light, read from `MediaQuery.platformBrightness`) instead of Flutter's debug yellow-underline; it also injects a `Directionality(ltr)` wrapper when no `Directionality` is inherited. Explicit `text-*` className, `foregroundColor`, and `textStyle` props override the fallback and are unaffected. +Note: Wind's smart column cross-axis stretch now covers clickable container rows (`WAnchor` and `WButton`) in addition to plain `WDiv` children, narrowing the divergence from CSS `align-items: stretch` (where element type is irrelevant). A `WAnchor`-wrapped nav row in a `flex flex-col` fills the column width by default without an explicit `items-stretch` or `w-full`. + State-shadowing edge: a disabled outer `WAnchor` does NOT suppress a `hover:` class that a nested `WDiv` carries on itself. `WDiv` auto-wraps in its own non-disabled `WAnchor` whose state provider shadows the ancestor's `isDisabled`, so the inner `hover:` still fires. This is an edge case (a disabled control wrapping a separately-hover-styled child); style the disabled state on the same element that owns the `hover:` class, or drive `disabled:` on the inner `WDiv` directly. From f91d75ec048897ee30985175d5b04c56fbb5dc55 Mon Sep 17 00:00:00 2001 From: Anilcan Cakir Date: Tue, 9 Jun 2026 03:10:44 +0300 Subject: [PATCH 3/5] docs(example): clickable-row stretch demo in flex_grow page Add a sidebar-nav demo with WAnchor-wrapped rows and a WButton column that fill the column width without an explicit items-stretch or w-full, showing the broadened smart-stretch behavior. Dark-mode pairs on every color token. --- example/lib/pages/layout/flex_grow.dart | 118 ++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/example/lib/pages/layout/flex_grow.dart b/example/lib/pages/layout/flex_grow.dart index e7af45a..e22b43b 100644 --- a/example/lib/pages/layout/flex_grow.dart +++ b/example/lib/pages/layout/flex_grow.dart @@ -140,6 +140,12 @@ class FlexGrowExamplePage extends StatelessWidget { ], ), ), + ExampleSection( + title: 'Clickable Row Stretch (WAnchor / WButton)', + description: + 'WAnchor and WButton column children also stretch to the column width by default. A nav sidebar built from WAnchor rows fills its container without w-full or items-stretch.', + child: const _ClickableStretchDemo(), + ), ExampleSection( title: 'Quick Reference', description: 'Eight tokens cover the bulk of flex sizing scenarios.', @@ -290,6 +296,118 @@ class _BasisRow extends StatelessWidget { } } +class _ClickableStretchDemo extends StatelessWidget { + const _ClickableStretchDemo(); + + @override + Widget build(BuildContext context) { + return WDiv( + className: ''' + flex flex-col gap-4 p-4 rounded-xl + bg-white dark:bg-slate-800 + border border-slate-200 dark:border-slate-700 + ''', + children: [ + WText( + 'Sidebar nav: WAnchor rows fill column width automatically', + className: 'text-xs font-mono text-slate-500 dark:text-slate-400', + ), + WDiv( + className: ''' + flex flex-col + rounded-lg overflow-hidden + border border-slate-200 dark:border-slate-700 + ''', + children: [ + _NavRow( + icon: '▶', + label: 'Dashboard', + color: 'bg-orange-500 dark:bg-orange-600', + onTap: () {}, + ), + _NavRow( + icon: '◉', + label: 'Projects', + color: 'bg-orange-400 dark:bg-orange-500', + onTap: () {}, + ), + _NavRow( + icon: '◈', + label: 'Settings', + color: 'bg-orange-300 dark:bg-orange-400', + onTap: () {}, + ), + ], + ), + WText( + 'WButton rows also stretch, no w-full needed', + className: 'text-xs font-mono text-slate-500 dark:text-slate-400', + ), + WDiv( + className: 'flex flex-col gap-2', + children: [ + WButton( + className: ''' + px-4 py-3 rounded-lg + bg-slate-100 dark:bg-slate-700 + text-slate-800 dark:text-slate-100 + ''', + onTap: () {}, + child: WText( + 'Save draft', + className: 'font-semibold text-sm', + ), + ), + WButton( + className: ''' + px-4 py-3 rounded-lg + bg-orange-500 dark:bg-orange-600 + ''', + onTap: () {}, + child: WText( + 'Publish', + className: 'font-semibold text-sm text-white', + ), + ), + ], + ), + ], + ); + } +} + +class _NavRow extends StatelessWidget { + final String icon; + final String label; + final String color; + final VoidCallback onTap; + + const _NavRow({ + required this.icon, + required this.label, + required this.color, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return WAnchor( + onTap: onTap, + child: WDiv( + className: ''' + flex flex-row items-center gap-3 + px-4 py-3 + $color + ''', + children: [ + WText(icon, className: 'text-white text-sm'), + WText(label, className: 'text-white font-semibold text-sm'), + ], + ), + ); + } +} + class _StretchDemo extends StatelessWidget { final String label; final String containerClass; From 64a288f735b5ebb9895804c519c3dd3c51d9bd7e Mon Sep 17 00:00:00 2001 From: Anilcan Cakir Date: Tue, 9 Jun 2026 03:48:32 +0300 Subject: [PATCH 4/5] docs(w-div): correct WAnchor>WDiv(grow) framing in stretch comments Address PR #99 review: the doc comments implied WAnchor > WDiv(grow) is a supported/safe config and that the self-flex exclusion provides crash safety for it. In Flutter, grow on the inner WDiv self-wraps in Expanded whose parent is the WAnchor's non-Flex wrappers, so that config asserts regardless of stretch. Reword to state the inner-WDiv self-flex check mirrors the direct-WDiv rule (and genuinely prevents the stretch-compounding crash for a direct WDiv), that WAnchor > WDiv(grow) is unsupported in its own right, and that a WAnchor-wrapped non-WDiv child is stretched by policy (the tap surface fills the column; any explicit width still constrains the wrapped widget's content). Also fix the test comment: WButton does not self-wrap in Expanded. --- doc/layout/flexbox.md | 2 +- lib/src/widgets/w_div.dart | 24 +++++++++++++++-------- test/widgets/w_div/flex_stretch_test.dart | 7 ++++--- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/doc/layout/flexbox.md b/doc/layout/flexbox.md index a4291df..575f854 100644 --- a/doc/layout/flexbox.md +++ b/doc/layout/flexbox.md @@ -208,7 +208,7 @@ Controls how children are distributed along the **cross axis** (Vertical for `ro WDiv(className: 'flex items-center h-20') ``` -> **Column default: smart cross-axis stretch.** A `flex flex-col` with no explicit `items-*` token stretches each `WDiv`, `WAnchor` (any child), and `WButton` child that does not control its own width to the column width, matching CSS `align-items: stretch`. For `WAnchor`: when the anchor wraps a `WDiv`, the inner `WDiv`'s className decides (so `WAnchor > WDiv(w-32)` keeps 128 px and `WAnchor > WDiv(grow)` stays self-flexing); when the anchor wraps a `WText` or raw widget, it always stretches. Left untouched: children with an explicit width (`w-*` / `min-w-*` / `max-w-*` / `w-full`, in any state/breakpoint variant), children that self-wrap in `Expanded`/`Flexible` (`grow`, `flex-grow`, `flex-auto`, `flex-initial`, `shrink`, `flex-shrink`, `flex-N`), absolute children, bare `WText` leaves, and raw Flutter widgets. `shrink-0` / `flex-none` children still stretch on the cross axis (`flex-shrink` is main-axis only, matching CSS). Add any `items-*` token (e.g. `items-start`) to turn this off and let children size to content. Rows are never auto-stretched on the cross axis. When the column itself sits in an unbounded-width context (a bare `Row` slot, `UnconstrainedBox`, horizontal scroll), the stretch safely falls back to content-sized children instead of forcing an infinite width. +> **Column default: smart cross-axis stretch.** A `flex flex-col` with no explicit `items-*` token stretches each `WDiv`, `WAnchor` (any child), and `WButton` child that does not control its own width to the column width, matching CSS `align-items: stretch`. For `WAnchor`: when the anchor wraps a `WDiv`, the inner `WDiv`'s className decides (so `WAnchor > WDiv(w-32)` keeps 128 px; a `WAnchor > WDiv` with a self-flex token is excluded just as a direct self-flexing `WDiv` is); when the anchor wraps a `WText` or raw widget, the anchor stretches by policy so its tap surface fills the column. Left untouched: children with an explicit width (`w-*` / `min-w-*` / `max-w-*` / `w-full`, in any state/breakpoint variant), children that self-wrap in `Expanded`/`Flexible` (`grow`, `flex-grow`, `flex-auto`, `flex-initial`, `shrink`, `flex-shrink`, `flex-N`), absolute children, bare `WText` leaves, and raw Flutter widgets. `shrink-0` / `flex-none` children still stretch on the cross axis (`flex-shrink` is main-axis only, matching CSS). Add any `items-*` token (e.g. `items-start`) to turn this off and let children size to content. Rows are never auto-stretched on the cross axis. When the column itself sits in an unbounded-width context (a bare `Row` slot, `UnconstrainedBox`, horizontal scroll), the stretch safely falls back to content-sized children instead of forcing an infinite width. ## Align Content diff --git a/lib/src/widgets/w_div.dart b/lib/src/widgets/w_div.dart index 2f2a7ee..c00494c 100644 --- a/lib/src/widgets/w_div.dart +++ b/lib/src/widgets/w_div.dart @@ -848,14 +848,20 @@ class WDiv extends StatelessWidget { /// - `WAnchor` delegates its eligibility to its effective child: when the /// child is a `WDiv`, the inner WDiv's `className` decides (a `WAnchor` /// carries no `className`), so a `WAnchor > WDiv(w-32)` keeps 128px and a - /// `WAnchor > WDiv(grow)` stays a self-flexing child that must never be - /// wrapped in a stretch `SizedBox`; when the child is anything else (a - /// `WText`, a raw widget) there is no width to honor, so it is eligible. + /// `WAnchor > WDiv` carrying a self-flex token is excluded for the same + /// reason a direct self-flexing `WDiv` is; when the child is anything else + /// (a `WText`, a raw widget) the wrapper is stretched by policy: the + /// `WAnchor` tap surface fills the column, and any explicit width on the + /// wrapped widget still constrains its own content. /// - /// The inner-WDiv delegation is load-bearing for crash safety: a - /// `WAnchor > WDiv(grow)` self-wraps in `Expanded` during its own build, so - /// adding a `SizedBox(width: infinity)` stretch around it would compound the - /// flex wrap. `_selfWrapsInFlex` on the inner className excludes it. + /// The inner-WDiv self-flex check mirrors the direct-`WDiv` rule: stretching a + /// self-flexing `WDiv` (a `SizedBox(width: infinity)` around a `WDiv` that + /// self-wraps in `Expanded`/`Flexible`) asserts ParentDataWidget, so + /// `_selfWrapsInFlex` excludes it. A `WAnchor > WDiv(grow)` is itself an + /// unsupported configuration: the inner `WDiv`'s `Expanded` asserts because + /// its parent is the `WAnchor`'s non-`Flex` wrappers, not a `Flex`. Excluding + /// it from stretch does not rescue that case; it only keeps the eligibility + /// rule consistent with a direct `WDiv`. static bool _shouldStretchColumnChild(Widget child) { final String? className; if (child is WDiv) { @@ -868,7 +874,9 @@ class WDiv extends StatelessWidget { if (inner is WDiv) { className = inner.className; } else { - // WText / raw widget: no cross-axis width to honor, always eligible. + // WText / raw widget: stretch the WAnchor surface by policy so the tap + // area fills the column; any explicit width on the wrapped widget still + // constrains its own content. return true; } } else { diff --git a/test/widgets/w_div/flex_stretch_test.dart b/test/widgets/w_div/flex_stretch_test.dart index 95b69f2..337b887 100644 --- a/test/widgets/w_div/flex_stretch_test.dart +++ b/test/widgets/w_div/flex_stretch_test.dart @@ -414,9 +414,10 @@ void main() { testWidgets('self-flexing WButton (grow) is not stretch-wrapped', (tester) async { - // A WButton whose own className self-wraps in Expanded must not be - // wrapped again in SizedBox(width: infinity); _selfWrapsInFlex on the - // button's className excludes it from the stretch pass. + // A WButton whose className carries a self-flex token (grow) is excluded + // from the stretch pass by _selfWrapsInFlex, mirroring the direct-WDiv + // rule. WButton itself does not wrap in Expanded; the exclusion keeps the + // eligibility rule consistent. await tester.pumpWidget( wrapWithTheme( SizedBox( From daaaf084aa16c079e525747ef9dee8fa157193e0 Mon Sep 17 00:00:00 2001 From: Anilcan Cakir Date: Tue, 9 Jun 2026 03:48:32 +0300 Subject: [PATCH 5/5] docs(example): pair text-white with dark:text-white in stretch demo Address PR #99 review: every color token in an example page must carry its dark: peer. The clickable-row stretch demo's white labels lacked dark:text-white. --- example/lib/pages/layout/flex_grow.dart | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/example/lib/pages/layout/flex_grow.dart b/example/lib/pages/layout/flex_grow.dart index e22b43b..3a16b87 100644 --- a/example/lib/pages/layout/flex_grow.dart +++ b/example/lib/pages/layout/flex_grow.dart @@ -366,7 +366,7 @@ class _ClickableStretchDemo extends StatelessWidget { onTap: () {}, child: WText( 'Publish', - className: 'font-semibold text-sm text-white', + className: 'font-semibold text-sm text-white dark:text-white', ), ), ], @@ -400,8 +400,11 @@ class _NavRow extends StatelessWidget { $color ''', children: [ - WText(icon, className: 'text-white text-sm'), - WText(label, className: 'text-white font-semibold text-sm'), + WText(icon, className: 'text-white dark:text-white text-sm'), + WText( + label, + className: 'text-white dark:text-white font-semibold text-sm', + ), ], ), ); @@ -441,7 +444,7 @@ class _StretchDemo extends StatelessWidget { ''', child: WText( 'Nav bar', - className: 'text-white font-semibold text-sm', + className: 'text-white dark:text-white font-semibold text-sm', ), ), WDiv( @@ -451,7 +454,7 @@ class _StretchDemo extends StatelessWidget { ''', child: WText( 'Content card', - className: 'text-white text-sm', + className: 'text-white dark:text-white text-sm', ), ), ],