From 1a070628a249a26a015865b8f9856b92b378e37d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?An=C4=B1lcan=20=C3=87ak=C4=B1r?= Date: Tue, 22 Sep 2026 23:20:36 +0300 Subject: [PATCH 1/2] chore(deps): re-pin the fluttersdk stack to this batch's releases magic 0.0.16, magic_deeplink 0.1.3, magic_notifications 0.3.4, magic_social_auth 0.0.5, magic_starter 0.0.35, magic_devtools 0.0.6 and fluttersdk_telescope 0.0.7; dusk 0.0.15 and artisan 0.0.16 are still the newest. The backend's magic-starter-laravel pin moves ^0.0.9 to ^0.0.10: Composer's caret on 0.0.x pins the patch, so it never follows a release by itself. --- backend/composer.json | 2 +- backend/composer.lock | 12 ++++++------ pubspec.lock | 36 ++++++++++++++++++------------------ pubspec.yaml | 14 +++++++------- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/backend/composer.json b/backend/composer.json index 957c2af..0d9dc08 100644 --- a/backend/composer.json +++ b/backend/composer.json @@ -7,7 +7,7 @@ "license": "MIT", "require": { "php": "^8.3", - "fluttersdk/magic-starter-laravel": "^0.0.9", + "fluttersdk/magic-starter-laravel": "^0.0.10", "laravel/framework": "^13.8", "laravel/tinker": "^3.0" }, diff --git a/backend/composer.lock b/backend/composer.lock index 8acdc33..9fa9565 100644 --- a/backend/composer.lock +++ b/backend/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8ea890d082e4ce1f52f90ae064addf1f", + "content-hash": "2a4a3e49ac1ec4f0ba7b2c85a7d75984", "packages": [ { "name": "bacon/bacon-qr-code", @@ -680,16 +680,16 @@ }, { "name": "fluttersdk/magic-starter-laravel", - "version": "0.0.9", + "version": "0.0.10", "source": { "type": "git", "url": "https://github.com/fluttersdk/magic-starter-laravel.git", - "reference": "b4644a5e642cc37fb6ba1e5110280e56db6d3509" + "reference": "a67e895dabb225a735df29182ba24adfb678d98e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fluttersdk/magic-starter-laravel/zipball/b4644a5e642cc37fb6ba1e5110280e56db6d3509", - "reference": "b4644a5e642cc37fb6ba1e5110280e56db6d3509", + "url": "https://api.github.com/repos/fluttersdk/magic-starter-laravel/zipball/a67e895dabb225a735df29182ba24adfb678d98e", + "reference": "a67e895dabb225a735df29182ba24adfb678d98e", "shasum": "" }, "require": { @@ -758,7 +758,7 @@ "issues": "https://github.com/fluttersdk/magic-starter-laravel/issues", "source": "https://github.com/fluttersdk/magic-starter-laravel" }, - "time": "2026-09-21T16:36:49+00:00" + "time": "2026-09-21T22:43:33+00:00" }, { "name": "fruitcake/php-cors", diff --git a/pubspec.lock b/pubspec.lock index d751afc..5016b78 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -465,18 +465,18 @@ packages: dependency: "direct main" description: name: fluttersdk_telescope - sha256: "9f6d0f11c8d9c7f2eddb733f3248f5ce61f2778d50b0cc257d60875f50a24056" + sha256: "9635ab5f657e05e357423a09adb90eeb254db327bb83c1a471abba5011c2db92" url: "https://pub.dev" source: hosted - version: "0.0.6" + version: "0.0.7" fluttersdk_wind: dependency: transitive description: name: fluttersdk_wind - sha256: "21bb766b4361c7cdd64973cc389093f540d81bb0d98dda8ebd004458f1920d97" + sha256: "537330873b7e5e0d022c297f4175fe6f96496badb7349368b109e0b765b13abb" url: "https://pub.dev" source: hosted - version: "1.6.2" + version: "1.6.3" fluttersdk_wind_diagnostics_contracts: dependency: transitive description: @@ -761,58 +761,58 @@ packages: dependency: "direct main" description: name: magic - sha256: d3659e0bbf246094483ca92af5eabe14443fcc9a8c484b339601465aaa8bff0c + sha256: "8cbf350f6d4d2700b190ec0769b9da65dc1163cd8e90545bc2b7e902d3af6324" url: "https://pub.dev" source: hosted - version: "0.0.15" + version: "0.0.16" magic_deeplink: dependency: "direct main" description: name: magic_deeplink - sha256: "4aeaf5fbb18d629a0b2f809cb0fac48b374a7ccc751a386a95db765b680e748d" + sha256: "9b62bb8c96b82705ac5a3100c0f0072235fa018bb0d063b259c9a62468eec2c1" url: "https://pub.dev" source: hosted - version: "0.1.2" + version: "0.1.3" magic_devtools: dependency: "direct main" description: name: magic_devtools - sha256: "26a920f33301a83689ef010096e8bac23bcaa737625ea3506a39942a0248d707" + sha256: f84b37768920093f9e56f323e45603cfc752f790365bf16d36329a9fe2375418 url: "https://pub.dev" source: hosted - version: "0.0.5" + version: "0.0.6" magic_notifications: dependency: "direct main" description: name: magic_notifications - sha256: add62ae150d2e91e8fdd96ae23120b0f00dbecac34ef6175f4b02e6c2e76aa0d + sha256: "033b92b6239b9bc443c9ba7479e131ece03e9f1b23b2517da18fa89763d21abd" url: "https://pub.dev" source: hosted - version: "0.3.3" + version: "0.3.4" magic_payments: dependency: transitive description: name: magic_payments - sha256: "65473127ebcad27a25a3b283d37b8aa82d1c6cb872aae17540faf32070273b47" + sha256: "344712a777bc51fbbe687e47f332af2a8f1258af06df5ab2c1135fe1e0058a5f" url: "https://pub.dev" source: hosted - version: "0.0.3" + version: "0.0.4" magic_social_auth: dependency: "direct main" description: name: magic_social_auth - sha256: "11189045e12f220f7afc5817b039e7fb016e2d740d712a7f96eae3653ff7df60" + sha256: "57b225c444a034d0630410557d72924f0b3a16563c173a00d5510aa49ef58fb4" url: "https://pub.dev" source: hosted - version: "0.0.4" + version: "0.0.5" magic_starter: dependency: "direct main" description: name: magic_starter - sha256: "86afdb01e13fe3528ed3184d5054a361195c08c4db7d4aebe2c52aebdceb5c9c" + sha256: "5ead437a91dcb121b0e81bf736072186f761bbc8d60fdda197b0377e846c83a9" url: "https://pub.dev" source: hosted - version: "0.0.31" + version: "0.0.35" matcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index aa557fc..5e9da71 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,16 +35,16 @@ dependencies: # copied outside this workspace resolves without any sibling checkout. # In-workspace development overrides these to local path checkouts via the # gitignored pubspec_overrides.yaml; that file never ships in the fork. - magic: ^0.0.15 - magic_deeplink: ^0.1.2 - magic_notifications: ^0.3.3 - magic_social_auth: ^0.0.4 - magic_starter: ^0.0.31 + magic: ^0.0.16 + magic_deeplink: ^0.1.3 + magic_notifications: ^0.3.4 + magic_social_auth: ^0.0.5 + magic_starter: ^0.0.35 # Dev-tooling, imported by lib/main.dart under kDebugMode (release tree-shakes). - magic_devtools: ^0.0.5 + magic_devtools: ^0.0.6 fluttersdk_dusk: ^0.0.15 - fluttersdk_telescope: ^0.0.6 + fluttersdk_telescope: ^0.0.7 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. From 1441f21585420d5c08bdbe0dfafd7a0ff90d4f78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?An=C4=B1lcan=20=C3=87ak=C4=B1r?= Date: Tue, 22 Sep 2026 23:28:28 +0300 Subject: [PATCH 2/2] chore(skills): sync the mirrored skills from magic 0.0.16 and wind 1.6.3 The magic-framework copy was stamped magic 0.0.9, seven releases behind the pin this branch sets, and wind-ui trailed its 2.18.0 skill. Synced with bin/sync-skills from the tagged releases rather than the local sibling checkouts, which sit on unreleased branches. --- .github/skills/magic-framework/SKILL.md | 37 +++++++++++++++++++------ .github/skills/wind-ui/SKILL.md | 21 ++++++++------ 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/.github/skills/magic-framework/SKILL.md b/.github/skills/magic-framework/SKILL.md index 5f6e86a..dc31242 100644 --- a/.github/skills/magic-framework/SKILL.md +++ b/.github/skills/magic-framework/SKILL.md @@ -2,14 +2,14 @@ name: magic-framework description: "Write correct, idiomatic code in a Flutter app that depends on the `magic` framework (Laravel-inspired: IoC container, 18 facades, Eloquent-style ORM, service providers, reactive controllers, GoRouter routing, validation, auth, broadcasting). Use whenever code imports `package:magic/magic.dart` or `package:magic/testing.dart`, or the work touches Magic.init, MagicApp, a facade (Auth/Http/Cache/DB/Echo/Event/Gate/Config/Lang/Launch/Log/Pick/MagicRoute/Schema/Session/Storage/Vault/Crypt), a Model, MagicController, a MagicView, MagicFormData, FormRequest, a ServiceProvider, a migration, or the artisan make:* CLI. UI styling is Wind (separate wind-ui skill). Do NOT use for plain Flutter or Wind-only work with no magic import." when_to_use: "Use proactively when editing or scaffolding a magic app: Magic.init / a facade / a Model / a MagicController or MagicView / a form (MagicFormData, FormRequest, Validator) / a ServiceProvider / a route or MagicMiddleware / a migration / MagicStateMixin + RxStatus + fetchList / Session flash + old() + trans() / testing with MagicTest + Http.fake/Auth.fake / the artisan make:* CLI / the magic_deeplink, magic_notifications, magic_social_auth, magic_starter, magic_payments, or magic_devtools plugins. Trigger even when the user does not say the word 'magic'. Do NOT trigger for plain Flutter or Wind-only UI with no package:magic import." -version: 0.1.11 +version: 0.1.37 --- - + # Magic Framework @@ -34,7 +34,7 @@ Hard constraints for every line of magic code. 3. **Controllers are singletons.** `static X get instance => Magic.findOrPut(X.new);` is the canonical accessor. Views resolve controllers via `Magic.find()` (automatic in `MagicView`), never through constructors. 4. **IoC over `new` for services.** Bind in a provider's `register()`, resolve via the facade or `Magic.make('key')`. Do not scatter `Service()` construction across the app. 5. **Provider discipline.** `register()` is synchronous and is where routes and bindings go. `boot()` is async and may resolve other services; set `Auth.manager.setUserFactory(...)` here. -6. **Reactive state, not setState.** Controllers extend `MagicController` (a `ChangeNotifier`); state flows through `MagicStateMixin` + `RxStatus`. Use `refreshUI()` (guarded `notifyListeners`, and the single seam every controller notification goes through, including validation), `setLoading/setSuccess/setError/setEmpty`, and `MagicBuilder` for sections. `MagicController.onRefreshUI` is a null-by-default static debug tooling sets to observe those notifications. Local `setState` belongs only to genuine widget-local UI state inside a `MagicStatefulView`. +6. **Reactive state, not setState.** Controllers extend `MagicController` (a `ChangeNotifier`); state flows through `MagicStateMixin` + `RxStatus`. Use `refreshUI()` (guarded `notifyListeners`, and the single seam every controller notification goes through, including validation), `setLoading/setSuccess/setError/setEmpty`, `MagicBuilder` for a section backed by a `ValueListenable`, and `MagicSelector` for a section backed by a plain controller field (it caches its child, so it survives the parent's `setState` and is the tool for a field that changes on every keystroke). `MagicController.onRefreshUI` is a null-by-default static debug tooling sets to observe those notifications. Local `setState` belongs only to genuine widget-local UI state inside a `MagicStatefulView`. 7. **Typed attribute access.** Models use `get('key')` and `set('key', v)`, never raw `getAttribute`. Declare `fillable`; use `fill(validated, strict: true)` after validation so schema drift throws `MassAssignmentException`. 8. **Context-free navigation and feedback.** `MagicRoute.to/back/replace`, `Magic.snackbar/toast/dialog/confirm/loading`. Never depend on a `BuildContext` for navigation or feedback. Never navigate or fetch inside `build()`. 9. **Validate at the boundary.** `MagicFormData` for forms, `FormRequest` for complex payloads, `Validator` for ad hoc checks. Surface server errors with `handleApiError(response)` (from the `ValidatesRequests` mixin). @@ -118,7 +118,7 @@ The five assumptions a Laravel developer gets wrong most: (1) the container auto | `Crypt` | `encrypter` | `encrypt`, `decrypt`, `encryptWithDeviceKey`, `decryptWithDeviceKey`, `hasDeviceKey`, `generateDeviceKey`, `clearDeviceKey` | | `Launch` | `launch` | `url(u, {mode})`, `email`, `phone`, `sms`, `canLaunch` | -Global helper functions exist and are idiomatic: `env(key, [default])`, `trans(key, [replace])`, `old(field, [fallback])`, `error(field)`, `carbonNow()`, `carbonToday()`, `carbonParse(s)`. Full per-facade signatures: `${CLAUDE_SKILL_DIR}/references/facades-api.md`. +Global helper functions exist and are idiomatic: `env(key, [default])`, `trans(key, [replace])`, `transChoice(key, count, [replace])`, `old(field, [fallback])`, `error(field)`, `carbonNow()`, `carbonToday()`, `carbonParse(s)`. Full per-facade signatures: `${CLAUDE_SKILL_DIR}/references/facades-api.md`. ## 5. Canonical patterns @@ -258,7 +258,7 @@ MagicRoute.resource('users', UserRoutes()); // index/create/show MagicRoute.resource('posts', PostRoutes(), only: ['index', 'show']); ``` -`ResourceController` supplies `index()`, `create()`, `show(id)`, `edit(id)`; `resource()` wires `GET /name`, `/name/create`, `/name/:id`, `/name/:id/edit` with auto names `{name}.{method}`. Middleware extends `MagicMiddleware` (`handle(next)`, call `next()` to allow), registered with `Kernel.register('name', () => Mw())`. Read path/query params via `Request.route('id')` / `Request.query('q')`. +`ResourceController` supplies `index()`, `create()`, `show(id)`, `edit(id)`; `resource()` wires `GET /name`, `/name/create`, `/name/:id`, `/name/:id/edit` with auto names `{name}.{method}`. Middleware extends `MagicMiddleware` (`handle(next)`, call `next()` to allow), registered with `Kernel.register('name', () => Mw())` from a provider's `register()` or `boot()`, both of which run before the router pre-builds; an alias nothing registered throws a `StateError` out of `Magic.init` naming every offending route, rather than leaving the route ungated. Read path/query params via `Request.route('id')` / `Request.query('q')`. Session flash survives one navigation but `Session.tick()` is NOT automatic: wire it once at bootstrap on a router-delegate listener gated to actual location changes (see `${CLAUDE_SKILL_DIR}/references/routing-navigation.md`). @@ -312,7 +312,8 @@ Six facades fake without any mock library: `Http.fake` (`FakeNetworkDriver`: `as | `Http.get()` or `MagicRoute.to()` in `build()` | call in `onInit()` or callbacks | no I/O or navigation during build | | `user.fill(unvalidated)` | `user.fill(validated, strict: true)` | catches schema drift after validation | | hand-rolled `if (!Gate.allows(...)) throw` | `authorize('ability')` in the controller | delegates to Gate, throws `AuthorizationException` | -| `FilePicker.platform.pickFiles()` | `Pick.image()` / `Pick.file()` (or `FilePicker.pickFiles()`) | file_picker v11 is a static API | +| `FilePicker.platform.pickFiles()` or `result.files` | `Pick.image()` / `Pick.file()` (or `FilePicker.pickFile()`) | file_picker v12 is static and returns `PlatformFile?` / `List`, no `FilePickerResult` | +| `Pick.saveFile(...)` treated as a path | it returns `Uri?`; check `scheme == 'file'` before `toFilePath()` | Android SAF returns `content://`, web returns `blob:` | | four `MagicRoute.page()` for CRUD | `MagicRoute.resource(name, ctrl)` | auto-wires canonical routes + titles | | `import 'package:fluttersdk_magic/...'` | `import 'package:magic/magic.dart'` | the package is `magic` | | skipping reset in tests | `MagicTest.init()` (or `MagicApp.reset()` + `Magic.flush()` in `setUp`) | leaked state, false passes | @@ -367,6 +368,24 @@ Official plugins, each its own package + service provider + config. When a user | Subscriptions + billing (Stripe on web, store IAP on mobile) | `magic_payments` | `Payments` facade | `references/plugin-payments.md` | | E2E (dusk) + runtime inspection (telescope) + component previews | `magic_devtools` | `MagicDevtools`, `MagicPreview` | `references/plugin-devtools.md` | +### Installing a magic plugin into an existing app + +Same five steps for every plugin, in this order. Run them from the app root; `dart run magic:artisan ` delegates to the app's own `bin/dispatcher.dart` when there is one, which is what makes a plugin's commands reachable. + +1. `flutter pub add `. +2. `dart run magic:artisan plugin:install `. This registers the plugin's `ArtisanServiceProvider` in `.artisan/plugins.json` and regenerates `lib/app/_plugins.g.dart`, which is what makes the plugin's own commands dispatchable. Skip it and step 3 reports an unknown command. +3. `dart run magic:artisan :install` (`deeplink:install`, `notifications:install`, `starter:install`, `social:install`, ...). The manifest install: publishes the config file, injects the service provider into `lib/config/app.dart`, and adds the config factory to `lib/main.dart`. A plugin whose `install.yaml` declares a `bootstrap_command` (magic_starter does) has this chained for you by step 2; run it by hand when that subprocess reports a failure. +4. `dart run magic:artisan :doctor` where the plugin ships one: `notifications:doctor`, `starter:doctor`, and `deeplink:doctor` (magic_deeplink 0.1.0). It is the only step that tells you the install actually took; `dart run magic:artisan list` showing the plugin's commands is the fallback check. +5. Whatever the manifest cannot do, which the plugin's own installation guide names. This is where the real failures live: magic_deeplink needs the iOS associated-domains entitlement plus an Android `autoVerify` intent filter and a `flutter_deeplinking_enabled` `false` meta-data inside ``, and a plugin installed without them compiles and never fires. + +Provider ORDER in `lib/config/app.dart` is free for BINDINGS and load-bearing for everything else. Every `register()` runs before any `boot()` (`lib/src/foundation/application.dart:353`), so a plugin that resolves another plugin's binding in `boot()` finds it whichever order they sit in. But `boot()` itself is a sequential await over the list (`application.dart:378-381`), so anything a provider DOES in `boot()` is invisible to a provider that booted before it, and the failure is silent both ways: + +- A same-key overwrite, where the later-booting provider's `Gate.define()` or config value wins. +- A publish nobody is subscribed to yet. `magic_notifications` publishes a cold-start push tap from its `boot()`, and `magic_deeplink` subscribes in its own; with notifications first the tap went into a broadcast stream with no listener and was dropped, so the app opened on its initial route instead of the link's screen. Neither order errors, and artisan's installer appends each provider to the END of the list, so which one an app gets is decided by install order. +- `AppServiceProvider` before `AuthServiceProvider`, so `setUserFactory` lands before auth restore runs (see the top of this file). + +The plugins named above now buffer that tap, so that specific case is closed from `magic_notifications 0.3.0` and `magic_deeplink 0.1.0`. The shape is not: when a provider's `boot()` has to observe what another provider's `boot()` did, order it, do not assume it. + `magic_devtools` is a REGULAR dependency loaded under `kDebugMode` so it tree-shakes out of release builds. Two calls straddle the bootstrap: `MagicDevtools.installPre()` before `Magic.init()` (boots the dusk + telescope plugins and telescope's `ExceptionWatcher` + `DumpWatcher`), `MagicDevtools.installPost()` after it (wires `MagicTelescopeIntegration` + `MagicDuskIntegration`, which resolve through the container). Keep `kDebugMode` at the call site, never inside the methods, or the release tree-shake breaks. `dart run magic:artisan magic:install --with-devtools` wires all of it in one step. Use it to drive and inspect a running app when verifying your work. ## 12. Community: star and issue (optional, consent-first) @@ -385,9 +404,9 @@ Every path below is relative to this skill's own directory, `${CLAUDE_SKILL_DIR} | `references/bootstrap-lifecycle.md` | app bootstrap, IoC API, ServiceProvider, Env/Config, the Laravel mapping + divergences | | `references/facades-api.md` | any facade method signature or return type | | `references/eloquent-orm.md` | models, casts, relations, mass assignment, hybrid persistence, query builder, migrations | -| `references/controllers-views.md` | controllers, `MagicStateMixin`, `RxStatus`, views, `MagicBuilder`, `MagicCan` | +| `references/controllers-views.md` | controllers, `MagicStateMixin`, `RxStatus`, views, `MagicBuilder`, `MagicSelector`, `MagicCan` | | `references/forms-validation.md` | `MagicFormData`, `FormRequest`, `ValidatesRequests`, rules, async validation, `Session` flash | -| `references/routing-navigation.md` | routes, `resource()`, middleware, params, URL strategy, page titles, `Session.tick` wiring | +| `references/routing-navigation.md` | routes, `resource()`, middleware, params, stacking + back gestures, URL strategy, page titles, `Session.tick` wiring | | `references/http-network.md` | `Http`, `MagicResponse`, `MagicNetworkInterceptor`, `configureDriver`, network config, `MagicPaginator` (url + fetcher) + `MagicPage` + `MagicPaginatedListView` | | `references/auth-system.md` | `Auth`, guards, `Gate`, policies, `authorize()`, `Vault`, `Crypt` | | `references/secondary-systems.md` | `Cache`, `Event`, `Log`, `Lang`, `Storage`, `Launch`, `Pick`, `Carbon`, `Echo` | diff --git a/.github/skills/wind-ui/SKILL.md b/.github/skills/wind-ui/SKILL.md index 88c2dfe..c4581e9 100644 --- a/.github/skills/wind-ui/SKILL.md +++ b/.github/skills/wind-ui/SKILL.md @@ -1,17 +1,17 @@ --- name: wind-ui -description: "fluttersdk_wind 1.5: utility-first Flutter styling with Tailwind-syntax className strings. 27 W-prefix widgets (WDiv, WText, WButton, WInput, WSelect, WDatePicker, WPopover, WCard, WTabs, plus five WForm* wrappers) parse className into a cached immutable WindStyle; WindRecipe and WindSlotRecipe compose variant classNames. Prefixes stack freely (dark: / hover: / focus: / md: / ios: / selected: / disabled: / custom), the last class in a family wins, an unrecognized token drops with a one-time kDebugMode hint, and every color token carries a dark: peer in the same className. TRIGGER when: writing or editing UI in a Flutter app that depends on fluttersdk_wind; any className string; any W-prefix widget; any WindTheme or WindThemeData reference; the user mentions Tailwind for Flutter, utility-first, className, or wind-ui. DO NOT TRIGGER when: backend, API, or state-management work that never touches a widget tree; a Flutter project without fluttersdk_wind in pubspec.yaml; Material-only widgets (Scaffold, AppBar, Dialog) with no Wind content inside." +description: "fluttersdk_wind 1.6: utility-first Flutter styling with Tailwind-syntax className strings. 27 W-prefix widgets (WDiv, WText, WButton, WInput, WSelect, WDatePicker, WPopover, WCard, WTabs, plus five WForm* wrappers) parse className into a cached immutable WindStyle; WindRecipe and WindSlotRecipe compose variant classNames. Prefixes stack freely (dark: / hover: / focus: / md: / ios: / selected: / disabled: / custom), the last class in a family wins, an unrecognized token drops with a one-time kDebugMode hint, and every color token carries a dark: peer in the same className. TRIGGER when: writing or editing UI in a Flutter app that depends on fluttersdk_wind; any className string; any W-prefix widget; any WindTheme or WindThemeData reference; the user mentions Tailwind for Flutter, utility-first, className, or wind-ui. DO NOT TRIGGER when: backend, API, or state-management work that never touches a widget tree; a Flutter project without fluttersdk_wind in pubspec.yaml; Material-only widgets (Scaffold, AppBar, Dialog) with no Wind content inside." when_to_use: "Any task that produces, modifies, or audits Wind-styled UI: composing a className, picking the right W-widget, wiring a Form field, customizing WindThemeData, pairing dark-mode classes, debugging a layout or a RenderFlex overflow, building a popover, rendering a JSON tree via WDynamic, or composing a WindRecipe. Load it before the first line of new UI, and equally when auditing UI that already exists." -version: 2.13.0 +version: 2.18.0 --- - + -# Wind UI 1.5 +# Wind UI 1.6 Utility-first Flutter styling. Every visual decision lives in a `className: String?` parsed at build time into an immutable `WindStyle` and composed into a native Flutter widget tree. Tailwind syntax (`flex`, `p-4`, `dark:bg-gray-800`, `hover:shadow-lg`), Flutter physics. @@ -67,6 +67,8 @@ These hold for every line of Wind code. Apply each as a hard constraint, not a s 10. **`active:` prefix is reserved but not wired.** `WAnchor` tracks hover and focus only; there is no onTapDown/onTapUp tracking. Don't rely on `active:bg-blue-700` for press feedback. Use a transient state in the consumer's controller and `states: {'pressed'}` if you genuinely need press feedback today. +11. **An `onTap` on `WAnchor` is reachable by keyboard and remote, and a gestureless anchor is not a stop.** `onTap` runs on `ActivateIntent`, which covers `Enter`, `Space`, the gamepad A button and `select` (the D-pad centre on Android TV, the click on the Apple TV remote). Only `onTap` is bound, and the action map is installed only when an enabled `onTap` exists: a `CallbackAction` is always enabled and `ShortcutManager` reports a key handled for any enabled action, so an anchor carrying only `onLongPress` would otherwise swallow the activation key belonging to the row around it, and on web swallow `Space`'s scroll with it. Only an anchor carrying a gesture is a traversal stop: the gestureless `WAnchor` that `WDiv` wraps itself in for `hover:` / `focus:` / `active:` inherits from the nearest anchor above it rather than claiming a second stop, so `WAnchor(onTap:) > WDiv('focus:ring-2')` is one control the user tabs to once and the ring lands on the thing they activate. What it inherits is narrow: the ancestor's PRIMARY focus and its `disabled`, never the ancestor's focus-within, because a tappable card containing a text field reports focus-within while the user types and every wrapper under it would light up. `hover` is never inherited. Wind ships no traversal policy; directional movement is Flutter's default, and `FocusTraversalGroup` is the consumer's tool for region memory and edge behaviour. + ## 2. The 27 public widgets (+ WindRecipe) at a glance `fluttersdk_wind` v1 ships 27 public widgets plus the `WindRecipe` / `WindSlotRecipe` variant-composition primitives, all imported from the single barrel `package:fluttersdk_wind/fluttersdk_wind.dart`. No sub-barrels exist; do not write `import 'package:fluttersdk_wind/widgets.dart'`. @@ -173,11 +175,11 @@ Inline this catalog as your default reach-for set. For the full per-parser regex **Position**: `relative` `absolute`. `top-N` `right-N` `bottom-N` `left-N` `inset-N` `inset-x-N` `inset-y-N`, negative `-top-N` `-inset-N`, arbitrary `top-[24px]` (no `%` for offsets). `fixed` / `sticky` are recognised by the parser but produce no visual effect. -**Colors** (every line needs a `dark:` peer): `bg-{family}-{shade}` `bg-[#hex]` `bg-transparent` `bg-white` `bg-black`. Opacity modifier `/N` (0-100): `bg-red-500/50`. Same shape for `text-*` `border-*` `ring-*` `shadow-*` `fill-*` `stroke-*`. Bare shade defaults to 500: `bg-red` = `bg-red-500`. Gradients: `bg-gradient-to-{t|tr|r|br|b|bl|l|tl}` + `from-{c}-{shade}` `via-{c}-{shade}` `to-{c}-{shade}`. +**Colors** (every line needs a `dark:` peer): `bg-{family}-{shade}` `bg-[#hex]` `bg-transparent` `bg-white` `bg-black`. Arbitrary hex takes 3, 4, 6 or 8 digits and alpha LEADS in the 4- and 8-digit forms (Flutter packs `AARRGGBB` where CSS writes `RRGGBBAA`): 50%-alpha red is `bg-[#80ff0000]`. Opacity modifier `/N` (0-100): `bg-red-500/50`. Same shape for `text-*` `border-*` `ring-*` `shadow-*` `fill-*` `stroke-*`. Bare shade defaults to 500: `bg-red` = `bg-red-500`. Gradients: `bg-gradient-to-{t|tr|r|br|b|bl|l|tl}` + `from-{c}-{shade}` `via-{c}-{shade}` `to-{c}-{shade}`. **Borders**: `border` `border-N` `border-t` `border-r` `border-b` `border-l` `border-x` `border-y`. `border-solid` `border-none` (only these two; `border-dashed` / `border-dotted` are recognised but not wired). `rounded` `rounded-{sm|md|lg|xl|2xl|3xl|full|none}`, directional `rounded-t-lg` `rounded-tl-xl`, arbitrary `rounded-[8px]`. -**Typography** (order of resolution inside `text-*`): color → align → size → weight → style. `text-xs` `text-sm` `text-base` `text-lg` `text-xl` `text-2xl` through `text-6xl` (60 px). `text-7xl` / `text-8xl` / `text-9xl` are no-ops; do not write them. `font-thin` through `font-black`. `text-left` `text-center` `text-right` `text-justify` `text-start` `text-end` (RTL-aware). `truncate` (= `text-ellipsis` + `maxLines: 1` + `softWrap: false`). `line-clamp-N`. `whitespace-nowrap` / `text-nowrap`. `uppercase` `lowercase` `capitalize` `normal-case`. `italic` / `not-italic`. `underline` `line-through` `no-underline`, plus `decoration-{color}/{style}/{thickness}`. `leading-tight` `leading-snug` `leading-normal` `leading-relaxed` `leading-loose` or arbitrary `leading-[24px]`. `tracking-tighter` through `tracking-widest`. Font size + line height combined: `text-xl/8`. +**Typography** (order of resolution inside `text-*`): color → align → size → weight → style. `text-xs` `text-sm` `text-base` `text-lg` `text-xl` `text-2xl` through `text-6xl` (60 px). `text-7xl` / `text-8xl` / `text-9xl` are no-ops; do not write them. `font-thin` through `font-black`. `text-left` `text-center` `text-right` `text-justify` `text-start` `text-end` (RTL-aware). `truncate` (= `text-ellipsis` + `maxLines: 1` + `softWrap: false`). `line-clamp-N`. `whitespace-nowrap` / `text-nowrap`. `uppercase` `lowercase` `capitalize` `normal-case` (`capitalize` raises the first letter of every word and leaves the rest as typed, so `the HTTP client` is `The HTTP Client` and `well-known` is `Well-Known`, while a digit, an underscore, an apostrophe or a combining mark continues the word, so `3rd party`, `l'orange` and decomposed NFD text keep theirs; casing follows the ambient locale, so `tr`/`az` get the dotted/dotless `i` right, and it falls back to locale-independent casing with no `Localizations` ancestor). `italic` / `not-italic`. `underline` `line-through` `no-underline`, plus `decoration-{color}/{style}/{thickness}`. `leading-tight` `leading-snug` `leading-normal` `leading-relaxed` `leading-loose` or arbitrary `leading-[24px]`. `tracking-tighter` through `tracking-widest`. Font size + line height combined: `text-xl/8`. **Effects**: `opacity-N` (5-step scale, plus arbitrary `opacity-[0.5]`). `shadow-sm` `shadow` `shadow-md` `shadow-lg` `shadow-xl` `shadow-2xl` `shadow-inner` `shadow-none`. Colored shadow `shadow-blue-500/20`. `ring-N` `ring-{color}` `ring-offset-N` `ring-inset`. `aspect-square` `aspect-video` `aspect-[4/3]`. `z-0` `z-10` through `z-50`, arbitrary `z-[100]`, `z-auto`. @@ -235,7 +237,7 @@ Wind hides most boilerplate but never changes Flutter's "constraints down, sizes `items-stretch` inside a `SingleChildScrollView` needs an `IntrinsicHeight` wrapper from native Flutter; Wind has no token for it. Rare; reach for it when row children inside a scroll must match heights. -**Intrinsic sizing limitation.** Wrapping Wind content in `IntrinsicHeight` / `IntrinsicWidth` (or a `Row`/`Column` that needs child intrinsic heights for equal-height columns) throws `LayoutBuilder does not support returning intrinsic dimensions` WHEN that content triggers Wind's internal `LayoutBuilder` paths: `h-full` (only in an unbounded-height context) or a flex `basis-*` (a single `LayoutBuilder` around the surrounding flex). `LayoutBuilder` cannot answer intrinsic queries (a Flutter constraint, not a Wind bug). Escape hatches: use explicit `h-*` / `size-*` instead of `h-full`; do not wrap such content in `IntrinsicHeight`; for equal-height rows use a `Stack` + `Positioned(top:0,bottom:0)`. Wind's own `items-stretch` column equalizes cross-axis size without you adding `IntrinsicHeight` (it uses its own `LayoutBuilder` internally, so reach for it INSTEAD of `IntrinsicHeight`, not nested inside one). +**Intrinsic sizing limitation.** Wrapping Wind content in `IntrinsicHeight` / `IntrinsicWidth` (or a `Row`/`Column` that needs child intrinsic heights for equal-height columns) throws `LayoutBuilder does not support returning intrinsic dimensions` WHEN that content contains a `grid`, which composes a `Wrap` inside a `LayoutBuilder` to compute its column width. `LayoutBuilder` cannot answer intrinsic queries (a Flutter constraint, not a Wind bug). `h-full` and flex `basis-*` are NO LONGER triggers: both resolve through render objects (`WindFullHeightBox`, `WindMainExtentProvider`), and a render object answers intrinsics. Escape hatches for the `grid` case: explicit `h-*` / `size-*` cells, or a `Stack` + `Positioned(top:0,bottom:0)`; Wind's own `items-stretch` grid equalizes row heights with real layout, so reach for it INSTEAD of `IntrinsicHeight`. ## 7. className formatting @@ -409,7 +411,10 @@ Compact catalog of consistent footguns. Each entry: what's wrong, why, the corre | `WText` with `truncate` inside Row without bounded width | Overflow | wrap in `WDiv(className: 'flex-1')` | | Putting `dark:` peers at the bottom of a long className | Hard to audit; missing pairs slip through | group beside the light variant on the same line | | `active:bg-blue-700` for press feedback | Not wired (Core Law §10); WAnchor tracks hover and focus only | track press in consumer state, pass via `states: {'pressed'}` if needed | +| Adding a `Focus` or `Shortcuts` wrapper to make a `WAnchor` keyboard-reachable | Already wired (Core Law §11); `onTap` answers `ActivateIntent` | pass `onTap` and let `Enter` / `Space` / D-pad `select` reach it | +| Expecting `focus:ring-*` on a `WDiv` inside a tappable `WAnchor` to need its own focus node | The gestureless wrapper inherits focus from the anchor (Core Law §11) | style the div, put the gesture on the anchor, leave the nodes alone | | Inline `Padding(padding: EdgeInsets.all(16))` around a `WDiv` | Duplicates work | move the padding into the `WDiv` className as `p-4` | +| Asserting `uppercase` output for Turkish in a bare `pumpWidget` | Casing reads the ambient locale, and with no `Localizations` ancestor it falls back to Dart's locale-independent rules, so the assertion measures the fallback | wrap the subtree in `Localizations(locale: Locale('tr'), delegates: [DefaultWidgetsLocalizations.delegate], ...)` | ## 13. Quick install