fluttersdk_wind 1.2.0
fluttersdk_wind: ^1.2.0 copied to clipboard
Tailwind CSS for Flutter. Write className='flex p-4 bg-white dark:bg-gray-800' and Wind renders optimized widget trees with dark mode and responsive breakpoints.
Changelog #
All notable changes to this project will be documented in this file.
This project follows Semantic Versioning 2.0.0.
Unreleased #
1.2.0 - 2026-07-08 #
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.
1.1.2 - 2026-06-25 #
Fixed #
WPopovernow opens reliably whentriggerBuilderreturns an interactive widget (aWButtonorWAnchorwith its ownonTap), and no longer dismisses itself on the same gesture that opened it. Two defects were in play. First, the trigger was wired through an outerGestureDetector(onTap: toggle); an interactive trigger owns its ownGestureDetector, won the gesture arena, and the outeronTapnever fired, so the popover never opened. The trigger now toggles through aListener(onPointerDown:), whose pointer events bypass the tap arena entirely, so opening works regardless of the trigger's interactivity (enableTriggerOnTapanddisabledsemantics are unchanged). Second, because the trigger and overlay share aTapRegiongroup id (intentionally, so a trigger re-tap does not self-close), the opening gesture's pointer-up reached the freshly mounted overlay'sonTapOutsideand dismissed the popover on the frame it opened. A one-shot, post-frame guard now swallows exactly that first outside-tap; a genuine, later outside tap still closes the popover. (lib/src/widgets/w_popover.dart; covered bytest/widgets/w_popover/gesture_regression_test.dart, the four-behavior regression set parameterized overWButtonandWDivtriggers.) Because the pointerListeneris invisible to assistive technologies, the trigger is wrapped inSemantics(button: true, onTap: ...)so screen readers and keyboard activation still reach the toggle, and the pointer toggle is filtered to the primary button so a secondary (right) click no longer opens the popover.
1.1.1 - 2026-06-23 #
Changed #
WInput: tap-to-focus andonTapdispatch now flow through Flutter's nativeTextSelectionGestureDetectorBuilder(the same gesture pathTextFielduses) instead of a hand-built whole-boxGestureDetector.onTapcontinues to fire when the user taps the field; the change is the gesture mechanism (which also brings drag-select and double-tap word selection), not a newonTapcontract.
Fixed #
WInputnow supports native text selection: mouse drag-select, double-tap word, and long-press all work again. The Material-free rewrite (#106) had dropped the selection gesture layer, so only keyboard select-all (CTRL+A) worked and dragging on web selected nothing. The field now uses the framework's canonical selectable-input recipe (the oneCupertinoTextFielduses):_WInputStateimplementsTextSelectionGestureDetectorBuilderDelegate, the whole decorated box is wrapped by aTextSelectionGestureDetectorBuilder(so a tap anywhere focuses and a drag over the glyphs selects), andEditableText.rendererIgnoresPointeristrueso the gesture layer is the only pointer handler. Selection handles stay Cupertino-style on all platforms (unchanged from 1.1.0), keepingWInputcupertino-only with nopackage:flutter/material.dartimport;cupertinoTextSelectionHandleControlsmixes inTextSelectionHandleControls, so the toolbar still flows through the Material-freecontextMenuBuilderand noOverlay-less long-press throws. Read-only fields stay selectable; disabled fields stay fully inert. (lib/src/widgets/w_input.dart; covered bytest/widgets/w_input/selection_test.dart.)WTextwith no color in its ownclassNamenow inherits an ancestorDefaultTextStylecolor (the CSS text-color cascade) before falling back to the platform-brightness baseline. A parentWDivwith atext-*class publishes its color throughDefaultTextStyle.merge, butWTextpreviously ignored it and forcedColors.white/Colors.blackfrom the OS platform brightness. That made colorless text vanish whenever the app theme disagreed with the OS theme: a secondary/outline button whose text color lives on the container (e.g. a dialog Cancel button) rendered an invisible label in a light app theme on a dark-mode OS. The brightness-aware baseline still applies only when no ancestor supplies a color (bare text with no Material ancestor), preserving the no-yellow-underline guarantee. (lib/src/widgets/w_text.dart; covered byWText baseline rendering > inherits an ancestor color (CSS cascade).)
1.1.0 - 2026-06-17 #
Added #
WindThemeData.aliases(Map<String, String>, empty by default): bare-token recursive className shortcuts expanded centrally inWindParser.parsebefore the 19-parser pipeline runs, so they work in every widget and inWDynamicwithout additional wiring. An alias that shadows a real token wins and emits a debug warning. Expansion is bounded three ways (per-chain cycle guard, depth cap, and a total-output-token budget) so a cyclic or fan-out alias map can never hang the parse. Resolves the#101class of silent unknown-token failures caused by shorthand tokens not being in the default token catalog. (#104)WIcon: inlineforegroundColorprop for runtime-dynamic icon colors. Overrides anytext-*/dark:text-*fromclassNameand stays out of the parser cache key, matchingWText.foregroundColor. (#103)WInput: debugAssertionErrorwhen bothvalueandcontrollerare supplied simultaneously; passing both was always a logic error and previously led to silent precedence behavior (controller wins). The assert surfaces the misuse immediately in debug builds (W2).WInput:readOnly: truenow activates areadonlystate, soreadonly:prefixed classes style a read-only field just likedisabled:does for a disabled one.
Changed #
WInput: selection handles are now Cupertino-style on all platforms (previously Material-style on Android/web). The context menu readsWidgetsLocalizationsso copy/cut/paste labels work under any ancestor, including bareWidgetsApp. This is a visual change on Android and web; behavior is identical. Under a custom root with noOverlayancestor (unusual;MaterialApp/CupertinoApp/WidgetsAppall provide one), typing, cursor movement, and focus still work, but the long-press selection toolbar and handles are suppressed instead of throwing.doc/layout/flexbox.mdand thewind-uiskill: layout-stability guidance added forIntrinsicHeight/IntrinsicWidthin animated subtrees; the safe alternative isStack+Positionedoritems-stretch(W3).WInput:InputType.numbernow restricts input to a signed decimal (digits, one decimal point, optional leading minus) on every platform via a default formatter, so it is numeric on web too, where the keyboard type alone enforces nothing. A caller-suppliedinputFormattersoverrides this default. The number keyboard is nownumberWithOptions(decimal: true, signed: true).
Fixed #
WInputnow renders Material-free (EditableTextcore,BoxDecorationborder/padding) and no longer crashes under a non-Material ancestor such as a bareWidgetsAppor Cupertino app. WrappingWInputin aMaterialAppis no longer required. (#102)WInputemits a single cleantextFieldsemantics node; the previous implementation produced a double-textbox node under the oldMergeSemantics > TextFieldwrapping (W1). Dusk snapshot consumers relying on a single textbox node perWInputcan expect clean output from this release (pairs with dusk D2).WFormInput,WFormSelect,WFormCheckbox, andWFormDatePicker: the default label, hint, and error class names now carrydark:pairs (text-gray-700 dark:text-gray-300,text-gray-500 dark:text-gray-400,text-red-500 dark:text-red-400), so labels and hints stay legible in dark mode instead of rendering dark-on-dark.WInput: a conditionalprefix/suffix(for example a clear button that appears once the field has text) no longer drops focus on the first keystroke, and an appearing suffix no longer grows the field height; the placeholder also shares the input strut so the box height stays constant between empty and filled.WInput:enabled: falseis now fully non-interactive again, the field cannot be tapped, focused, or expose selection handles/toolbar (the Material-free backend would otherwise still react to taps on the text).WInput: a disabled field again reportsisEnabled: falseto assistive technology through itsSemanticsnode. The Material-free rewrite had dropped the flag (theEditableTextnode carries onlyisReadOnly), so a screen reader could not tell a disabled field from a read-only one; the 1.0.0 MaterialTextFieldexposed it and parity is restored.
Quality #
- CI: pushing a version tag (
X.Y.Z) now auto-creates a GitHub Release from the matchingCHANGELOG.mdsection via.github/workflows/publish.yml, alongside the existing pub.dev publish step. (#105)
1.0.0 - 2026-06-09 #
First stable release. wind is utility-first, Tailwind-syntax styling for Flutter; v1.0.0 is a complete rewrite of the 0.0.x line with a fresh API surface, a 19-parser pipeline with cached resolution, dark-mode pairs as a first-class contract, and a contracts-based debug bridge for external tooling. All public APIs follow Semantic Versioning 2.0.0 from this point forward.
Migration from 0.0.x. v1 is not source-compatible with v0. Class names like WText, WDiv, WButton are preserved but constructor signatures, the supported className token set, and theme integration differ throughout. Consumers on 0.0.x rewrite their UI against the v1 API; the full v1 documentation lives at fluttersdk.com/wind.
Added #
- 22 public widgets exported from the single barrel
package:fluttersdk_wind/fluttersdk_wind.dart:- Layout:
WDiv,WSpacer - Structural:
WBreakpoint,WDynamic - Display:
WText,WIcon,WImage,WSvg - Interactive:
WAnchor,WButton - Overlay:
WPopover - Form (raw):
WInput,WSelect<T>,WCheckbox,WDatePicker - Form (FormField wrappers):
WFormInput,WFormSelect<T>,WFormMultiSelect<T>,WFormCheckbox,WFormDatePicker - Utility:
WKeyboardActions,WindAnimationWrapper
- Layout:
- 19 className parsers in a token-routing pipeline: background (color, gradient, image), border + radius, ring, shadow, opacity, padding, margin, sizing, flexbox + grid, position (relative + absolute + insets), order, overflow, aspect-ratio, z-index, text (size / weight / family / tracking / leading / decoration / transform / align / overflow), animation, transition (duration + easing), svg fill/stroke + preserve-colors, debug.
WindThemeDatawith 23 configurable fields:brightness,colors,screens,containers,fontSizes,fontWeights,tracking,leading,borderWidths,borderRadius,fontFamilies,ringWidths,ringOffsets,applyDefaultFontFamily,syncWithSystem,baseSpacingUnit,ringColor,opacities,zIndices,shadows,transitionDurations,transitionCurves,animations.WindThemeControllerexposestoggleTheme(),setTheme(),updateTheme(),resetToSystem(). OptionalonThemeChangedcallback fires only on user-initiated toggles, not system brightness syncs.- State system, three layers. Automatic (
hover:,focus:viaWAnchorpointer + focus listeners), framework-managed (loading:,disabled:,checked:,error:), consumer-passed (states: Set<String>?for any custom string likeselected:). All states funnel into a singleSet<String>on the parser cache key. - Responsive prefixes (
sm:,md:,lg:,xl:,2xl:, customizable viaWindThemeData.screens), dark mode (dark:), and platform prefixes (ios:,android:,macos:,web:,mobile:,windows:,linux:). Stackable freely. - CSS positioning utilities:
relative,absolute,top-*,right-*,bottom-*,left-*,inset-*,inset-x-*,inset-y-*, plus negative variants (-top-*,-inset-*) and arbitrary-px values (top-[24px]). - Child order utilities:
order-0throughorder-12,order-first,order-last,order-none, plus arbitraryorder-[n](including negatives) for reordering flex children without changing source order. - Reverse flex direction:
flex-row-reverseandflex-col-reverseflip the main-axis direction sojustify-startmirrors per CSS semantics. self-*align-self shorthand:self-start,self-end,self-center,self-stretch,self-auto,self-baselineas aliases for thealign-self-*long form, matching Tailwind's canonical class name.- Flex grow / basis tokens:
grow(Tailwind shorthand forflex-grow, i.e.flex: 1),grow-0(no grow), andbasis-1/2/basis-1/3/basis-1/4/basis-full/basis-[Npx]. Basis approximates CSSflex-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-0cancels an earliergrow/flex-grow/flex-N,shrink-0cancels an earliershrink/flex-shrink, andflex-none(flex: 0 0 auto) cancels both, within the same active class list. - Smart column cross-axis stretch: a
flex flex-colwith no explicititems-*token now stretches eachWDiv,WAnchor(any child), andWButtonchild that does not control its own width to the column width (CSSalign-items: stretchdefault), so a clickable nav row (WAnchor(onTap) > WDiv) fills the column width without an explicititems-stretchorw-full. For aWAnchor: when it wraps aWDiv, the innerWDiv's className decides; when it wraps aWTextor raw widget, it always stretches. Children that self-wrap inExpanded/Flexible(grow,flex-grow,flex-auto,flex-initial,shrink,flex-shrink,flex-N, in any state/breakpoint variant), children with an explicitw-*/min-w-*/max-w-*(includingw-full), absolute children, bareWTextleaves, and raw Flutter widgets are left untouched.shrink-0/flex-nonechildren still stretch on the cross axis (their no-shrink effect is main-axis only, matching CSS). Rows are unaffected. - Inline color props:
WDiv.backgroundColorandWText.foregroundColorfor runtime-dynamic colors. Override anybg-*/text-*from className and stay out of the parser cache key. WBreakpointwidget: 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 viabuilders:, restrictable viadenyWidgets:, withcustomIcons:for user-defined icon mappings (24 built-in glyphs). State binding by widgetid, action dispatch viaWActionHandler, max recursion depth 50.- Accessibility / Semantics on 7 interactive widgets (
WAnchor,WButton,WInput,WFormInput,WCheckbox,WSelect,WDatePicker): emitSemanticsnodes with role + label, password fields markobscured. Enables PlaywrightgetByRole/getByLabel/getByTextresolution against the Flutter web build. New optionalsemanticLabelparameters onWInput,WButton, andWAnchor; the button/anchor label gives icon-only controls an accessible name when there is no child text forMergeSemanticsto absorb. Wind.installDebugResolver(): call insidekDebugModeto register aWindDebugResolverImplagainst the newfluttersdk_wind_diagnostics_contractsbridge. Resolves 7 fields per Wind widget element:className,breakpoint,brightness,platform,states, conditionalbgColor, conditionaltextColor. Consumed byfluttersdk_duskfor E2E snapshot capture and by any runtime inspector. Tree-shaken in release builds.wind-uiskill v2.0.2 community pattern:skills/wind-ui/SKILL.mdsection 15 plusskills/wind-ui/references/community.mdadd opt-in star + issue-report CTAs surfaced once per session after a verified Wind task or a genuine wind-side bug. Prose-permission only, never auto-executed,gh auth status-gated. (#89)
Changed #
- BREAKING. Complete API rewrite from 0.0.x. The legacy
lib/src/parsers/(28 modules) andlib/src/components/(8 modules) directories were deleted and reimplemented underlib/src/parser/parsers/andlib/src/widgets/. Class namesWText,WDiv,WButtonare preserved but their constructor signatures, the className token set they accept, and theme integration differ. Not source-compatible with v0. - BREAKING. Flutter SDK minimum raised from
>=3.3.0to>=3.27.0. Dart SDK constraint set to>=3.4.0 <4.0.0. - BREAKING. Parser cache is now always on. The opt-in
trackProvenanceflag onWindParser.parse()and theWindStyle.resolvedViafield are gone; debug-tooling consumers read widget state throughWind.installDebugResolver()and thefluttersdk_wind_diagnostics_contractscontract package instead. - BREAKING. The public
DatePickerModeenum is renamed toWDatePickerMode. The old name collided with Flutter Material's ownDatePickerMode, forcing any consumer importing bothpackage:flutter/material.dartand the wind barrel tohideone symbol.WDatePicker/WFormDatePickermode:now takesWDatePickerMode.
Removed #
- BREAKING.
WindDuskIntegrationclass and thelib/dusk_integration.dartsub-barrel. Replaced byWind.installDebugResolver()from the main barrel. - BREAKING.
fluttersdk_duskas a wind dependency at any level. Consumers needing Dusk for their own E2E add it to their own pubspec. - BREAKING.
google_fontsandplatform_infodependencies. Consumers depending on them transitively must add explicit deps. - BREAKING.
WindParser.parse(trackProvenance:)parameter,WindStyle.resolvedViafield, and theenableProvenance()toggle. The contracts-based diagnostic bridge does not require provenance instrumentation. - BREAKING. 13 internal parser classes (
AspectRatioParser,BackgroundParser,BorderParser,FlexboxGridParser,MarginParser,OpacityParser,OverflowParser,PaddingParser,RingParser,SizingParser,TextParser,TransitionParser,ZIndexParser),WindPlatformService,WindLogger,LogEntry,WDynamicRenderer, andWindDebugResolverImplare no longer exported from the public barrel (package:fluttersdk_wind/fluttersdk_wind.dart). These were always internal implementation details; any consumer referencing them by name must remove those references. The public widget and theme API is unaffected.
Fixed #
WTextbare rendering: aWTextused outside aMaterialApp/Scaffoldnow renders with a brightness-aware baseline color (Colors.whiteon dark platforms,Colors.blackon light, read fromMediaQuery.platformBrightness) instead of Flutter's debug yellow-underline fallback. When noDirectionalityancestor exists,WTextinjects one defaulting toTextDirection.ltr. Explicitly supplied colors (className text-*,foregroundColor,textStyle) still win and are unaffected.- Background image parser (
bg-[/abs/path]): theFileImage(File(...))branch is now guarded bykIsWeb; on web, wheredart:ioFileis unsupported, the image degrades gracefully (skipped) instead of throwing at runtime. Non-web behavior is unchanged.pubspec.yamlnow declares explicit platform support (android,ios,macos,web,linux,windows) so pub.dev platform detection is not narrowed by thedart:ioimport graph. - WASM compatibility: removed
dart:iofrom the library import graph (platform_service.dartnow usesdefaultTargetPlatform; absolute-pathbg-[/...]image resolution moved behind a conditional import). The package is nowis:wasm-ready, raising the pana/pub.dev platform-support score to 20/20 (160/160). (#95) max-w-prose: corrected value from 1040 px (65 × 16, an incorrect approximation) to 512 px, matching the actual parser output. Docs and skill references updated accordingly.WButton/WAnchorsemanticLabel: theSemanticsnode now setsexcludeSemantics: trueand liftsonTap/onLongPressonto itself whensemanticLabelis set, so the label overrides any child text instead of concatenating with it underMergeSemantics, while activation is preserved.Wind.installDebugResolver(): the resolver no longer crashes on a className-less W-widget.WindDebugResolverImpl.resolveguarded its dynamicclassNameread, so a bareWAnchor(orWBreakpoint/WindAnimationWrapper/WKeyboardActions) in the tree no longer throwsNoSuchMethodErrorand abort the entirefluttersdk_dusk/ telescope diagnostic snapshot.WInput:px-*horizontal padding now matches the requested value exactly;OutlineInputBorder.gapPaddingis set to0.0sopx-3produces a 12 px inset instead of 16 px. Multiline geometry unchanged. (#61)WindParser.findAndGroupClasses: duplicate tokens flow through to the parser pipeline so the documented last-class-wins contract holds on repeated overrides liketop-8 top-4 top-8; previously.toSet()dropped the trailing occurrence.WindParser.parse: cache is bypassed in both directions (no read, no write) whenbaseStyleis non-null so per-call styles do not return stale cached entries or poison the cache slot for default-flag callers.example/lib/routes.dart: six widget routes (/widgets/w-input-multiline,/widgets/w-input-search,/widgets/w-popover-alignment,/widgets/w-select_multi,/widgets/w-select_single,/widgets/w-text-transform) renamed to snake_case to match their page-file basenames so live doc iframes atwind.fluttersdk.com/preview/widgets/<key>resolve. Two dead pages (layout/grid_basic,layout/order) without documentation references were removed.BackgroundParser:bg-[#hex]arbitrary-color backgrounds no longer also resolve to a bogusAssetImage("assets/#hex"). The image regex now excludes#-leading bracket values so a hex literal is parsed only as a color, eliminating a stray failed asset fetch on every arbitrary-hex background.flex-none: now means CSSflex: 0 0 auto(no grow AND no shrink). It no longer maps to a shrinkingFlexFit.loose; instead it routes through the same no-shrink path asshrink-0, so aflex-nonechild in ajustify-betweenrow keeps its intrinsic size instead of being forced into aFlexibleshrink allocation.WindStyle.copyWith: a padding-, margin-, or text-only style keepsdecoration == nullinstead of fabricating an emptyBoxDecoration. This stopsWDiv/WTextfrom wrapping a needlessContaineraround non-decorated content.shadow-nonelikewise no longer forces a Container.WDynamicRenderer: malformed JSON degrades gracefully. A non-stringtypeor non-listchildrenis coerced defensively (routed through the whitelist / treated as no children) instead of throwing an implicit-downcastTypeErrorout ofbuild().WindThemeData: implements value-basedoperator ==andhashCode. The equality guards inWindThemeController.setThemeand_WindThemeState.didUpdateWidgetnow compare by value, so a fresh defaultWindThemeData()on a parent rebuild no longer clobbers a priortoggleTheme()choice or triggers spurious full-tree rebuilds.
Quality #
- 1,224 tests across 83 test files; line coverage 90.3% (CI gate enforces
>= 90%via./tool/coverage.sh 90). - New regression coverage in
test/parser/wind_parser_cache_test.dartfor the last-class-wins-on-duplicates andbaseStyle-bypasses-cache contracts. - New regression coverage for the arbitrary-hex background (
background_parser_test.dart),decoration-stays-null contract (wind_style_test.dart), malformed-JSON graceful degradation (w_dynamic_renderer_test.dart),WindThemeDatavalue equality (wind_theme_data_test.dart), and icon-only button Semantics label (w_button_test.dart). tool/coverage.shportable threshold-aware lcov wrapper; GitHub Actions gate fails any PR dropping below 90%.- Surgical
// coverage:ignore-linepragmas only on lines structurally unreachable fromflutter test(kDebugModebranches,dart:ioPlatform.is*branches not matching the CI host). Each pragma carries a one-line WHY comment. - Final 1.0.0 QA gate: added permanent regression suites under
test/pixel/(px-exact geometry + color viagetRect/getSize/renderObject, no golden files),test/interaction/(tap/hover/focus/disabled/responsive/animation/overlay in-process), andtest/performance/(parser cache hit/miss speedup ratio gate, ~26x, plus report-only large-tree pump timing). Validated live via a fresh consumer app driven byfluttersdk_dusk(all routes navigated,wind:enricher 7-field block confirmed, screenshots manually compared). No behavioral regressions found.
Production deps: flutter (SDK), flutter_svg ^2.0.0, fluttersdk_wind_diagnostics_contracts ^1.0.0. Dev deps: flutter_test (SDK), flutter_lints ^5.0.0. Full v1 documentation at fluttersdk.com/wind; LLM-facing skill at skills/wind-ui/ distributed via fluttersdk/ai (npx skills add fluttersdk/ai --skill wind-ui).
Previous versions #
The 1.0.0-alpha.1 through 1.0.0-alpha.10 release notes (Feb 2026 to May 2026) are preserved in git history and on the v0 branch. The 0.0.x line is end-of-life; consumers pin to ^1.0.0 going forward.
