Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ All notable changes to this project will be documented in this file.
- [ ] `example/` updated when the change touches the canonical consumer scaffold
- [ ] `flutter test` green; `dart analyze` clean; `dart format` no diff; `dart pub publish --dry-run` no blocking errors

## [0.0.4] - 2026-07-08

### Added

- **`design:sync` + `design:lint` commands make DESIGN.md the single source of truth for the app theme.** Two new commands join `MagicArtisanProvider`. `design:sync` parses a `DESIGN.md` (YAML front matter: color roles with a single-file `dark:` overlay, typography, `rounded`, `spacing`, and `components` carrying `{colors.x}` / `{rounded.x}` / `{spacing.x}` references; the markdown body is ignored), resolves the references against a dotted-path symbol table with a cycle guard, and emits a wind theme source file (`--output`, default `lib/config/wind_theme.g.dart`). The generated file exposes `Map<String, String> designAliases` carrying the 17 property-prefixed semantic keys (`bg-surface`, `text-fg`, `border-color-border`, ...) with arbitrary-hex light + `dark:` pairs (`'bg-surface': 'bg-[#f9f9ff] dark:bg-[#0f1419]'`), drop-in for `WindThemeData(aliases: ...)` and matching the `MagicStarterTokens.defaultAliases` contract, plus a brand `primary` `MaterialColor` with a generated 50-900 ramp (seeded from the DESIGN.md `primary` light hex) for `WindThemeData.toThemeData()` Material interop. It writes atomically via `.tmp` + rename and is idempotent (byte-identical output on re-run for an unchanged DESIGN.md). `design:lint` validates a DESIGN.md against six rules ported from the open design.md reference linter and adapted to the wind-flavored superset: broken-ref (error), missing-primary (warning), unknown-key (warning; the `dark:` overlay lives inside `colors` and is structurally never a top-level key, so it is never flagged), section-order (warning), missing-sections (info), orphaned-tokens (warning, with Material Design 3 baseline families exempt), and contrast-ratio (warning; a greenfield WCAG relative-luminance helper does sRGB channel linearization + the 4.5:1 ratio check on each component `backgroundColor` / `textColor` pair). The Tailwind/DTCG export-conformance and rem-based spacing/rounded rules from the reference linter are intentionally dropped (wind uses 4px logical spacing and arbitrary-hex aliases). The command exits nonzero only on an error-severity finding. Touches `lib/src/cli/commands/design_sync_command.dart`, `lib/src/cli/commands/design_lint_command.dart`, `lib/src/cli/helpers/design_md_parser.dart`, `lib/src/cli/magic_artisan_provider.dart`; adds `test/cli/commands/{design_sync,design_lint}_command_test.dart` + `test/cli/helpers/design_md_parser_test.dart`; documented in `doc/packages/magic-cli.md` (including the DESIGN.md format page).
- **`previews:refresh` + `make:component` codegen commands for the design-first preview catalog.** Two new commands join `MagicArtisanProvider`. `previews:refresh` scans a configurable target directory (`--path`, default `lib`) for `*.preview.dart` files, extracts the single public `*Preview` class from each via regex (the private `_*State` companion of a stateful preview is ignored), validates the class name is a clean PascalCase identifier before interpolation, fails fast on a slug collision, sorts deterministically, and renders `<scan-dir>/_previews.g.dart` through an atomic `.tmp` + rename. The generated file returns a freshly-built `List<PreviewEntry>` from the `previewEntries()` FUNCTION (never a top-level const list) so the dev-only catalog tree-shakes from release builds (dart-lang/sdk#33920); it imports `PreviewEntry` from `package:magic_devtools/preview.dart` and each preview widget by its path relative to the generated file (preview files are not exported from any barrel). Re-running the command produces a byte-identical file. `make:component <Name> [--variants=intent,size] [--slots]` extends `ArtisanGeneratorCommand` and scaffolds the canonical 4-file atomic component folder under `lib/ui/components/<name>/` (`<name>.dart`, `<name>.recipe.dart`, `<name>.preview.dart`, `index.dart`): the class is unprefixed PascalCase, the recipe is seeded with the requested variant axes (or a `WindSlotRecipe` shape under `--slots`), the index re-exports the component + recipe but not the preview, then the command chains `previews:refresh` so the new preview lands in `_previews.g.dart`. Touches `lib/src/cli/commands/previews_refresh_command.dart`, `lib/src/cli/commands/make_component_command.dart`, `lib/src/cli/helpers/previews_index_writer.dart`, `lib/src/cli/helpers/magic_stub_loader.dart` (adds `loadFrom`), `lib/src/cli/magic_artisan_provider.dart`, and six stubs under `assets/stubs/`; adds `test/cli/commands/{previews_refresh,make_component}_command_test.dart`.
- **`magic:install --with-devtools` wires the debug trio in one step.** Installing the optional debug tooling (`magic_devtools` + `fluttersdk_dusk` + `fluttersdk_telescope`) previously meant a manual multi-step bootstrap: add three deps, run `plugin:install` twice, then `dusk:install` + `telescope:install`. The new `--with-devtools` flag does all of it after the core install: it adds the three packages to `dependencies` (regular, not `dev_dependencies`, because `lib/main.dart` imports them and the `kDebugMode` gate tree-shakes the subsystem from release builds, so `dev_dependencies` would trip `depend_on_referenced_packages`) and wires `lib/main.dart` under `kDebugMode` exactly as `dusk:install` / `telescope:install` do: `DuskPlugin.install()` and `TelescopePlugin.install()` (plus `ExceptionWatcher` + `DumpWatcher`) before `Magic.init()`, then `MagicDuskIntegration.install()` and `MagicTelescopeIntegration.install()` after it. The wiring is a pure-functional, idempotent transform (`buildDevtoolsWiring`) over the generated main.dart, and the dep-add rides the same `installer.addDependency` mechanism the install already uses, so re-running `magic:install --with-devtools` never duplicates a wiring block or a dependency entry. The injected package imports are placed within the existing package-import group (before the relative `config/...` imports, with `package:flutter/foundation.dart` ordered before `package:flutter/material.dart`), so the generated main.dart stays `directives_ordering`-clean and a freshly installed app emits no analyzer warnings. Absent the flag, nothing changes for the existing install path. Touches `lib/src/cli/commands/magic_install_command.dart`; adds the `MagicInstallCommand.buildDevtoolsWiring` test group plus a real-FS full-install group to `test/cli/commands/magic_install_command_test.dart`.
- **`MagicMiddleware.redirectTarget(String location)` for pre-build redirect guards.** Redirect-style guards (auth / guest) can now return a redirect target synchronously, evaluated inside the router's `redirect` callback BEFORE any page builds. Previously the only way to redirect was an imperative `MagicRoute.to()` inside `handle()`, which runs post-mount and remounts the destination view, recreating its form state on every mount (the login-double-mount bug). `_handleRedirect` now evaluates every matched route's global + route middleware `redirectTarget` and returns the first non-null target. The default returns `null`, and `handle()` now defaults to `next()`, so a redirect-only guard overrides just `redirectTarget`. Fully backward compatible: existing `handle()`-based guards keep working. Touches `lib/src/http/middleware/magic_middleware.dart`, `lib/src/routing/magic_router.dart`; adds `test/routing/redirect_guard_mount_test.dart` (asserts the destination mounts exactly once, including through a layout ShellRoute).

Expand All @@ -28,7 +32,7 @@ All notable changes to this project will be documented in this file.

### Changed

- **`fluttersdk_wind` constraint bumped to `^1.1.2`.** Picks up wind 1.1.0's Material-free `WInput`/`WText` rewrite, 1.1.1's two fixes that magic's W-widget UI depends on (`WInput` native text selection restored (mouse drag-select, double-tap word, long-press), and `WText` now inherits an ancestor `DefaultTextStyle` color (the CSS text-color cascade) before falling back to the OS-brightness baseline; the latter fixes invisible labels on magic's W-rendered surfaces (`Magic*View`, `MagicFeedback`, dialog buttons whose color lives on the container) when the app theme disagrees with the OS theme), and 1.1.2's `WPopover` fix: a popover with an interactive trigger (a `WButton`/`WAnchor` with its own `onTap`) now opens reliably and no longer dismisses itself on the opening gesture, with the trigger kept accessible via a `Semantics` tap action. This is the primitive behind magic_starter's team selector and user/notification dropdowns. Touches `pubspec.yaml`.
- **`fluttersdk_wind` constraint bumped to `^1.2.0`.** Requires wind 1.2.0's `WindRecipe`/`WindSlotRecipe` (the `tv()`-equivalent recipe API, NEW in 1.2.0 and re-exported by `package:magic/magic.dart` — the design-first component layer and `make:component` scaffolds depend on it), plus the intrinsic-safe flex, the seeded `primary` token, the min-width-stretch scroll, and the `h-full`-inside-vertical-scroll dev assert. Also picks up wind 1.1.0's Material-free `WInput`/`WText` rewrite, 1.1.1's two fixes that magic's W-widget UI depends on (`WInput` native text selection restored (mouse drag-select, double-tap word, long-press), and `WText` now inherits an ancestor `DefaultTextStyle` color (the CSS text-color cascade) before falling back to the OS-brightness baseline; the latter fixes invisible labels on magic's W-rendered surfaces (`Magic*View`, `MagicFeedback`, dialog buttons whose color lives on the container) when the app theme disagrees with the OS theme), and 1.1.2's `WPopover` fix: a popover with an interactive trigger (a `WButton`/`WAnchor` with its own `onTap`) now opens reliably and no longer dismisses itself on the opening gesture, with the trigger kept accessible via a `Semantics` tap action. This is the primitive behind magic_starter's team selector and user/notification dropdowns. Touches `pubspec.yaml`.
- **Debug-tooling install guidance corrected to regular `dependencies`.** The `magic:install` post-install message recommended adding `magic_devtools` / `fluttersdk_dusk` / `fluttersdk_telescope` to `dev_dependencies`, but the install commands wire them into `lib/main.dart` (under `kDebugMode`), which trips the `depend_on_referenced_packages` lint. They are now documented as regular `dependencies` (tree-shaken from release via `kDebugMode`), matching dusk/telescope's own install docs. Also bumps the message's stale `fluttersdk_dusk ^0.0.7` to `^0.0.8`. Touches `install.yaml`.

## [0.0.3] - 2026-06-17
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ The same Facades, the same Eloquent syntax, the same Service Provider lifecycle
| 🎨 | **Wind UI** | Built-in [Wind](https://wind.fluttersdk.com) Tailwind-syntax styling with `className` strings. |
| 📡 | **Broadcasting** | Laravel Echo equivalent: real-time WebSocket channels via the `Echo` facade with presence support and `Echo.fake()`. |
| 🧪 | **Testing** | First-class fakes: `Http.fake()`, `Auth.fake()`, `Cache.fake()`, `Vault.fake()`, `Log.fake()`, `Echo.fake()`. No mockito needed. |
| 🧰 | **Magic CLI** | Artisan-style scaffolding via `dart run magic:artisan make:model`, `make:controller`, and 14 generators. |
| 🧰 | **Magic CLI** | Artisan-style scaffolding via `dart run magic:artisan make:model`, `make:controller`, 15 generators, plus `make:component` for design-first component workflows and `design:sync` / `design:lint` to drive the Wind theme from a `DESIGN.md`. |

## A taste of Magic

Expand Down
15 changes: 15 additions & 0 deletions assets/stubs/component.recipe.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import 'package:magic/magic.dart';

/// Builds the [WindRecipe] for the {{ className }} component.
///
/// Each variant axis maps a value to a className built from semantic tokens
/// (`bg-surface`, `text-fg`, `border-color-border`, ...). Fill in the per-value
/// classNames; the emission order is `base ++ variant ++ compound ++ caller`
/// (never sorted or deduped) so state-prefixed tokens survive to parse time.
WindRecipe {{ camelName }}Recipe() {
return WindRecipe(
base: 'flex flex-col',
variants: {
{{ variantAxes }} },
{{ defaultVariants }} );
}
17 changes: 17 additions & 0 deletions assets/stubs/component.slot_recipe.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import 'package:magic/magic.dart';

/// Builds the [WindSlotRecipe] for the {{ className }} component.
///
/// Each slot maps to its own className, built from semantic tokens
/// (`bg-surface`, `text-fg`, `border-color-border`, ...). Variant axes carry a
/// per-slot className map; the emission order per slot is
/// `base ++ variant ++ compound ++ caller` (never sorted or deduped).
WindSlotRecipe {{ camelName }}Recipe() {
return WindSlotRecipe(
slots: {
'root': 'flex flex-col',
},
variants: {
{{ slotVariantAxes }} },
{{ defaultVariants }} );
}
30 changes: 30 additions & 0 deletions assets/stubs/component.slots.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import 'package:flutter/widgets.dart';
import 'package:magic/magic.dart';

import '{{ snakeName }}.recipe.dart';

/// {{ className }} component (slot-based).
///
/// Slot styling lives in `{{ snakeName }}.recipe.dart` (a [WindSlotRecipe]
/// reading semantic tokens); this widget resolves the recipe to a per-slot
/// className map and threads each slot into its [WDiv].
@immutable
class {{ className }} extends StatelessWidget {
/// The main content placed in the `root` slot.
final Widget child;

/// Creates a [{{ className }}].
const {{ className }}({
super.key,
required this.child,
});

@override
Widget build(BuildContext context) {
final slots = {{ camelName }}Recipe()();
return WDiv(
className: slots['root'],
child: child,
);
}
}
33 changes: 33 additions & 0 deletions assets/stubs/component.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'package:flutter/widgets.dart';
import 'package:magic/magic.dart';

import '{{ snakeName }}.recipe.dart';

/// {{ className }} component.
///
/// Variant logic lives in `{{ snakeName }}.recipe.dart` (a [WindRecipe] reading
/// semantic tokens); this widget threads the resolved className into a [WDiv].
/// Pass [className] to override the recipe output entirely (escape hatch).
@immutable
class {{ className }} extends StatelessWidget {
/// The main content.
final Widget child;

/// Optional className that overrides the recipe output entirely.
final String? className;

/// Creates a [{{ className }}].
const {{ className }}({
super.key,
required this.child,
this.className,
});

@override
Widget build(BuildContext context) {
return WDiv(
className: className ?? {{ camelName }}Recipe()(),
child: child,
);
}
}
8 changes: 8 additions & 0 deletions assets/stubs/component_index.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// {{ className }} component folder-local barrel.
//
// Re-exports the public surface (component + recipe). The preview is
// intentionally NOT re-exported: `previews:refresh` discovers `*.preview.dart`
// files directly and the preview must stay out of the release barrel.

export '{{ snakeName }}.dart';
export '{{ snakeName }}.recipe.dart';
1 change: 1 addition & 0 deletions assets/stubs/install/app_config.stub
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
Map<String, dynamic> get appConfig => {
'app': {
'name': env('APP_NAME', 'My App'),
'title_separator': ' - ',
'env': env('APP_ENV', 'production'),
'debug': env('APP_DEBUG', false),
'key': env('APP_KEY'),
Expand Down
2 changes: 1 addition & 1 deletion assets/stubs/install/main.stub
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ void main() async {
);

runApp(
MagicApplication(title: '{{ appName }}'),
MagicApplication(title: '{{ appName }}', titleSuffix: '{{ appName }}'),
);
}
24 changes: 24 additions & 0 deletions assets/stubs/preview.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'package:flutter/widgets.dart';
import 'package:magic/magic.dart';

import '{{ snakeName }}.dart';

/// Static variant-matrix preview for [{{ className }}].
///
/// Renders the component so the dev-only `/preview` catalog can show it in both
/// light and dark. One public preview class per file is the discovery contract
/// `previews:refresh` enforces.
class {{ className }}Preview extends StatelessWidget {
/// Creates the {{ className }} preview.
const {{ className }}Preview({super.key});

@override
Widget build(BuildContext context) {
return const WDiv(
className: 'flex flex-col gap-6 p-6',
child: {{ className }}(
child: WText('{{ className }}', className: 'text-sm text-fg'),
),
);
}
}
Loading
Loading