fluttersdk_wind
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.
1.4.1
71K downloads/mo
#1271 most downloaded on pub.dev
fluttersdk/wind
What this package is like to depend on
Last release today
23 Aug 2026
Ships unpredictably
gaps range from 9 days to 8 months
Most releases are documented
notes for 10 of 14 stable releases
Nothing withdrawn
no release was ever pulled
2 years old
20 releases · first in 2025
16 releases in the last 12 months
see the full history below
Release timeline
20 releases · Jan 2025 to Aug 2026Releases
latest 20-
1.4.123 Aug 2026Release notes
Open source →Fixed
-
A multi-line
classNamehid its flex tokens from the row and column composers, and this project's own style guide is what put them there..claude/rules/widgets.mdandSKILL.mdboth instruct a className covering 3+ concerns to be a triple-quoted string with one concern per line, soflex-1at the end of a line arrives at the composition helpers asflex-1\n. Three of them split on a single space and therefore never matched it. Two consequences, both reproduced: aflex-1child of anoverflow-hiddenrow got wrapped a second time and threwIncorrect use of ParentDataWidget(_selfWrapsInFlex), and aw-24 shrink-0child in a crowded row shrank to its 50pt flex share instead of holding 96pt (_hasShrinkZero). The third site,_hasExplicitCrossWidth, never missed at all and is changed only so all five scans tokenize the same way: it matches withstartsWith('w-')rather than equality, and'w-24\n'.startsWith('w-')is true, so a trailing newline never hid an explicit width from it. That is also the real reason a test written for it passed before the fix, and the reason it ships without one. All five token scans inWDivnow share one hoisted_whitespaceRegex, which also stops the two that already split on whitespace from allocating a freshRegExpon every pass through the composition path. (lib/src/widgets/w_div.dart,test/widgets/w_div/multiline_classname_test.dart) -
Every hoverable
WDivannounced itself as a button that cannot be pressed.WDivauto-wraps into aWAnchorwhenever its className carrieshover:,focus:oractive:, purely to get the state this widget tracks, andWAnchorpublishedSemantics(button: true)unconditionally: with no gesture behind it. So a decorative card styledhover:bg-slate-100reached assistive technology as a control. Measured onWDiv(className: 'px-4 py-3 hover:bg-slate-100', child: WText('Latency')): one button node labelledLatencywhose action set wasfocusalone, with notap. A screen reader offers it as a button, the user activates it, and nothing happens, because there is no gesture in the tree to run. A gestureless, UNLABELLEDWAnchornow publishes no semantics node of its own and lets its descendants speak, which is what a state propagator should do (an explicitsemanticLabelstill publishes the named node either way, which is how a disabled control reports that it exists and is unavailable;WDiv's auto-wrap never passes one): the same card keeps itsLatencylabel and loses the role. An anchor withonTap,onLongPressoronDoubleTapis unchanged, soWButtonand every real control keep their single named button node with its tap action, and an explicitsemanticLabelstill wins for the icon-only case that has no child text to merge. One claim to retire with this: the nestingWAnchor(onTap:) > WDiv(hover:...)did NOT announce twice, though it published two button nodes in the widget tree. The inner one carriedisMergedIntoParent, so Flutter folded it into the real tap surface and never sent it to the platform. A reading that counts raw tree nodes rather than platform nodes will report a duplicate announcement that no screen reader ever made.MergeSemanticsgoes with the role rather than staying, and that was measured too: keeping it made a gestureless wrapper ABSORB a descendant control's role and actions, so a locked region tile in a consumer app swallowed the display-onlyWCheckboxinside it and published itself as "US West, button" carrying the checkbox's tap. That is the same bogus claim in a new place, so a styling-only wrapper now publishes nothing at all. (lib/src/widgets/w_anchor.dart,doc/widgets/w-anchor.md,doc/widgets/w-div.md,skills/wind-ui/SKILL.md,skills/wind-ui/references/widgets.md) -
WCheckboxoffered a tap action foronChanged: null. The anchor's callback was gated ondisabledalone, so a caller rendering a display-only checkbox (a read-only summary row, or a tile whose own tap drives the toggle) still installed() => onChanged?.call(!value): a control that announces itself as pressable and runs a no-op when pressed. Found in a consumer's region picker, whereIgnorePointer(child: WCheckbox(value: selected, onChanged: null))published a 16x16 nameless node with a tap action inside a tile that was already doing the work;IgnorePointerblocks pointers and leaves the semantics node in place. The gate is nowdisabled || onChanged == null, and thecheckedstate is still reported either way, so a read-only checkbox still tells assistive technology whether it is ticked. (lib/src/widgets/w_checkbox.dart) -
The same read-only
WCheckboxstill announced itself as an enabled control, and itsdisabled:classes never fired. The gate above removed the false tap affordance and stopped there, so the two statements beside it kept reading thedisabledprop alone:Semantics(enabled: !disabled)publishedenabled: truefor a control with nothing to press, and theactiveStatesset leftdisabledout, soWCheckbox(value: true, onChanged: null, className: 'disabled:opacity-50')rendered at full opacity.WRadioandWSwitchhave deriveddisabled || onChanged == nullonce at the top ofbuildin every released version (both landed before 1.2.0), which means one input produced three different behaviours across three sibling controls and the checkbox was the odd one out. It now derives the same way, and the singleisDisabledfeeds all four consumers: the state set, the semanticsenabledflag, the anchor'sisDisabled, and the gesture. Thecheckedstate is still reported either way, so a display-only checkbox keeps telling assistive technology whether it is ticked. Two visible consequences to expect on upgrade. AWCheckboxwith a nullonChangedanddisabled:classes now renders them, where before they never applied. And on theWDynamicJSON path,_buildWCheckboxpasses whateverparseValueActionreturns, which isnullwheneverprops.onChangeis absent or malformed, so a node like{"type": "WCheckbox", "props": {"id": "agree", "className": "... disabled:opacity-50"}}now renders dimmed and announces itself as not enabled. That node was already inert, since the state write lives inside the same null callback and tapping it never updatedid, so what changes is that a silently non-functional control became a visibly disabled one. (lib/src/widgets/w_checkbox.dart,doc/widgets/w-checkbox.md,skills/wind-ui/SKILL.md,skills/wind-ui/references/widgets.md,example/lib/pages/checkbox/checkbox_basic.dart)
Quality
-
The registry dispatch could never fire, so 1.4.0 shipped a skill the registry never received.
dispatch-to-registry.ymldeclaredrelease: [published], but the release is created insidepublish.yml'sgithub-releasejob bygh release createrunning underGH_TOKEN: ${{ github.token }}, and GitHub does not start workflow runs from events raised byGITHUB_TOKEN. The trigger was added on 2026-08-03 and 1.4.0 was the first release after it, so it had exactly one chance and missed: the run history showed nothing since 2026-08-03, both entries there beingworkflow_dispatchand the retiredpushtrigger. It reachedfluttersdk/aionly because it was dispatched by hand. The cost of this failure mode is that it is silent, since the publish workflow goes green either way and the only symptom is end users installing a skill a version behind.publish.ymlnow calls the workflow directly withneeds: github-release, which removes the cross-workflow event entirely and also guarantees the dispatch happens after pub.dev has accepted the release rather than in parallel with it. The deadreleasetrigger is gone andworkflow_callreplaces it;workflow_dispatchstays as the manual escape hatch. Secrets are passed by name rather thansecrets: inherit, since the called workflow needs exactly two and this repo pins every action by SHA and runs zizmor over the result. (.github/workflows/dispatch-to-registry.yml,.github/workflows/publish.yml) -
The version extractor tested the ref for a
vprefix this repo never tags with, so it always fell through topubspec.yaml. The step matched^v[0-9]+\.[0-9]+\.[0-9]+, whilepublish.yml's tag filter is[0-9]+.[0-9]+.[0-9]+*and CLAUDE.md's release step isgit tag X.Y.Z: that branch could not match a real tag here. Nothing surfaced it because the fallback happens to be correct on master after a release bump, so the dead branch and the working one returned the same string. The test is now^v?[0-9]+\.[0-9]+\.[0-9]+, which reads the tag when there is one and keeps the pubspec fallback for a manual run off a branch. (.github/workflows/dispatch-to-registry.yml) -
The registry sync now skips a prerelease tag, so a beta release does not hand beta skill content to every consumer.
publish.yml's tag filter is[0-9]+.[0-9]+.[0-9]+*, so1.5.0-beta.1matches it and this repo has shipped1.0.0-alpha.*tags. The reason to skip is distribution rather than a malformed version:fluttersdk/ai'ssync.ymlvalidates the upstream version as^[0-9]+\.[0-9]+\.[0-9]+(-[a-z0-9.]+)?$(a prerelease is explicitly allowed) and derives its own version by bumping its own manifest's patch number, so the string sent only reaches its release notes. What a sync does do is rsyncskills/wind-ui/onto the registry'smain, which is whatnpx skills add fluttersdk/aiserves. A branch name containing a dash cannot trip the guard, because on aworkflow_dispatchthegithub-releasejob is skipped by its own tag check and a dependant of a skipped job is skipped with it. (.github/workflows/publish.yml) -
The called workflow's checkout stops carrying a token it does not use. It now sets
persist-credentials: falselike every other checkout here: the job only readspubspec.yaml, zizmor'sartipackedaudit is on by default, and this change is what puts the workflow inside the release pipeline. (.github/workflows/dispatch-to-registry.yml) -
The skill's own title had been reading
Wind UI 1.3since the 1.4.0 release, and the release checklist is why. That checklist names three version spots inskills/wind-ui/: the ninereferences/*.mdH1s, the SKILL.mddescriptionprefix, and the<!-- fluttersdk_wind X.Y.x | Skill vN -->marker. There is a fourth, SKILL.md's own H1 on line 10, and nothing pointed at it, so 1.4.0 moved the other three and left this one behind. It is the first line of the file an agent loads. Corrected here with the rest of the 1.4.1 surface pass (pubspec.yaml,example/pubspec.yaml, thedartdoc_options.yamlsource-link tag, thellms.txtversion string); the nine reference H1s and the marker already read1.4and need no change for a patch. (skills/wind-ui/SKILL.md,pubspec.yaml,example/pubspec.yaml,dartdoc_options.yaml,llms.txt)
Release notes
Open source →Fixed
-
A multi-line
classNamehid its flex tokens from the row and column composers, and this project's own style guide is what put them there..claude/rules/widgets.mdandSKILL.mdboth instruct a className covering 3+ concerns to be a triple-quoted string with one concern per line, soflex-1at the end of a line arrives at the composition helpers asflex-1\n. Three of them split on a single space and therefore never matched it. Two consequences, both reproduced: aflex-1child of anoverflow-hiddenrow got wrapped a second time and threwIncorrect use of ParentDataWidget(_selfWrapsInFlex), and aw-24 shrink-0child in a crowded row shrank to its 50pt flex share instead of holding 96pt (_hasShrinkZero). The third site,_hasExplicitCrossWidth, never missed at all and is changed only so all five scans tokenize the same way: it matches withstartsWith('w-')rather than equality, and'w-24\n'.startsWith('w-')is true, so a trailing newline never hid an explicit width from it. That is also the real reason a test written for it passed before the fix, and the reason it ships without one. All five token scans inWDivnow share one hoisted_whitespaceRegex, which also stops the two that already split on whitespace from allocating a freshRegExpon every pass through the composition path. (lib/src/widgets/w_div.dart,test/widgets/w_div/multiline_classname_test.dart) -
Every hoverable
WDivannounced itself as a button that cannot be pressed.WDivauto-wraps into aWAnchorwhenever its className carrieshover:,focus:oractive:, purely to get the state this widget tracks, andWAnchorpublishedSemantics(button: true)unconditionally: with no gesture behind it. So a decorative card styledhover:bg-slate-100reached assistive technology as a control. Measured onWDiv(className: 'px-4 py-3 hover:bg-slate-100', child: WText('Latency')): one button node labelledLatencywhose action set wasfocusalone, with notap. A screen reader offers it as a button, the user activates it, and nothing happens, because there is no gesture in the tree to run. A gestureless, UNLABELLEDWAnchornow publishes no semantics node of its own and lets its descendants speak, which is what a state propagator should do (an explicitsemanticLabelstill publishes the named node either way, which is how a disabled control reports that it exists and is unavailable;WDiv's auto-wrap never passes one): the same card keeps itsLatencylabel and loses the role. An anchor withonTap,onLongPressoronDoubleTapis unchanged, soWButtonand every real control keep their single named button node with its tap action, and an explicitsemanticLabelstill wins for the icon-only case that has no child text to merge. One claim to retire with this: the nestingWAnchor(onTap:) > WDiv(hover:...)did NOT announce twice, though it published two button nodes in the widget tree. The inner one carriedisMergedIntoParent, so Flutter folded it into the real tap surface and never sent it to the platform. A reading that counts raw tree nodes rather than platform nodes will report a duplicate announcement that no screen reader ever made.MergeSemanticsgoes with the role rather than staying, and that was measured too: keeping it made a gestureless wrapper ABSORB a descendant control's role and actions, so a locked region tile in a consumer app swallowed the display-onlyWCheckboxinside it and published itself as "US West, button" carrying the checkbox's tap. That is the same bogus claim in a new place, so a styling-only wrapper now publishes nothing at all. (lib/src/widgets/w_anchor.dart,doc/widgets/w-anchor.md,doc/widgets/w-div.md,skills/wind-ui/SKILL.md,skills/wind-ui/references/widgets.md) -
WCheckboxoffered a tap action foronChanged: null. The anchor's callback was gated ondisabledalone, so a caller rendering a display-only checkbox (a read-only summary row, or a tile whose own tap drives the toggle) still installed() => onChanged?.call(!value): a control that announces itself as pressable and runs a no-op when pressed. Found in a consumer's region picker, whereIgnorePointer(child: WCheckbox(value: selected, onChanged: null))published a 16x16 nameless node with a tap action inside a tile that was already doing the work;IgnorePointerblocks pointers and leaves the semantics node in place. The gate is nowdisabled || onChanged == null, and thecheckedstate is still reported either way, so a read-only checkbox still tells assistive technology whether it is ticked. (lib/src/widgets/w_checkbox.dart) -
The same read-only
WCheckboxstill announced itself as an enabled control, and itsdisabled:classes never fired. The gate above removed the false tap affordance and stopped there, so the two statements beside it kept reading thedisabledprop alone:Semantics(enabled: !disabled)publishedenabled: truefor a control with nothing to press, and theactiveStatesset leftdisabledout, soWCheckbox(value: true, onChanged: null, className: 'disabled:opacity-50')rendered at full opacity.WRadioandWSwitchhave deriveddisabled || onChanged == nullonce at the top ofbuildin every released version (both landed before 1.2.0), which means one input produced three different behaviours across three sibling controls and the checkbox was the odd one out. It now derives the same way, and the singleisDisabledfeeds all four consumers: the state set, the semanticsenabledflag, the anchor'sisDisabled, and the gesture. Thecheckedstate is still reported either way, so a display-only checkbox keeps telling assistive technology whether it is ticked. Two visible consequences to expect on upgrade. AWCheckboxwith a nullonChangedanddisabled:classes now renders them, where before they never applied. And on theWDynamicJSON path,_buildWCheckboxpasses whateverparseValueActionreturns, which isnullwheneverprops.onChangeis absent or malformed, so a node like{"type": "WCheckbox", "props": {"id": "agree", "className": "... disabled:opacity-50"}}now renders dimmed and announces itself as not enabled. That node was already inert, since the state write lives inside the same null callback and tapping it never updatedid, so what changes is that a silently non-functional control became a visibly disabled one. (lib/src/widgets/w_checkbox.dart,doc/widgets/w-checkbox.md,skills/wind-ui/SKILL.md,skills/wind-ui/references/widgets.md,example/lib/pages/checkbox/checkbox_basic.dart)
Quality
-
The registry dispatch could never fire, so 1.4.0 shipped a skill the registry never received.
dispatch-to-registry.ymldeclaredrelease: [published], but the release is created insidepublish.yml'sgithub-releasejob bygh release createrunning underGH_TOKEN: ${{ github.token }}, and GitHub does not start workflow runs from events raised byGITHUB_TOKEN. The trigger was added on 2026-08-03 and 1.4.0 was the first release after it, so it had exactly one chance and missed: the run history showed nothing since 2026-08-03, both entries there beingworkflow_dispatchand the retiredpushtrigger. It reachedfluttersdk/aionly because it was dispatched by hand. The cost of this failure mode is that it is silent, since the publish workflow goes green either way and the only symptom is end users installing a skill a version behind.publish.ymlnow calls the workflow directly withneeds: github-release, which removes the cross-workflow event entirely and also guarantees the dispatch happens after pub.dev has accepted the release rather than in parallel with it. The deadreleasetrigger is gone andworkflow_callreplaces it;workflow_dispatchstays as the manual escape hatch. Secrets are passed by name rather thansecrets: inherit, since the called workflow needs exactly two and this repo pins every action by SHA and runs zizmor over the result. (.github/workflows/dispatch-to-registry.yml,.github/workflows/publish.yml) -
The version extractor tested the ref for a
vprefix this repo never tags with, so it always fell through topubspec.yaml. The step matched^v[0-9]+\.[0-9]+\.[0-9]+, whilepublish.yml's tag filter is[0-9]+.[0-9]+.[0-9]+*and CLAUDE.md's release step isgit tag X.Y.Z: that branch could not match a real tag here. Nothing surfaced it because the fallback happens to be correct on master after a release bump, so the dead branch and the working one returned the same string. The test is now^v?[0-9]+\.[0-9]+\.[0-9]+, which reads the tag when there is one and keeps the pubspec fallback for a manual run off a branch. (.github/workflows/dispatch-to-registry.yml) -
The registry sync now skips a prerelease tag, so a beta release does not hand beta skill content to every consumer.
publish.yml's tag filter is[0-9]+.[0-9]+.[0-9]+*, so1.5.0-beta.1matches it and this repo has shipped1.0.0-alpha.*tags. The reason to skip is distribution rather than a malformed version:fluttersdk/ai'ssync.ymlvalidates the upstream version as^[0-9]+\.[0-9]+\.[0-9]+(-[a-z0-9.]+)?$(a prerelease is explicitly allowed) and derives its own version by bumping its own manifest's patch number, so the string sent only reaches its release notes. What a sync does do is rsyncskills/wind-ui/onto the registry'smain, which is whatnpx skills add fluttersdk/aiserves. A branch name containing a dash cannot trip the guard, because on aworkflow_dispatchthegithub-releasejob is skipped by its own tag check and a dependant of a skipped job is skipped with it. (.github/workflows/publish.yml) -
The called workflow's checkout stops carrying a token it does not use. It now sets
persist-credentials: falselike every other checkout here: the job only readspubspec.yaml, zizmor'sartipackedaudit is on by default, and this change is what puts the workflow inside the release pipeline. (.github/workflows/dispatch-to-registry.yml) -
The skill's own title had been reading
Wind UI 1.3since the 1.4.0 release, and the release checklist is why. That checklist names three version spots inskills/wind-ui/: the ninereferences/*.mdH1s, the SKILL.mddescriptionprefix, and the<!-- fluttersdk_wind X.Y.x | Skill vN -->marker. There is a fourth, SKILL.md's own H1 on line 10, and nothing pointed at it, so 1.4.0 moved the other three and left this one behind. It is the first line of the file an agent loads. Corrected here with the rest of the 1.4.1 surface pass (pubspec.yaml,example/pubspec.yaml, thedartdoc_options.yamlsource-link tag, thellms.txtversion string); the nine reference H1s and the marker already read1.4and need no change for a patch. (skills/wind-ui/SKILL.md,pubspec.yaml,example/pubspec.yaml,dartdoc_options.yaml,llms.txt)
-
-
1.4.021 Aug 2026Release notes
Open source →Added
skills/wind-ui/references/design-culture.md: the taste layer the skill never had. Every other reference answers "does this token exist and what does it do"; nothing answered "which token should this be", so an agent handed a screen with no design spec picked plausible values and produced work that rendered correctly and looked wrong. The new file carries the three-level hierarchy with the type scale that implements it, the semantic / status / dark-surface color pair tables (routed through the seededprimarytoken rather than a literalblue-600), the spacing scale with the touch-target floors, HSL palette construction, the depth scale plus the border-free alternatives, mobile form / loading / empty / feedback / list patterns, the iOS navigation and gesture contracts, and a 14-row anti-pattern wall. It came from the distribution repo (fluttersdk/ai), which is a mirror: content that only lived there was onersync --deleteaway from disappearing, and no wind consumer ever received it. Wired into SKILL.md section 11 and the section 14 reference table. Skill version 2.11.0. (skills/wind-ui/references/design-culture.md,skills/wind-ui/SKILL.md)
Changed
- BREAKING (behavioural): a single-line
WInputdefaults its Return key toTextInputAction.doneinstead of.next. Multiline still defaults to.newline, and an explicittextInputActionstill wins, so the escape hatch is unchanged. The old default was wrong in a way that only shows up on a real form: Flutter implements.nextasfocusNode.nextFocus(), which moves to the next FOCUSABLE widget rather than the next text field. Measured on a status-page form orderedName → [segmented control] → Slug → [8 colour swatches] → [Replace/Remove] → Initials → Description, Return on Name focused the segmented-control button, so iOS dismissed the keyboard with nothing editable holding focus, while Return on Initials happened to land on the Description textarea, so the keyboard stayed and the caret jumped. Reported by a user as "why does Enter close the keyboard on one field and move to another on the next one". A key labelled Next that lands on a colour swatch is worse than one labelled Done. Field-to-field advance stays available throughtextInputAction, which is the only place the intent can be stated correctly, since only the form knows its own field order;WKeyboardActionsremains the option for an explicit, data-driven advance order. Any form that relied on the old default gets its behaviour back by passingtextInputAction: TextInputAction.nextper field. (lib/src/widgets/w_input.dart,doc/widgets/w-input.md,skills/wind-ui/references/forms.md)
Fixed
- A
justify-betweenrow starved the one child that asked for the space. Space distribution wrapped every child in aFlexibleto reproduce the CSSflex: 0 1 autoshrink default, but Flutter splits free space equally between flex children, so the wrap also handed a share to siblings that never asked for one. Measured on a two-child page header at 402pt: the row gave 185pt to aflex-1title column and 185pt to an icon column that painted 24pt of it, and the title column, a loose flex child with no leftover left to take, laid out at ZERO width while 140pt of the row sat blank. A grow claim on any child (flex-1,flex-{n},grow,flex-grow,flex-auto, a barew-full, or a rawExpanded/Flexible) now turns the wrap off for the row: the growing child takes the whole remainder and its siblings keep their content width, which is whatjustify-content: space-betweendoes in CSS, where the free space is distributed BETWEEN items rather than made flexible. There is nothing left to distribute once a child grows, so the wrap was only ever about shrinking:overflow-hiddenkeeps it unconditionally because that token asks for shrinking on purpose, and the shrink-only tokens (shrink,flex-shrink,flex-initial, CSSflex: 0 1 auto) still self-wrap to shrink without counting as a claim. A barew-fullcounts because the Row composer already turns exactly that child into anExpanded; leaving it out would have capped it at half the row whileflex-1took the remainder, and the two are documented as equivalent on a row child. A PREFIXED grow token does not count:hover:flex-1andmd:groware conditional and cannot be resolved from the class string, so counting one strips the shrink wrap off every sibling at a state or breakpoint where nothing actually grows, which measured a text sibling laying out at 504 in a 100pt row withA RenderFlex overflowed by 424 pixels on the right. That mirrors the policymd:w-fullalready had for the same reason, and it leaves a prefixed grow token on the pre-existing equal-share split while its variant is active rather than trading a starved child for an overflowing row. (lib/src/widgets/w_div.dart,doc/layout/flexbox.md,doc/widgets/w-div.md,skills/wind-ui/SKILL.md,skills/wind-ui/references/layouts.md) WKeyboardActionshosted its toolbar in the nearestOverlay, so in a nested one the toolbar rendered off-screen. The toolbar is positioned in screen terms (bottom: viewInsets.bottom, so it sits on top of the keyboard), and a nested overlay is measured in its own box rather than the screen's. Measured on an iPhone in an app whose page host owns anOverlayinside a scroll view: two overlays, one 402x874 (the screen) and one 402x2146 (the scrolled content), both reporting the same 335pt bottom inset. The entry went into the second, sobottom: 335put the toolbar 335pt from the bottom of the CONTENT, roughly 1300pt below the viewport. It was in the semantics tree the whole time and nowhere on the screen, which is the worst shape this bug can take: a tree assertion passes and the user still has nothing to press. It now inserts intoOverlay.of(context, rootOverlay: true), and consumers need noOverlayof their own sinceMaterialAppandCupertinoAppboth provide the root one. (lib/src/widgets/w_keyboard_actions.dart,doc/widgets/w-keyboard-actions.md,skills/wind-ui/references/theme.md)
Quality
Lint & Testwas red on every open PR, and not one of them had broken anything. The Flutter tool ships ananalysis_options.yamlmigrator that appends ananalyzer.excludeblock forbuild/and the six platform runner directories, and it runs on everyflutter pub get. CI's first step after checkout isflutter pub get, so by the timedart pub publish --dry-runran seven steps later the checkout was dirty, the dry-run reported1 checked-in file is modified in git, and it exits 65 on a warning. Every gate before it was green; the failure was the toolchain editing the repo mid-run. That is the worst shape a red check can take, because it fails identically on a workflow-only Dependabot bump and on a real regression, so the signal stops carrying information. Both files now carry the block the migrator wants, which makes the migrator a no-op and the checkout clean. The alternative, reverting the file inside the workflow before the dry-run, was rejected: it would leave every contributor's tree dirty after apub getand hide the drift instead of settling it. The excludes are also correct on their own terms, since none of those directories hold hand-written Dart. (analysis_options.yaml,example/analysis_options.yaml)- 48 branches had accumulated, 45 of them PRs that landed months ago.
delete_branch_on_mergewas off, so every task branch outlived its merge and the list grew by one per PR since December 2025. It is on now, which handles everything from here without a workflow, a token or a cron: the setting fires on the merge event alone, touches only that PR's head branch, and cannot reachmasterorv0because both are protected. The 46 leftovers (45 merged, plus a branch from the abandoned release-please setup whose PR #80 was closed unmerged) are deleted. The tempting alternative, a scheduled stale-branch job, was rejected: with the setting on it would only ever catch branches that never merged, this repo has produced exactly one of those in its history, and its "untouched for N days" test cannot tell an abandoned branch from one you set down for a fortnight. A merge is a statement of intent; a date is not. Recorded inCLAUDE.mdunder Branching, because a policy nobody wrote down is not a policy. - Do not reach for
git branch --mergedin this repo. The merge button squashes, which writes a new commit and severs the branch's ancestry tomaster, so all 45 landed branches reported as unmerged while their PRs read MERGED. A cleanup script built on that signal deletes nothing, and one built on content comparison deletes the wrong thing. The audit ran off PR state instead, and its guard earned its keep: it refused a local branch whose tip had drifted from the remote by one unpushed commit, which turned out to be a popover fix that reachedmasterthrough #157 under a different commit. - The Dependabot auto-merge job failed on every bump, and it was never going to pass. Its first line was
gh pr review --approverun withGITHUB_TOKEN, against a repo where "Allow GitHub Actions to create and approve pull requests" is off. GitHub answeredGraphQL: GitHub Actions is not permitted to approve pull requests (addPullRequestReview), the step exited 1, and thegh pr merge --autoline underneath it never ran once. Three open bumps (#169, #170, #171) each carried that red check while every other gate was green, which is the worst training a CI surface can give you: a check that is always red is a check you stop reading. The approve line is gone and the approval stays human on purpose, because this repo pins every action by SHA and runs zizmor plus scorecard over the result, so a person checking the new SHA against its upstream tag is the point of the exercise rather than paperwork in front of it. Arming auto-merge is now the whole job, so an approval merges the PR by itself. The repo settingallow_auto_mergewas off as well, which means even a successful approve would have failed on the very next line; it is on now. (.github/workflows/dependabot-auto-merge.yml) - Nothing was gating a merge into
master. Branch protection required one approving review and setstrict: true, butrequired_status_checks.contextswas empty, soLint & Testcould be red, or still running, and the merge button stayed green anyway. That is survivable while a human clicks merge after reading the checks by eye; it is not survivable next to an armed auto-merge, which fires the moment the approval lands.Lint & TestandInternal Links & Previewsare required contexts now.zizmor SASTis deliberately NOT among them:zizmor.ymlfilters onpaths: ['.github/workflows/**'], so a PR touching onlylib/never triggers it, the context never reports, and the PR waits forever on a status that is never coming. That is the same trapdocs-link-check.ymlspells out in its own trigger comment, which is why that workflow carries no paths filter. - The docs had no link gate, and fluttersdk.com never had one either. The site ingests
doc/verbatim:DocLinkExtensionstrips the.mdsuffix off an internal link without checking the target resolves,DocsScaffolderupserts every page it walks, and the<x-preview>component builds its demo iframe URL and its GitHub blob URL straight from the tag attributes. Nothing in that path validates anything, so a typo synced cleanly and shipped as a dead link or an empty preview, and the only detector was a reader.tool/check-docs.pynow closes it from the repo side across seven surfaces: one H1 per page and it is the page's opening line (the file shape.claude/rules/docs.mddocuments; the site itself reads the title from the first H1 wherever it sits), relative.mdtargets that stay insidedoc/(the only directory the site publishes), same-page and cross-page fragments, explicit anchors listed in the page's own table of contents,<x-preview source>resolving to a real file underexample/lib/pages/,<x-preview path>matching a route registered inexample/lib/routes.dart(the iframe URL is the preview base plus that path, so an unregistered path renders an empty frame) and naming the same example as itssource, and absolutefluttersdk.com/wind/....mdURLs resolving to a page that exists. Pure stdlib Python with no network and no Flutter toolchain, so the same command runs locally and in CI. The newdocs-link-check.ymlruns it per PR next to an offlinelycheepass that covers the HTML<a href>and<img src>forms the script does not parse, then checks external URLs weekly on a schedule where a rate-limited host cannot block a doc merge. Fragments are deliberately the script's job and not lychee's: the bundled lychee 0.24.2 slugifies headings with a simplified kebab-case that collapses runs of whitespace, so## Date + Time Selectionwould be reported broken even though GitHub and the site both render it as#date--time-selection. (tool/check-docs.py,.github/workflows/docs-link-check.yml,.lycheeignore) - The new gate found five live defects on its first run.
doc/typography/text-color.mdlinkedtext-decoration-color.md, a page that has never existed, now pointing at./text-decoration.md#decoration-colorwhere that content lives.doc/core-concepts/debugging.mdand four entries inllms.txtlinkedfluttersdk.com/wind/<section>/index.md; section landing pages are generated by the site and have no backing file, so all five answered 404 (verified against the live site) and now use the extensionless section URL.doc/layout/overflow.mdanddoc/typography/text-overflow.mdeach had a real##section missing from the table of contents (#min-width-scroll,#customizing-theme), anddoc/interactivity/animation.mdcarried a stray<a name="preview">above its<x-preview>that nothing linked to. (doc/typography/text-color.md,doc/core-concepts/debugging.md,doc/layout/overflow.md,doc/typography/text-overflow.md,doc/interactivity/animation.md,llms.txt) - The registry dispatch fires on a published release now, not on every push that touches the skill. Under the push trigger
fluttersdk/aiclimbed to v1.3.75, and most of those releases re-published identical skill content: a docs commit and a release commit each cost the registry a version. The registry version now tracks published wind releases instead of counting commits.workflow_dispatchstays as the manual escape hatch when a skill fix has to reach users before the next release. (.github/workflows/dispatch-to-registry.yml) - The skill's trigger surface was 614 characters past the point where Claude Code truncates it.
descriptionalone ran 1,526 characters against a 1,536-character listing budget shared withwhen_to_use, so the entirewhen_to_useblock (624 characters) was invisible at the moment the model decides whether to load the skill, and the tail of the description went with it. Both are now single-line and total 1,443. Nothing was lost: the full 27-widget roster lives in section 2, the parser count and the alias pipeline inreferences/tokens.md, the recipe emission order in section 2's recipe subsection, and the complete prefix list in sections 3 and 4. A description that repeats the body spends the selection budget on text the model already gets after it decides. (skills/wind-ui/SKILL.md) references/design-culture.mdgained the## Contentsblock every other long reference in this skill carries, so a partial read cannot miss the scope. (skills/wind-ui/references/design-culture.md)- The
wind-uiskill references still carried aWind 1.2title after the 1.3.0 release; all nine now readWind 1.3, and the skill version moved to2.11.0. (skills/wind-ui/SKILL.md,skills/wind-ui/references/*.md) - The token catalog presented
primaryas a family the consumer has to register, wrong since 1.2.0 seeded it. The default family list now names it (23 families), saysbg-primary/text-primary/border-primaryresolve with no registration, and points at the single override that rebrandsWSelect/WCheckbox/WRadio/WDatePicker. The custom-family example moved to abrandkey so it stops modelling an override as a registration. (skills/wind-ui/references/tokens.md) - The token catalog had no way to tell a deliberate no-op from a typo: section 17 gained a "Recognised as deliberate no-ops (no debug hint)" subsection mirroring
_knownUnparsedTokens(thetransitionshorthands,antialiased,sr-only, the font-variant-numeric family, the inline display keywords) plus theobject-*family, which never reachesWindStyleand yet changes rendering becauseWImagereads it straight off the className. (skills/wind-ui/references/tokens.md, mirrorslib/src/parser/wind_parser.dart) theme.mdcounted 22 color families in two places; the seededprimarymakes 23. (skills/wind-ui/references/theme.md)doc/utilities/context-extensions.mddemonstratedcontext.windTheme.setBrightness(Brightness.dark), a methodWindThemeControllerhas never exposed, so a reader following the page hit a compile error. Replaced with the two calls that actually pin a preference:toggleTheme()(which setssyncWithSystem: falseitself) andsetTheme(data.copyWith(brightness: ..., syncWithSystem: false)), plusresetToSystem()to hand control back. A bareupdateTheme(brightness: ...)is the partial-update call the framework's owndidChangePlatformBrightnesslistener makes, so withsyncWithSystemstill true the next OS change overwrites it. (doc/utilities/context-extensions.md)llms.txtpointedWFormMultiSelectatwidgets/w-form-multiselect.md, which does not exist; the widget is documented insidew-form-select.md. (llms.txt)- The README acceptance criterion in
CLAUDE.mdstill named the pre-1.2.0 roster (22 widgets, 19 parsers), which is what a future session would have synced the README back to. It now matches the shipped surface (27 widgets, 20 parsers, 24 theme fields). (CLAUDE.md) - Release surfaces moved to the 1.4 line in one pass:
pubspec.yaml,example/pubspec.yaml, thedartdoc_options.yamlsource-link tag (which pins generated dartdoc line links to the release tag, so a stale value points readers at the previous release's line numbers), thellms.txtversion string, and the nineskills/wind-ui/references/*.mdH1s plus the SKILL.md description and version marker. The skill titles are the ones that rot quietly: they lagged a release twice already, at 1.2.0 and again at 1.3.0. (pubspec.yaml,example/pubspec.yaml,dartdoc_options.yaml,llms.txt,skills/wind-ui/)
Release notes
Open source →Added
skills/wind-ui/references/design-culture.md: the taste layer the skill never had. Every other reference answers "does this token exist and what does it do"; nothing answered "which token should this be", so an agent handed a screen with no design spec picked plausible values and produced work that rendered correctly and looked wrong. The new file carries the three-level hierarchy with the type scale that implements it, the semantic / status / dark-surface color pair tables (routed through the seededprimarytoken rather than a literalblue-600), the spacing scale with the touch-target floors, HSL palette construction, the depth scale plus the border-free alternatives, mobile form / loading / empty / feedback / list patterns, the iOS navigation and gesture contracts, and a 14-row anti-pattern wall. It came from the distribution repo (fluttersdk/ai), which is a mirror: content that only lived there was onersync --deleteaway from disappearing, and no wind consumer ever received it. Wired into SKILL.md section 11 and the section 14 reference table. Skill version 2.11.0. (skills/wind-ui/references/design-culture.md,skills/wind-ui/SKILL.md)
Changed
- BREAKING (behavioural): a single-line
WInputdefaults its Return key toTextInputAction.doneinstead of.next. Multiline still defaults to.newline, and an explicittextInputActionstill wins, so the escape hatch is unchanged. The old default was wrong in a way that only shows up on a real form: Flutter implements.nextasfocusNode.nextFocus(), which moves to the next FOCUSABLE widget rather than the next text field. Measured on a status-page form orderedName → [segmented control] → Slug → [8 colour swatches] → [Replace/Remove] → Initials → Description, Return on Name focused the segmented-control button, so iOS dismissed the keyboard with nothing editable holding focus, while Return on Initials happened to land on the Description textarea, so the keyboard stayed and the caret jumped. Reported by a user as "why does Enter close the keyboard on one field and move to another on the next one". A key labelled Next that lands on a colour swatch is worse than one labelled Done. Field-to-field advance stays available throughtextInputAction, which is the only place the intent can be stated correctly, since only the form knows its own field order;WKeyboardActionsremains the option for an explicit, data-driven advance order. Any form that relied on the old default gets its behaviour back by passingtextInputAction: TextInputAction.nextper field. (lib/src/widgets/w_input.dart,doc/widgets/w-input.md,skills/wind-ui/references/forms.md)
Fixed
- A
justify-betweenrow starved the one child that asked for the space. Space distribution wrapped every child in aFlexibleto reproduce the CSSflex: 0 1 autoshrink default, but Flutter splits free space equally between flex children, so the wrap also handed a share to siblings that never asked for one. Measured on a two-child page header at 402pt: the row gave 185pt to aflex-1title column and 185pt to an icon column that painted 24pt of it, and the title column, a loose flex child with no leftover left to take, laid out at ZERO width while 140pt of the row sat blank. A grow claim on any child (flex-1,flex-{n},grow,flex-grow,flex-auto, a barew-full, or a rawExpanded/Flexible) now turns the wrap off for the row: the growing child takes the whole remainder and its siblings keep their content width, which is whatjustify-content: space-betweendoes in CSS, where the free space is distributed BETWEEN items rather than made flexible. There is nothing left to distribute once a child grows, so the wrap was only ever about shrinking:overflow-hiddenkeeps it unconditionally because that token asks for shrinking on purpose, and the shrink-only tokens (shrink,flex-shrink,flex-initial, CSSflex: 0 1 auto) still self-wrap to shrink without counting as a claim. A barew-fullcounts because the Row composer already turns exactly that child into anExpanded; leaving it out would have capped it at half the row whileflex-1took the remainder, and the two are documented as equivalent on a row child. A PREFIXED grow token does not count:hover:flex-1andmd:groware conditional and cannot be resolved from the class string, so counting one strips the shrink wrap off every sibling at a state or breakpoint where nothing actually grows, which measured a text sibling laying out at 504 in a 100pt row withA RenderFlex overflowed by 424 pixels on the right. That mirrors the policymd:w-fullalready had for the same reason, and it leaves a prefixed grow token on the pre-existing equal-share split while its variant is active rather than trading a starved child for an overflowing row. (lib/src/widgets/w_div.dart,doc/layout/flexbox.md,doc/widgets/w-div.md,skills/wind-ui/SKILL.md,skills/wind-ui/references/layouts.md) WKeyboardActionshosted its toolbar in the nearestOverlay, so in a nested one the toolbar rendered off-screen. The toolbar is positioned in screen terms (bottom: viewInsets.bottom, so it sits on top of the keyboard), and a nested overlay is measured in its own box rather than the screen's. Measured on an iPhone in an app whose page host owns anOverlayinside a scroll view: two overlays, one 402x874 (the screen) and one 402x2146 (the scrolled content), both reporting the same 335pt bottom inset. The entry went into the second, sobottom: 335put the toolbar 335pt from the bottom of the CONTENT, roughly 1300pt below the viewport. It was in the semantics tree the whole time and nowhere on the screen, which is the worst shape this bug can take: a tree assertion passes and the user still has nothing to press. It now inserts intoOverlay.of(context, rootOverlay: true), and consumers need noOverlayof their own sinceMaterialAppandCupertinoAppboth provide the root one. (lib/src/widgets/w_keyboard_actions.dart,doc/widgets/w-keyboard-actions.md,skills/wind-ui/references/theme.md)
Quality
Lint & Testwas red on every open PR, and not one of them had broken anything. The Flutter tool ships ananalysis_options.yamlmigrator that appends ananalyzer.excludeblock forbuild/and the six platform runner directories, and it runs on everyflutter pub get. CI's first step after checkout isflutter pub get, so by the timedart pub publish --dry-runran seven steps later the checkout was dirty, the dry-run reported1 checked-in file is modified in git, and it exits 65 on a warning. Every gate before it was green; the failure was the toolchain editing the repo mid-run. That is the worst shape a red check can take, because it fails identically on a workflow-only Dependabot bump and on a real regression, so the signal stops carrying information. Both files now carry the block the migrator wants, which makes the migrator a no-op and the checkout clean. The alternative, reverting the file inside the workflow before the dry-run, was rejected: it would leave every contributor's tree dirty after apub getand hide the drift instead of settling it. The excludes are also correct on their own terms, since none of those directories hold hand-written Dart. (analysis_options.yaml,example/analysis_options.yaml)- 48 branches had accumulated, 45 of them PRs that landed months ago.
delete_branch_on_mergewas off, so every task branch outlived its merge and the list grew by one per PR since December 2025. It is on now, which handles everything from here without a workflow, a token or a cron: the setting fires on the merge event alone, touches only that PR's head branch, and cannot reachmasterorv0because both are protected. The 46 leftovers (45 merged, plus a branch from the abandoned release-please setup whose PR #80 was closed unmerged) are deleted. The tempting alternative, a scheduled stale-branch job, was rejected: with the setting on it would only ever catch branches that never merged, this repo has produced exactly one of those in its history, and its "untouched for N days" test cannot tell an abandoned branch from one you set down for a fortnight. A merge is a statement of intent; a date is not. Recorded inCLAUDE.mdunder Branching, because a policy nobody wrote down is not a policy. - Do not reach for
git branch --mergedin this repo. The merge button squashes, which writes a new commit and severs the branch's ancestry tomaster, so all 45 landed branches reported as unmerged while their PRs read MERGED. A cleanup script built on that signal deletes nothing, and one built on content comparison deletes the wrong thing. The audit ran off PR state instead, and its guard earned its keep: it refused a local branch whose tip had drifted from the remote by one unpushed commit, which turned out to be a popover fix that reachedmasterthrough #157 under a different commit. - The Dependabot auto-merge job failed on every bump, and it was never going to pass. Its first line was
gh pr review --approverun withGITHUB_TOKEN, against a repo where "Allow GitHub Actions to create and approve pull requests" is off. GitHub answeredGraphQL: GitHub Actions is not permitted to approve pull requests (addPullRequestReview), the step exited 1, and thegh pr merge --autoline underneath it never ran once. Three open bumps (#169, #170, #171) each carried that red check while every other gate was green, which is the worst training a CI surface can give you: a check that is always red is a check you stop reading. The approve line is gone and the approval stays human on purpose, because this repo pins every action by SHA and runs zizmor plus scorecard over the result, so a person checking the new SHA against its upstream tag is the point of the exercise rather than paperwork in front of it. Arming auto-merge is now the whole job, so an approval merges the PR by itself. The repo settingallow_auto_mergewas off as well, which means even a successful approve would have failed on the very next line; it is on now. (.github/workflows/dependabot-auto-merge.yml) - Nothing was gating a merge into
master. Branch protection required one approving review and setstrict: true, butrequired_status_checks.contextswas empty, soLint & Testcould be red, or still running, and the merge button stayed green anyway. That is survivable while a human clicks merge after reading the checks by eye; it is not survivable next to an armed auto-merge, which fires the moment the approval lands.Lint & TestandInternal Links & Previewsare required contexts now.zizmor SASTis deliberately NOT among them:zizmor.ymlfilters onpaths: ['.github/workflows/**'], so a PR touching onlylib/never triggers it, the context never reports, and the PR waits forever on a status that is never coming. That is the same trapdocs-link-check.ymlspells out in its own trigger comment, which is why that workflow carries no paths filter. - The docs had no link gate, and fluttersdk.com never had one either. The site ingests
doc/verbatim:DocLinkExtensionstrips the.mdsuffix off an internal link without checking the target resolves,DocsScaffolderupserts every page it walks, and the<x-preview>component builds its demo iframe URL and its GitHub blob URL straight from the tag attributes. Nothing in that path validates anything, so a typo synced cleanly and shipped as a dead link or an empty preview, and the only detector was a reader.tool/check-docs.pynow closes it from the repo side across seven surfaces: one H1 per page and it is the page's opening line (the file shape.claude/rules/docs.mddocuments; the site itself reads the title from the first H1 wherever it sits), relative.mdtargets that stay insidedoc/(the only directory the site publishes), same-page and cross-page fragments, explicit anchors listed in the page's own table of contents,<x-preview source>resolving to a real file underexample/lib/pages/,<x-preview path>matching a route registered inexample/lib/routes.dart(the iframe URL is the preview base plus that path, so an unregistered path renders an empty frame) and naming the same example as itssource, and absolutefluttersdk.com/wind/....mdURLs resolving to a page that exists. Pure stdlib Python with no network and no Flutter toolchain, so the same command runs locally and in CI. The newdocs-link-check.ymlruns it per PR next to an offlinelycheepass that covers the HTML<a href>and<img src>forms the script does not parse, then checks external URLs weekly on a schedule where a rate-limited host cannot block a doc merge. Fragments are deliberately the script's job and not lychee's: the bundled lychee 0.24.2 slugifies headings with a simplified kebab-case that collapses runs of whitespace, so## Date + Time Selectionwould be reported broken even though GitHub and the site both render it as#date--time-selection. (tool/check-docs.py,.github/workflows/docs-link-check.yml,.lycheeignore) - The new gate found five live defects on its first run.
doc/typography/text-color.mdlinkedtext-decoration-color.md, a page that has never existed, now pointing at./text-decoration.md#decoration-colorwhere that content lives.doc/core-concepts/debugging.mdand four entries inllms.txtlinkedfluttersdk.com/wind/<section>/index.md; section landing pages are generated by the site and have no backing file, so all five answered 404 (verified against the live site) and now use the extensionless section URL.doc/layout/overflow.mdanddoc/typography/text-overflow.mdeach had a real##section missing from the table of contents (#min-width-scroll,#customizing-theme), anddoc/interactivity/animation.mdcarried a stray<a name="preview">above its<x-preview>that nothing linked to. (doc/typography/text-color.md,doc/core-concepts/debugging.md,doc/layout/overflow.md,doc/typography/text-overflow.md,doc/interactivity/animation.md,llms.txt) - The registry dispatch fires on a published release now, not on every push that touches the skill. Under the push trigger
fluttersdk/aiclimbed to v1.3.75, and most of those releases re-published identical skill content: a docs commit and a release commit each cost the registry a version. The registry version now tracks published wind releases instead of counting commits.workflow_dispatchstays as the manual escape hatch when a skill fix has to reach users before the next release. (.github/workflows/dispatch-to-registry.yml) - The skill's trigger surface was 614 characters past the point where Claude Code truncates it.
descriptionalone ran 1,526 characters against a 1,536-character listing budget shared withwhen_to_use, so the entirewhen_to_useblock (624 characters) was invisible at the moment the model decides whether to load the skill, and the tail of the description went with it. Both are now single-line and total 1,443. Nothing was lost: the full 27-widget roster lives in section 2, the parser count and the alias pipeline inreferences/tokens.md, the recipe emission order in section 2's recipe subsection, and the complete prefix list in sections 3 and 4. A description that repeats the body spends the selection budget on text the model already gets after it decides. (skills/wind-ui/SKILL.md) references/design-culture.mdgained the## Contentsblock every other long reference in this skill carries, so a partial read cannot miss the scope. (skills/wind-ui/references/design-culture.md)- The
wind-uiskill references still carried aWind 1.2title after the 1.3.0 release; all nine now readWind 1.3, and the skill version moved to2.11.0. (skills/wind-ui/SKILL.md,skills/wind-ui/references/*.md) - The token catalog presented
primaryas a family the consumer has to register, wrong since 1.2.0 seeded it. The default family list now names it (23 families), saysbg-primary/text-primary/border-primaryresolve with no registration, and points at the single override that rebrandsWSelect/WCheckbox/WRadio/WDatePicker. The custom-family example moved to abrandkey so it stops modelling an override as a registration. (skills/wind-ui/references/tokens.md) - The token catalog had no way to tell a deliberate no-op from a typo: section 17 gained a "Recognised as deliberate no-ops (no debug hint)" subsection mirroring
_knownUnparsedTokens(thetransitionshorthands,antialiased,sr-only, the font-variant-numeric family, the inline display keywords) plus theobject-*family, which never reachesWindStyleand yet changes rendering becauseWImagereads it straight off the className. (skills/wind-ui/references/tokens.md, mirrorslib/src/parser/wind_parser.dart) theme.mdcounted 22 color families in two places; the seededprimarymakes 23. (skills/wind-ui/references/theme.md)doc/utilities/context-extensions.mddemonstratedcontext.windTheme.setBrightness(Brightness.dark), a methodWindThemeControllerhas never exposed, so a reader following the page hit a compile error. Replaced with the two calls that actually pin a preference:toggleTheme()(which setssyncWithSystem: falseitself) andsetTheme(data.copyWith(brightness: ..., syncWithSystem: false)), plusresetToSystem()to hand control back. A bareupdateTheme(brightness: ...)is the partial-update call the framework's owndidChangePlatformBrightnesslistener makes, so withsyncWithSystemstill true the next OS change overwrites it. (doc/utilities/context-extensions.md)llms.txtpointedWFormMultiSelectatwidgets/w-form-multiselect.md, which does not exist; the widget is documented insidew-form-select.md. (llms.txt)- The README acceptance criterion in
CLAUDE.mdstill named the pre-1.2.0 roster (22 widgets, 19 parsers), which is what a future session would have synced the README back to. It now matches the shipped surface (27 widgets, 20 parsers, 24 theme fields). (CLAUDE.md) - Release surfaces moved to the 1.4 line in one pass:
pubspec.yaml,example/pubspec.yaml, thedartdoc_options.yamlsource-link tag (which pins generated dartdoc line links to the release tag, so a stale value points readers at the previous release's line numbers), thellms.txtversion string, and the nineskills/wind-ui/references/*.mdH1s plus the SKILL.md description and version marker. The skill titles are the ones that rot quietly: they lagged a release twice already, at 1.2.0 and again at 1.3.0. (pubspec.yaml,example/pubspec.yaml,dartdoc_options.yaml,llms.txt,skills/wind-ui/)
-
1.3.003 Aug 2026Release notes
Open source →Added
WDatePickerMode.dateTime: the picker can now capture a time of day, not just a date. Every previous mode ran its selection through_normalizeToDay, so the emitted value was always midnight and no input anywhere in the ecosystem could express "10 Aug 2026, 14:30"; a consumer needing a scheduled window had to invent its own control. The new mode composes the tapped day with a time row and emits a plain localDateTimecarrying the hour and minute (no timezone conversion:DateTimecannot hold an arbitrary offset, so a value crossing the wire is the caller'stoUtc()to make). The time row is authored in Wind markup, two 24-hour spinners plus a confirm control built fromWDiv/WText/WIconand className tokens with theirdark:pairs, NOT a MaterialshowTimePickerdialog; it exposes a labelled Semantics container and per-button Semantics, honorsminDate/maxDateon the full instant (a step that would leave the window renders disabled, and a day tap that would land outside it is pulled to the bound), and never wraps 23:00 up to 00:00, which would move the instant a day backwards. New props:minuteStep(default 5, asserted between 1 and 59 on BOTH widgets so a bad value names the one the caller wrote),timeLabel,doneLabel, forwarded byWFormDatePicker.singleandrangebehavior, the default mode, and the emitted midnight values are unchanged. (lib/src/widgets/w_date_picker.dart,lib/src/widgets/w_form_date_picker.dart)
Fixed
- The
dateTimetime row's step controls and confirm control now carry aSemanticsAction.tapof their own.Semantics(button: true)declared the role, but theGestureDetectorunderneath was not that node's semantics owner, so a screen reader (or an automation driver like dusk) could name each control and had nothing to invoke; a pointer tap was the only way in. A disabled step control still exposes no action. (lib/src/widgets/w_date_picker.dart) - Clearing a controlled
dateTimevalue (a form reset settingvalueback tonull) now re-seeds the time row from the wall clock instead of leaving the last picked time on display, which advertised a time the picker no longer held. (lib/src/widgets/w_date_picker.dart) minuteStepis clamped to 1-59 at the point of use. The constructor assert on both widgets is stripped from a release build, where a step computed at runtime (a remote config value, a user preference) could still arrive as0and leave the minute spinners silently inert. (lib/src/widgets/w_date_picker.dart)
Quality
WDatePickerMode.dateTimegained its example page,example/lib/pages/widgets/date_picker_datetime.dart(basic usage,minuteStep, row labels +displayFormat, an instant-bounded maintenance window pair, and aWFormDatePickerwith a time-of-day validator), wired intoexample/lib/routes.dartand referenced from both date-picker doc pages by<x-preview>. The mode shipped without one. (example/,doc/widgets/w-date-picker.md,doc/widgets/w-form-date-picker.md)- Documented the one-sided
maxDatetrap indateTimemode: a bare-day bound is that day at00:00, so the last day admits only midnight. Pinned by tests on both shapes of bound. The bounds stay instant-level; special-casing a midnightmaxDateinto an end-of-day would make00:00unexpressible. (lib/src/widgets/w_date_picker.dart,doc/widgets/w-date-picker.md,skills/wind-ui/references/widgets.md) - The
dateTimesemantics tests run through atestWidgetsWithSemanticswrapper that disposes theSemanticsHandlein afinally. Disposal cannot move totearDown/addTearDown: flutter_test verifies it in_endOfTestVerifications, which runs first and reports a leaked handle on every test. (test/widgets/w_date_picker_test.dart)
Release notes
Open source →Added
WDatePickerMode.dateTime: the picker can now capture a time of day, not just a date. Every previous mode ran its selection through_normalizeToDay, so the emitted value was always midnight and no input anywhere in the ecosystem could express "10 Aug 2026, 14:30"; a consumer needing a scheduled window had to invent its own control. The new mode composes the tapped day with a time row and emits a plain localDateTimecarrying the hour and minute (no timezone conversion:DateTimecannot hold an arbitrary offset, so a value crossing the wire is the caller'stoUtc()to make). The time row is authored in Wind markup, two 24-hour spinners plus a confirm control built fromWDiv/WText/WIconand className tokens with theirdark:pairs, NOT a MaterialshowTimePickerdialog; it exposes a labelled Semantics container and per-button Semantics, honorsminDate/maxDateon the full instant (a step that would leave the window renders disabled, and a day tap that would land outside it is pulled to the bound), and never wraps 23:00 up to 00:00, which would move the instant a day backwards. New props:minuteStep(default 5, asserted between 1 and 59 on BOTH widgets so a bad value names the one the caller wrote),timeLabel,doneLabel, forwarded byWFormDatePicker.singleandrangebehavior, the default mode, and the emitted midnight values are unchanged. (lib/src/widgets/w_date_picker.dart,lib/src/widgets/w_form_date_picker.dart)
Fixed
- The
dateTimetime row's step controls and confirm control now carry aSemanticsAction.tapof their own.Semantics(button: true)declared the role, but theGestureDetectorunderneath was not that node's semantics owner, so a screen reader (or an automation driver like dusk) could name each control and had nothing to invoke; a pointer tap was the only way in. A disabled step control still exposes no action. (lib/src/widgets/w_date_picker.dart) - Clearing a controlled
dateTimevalue (a form reset settingvalueback tonull) now re-seeds the time row from the wall clock instead of leaving the last picked time on display, which advertised a time the picker no longer held. (lib/src/widgets/w_date_picker.dart) minuteStepis clamped to 1-59 at the point of use. The constructor assert on both widgets is stripped from a release build, where a step computed at runtime (a remote config value, a user preference) could still arrive as0and leave the minute spinners silently inert. (lib/src/widgets/w_date_picker.dart)
Quality
WDatePickerMode.dateTimegained its example page,example/lib/pages/widgets/date_picker_datetime.dart(basic usage,minuteStep, row labels +displayFormat, an instant-bounded maintenance window pair, and aWFormDatePickerwith a time-of-day validator), wired intoexample/lib/routes.dartand referenced from both date-picker doc pages by<x-preview>. The mode shipped without one. (example/,doc/widgets/w-date-picker.md,doc/widgets/w-form-date-picker.md)- Documented the one-sided
maxDatetrap indateTimemode: a bare-day bound is that day at00:00, so the last day admits only midnight. Pinned by tests on both shapes of bound. The bounds stay instant-level; special-casing a midnightmaxDateinto an end-of-day would make00:00unexpressible. (lib/src/widgets/w_date_picker.dart,doc/widgets/w-date-picker.md,skills/wind-ui/references/widgets.md) - The
dateTimesemantics tests run through atestWidgetsWithSemanticswrapper that disposes theSemanticsHandlein afinally. Disposal cannot move totearDown/addTearDown: flutter_test verifies it in_endOfTestVerifications, which runs first and reports a leaked handle on every test. (test/widgets/w_date_picker_test.dart)
-
1.2.121 Jul 2026Release notes
Open source →Fixed
- Unknown theme-spacing tokens (
p-primary,m-foo,top-abc,gap-x-blue, ...) now silently drop instead of throwingArgumentError: Invalid spacing multiplier: <token>insidebuild(), matching the "unknown className is dropped with a debug warning" contract other parsers already follow. AddsWindThemeData.tryGetSpacing(String); the padding, margin, position, and flex-gap parsers now use it (sizing already pre-validated withdouble.tryParse).getSpacingis unchanged for backward compatibility. (lib/src/theme/wind_theme_data.dart,lib/src/parser/parsers/padding_parser.dart,lib/src/parser/parsers/margin_parser.dart,lib/src/parser/parsers/position_parser.dart,lib/src/parser/parsers/flexbox_grid_parser.dart) WAnchornow hit-tests its whole bounds viaHitTestBehavior.translucent. The innerGestureDetectorused the defaultHitTestBehavior.deferToChild, so it only fired when a painted child sat under the exact tap point. An anchor wrapping transparent content (a settings row, a checkbox row, a link with padding) ignored taps that landed on its empty regions, including the element centre that automated drivers and centred pointer events target. It now behaves likeWInput/WPopover, which already use whole-box hit-testing, so the full anchor rectangle is tappable. (lib/src/widgets/w_anchor.dart; covered bytest/widgets/w_anchor_test.dart)WPopoverno longer flickers open-then-closed on the first trigger tap on web, and its menu items now respond on the first open. A leftover focus-loss auto-close (_onFocusChange) dismissed the popover roughly 150ms after the opening click, when web transiently blurs the trigger, so it closed on the same gesture that opened it (and unmounted the overlay before a menu item's tap could resolve). The auto-close is removed; dismissal is now driven solely by an outside tap and programmaticclose, matching the same fix already applied toWSelect. (lib/src/widgets/w_popover.dart)
Release notes
Open source →Fixed
- Unknown theme-spacing tokens (
p-primary,m-foo,top-abc,gap-x-blue, ...) now silently drop instead of throwingArgumentError: Invalid spacing multiplier: <token>insidebuild(), matching the "unknown className is dropped with a debug warning" contract other parsers already follow. AddsWindThemeData.tryGetSpacing(String); the padding, margin, position, and flex-gap parsers now use it (sizing already pre-validated withdouble.tryParse).getSpacingis unchanged for backward compatibility. (lib/src/theme/wind_theme_data.dart,lib/src/parser/parsers/padding_parser.dart,lib/src/parser/parsers/margin_parser.dart,lib/src/parser/parsers/position_parser.dart,lib/src/parser/parsers/flexbox_grid_parser.dart) WAnchornow hit-tests its whole bounds viaHitTestBehavior.translucent. The innerGestureDetectorused the defaultHitTestBehavior.deferToChild, so it only fired when a painted child sat under the exact tap point. An anchor wrapping transparent content (a settings row, a checkbox row, a link with padding) ignored taps that landed on its empty regions, including the element centre that automated drivers and centred pointer events target. It now behaves likeWInput/WPopover, which already use whole-box hit-testing, so the full anchor rectangle is tappable. (lib/src/widgets/w_anchor.dart; covered bytest/widgets/w_anchor_test.dart)WPopoverno longer flickers open-then-closed on the first trigger tap on web, and its menu items now respond on the first open. A leftover focus-loss auto-close (_onFocusChange) dismissed the popover roughly 150ms after the opening click, when web transiently blurs the trigger, so it closed on the same gesture that opened it (and unmounted the overlay before a menu item's tap could resolve). The auto-close is removed; dismissal is now driven solely by an outside tap and programmaticclose, matching the same fix already applied toWSelect. (lib/src/widgets/w_popover.dart)
- Unknown theme-spacing tokens (
-
1.2.007 Jul 2026Release notes
Open source →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.
Release notes
Open source →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.
- Wind's internal
-
1.1.224 Jun 2026Release notes
Open source →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.
Release notes
Open source →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.123 Jun 2026Release notes
Open source →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).)
Release notes
Open source →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.017 Jun 2026Release notes
Open source →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)
Release notes
Open source →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.009 Jun 2026Release notes
Open source →First stable release.
fluttersdk_windis utility-first, Tailwind-syntax styling for Flutter:classNamestrings compile to widget trees through 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.Published to pub.dev: https://pub.dev/packages/fluttersdk_wind/versions/1.0.0
Highlights
- 22 public widgets from a single barrel: layout (
WDiv,WSpacer), structural (WBreakpoint,WDynamic), display (WText,WIcon,WImage,WSvg), interactive (WAnchor,WButton), overlay (WPopover), form (WInput,WSelect,WCheckbox,WDatePicker+ 5WForm*wrappers), utility (WKeyboardActions,WindAnimationWrapper). - 19 className parsers in a token-routing pipeline with an always-on cache (~26x hit/miss speedup).
WindThemeDatawith 23 configurable fields plusWindThemeController(toggleTheme/setTheme/updateTheme/resetToSystem).- Three-layer state system (
hover:/focus:, framework-managedloading:/disabled:/checked:/error:, consumer-passed custom states), responsive prefixes,dark:, and platform prefixes (ios:/android:/web:/mobile:/ ...). - Accessibility / Semantics on 7 interactive widgets for Playwright-style
getByRole/getByLabelresolution against the Flutter web build. Wind.installDebugResolver()bridges runtime diagnostics throughfluttersdk_wind_diagnostics_contracts, consumed byfluttersdk_duskand runtime inspectors; tree-shaken in release builds.- WASM-ready:
dart:ioremoved from the import graph, pana/pub.dev platform score 160/160.
Breaking changes (migration from 0.0.x)
v1 is not source-compatible with v0. Class names
WText,WDiv,WButtonare preserved but constructor signatures, the supported className token set, and theme integration differ throughout. Notable removals:WindDuskIntegration+lib/dusk_integration.dart(replaced byWind.installDebugResolver()),fluttersdk_dusk/google_fonts/platform_infodependencies, and thetrackProvenance/resolvedViaAPI.DatePickerModerenamed toWDatePickerMode. Flutter min raised to>=3.27.0, Dart>=3.4.0.Quality
1,224 tests across 83 files, line coverage 90.3% (CI floor enforced at 90%), plus permanent pixel / interaction / performance regression suites and a live
fluttersdk_dusk-driven QA pass.Full v1 documentation: https://fluttersdk.com/wind
See CHANGELOG.md for the complete entry.
Release notes
Open source →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,WButtonare 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 atskills/wind-ui/distributed via fluttersdk/ai (npx skills add fluttersdk/ai --skill wind-ui).
- 22 public widgets from a single barrel: layout (
-
1.0.0-alpha.604 Apr 2026 pre-releaseNothing published for this version
-
1.0.0-alpha.531 Mar 2026 pre-releaseNothing published for this version
-
1.0.0-alpha.424 Mar 2026 pre-releaseNothing published for this version
-
1.0.0-alpha.305 Feb 2026 pre-releaseNothing published for this version
-
1.0.0-alpha.205 Feb 2026 pre-releaseNothing published for this version
-
1.0.0-alpha.105 Feb 2026 pre-releaseNothing published for this version
-
0.0.530 May 2026Release notes
Open source →- chore: bump version to 0.0.5 and promote changelog
Docs-only release for the 0.0.x line: ship the in-repo doc/ reference. No source code change since 0.0.4.
- docs: update install version pin to ^0.0.5
Caret ^0.0.4 resolves to >=0.0.4 <0.0.5, so it excludes the new release. Bump the pin in README and the installation doc.
-
0.0.412 Jun 2025Nothing published for this version
-
0.0.309 Jun 2025Nothing published for this version
-
0.0.202 Feb 2025Nothing published for this version
-
0.0.129 Jan 2025Nothing published for this version