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: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion doc/layout/flexbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -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; 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.

<a name="align-content"></a>
## Align Content
Expand Down
125 changes: 123 additions & 2 deletions example/lib/pages/layout/flex_grow.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down Expand Up @@ -290,6 +296,121 @@ 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 dark:text-white',
),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
),
],
),
],
);
}
}

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 dark:text-white text-sm'),
WText(
label,
className: 'text-white dark:text-white font-semibold text-sm',
),
],
),
);
}
}

class _StretchDemo extends StatelessWidget {
final String label;
final String containerClass;
Expand Down Expand Up @@ -323,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(
Expand All @@ -333,7 +454,7 @@ class _StretchDemo extends StatelessWidget {
''',
child: WText(
'Content card',
className: 'text-white text-sm',
className: 'text-white dark:text-white text-sm',
),
),
],
Expand Down
40 changes: 39 additions & 1 deletion lib/src/widgets/w_div.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Comment on lines 9 to 11

/// **The Fundamental Building Block of Wind**
Expand Down Expand Up @@ -837,12 +838,49 @@ 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` 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 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) {
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: 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 {
// Non-WDiv children (raw Flutter widgets, WText leaves) are untouched.
// Non-Wind children (raw Flutter widgets, WText leaves) are untouched.
return false;
}

Expand Down
2 changes: 1 addition & 1 deletion skills/wind-ui/references/layouts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 2 additions & 0 deletions skills/wind-ui/references/tailwind-divergence.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading