Skip to content

Releases

1.12.1

Choose a tag to compare

@github-actions github-actions released this 15 Nov 15:15
8c5c981

Fixed

  • llms.txt links PROJECT.md. The index now points an agent at the scope and the decisions already made. (llms.txt)
  • The grid-cols-N docs no longer say the grid is a Flutter GridView with crossAxisCount: N. A grid is a Wrap with computed column widths (or, with items-stretch, a column of equal-height rows); the Quick Reference on doc/layout/grid.md, the Display page and the gallery now read "N equal columns", and the page names the Row used with horizontal overflow scroll. (doc/layout/grid.md, doc/layout/display.md, example/lib/pages/layout/display.dart, example/lib/pages/layout/grid_cols.dart, #305)
  • The press-state advice is one answer everywhere. The README and the WDiv page said to pass states: {'pressed'}, while the state guide, llms.txt and the skill said states: {'active'}; they now all say active, the state the active: prefix reads. (README.md, doc/widgets/w-div.md, #305)
  • Eleven doc pages gain a ## Related Documentation section (dark mode, debugging, theme binding, theming, utility-first, installation, upgrade guide and the colour, responsive, spacing and typography helpers), and the Z-Index, Opacity and WindRecipe pages use the same bare-list table of contents as every other page. (doc/core-concepts/, doc/getting-started/, doc/utilities/, doc/layout/z-index.md, doc/styling/opacity.md, doc/styling/wind-recipe.md, #305)
  • The Windows and macOS example runners carry com.anilcancakir.wind_kit instead of the old com.fluttersdk.wind company string, and the stale Android MainActivity of the old package name is gone. (example/windows/runner/Runner.rc, example/macos/Runner/Configs/AppInfo.xcconfig, example/android/, #305)
  • The tables in the README and the skill are left-aligned. They used the unaligned |---| separator, which the repo standard does not accept (every table is left-aligned with an explicit colon); they use |:---| like the docs. The wind-ui skill's frontmatter lists version before when_to_use, as the standard orders it, and example/pubspec.lock records wind_kit 1.12.0. (README.md, skills/wind-ui/SKILL.md, skills/wind-ui/references/{debug,dynamic,forms,layouts,tailwind-divergence,theme,tokens}.md, example/pubspec.lock, #307)

1.12.0

Choose a tag to compare

@anilcancakir anilcancakir released this 13 Nov 21:28
2a50099

Changed

  • Renamed from fluttersdk_wind to wind_kit. The package, its barrel (package:wind_kit/wind_kit.dart) and every test carry the new name, and the diagnostics dependency is wind_kit_diagnostics_contracts ^1.3.0 (was fluttersdk_wind_diagnostics_contracts ^1.2.0; WindDebugRegistry, WindDebugResolver and WindPerfResolver are unchanged). Migration: replace the dependency in pubspec.yaml and the import package:fluttersdk_wind/fluttersdk_wind.dart with package:wind_kit/wind_kit.dart. Every W* and Wind* class, every className token and the parser cache are unchanged by the rename; the Material types in the public signatures move to material_ui in the next entry, so an app that passes Colors or a ThemeData follows that migration too. Releasing both names in parallel was rejected because two packages with one codebase would split the ecosystem's dependants between them. The old fluttersdk_wind package receives no further feature releases. Do not depend on both fluttersdk_wind and wind_kit in one app: each has its own WindTheme and its own WindDebugRegistry, so a WDiv from one finds no theme from the other. The packages that depend on it (magic, magic_starter, magic_notifications, magic_devtools) move to their wind_kit releases together, and tapir moves to wind_kit_diagnostics_contracts in step, or its snapshots lose the Wind data. pubspec.yaml, lib/wind_kit.dart, test/, example/pubspec.yaml (#272)
  • BREAKING. wind_kit builds on material_ui, so an app uses package:material_ui/material_ui.dart for its MaterialApp, ThemeData, Colors and MaterialColor. Flutter 3.50 deprecates package:flutter/material.dart and package:flutter/cupertino.dart, and pana's static analysis score drops 10 points for a package that still imports them, so every file in lib/ now imports material_ui ^1.11.0 instead. The new package defines its own MaterialApp, ThemeData, MaterialColor and localisations, so an app that still imports package:flutter/material.dart gets "No MaterialLocalizations found" from the default close button of WKeyboardActions and from the selection toolbar of WText(selectable: true), a ThemeData type mismatch from WindThemeData.toThemeData() and WindThemeController.toThemeData(), and a MaterialColor type mismatch wherever it passes colours: the colors maps of WindThemeData (constructor and copyWith), WindThemeController (colors, updateTheme) and windColors, and invertMaterialColor, take the material_ui type, so WindThemeData(colors: {'brand': Colors.indigo}) with Flutter's Colors no longer compiles. Run flutter pub add material_ui so the app declares the package it now imports, migrate its imports in the same upgrade and replace the flutter_localizations delegates (GlobalMaterialLocalizations, GlobalCupertinoLocalizations) with GlobalMaterialLocalizations.delegates from material_ui. Keeping the deprecated package:flutter/material.dart barrels was rejected: Flutter 3.50 flags every import of them, pana takes 10 static analysis points off, and the barrels are slated for removal. pubspec.yaml, lib/, test/, example/pubspec.yaml, example/lib/, CLAUDE.md (#279)
  • BREAKING. wind_kit needs Dart 3.13 and Flutter 3.47. material_ui 1.11 and cupertino_ui 1.1 require that floor, so the old one (Dart 3.4, Flutter 3.27) no longer resolves with them; an app on an older SDK stays on fluttersdk_wind 1.11.x, the last release under the previous name. Staying on the old floor was not an option, since material_ui and cupertino_ui require the new one. The floor also moves the package to the tall Dart formatter, so lib/ and test/ are reformatted with it, and the lints the new floor enables are satisfied without a change to any public signature. pubspec.yaml, CLAUDE.md, lib/, test/ (#285)
  • The user docs, the wind-ui skill (2.32.0) and ARCHITECTURE.md no longer promise behaviour the code does not have. An audit of 42 changed hunks against lib/ found the old text false in 26 of them: a WindController class that does not exist, states: {'pressed'} where only 'active' reaches active:, an 'input-border' theme key that WInput never reads, camelCase customIcons keys that never match, font stacks that were wrong, and a cache note that said the parser cache never needs clearing in production although its key leaves out the theme maps. Deleting the unverified claims instead of correcting them was rejected because pub.dev and the skill registry publish these pages as the reference. ARCHITECTURE.md gains alias expansion, the cache bypass, invariants and a change guide, and the .claude/ hook formats then analyses a Dart file in one script. doc/, skills/wind-ui/, llms.txt, ARCHITECTURE.md, .claude/settings.json, .claude/hooks/dart-format-analyze.sh (#273, #274, #275, #276, #277, #288, #292)
  • The gallery is wind_kit_example, builds from the working tree and needs Dart 3.13 and Flutter 3.47. The example package is renamed from the previous name and depends on wind_kit with path: ../, so the gallery always shows the code it ships with instead of the last published release; its native ids are com.anilcancakir.wind_kit (Android and the other platforms) and com.anilcancakir.windKit (Apple, which takes no underscore); its SDK floor matches the package's; and the unused cupertino_ui dependency is dropped. Pinning the gallery to a hosted wind_kit release was rejected because the docs site embeds it as the live demo of the current code, and a hosted pin would lag every change until a publish. example/pubspec.yaml, example/lib/, example/android/, example/ios/, example/macos/, example/linux/, example/windows/, example/web/, .github/dependabot.yml (#278, #282, #286)
  • The pub.dev description is marketing-first and the lockfile is no longer tracked. The pubspec.yaml description is the 174-character one the GitHub repository carries, so pub.dev and GitHub show the same pitch, the dartdoc_options.yaml source links point at anilcancakir/wind_kit, and the root pubspec.lock is untracked and ignored, because a library does not commit its lockfile and an app resolves its own graph. Tracking the lockfile was rejected because it pins the CI to versions no consumer resolves and hides a breaking upper bound until a user hits it. pubspec.yaml, dartdoc_options.yaml, .gitignore, pubspec.lock (#283)
  • The package and the gallery lint with flutter_lints ^6.0.0, and the 13 findings it adds are fixed. The Dart 3.13 floor lets the newer rule set resolve; it flags if (x != null) x! list and map entries (use_null_aware_elements) and a double underscore parameter (unnecessary_underscores). dart fix rewrote them to ?x entries and _, with identical behaviour: the 2037 tests and the 96.1% coverage are unchanged. Staying on flutter_lints ^5.0.0 was rejected because it keeps the repo linting against a rule set that cannot see the null-aware syntax the new floor already allows, and turning the two rules off in analysis_options.yaml was rejected because it would hide the same findings in new code. pubspec.yaml, example/pubspec.yaml, lib/src/utils/wind_perf_counters.dart, lib/src/widgets/ (w_breakpoint.dart, w_card.dart, w_div.dart, w_form_date_picker.dart, w_form_input.dart, w_form_select.dart) (#289)
  • WInput draws its own selection handles and selection toolbar, so the package no longer imports cupertino_ui and a bare WidgetsApp root still gets handles and a copy, cut, paste and select-all menu. The Cupertino handles (cupertinoTextSelectionHandleControls), the Cupertino toolbar and one grey constant were the only use of cupertino_ui, so they are replaced by WindTextSelectionHandleControls (a lollipop handle in the ambient cursor colour, blue-500 without one, still mixing in TextSelectionHandleControls so MaterialLocalizations is never read) and WindTextSelectionToolbar (a rounded row with a shadow, positioned by TextSelectionToolbarLayoutDelegate, white or slate-800 by the field's own brightness and refreshed while open when that brightness changes), both built on package:flutter/widgets.dart only and not exported. The toolbar scrolls sideways instead of paging when its buttons are wider than the screen (seven iOS actions or German labels on a 320px phone), so every action stays reachable, and it draws no arrow. Look Up, Search Web and Share now carry their WidgetsLocalizations labels instead of rendering blank, and an action with no label at all (Live Text, which WidgetsLocalizations does not name) is left out. material_ui still pulls cupertino_ui in transitively, so an app's resolved graph does not shrink. pubspec.yaml, lib/src/widgets/w_input.dart, lib/src/widgets/w_text_selection.dart, test/widgets/w_text_selection_test.dart, test/widgets/w_input/selection_test.dart, CLAUDE.md (#287)
  • The dartdoc on WindTheme, WImage, WSvg, WPopover, WKeyboardActions and the colour and context helpers now describes what the code does. Each claim was checked against the source: onThemeChanged also fires for setTheme, updateTheme(brightness:), resetToSystem and a new data: from a parent rebuild when the brightness differs (never for an OS brightness change), WImage and WSvg ignore shadow-*, w-full, max-w-* and animate-*, a WPopover maxHeight caps the overlay without scrolling its content, the keyboard bar's default button reads MaterialLocalizations.okButtonLabel, and wColorExt takes shade:. The examples in the comments carry their dark: pairs and debugPrint replaces print. The gallery page z_index_hover.dart now shows paint order and hover shadows instead of a z-* class that no widget reads. Comments only, no behaviour change. Deleting the wrong claims instead of correcting them was ...
Read more

1.11.1

Choose a tag to compare

@github-actions github-actions released this 06 Nov 15:30
c595099

Fixed

  • A horizontally scrolling WDiv (overflow-x-* alone or overflow-auto on both axes) paints its border, background and padding across at least the full viewport, so a w-full flex flex-row overflow-x-auto border-b tab strip keeps its baseline edge to edge. The div's own box was built inside the horizontal SingleChildScrollView and sized to its content, so with four tabs on a desktop the baseline stopped after the fourth one; the w-full it carried sized the scroll view, not the box inside it. CSS paints those on the scroll container. The box is now wrapped in a WindMinWidthBox in a new loose mode: at least the published viewport width, and free to be wider, so content wider than the viewport still scrolls. Reusing the tight mode w-full children get was rejected: it forced wide content into the viewport width and overflowed. The div's own min-w-* is not applied here, since it sizes the scroll container rather than what it scrolls. (#264) (lib/src/widgets/w_div.dart, lib/src/widgets/wind_min_width_scroll.dart, test/widgets/w_div/overflow_test.dart, test/flex/wind_min_width_scroll_test.dart, doc/layout/overflow.md)

1.11.0

Choose a tag to compare

@github-actions github-actions released this 29 Oct 20:59
db3dd5d

Added

  • wWindowClassOf(context) returns a WindowSizeClass (compact, medium, expanded), so a shell chooses a bar or a rail by window width and not by platform. Wind maps iOS to isMobile (platform_service.dart), so an iPad reads as a phone by platform, and wIsTablet is only the md to lg band, which turns false again on a wide iPad or a desktop window; neither answers "does this window have room for a rail". The classes are cut at the theme's own md and lg screens (compact below md, medium up to below lg, expanded from lg), so a custom screens table moves them, and it reads the size aspect of MediaQuery through wScreenIs, so a keyboard inset does not rebuild the caller. A platform or device-idiom check was rejected: split view puts an iPad in a phone-sized window. wIsTablet and isMobile are unchanged. (#259) (lib/src/utils/wind_extensions.dart, test/utils/wind_extensions_test.dart, doc/utilities/responsive-helpers.md, example/lib/pages/utilities/responsive_helpers_basic.dart, skills/wind-ui/SKILL.md, skills/wind-ui/references/theme.md)

1.10.0

Choose a tag to compare

@github-actions github-actions released this 27 Oct 00:24
ee1ef39

Added

  • WImage supports Tailwind's object-position classes, so object-cover object-top keeps the upper part of a backdrop in a short, wide banner. object-cover always centred the crop, so faces and skylines in the top of a TV or film backdrop were cut off in a w-full h-full hero, and there was no alignment prop to reach for. object-bottom, object-center, object-left, object-left-bottom, object-left-top, object-right, object-right-bottom, object-right-top and object-top now map to the matching Alignment on the rendered Image (Alignment.center stays the default), with every fit. They follow the object-fit design rather than a new one: read straight off the className in WImage, never in WindStyle, first match wins, and a prefixed form such as md:object-top is not applied (as md:object-cover is not today). The nine tokens join the parser's known-unparsed set, so they no longer log the one-time unknown-className hint. (#257) (lib/src/widgets/w_image.dart, lib/src/parser/wind_parser.dart, test/widgets/w_image_test.dart, test/parser/wind_parser_test.dart, doc/widgets/w-image.md`)
  • A number WInput or WFormInput accepts the decimal mark the user's locale writes. The default InputType.number filter allowed only ., and swallowed any other keystroke, so a Turkish user typing 2,5 got 25, a silent 10x amount, and every app writing a comma locale had to replace the filter with a formatter of its own. A new decimalSeparator (default '.', one character other than a digit, + or -, asserted) names the one mark the filter accepts; , keeps 2,5 and refuses a ., and the default keeps today's behaviour. The field keeps the text as typed, so converting to the . a wire format expects stays with the caller. Accepting both marks at once was rejected: in English 1,000 is a thousand, and reading it as 1.000 would turn a grouping comma into a decimal one. Caller inputFormatters still replace the filter entirely. (#253) (lib/src/widgets/w_input.dart, lib/src/widgets/w_form_input.dart, test/widgets/w_input_test.dart, test/widgets/w_form_input_test.dart, doc/widgets/w-input.md, doc/widgets/w-form-input.md, skills/wind-ui/SKILL.md)

1.9.4

Choose a tag to compare

@github-actions github-actions released this 18 Oct 11:45
b2b615e

Fixed

  • A single-line WInput centres its text and placeholder vertically in a box taller than the line. With min-h-11 or h-12 the text and the placeholder were drawn at the top of the box, where a CSS input centres them. The box size lands on the outer Container, and below it the editable sat directly in the content Padding (or, with a placeholder, in a Stack that aligned topStart), so the extra height stayed under the text. Measured on a 44 px box, the caret line centred at 7 px against a box centre of 22 px, and at 7 px against a box centre of 24 px on a 48 px box. The no-adornment path now wraps the field in an Align (AlignmentDirectional.centerStart, heightFactor: 1) inside the content padding, so it stays one line tall and takes the box's min height through the constraints; the placeholder sits in that same aligned line. Horizontal position, content padding and the height of a field with no height class are unchanged (30 px for one 14 px line with 8 px of vertical padding), the whole box stays tappable, and a multiline field keeps its top alignment. The editable (and its text-field semantics node) is now one line tall inside the box, as it already was with a placeholder, so an accessibility focus rect or a dusk bound for a field with no placeholder covers the line rather than the padded box. The prefix and suffix path already centred through its Row and is untouched. (lib/src/widgets/w_input.dart, test/widgets/w_input_test.dart, doc/widgets/w-input.md)

1.9.3

Choose a tag to compare

@github-actions github-actions released this 18 Oct 09:29
99be55f

Fixed

  • A closed WSelect publishes one button node with the tap action, so a screen reader announces it once. The trigger was a Semantics(container: true, button: true) node around a MergeSemantics, and a container is a semantics boundary, so the merge did not fold into it: it became a second boundary. The platform received two nested nodes with the same name, an outer button with the label and no action, and an inner one with the merged trigger text, the tap and the focus. Neither is isMergedIntoParent, so dusk listed the control twice and an agent could pick the action-less node. Measured on the default single trigger, the multi trigger with chips and a custom triggerBuilder: each had two platform nodes, now one. The outer node now carries the label, the button flag, the enabled state and the tap that opens or closes the menu (absent when disabled), and an ExcludeSemantics drops the trigger subtree below it, as WAnchor does for an explicit label. Focus stays outside the exclusion so the node keeps focusable, focused and the focus action. Nothing reachable is lost: the chips' remove taps and a custom trigger's content were already merged into the inner node, and the search field lives in the overlay, which this does not touch. (lib/src/widgets/w_select.dart, test/widgets/w_select_test.dart, doc/widgets/w-select.md)

1.9.2

Choose a tag to compare

@github-actions github-actions released this 18 Oct 07:12
8f34f68

Fixed

  • A WSelect is named by its selection, and by its placeholder only while it is empty. The closed trigger's accessible name was the placeholder whenever one was set, and the default is the English "Select an option", so a select showing "Apple" was announced as "Select an option" and, in a Turkish app, in the wrong language. The name now follows the trigger's visible text: the selected option's label, the labels joined with ', ' for a multi select, and the placeholder (none when it is empty) when nothing is selected, as Flutter's DropdownButton names itself by its visible child. Visible to dusk and tests: a script that found a selected select by its placeholder text now finds it by the selected label. (lib/src/widgets/w_select.dart, test/widgets/w_select_test.dart, doc/widgets/w-select.md, doc/widgets/w-form-select.md)

1.9.1

Choose a tag to compare

@github-actions github-actions released this 18 Oct 04:06
d12b603

Fixed

  • Every WButton is a button to assistive technology, including one with no callback, and it is not enabled while disabled, loading or without a callback. A button with no onTap, onLongPress or onDoubleTap (the common disabled: !canSubmit, onTap: canSubmit ? submit : null, or onTap: null alone) gave WAnchor nothing to gesture on, and a gestureless anchor publishes no node, so its label read as plain text. It now publishes one button node of its own with enabled: false, labelled by its child text or its semanticLabel, and no tap action; a semanticLabel on such a button used to publish an enabled button. An enabled button keeps the single node WAnchor gives it, and a gestureless WAnchor used for styling still publishes nothing. (lib/src/widgets/w_button.dart, test/widgets/w_button_test.dart)
  • A disabled or loading WButton is still a button, announced as disabled. It handed WAnchor no callbacks while inert, and a gestureless anchor is a styling wrapper that publishes no button node, so screen readers lost the control and Playwright's getByRole('button') found nothing. Visible to dusk: a disabled or loading button now appears in a snapshot as a button with enabled: false and its label (the loadingText while loading), where it used to be absent. The callbacks now reach WAnchor with isDisabled set for loading as well as disabled, so WAnchor's own guard keeps tap, long press, double tap, the activation key and the lifted semantics actions from firing; loading still activates only loading:, not disabled:. A loading button is no longer a Tab stop and no longer lights hover: or focus:, as Material's buttons behave with a null callback. (lib/src/widgets/w_button.dart, test/widgets/w_button_test.dart)
  • A WPopover trigger is one button. The trigger's Semantics now merges its subtree, so the documented WButton(onTap: null) trigger (which now publishes its own disabled button node) folds into the popover's single enabled button named by its label instead of nesting a second one. (lib/src/widgets/w_popover.dart, test/widgets/w_popover_test.dart)
  • A WSelect shows keyboard focus, and a disabled one is skipped by Tab. The trigger lit focus: only while its menu was open, so a keyboard user tabbing through a form saw nothing on the select they had landed on. It now also lights while the trigger holds focus in the traditional (keyboard) highlight mode, so a tap does not ring it. Its Focus node takes canRequestFocus: !disabled, and focus is never active on a disabled select. The trigger publishes its state through WindAnchorStateProvider, as WPopover does, so the WAnchor a focus: class wraps around the trigger div inherits instead of being a second Tab stop that a disabled select kept. (lib/src/widgets/w_select.dart, test/widgets/w_select_test.dart)

1.9.0

Choose a tag to compare

@github-actions github-actions released this 14 Oct 09:10
f61f623

Changed

  • BREAKING (behaviour): tracking-* is now em, as in Tailwind, so letter spacing scales with the font size. It ships in 1.9.0, a minor release, as the behavioural break in 1.4.0 did, so a ^1.8 constraint picks it up on the next pub get. Every app that uses a tracking-* class renders differently, and a custom WindThemeData.tracking map is now read as em multipliers: a value written as pixels ('tight': -1) now means -1em, so convert it (-1px on 14px text is about -0.07). The defaults were -2/-1/0/1/2/4 logical pixels under comments that said -0.05em through 0.1em, and Flutter's TextStyle.letterSpacing is pixels, so tracking-tight was a flat -1px: -0.07em on 14px text, nearly three times Tailwind's -0.025em, and a 14px semibold title rendered with its letters colliding. The theme map now holds Tailwind's values (-0.05, -0.025, 0, 0.025, 0.05, 0.1). WindStyle.letterSpacing carries a new letterSpacingUnit (WindLetterSpacingUnit.em or .px) and toTextStyle({inheritedFontSize}) converts to pixels against the size the text renders at: the text-* size in the same className (responsive and state variants included), else the inherited DefaultTextStyle size, else kDefaultFontSize (14), the size unsized text paints at (the theme's base is not used, because no text paints at it unless a DefaultTextStyle sets it). The conversion happens at build in WText, WDiv, WButton and WInput, because the parser cannot know the inherited size and its cache key does not carry it; resolving to a fixed 16 in the parser was rejected for exactly that reason. WText resolves against a textStyle: font size when one is given. WDiv and WButton publish the resolved pixels to their children, which is how CSS inherits a computed letter-spacing. WInput with no text-* resolves against kDefaultFontSize (14), since EditableText reads no DefaultTextStyle. The new wInheritedFontSize(context) helper exposes the same fallback chain for custom widgets. The em is taken from the unscaled size: Flutter scales fontSize by the accessibility TextScaler and leaves letterSpacing as written. (lib/src/theme/defaults/tracking.dart, lib/src/theme/wind_theme_data.dart, lib/src/parser/wind_style.dart, lib/src/parser/parsers/text_parser.dart, lib/src/utils/wind_helpers.dart, lib/src/widgets/w_text.dart, lib/src/widgets/w_div.dart, lib/src/widgets/w_button.dart, lib/src/widgets/w_input.dart)

Fixed

  • WButton paints its opacity-*, so a disabled:opacity-50 button looks disabled. It built its box without the parsed opacity, so disabled:, loading: and hover: opacity classes parsed and painted nothing, and a disabled button looked enabled. It now wraps the box in an Opacity, or an AnimatedOpacity under a duration-*, as WDiv does. (lib/src/widgets/w_button.dart)
  • A ring-* is an outline, so a focused element with no background is no longer a solid block. Wind drew the ring as a spread BoxShadow. A browser clips a box-shadow to the outside of its element, so Tailwind's ring reads as an outline; Flutter does not clip it, and an element with no background filled with the ring colour. A host app's rail drew every keyboard-focused icon button as a solid blue block with the glyph lost inside it. WDiv now paints WindStyle.ringOutline, a stroke over the box on its own corner radius: outside the edge, inside it for ring-inset (which never showed over a background before), and moved out by ring-offset-* with the gap left unpainted (the offset used to be covered by the ring itself). The ring no longer joins the box shadows. A focused WInput paints the same outline instead of its ring shadow (it still promotes the ring to its border), so a transparent field no longer fills with the ring colour. The docs' ring-offset-white example is gone: Wind has no ring-offset colour and the class did nothing. (lib/src/parser/wind_style.dart, lib/src/widgets/w_div.dart, lib/src/widgets/w_input.dart, test/pixel/ring_outline_pixel_test.dart, test/widgets/w_input_test.dart, test/pixel/radius_ring_border_pixel_test.dart, doc/borders/ring.md)
  • WButton paints its ring-* and shadow-*. It built its box from the parsed decoration alone, so both parsed and painted nothing: a host app's dialog buttons showed no focus while Tab moved between them. It now folds boxShadow into the decoration and paints ringOutline over the box. (lib/src/widgets/w_button.dart, test/widgets/w_button_test.dart)
  • A WPopover opens from the keyboard and shows where focus is. Its trigger took focus on Tab and then answered nothing: Enter and Space did not open it, Escape did not close it, and no focus: class on the trigger could light, so a host app's notification bell was a Tab stop a keyboard user could neither see nor use. The trigger now answers ActivateIntent (Enter, Space, the Android TV D-pad centre), as WAnchor does, only where a tap would toggle; Escape closes an open popover from the trigger or from inside the panel, returns focus to the trigger and consumes the key, so a popover in a dialog closes alone; and the popover publishes its trigger's hover, focus and disabled state through WindAnchorStateProvider, so a plain WDiv trigger answers focus: and its own styling wrapper stops being a second Tab stop. Escape is a key handler on the focus tree, above the portal. In a host app's web walk it first went deaf: the browser drops the trigger's focus as the panel mounts, so after a keyboard open Escape reached nothing and the next Tab started over from the top of the page. The first focus change after a keyboard open, when it lands on a scope rather than a control, now hands focus back to the trigger, once. A HardwareKeyboard handler was rejected for Escape: returning true from one does not stop the key reaching the focus tree, so a popover open in a dialog closed together with the dialog. The trigger and the panel are one FocusTraversalGroup in widget order, so Tab goes from the trigger into the panel: reading order had ranked a control beside the trigger before the panel below it, and a panel flipped above its trigger before the trigger itself, so the walk measured a phone shell that never reached the panel and a rail bell whose panel was reachable only with Shift+Tab. A DismissIntent action was rejected because ShortcutManager asks only the nearest action for an intent, so a disabled one would have swallowed Escape on a closed popover and a dialog holding it would have stopped closing. A disabled popover is no longer a Tab stop. (lib/src/widgets/w_popover.dart, test/widgets/w_popover/keyboard_test.dart, doc/widgets/w-popover.md, skills/wind-ui/SKILL.md)
  • Arbitrary tracking-[...] honours its unit and its sign. tracking-[0.1em] stripped the unit and set 0.1 logical pixels, and a negative value such as tracking-[-0.02em] did not match the pattern at all, so it was dropped. An em value is now em, a px value or a bare number is logical pixels (as in leading-[24] and text-[20]), and both take a negative sign. (lib/src/parser/parsers/text_parser.dart, test/parser/parsers/text_parser_test.dart, doc/typography/letter-spacing.md)