Skip to content

1.4.0

Choose a tag to compare

@github-actions github-actions released this 21 Aug 11:55
· 11 commits to master since this release
af36b8c

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/)