Skip to content
Open
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
39 changes: 27 additions & 12 deletions .claude/agents/component-visual-reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,30 @@ You receive:

- `screenshot_light`: path to a JPEG/PNG screenshot of the component in light mode
- `screenshot_dark`: path to a JPEG/PNG screenshot of the component in dark mode
- `design_md`: path to the DESIGN.md file (default: `magic_example/DESIGN.md`)
- `design_md`: path to the DESIGN.md file (default: `DESIGN.md`, repo-relative like every other path in this file)
- `component`: name of the component or screen being reviewed

---

## PROCESS

### 1. Read DESIGN.md
### 1. Load the design system from disk, before looking at anything

Read the `design_md` file to load:
- The `colors` section: light/dark hex values for every semantic token role.
- The `typography` section: font family, sizes, weights, line-heights.
- The `rounded` section: corner radius values.
- The `spacing` section: scale values.
- The `components` section: token bindings for specific components.
**Every expected value comes from a file you read in this step, never from memory and never from an
example in this document.** Where a constant quoted here disagrees with a file you read, THE FILE
WINS and the constant is stale: say so in your output, because it means this reviewer needs updating.

Read, all of them, from the repository root:

| File | What it gives you |
|---|---|
| `DESIGN.md` (the `design_md` argument) | the `colors`, `typography`, `rounded`, and `spacing` sections; the light/dark hex per role, type scale, radii, spacing. Then the body, which carries the component conventions and the deliberate exceptions |
| `lib/config/wind_theme.g.dart` | what `design:sync` actually emitted. `DESIGN.md` may declare a token this table does not carry, and a declared-but-unemitted token silently does nothing |
| `lib/config/wind_theme.dart` | the `supplementAliases` map, the hand-authored token families `design:sync` does not generate (see DESIGN.md's "Custom token families" section) |
| `.claude/rules/design.md` | the 17-token alias table and the anti-pattern table. Every row is a measured defect that already shipped here, and it is the highest-value part of your checklist |

A hex you cannot find in `wind_theme.g.dart` is probably legitimate and probably in the supplement.
A hex you cannot find in either file is a violation.

### 2. Read the screenshots

Expand All @@ -47,19 +56,25 @@ Read both screenshots visually. Identify:

### 3. Check the component source (optional but preferred)

If the component source is accessible, read it to confirm token usage:
If the component source is accessible, read it to confirm token usage. Paths are relative to the
repository root, which is the magic_example project itself:

```bash
find /Users/anilcan/Code/fluttersdk/lib/ui/components -name "*.dart" | xargs grep -l "<ComponentName>"
find lib/ui/components -name "*.dart" | xargs grep -l "<ComponentName>"
```

Look for raw `Color(0xFF...)`, `Colors.*`, or hardcoded pixel margins that indicate a token bypass.

```bash
grep -rn "Color(0x\|Colors\." /Users/anilcan/Code/fluttersdk/lib/ui/components/<name>/
grep -rn "SizedBox(height: [0-9]\|SizedBox(width: [0-9]" /Users/anilcan/Code/fluttersdk/lib/ui/components/<name>/
grep -rn "Color(0x\|Colors\." lib/ui/components/<name>/
grep -rn "SizedBox(height: [0-9]\|SizedBox(width: [0-9]" lib/ui/components/<name>/
```

An earlier version of this file hardcoded an absolute path one segment short of the project (missing
the `magic_example/` segment). That directory did not exist, so the grep matched nothing and every
review silently passed this step. If a command here returns nothing, confirm the path resolves before
concluding the component is clean.

---

## SCORING DIMENSIONS
Expand Down
4 changes: 2 additions & 2 deletions .claude/rules/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ These rules apply whenever you touch any file under `lib/`. They complement `CLA

## Atomic Component Folder Contract

Every component in the `magic_starter` generic library lives in a 4-file atomic folder:
Every component in this app's own `lib/ui/components/` library lives in a 4-file atomic folder:

```
lib/ui/components/<name>/
Expand Down Expand Up @@ -52,7 +52,7 @@ final myRecipe = WindRecipe(
- Emission order is always: `base ++ variant (definition order) ++ compound ++ caller`. Never sort or deduplicate.
- Pass variant values as strings matching the map keys. Pass `null` to clear a default.
- The caller `className` argument appends last; it can override variant output at the same granularity.
- Import `WindRecipe` via `package:magic/magic.dart` inside `magic_starter` files (it re-exports the wind barrel). Direct `package:fluttersdk_wind/...` imports trip `depend_on_referenced_packages`.
- Import `WindRecipe` via `package:magic/magic.dart` inside this app's files (it re-exports the wind barrel). Direct `package:fluttersdk_wind/...` imports trip `depend_on_referenced_packages`.

## Token-Only Rule

Expand Down
36 changes: 36 additions & 0 deletions .claude/rules/flutter-app.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
paths:
- "lib/**"
- "test/**"
---

# The Flutter app

Applies to `lib/` and `test/`. Colours, the component folder contract and the anti-pattern table live in `.claude/rules/design.md`, which loads alongside this file.

## The two skills are the standard, and this file is only where we differ

`magic-framework` and `wind-ui` define how code on this stack is written, and copies of both sit at `.github/skills/` so a reviewer with only this checkout has them too. Load them before the first line of Dart rather than working from memory. This file does not restate them; it carries what this app does differently, and what has not been built yet.

## One controller exists, and it is the one to copy

`lib/app/controllers/dashboard_controller.dart` paired with `lib/resources/views/dashboard_view.dart` is the single worked instance of the framework's controller and view pattern in this repo. Read it before adding a second; its docblock carries the reasoning, not just the shape. `lib/resources/views/welcome_view.dart` is still a plain `StatelessWidget` reading `Config.get('app.name', ...)` directly, which is fine for a screen with no state and no identity in it.

Follow the skill's definition rather than inventing a shape here:

- A controller is a `MagicController` resolved through a canonical `static X get instance => Magic.findOrPut(X.new);`, notifying through `refreshUI()` rather than calling `notifyListeners()` directly.
- A view pairs with it as `MagicStatefulView<XController>` / `MagicStatefulViewState`. Do not pass a controller through a view's constructor; nothing then resets it between logins or tests.
- A controller holding anything that belongs to the current identity implements `SessionScopedController`. `SessionScopeSync.attach()` (`lib/app/providers/app_service_provider.dart:81`) resets every registered one on login and team switch; `onInit` alone cannot cover this, because it runs once per controller lifetime rather than once per session. Skip it and a team switch leaves the previous tenant's data on screen until the app restarts.
- No app shell under `lib/ui/layouts/`. `lib/routes/app.dart:16` already mounts `magic_starter`'s `layout.app` through `MagicRoute.group(layout: ...)`; a second shell competes with it and decays.

## Routes register in `boot()`, not `register()`

`RouteServiceProvider.boot()` (`lib/app/providers/route_service_provider.dart`) calls `registerAppRoutes()` and the starter route registrars, then registers the dev-only preview catalog, all inside `boot()`. That is deliberate here: the preview registration must land before `MagicRouter` locks its route table on first build, and `boot()` is the phase both dev tooling and the app routes share. Do not move route registration into `register()` on the assumption that is the framework default; it is not what this repo does, and the comment at that call site explains why.

## Config-plus-factory is the wiring shape for a new subsystem

A subsystem gets its own `lib/config/<name>.dart` exposing a single `Map<String, dynamic> get <name>Config => {...}` getter, then a `() => <name>Config` entry added to the `configFactories` list in `lib/main.dart`. `lib/config/localization.dart` and `lib/config/notifications.dart` are the two current instances (wired at `lib/main.dart:39-41`); read either before adding a third. Every value goes through `env()` with an explicit fallback rather than requiring a `.env` entry, and each non-obvious default carries a comment saying why that default and not another (see `notifications.dart`'s push section). This is the only place a subsystem is configured; do not scatter its options across the provider that consumes it.

## Generated files, never hand-edited

`lib/config/wind_theme.g.dart` (`design:sync`), `lib/_previews.g.dart` (`previews:refresh`), `lib/app/_plugins.g.dart` and `lib/app/commands/_index.g.dart` (`commands:refresh`). Regenerate through the dispatcher command named in parentheses; a hand edit is overwritten on the next run and diverges from `analysis_options.yaml`'s strict-mode expectations in the meantime.
8 changes: 6 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ That override file is also why a green local run can be a red CI: with it, this

## Stack

- Flutter >=3.27.0, Dart >=3.6.0.
- Dart `sdk: ^3.12.2` (`pubspec.yaml:22`), with no separate Flutter version floor declared.
- `magic` (framework: IoC container, ORM, auth, routing over `go_router`), `magic_starter` (auth, profile, teams, notifications, 13 opt-in features), `fluttersdk_wind` (utility-first styling through `className`), `magic_devtools` (dev-only preview catalog and dusk integration).
- A Laravel backend under `backend/` as the API counterpart.

Expand Down Expand Up @@ -50,7 +50,7 @@ A green suite is the floor, not the finish line. Anything a person clicks gets d

## Off-limits

- Generated files are regenerated, never edited: `docs/component-registry.md` (`bin/sync-registry`), `.github/skills/{magic-framework,wind-ui}/SKILL.md` (`bin/sync-skills`, each carrying the hash CI checks it against), `lib/config/wind_theme.g.dart` (`design:sync`), `lib/preview/_previews.g.dart` (`previews:refresh`), `lib/app/commands/_index.g.dart` (`commands:refresh`), `.artisan/plugins.json`, and everything `bin/sync-instructions` writes under `.github/`.
- Generated files are regenerated, never edited: `docs/component-registry.md` (`bin/sync-registry`), `.github/skills/{magic-framework,wind-ui}/SKILL.md` (`bin/sync-skills`, each carrying the hash CI checks it against), `lib/config/wind_theme.g.dart` (`design:sync`), `lib/_previews.g.dart` (`previews:refresh`), `lib/app/commands/_index.g.dart` (`commands:refresh`), `.artisan/plugins.json`, and everything `bin/sync-instructions` writes under `.github/`.
- `backend/vendor/`, `build/`, `.dart_tool/`.
- The fluttersdk packages are separate repositories. Reading them is expected; changing one is a PR in that repo under its own rules. `design:sync`, `design:lint`, `make:component`, and `previews:refresh` are `magic`'s commands, not this project's, and there is no `magic_example:artisan`.

Expand All @@ -64,6 +64,10 @@ App components live in `lib/ui/components/<name>/` as a four-file atomic folder,

Regeneration commands, all through the dispatcher: `dart run bin/dispatcher.dart design:sync`, `design:lint`, `previews:refresh`, `make:component <Name> [--variants=intent,size] [--slots]`.

## Mirroring the boilerplate

This repo is the fork source for real products in the ecosystem. `uptizm/AGENTS.md` carries the sending half of this policy and names `../magic_example` as the boilerplate it was forked from. Not every fork carries it yet (depools does not), so a fork that has sent nothing back is a gap to close rather than an exemption. A structural change proven in one of those products (a rule, a skill, the component contract, tooling like `bin/check`) comes back here as its own PR, in the same piece of work that proved it out. Product code (a fork's domain models, screens, billing wiring) does not travel; only the pattern does. When a PR against this repo cites a fork as the reason for a change, that is the mechanism working as intended, not scope creep to push back on.

## Where the instructions live

This file is canonical. Everything else either points at it or is generated from it:
Expand Down
4 changes: 2 additions & 2 deletions .github/instructions/design.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ These rules apply whenever you touch any file under `lib/`. They complement `.gi

## Atomic Component Folder Contract

Every component in the `magic_starter` generic library lives in a 4-file atomic folder:
Every component in this app's own `lib/ui/components/` library lives in a 4-file atomic folder:

```
lib/ui/components/<name>/
Expand Down Expand Up @@ -53,7 +53,7 @@ final myRecipe = WindRecipe(
- Emission order is always: `base ++ variant (definition order) ++ compound ++ caller`. Never sort or deduplicate.
- Pass variant values as strings matching the map keys. Pass `null` to clear a default.
- The caller `className` argument appends last; it can override variant output at the same granularity.
- Import `WindRecipe` via `package:magic/magic.dart` inside `magic_starter` files (it re-exports the wind barrel). Direct `package:fluttersdk_wind/...` imports trip `depend_on_referenced_packages`.
- Import `WindRecipe` via `package:magic/magic.dart` inside this app's files (it re-exports the wind barrel). Direct `package:fluttersdk_wind/...` imports trip `depend_on_referenced_packages`.

## Token-Only Rule

Expand Down
36 changes: 36 additions & 0 deletions .github/instructions/flutter-app.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
applyTo: "lib/**,test/**"
---

<!-- GENERATED from .claude/rules/flutter-app.md by bin/sync-instructions. Edit that file, not this one. -->

# The Flutter app

Applies to `lib/` and `test/`. Colours, the component folder contract and the anti-pattern table live in `.github/instructions/design.instructions.md`, which loads alongside this file.

## The two skills are the standard, and this file is only where we differ

`magic-framework` and `wind-ui` define how code on this stack is written, and copies of both sit at `.github/skills/` so a reviewer with only this checkout has them too. Load them before the first line of Dart rather than working from memory. This file does not restate them; it carries what this app does differently, and what has not been built yet.

## One controller exists, and it is the one to copy

`lib/app/controllers/dashboard_controller.dart` paired with `lib/resources/views/dashboard_view.dart` is the single worked instance of the framework's controller and view pattern in this repo. Read it before adding a second; its docblock carries the reasoning, not just the shape. `lib/resources/views/welcome_view.dart` is still a plain `StatelessWidget` reading `Config.get('app.name', ...)` directly, which is fine for a screen with no state and no identity in it.

Follow the skill's definition rather than inventing a shape here:

- A controller is a `MagicController` resolved through a canonical `static X get instance => Magic.findOrPut(X.new);`, notifying through `refreshUI()` rather than calling `notifyListeners()` directly.
- A view pairs with it as `MagicStatefulView<XController>` / `MagicStatefulViewState`. Do not pass a controller through a view's constructor; nothing then resets it between logins or tests.
- A controller holding anything that belongs to the current identity implements `SessionScopedController`. `SessionScopeSync.attach()` (`lib/app/providers/app_service_provider.dart:81`) resets every registered one on login and team switch; `onInit` alone cannot cover this, because it runs once per controller lifetime rather than once per session. Skip it and a team switch leaves the previous tenant's data on screen until the app restarts.
- No app shell under `lib/ui/layouts/`. `lib/routes/app.dart:16` already mounts `magic_starter`'s `layout.app` through `MagicRoute.group(layout: ...)`; a second shell competes with it and decays.

## Routes register in `boot()`, not `register()`

`RouteServiceProvider.boot()` (`lib/app/providers/route_service_provider.dart`) calls `registerAppRoutes()` and the starter route registrars, then registers the dev-only preview catalog, all inside `boot()`. That is deliberate here: the preview registration must land before `MagicRouter` locks its route table on first build, and `boot()` is the phase both dev tooling and the app routes share. Do not move route registration into `register()` on the assumption that is the framework default; it is not what this repo does, and the comment at that call site explains why.

## Config-plus-factory is the wiring shape for a new subsystem

A subsystem gets its own `lib/config/<name>.dart` exposing a single `Map<String, dynamic> get <name>Config => {...}` getter, then a `() => <name>Config` entry added to the `configFactories` list in `lib/main.dart`. `lib/config/localization.dart` and `lib/config/notifications.dart` are the two current instances (wired at `lib/main.dart:39-41`); read either before adding a third. Every value goes through `env()` with an explicit fallback rather than requiring a `.env` entry, and each non-obvious default carries a comment saying why that default and not another (see `notifications.dart`'s push section). This is the only place a subsystem is configured; do not scatter its options across the provider that consumes it.

## Generated files, never hand-edited

`lib/config/wind_theme.g.dart` (`design:sync`), `lib/_previews.g.dart` (`previews:refresh`), `lib/app/_plugins.g.dart` and `lib/app/commands/_index.g.dart` (`commands:refresh`). Regenerate through the dispatcher command named in parentheses; a hand edit is overwritten on the next run and diverges from `analysis_options.yaml`'s strict-mode expectations in the meantime.
Loading