Initial commit of the Flutter Cursor Generator project, including the core generator tool, project brief schema, example project setup, and CI configuration. Added README documentation outlining repository structure, quick start guide, and detailed descriptions of features and architecture pillars.

This commit is contained in:
2026-05-12 22:29:55 +05:30
commit 6dfb9a8aa5
72 changed files with 4542 additions and 0 deletions
@@ -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`
@@ -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