chore: update README and CLI usage for cursor_gen, version bump to 1.0.1
- Changed CLI usage instructions from `dart run cursor_gen` to `cursor_gen` for global activation. - Updated project-brief.yaml example and README to reflect new command usage. - Added app_context section in project-brief.yaml for theme variants and RBAC roles. - Fixed bundled template resolution for local and global installs to prevent 'Template not found' errors. - Version bump to 1.0.1 with corresponding updates in CHANGELOG and pubspec.yaml.
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
---
|
||||
description: "BLoC / Cubit conventions for {{PROJECT_NAME}}"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# BLoC / Cubit Standards — {{PROJECT_NAME}}
|
||||
|
||||
## When to use BLoC vs Cubit
|
||||
- **Cubit**: simple state with no meaningful event history (toggle, counter, pagination)
|
||||
- **BLoC**: event-driven flows where event history or transitions matter (auth, checkout, form wizard)
|
||||
|
||||
## Event and State classes
|
||||
```dart
|
||||
// Events — sealed class (exhaustive switch)
|
||||
sealed class AuthEvent { const AuthEvent(); }
|
||||
final class AuthLoginRequested extends AuthEvent {
|
||||
final String email, password;
|
||||
const AuthLoginRequested({required this.email, required this.password});
|
||||
}
|
||||
final class AuthLogoutRequested extends AuthEvent { const AuthLogoutRequested(); }
|
||||
|
||||
// States — sealed class, immutable
|
||||
sealed class AuthState { const AuthState(); }
|
||||
final class AuthInitial extends AuthState { const AuthInitial(); }
|
||||
final class AuthLoading extends AuthState { const AuthLoading(); }
|
||||
final class AuthAuthenticated extends AuthState {
|
||||
final User user;
|
||||
const AuthAuthenticated(this.user);
|
||||
}
|
||||
final class AuthUnauthenticated extends AuthState { const AuthUnauthenticated(); }
|
||||
final class AuthFailure extends AuthState {
|
||||
final String message;
|
||||
const AuthFailure(this.message);
|
||||
}
|
||||
```
|
||||
|
||||
## BlocProvider placement
|
||||
- Create `BlocProvider` at the **route level** (in {{ROUTING}} route definitions)
|
||||
- `MultiBlocProvider` at route level for screens needing multiple blocs
|
||||
- **NEVER** create `BlocProvider` inside a widget's `build()` method
|
||||
|
||||
## Usage rules
|
||||
- **NEVER** call `bloc.add()` inside `build()` — only in gesture callbacks or `initState()`
|
||||
- Use `BlocConsumer` only when BOTH `listen` + `build` logic are needed
|
||||
- Use `BlocSelector` when only a subset of state triggers a rebuild
|
||||
- Every BLoC must override `close()` and cancel `StreamSubscription`s
|
||||
|
||||
## BlocBuilder patterns
|
||||
```dart
|
||||
BlocBuilder<AuthBloc, AuthState>(
|
||||
builder: (context, state) => switch (state) {
|
||||
AuthInitial() => const SizedBox.shrink(),
|
||||
AuthLoading() => const LoadingIndicator(),
|
||||
AuthAuthenticated(user: final u) => HomeScreen(user: u),
|
||||
AuthUnauthenticated() => const LoginScreen(),
|
||||
AuthFailure(message: final m) => ErrorScreen(message: m),
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
## File locations in {{PROJECT_NAME}}
|
||||
- `lib/features/[feature]/presentation/bloc/[feature]_bloc.dart`
|
||||
- `lib/features/[feature]/presentation/bloc/[feature]_event.dart`
|
||||
- `lib/features/[feature]/presentation/bloc/[feature]_state.dart`
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
description: "GetX conventions for {{PROJECT_NAME}} (legacy — migration available)"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# GetX Standards — {{PROJECT_NAME}}
|
||||
> ⚠️ This project uses GetX. See `migration-agent` for incremental migration to Riverpod.
|
||||
|
||||
## Controller structure
|
||||
```dart
|
||||
class ProductsController extends GetxController {
|
||||
final ProductRepository _repo;
|
||||
ProductsController(this._repo);
|
||||
|
||||
final RxList<Product> products = <Product>[].obs;
|
||||
final RxBool isLoading = false.obs;
|
||||
final Rx<String?> error = Rx(null);
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchProducts();
|
||||
}
|
||||
|
||||
Future<void> fetchProducts() async {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
products.value = await _repo.getProducts();
|
||||
} catch (e) {
|
||||
error.value = e.toString();
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## View pattern
|
||||
```dart
|
||||
// Views extend GetView<Controller> — never GetWidget or raw StatelessWidget
|
||||
class ProductsView extends GetView<ProductsController> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Obx(() {
|
||||
if (controller.isLoading.value) return const ProductShimmer();
|
||||
if (controller.error.value != null) return ErrorWidget(controller.error.value!);
|
||||
return ProductList(controller.products);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Rules
|
||||
- **NEVER** pass `BuildContext` into a controller
|
||||
- Use `Binding` classes for dependency injection — never `Get.put()` in a widget
|
||||
- Use `.obs` for all reactive state — never call `update()` on non-observable state
|
||||
- Use `Get.find<Controller>()` only in `Binding` classes, not in widgets
|
||||
- **No business logic in Views** — controllers handle all logic
|
||||
|
||||
## File locations in {{PROJECT_NAME}}
|
||||
- `lib/features/[feature]/views/[feature]_view.dart`
|
||||
- `lib/features/[feature]/controllers/[feature]_controller.dart`
|
||||
- `lib/features/[feature]/bindings/[feature]_binding.dart`
|
||||
- `lib/features/[feature]/models/[feature]_model.dart`
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
---
|
||||
description: "Hooks + Riverpod conventions for {{PROJECT_NAME}}"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Hooks + Riverpod Standards — {{PROJECT_NAME}}
|
||||
|
||||
## Widget base classes
|
||||
- `HookConsumerWidget` — when you need BOTH hooks and Riverpod providers
|
||||
- `HookWidget` — when you need ONLY hooks (no Riverpod)
|
||||
- `ConsumerWidget` — when you need ONLY Riverpod (no hooks)
|
||||
|
||||
## Hook rules
|
||||
```dart
|
||||
class ProductSearchWidget extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// Hooks at the TOP of build() — never inside conditions or loops
|
||||
final searchQuery = useState('');
|
||||
final searchCtrl = useTextEditingController();
|
||||
final focusNode = useFocusNode();
|
||||
final debounced = useDebounced(searchQuery.value, const Duration(milliseconds: 300));
|
||||
|
||||
// Riverpod below hooks
|
||||
final results = ref.watch(searchResultsProvider(debounced));
|
||||
|
||||
return // ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## What goes in hooks vs providers
|
||||
| Concern | Tool |
|
||||
|---------|------|
|
||||
| Local UI state (text controller, animation, focus) | `useState`, `useAnimationController` |
|
||||
| Server/async state | Riverpod `AsyncNotifier` |
|
||||
| Cross-widget/feature state | Riverpod providers |
|
||||
| Lifecycle side effects | `useEffect` |
|
||||
|
||||
## Rules
|
||||
- **NEVER** call hooks inside `if`, `for`, or callbacks
|
||||
- `useEffect` cleanup MUST return a dispose function
|
||||
- Custom hooks: prefix with `use`, live in `lib/core/hooks/`
|
||||
- Do not use `flutter_riverpod` `ref.watch` inside `useEffect` — use `useRef` + `ref.listen`
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
description: "Riverpod conventions for {{PROJECT_NAME}}"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Riverpod Standards — {{PROJECT_NAME}}
|
||||
|
||||
## Provider types
|
||||
| Type | Use case |
|
||||
|------|----------|
|
||||
| `AsyncNotifier` | Async state from {{BACKEND}} |
|
||||
| `Notifier` | Synchronous derived/local UI state |
|
||||
| `StreamNotifier` | Real-time subscriptions |
|
||||
| `@riverpod` function | Simple computed/derived values |
|
||||
|
||||
## Code generation (mandatory)
|
||||
```dart
|
||||
// ✅ Always use @riverpod annotation
|
||||
@riverpod
|
||||
class AuthNotifier extends _$AuthNotifier {
|
||||
@override
|
||||
Future<User?> build() => ref.watch(authRepositoryProvider).currentUser();
|
||||
|
||||
Future<void> login(String email, String password) async {
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(
|
||||
() => ref.read(authRepositoryProvider).login(email, password),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ Never write manual provider declarations
|
||||
final authProvider = StateNotifierProvider<AuthNotifier, AsyncValue<User?>>(
|
||||
(ref) => AuthNotifier(),
|
||||
); // DON'T DO THIS
|
||||
```
|
||||
|
||||
## Rules
|
||||
- `ref.watch()` inside `build()` ONLY — **never** `ref.read()` inside `build()`
|
||||
- `ref.read(provider.notifier).method()` for mutations in gesture handlers
|
||||
- `ref.invalidate(provider)` to refresh — never manually reset state to `AsyncLoading()`
|
||||
- Family providers for parameterized data: `productDetailsProvider(productId)`
|
||||
- Providers scoped at feature level; core providers in `lib/core/di/`
|
||||
|
||||
## AsyncValue in widgets
|
||||
Every `AsyncValue` MUST handle all three states:
|
||||
```dart
|
||||
ref.watch(productsProvider).when(
|
||||
data: (products) => ProductList(products: products),
|
||||
loading: () => const ProductListShimmer(), // required
|
||||
error: (e, _) => ErrorWidget(error: e), // required
|
||||
)
|
||||
```
|
||||
|
||||
## File locations in {{PROJECT_NAME}}
|
||||
- `lib/features/[feature]/[feature]_provider.dart` (generated: `[feature]_provider.g.dart`)
|
||||
- `lib/features/[feature]/[feature]_repository.dart`
|
||||
- Run `dart run build_runner watch` during development
|
||||
Reference in New Issue
Block a user