From b3a5cb75a25fcd7a37ca0670b1d8d4f1198d413a Mon Sep 17 00:00:00 2001 From: Anilcan Cakir Date: Thu, 10 Sep 2026 01:13:51 +0300 Subject: [PATCH 1/3] release: 1.5.3 Promotes the unreleased WAnchor keyboard and remote activation entry (92f20f5) to a version consumers can pin to. WidgetsApp's ActivateIntent now reaches onTap for Enter, Space, numpad Enter, the gamepad A button and select, the D-pad centre on Android TV, and the gestureless WDiv wrapper it depends on no longer shadows a ring-styled ancestor's focus or disabled state. Five surfaces bumped, the patch-release set: pubspec.yaml, example/pubspec.yaml, the dartdoc_options.yaml source-link tag, the llms.txt version string, and the CHANGELOG.md promotion with its link references. example/pubspec.lock tracked via flutter pub get in example/. Gates: dart analyze clean, dart format no diff, flutter test 1804 passing with the one pre-existing skip, ./tool/coverage.sh 90 at 95.3%. Not tagged and not published: this PR prepares the version, it does not ship it. --- CHANGELOG.md | 5 +++-- dartdoc_options.yaml | 2 +- example/pubspec.lock | 2 +- example/pubspec.yaml | 2 +- llms.txt | 2 +- pubspec.yaml | 2 +- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e162b7..4b939a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ This project follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0. --- -## [Unreleased] +## [1.5.3] - 2026-09-10 ### Added @@ -330,7 +330,8 @@ Production deps: `flutter` (SDK), `flutter_svg ^2.0.0`, `fluttersdk_wind_diagnos The 1.0.0-alpha.1 through 1.0.0-alpha.10 release notes (Feb 2026 to May 2026) are preserved in git history and on the `v0` branch. The 0.0.x line is end-of-life; consumers pin to `^1.0.0` going forward. -[Unreleased]: https://github.com/fluttersdk/wind/compare/1.5.2...HEAD +[Unreleased]: https://github.com/fluttersdk/wind/compare/1.5.3...HEAD +[1.5.3]: https://github.com/fluttersdk/wind/releases/tag/1.5.3 [1.5.2]: https://github.com/fluttersdk/wind/releases/tag/1.5.2 [1.5.1]: https://github.com/fluttersdk/wind/releases/tag/1.5.1 [1.5.0]: https://github.com/fluttersdk/wind/releases/tag/1.5.0 diff --git a/dartdoc_options.yaml b/dartdoc_options.yaml index b74e905..0ae4a2f 100644 --- a/dartdoc_options.yaml +++ b/dartdoc_options.yaml @@ -1,3 +1,3 @@ dartdoc: linkTo: - url: 'https://github.com/fluttersdk/wind/blob/1.5.2/%f%#L%l%' + url: 'https://github.com/fluttersdk/wind/blob/1.5.3/%f%#L%l%' diff --git a/example/pubspec.lock b/example/pubspec.lock index 370f5c4..229d176 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -126,7 +126,7 @@ packages: path: ".." relative: true source: path - version: "1.5.2" + version: "1.5.3" fluttersdk_wind_diagnostics_contracts: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index eb2a41b..e12ed07 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -14,7 +14,7 @@ publish_to: 'none' # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.5.2+1 +version: 1.5.3+1 environment: sdk: ">=3.4.0 <4.0.0" diff --git a/llms.txt b/llms.txt index 8d36872..e43bc1c 100644 --- a/llms.txt +++ b/llms.txt @@ -1,6 +1,6 @@ # fluttersdk_wind -> Tailwind CSS for Flutter. A utility-first styling framework that maps `className` strings to optimized Flutter widget trees, with 24-field `WindThemeData` theming, responsive prefixes (`sm:` / `md:` / `lg:` / `xl:` / `2xl:`), `dark:`, state prefixes (`hover:` / `focus:` / `disabled:` / `loading:` / `selected:`), platform prefixes (`ios:` / `android:` / `web:` / `mobile:`), 27 W-prefix widgets, `WindRecipe` / `WindSlotRecipe` variant composition, server-driven UI via `WDynamic`, and three AI integration layers (hosted MCP server, Claude Code skill, and a skill distributed to 8+ AI clients via fluttersdk/ai). Open Source. Tailwind syntax. Flutter native. Version 1.5.2 stable. +> Tailwind CSS for Flutter. A utility-first styling framework that maps `className` strings to optimized Flutter widget trees, with 24-field `WindThemeData` theming, responsive prefixes (`sm:` / `md:` / `lg:` / `xl:` / `2xl:`), `dark:`, state prefixes (`hover:` / `focus:` / `disabled:` / `loading:` / `selected:`), platform prefixes (`ios:` / `android:` / `web:` / `mobile:`), 27 W-prefix widgets, `WindRecipe` / `WindSlotRecipe` variant composition, server-driven UI via `WDynamic`, and three AI integration layers (hosted MCP server, Claude Code skill, and a skill distributed to 8+ AI clients via fluttersdk/ai). Open Source. Tailwind syntax. Flutter native. Version 1.5.3 stable. **Stack**: Flutter >=3.27.0 · Dart >=3.4.0 · Runtime deps: `flutter_svg`, `fluttersdk_wind_diagnostics_contracts`. No `mockito`, no state management library, no router. diff --git a/pubspec.yaml b/pubspec.yaml index d2d5b24..1a96e42 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: fluttersdk_wind description: 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. -version: 1.5.2 +version: 1.5.3 homepage: https://fluttersdk.com/wind repository: https://github.com/fluttersdk/wind issue_tracker: https://github.com/fluttersdk/wind/issues From 1b789a9d5f29ee578da181fad279b812ff121dcd Mon Sep 17 00:00:00 2001 From: Anilcan Cakir Date: Fri, 11 Sep 2026 00:13:12 +0300 Subject: [PATCH 2/3] fix(w-anchor): drop the focus ring when a control turns disabled Turning disabled sets `canRequestFocus` to false and `FocusNode` gives up focus in response, but `_onFocusChange` returns early for a disabled widget, so `_isFocused` and `_hasPrimaryFocus` kept the values they held on the last enabled frame. Hover was already cleared on that transition and focus was not. The asymmetry stayed private while a gestureless wrapper published its own `isFocused: false` over it. The inheritance that landed in #202 is what made it visible: a `WDiv` carrying `focus:ring-2 disabled:opacity-50` inside a button that disables itself on submit drew the ring and the dimming at the same time. Cleared in `didUpdateWidget` beside the hover flag, not in the listener, which cannot see a transition it returns early from. Covered by a transition test in `dpad_activation_test.dart`, red before the change. The CHANGELOG entry joins the 1.5.3 section, which this PR has not tagged yet. --- CHANGELOG.md | 6 ++-- lib/src/widgets/w_anchor.dart | 10 ++++++ .../w_anchor/dpad_activation_test.dart | 32 +++++++++++++++++++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b939a6..b3a53f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,15 +11,13 @@ This project follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0. ### Added - **A focused `WAnchor` activates on the keyboard and on a television remote.** `onTap` now answers `ActivateIntent`, which `WidgetsApp` raises for `Enter`, `Space`, numpad `Enter`, the gamepad A button and `select`, the D-pad centre on Android TV and the click on the Apple TV remote. `WAnchor` binds no key of its own, so a key the platform adds later arrives for free. Only `onTap` is bound: `ActivateIntent` means the primary action and there is no second key for `onLongPress` or `onDoubleTap`, which matches every button Flutter ships. +- **`WindAnchorState.hasPrimaryFocus`**, separating "this element is the focus" from "this element contains the focus". A gestureless wrapper republishes it with the inherited value ORed in, so the signal chains through nesting: a wrapper's own node never holds primary focus, so stopping at it left a ring two wrappers deep dark, and any `hover:` or `active:` class on an intermediate div creates that second wrapper. `isFocused` has always been the second of those, because it comes from `FocusNode.hasFocus`, which is true for an ancestor of the real holder. The distinction is what a styling wrapper has to inherit: a tappable card containing a text field reports focus-within while the user types, so a wrapper inheriting `isFocused` lit its ring while sitting beside the field rather than around it. Defaults to `false`, so nothing that constructs the state by hand has to change. ### Fixed - **One control is one traversal stop, and the focus ring lands on the thing that activates.** `WDiv` wraps itself in a gestureless `WAnchor` whenever its className carries `hover:`, `focus:` or `active:`, and that wrapper was a full focus stop publishing its own state. So `WAnchor(onTap:) > WDiv('focus:ring-2')`, the shape of every ring-styled control, cost two presses of Tab: the first landed on the node carrying the gesture and drew no ring, the second drew the ring on a node `Enter` could not activate. A gestureless wrapper is now a styling wrapper: it is not a traversal stop, and it inherits `focus` and `disabled` from the nearest anchor above it instead of shadowing them. Two shapes are deliberately unchanged: a `WDiv` carrying `focus:` with no anchor above it keeps its own node, because that is how a consumer styles a custom control, and a focusable descendant still lights the wrapper's ring, because `FocusNode.hasFocus` covers descendants and a `WInput` inside a ring-styled `WDiv` has always drawn the ring around the field being typed in. - **`disabled:` now fires on a `WDiv` inside a disabled `WAnchor`.** The same shadowing, in a third state: the gestureless wrapper published `isDisabled: false` over a disabled ancestor, so the element carrying `disabled:opacity-50` never saw it. Covered by the same inheritance. - -### Added - -- **`WindAnchorState.hasPrimaryFocus`**, separating "this element is the focus" from "this element contains the focus". A gestureless wrapper republishes it with the inherited value ORed in, so the signal chains through nesting: a wrapper's own node never holds primary focus, so stopping at it left a ring two wrappers deep dark, and any `hover:` or `active:` class on an intermediate div creates that second wrapper. `isFocused` has always been the second of those, because it comes from `FocusNode.hasFocus`, which is true for an ancestor of the real holder. The distinction is what a styling wrapper has to inherit: a tappable card containing a text field reports focus-within while the user types, so a wrapper inheriting `isFocused` lit its ring while sitting beside the field rather than around it. Defaults to `false`, so nothing that constructs the state by hand has to change. +- **Disabling a focused `WAnchor` takes its focus ring with it.** Turning disabled sets `canRequestFocus` to false and `FocusNode` gives up focus in response, but the focus listener returns early for a disabled widget, so the flags kept the values they held on the last enabled frame. Hover was already cleared on that transition and focus was not; the asymmetry stayed private while a gestureless wrapper published its own `isFocused: false` over it, and the inheritance above is what made it visible. A `WDiv` carrying `focus:ring-2 disabled:opacity-50` inside a button that disables itself on submit drew the ring and the dimming at the same time. (`lib/src/widgets/w_anchor.dart`) ## [1.5.2] - 2026-09-08 diff --git a/lib/src/widgets/w_anchor.dart b/lib/src/widgets/w_anchor.dart index 6bdb090..55371b2 100644 --- a/lib/src/widgets/w_anchor.dart +++ b/lib/src/widgets/w_anchor.dart @@ -158,11 +158,21 @@ class _WAnchorState extends State { _focusNode.addListener(_onFocusChange); } + /// Drops every interaction flag the moment the widget turns disabled. + /// + /// Focus has to be cleared here, not left to the listener. Turning disabled + /// sets `canRequestFocus` to false, which makes `FocusNode` unfocus itself, + /// but [_onFocusChange] returns early for a disabled widget and the flags + /// would keep the values they held on the last enabled frame. A gestureless + /// wrapper inherits `hasPrimaryFocus`, so a stale one is not a private + /// detail: it draws `focus:ring-*` around a control that is disabled. @override void didUpdateWidget(covariant WAnchor oldWidget) { super.didUpdateWidget(oldWidget); if (widget.isDisabled && !oldWidget.isDisabled) { _isHovering = false; + _isFocused = false; + _hasPrimaryFocus = false; } } diff --git a/test/widgets/w_anchor/dpad_activation_test.dart b/test/widgets/w_anchor/dpad_activation_test.dart index 3faeda5..945bdd6 100644 --- a/test/widgets/w_anchor/dpad_activation_test.dart +++ b/test/widgets/w_anchor/dpad_activation_test.dart @@ -524,5 +524,37 @@ void main() { final BuildContext inner = tester.element(find.byType(WText)); expect(WindAnchorStateProvider.of(inner)?.isDisabled, isTrue); }); + + testWidgets('and disabling a focused one takes its ring with it', ( + tester, + ) async { + // Turning disabled unfocuses the node (`canRequestFocus: false` makes + // `FocusNode` give up focus), but `_onFocusChange` returns early for a + // disabled widget, so the flags survived the transition. The wrapper + // inherits `hasPrimaryFocus`, which is what turned a private staleness + // into a ring drawn around a control the user can no longer activate. + Widget build(bool disabled) => WAnchor( + onTap: () {}, + isDisabled: disabled, + child: const WDiv( + className: 'p-2 focus:ring-2 disabled:opacity-50', + child: WText('Play'), + ), + ); + + await pump(tester, build(false)); + traversalStops(tester).single.requestFocus(); + await tester.pump(); + + BuildContext inner() => tester.element(find.byType(WText)); + expect(WindAnchorStateProvider.of(inner())?.isFocused, isTrue); + + await pump(tester, build(true)); + await tester.pump(); + + expect(WindAnchorStateProvider.of(inner())?.isDisabled, isTrue); + expect(WindAnchorStateProvider.of(inner())?.isFocused, isFalse); + expect(WindAnchorStateProvider.of(inner())?.hasPrimaryFocus, isFalse); + }); }); } From 3ef430648e88e7ee04e954e537c55c214e9b436c Mon Sep 17 00:00:00 2001 From: Anilcan Cakir Date: Fri, 11 Sep 2026 00:13:55 +0300 Subject: [PATCH 3/3] docs(changelog): date 1.5.3 for the day the PR closes --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3a53f2..1b54ff6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ This project follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0. --- -## [1.5.3] - 2026-09-10 +## [1.5.3] - 2026-09-11 ### Added