1.2.0
Changed
- Wind's internal
flexlayout is now fully intrinsic-safe: the column cross-axis stretch and thebasis-*resolution no longer use aLayoutBuilder. Column stretch is a real render object (WindCrossStretch) and fractionalbasis-*resolves against the flex's own extent viaWindMainExtentProvider/WindFractionBasis. Aflex flex-col(with or withoutbasis-*) now renders inside anitems-stretchgrid cell, under a FlutterIntrinsicHeight/IntrinsicWidth, or in aTablecell without theLayoutBuilder does not support returning intrinsic dimensionsassert (the web_owner != nullcascade that produced). (lib/src/widgets/w_div.dart,lib/src/widgets/wind_equal_height_row.dart) (WIND-4) - Interactive widgets (
WSelect,WCheckbox,WRadio,WDatePicker) now route their selection, checkmark, and accent colors through the themeprimarytoken instead of hardcodedblue-*classes andColors.blueshades, so a consumer's brand color drives them. The shipped default look is unchanged (primarydefaults to the Tailwind blue swatch).WindThemeData.toThemeData()keeps its indigo MaterialColorSchemebaseline unless a customprimaryis registered, so the default Material appearance is unchanged too. (lib/src/widgets/w_select.dart,lib/src/widgets/w_checkbox.dart,lib/src/widgets/w_radio.dart,lib/src/widgets/w_date_picker.dart,lib/src/theme/wind_theme_data.dart) (WIND-3) - Documented and test-proved the
WindRecipe/WindSlotRecipecaller-classNamemerge contract: the recipe only appends the caller's className last (already the behavior since introduction); the conflict with a base token is resolved one layer down, at parse time, byWindParser's per-family last-wins. No twMerge/cnport was added or is planned. (test/recipe/wind_recipe_test.dart,test/parser/parsers/sizing_parser_test.dart,doc/styling/wind-recipe.md,skills/wind-ui/SKILL.md) skills/wind-ui/references/tailwind-divergence.md,skills/wind-ui/SKILL.md, anddoc/layout/display.mdreconciled to reflect theflex-wrapalias (no longer a listed unsupported token) and the unknown-token debug hint (no longer purely silent). Example pages underexample/lib/pages/switched fromflex-wrapto the canonicalwraptoken. (WIND-5)
Added
- Min-width-stretch horizontal scroll primitive ("fill on desktop, scroll on narrow", the shadcn Table pattern), composed from existing tokens with no new className:
overflow-x-autoon a wrapper withw-full(optionallymin-w-[Npx]) on the inner content.w-fullinside a horizontal scroll is now sized tomax(viewport, min-w-*)via the threaded viewport width instead of asserting on the scroll's unbounded width, so the content fills the viewport when wide and honors its min width (scrolling) when narrow. (lib/src/widgets/wind_min_width_scroll.dart,lib/src/state/wind_min_width_scroll_scope.dart,lib/src/widgets/w_div.dart,example/lib/pages/layout/responsive_table.dart) (WIND-4) - Explicit
flex flex-col items-stretchnow equalizes child widths (every eligible child fills the column), closing the asymmetry withgrid ... items-stretch. Like the smart-stretch default it is intrinsic-safe and unbounded-safe (noLayoutBuilder, no infinite-widthSizedBox), so it also works in a bareRowslot or under an intrinsic-measuring ancestor. (lib/src/widgets/w_div.dart,lib/src/widgets/wind_equal_height_row.dart) (WIND-4) - Actionable dev-time assert for
h-fullinside a vertical scroll: a child that resolvesh-fullunder anoverflow-y-auto/overflow-y-scrollparent (an unbounded height) now fails fast with a message pointing at the fix (flex-1inside aflex flex-col), instead of a cryptic Flutter unbounded-height error. The scrollable parent threads the signal down viaWindMinWidthScrollScope; the assert is stripped in release. (lib/src/widgets/w_div.dart,lib/src/state/wind_min_width_scroll_scope.dart) (WIND-4) - Seeded default
primarycolor token: Wind's default theme now includes aprimaryMaterialColor(aliased 1:1 to the Tailwindblueswatch), sobg-primary/text-primary/border-primaryresolve out of the box and become brand-overridable viaWindThemeData.colors: {'primary': ...}. Previouslyprimarywas consulted only bytoThemeData()and was a silent no-op in className tokens. (lib/src/theme/defaults/colors.dart,lib/src/theme/wind_theme_data.dart) (WIND-3) flex-wrap->wrapalias:FlexboxGridParsernow maps the Tailwind spellingflex-wrapdirectly toWindDisplayType.wrap(canParsealready acceptedflex-*; only theparse()handler was missing).wrapremains the canonical, unaliased token;flex-wrapprints a one-timekDebugModehint suggesting it. (lib/src/parser/parsers/flexbox_grid_parser.dart) (WIND-5)- Unknown-className debug warning:
WindParser.findAndGroupClassesnow emits a one-timekDebugModedebugPrintnaming any className that no parser recognizes, deduped per unique token per session (mirrors the existing_warnedAliasesshadow/cycle dedup). The token is still dropped from output; release builds print nothing. Valid Wind tokens handled outside the parser map are exempt so the warning aims only at genuine typos: the widget-consumedobject-*fit family (read byWImage), theinline-flex/inline-block/inlinedisplay keywords (emitted byWBadge, inert in Flutter), and deliberately inert compat tokens that Wind's own widget docstrings / the consumer contract emit (transition/transition-colors/etc.,antialiased,sr-only, the*-numsfont-variant family incl.tabular-nums). (lib/src/parser/wind_parser.dart) (WIND-5) cursor-*utilities: a newCursorParsermaps Tailwind cursor names (cursor-pointer,cursor-not-allowed,cursor-text,cursor-grab/cursor-grabbing,cursor-zoom-in/cursor-zoom-out, the full resize set, etc.) to the matchingSystemMouseCursors.WDivapplies the resolved cursor through aMouseRegion, so a plain container can feel clickable on web/desktop withoutWAnchoror a manualMouseRegion; on ahover:/focus:/active:WDivthe cursorMouseRegionsits below the auto-wrappedWAnchorand wins. Inert on touch platforms; last token wins; unknown names no-op. (lib/src/parser/parsers/cursor_parser.dart,lib/src/parser/wind_style.dart,lib/src/widgets/w_div.dart) (#125)size-*sizing utility (Tailwind v3.4+ shorthand):size-2,size-full,size-1/2,size-[20px],size-[50%],size-screenset BOTH width and height in one token. Fixes childlessWDivsizing: aWDiv(className: 'size-2 rounded-full bg-...')status dot now renders its box instead of collapsing (the previous gap was thatsize-*was unrecognized, not thatw-*/h-*were ignored, which already worked childless). A laterw-*/h-*overrides the matching axis. (lib/src/parser/parsers/sizing_parser.dart) (#123)gridequal-height rows viaitems-stretch. By default a Windgridrenders as aWrapand sizes each cell to its own content, leaving a row ragged when one card is taller. Addingitems-stretch(CSS Grid's defaultalign-items: stretch) now builds the grid as a column ofIntrinsicHeightrows so every cell in a row matches the tallest, and cells divide the row width evenly. Cells should size from their own content;h-fullon a stretched cell is unsupported (it inserts aLayoutBuilderthat asserts underIntrinsicHeight, see the intrinsic-sizing limitation docs). (lib/src/widgets/w_div.dart) (#126)WindRecipeandWindSlotRecipe(+WindCompoundVariant,WindSlotCompoundVariant): atv()(tailwind-variants) equivalent.WindReciperesolves a single className frombase+ variant axes + compoundVariants + a callerclassName, in strict emission order (base ++ variant ++ compound ++ caller), with no dedupe/sort/twMerge.WindSlotRecipeextends the same model to multi-slot components and returns aMap<String, String>(slot -> className). (lib/src/recipe/wind_recipe.dart) (#120)WBadge: inline status/label pill. Composes a rounded-fullWDivaround aWText(text-xs); all visual tone is className-driven (bg-*,text-*,dark:pairs). (lib/src/widgets/w_badge.dart) (#120)WCard: surface container with optionalheaderandfooterslots. Delegates toWDiv(flex-col); all styling is className-driven. (lib/src/widgets/w_card.dart) (#120)WSwitch: controlled toggle switch. Pushes thechecked:state prefix whenvalueistrue. The thumb is a flex child of the track, so the caller positions it withjustify-start->checked:justify-endon the trackclassName(the track stays a flex Row;WSwitchdoes not injectrelative, which would force a single-child Stack and defeatjustify-*).thumbClassNamesupplies thumb shape/color only;translate-x-*is intentionally unsupported (Wind has no transform parser). WrapsWAnchorfor hover/focus/disabled state propagation. (lib/src/widgets/w_switch.dart) (#120)WRadio<T>: controlled radio button. Drives theselected:state prefix whenvalue == groupValue. Group behavior (mutual exclusivity) is the caller's responsibility. (lib/src/widgets/w_radio.dart) (#120)WTabs: controlled tabs widget. Theselected:state prefix activates on the active tab;listClassName,tabClassName,selectedTabClassName, andpanelClassNamecover every structural region.panelBuilderreceives the selected index each rebuild. The tab list defaults to full container width (fullWidthList: true) so aborder-bunderline spans the container rather than only the tabs; setfullWidthList: falsefor a content-width / pill tab strip. (#128) (lib/src/widgets/w_tabs.dart)
Fixed
-
WPopoveroverlay no longer overflows the right edge of the viewport whenautoFlipis on. A wide panel next to a near-centered trigger on a narrow screen flipped to the opposite side but still spilled off-screen; acomputeHorizontalClamphelper now folds a correctivedxinto the follower offset so the panel is pulled fully inside the viewport (seated8pxfrom the edge, or flush when it is too wide to honor both margins). The clamp engages only on genuine off-screen overflow, so an edge-anchored popover already fully visible is left untouched. (lib/src/widgets/w_popover.dart) -
grid ... items-stretchno longer emits a residualRenderFlex overflowed by ~2px on the bottomwhen it stretches an unequal cell (follow-up to #139).WindEqualHeightRowpreviously re-laid each cell to a TIGHT height equal to the loose-measured row max; a cell whoseflex flex-colcontent 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-stretchno longer assertsRenderBox was not laid out(rooted atRenderIntrinsicHeight/LayoutBuilder does not support returning intrinsic dimensions) when it stretches an unequalflex flex-colcell. The equal-height mechanism from #126 usedIntrinsicHeight, which queries child intrinsics; aflex flex-colcell (or one usingh-full/basis-*) carries aLayoutBuilderthat cannot answer intrinsic queries, so stretching a shorter cell crashed the exact case the feature exists for. Replaced it withWindEqualHeightRow, aRenderObjectthat 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-fullon 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, soprefix:+ a known alias key was a silent no-op while plainbg-surface-containerworked. 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-surfacecarrieshover:onto each token including the value's owndark:peer, yieldinghover: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) -
WPopoveroverlay no longer stretches unbounded. It applied only aminWidth(trigger width) with no upper bound, so a menu without an explicit width could fill the available space and overflow a narrow content column (the asymmetry withWSelect, which pins a width). NewwidthandmaxWidthprops:width(or aw-*token) pins a fixed overlay width likeWSelect'smenuWidth;maxWidth(or amax-w-*token) bounds it, defaulting to the screen width so the overlay never overflows the viewport. (lib/src/widgets/w_popover.dart) (#127) -
w-fullon a direct child of aflex flex-rowno longer aborts layout with "RenderBox was not laid out". A Row hands non-flex children an unbounded main-axis constraint, so aw-fullchild'sSizedBox(width: infinity)could not resolve. Wind now treats a barew-fullRow child asflex-1: the Row wraps it inExpanded(and defaults toMainAxisSize.max), giving the child a bounded width share inside which its infinite-width box resolves, so it fills the row.flex-1remains the idiomatic choice; a prefixedmd:w-fullis intentionally not auto-expanded (usemd:flex-1). Nested (non-direct)w-fullchildren are unaffected. (lib/src/widgets/w_div.dart) (#122) -
WSelect: the dropdown no longer dismisses itself on the same click that opens it on web (verified with real browser mouse events via CDP). Two independent self-close paths fired on the opening click: (1) the overlay'sTapRegion.onTapOutsidereceived the opening tap's own pointer-up becauseOverlayPortalmounts synchronously on the frame it opens, and (2)_onFocusChangeclosed the menu when the trigger lost focus, which web does transiently (~150ms) right after the opening click. Fixes: the overlay mount is now deferred one frame (addPostFrameCallback->OverlayPortal.show()) so the opening pointer-up is fully dispatched before the overlay'sTapRegionexists; and the fragile focus-loss auto-close was removed (the tap-only trigger cannot be opened by keyboard, so it added no real dismissal path while firing spuriously on web). Dismissal is now driven solely by an outside tap (onTapOutside) and option selection; the trigger and open menu still share aTapRegiongroup id so a trigger re-tap toggles closed instead of self-closing. CustomtriggerBuilder/multiTriggerBuildertriggers are wrapped in that same shared group too, so a custom trigger no longer reads as an outside tap and self-closes the menu. (lib/src/widgets/w_select.dart)
Quality
- Documentation, the
wind-uiskill references, andllms.txtsynced to the 1.2.0 surface: the five new widgets gained sections inskills/wind-ui/references/widgets.md, the unknown-className debug hint is now documented indoc/core-concepts/debugging.md, the parser count (20) and thecursor-*/flex-wrapsupport state were corrected across the skill, andllms.txtnow lists every new widget, thecursor-*/size-*families, andWindRecipe. - Removed every em-dash and en-dash from
doc/,skills/,lib/comments, and theexample/gallery to honor the project style rule; theWindDynamicunknown-action debug message now reads..., ignored.(comma form) to match.