1.4.1
Fixed
-
A multi-line
classNamehid its flex tokens from the row and column composers, and this project's own style guide is what put them there..claude/rules/widgets.mdandSKILL.mdboth instruct a className covering 3+ concerns to be a triple-quoted string with one concern per line, soflex-1at the end of a line arrives at the composition helpers asflex-1\n. Three of them split on a single space and therefore never matched it. Two consequences, both reproduced: aflex-1child of anoverflow-hiddenrow got wrapped a second time and threwIncorrect use of ParentDataWidget(_selfWrapsInFlex), and aw-24 shrink-0child in a crowded row shrank to its 50pt flex share instead of holding 96pt (_hasShrinkZero). The third site,_hasExplicitCrossWidth, never missed at all and is changed only so all five scans tokenize the same way: it matches withstartsWith('w-')rather than equality, and'w-24\n'.startsWith('w-')is true, so a trailing newline never hid an explicit width from it. That is also the real reason a test written for it passed before the fix, and the reason it ships without one. All five token scans inWDivnow share one hoisted_whitespaceRegex, which also stops the two that already split on whitespace from allocating a freshRegExpon every pass through the composition path. (lib/src/widgets/w_div.dart,test/widgets/w_div/multiline_classname_test.dart) -
Every hoverable
WDivannounced itself as a button that cannot be pressed.WDivauto-wraps into aWAnchorwhenever its className carrieshover:,focus:oractive:, purely to get the state this widget tracks, andWAnchorpublishedSemantics(button: true)unconditionally: with no gesture behind it. So a decorative card styledhover:bg-slate-100reached assistive technology as a control. Measured onWDiv(className: 'px-4 py-3 hover:bg-slate-100', child: WText('Latency')): one button node labelledLatencywhose action set wasfocusalone, with notap. A screen reader offers it as a button, the user activates it, and nothing happens, because there is no gesture in the tree to run. A gestureless, UNLABELLEDWAnchornow publishes no semantics node of its own and lets its descendants speak, which is what a state propagator should do (an explicitsemanticLabelstill publishes the named node either way, which is how a disabled control reports that it exists and is unavailable;WDiv's auto-wrap never passes one): the same card keeps itsLatencylabel and loses the role. An anchor withonTap,onLongPressoronDoubleTapis unchanged, soWButtonand every real control keep their single named button node with its tap action, and an explicitsemanticLabelstill wins for the icon-only case that has no child text to merge. One claim to retire with this: the nestingWAnchor(onTap:) > WDiv(hover:...)did NOT announce twice, though it published two button nodes in the widget tree. The inner one carriedisMergedIntoParent, so Flutter folded it into the real tap surface and never sent it to the platform. A reading that counts raw tree nodes rather than platform nodes will report a duplicate announcement that no screen reader ever made.MergeSemanticsgoes with the role rather than staying, and that was measured too: keeping it made a gestureless wrapper ABSORB a descendant control's role and actions, so a locked region tile in a consumer app swallowed the display-onlyWCheckboxinside it and published itself as "US West, button" carrying the checkbox's tap. That is the same bogus claim in a new place, so a styling-only wrapper now publishes nothing at all. (lib/src/widgets/w_anchor.dart,doc/widgets/w-anchor.md,doc/widgets/w-div.md,skills/wind-ui/SKILL.md,skills/wind-ui/references/widgets.md) -
WCheckboxoffered a tap action foronChanged: null. The anchor's callback was gated ondisabledalone, so a caller rendering a display-only checkbox (a read-only summary row, or a tile whose own tap drives the toggle) still installed() => onChanged?.call(!value): a control that announces itself as pressable and runs a no-op when pressed. Found in a consumer's region picker, whereIgnorePointer(child: WCheckbox(value: selected, onChanged: null))published a 16x16 nameless node with a tap action inside a tile that was already doing the work;IgnorePointerblocks pointers and leaves the semantics node in place. The gate is nowdisabled || onChanged == null, and thecheckedstate is still reported either way, so a read-only checkbox still tells assistive technology whether it is ticked. (lib/src/widgets/w_checkbox.dart) -
The same read-only
WCheckboxstill announced itself as an enabled control, and itsdisabled:classes never fired. The gate above removed the false tap affordance and stopped there, so the two statements beside it kept reading thedisabledprop alone:Semantics(enabled: !disabled)publishedenabled: truefor a control with nothing to press, and theactiveStatesset leftdisabledout, soWCheckbox(value: true, onChanged: null, className: 'disabled:opacity-50')rendered at full opacity.WRadioandWSwitchhave deriveddisabled || onChanged == nullonce at the top ofbuildin every released version (both landed before 1.2.0), which means one input produced three different behaviours across three sibling controls and the checkbox was the odd one out. It now derives the same way, and the singleisDisabledfeeds all four consumers: the state set, the semanticsenabledflag, the anchor'sisDisabled, and the gesture. Thecheckedstate is still reported either way, so a display-only checkbox keeps telling assistive technology whether it is ticked. Two visible consequences to expect on upgrade. AWCheckboxwith a nullonChangedanddisabled:classes now renders them, where before they never applied. And on theWDynamicJSON path,_buildWCheckboxpasses whateverparseValueActionreturns, which isnullwheneverprops.onChangeis absent or malformed, so a node like{"type": "WCheckbox", "props": {"id": "agree", "className": "... disabled:opacity-50"}}now renders dimmed and announces itself as not enabled. That node was already inert, since the state write lives inside the same null callback and tapping it never updatedid, so what changes is that a silently non-functional control became a visibly disabled one. (lib/src/widgets/w_checkbox.dart,doc/widgets/w-checkbox.md,skills/wind-ui/SKILL.md,skills/wind-ui/references/widgets.md,example/lib/pages/checkbox/checkbox_basic.dart)
Quality
-
The registry dispatch could never fire, so 1.4.0 shipped a skill the registry never received.
dispatch-to-registry.ymldeclaredrelease: [published], but the release is created insidepublish.yml'sgithub-releasejob bygh release createrunning underGH_TOKEN: ${{ github.token }}, and GitHub does not start workflow runs from events raised byGITHUB_TOKEN. The trigger was added on 2026-08-03 and 1.4.0 was the first release after it, so it had exactly one chance and missed: the run history showed nothing since 2026-08-03, both entries there beingworkflow_dispatchand the retiredpushtrigger. It reachedfluttersdk/aionly because it was dispatched by hand. The cost of this failure mode is that it is silent, since the publish workflow goes green either way and the only symptom is end users installing a skill a version behind.publish.ymlnow calls the workflow directly withneeds: github-release, which removes the cross-workflow event entirely and also guarantees the dispatch happens after pub.dev has accepted the release rather than in parallel with it. The deadreleasetrigger is gone andworkflow_callreplaces it;workflow_dispatchstays as the manual escape hatch. Secrets are passed by name rather thansecrets: inherit, since the called workflow needs exactly two and this repo pins every action by SHA and runs zizmor over the result. (.github/workflows/dispatch-to-registry.yml,.github/workflows/publish.yml) -
The version extractor tested the ref for a
vprefix this repo never tags with, so it always fell through topubspec.yaml. The step matched^v[0-9]+\.[0-9]+\.[0-9]+, whilepublish.yml's tag filter is[0-9]+.[0-9]+.[0-9]+*and CLAUDE.md's release step isgit tag X.Y.Z: that branch could not match a real tag here. Nothing surfaced it because the fallback happens to be correct on master after a release bump, so the dead branch and the working one returned the same string. The test is now^v?[0-9]+\.[0-9]+\.[0-9]+, which reads the tag when there is one and keeps the pubspec fallback for a manual run off a branch. (.github/workflows/dispatch-to-registry.yml) -
The registry sync now skips a prerelease tag, so a beta release does not hand beta skill content to every consumer.
publish.yml's tag filter is[0-9]+.[0-9]+.[0-9]+*, so1.5.0-beta.1matches it and this repo has shipped1.0.0-alpha.*tags. The reason to skip is distribution rather than a malformed version:fluttersdk/ai'ssync.ymlvalidates the upstream version as^[0-9]+\.[0-9]+\.[0-9]+(-[a-z0-9.]+)?$(a prerelease is explicitly allowed) and derives its own version by bumping its own manifest's patch number, so the string sent only reaches its release notes. What a sync does do is rsyncskills/wind-ui/onto the registry'smain, which is whatnpx skills add fluttersdk/aiserves. A branch name containing a dash cannot trip the guard, because on aworkflow_dispatchthegithub-releasejob is skipped by its own tag check and a dependant of a skipped job is skipped with it. (.github/workflows/publish.yml) -
The called workflow's checkout stops carrying a token it does not use. It now sets
persist-credentials: falselike every other checkout here: the job only readspubspec.yaml, zizmor'sartipackedaudit is on by default, and this change is what puts the workflow inside the release pipeline. (.github/workflows/dispatch-to-registry.yml) -
The skill's own title had been reading
Wind UI 1.3since the 1.4.0 release, and the release checklist is why. That checklist names three version spots inskills/wind-ui/: the ninereferences/*.mdH1s, the SKILL.mddescriptionprefix, and the<!-- fluttersdk_wind X.Y.x | Skill vN -->marker. There is a fourth, SKILL.md's own H1 on line 10, and nothing pointed at it, so 1.4.0 moved the other three and left this one behind. It is the first line of the file an agent loads. Corrected here with the rest of the 1.4.1 surface pass (pubspec.yaml,example/pubspec.yaml, thedartdoc_options.yamlsource-link tag, thellms.txtversion string); the nine reference H1s and the marker already read1.4and need no change for a patch. (skills/wind-ui/SKILL.md,pubspec.yaml,example/pubspec.yaml,dartdoc_options.yaml,llms.txt)