PackageTrack
Sign in Get early access

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 2026
2026
Release Pre-release

Releases

latest 20
  1. 1.4.1 23 Aug 2026
    Release notes

    Fixed

    • A multi-line className hid its flex tokens from the row and column composers, and this project's own style guide is what put them there. .claude/rules/widgets.md and SKILL.md both instruct a className covering 3+ concerns to be a triple-quoted string with one concern per line, so flex-1 at the end of a line arrives at the composition helpers as flex-1\n. Three of them split on a single space and therefore never matched it. Two consequences, both reproduced: a flex-1 child of an overflow-hidden row got wrapped a second time and threw Incorrect use of ParentDataWidget (_selfWrapsInFlex), and a w-24 shrink-0 child 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 with startsWith('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 in WDiv now share one hoisted _whitespaceRegex, which also stops the two that already split on whitespace from allocating a fresh RegExp on every pass through the composition path. (lib/src/widgets/w_div.dart, test/widgets/w_div/multiline_classname_test.dart)

    • Every hoverable WDiv announced itself as a button that cannot be pressed. WDiv auto-wraps into a WAnchor whenever its className carries hover:, focus: or active:, purely to get the state this widget tracks, and WAnchor published Semantics(button: true) unconditionally: with no gesture behind it. So a decorative card styled hover:bg-slate-100 reached assistive technology as a control. Measured on WDiv(className: 'px-4 py-3 hover:bg-slate-100', child: WText('Latency')): one button node labelled Latency whose action set was focus alone, with no tap. 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, UNLABELLED WAnchor now publishes no semantics node of its own and lets its descendants speak, which is what a state propagator should do (an explicit semanticLabel still 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 its Latency label and loses the role. An anchor with onTap, onLongPress or onDoubleTap is unchanged, so WButton and every real control keep their single named button node with its tap action, and an explicit semanticLabel still wins for the icon-only case that has no child text to merge. One claim to retire with this: the nesting WAnchor(onTap:) > WDiv(hover:...) did NOT announce twice, though it published two button nodes in the widget tree. The inner one carried isMergedIntoParent, 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. MergeSemantics goes 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-only WCheckbox inside 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)

    • WCheckbox offered a tap action for onChanged: null. The anchor's callback was gated on disabled alone, 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, where IgnorePointer(child: WCheckbox(value: selected, onChanged: null)) published a 16x16 nameless node with a tap action inside a tile that was already doing the work; IgnorePointer blocks pointers and leaves the semantics node in place. The gate is now disabled || onChanged == null, and the checked state 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 WCheckbox still announced itself as an enabled control, and its disabled: classes never fired. The gate above removed the false tap affordance and stopped there, so the two statements beside it kept reading the disabled prop alone: Semantics(enabled: !disabled) published enabled: true for a control with nothing to press, and the activeStates set left disabled out, so WCheckbox(value: true, onChanged: null, className: 'disabled:opacity-50') rendered at full opacity. WRadio and WSwitch have derived disabled || onChanged == null once at the top of build in 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 single isDisabled feeds all four consumers: the state set, the semantics enabled flag, the anchor's isDisabled, and the gesture. The checked state 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. A WCheckbox with a null onChanged and disabled: classes now renders them, where before they never applied. And on the WDynamic JSON path, _buildWCheckbox passes whatever parseValueAction returns, which is null whenever props.onChange is 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 updated id, 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.yml declared release: [published], but the release is created inside publish.yml's github-release job by gh release create running under GH_TOKEN: ${{ github.token }}, and GitHub does not start workflow runs from events raised by GITHUB_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 being workflow_dispatch and the retired push trigger. It reached fluttersdk/ai only 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.yml now calls the workflow directly with needs: 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 dead release trigger is gone and workflow_call replaces it; workflow_dispatch stays as the manual escape hatch. Secrets are passed by name rather than secrets: 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 v prefix this repo never tags with, so it always fell through to pubspec.yaml. The step matched ^v[0-9]+\.[0-9]+\.[0-9]+, while publish.yml's tag filter is [0-9]+.[0-9]+.[0-9]+* and CLAUDE.md's release step is git 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]+*, so 1.5.0-beta.1 matches it and this repo has shipped 1.0.0-alpha.* tags. The reason to skip is distribution rather than a malformed version: fluttersdk/ai's sync.yml validates 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 rsync skills/wind-ui/ onto the registry's main, which is what npx skills add fluttersdk/ai serves. A branch name containing a dash cannot trip the guard, because on a workflow_dispatch the github-release job 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: false like every other checkout here: the job only reads pubspec.yaml, zizmor's artipacked audit 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.3 since the 1.4.0 release, and the release checklist is why. That checklist names three version spots in skills/wind-ui/: the nine references/*.md H1s, the SKILL.md description prefix, 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, the dartdoc_options.yaml source-link tag, the llms.txt version string); the nine reference H1s and the marker already read 1.4 and need no change for a patch. (skills/wind-ui/SKILL.md, pubspec.yaml, example/pubspec.yaml, dartdoc_options.yaml, llms.txt)

    Open source →
    Release notes

    Fixed

    • A multi-line className hid its flex tokens from the row and column composers, and this project's own style guide is what put them there. .claude/rules/widgets.md and SKILL.md both instruct a className covering 3+ concerns to be a triple-quoted string with one concern per line, so flex-1 at the end of a line arrives at the composition helpers as flex-1\n. Three of them split on a single space and therefore never matched it. Two consequences, both reproduced: a flex-1 child of an overflow-hidden row got wrapped a second time and threw Incorrect use of ParentDataWidget (_selfWrapsInFlex), and a w-24 shrink-0 child 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 with startsWith('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 in WDiv now share one hoisted _whitespaceRegex, which also stops the two that already split on whitespace from allocating a fresh RegExp on every pass through the composition path. (lib/src/widgets/w_div.dart, test/widgets/w_div/multiline_classname_test.dart)

    • Every hoverable WDiv announced itself as a button that cannot be pressed. WDiv auto-wraps into a WAnchor whenever its className carries hover:, focus: or active:, purely to get the state this widget tracks, and WAnchor published Semantics(button: true) unconditionally: with no gesture behind it. So a decorative card styled hover:bg-slate-100 reached assistive technology as a control. Measured on WDiv(className: 'px-4 py-3 hover:bg-slate-100', child: WText('Latency')): one button node labelled Latency whose action set was focus alone, with no tap. 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, UNLABELLED WAnchor now publishes no semantics node of its own and lets its descendants speak, which is what a state propagator should do (an explicit semanticLabel still 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 its Latency label and loses the role. An anchor with onTap, onLongPress or onDoubleTap is unchanged, so WButton and every real control keep their single named button node with its tap action, and an explicit semanticLabel still wins for the icon-only case that has no child text to merge. One claim to retire with this: the nesting WAnchor(onTap:) > WDiv(hover:...) did NOT announce twice, though it published two button nodes in the widget tree. The inner one carried isMergedIntoParent, 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. MergeSemantics goes 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-only WCheckbox inside 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)

    • WCheckbox offered a tap action for onChanged: null. The anchor's callback was gated on disabled alone, 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, where IgnorePointer(child: WCheckbox(value: selected, onChanged: null)) published a 16x16 nameless node with a tap action inside a tile that was already doing the work; IgnorePointer blocks pointers and leaves the semantics node in place. The gate is now disabled || onChanged == null, and the checked state 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 WCheckbox still announced itself as an enabled control, and its disabled: classes never fired. The gate above removed the false tap affordance and stopped there, so the two statements beside it kept reading the disabled prop alone: Semantics(enabled: !disabled) published enabled: true for a control with nothing to press, and the activeStates set left disabled out, so WCheckbox(value: true, onChanged: null, className: 'disabled:opacity-50') rendered at full opacity. WRadio and WSwitch have derived disabled || onChanged == null once at the top of build in 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 single isDisabled feeds all four consumers: the state set, the semantics enabled flag, the anchor's isDisabled, and the gesture. The checked state 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. A WCheckbox with a null onChanged and disabled: classes now renders them, where before they never applied. And on the WDynamic JSON path, _buildWCheckbox passes whatever parseValueAction returns, which is null whenever props.onChange is 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 updated id, 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.yml declared release: [published], but the release is created inside publish.yml's github-release job by gh release create running under GH_TOKEN: ${{ github.token }}, and GitHub does not start workflow runs from events raised by GITHUB_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 being workflow_dispatch and the retired push trigger. It reached fluttersdk/ai only 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.yml now calls the workflow directly with needs: 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 dead release trigger is gone and workflow_call replaces it; workflow_dispatch stays as the manual escape hatch. Secrets are passed by name rather than secrets: 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 v prefix this repo never tags with, so it always fell through to pubspec.yaml. The step matched ^v[0-9]+\.[0-9]+\.[0-9]+, while publish.yml's tag filter is [0-9]+.[0-9]+.[0-9]+* and CLAUDE.md's release step is git 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]+*, so 1.5.0-beta.1 matches it and this repo has shipped 1.0.0-alpha.* tags. The reason to skip is distribution rather than a malformed version: fluttersdk/ai's sync.yml validates 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 rsync skills/wind-ui/ onto the registry's main, which is what npx skills add fluttersdk/ai serves. A branch name containing a dash cannot trip the guard, because on a workflow_dispatch the github-release job 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: false like every other checkout here: the job only reads pubspec.yaml, zizmor's artipacked audit 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.3 since the 1.4.0 release, and the release checklist is why. That checklist names three version spots in skills/wind-ui/: the nine references/*.md H1s, the SKILL.md description prefix, 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, the dartdoc_options.yaml source-link tag, the llms.txt version string); the nine reference H1s and the marker already read 1.4 and need no change for a patch. (skills/wind-ui/SKILL.md, pubspec.yaml, example/pubspec.yaml, dartdoc_options.yaml, llms.txt)

    Open source →
  2. 1.4.0 21 Aug 2026
    Release notes

    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 seeded primary token rather than a literal blue-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 one rsync --delete away 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 WInput defaults its Return key to TextInputAction.done instead of .next. Multiline still defaults to .newline, and an explicit textInputAction still 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 .next as focusNode.nextFocus(), which moves to the next FOCUSABLE widget rather than the next text field. Measured on a status-page form ordered Name → [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 through textInputAction, which is the only place the intent can be stated correctly, since only the form knows its own field order; WKeyboardActions remains the option for an explicit, data-driven advance order. Any form that relied on the old default gets its behaviour back by passing textInputAction: TextInputAction.next per field. (lib/src/widgets/w_input.dart, doc/widgets/w-input.md, skills/wind-ui/references/forms.md)

    Fixed

    • A justify-between row starved the one child that asked for the space. Space distribution wrapped every child in a Flexible to reproduce the CSS flex: 0 1 auto shrink 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 a flex-1 title 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 bare w-full, or a raw Expanded / 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 what justify-content: space-between does 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-hidden keeps it unconditionally because that token asks for shrinking on purpose, and the shrink-only tokens (shrink, flex-shrink, flex-initial, CSS flex: 0 1 auto) still self-wrap to shrink without counting as a claim. A bare w-full counts because the Row composer already turns exactly that child into an Expanded; leaving it out would have capped it at half the row while flex-1 took the remainder, and the two are documented as equivalent on a row child. A PREFIXED grow token does not count: hover:flex-1 and md:grow are 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 with A RenderFlex overflowed by 424 pixels on the right. That mirrors the policy md:w-full already 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)
    • WKeyboardActions hosted its toolbar in the nearest Overlay, 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 an Overlay inside 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, so bottom: 335 put 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 into Overlay.of(context, rootOverlay: true), and consumers need no Overlay of their own since MaterialApp and CupertinoApp both 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 & Test was red on every open PR, and not one of them had broken anything. The Flutter tool ships an analysis_options.yaml migrator that appends an analyzer.exclude block for build/ and the six platform runner directories, and it runs on every flutter pub get. CI's first step after checkout is flutter pub get, so by the time dart pub publish --dry-run ran seven steps later the checkout was dirty, the dry-run reported 1 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 a pub get and 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_merge was 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 reach master or v0 because 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 in CLAUDE.md under Branching, because a policy nobody wrote down is not a policy.
    • Do not reach for git branch --merged in this repo. The merge button squashes, which writes a new commit and severs the branch's ancestry to master, 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 reached master through #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 --approve run with GITHUB_TOKEN, against a repo where "Allow GitHub Actions to create and approve pull requests" is off. GitHub answered GraphQL: GitHub Actions is not permitted to approve pull requests (addPullRequestReview), the step exited 1, and the gh pr merge --auto line 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 setting allow_auto_merge was 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 set strict: true, but required_status_checks.contexts was empty, so Lint & Test could 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 & Test and Internal Links & Previews are required contexts now. zizmor SAST is deliberately NOT among them: zizmor.yml filters on paths: ['.github/workflows/**'], so a PR touching only lib/ never triggers it, the context never reports, and the PR waits forever on a status that is never coming. That is the same trap docs-link-check.yml spells 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: DocLinkExtension strips the .md suffix off an internal link without checking the target resolves, DocsScaffolder upserts 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.py now 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.md documents; the site itself reads the title from the first H1 wherever it sits), relative .md targets that stay inside doc/ (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 under example/lib/pages/, <x-preview path> matching a route registered in example/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 its source, and absolute fluttersdk.com/wind/....md URLs 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 new docs-link-check.yml runs it per PR next to an offline lychee pass 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 Selection would 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.md linked text-decoration-color.md, a page that has never existed, now pointing at ./text-decoration.md#decoration-color where that content lives. doc/core-concepts/debugging.md and four entries in llms.txt linked fluttersdk.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.md and doc/typography/text-overflow.md each had a real ## section missing from the table of contents (#min-width-scroll, #customizing-theme), and doc/interactivity/animation.md carried 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/ai climbed 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_dispatch stays 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. description alone ran 1,526 characters against a 1,536-character listing budget shared with when_to_use, so the entire when_to_use block (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 in references/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.md gained the ## Contents block 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-ui skill references still carried a Wind 1.2 title after the 1.3.0 release; all nine now read Wind 1.3, and the skill version moved to 2.11.0. (skills/wind-ui/SKILL.md, skills/wind-ui/references/*.md)
    • The token catalog presented primary as a family the consumer has to register, wrong since 1.2.0 seeded it. The default family list now names it (23 families), says bg-primary / text-primary / border-primary resolve with no registration, and points at the single override that rebrands WSelect / WCheckbox / WRadio / WDatePicker. The custom-family example moved to a brand key 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 (the transition shorthands, antialiased, sr-only, the font-variant-numeric family, the inline display keywords) plus the object-* family, which never reaches WindStyle and yet changes rendering because WImage reads it straight off the className. (skills/wind-ui/references/tokens.md, mirrors lib/src/parser/wind_parser.dart)
    • theme.md counted 22 color families in two places; the seeded primary makes 23. (skills/wind-ui/references/theme.md)
    • doc/utilities/context-extensions.md demonstrated context.windTheme.setBrightness(Brightness.dark), a method WindThemeController has never exposed, so a reader following the page hit a compile error. Replaced with the two calls that actually pin a preference: toggleTheme() (which sets syncWithSystem: false itself) and setTheme(data.copyWith(brightness: ..., syncWithSystem: false)), plus resetToSystem() to hand control back. A bare updateTheme(brightness: ...) is the partial-update call the framework's own didChangePlatformBrightness listener makes, so with syncWithSystem still true the next OS change overwrites it. (doc/utilities/context-extensions.md)
    • llms.txt pointed WFormMultiSelect at widgets/w-form-multiselect.md, which does not exist; the widget is documented inside w-form-select.md. (llms.txt)
    • The README acceptance criterion in CLAUDE.md still 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, the dartdoc_options.yaml source-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), the llms.txt version string, and the nine skills/wind-ui/references/*.md H1s 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/)
    Open source →
    Release notes

    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 seeded primary token rather than a literal blue-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 one rsync --delete away 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 WInput defaults its Return key to TextInputAction.done instead of .next. Multiline still defaults to .newline, and an explicit textInputAction still 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 .next as focusNode.nextFocus(), which moves to the next FOCUSABLE widget rather than the next text field. Measured on a status-page form ordered Name → [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 through textInputAction, which is the only place the intent can be stated correctly, since only the form knows its own field order; WKeyboardActions remains the option for an explicit, data-driven advance order. Any form that relied on the old default gets its behaviour back by passing textInputAction: TextInputAction.next per field. (lib/src/widgets/w_input.dart, doc/widgets/w-input.md, skills/wind-ui/references/forms.md)

    Fixed

    • A justify-between row starved the one child that asked for the space. Space distribution wrapped every child in a Flexible to reproduce the CSS flex: 0 1 auto shrink 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 a flex-1 title 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 bare w-full, or a raw Expanded / 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 what justify-content: space-between does 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-hidden keeps it unconditionally because that token asks for shrinking on purpose, and the shrink-only tokens (shrink, flex-shrink, flex-initial, CSS flex: 0 1 auto) still self-wrap to shrink without counting as a claim. A bare w-full counts because the Row composer already turns exactly that child into an Expanded; leaving it out would have capped it at half the row while flex-1 took the remainder, and the two are documented as equivalent on a row child. A PREFIXED grow token does not count: hover:flex-1 and md:grow are 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 with A RenderFlex overflowed by 424 pixels on the right. That mirrors the policy md:w-full already 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)
    • WKeyboardActions hosted its toolbar in the nearest Overlay, 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 an Overlay inside 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, so bottom: 335 put 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 into Overlay.of(context, rootOverlay: true), and consumers need no Overlay of their own since MaterialApp and CupertinoApp both 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 & Test was red on every open PR, and not one of them had broken anything. The Flutter tool ships an analysis_options.yaml migrator that appends an analyzer.exclude block for build/ and the six platform runner directories, and it runs on every flutter pub get. CI's first step after checkout is flutter pub get, so by the time dart pub publish --dry-run ran seven steps later the checkout was dirty, the dry-run reported 1 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 a pub get and 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_merge was 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 reach master or v0 because 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 in CLAUDE.md under Branching, because a policy nobody wrote down is not a policy.
    • Do not reach for git branch --merged in this repo. The merge button squashes, which writes a new commit and severs the branch's ancestry to master, 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 reached master through #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 --approve run with GITHUB_TOKEN, against a repo where "Allow GitHub Actions to create and approve pull requests" is off. GitHub answered GraphQL: GitHub Actions is not permitted to approve pull requests (addPullRequestReview), the step exited 1, and the gh pr merge --auto line 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 setting allow_auto_merge was 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 set strict: true, but required_status_checks.contexts was empty, so Lint & Test could 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 & Test and Internal Links & Previews are required contexts now. zizmor SAST is deliberately NOT among them: zizmor.yml filters on paths: ['.github/workflows/**'], so a PR touching only lib/ never triggers it, the context never reports, and the PR waits forever on a status that is never coming. That is the same trap docs-link-check.yml spells 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: DocLinkExtension strips the .md suffix off an internal link without checking the target resolves, DocsScaffolder upserts 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.py now 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.md documents; the site itself reads the title from the first H1 wherever it sits), relative .md targets that stay inside doc/ (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 under example/lib/pages/, <x-preview path> matching a route registered in example/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 its source, and absolute fluttersdk.com/wind/....md URLs 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 new docs-link-check.yml runs it per PR next to an offline lychee pass 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 Selection would 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.md linked text-decoration-color.md, a page that has never existed, now pointing at ./text-decoration.md#decoration-color where that content lives. doc/core-concepts/debugging.md and four entries in llms.txt linked fluttersdk.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.md and doc/typography/text-overflow.md each had a real ## section missing from the table of contents (#min-width-scroll, #customizing-theme), and doc/interactivity/animation.md carried 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/ai climbed 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_dispatch stays 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. description alone ran 1,526 characters against a 1,536-character listing budget shared with when_to_use, so the entire when_to_use block (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 in references/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.md gained the ## Contents block 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-ui skill references still carried a Wind 1.2 title after the 1.3.0 release; all nine now read Wind 1.3, and the skill version moved to 2.11.0. (skills/wind-ui/SKILL.md, skills/wind-ui/references/*.md)
    • The token catalog presented primary as a family the consumer has to register, wrong since 1.2.0 seeded it. The default family list now names it (23 families), says bg-primary / text-primary / border-primary resolve with no registration, and points at the single override that rebrands WSelect / WCheckbox / WRadio / WDatePicker. The custom-family example moved to a brand key 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 (the transition shorthands, antialiased, sr-only, the font-variant-numeric family, the inline display keywords) plus the object-* family, which never reaches WindStyle and yet changes rendering because WImage reads it straight off the className. (skills/wind-ui/references/tokens.md, mirrors lib/src/parser/wind_parser.dart)
    • theme.md counted 22 color families in two places; the seeded primary makes 23. (skills/wind-ui/references/theme.md)
    • doc/utilities/context-extensions.md demonstrated context.windTheme.setBrightness(Brightness.dark), a method WindThemeController has never exposed, so a reader following the page hit a compile error. Replaced with the two calls that actually pin a preference: toggleTheme() (which sets syncWithSystem: false itself) and setTheme(data.copyWith(brightness: ..., syncWithSystem: false)), plus resetToSystem() to hand control back. A bare updateTheme(brightness: ...) is the partial-update call the framework's own didChangePlatformBrightness listener makes, so with syncWithSystem still true the next OS change overwrites it. (doc/utilities/context-extensions.md)
    • llms.txt pointed WFormMultiSelect at widgets/w-form-multiselect.md, which does not exist; the widget is documented inside w-form-select.md. (llms.txt)
    • The README acceptance criterion in CLAUDE.md still 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, the dartdoc_options.yaml source-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), the llms.txt version string, and the nine skills/wind-ui/references/*.md H1s 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/)
    Open source →
  3. 1.3.0 03 Aug 2026
    Release notes

    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 local DateTime carrying the hour and minute (no timezone conversion: DateTime cannot hold an arbitrary offset, so a value crossing the wire is the caller's toUtc() to make). The time row is authored in Wind markup, two 24-hour spinners plus a confirm control built from WDiv / WText / WIcon and className tokens with their dark: pairs, NOT a Material showTimePicker dialog; it exposes a labelled Semantics container and per-button Semantics, honors minDate/maxDate on 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 by WFormDatePicker. single and range behavior, 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 dateTime time row's step controls and confirm control now carry a SemanticsAction.tap of their own. Semantics(button: true) declared the role, but the GestureDetector underneath 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 dateTime value (a form reset setting value back to null) 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)
    • minuteStep is 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 as 0 and leave the minute spinners silently inert. (lib/src/widgets/w_date_picker.dart)

    Quality

    • WDatePickerMode.dateTime gained its example page, example/lib/pages/widgets/date_picker_datetime.dart (basic usage, minuteStep, row labels + displayFormat, an instant-bounded maintenance window pair, and a WFormDatePicker with a time-of-day validator), wired into example/lib/routes.dart and 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 maxDate trap in dateTime mode: a bare-day bound is that day at 00:00, so the last day admits only midnight. Pinned by tests on both shapes of bound. The bounds stay instant-level; special-casing a midnight maxDate into an end-of-day would make 00:00 unexpressible. (lib/src/widgets/w_date_picker.dart, doc/widgets/w-date-picker.md, skills/wind-ui/references/widgets.md)
    • The dateTime semantics tests run through a testWidgetsWithSemantics wrapper that disposes the SemanticsHandle in a finally. Disposal cannot move to tearDown / 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)
    Open source →
    Release notes

    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 local DateTime carrying the hour and minute (no timezone conversion: DateTime cannot hold an arbitrary offset, so a value crossing the wire is the caller's toUtc() to make). The time row is authored in Wind markup, two 24-hour spinners plus a confirm control built from WDiv / WText / WIcon and className tokens with their dark: pairs, NOT a Material showTimePicker dialog; it exposes a labelled Semantics container and per-button Semantics, honors minDate/maxDate on 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 by WFormDatePicker. single and range behavior, 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 dateTime time row's step controls and confirm control now carry a SemanticsAction.tap of their own. Semantics(button: true) declared the role, but the GestureDetector underneath 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 dateTime value (a form reset setting value back to null) 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)
    • minuteStep is 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 as 0 and leave the minute spinners silently inert. (lib/src/widgets/w_date_picker.dart)

    Quality

    • WDatePickerMode.dateTime gained its example page, example/lib/pages/widgets/date_picker_datetime.dart (basic usage, minuteStep, row labels + displayFormat, an instant-bounded maintenance window pair, and a WFormDatePicker with a time-of-day validator), wired into example/lib/routes.dart and 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 maxDate trap in dateTime mode: a bare-day bound is that day at 00:00, so the last day admits only midnight. Pinned by tests on both shapes of bound. The bounds stay instant-level; special-casing a midnight maxDate into an end-of-day would make 00:00 unexpressible. (lib/src/widgets/w_date_picker.dart, doc/widgets/w-date-picker.md, skills/wind-ui/references/widgets.md)
    • The dateTime semantics tests run through a testWidgetsWithSemantics wrapper that disposes the SemanticsHandle in a finally. Disposal cannot move to tearDown / 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)
    Open source →
  4. 1.2.1 21 Jul 2026
    Release notes

    Fixed

    • Unknown theme-spacing tokens (p-primary, m-foo, top-abc, gap-x-blue, ...) now silently drop instead of throwing ArgumentError: Invalid spacing multiplier: <token> inside build(), matching the "unknown className is dropped with a debug warning" contract other parsers already follow. Adds WindThemeData.tryGetSpacing(String); the padding, margin, position, and flex-gap parsers now use it (sizing already pre-validated with double.tryParse). getSpacing is 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)
    • WAnchor now hit-tests its whole bounds via HitTestBehavior.translucent. The inner GestureDetector used the default HitTestBehavior.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 like WInput/WPopover, which already use whole-box hit-testing, so the full anchor rectangle is tappable. (lib/src/widgets/w_anchor.dart; covered by test/widgets/w_anchor_test.dart)
    • WPopover no 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 programmatic close, matching the same fix already applied to WSelect. (lib/src/widgets/w_popover.dart)
    Open source →
    Release notes

    Fixed

    • Unknown theme-spacing tokens (p-primary, m-foo, top-abc, gap-x-blue, ...) now silently drop instead of throwing ArgumentError: Invalid spacing multiplier: <token> inside build(), matching the "unknown className is dropped with a debug warning" contract other parsers already follow. Adds WindThemeData.tryGetSpacing(String); the padding, margin, position, and flex-gap parsers now use it (sizing already pre-validated with double.tryParse). getSpacing is 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)
    • WAnchor now hit-tests its whole bounds via HitTestBehavior.translucent. The inner GestureDetector used the default HitTestBehavior.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 like WInput/WPopover, which already use whole-box hit-testing, so the full anchor rectangle is tappable. (lib/src/widgets/w_anchor.dart; covered by test/widgets/w_anchor_test.dart)
    • WPopover no 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 programmatic close, matching the same fix already applied to WSelect. (lib/src/widgets/w_popover.dart)
    Open source →
  5. 1.2.0 07 Jul 2026
    Release notes

    Changed

    • Wind's internal flex layout is now fully intrinsic-safe: the column cross-axis stretch and the basis-* resolution no longer use a LayoutBuilder. Column stretch is a real render object (WindCrossStretch) and fractional basis-* resolves against the flex's own extent via WindMainExtentProvider/WindFractionBasis. A flex flex-col (with or without basis-*) now renders inside an items-stretch grid cell, under a Flutter IntrinsicHeight/IntrinsicWidth, or in a Table cell without the LayoutBuilder does not support returning intrinsic dimensions assert (the web _owner != null cascade 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 theme primary token instead of hardcoded blue-* classes and Colors.blue shades, so a consumer's brand color drives them. The shipped default look is unchanged (primary defaults to the Tailwind blue swatch). WindThemeData.toThemeData() keeps its indigo Material ColorScheme baseline unless a custom primary is 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/WindSlotRecipe caller-className merge 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, by WindParser's per-family last-wins. No twMerge/cn port 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, and doc/layout/display.md reconciled to reflect the flex-wrap alias (no longer a listed unsupported token) and the unknown-token debug hint (no longer purely silent). Example pages under example/lib/pages/ switched from flex-wrap to the canonical wrap token. (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-auto on a wrapper with w-full (optionally min-w-[Npx]) on the inner content. w-full inside a horizontal scroll is now sized to max(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-stretch now equalizes child widths (every eligible child fills the column), closing the asymmetry with grid ... items-stretch. Like the smart-stretch default it is intrinsic-safe and unbounded-safe (no LayoutBuilder, no infinite-width SizedBox), so it also works in a bare Row slot 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-full inside a vertical scroll: a child that resolves h-full under an overflow-y-auto/overflow-y-scroll parent (an unbounded height) now fails fast with a message pointing at the fix (flex-1 inside a flex flex-col), instead of a cryptic Flutter unbounded-height error. The scrollable parent threads the signal down via WindMinWidthScrollScope; 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 primary color token: Wind's default theme now includes a primary MaterialColor (aliased 1:1 to the Tailwind blue swatch), so bg-primary / text-primary / border-primary resolve out of the box and become brand-overridable via WindThemeData.colors: {'primary': ...}. Previously primary was consulted only by toThemeData() 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 -> wrap alias: FlexboxGridParser now maps the Tailwind spelling flex-wrap directly to WindDisplayType.wrap (canParse already accepted flex-*; only the parse() handler was missing). wrap remains the canonical, unaliased token; flex-wrap prints a one-time kDebugMode hint suggesting it. (lib/src/parser/parsers/flexbox_grid_parser.dart) (WIND-5)
    • Unknown-className debug warning: WindParser.findAndGroupClasses now emits a one-time kDebugMode debugPrint naming any className that no parser recognizes, deduped per unique token per session (mirrors the existing _warnedAliases shadow/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-consumed object-* fit family (read by WImage), the inline-flex/inline-block/inline display keywords (emitted by WBadge, 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 *-nums font-variant family incl. tabular-nums). (lib/src/parser/wind_parser.dart) (WIND-5)
    • cursor-* utilities: a new CursorParser maps 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 matching SystemMouseCursors. WDiv applies the resolved cursor through a MouseRegion, so a plain container can feel clickable on web/desktop without WAnchor or a manual MouseRegion; on a hover:/focus:/active: WDiv the cursor MouseRegion sits below the auto-wrapped WAnchor and 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-screen set BOTH width and height in one token. Fixes childless WDiv sizing: a WDiv(className: 'size-2 rounded-full bg-...') status dot now renders its box instead of collapsing (the previous gap was that size-* was unrecognized, not that w-*/h-* were ignored, which already worked childless). A later w-*/h-* overrides the matching axis. (lib/src/parser/parsers/sizing_parser.dart) (#123)
    • grid equal-height rows via items-stretch. By default a Wind grid renders as a Wrap and sizes each cell to its own content, leaving a row ragged when one card is taller. Adding items-stretch (CSS Grid's default align-items: stretch) now builds the grid as a column of IntrinsicHeight rows so every cell in a row matches the tallest, and cells divide the row width evenly. Cells should size from their own content; h-full on a stretched cell is unsupported (it inserts a LayoutBuilder that asserts under IntrinsicHeight, see the intrinsic-sizing limitation docs). (lib/src/widgets/w_div.dart) (#126)
    • WindRecipe and WindSlotRecipe (+ WindCompoundVariant, WindSlotCompoundVariant): a tv() (tailwind-variants) equivalent. WindRecipe resolves a single className from base + variant axes + compoundVariants + a caller className, in strict emission order (base ++ variant ++ compound ++ caller), with no dedupe/sort/twMerge. WindSlotRecipe extends the same model to multi-slot components and returns a Map<String, String> (slot -> className). (lib/src/recipe/wind_recipe.dart) (#120)
    • WBadge: inline status/label pill. Composes a rounded-full WDiv around a WText(text-xs); all visual tone is className-driven (bg-*, text-*, dark: pairs). (lib/src/widgets/w_badge.dart) (#120)
    • WCard: surface container with optional header and footer slots. Delegates to WDiv (flex-col); all styling is className-driven. (lib/src/widgets/w_card.dart) (#120)
    • WSwitch: controlled toggle switch. Pushes the checked: state prefix when value is true. The thumb is a flex child of the track, so the caller positions it with justify-start -> checked:justify-end on the track className (the track stays a flex Row; WSwitch does not inject relative, which would force a single-child Stack and defeat justify-*). thumbClassName supplies thumb shape/color only; translate-x-* is intentionally unsupported (Wind has no transform parser). Wraps WAnchor for hover/focus/disabled state propagation. (lib/src/widgets/w_switch.dart) (#120)
    • WRadio<T>: controlled radio button. Drives the selected: state prefix when value == groupValue. Group behavior (mutual exclusivity) is the caller's responsibility. (lib/src/widgets/w_radio.dart) (#120)
    • WTabs: controlled tabs widget. The selected: state prefix activates on the active tab; listClassName, tabClassName, selectedTabClassName, and panelClassName cover every structural region. panelBuilder receives the selected index each rebuild. The tab list defaults to full container width (fullWidthList: true) so a border-b underline spans the container rather than only the tabs; set fullWidthList: false for a content-width / pill tab strip. (#128) (lib/src/widgets/w_tabs.dart)

    Fixed

    • WPopover overlay no longer overflows the right edge of the viewport when autoFlip is on. A wide panel next to a near-centered trigger on a narrow screen flipped to the opposite side but still spilled off-screen; a computeHorizontalClamp helper now folds a corrective dx into the follower offset so the panel is pulled fully inside the viewport (seated 8px from 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-stretch no longer emits a residual RenderFlex overflowed by ~2px on the bottom when it stretches an unequal cell (follow-up to #139). WindEqualHeightRow previously re-laid each cell to a TIGHT height equal to the loose-measured row max; a cell whose flex flex-col content 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-stretch no longer asserts RenderBox was not laid out (rooted at RenderIntrinsicHeight / LayoutBuilder does not support returning intrinsic dimensions) when it stretches an unequal flex flex-col cell. The equal-height mechanism from #126 used IntrinsicHeight, which queries child intrinsics; a flex flex-col cell (or one using h-full / basis-*) carries a LayoutBuilder that cannot answer intrinsic queries, so stretching a shorter cell crashed the exact case the feature exists for. Replaced it with WindEqualHeightRow, a RenderObject that 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-full on 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, so prefix: + a known alias key was a silent no-op while plain bg-surface-container worked. 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-surface carries hover: onto each token including the value's own dark: peer, yielding hover: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)

    • WPopover overlay no longer stretches unbounded. It applied only a minWidth (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 with WSelect, which pins a width). New width and maxWidth props: width (or a w-* token) pins a fixed overlay width like WSelect's menuWidth; maxWidth (or a max-w-* token) bounds it, defaulting to the screen width so the overlay never overflows the viewport. (lib/src/widgets/w_popover.dart) (#127)

    • w-full on a direct child of a flex flex-row no longer aborts layout with "RenderBox was not laid out". A Row hands non-flex children an unbounded main-axis constraint, so a w-full child's SizedBox(width: infinity) could not resolve. Wind now treats a bare w-full Row child as flex-1: the Row wraps it in Expanded (and defaults to MainAxisSize.max), giving the child a bounded width share inside which its infinite-width box resolves, so it fills the row. flex-1 remains the idiomatic choice; a prefixed md:w-full is intentionally not auto-expanded (use md:flex-1). Nested (non-direct) w-full children 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's TapRegion.onTapOutside received the opening tap's own pointer-up because OverlayPortal mounts synchronously on the frame it opens, and (2) _onFocusChange closed 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's TapRegion exists; 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 a TapRegion group id so a trigger re-tap toggles closed instead of self-closing. Custom triggerBuilder / multiTriggerBuilder triggers 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-ui skill references, and llms.txt synced to the 1.2.0 surface: the five new widgets gained sections in skills/wind-ui/references/widgets.md, the unknown-className debug hint is now documented in doc/core-concepts/debugging.md, the parser count (20) and the cursor-* / flex-wrap support state were corrected across the skill, and llms.txt now lists every new widget, the cursor-* / size-* families, and WindRecipe.
    • Removed every em-dash and en-dash from doc/, skills/, lib/ comments, and the example/ gallery to honor the project style rule; the WindDynamic unknown-action debug message now reads ..., ignored. (comma form) to match.
    Open source →
    Release notes

    Changed

    • Wind's internal flex layout is now fully intrinsic-safe: the column cross-axis stretch and the basis-* resolution no longer use a LayoutBuilder. Column stretch is a real render object (WindCrossStretch) and fractional basis-* resolves against the flex's own extent via WindMainExtentProvider/WindFractionBasis. A flex flex-col (with or without basis-*) now renders inside an items-stretch grid cell, under a Flutter IntrinsicHeight/IntrinsicWidth, or in a Table cell without the LayoutBuilder does not support returning intrinsic dimensions assert (the web _owner != null cascade 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 theme primary token instead of hardcoded blue-* classes and Colors.blue shades, so a consumer's brand color drives them. The shipped default look is unchanged (primary defaults to the Tailwind blue swatch). WindThemeData.toThemeData() keeps its indigo Material ColorScheme baseline unless a custom primary is 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/WindSlotRecipe caller-className merge 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, by WindParser's per-family last-wins. No twMerge/cn port 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, and doc/layout/display.md reconciled to reflect the flex-wrap alias (no longer a listed unsupported token) and the unknown-token debug hint (no longer purely silent). Example pages under example/lib/pages/ switched from flex-wrap to the canonical wrap token. (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-auto on a wrapper with w-full (optionally min-w-[Npx]) on the inner content. w-full inside a horizontal scroll is now sized to max(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-stretch now equalizes child widths (every eligible child fills the column), closing the asymmetry with grid ... items-stretch. Like the smart-stretch default it is intrinsic-safe and unbounded-safe (no LayoutBuilder, no infinite-width SizedBox), so it also works in a bare Row slot 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-full inside a vertical scroll: a child that resolves h-full under an overflow-y-auto/overflow-y-scroll parent (an unbounded height) now fails fast with a message pointing at the fix (flex-1 inside a flex flex-col), instead of a cryptic Flutter unbounded-height error. The scrollable parent threads the signal down via WindMinWidthScrollScope; 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 primary color token: Wind's default theme now includes a primary MaterialColor (aliased 1:1 to the Tailwind blue swatch), so bg-primary / text-primary / border-primary resolve out of the box and become brand-overridable via WindThemeData.colors: {'primary': ...}. Previously primary was consulted only by toThemeData() 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 -> wrap alias: FlexboxGridParser now maps the Tailwind spelling flex-wrap directly to WindDisplayType.wrap (canParse already accepted flex-*; only the parse() handler was missing). wrap remains the canonical, unaliased token; flex-wrap prints a one-time kDebugMode hint suggesting it. (lib/src/parser/parsers/flexbox_grid_parser.dart) (WIND-5)
    • Unknown-className debug warning: WindParser.findAndGroupClasses now emits a one-time kDebugMode debugPrint naming any className that no parser recognizes, deduped per unique token per session (mirrors the existing _warnedAliases shadow/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-consumed object-* fit family (read by WImage), the inline-flex/inline-block/inline display keywords (emitted by WBadge, 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 *-nums font-variant family incl. tabular-nums). (lib/src/parser/wind_parser.dart) (WIND-5)
    • cursor-* utilities: a new CursorParser maps 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 matching SystemMouseCursors. WDiv applies the resolved cursor through a MouseRegion, so a plain container can feel clickable on web/desktop without WAnchor or a manual MouseRegion; on a hover:/focus:/active: WDiv the cursor MouseRegion sits below the auto-wrapped WAnchor and 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-screen set BOTH width and height in one token. Fixes childless WDiv sizing: a WDiv(className: 'size-2 rounded-full bg-...') status dot now renders its box instead of collapsing (the previous gap was that size-* was unrecognized, not that w-*/h-* were ignored, which already worked childless). A later w-*/h-* overrides the matching axis. (lib/src/parser/parsers/sizing_parser.dart) (#123)
    • grid equal-height rows via items-stretch. By default a Wind grid renders as a Wrap and sizes each cell to its own content, leaving a row ragged when one card is taller. Adding items-stretch (CSS Grid's default align-items: stretch) now builds the grid as a column of IntrinsicHeight rows so every cell in a row matches the tallest, and cells divide the row width evenly. Cells should size from their own content; h-full on a stretched cell is unsupported (it inserts a LayoutBuilder that asserts under IntrinsicHeight, see the intrinsic-sizing limitation docs). (lib/src/widgets/w_div.dart) (#126)
    • WindRecipe and WindSlotRecipe (+ WindCompoundVariant, WindSlotCompoundVariant): a tv() (tailwind-variants) equivalent. WindRecipe resolves a single className from base + variant axes + compoundVariants + a caller className, in strict emission order (base ++ variant ++ compound ++ caller), with no dedupe/sort/twMerge. WindSlotRecipe extends the same model to multi-slot components and returns a Map<String, String> (slot -> className). (lib/src/recipe/wind_recipe.dart) (#120)
    • WBadge: inline status/label pill. Composes a rounded-full WDiv around a WText(text-xs); all visual tone is className-driven (bg-*, text-*, dark: pairs). (lib/src/widgets/w_badge.dart) (#120)
    • WCard: surface container with optional header and footer slots. Delegates to WDiv (flex-col); all styling is className-driven. (lib/src/widgets/w_card.dart) (#120)
    • WSwitch: controlled toggle switch. Pushes the checked: state prefix when value is true. The thumb is a flex child of the track, so the caller positions it with justify-start -> checked:justify-end on the track className (the track stays a flex Row; WSwitch does not inject relative, which would force a single-child Stack and defeat justify-*). thumbClassName supplies thumb shape/color only; translate-x-* is intentionally unsupported (Wind has no transform parser). Wraps WAnchor for hover/focus/disabled state propagation. (lib/src/widgets/w_switch.dart) (#120)
    • WRadio<T>: controlled radio button. Drives the selected: state prefix when value == groupValue. Group behavior (mutual exclusivity) is the caller's responsibility. (lib/src/widgets/w_radio.dart) (#120)
    • WTabs: controlled tabs widget. The selected: state prefix activates on the active tab; listClassName, tabClassName, selectedTabClassName, and panelClassName cover every structural region. panelBuilder receives the selected index each rebuild. The tab list defaults to full container width (fullWidthList: true) so a border-b underline spans the container rather than only the tabs; set fullWidthList: false for a content-width / pill tab strip. (#128) (lib/src/widgets/w_tabs.dart)

    Fixed

    • WPopover overlay no longer overflows the right edge of the viewport when autoFlip is on. A wide panel next to a near-centered trigger on a narrow screen flipped to the opposite side but still spilled off-screen; a computeHorizontalClamp helper now folds a corrective dx into the follower offset so the panel is pulled fully inside the viewport (seated 8px from 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-stretch no longer emits a residual RenderFlex overflowed by ~2px on the bottom when it stretches an unequal cell (follow-up to #139). WindEqualHeightRow previously re-laid each cell to a TIGHT height equal to the loose-measured row max; a cell whose flex flex-col content 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-stretch no longer asserts RenderBox was not laid out (rooted at RenderIntrinsicHeight / LayoutBuilder does not support returning intrinsic dimensions) when it stretches an unequal flex flex-col cell. The equal-height mechanism from #126 used IntrinsicHeight, which queries child intrinsics; a flex flex-col cell (or one using h-full / basis-*) carries a LayoutBuilder that cannot answer intrinsic queries, so stretching a shorter cell crashed the exact case the feature exists for. Replaced it with WindEqualHeightRow, a RenderObject that 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-full on 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, so prefix: + a known alias key was a silent no-op while plain bg-surface-container worked. 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-surface carries hover: onto each token including the value's own dark: peer, yielding hover: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)

    • WPopover overlay no longer stretches unbounded. It applied only a minWidth (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 with WSelect, which pins a width). New width and maxWidth props: width (or a w-* token) pins a fixed overlay width like WSelect's menuWidth; maxWidth (or a max-w-* token) bounds it, defaulting to the screen width so the overlay never overflows the viewport. (lib/src/widgets/w_popover.dart) (#127)

    • w-full on a direct child of a flex flex-row no longer aborts layout with "RenderBox was not laid out". A Row hands non-flex children an unbounded main-axis constraint, so a w-full child's SizedBox(width: infinity) could not resolve. Wind now treats a bare w-full Row child as flex-1: the Row wraps it in Expanded (and defaults to MainAxisSize.max), giving the child a bounded width share inside which its infinite-width box resolves, so it fills the row. flex-1 remains the idiomatic choice; a prefixed md:w-full is intentionally not auto-expanded (use md:flex-1). Nested (non-direct) w-full children 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's TapRegion.onTapOutside received the opening tap's own pointer-up because OverlayPortal mounts synchronously on the frame it opens, and (2) _onFocusChange closed 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's TapRegion exists; 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 a TapRegion group id so a trigger re-tap toggles closed instead of self-closing. Custom triggerBuilder / multiTriggerBuilder triggers 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-ui skill references, and llms.txt synced to the 1.2.0 surface: the five new widgets gained sections in skills/wind-ui/references/widgets.md, the unknown-className debug hint is now documented in doc/core-concepts/debugging.md, the parser count (20) and the cursor-* / flex-wrap support state were corrected across the skill, and llms.txt now lists every new widget, the cursor-* / size-* families, and WindRecipe.
    • Removed every em-dash and en-dash from doc/, skills/, lib/ comments, and the example/ gallery to honor the project style rule; the WindDynamic unknown-action debug message now reads ..., ignored. (comma form) to match.
    Open source →
  6. 1.1.2 24 Jun 2026
    Release notes

    Fixed

    • WPopover now opens reliably when triggerBuilder returns an interactive widget (a WButton or WAnchor with its own onTap), and no longer dismisses itself on the same gesture that opened it. Two defects were in play. First, the trigger was wired through an outer GestureDetector(onTap: toggle); an interactive trigger owns its own GestureDetector, won the gesture arena, and the outer onTap never fired, so the popover never opened. The trigger now toggles through a Listener(onPointerDown:), whose pointer events bypass the tap arena entirely, so opening works regardless of the trigger's interactivity (enableTriggerOnTap and disabled semantics are unchanged). Second, because the trigger and overlay share a TapRegion group id (intentionally, so a trigger re-tap does not self-close), the opening gesture's pointer-up reached the freshly mounted overlay's onTapOutside and 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 by test/widgets/w_popover/gesture_regression_test.dart, the four-behavior regression set parameterized over WButton and WDiv triggers.) Because the pointer Listener is invisible to assistive technologies, the trigger is wrapped in Semantics(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.
    Open source →
    Release notes

    Fixed

    • WPopover now opens reliably when triggerBuilder returns an interactive widget (a WButton or WAnchor with its own onTap), and no longer dismisses itself on the same gesture that opened it. Two defects were in play. First, the trigger was wired through an outer GestureDetector(onTap: toggle); an interactive trigger owns its own GestureDetector, won the gesture arena, and the outer onTap never fired, so the popover never opened. The trigger now toggles through a Listener(onPointerDown:), whose pointer events bypass the tap arena entirely, so opening works regardless of the trigger's interactivity (enableTriggerOnTap and disabled semantics are unchanged). Second, because the trigger and overlay share a TapRegion group id (intentionally, so a trigger re-tap does not self-close), the opening gesture's pointer-up reached the freshly mounted overlay's onTapOutside and 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 by test/widgets/w_popover/gesture_regression_test.dart, the four-behavior regression set parameterized over WButton and WDiv triggers.) Because the pointer Listener is invisible to assistive technologies, the trigger is wrapped in Semantics(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.
    Open source →
  7. 1.1.1 23 Jun 2026
    Release notes

    Changed

    • WInput: tap-to-focus and onTap dispatch now flow through Flutter's native TextSelectionGestureDetectorBuilder (the same gesture path TextField uses) instead of a hand-built whole-box GestureDetector. onTap continues 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 new onTap contract.

    Fixed

    • WInput now 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 one CupertinoTextField uses): _WInputState implements TextSelectionGestureDetectorBuilderDelegate, the whole decorated box is wrapped by a TextSelectionGestureDetectorBuilder (so a tap anywhere focuses and a drag over the glyphs selects), and EditableText.rendererIgnoresPointer is true so the gesture layer is the only pointer handler. Selection handles stay Cupertino-style on all platforms (unchanged from 1.1.0), keeping WInput cupertino-only with no package:flutter/material.dart import; cupertinoTextSelectionHandleControls mixes in TextSelectionHandleControls, so the toolbar still flows through the Material-free contextMenuBuilder and no Overlay-less long-press throws. Read-only fields stay selectable; disabled fields stay fully inert. (lib/src/widgets/w_input.dart; covered by test/widgets/w_input/selection_test.dart.)
    • WText with no color in its own className now inherits an ancestor DefaultTextStyle color (the CSS text-color cascade) before falling back to the platform-brightness baseline. A parent WDiv with a text-* class publishes its color through DefaultTextStyle.merge, but WText previously ignored it and forced Colors.white/Colors.black from 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 by WText baseline rendering > inherits an ancestor color (CSS cascade).)
    Open source →
    Release notes

    Changed

    • WInput: tap-to-focus and onTap dispatch now flow through Flutter's native TextSelectionGestureDetectorBuilder (the same gesture path TextField uses) instead of a hand-built whole-box GestureDetector. onTap continues 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 new onTap contract.

    Fixed

    • WInput now 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 one CupertinoTextField uses): _WInputState implements TextSelectionGestureDetectorBuilderDelegate, the whole decorated box is wrapped by a TextSelectionGestureDetectorBuilder (so a tap anywhere focuses and a drag over the glyphs selects), and EditableText.rendererIgnoresPointer is true so the gesture layer is the only pointer handler. Selection handles stay Cupertino-style on all platforms (unchanged from 1.1.0), keeping WInput cupertino-only with no package:flutter/material.dart import; cupertinoTextSelectionHandleControls mixes in TextSelectionHandleControls, so the toolbar still flows through the Material-free contextMenuBuilder and no Overlay-less long-press throws. Read-only fields stay selectable; disabled fields stay fully inert. (lib/src/widgets/w_input.dart; covered by test/widgets/w_input/selection_test.dart.)
    • WText with no color in its own className now inherits an ancestor DefaultTextStyle color (the CSS text-color cascade) before falling back to the platform-brightness baseline. A parent WDiv with a text-* class publishes its color through DefaultTextStyle.merge, but WText previously ignored it and forced Colors.white/Colors.black from 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 by WText baseline rendering > inherits an ancestor color (CSS cascade).)
    Open source →
  8. 1.1.0 17 Jun 2026
    Release notes

    Added

    • WindThemeData.aliases (Map<String, String>, empty by default): bare-token recursive className shortcuts expanded centrally in WindParser.parse before the 19-parser pipeline runs, so they work in every widget and in WDynamic without 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 #101 class of silent unknown-token failures caused by shorthand tokens not being in the default token catalog. (#104)
    • WIcon: inline foregroundColor prop for runtime-dynamic icon colors. Overrides any text-* / dark:text-* from className and stays out of the parser cache key, matching WText.foregroundColor. (#103)
    • WInput: debug AssertionError when both value and controller are 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: true now activates a readonly state, so readonly: prefixed classes style a read-only field just like disabled: 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 reads WidgetsLocalizations so copy/cut/paste labels work under any ancestor, including bare WidgetsApp. This is a visual change on Android and web; behavior is identical. Under a custom root with no Overlay ancestor (unusual; MaterialApp / CupertinoApp / WidgetsApp all 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.md and the wind-ui skill: layout-stability guidance added for IntrinsicHeight/IntrinsicWidth in animated subtrees; the safe alternative is Stack+Positioned or items-stretch (W3).
    • WInput: InputType.number now 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-supplied inputFormatters overrides this default. The number keyboard is now numberWithOptions(decimal: true, signed: true).

    Fixed

    • WInput now renders Material-free (EditableText core, BoxDecoration border/padding) and no longer crashes under a non-Material ancestor such as a bare WidgetsApp or Cupertino app. Wrapping WInput in a MaterialApp is no longer required. (#102)
    • WInput emits a single clean textField semantics node; the previous implementation produced a double-textbox node under the old MergeSemantics > TextField wrapping (W1). Dusk snapshot consumers relying on a single textbox node per WInput can expect clean output from this release (pairs with dusk D2).
    • WFormInput, WFormSelect, WFormCheckbox, and WFormDatePicker: the default label, hint, and error class names now carry dark: 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 conditional prefix/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: false is 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 reports isEnabled: false to assistive technology through its Semantics node. The Material-free rewrite had dropped the flag (the EditableText node carries only isReadOnly), so a screen reader could not tell a disabled field from a read-only one; the 1.0.0 Material TextField exposed it and parity is restored.

    Quality

    • CI: pushing a version tag (X.Y.Z) now auto-creates a GitHub Release from the matching CHANGELOG.md section via .github/workflows/publish.yml, alongside the existing pub.dev publish step. (#105)

    Open source →
    Release notes

    Added

    • WindThemeData.aliases (Map<String, String>, empty by default): bare-token recursive className shortcuts expanded centrally in WindParser.parse before the 19-parser pipeline runs, so they work in every widget and in WDynamic without 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 #101 class of silent unknown-token failures caused by shorthand tokens not being in the default token catalog. (#104)
    • WIcon: inline foregroundColor prop for runtime-dynamic icon colors. Overrides any text-* / dark:text-* from className and stays out of the parser cache key, matching WText.foregroundColor. (#103)
    • WInput: debug AssertionError when both value and controller are 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: true now activates a readonly state, so readonly: prefixed classes style a read-only field just like disabled: 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 reads WidgetsLocalizations so copy/cut/paste labels work under any ancestor, including bare WidgetsApp. This is a visual change on Android and web; behavior is identical. Under a custom root with no Overlay ancestor (unusual; MaterialApp / CupertinoApp / WidgetsApp all 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.md and the wind-ui skill: layout-stability guidance added for IntrinsicHeight/IntrinsicWidth in animated subtrees; the safe alternative is Stack+Positioned or items-stretch (W3).
    • WInput: InputType.number now 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-supplied inputFormatters overrides this default. The number keyboard is now numberWithOptions(decimal: true, signed: true).

    Fixed

    • WInput now renders Material-free (EditableText core, BoxDecoration border/padding) and no longer crashes under a non-Material ancestor such as a bare WidgetsApp or Cupertino app. Wrapping WInput in a MaterialApp is no longer required. (#102)
    • WInput emits a single clean textField semantics node; the previous implementation produced a double-textbox node under the old MergeSemantics > TextField wrapping (W1). Dusk snapshot consumers relying on a single textbox node per WInput can expect clean output from this release (pairs with dusk D2).
    • WFormInput, WFormSelect, WFormCheckbox, and WFormDatePicker: the default label, hint, and error class names now carry dark: 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 conditional prefix/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: false is 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 reports isEnabled: false to assistive technology through its Semantics node. The Material-free rewrite had dropped the flag (the EditableText node carries only isReadOnly), so a screen reader could not tell a disabled field from a read-only one; the 1.0.0 Material TextField exposed it and parity is restored.

    Quality

    • CI: pushing a version tag (X.Y.Z) now auto-creates a GitHub Release from the matching CHANGELOG.md section via .github/workflows/publish.yml, alongside the existing pub.dev publish step. (#105)

    Open source →
  9. 1.0.0 09 Jun 2026
    Release notes

    First stable release. fluttersdk_wind is utility-first, Tailwind-syntax styling for Flutter: className strings 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 + 5 WForm* wrappers), utility (WKeyboardActions, WindAnimationWrapper).
    • 19 className parsers in a token-routing pipeline with an always-on cache (~26x hit/miss speedup).
    • WindThemeData with 23 configurable fields plus WindThemeController (toggleTheme / setTheme / updateTheme / resetToSystem).
    • Three-layer state system (hover: / focus:, framework-managed loading: / 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 / getByLabel resolution against the Flutter web build.
    • Wind.installDebugResolver() bridges runtime diagnostics through fluttersdk_wind_diagnostics_contracts, consumed by fluttersdk_dusk and runtime inspectors; tree-shaken in release builds.
    • WASM-ready: dart:io removed 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, WButton are preserved but constructor signatures, the supported className token set, and theme integration differ throughout. Notable removals: WindDuskIntegration + lib/dusk_integration.dart (replaced by Wind.installDebugResolver()), fluttersdk_dusk / google_fonts / platform_info dependencies, and the trackProvenance / resolvedVia API. DatePickerMode renamed to WDatePickerMode. 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.

    Open source →
    Release notes

    First stable release. wind is utility-first, Tailwind-syntax styling for Flutter; v1.0.0 is a complete rewrite of the 0.0.x line with a fresh API surface, a 19-parser pipeline with cached resolution, dark-mode pairs as a first-class contract, and a contracts-based debug bridge for external tooling. All public APIs follow Semantic Versioning 2.0.0 from this point forward.

    Migration from 0.0.x. v1 is not source-compatible with v0. Class names like WText, WDiv, WButton are preserved but constructor signatures, the supported className token set, and theme integration differ throughout. Consumers on 0.0.x rewrite their UI against the v1 API; the full v1 documentation lives at fluttersdk.com/wind.

    Added

    • 22 public widgets exported from the single barrel package:fluttersdk_wind/fluttersdk_wind.dart:
      • Layout: WDiv, WSpacer
      • Structural: WBreakpoint, WDynamic
      • Display: WText, WIcon, WImage, WSvg
      • Interactive: WAnchor, WButton
      • Overlay: WPopover
      • Form (raw): WInput, WSelect<T>, WCheckbox, WDatePicker
      • Form (FormField wrappers): WFormInput, WFormSelect<T>, WFormMultiSelect<T>, WFormCheckbox, WFormDatePicker
      • Utility: WKeyboardActions, WindAnimationWrapper
    • 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.
    • WindThemeData with 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. WindThemeController exposes toggleTheme(), setTheme(), updateTheme(), resetToSystem(). Optional onThemeChanged callback fires only on user-initiated toggles, not system brightness syncs.
    • State system, three layers. Automatic (hover:, focus: via WAnchor pointer + focus listeners), framework-managed (loading:, disabled:, checked:, error:), consumer-passed (states: Set<String>? for any custom string like selected:). All states funnel into a single Set<String> on the parser cache key.
    • Responsive prefixes (sm:, md:, lg:, xl:, 2xl:, customizable via WindThemeData.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-0 through order-12, order-first, order-last, order-none, plus arbitrary order-[n] (including negatives) for reordering flex children without changing source order.
    • Reverse flex direction: flex-row-reverse and flex-col-reverse flip the main-axis direction so justify-start mirrors per CSS semantics.
    • self-* align-self shorthand: self-start, self-end, self-center, self-stretch, self-auto, self-baseline as aliases for the align-self-* long form, matching Tailwind's canonical class name.
    • Flex grow / basis tokens: grow (Tailwind shorthand for flex-grow, i.e. flex: 1), grow-0 (no grow), and basis-1/2 / basis-1/3 / basis-1/4 / basis-full / basis-[Npx]. Basis approximates CSS flex-basis: it sets the child's initial MAIN-axis size (width in a row, height in a column) and ignores grow/shrink interplay. The no-grow / no-shrink resets follow last-class-wins: grow-0 cancels an earlier grow/flex-grow/flex-N, shrink-0 cancels an earlier shrink/flex-shrink, and flex-none (flex: 0 0 auto) cancels both, within the same active class list.
    • Smart column cross-axis stretch: a flex flex-col with no explicit items-* token now stretches each WDiv, WAnchor (any child), and WButton child that does not control its own width to the column width (CSS align-items: stretch default), so a clickable nav row (WAnchor(onTap) > WDiv) fills the column width without an explicit items-stretch or w-full. For a WAnchor: when it wraps a WDiv, the inner WDiv's className decides; when it wraps a WText or raw widget, it always stretches. Children that self-wrap in Expanded/Flexible (grow, flex-grow, flex-auto, flex-initial, shrink, flex-shrink, flex-N, in any state/breakpoint variant), children with an explicit w-* / min-w-* / max-w-* (including w-full), absolute children, bare WText leaves, and raw Flutter widgets are left untouched. shrink-0 / flex-none children still stretch on the cross axis (their no-shrink effect is main-axis only, matching CSS). Rows are unaffected.
    • Inline color props: WDiv.backgroundColor and WText.foregroundColor for runtime-dynamic colors. Override any bg-* / text-* from className and stay out of the parser cache key.
    • WBreakpoint widget: declarative per-breakpoint widget tree builder. Reach for it when className prefixes are not enough because the widget structure itself changes between breakpoints.
    • WDynamic: JSON-driven widget tree renderer. 13 Wind types + 16 Flutter core types allowed by default, extensible via builders:, restrictable via denyWidgets:, with customIcons: for user-defined icon mappings (24 built-in glyphs). State binding by widget id, action dispatch via WActionHandler, max recursion depth 50.
    • Accessibility / Semantics on 7 interactive widgets (WAnchor, WButton, WInput, WFormInput, WCheckbox, WSelect, WDatePicker): emit Semantics nodes with role + label, password fields mark obscured. Enables Playwright getByRole / getByLabel / getByText resolution against the Flutter web build. New optional semanticLabel parameters on WInput, WButton, and WAnchor; the button/anchor label gives icon-only controls an accessible name when there is no child text for MergeSemantics to absorb.
    • Wind.installDebugResolver(): call inside kDebugMode to register a WindDebugResolverImpl against the new fluttersdk_wind_diagnostics_contracts bridge. Resolves 7 fields per Wind widget element: className, breakpoint, brightness, platform, states, conditional bgColor, conditional textColor. Consumed by fluttersdk_dusk for E2E snapshot capture and by any runtime inspector. Tree-shaken in release builds.
    • wind-ui skill v2.0.2 community pattern: skills/wind-ui/SKILL.md section 15 plus skills/wind-ui/references/community.md add 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) and lib/src/components/ (8 modules) directories were deleted and reimplemented under lib/src/parser/parsers/ and lib/src/widgets/. Class names WText, WDiv, WButton are 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.0 to >=3.27.0. Dart SDK constraint set to >=3.4.0 <4.0.0.
    • BREAKING. Parser cache is now always on. The opt-in trackProvenance flag on WindParser.parse() and the WindStyle.resolvedVia field are gone; debug-tooling consumers read widget state through Wind.installDebugResolver() and the fluttersdk_wind_diagnostics_contracts contract package instead.
    • BREAKING. The public DatePickerMode enum is renamed to WDatePickerMode. The old name collided with Flutter Material's own DatePickerMode, forcing any consumer importing both package:flutter/material.dart and the wind barrel to hide one symbol. WDatePicker / WFormDatePicker mode: now takes WDatePickerMode.

    Removed

    • BREAKING. WindDuskIntegration class and the lib/dusk_integration.dart sub-barrel. Replaced by Wind.installDebugResolver() from the main barrel.
    • BREAKING. fluttersdk_dusk as a wind dependency at any level. Consumers needing Dusk for their own E2E add it to their own pubspec.
    • BREAKING. google_fonts and platform_info dependencies. Consumers depending on them transitively must add explicit deps.
    • BREAKING. WindParser.parse(trackProvenance:) parameter, WindStyle.resolvedVia field, and the enableProvenance() 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, and WindDebugResolverImpl are 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

    • WText bare rendering: a WText used outside a MaterialApp / Scaffold now renders with a brightness-aware baseline color (Colors.white on dark platforms, Colors.black on light, read from MediaQuery.platformBrightness) instead of Flutter's debug yellow-underline fallback. When no Directionality ancestor exists, WText injects one defaulting to TextDirection.ltr. Explicitly supplied colors (className text-*, foregroundColor, textStyle) still win and are unaffected.
    • Background image parser (bg-[/abs/path]): the FileImage(File(...)) branch is now guarded by kIsWeb; on web, where dart:io File is unsupported, the image degrades gracefully (skipped) instead of throwing at runtime. Non-web behavior is unchanged. pubspec.yaml now declares explicit platform support (android, ios, macos, web, linux, windows) so pub.dev platform detection is not narrowed by the dart:io import graph.
    • WASM compatibility: removed dart:io from the library import graph (platform_service.dart now uses defaultTargetPlatform; absolute-path bg-[/...] image resolution moved behind a conditional import). The package is now is: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 / WAnchor semanticLabel: the Semantics node now sets excludeSemantics: true and lifts onTap/onLongPress onto itself when semanticLabel is set, so the label overrides any child text instead of concatenating with it under MergeSemantics, while activation is preserved.
    • Wind.installDebugResolver(): the resolver no longer crashes on a className-less W-widget. WindDebugResolverImpl.resolve guarded its dynamic className read, so a bare WAnchor (or WBreakpoint / WindAnimationWrapper / WKeyboardActions) in the tree no longer throws NoSuchMethodError and abort the entire fluttersdk_dusk / telescope diagnostic snapshot.
    • WInput: px-* horizontal padding now matches the requested value exactly; OutlineInputBorder.gapPadding is set to 0.0 so px-3 produces 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 like top-8 top-4 top-8; previously .toSet() dropped the trailing occurrence.
    • WindParser.parse: cache is bypassed in both directions (no read, no write) when baseStyle is 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 at wind.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 bogus AssetImage("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 CSS flex: 0 0 auto (no grow AND no shrink). It no longer maps to a shrinking FlexFit.loose; instead it routes through the same no-shrink path as shrink-0, so a flex-none child in a justify-between row keeps its intrinsic size instead of being forced into a Flexible shrink allocation.
    • WindStyle.copyWith: a padding-, margin-, or text-only style keeps decoration == null instead of fabricating an empty BoxDecoration. This stops WDiv/WText from wrapping a needless Container around non-decorated content. shadow-none likewise no longer forces a Container.
    • WDynamicRenderer: malformed JSON degrades gracefully. A non-string type or non-list children is coerced defensively (routed through the whitelist / treated as no children) instead of throwing an implicit-downcast TypeError out of build().
    • WindThemeData: implements value-based operator == and hashCode. The equality guards in WindThemeController.setTheme and _WindThemeState.didUpdateWidget now compare by value, so a fresh default WindThemeData() on a parent rebuild no longer clobbers a prior toggleTheme() 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.dart for the last-class-wins-on-duplicates and baseStyle-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), WindThemeData value equality (wind_theme_data_test.dart), and icon-only button Semantics label (w_button_test.dart).
    • tool/coverage.sh portable threshold-aware lcov wrapper; GitHub Actions gate fails any PR dropping below 90%.
    • Surgical // coverage:ignore-line pragmas only on lines structurally unreachable from flutter test (kDebugMode branches, dart:io Platform.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 via getRect/getSize/renderObject, no golden files), test/interaction/ (tap/hover/focus/disabled/responsive/animation/overlay in-process), and test/performance/ (parser cache hit/miss speedup ratio gate, ~26x, plus report-only large-tree pump timing). Validated live via a fresh consumer app driven by fluttersdk_dusk (all routes navigated, wind: enricher 7-field block confirmed, screenshots manually compared). No behavioral regressions found.

    Production deps: flutter (SDK), flutter_svg ^2.0.0, fluttersdk_wind_diagnostics_contracts ^1.0.0. Dev deps: flutter_test (SDK), flutter_lints ^5.0.0. Full v1 documentation at fluttersdk.com/wind; LLM-facing skill at skills/wind-ui/ distributed via fluttersdk/ai (npx skills add fluttersdk/ai --skill wind-ui).


    Open source →
  10. 1.0.0-alpha.6 04 Apr 2026 pre-release

    Nothing published for this version

  11. 1.0.0-alpha.5 31 Mar 2026 pre-release

    Nothing published for this version

  12. 1.0.0-alpha.4 24 Mar 2026 pre-release

    Nothing published for this version

  13. 1.0.0-alpha.3 05 Feb 2026 pre-release

    Nothing published for this version

  14. 1.0.0-alpha.2 05 Feb 2026 pre-release

    Nothing published for this version

  15. 1.0.0-alpha.1 05 Feb 2026 pre-release

    Nothing published for this version

  16. 0.0.5 30 May 2026
    Release notes
    • 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.

    Open source →
  17. 0.0.4 12 Jun 2025

    Nothing published for this version

  18. 0.0.3 09 Jun 2025

    Nothing published for this version

  19. 0.0.2 02 Feb 2025

    Nothing published for this version

  20. 0.0.1 29 Jan 2025

    Nothing published for this version

Every package, every release, already written down.

The archive is open and free. Watching your own project is what we are building next.

Browse the archive