Skip to content

1.4.1

Choose a tag to compare

@github-actions github-actions released this 23 Aug 16:57
· 5 commits to master since this release
7d0178b

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)