399 files changed
+31670
.agent/skills/flutter-adding-home-screen-widgets/SKILL.md
new
+202
@@ -0,0 +1,202 @@
1
+---
2
+name: "flutter-adding-home-screen-widgets"
3
+description: "Adds home screen widgets to a Flutter app for Android and iOS. Use when providing glanceable app information or quick actions on the device home screen."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:23:50 GMT"
7
+
8
+---
9
+# Implementing Flutter Home Screen Widgets
10
+
11
+## Contents
12
+- [Architecture & Data Flow](#architecture--data-flow)
13
+- [Flutter Integration Workflow](#flutter-integration-workflow)
14
+- [iOS Implementation Workflow](#ios-implementation-workflow)
15
+- [Android Implementation Workflow](#android-implementation-workflow)
16
+- [Advanced Techniques](#advanced-techniques)
17
+- [Examples](#examples)
18
+
19
+## Architecture & Data Flow
20
+Home Screen Widgets require native UI implementation (SwiftUI for iOS, XML/Kotlin for Android). The Flutter app communicates with these native widgets via shared local storage (`UserDefaults` on iOS, `SharedPreferences` on Android) using the `home_widget` package.
21
+
22
+- **Data Write:** Flutter app writes key-value pairs or renders images to a shared container.
23
+- **Trigger:** Flutter app signals the native OS to update the widget.
24
+- **Data Read:** Native widget wakes up, reads the key-value pairs or images from the shared container, and updates its UI.
25
+
26
+## Flutter Integration Workflow
27
+
28
+Use this checklist to implement the Dart side of the Home Screen Widget integration.
29
+
30
+- [ ] **Step 1: Initialize the App Group.** Call `HomeWidget.setAppGroupId('<YOUR_APP_GROUP>')` in `initState()` or app startup.
31
+- [ ] **Step 2: Save Data.** Use `HomeWidget.saveWidgetData<T>('key', value)` to write data to shared storage.
32
+- [ ] **Step 3: Trigger Update.** Call `HomeWidget.updateWidget(iOSName: 'YourIOSWidget', androidName: 'YourAndroidWidget')` to notify the OS.
33
+- [ ] **Step 4: Validate.** Run Flutter build -> review console for missing plugin registrations -> fix.
34
+
35
+## iOS Implementation Workflow
36
+
37
+If targeting iOS, implement the widget using Xcode and SwiftUI.
38
+
39
+- [ ] **Step 1: Create Target.** Open `ios/Runner.xcworkspace` in Xcode. Add a new **Widget Extension** target. Disable "Include Live Activity" and "Include Configuration Intent" unless explicitly required.
40
+- [ ] **Step 2: Configure App Groups.** Add the **App Groups** capability to *both* the Runner target and the Widget Extension target. Ensure the App Group ID matches the one used in Dart.
41
+- [ ] **Step 3: Define TimelineEntry.** Create a struct conforming to `TimelineEntry` to hold the data passed from shared storage.
42
+- [ ] **Step 4: Implement TimelineProvider.**
43
+ - In `getSnapshot` and `getTimeline`, instantiate `UserDefaults(suiteName: "<YOUR_APP_GROUP>")`.
44
+ - Extract values using `userDefaults?.string(forKey: "your_key")`.
45
+ - Return the populated `TimelineEntry`.
46
+- [ ] **Step 5: Build UI.** Implement the SwiftUI `View` to display the data from the `TimelineEntry`.
47
+- [ ] **Step 6: Validate.** Run Xcode build for the Widget Extension -> review provisioning/App Group errors -> fix.
48
+
49
+## Android Implementation Workflow
50
+
51
+If targeting Android, implement the widget using Android Studio and XML/Kotlin.
52
+
53
+- [ ] **Step 1: Create App Widget.** Open the `android` folder in Android Studio. Right-click the app directory -> **New -> Widget -> App Widget**.
54
+- [ ] **Step 2: Define Layout.** Edit `res/layout/<widget_name>.xml` to define the UI using standard Android XML layouts (e.g., `RelativeLayout`, `TextView`, `ImageView`).
55
+- [ ] **Step 3: Implement AppWidgetProvider.**
56
+ - Open the generated Kotlin class extending `AppWidgetProvider`.
57
+ - In the `onUpdate` method, retrieve shared data using `HomeWidgetPlugin.getData(context)`.
58
+ - Extract values using `widgetData.getString("your_key", null)`.
59
+ - Update the UI using `RemoteViews` and `setTextViewText` or `setImageViewBitmap`.
60
+ - Call `appWidgetManager.updateAppWidget(appWidgetId, views)`.
61
+- [ ] **Step 4: Validate.** Run Android build -> review Manifest registration errors -> fix.
62
+
63
+## Advanced Techniques
64
+
65
+### Rendering Flutter Widgets as Images
66
+If the UI is too complex to recreate natively (e.g., custom charts), render the Flutter widget to an image and display the image in the native widget.
67
+
68
+1. Wrap the target Flutter widget with a `GlobalKey`.
69
+2. Call `HomeWidget.renderFlutterWidget()`, passing the widget, a filename, and the key.
70
+3. **iOS:** Read the file path from `UserDefaults` and render using `UIImage(contentsOfFile:)` inside a SwiftUI `Image`.
71
+4. **Android:** Read the file path from `SharedPreferences`, decode using `BitmapFactory.decodeFile()`, and render using `setImageViewBitmap()`.
72
+
73
+### Using Custom Flutter Fonts (iOS Only)
74
+If utilizing custom fonts defined in Flutter on iOS Home Screen Widgets:
75
+
76
+1. Extract the Flutter asset bundle path in Swift.
77
+2. Register the font using `CTFontManagerRegisterFontsForURL`.
78
+3. Apply the font in SwiftUI using `Font.custom()`.
79
+
80
+## Examples
81
+
82
+### Example: Flutter Data Update
83
+```dart
84
+import 'package:home_widget/home_widget.dart';
85
+
86
+const String appGroupId = 'group.com.example.app';
87
+const String iOSWidgetName = 'NewsWidgets';
88
+const String androidWidgetName = 'NewsWidget';
89
+
90
+Future<void> updateWidgetData(String title, String description) async {
91
+ await HomeWidget.setAppGroupId(appGroupId);
92
+ await HomeWidget.saveWidgetData<String>('headline_title', title);
93
+ await HomeWidget.saveWidgetData<String>('headline_description', description);
94
+ await HomeWidget.updateWidget(
95
+ iOSName: iOSWidgetName,
96
+ androidName: androidWidgetName,
97
+ );
98
+}
99
+```
100
+
101
+### Example: iOS SwiftUI Provider & View
102
+```swift
103
+import WidgetKit
104
+import SwiftUI
105
+
106
+struct NewsArticleEntry: TimelineEntry {
107
+ let date: Date
108
+ let title: String
109
+ let description: String
110
+}
111
+
112
+struct Provider: TimelineProvider {
113
+ func placeholder(in context: Context) -> NewsArticleEntry {
114
+ NewsArticleEntry(date: Date(), title: "Loading...", description: "Loading...")
115
+ }
116
+
117
+ func getSnapshot(in context: Context, completion: @escaping (NewsArticleEntry) -> ()) {
118
+ let userDefaults = UserDefaults(suiteName: "group.com.example.app")
119
+ let title = userDefaults?.string(forKey: "headline_title") ?? "No Title"
120
+ let description = userDefaults?.string(forKey: "headline_description") ?? "No Description"
121
+
122
+ let entry = NewsArticleEntry(date: Date(), title: title, description: description)
123
+ completion(entry)
124
+ }
125
+
126
+ func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
127
+ getSnapshot(in: context) { (entry) in
128
+ let timeline = Timeline(entries: [entry], policy: .atEnd)
129
+ completion(timeline)
130
+ }
131
+ }
132
+}
133
+
134
+struct NewsWidgetsEntryView : View {
135
+ var entry: Provider.Entry
136
+
137
+ var body: some View {
138
+ VStack(alignment: .leading) {
139
+ Text(entry.title).font(.headline)
140
+ Text(entry.description).font(.subheadline)
141
+ }
142
+ }
143
+}
144
+```
145
+
146
+### Example: Android Kotlin Provider
147
+```kotlin
148
+package com.example.app.widgets
149
+
150
+import android.appwidget.AppWidgetManager
151
+import android.appwidget.AppWidgetProvider
152
+import android.content.Context
153
+import android.widget.RemoteViews
154
+import es.antonborri.home_widget.HomeWidgetPlugin
155
+import com.example.app.R
156
+
157
+class NewsWidget : AppWidgetProvider() {
158
+ override fun onUpdate(
159
+ context: Context,
160
+ appWidgetManager: AppWidgetManager,
161
+ appWidgetIds: IntArray,
162
+ ) {
163
+ for (appWidgetId in appWidgetIds) {
164
+ val widgetData = HomeWidgetPlugin.getData(context)
165
+ val views = RemoteViews(context.packageName, R.layout.news_widget).apply {
166
+ val title = widgetData.getString("headline_title", "No Title")
167
+ setTextViewText(R.id.headline_title, title)
168
+
169
+ val description = widgetData.getString("headline_description", "No Description")
170
+ setTextViewText(R.id.headline_description, description)
171
+ }
172
+ appWidgetManager.updateAppWidget(appWidgetId, views)
173
+ }
174
+ }
175
+}
176
+```
177
+
178
+<details>
179
+<summary>Example: iOS Custom Font Registration Helper</summary>
180
+
181
+```swift
182
+// Add this to your SwiftUI View struct
183
+var bundle: URL {
184
+ let bundle = Bundle.main
185
+ if bundle.bundleURL.pathExtension == "appex" {
186
+ var url = bundle.bundleURL.deletingLastPathComponent().deletingLastPathComponent()
187
+ url.append(component: "Frameworks/App.framework/flutter_assets")
188
+ return url
189
+ }
190
+ return bundle.bundleURL
191
+}
192
+
193
+init(entry: Provider.Entry) {
194
+ self.entry = entry
195
+ CTFontManagerRegisterFontsForURL(
196
+ bundle.appending(path: "/fonts/YourCustomFont.ttf") as CFURL,
197
+ CTFontManagerScope.process,
198
+ nil
199
+ )
200
+}
201
+```
202
+</details>
.agent/skills/flutter-animating-apps/SKILL.md
new
+182
@@ -0,0 +1,182 @@
1
+---
2
+name: "flutter-animating-apps"
3
+description: "Implements animated effects, transitions, and motion in a Flutter app. Use when adding visual feedback, shared element transitions, or physics-based animations."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:16:34 GMT"
7
+
8
+---
9
+# Implementing Flutter Animations
10
+
11
+## Contents
12
+- [Core Concepts](#core-concepts)
13
+- [Animation Strategies](#animation-strategies)
14
+- [Workflows](#workflows)
15
+ - [Implementing Implicit Animations](#implementing-implicit-animations)
16
+ - [Implementing Explicit Animations](#implementing-explicit-animations)
17
+ - [Implementing Hero Transitions](#implementing-hero-transitions)
18
+ - [Implementing Physics-Based Animations](#implementing-physics-based-animations)
19
+- [Examples](#examples)
20
+
21
+## Core Concepts
22
+
23
+Manage Flutter animations using the core typed `Animation` system. Do not manually calculate frames; rely on the framework's ticker and interpolation classes.
24
+
25
+* **`Animation<T>`**: Treat this as an abstract representation of a value that changes over time. It holds state (completed, dismissed) and notifies listeners, but knows nothing about the UI.
26
+* **`AnimationController`**: Instantiate this to drive the animation. It generates values (typically 0.0 to 1.0) tied to the screen refresh rate. Always provide a `vsync` (usually via `SingleTickerProviderStateMixin`) to prevent offscreen resource consumption. Always `dispose()` controllers to prevent memory leaks.
27
+* **`Tween<T>`**: Define a stateless mapping from an input range (usually 0.0-1.0) to an output type (e.g., `Color`, `Offset`, `double`). Chain tweens with curves using `.animate()`.
28
+* **`Curve`**: Apply non-linear timing (e.g., `Curves.easeIn`, `Curves.bounceOut`) to an animation using a `CurvedAnimation` or `CurveTween`.
29
+
30
+## Animation Strategies
31
+
32
+Apply conditional logic to select the correct animation approach:
33
+
34
+* **If animating simple property changes (size, color, opacity) without playback control:** Use **Implicit Animations** (e.g., `AnimatedContainer`, `AnimatedOpacity`, `TweenAnimationBuilder`).
35
+* **If requiring playback control (play, pause, reverse, loop) or coordinating multiple properties:** Use **Explicit Animations** (e.g., `AnimationController` with `AnimatedBuilder` or `AnimatedWidget`).
36
+* **If animating elements between two distinct routes:** Use **Hero Animations** (Shared Element Transitions).
37
+* **If modeling real-world motion (e.g., snapping back after a drag):** Use **Physics-Based Animations** (e.g., `SpringSimulation`).
38
+* **If animating a sequence of overlapping or delayed motions:** Use **Staggered Animations** (multiple `Tween`s driven by a single `AnimationController` using `Interval` curves).
39
+
40
+## Workflows
41
+
42
+### Implementing Implicit Animations
43
+
44
+Use this workflow for "fire-and-forget" state-driven animations.
45
+
46
+- [ ] **Task Progress:**
47
+ - [ ] Identify the target properties to animate (e.g., width, color).
48
+ - [ ] Replace the static widget (e.g., `Container`) with its animated counterpart (e.g., `AnimatedContainer`).
49
+ - [ ] Define the `duration` property.
50
+ - [ ] (Optional) Define the `curve` property for non-linear motion.
51
+ - [ ] Trigger the animation by updating the properties inside a `setState()` call.
52
+ - [ ] Run validator -> review UI for jank -> adjust duration/curve if necessary.
53
+
54
+### Implementing Explicit Animations
55
+
56
+Use this workflow when you need granular control over the animation lifecycle.
57
+
58
+- [ ] **Task Progress:**
59
+ - [ ] Add `SingleTickerProviderStateMixin` (or `TickerProviderStateMixin` for multiple controllers) to the `State` class.
60
+ - [ ] Initialize an `AnimationController` in `initState()`, providing `vsync: this` and a `duration`.
61
+ - [ ] Define a `Tween` and chain it to the controller using `.animate()`.
62
+ - [ ] Wrap the target UI in an `AnimatedBuilder` (preferred for complex trees) or subclass `AnimatedWidget`.
63
+ - [ ] Pass the `Animation` object to the `AnimatedBuilder`'s `animation` property.
64
+ - [ ] Control playback using `controller.forward()`, `controller.reverse()`, or `controller.repeat()`.
65
+ - [ ] Call `controller.dispose()` in the `dispose()` method.
66
+ - [ ] Run validator -> check for memory leaks -> ensure `dispose()` is called.
67
+
68
+### Implementing Hero Transitions
69
+
70
+Use this workflow to fly a widget between two routes.
71
+
72
+- [ ] **Task Progress:**
73
+ - [ ] Wrap the source widget in a `Hero` widget.
74
+ - [ ] Assign a unique, data-driven `tag` to the source `Hero`.
75
+ - [ ] Wrap the destination widget in a `Hero` widget.
76
+ - [ ] Assign the *exact same* `tag` to the destination `Hero`.
77
+ - [ ] Ensure the widget trees inside both `Hero` widgets are visually similar to prevent jarring jumps.
78
+ - [ ] Trigger the transition by pushing the destination route via `Navigator`.
79
+
80
+### Implementing Physics-Based Animations
81
+
82
+Use this workflow for gesture-driven, natural motion.
83
+
84
+- [ ] **Task Progress:**
85
+ - [ ] Set up an `AnimationController` (do not set a fixed duration).
86
+ - [ ] Capture gesture velocity using a `GestureDetector` (e.g., `onPanEnd` providing `DragEndDetails`).
87
+ - [ ] Convert the pixel velocity to the coordinate space of the animating property.
88
+ - [ ] Instantiate a `SpringSimulation` with mass, stiffness, damping, and the calculated velocity.
89
+ - [ ] Drive the controller using `controller.animateWith(simulation)`.
90
+
91
+## Examples
92
+
93
+<details>
94
+<summary><b>Example: Explicit Animation (Staggered with AnimatedBuilder)</b></summary>
95
+
96
+```dart
97
+class StaggeredAnimationDemo extends StatefulWidget {
98
+ @override
99
+ State<StaggeredAnimationDemo> createState() => _StaggeredAnimationDemoState();
100
+}
101
+
102
+class _StaggeredAnimationDemoState extends State<StaggeredAnimationDemo> with SingleTickerProviderStateMixin {
103
+ late AnimationController _controller;
104
+ late Animation<double> _widthAnimation;
105
+ late Animation<Color?> _colorAnimation;
106
+
107
+ @override
108
+ void initState() {
109
+ super.initState();
110
+ _controller = AnimationController(
111
+ duration: const Duration(seconds: 2),
112
+ vsync: this,
113
+ );
114
+
115
+ // Staggered width animation (0.0 to 0.5 interval)
116
+ _widthAnimation = Tween<double>(begin: 50.0, end: 200.0).animate(
117
+ CurvedAnimation(
118
+ parent: _controller,
119
+ curve: const Interval(0.0, 0.5, curve: Curves.easeIn),
120
+ ),
121
+ );
122
+
123
+ // Staggered color animation (0.5 to 1.0 interval)
124
+ _colorAnimation = ColorTween(begin: Colors.blue, end: Colors.red).animate(
125
+ CurvedAnimation(
126
+ parent: _controller,
127
+ curve: const Interval(0.5, 1.0, curve: Curves.easeOut),
128
+ ),
129
+ );
130
+
131
+ _controller.forward();
132
+ }
133
+
134
+ @override
135
+ void dispose() {
136
+ _controller.dispose(); // CRITICAL: Prevent memory leaks
137
+ super.dispose();
138
+ }
139
+
140
+ @override
141
+ Widget build(BuildContext context) {
142
+ return AnimatedBuilder(
143
+ animation: _controller,
144
+ builder: (context, child) {
145
+ return Container(
146
+ width: _widthAnimation.value,
147
+ height: 50.0,
148
+ color: _colorAnimation.value,
149
+ );
150
+ },
151
+ );
152
+ }
153
+}
154
+```
155
+</details>
156
+
157
+<details>
158
+<summary><b>Example: Custom Page Route Transition</b></summary>
159
+
160
+```dart
161
+Route createCustomRoute(Widget destination) {
162
+ return PageRouteBuilder(
163
+ pageBuilder: (context, animation, secondaryAnimation) => destination,
164
+ transitionsBuilder: (context, animation, secondaryAnimation, child) {
165
+ const begin = Offset(0.0, 1.0); // Start from bottom
166
+ const end = Offset.zero;
167
+ const curve = Curves.easeOut;
168
+
169
+ final tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
170
+ final offsetAnimation = animation.drive(tween);
171
+
172
+ return SlideTransition(
173
+ position: offsetAnimation,
174
+ child: child,
175
+ );
176
+ },
177
+ );
178
+}
179
+
180
+// Usage: Navigator.of(context).push(createCustomRoute(const NextPage()));
181
+```
182
+</details>
.agent/skills/flutter-architecting-apps/SKILL.md
new
+163
@@ -0,0 +1,163 @@
1
+---
2
+name: "flutter-architecting-apps"
3
+description: "Architects a Flutter application using the recommended layered approach (UI, Logic, Data). Use when structuring a new project or refactoring for scalability."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:13:42 GMT"
7
+
8
+---
9
+# Architecting Flutter Applications
10
+
11
+## Contents
12
+- [Core Architectural Principles](#core-architectural-principles)
13
+- [Structuring the Layers](#structuring-the-layers)
14
+- [Implementing the Data Layer](#implementing-the-data-layer)
15
+- [Feature Implementation Workflow](#feature-implementation-workflow)
16
+- [Examples](#examples)
17
+
18
+## Core Architectural Principles
19
+
20
+Design Flutter applications to scale by strictly adhering to the following principles:
21
+
22
+* **Enforce Separation of Concerns:** Decouple UI rendering from business logic and data fetching. Organize the codebase into distinct layers (UI, Logic, Data) and further separate by feature within those layers.
23
+* **Maintain a Single Source of Truth (SSOT):** Centralize application state and data in the Data layer. Ensure the SSOT is the only component authorized to mutate its respective data.
24
+* **Implement Unidirectional Data Flow (UDF):** Flow state downwards from the Data layer to the UI layer. Flow events upwards from the UI layer to the Data layer.
25
+* **Treat UI as a Function of State:** Drive the UI entirely via immutable state objects. Rebuild widgets reactively when the underlying state changes.
26
+
27
+## Structuring the Layers
28
+
29
+Separate the application into 2 to 3 distinct layers depending on complexity. Restrict communication so that a layer only interacts with the layer directly adjacent to it.
30
+
31
+### 1. UI Layer (Presentation)
32
+* **Views (Widgets):** Build reusable, lean widgets. Strip all business and data-fetching logic from the widget tree. Restrict widget logic to UI-specific concerns (e.g., animations, routing, layout constraints).
33
+* **ViewModels:** Manage the UI state. Consume domain models from the Data/Logic layers and transform them into presentation-friendly formats. Expose state to the Views and handle user interaction events.
34
+
35
+### 2. Logic Layer (Domain) - *Conditional*
36
+* **If the application requires complex client-side business logic:** Implement a Logic layer containing Use Cases or Interactors. Use this layer to orchestrate interactions between multiple repositories before passing data to the UI layer.
37
+* **If the application is a standard CRUD app:** Omit this layer. Allow ViewModels to interact directly with Repositories.
38
+
39
+### 3. Data Layer (Model)
40
+* **Responsibilities:** Act as the SSOT for all application data. Handle business data, external API consumption, event processing, and data synchronization.
41
+* **Components:** Divide the Data layer strictly into **Repositories** and **Services**.
42
+
43
+## Implementing the Data Layer
44
+
45
+### Services
46
+* **Role:** Wrap external APIs (HTTP servers, local databases, platform plugins).
47
+* **Implementation:** Write Services as stateless Dart classes. Do not store application state here.
48
+* **Mapping:** Create exactly one Service class per external data source.
49
+
50
+### Repositories
51
+* **Role:** Act as the SSOT for domain data.
52
+* **Implementation:** Consume raw data from Services. Handle caching, offline synchronization, and retry logic.
53
+* **Transformation:** Transform raw API/Service data into clean Domain Models formatted for consumption by ViewModels.
54
+
55
+## Feature Implementation Workflow
56
+
57
+Follow this sequential workflow when adding a new feature to the application.
58
+
59
+**Task Progress:**
60
+- [ ] **Step 1: Define Domain Models.** Create immutable Dart classes representing the core data structures required by the feature.
61
+- [ ] **Step 2: Implement Services.** Create stateless Service classes to handle raw data fetching (e.g., HTTP GET/POST).
62
+- [ ] **Step 3: Implement Repositories.** Create Repository classes that consume the Services, handle caching, and return Domain Models.
63
+- [ ] **Step 4: Implement ViewModels.** Create ViewModels that consume the Repositories. Expose immutable state and define methods (commands) for user actions.
64
+- [ ] **Step 5: Implement Views.** Create Flutter Widgets that bind to the ViewModel state and trigger ViewModel methods on user interaction.
65
+- [ ] **Step 6: Run Validator.** Execute unit tests for Services, Repositories, and ViewModels. Execute widget tests for Views.
66
+ * *Feedback Loop:* Review test failures -> Fix logic/mocking errors -> Re-run tests until passing.
67
+
68
+## Examples
69
+
70
+### Data Layer: Service and Repository
71
+
72
+```dart
73
+// 1. Service (Stateless API Wrapper)
74
+class UserApiService {
75
+ final HttpClient _client;
76
+
77
+ UserApiService(this._client);
78
+
79
+ Future<Map<String, dynamic>> fetchUserRaw(String userId) async {
80
+ final response = await _client.get('/users/$userId');
81
+ return response.data;
82
+ }
83
+}
84
+
85
+// 2. Domain Model (Immutable)
86
+class User {
87
+ final String id;
88
+ final String name;
89
+
90
+ const User({required this.id, required this.name});
91
+}
92
+
93
+// 3. Repository (SSOT & Data Transformer)
94
+class UserRepository {
95
+ final UserApiService _apiService;
96
+ User? _cachedUser;
97
+
98
+ UserRepository(this._apiService);
99
+
100
+ Future<User> getUser(String userId) async {
101
+ if (_cachedUser != null && _cachedUser!.id == userId) {
102
+ return _cachedUser!;
103
+ }
104
+
105
+ final rawData = await _apiService.fetchUserRaw(userId);
106
+ final user = User(id: rawData['id'], name: rawData['name']);
107
+
108
+ _cachedUser = user; // Cache data
109
+ return user;
110
+ }
111
+}
112
+```
113
+
114
+### UI Layer: ViewModel and View
115
+
116
+```dart
117
+// 4. ViewModel (State Management)
118
+class UserViewModel extends ChangeNotifier {
119
+ final UserRepository _userRepository;
120
+
121
+ User? user;
122
+ bool isLoading = false;
123
+ String? error;
124
+
125
+ UserViewModel(this._userRepository);
126
+
127
+ Future<void> loadUser(String userId) async {
128
+ isLoading = true;
129
+ error = null;
130
+ notifyListeners();
131
+
132
+ try {
133
+ user = await _userRepository.getUser(userId);
134
+ } catch (e) {
135
+ error = e.toString();
136
+ } finally {
137
+ isLoading = false;
138
+ notifyListeners();
139
+ }
140
+ }
141
+}
142
+
143
+// 5. View (Lean UI)
144
+class UserProfileView extends StatelessWidget {
145
+ final UserViewModel viewModel;
146
+
147
+ const UserProfileView({Key? key, required this.viewModel}) : super(key: key);
148
+
149
+ @override
150
+ Widget build(BuildContext context) {
151
+ return ListenableBuilder(
152
+ listenable: viewModel,
153
+ builder: (context, child) {
154
+ if (viewModel.isLoading) return const CircularProgressIndicator();
155
+ if (viewModel.error != null) return Text('Error: ${viewModel.error}');
156
+ if (viewModel.user == null) return const Text('No user data.');
157
+
158
+ return Text('Hello, ${viewModel.user!.name}');
159
+ },
160
+ );
161
+ }
162
+}
163
+```
.agent/skills/flutter-building-forms/SKILL.md
new
+122
@@ -0,0 +1,122 @@
1
+---
2
+name: "flutter-building-forms"
3
+description: "Builds Flutter forms with validation and user input handling. Use when creating login screens, data entry forms, or any multi-field user input."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:15:24 GMT"
7
+
8
+---
9
+# Building Validated Forms
10
+
11
+## Contents
12
+- [Form Architecture](#form-architecture)
13
+- [Field Validation](#field-validation)
14
+- [Workflow: Implementing a Validated Form](#workflow-implementing-a-validated-form)
15
+- [Examples](#examples)
16
+
17
+## Form Architecture
18
+
19
+Implement forms using a `Form` widget to group and validate multiple input fields together.
20
+
21
+- **Use a StatefulWidget:** Always host your `Form` inside a `StatefulWidget`.
22
+- **Persist the GlobalKey:** Instantiate a `GlobalKey<FormState>` exactly once as a final variable within the `State` class. Do not generate a new `GlobalKey` inside the `build` method; doing so is resource-expensive and destroys the form's state on every rebuild.
23
+- **Bind the Key:** Pass the `GlobalKey<FormState>` to the `key` property of the `Form` widget. This uniquely identifies the form and provides access to the `FormState` for validation and submission.
24
+- **Alternative Access:** If dealing with highly complex widget trees where passing the key is impractical, use `Form.of(context)` to access the `FormState` from a descendant widget.
25
+
26
+## Field Validation
27
+
28
+Use `TextFormField` to render Material Design text inputs with built-in validation support. `TextFormField` is a convenience widget that automatically wraps a standard `TextField` inside a `FormField`.
29
+
30
+- **Implement the Validator:** Provide a `validator()` callback function to each `TextFormField`.
31
+- **Return Error Messages:** If the user's input is invalid, return a `String` containing the specific error message. The `Form` will automatically rebuild to display this text below the field.
32
+- **Return Null for Success:** If the input passes validation, you must return `null`.
33
+
34
+## Workflow: Implementing a Validated Form
35
+
36
+Follow this sequential workflow to implement and validate a form. Copy the checklist to track your progress.
37
+
38
+**Task Progress:**
39
+- [ ] 1. Create a `StatefulWidget` and its corresponding `State` class.
40
+- [ ] 2. Instantiate `final _formKey = GlobalKey<FormState>();` in the `State` class.
41
+- [ ] 3. Return a `Form` widget in the `build` method and assign `key: _formKey`.
42
+- [ ] 4. Add `TextFormField` widgets as descendants of the `Form`.
43
+- [ ] 5. Write a `validator` function for each `TextFormField` (return `String` on error, `null` on success).
44
+- [ ] 6. Add a submit button (e.g., `ElevatedButton`).
45
+- [ ] 7. Implement the validation check in the button's `onPressed` callback using `_formKey.currentState!.validate()`.
46
+
47
+### Validation Decision Logic
48
+
49
+When the user triggers the submit action, execute the following conditional logic:
50
+
51
+1. Call `_formKey.currentState!.validate()`.
52
+2. **If `true` (Valid):** All validators returned `null`. Proceed with form submission (e.g., save data, make API call) and display a success indicator (e.g., a `SnackBar`).
53
+3. **If `false` (Invalid):** One or more validators returned an error string. The `FormState` automatically rebuilds the UI to display the error messages.
54
+4. **Feedback Loop:** Run validator -> review errors -> fix. The user must adjust their input and resubmit until `validate()` returns `true`.
55
+
56
+## Examples
57
+
58
+### Complete Validated Form Implementation
59
+
60
+Use the following pattern to implement a robust, validated form.
61
+
62
+```dart
63
+import 'package:flutter/material.dart';
64
+
65
+class UserRegistrationForm extends StatefulWidget {
66
+ const UserRegistrationForm({super.key});
67
+
68
+ @override
69
+ State<UserRegistrationForm> createState() => _UserRegistrationFormState();
70
+}
71
+
72
+class _UserRegistrationFormState extends State<UserRegistrationForm> {
73
+ // 1. Persist the GlobalKey in the State class
74
+ final _formKey = GlobalKey<FormState>();
75
+
76
+ @override
77
+ Widget build(BuildContext context) {
78
+ // 2. Bind the key to the Form
79
+ return Form(
80
+ key: _formKey,
81
+ child: Column(
82
+ crossAxisAlignment: CrossAxisAlignment.start,
83
+ children: [
84
+ // 3. Add TextFormFields with validators
85
+ TextFormField(
86
+ decoration: const InputDecoration(
87
+ labelText: 'Username',
88
+ hintText: 'Enter your username',
89
+ ),
90
+ validator: (value) {
91
+ if (value == null || value.isEmpty) {
92
+ return 'Please enter a username'; // Error state
93
+ }
94
+ if (value.length < 4) {
95
+ return 'Username must be at least 4 characters'; // Error state
96
+ }
97
+ return null; // Valid state
98
+ },
99
+ ),
100
+ const SizedBox(height: 16),
101
+ // 4. Add the submit button
102
+ ElevatedButton(
103
+ onPressed: () {
104
+ // 5. Trigger validation logic
105
+ if (_formKey.currentState!.validate()) {
106
+ // Form is valid: Process data
107
+ ScaffoldMessenger.of(context).showSnackBar(
108
+ const SnackBar(content: Text('Processing Data')),
109
+ );
110
+ } else {
111
+ // Form is invalid: Errors are automatically displayed
112
+ debugPrint('Form validation failed.');
113
+ }
114
+ },
115
+ child: const Text('Submit'),
116
+ ),
117
+ ],
118
+ ),
119
+ );
120
+ }
121
+}
122
+```
.agent/skills/flutter-building-layouts/SKILL.md
new
+128
@@ -0,0 +1,128 @@
1
+---
2
+name: "flutter-building-layouts"
3
+description: "Builds Flutter layouts using the constraint system and layout widgets. Use when creating or refining the UI structure of a Flutter application."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:14:15 GMT"
7
+
8
+---
9
+# Architecting Flutter Layouts
10
+
11
+## Contents
12
+- [Core Layout Principles](#core-layout-principles)
13
+- [Structural Widgets](#structural-widgets)
14
+- [Adaptive and Responsive Design](#adaptive-and-responsive-design)
15
+- [Workflow: Implementing a Complex Layout](#workflow-implementing-a-complex-layout)
16
+- [Examples](#examples)
17
+
18
+## Core Layout Principles
19
+
20
+Master the fundamental Flutter layout rule: **Constraints go down. Sizes go up. Parent sets position.**
21
+
22
+* **Pass Constraints Down:** Always pass constraints (minimum/maximum width and height) from the parent Widget to its children. A Widget cannot choose its own size independently of its parent's constraints.
23
+* **Pass Sizes Up:** Calculate the child Widget's desired size within the given constraints and pass this size back up to the parent.
24
+* **Set Position via Parent:** Define the `x` and `y` coordinates of a child Widget exclusively within the parent Widget. Children do not know their own position on the screen.
25
+* **Avoid Unbounded Constraints:** Never pass unbounded constraints (e.g., `double.infinity`) in the cross-axis of a flex box (`Row` or `Column`) or within scrollable regions (`ListView`). This causes render exceptions.
26
+
27
+## Structural Widgets
28
+
29
+Select the appropriate structural Widget based on the required spatial arrangement.
30
+
31
+* **Use `Row` and `Column`:** Implement `Row` for horizontal linear layouts and `Column` for vertical linear layouts. Control child alignment using `mainAxisAlignment` and `crossAxisAlignment`.
32
+* **Use `Expanded` and `Flexible`:** Wrap children of `Row` or `Column` in `Expanded` to force them to fill available space, or `Flexible` to allow them to size themselves up to the available space.
33
+* **Use `Container`:** Wrap Widgets in a `Container` when you need to apply padding, margins, borders, or background colors.
34
+* **Use `Stack`:** Implement `Stack` when Widgets must overlap on the Z-axis. Use `Positioned` to anchor children to specific edges of the `Stack`.
35
+* **Use `SizedBox`:** Enforce strict, tight constraints on a child Widget by wrapping it in a `SizedBox` with explicit `width` and `height` values.
36
+
37
+## Adaptive and Responsive Design
38
+
39
+Apply conditional logic to handle varying screen sizes and form factors.
40
+
41
+* **If fitting UI into available space (Responsive):** Use `LayoutBuilder`, `Expanded`, and `Flexible` to dynamically adjust the size and placement of elements based on the parent's constraints.
42
+* **If adjusting UI usability for a specific form factor (Adaptive):** Use conditional rendering to swap entire layout structures. For example, render a bottom navigation bar on mobile, but a side navigation rail on tablets/desktop.
43
+
44
+## Workflow: Implementing a Complex Layout
45
+
46
+Follow this sequential workflow to architect and implement robust Flutter layouts.
47
+
48
+### Task Progress
49
+- [ ] **Phase 1: Visual Deconstruction**
50
+ - [ ] Break down the target UI into a hierarchy of rows, columns, and grids.
51
+ - [ ] Identify overlapping elements (requiring `Stack`).
52
+ - [ ] Identify scrolling regions (requiring `ListView` or `SingleChildScrollView`).
53
+- [ ] **Phase 2: Constraint Planning**
54
+ - [ ] Determine which Widgets require tight constraints (fixed size) vs. loose constraints (flexible size).
55
+ - [ ] Identify potential unbounded constraint risks (e.g., a `ListView` inside a `Column`).
56
+- [ ] **Phase 3: Implementation**
57
+ - [ ] Build the layout from the outside in, starting with the `Scaffold` and primary structural Widgets.
58
+ - [ ] Extract deeply nested layout sections into separate, stateless Widgets to maintain readability.
59
+- [ ] **Phase 4: Validation and Feedback Loop**
60
+ - [ ] Run the application on target devices/simulators.
61
+ - [ ] **Run validator -> review errors -> fix:** Open the Flutter Inspector. Enable "Debug Paint" to visualize render boxes.
62
+ - [ ] Check for yellow/black striped overflow warnings.
63
+ - [ ] If overflow occurs: Wrap the overflowing Widget in `Expanded` (if inside a flex box) or wrap the parent in a scrollable Widget.
64
+
65
+## Examples
66
+
67
+### Example: Resolving Unbounded Constraints in Flex Boxes
68
+
69
+**Anti-pattern:** Placing a `ListView` directly inside a `Column` causes an unbounded height exception because the `Column` provides infinite vertical space to the `ListView`.
70
+
71
+```dart
72
+// BAD: Throws unbounded height exception
73
+Column(
74
+ children: [
75
+ Text('Header'),
76
+ ListView(
77
+ children: [/* items */],
78
+ ),
79
+ ],
80
+)
81
+```
82
+
83
+**Implementation:** Wrap the `ListView` in an `Expanded` Widget to bound its height to the remaining space in the `Column`.
84
+
85
+```dart
86
+// GOOD: ListView is constrained to remaining space
87
+Column(
88
+ children: [
89
+ Text('Header'),
90
+ Expanded(
91
+ child: ListView(
92
+ children: [/* items */],
93
+ ),
94
+ ),
95
+ ],
96
+)
97
+```
98
+
99
+### Example: Responsive Layout with LayoutBuilder
100
+
101
+Implement `LayoutBuilder` to conditionally render different structural Widgets based on available width.
102
+
103
+```dart
104
+Widget buildAdaptiveLayout(BuildContext context) {
105
+ return LayoutBuilder(
106
+ builder: (context, constraints) {
107
+ // Conditional logic based on screen width
108
+ if (constraints.maxWidth > 600) {
109
+ // Tablet/Desktop: Side-by-side layout
110
+ return Row(
111
+ children: [
112
+ SizedBox(width: 250, child: SidebarWidget()),
113
+ Expanded(child: MainContentWidget()),
114
+ ],
115
+ );
116
+ } else {
117
+ // Mobile: Stacked layout with navigation
118
+ return Column(
119
+ children: [
120
+ Expanded(child: MainContentWidget()),
121
+ BottomNavigationBarWidget(),
122
+ ],
123
+ );
124
+ }
125
+ },
126
+ );
127
+}
128
+```
.agent/skills/flutter-building-plugins/SKILL.md
new
+206
@@ -0,0 +1,206 @@
1
+---
2
+name: "flutter-building-plugins"
3
+description: "Builds Flutter plugins that provide native interop for other apps to use. Use when creating reusable packages that bridge Flutter with platform-specific functionality."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:21:35 GMT"
7
+
8
+---
9
+# Developing Flutter Plugins
10
+
11
+## Contents
12
+- [Architecture & Design Patterns](#architecture--design-patterns)
13
+- [Workflow: Creating a New Plugin](#workflow-creating-a-new-plugin)
14
+- [Workflow: Implementing Android Platform Code](#workflow-implementing-android-platform-code)
15
+- [Workflow: Implementing Windows Platform Code](#workflow-implementing-windows-platform-code)
16
+- [Workflow: Adding Platforms to an Existing Plugin](#workflow-adding-platforms-to-an-existing-plugin)
17
+- [Examples](#examples)
18
+
19
+## Architecture & Design Patterns
20
+
21
+### Federated Plugins
22
+Implement federated plugins to split a plugin's API across multiple packages, allowing independent teams to build platform-specific implementations. Structure federated plugins into three distinct components:
23
+1. **App-facing interface:** The primary package users depend on. It exports the public API.
24
+2. **Platform interface:** The package defining the common interface that all platform implementations must implement.
25
+3. **Platform implementations:** Independent packages containing platform-specific code (e.g., `my_plugin_android`, `my_plugin_windows`).
26
+
27
+### FFI vs. Standard Plugins
28
+Choose the correct plugin template based on your native interoperability requirements:
29
+* **Standard Plugins (`--template=plugin`):** Use for accessing platform-specific APIs (e.g., Android SDK, iOS frameworks) via Method Channels.
30
+* **FFI Plugins (`--template=plugin_ffi`):** Use for accessing C/C++ native libraries, configuring Google Play services on Android, or using static linking on iOS/macOS.
31
+ * *Constraint:* FFI plugin packages support bundling native code and method channel registration code, but *not* method channels themselves. If you require both method channels and FFI, use the standard non-FFI plugin template.
32
+
33
+## Workflow: Creating a New Plugin
34
+
35
+Follow this workflow to initialize a new plugin package.
36
+
37
+**Task Progress:**
38
+- [ ] Determine if the plugin requires FFI or standard Method Channels.
39
+- [ ] Execute the appropriate `flutter create` command.
40
+- [ ] Verify the generated directory structure.
41
+
42
+**Conditional Initialization:**
43
+* **If creating a STANDARD plugin:**
44
+ Run the following command, specifying your supported platforms, organization, and preferred languages (defaults are Swift and Kotlin):
45
+ ```bash
46
+ flutter create --template=plugin \
47
+ --platforms=android,ios,web,linux,macos,windows \
48
+ --org com.example.organization \
49
+ -i objc -a java \
50
+ my_plugin
51
+ ```
52
+* **If creating an FFI plugin:**
53
+ Run the following command to generate a project with Dart code in `lib` (using `dart:ffi`) and native source code in `src` (with a `CMakeLists.txt`):
54
+ ```bash
55
+ flutter create --template=plugin_ffi my_ffi_plugin
56
+ ```
57
+
58
+## Workflow: Implementing Android Platform Code
59
+
60
+Always edit Android platform code using Android Studio to ensure proper code completion and Gradle synchronization.
61
+
62
+**Task Progress:**
63
+- [ ] Run initial build to generate necessary Gradle files.
64
+- [ ] Open the Android module in Android Studio.
65
+- [ ] Implement `FlutterPlugin` and lifecycle-aware interfaces.
66
+- [ ] Refactor legacy `registerWith` logic.
67
+- [ ] Run validator -> review errors -> fix.
68
+
69
+1. **Generate Build Files:**
70
+ Build the code at least once before editing to resolve dependencies.
71
+ ```bash
72
+ cd example
73
+ flutter build apk --config-only
74
+ ```
75
+2. **Open in IDE:**
76
+ Launch Android Studio and open the `example/android/build.gradle` or `example/android/build.gradle.kts` file.
77
+3. **Locate Source:**
78
+ Navigate to your plugin's source code at `java/<organization-path>/<PluginName>`.
79
+4. **Implement V2 Embedding:**
80
+ * Implement the `FlutterPlugin` interface.
81
+ * Ensure your plugin class has a public constructor.
82
+ * Extract shared initialization logic from the legacy `registerWith()` method and the new `onAttachedToEngine()` method into a single private method. Both entry points must call this private method to maintain backward compatibility without duplicating logic.
83
+5. **Implement Lifecycle Interfaces:**
84
+ * **If your plugin requires an `Activity` reference:** Implement the `ActivityAware` interface and handle the `onAttachedToActivity`, `onDetachedFromActivityForConfigChanges`, `onReattachedToActivityForConfigChanges`, and `onDetachedFromActivity` callbacks.
85
+ * **If your plugin runs in a background `Service`:** Implement the `ServiceAware` interface.
86
+6. **Update Example App:**
87
+ Ensure the example app's `MainActivity.java` extends the v2 embedding `io.flutter.embedding.android.FlutterActivity`.
88
+7. **Document API:**
89
+ Document all non-overridden public members in your Android implementation.
90
+
91
+## Workflow: Implementing Windows Platform Code
92
+
93
+Always edit Windows platform code using Visual Studio.
94
+
95
+**Task Progress:**
96
+- [ ] Run initial build to generate the Visual Studio solution.
97
+- [ ] Open the solution in Visual Studio.
98
+- [ ] Implement C++ logic.
99
+- [ ] Rebuild the solution.
100
+
101
+1. **Generate Build Files:**
102
+ ```bash
103
+ cd example
104
+ flutter build windows
105
+ ```
106
+2. **Open in IDE:**
107
+ Launch Visual Studio and open the `example/build/windows/hello_example.sln` file.
108
+3. **Locate Source:**
109
+ Navigate to `hello_plugin/Source Files` and `hello_plugin/Header Files` in the Solution Explorer.
110
+4. **Rebuild:**
111
+ After making changes to the C++ plugin code, you *must* rebuild the solution in Visual Studio before running the app, or the outdated plugin binary will be used.
112
+
113
+## Workflow: Adding Platforms to an Existing Plugin
114
+
115
+Use this workflow to retrofit an existing plugin with support for additional platforms.
116
+
117
+**Task Progress:**
118
+- [ ] Run the platform addition command.
119
+- [ ] Update iOS/macOS podspecs (if applicable).
120
+- [ ] Implement the platform-specific code.
121
+
122
+1. **Run Create Command:**
123
+ Navigate to the root directory of your existing plugin and run:
124
+ ```bash
125
+ flutter create --template=plugin --platforms=web,macos .
126
+ ```
127
+2. **Update Podspecs:**
128
+ If adding iOS or macOS support, open the generated `.podspec` file and configure the required dependencies and deployment targets.
129
+
130
+## Examples
131
+
132
+### Android V2 Embedding Implementation
133
+High-fidelity example of an Android plugin implementing `FlutterPlugin` and `ActivityAware` while maintaining legacy compatibility.
134
+
135
+```java
136
+package com.example.myplugin;
137
+
138
+import androidx.annotation.NonNull;
139
+import io.flutter.embedding.engine.plugins.FlutterPlugin;
140
+import io.flutter.embedding.engine.plugins.activity.ActivityAware;
141
+import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
142
+import io.flutter.plugin.common.MethodCall;
143
+import io.flutter.plugin.common.MethodChannel;
144
+import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
145
+import io.flutter.plugin.common.MethodChannel.Result;
146
+import io.flutter.plugin.common.PluginRegistry.Registrar;
147
+
148
+/** MyPlugin */
149
+public class MyPlugin implements FlutterPlugin, MethodCallHandler, ActivityAware {
150
+ private MethodChannel channel;
151
+
152
+ // Public constructor required for v2 embedding
153
+ public MyPlugin() {}
154
+
155
+ @Override
156
+ public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
157
+ setupChannel(flutterPluginBinding.getBinaryMessenger());
158
+ }
159
+
160
+ // Legacy v1 embedding support
161
+ public static void registerWith(Registrar registrar) {
162
+ MyPlugin plugin = new MyPlugin();
163
+ plugin.setupChannel(registrar.messenger());
164
+ }
165
+
166
+ // Shared initialization logic
167
+ private void setupChannel(BinaryMessenger messenger) {
168
+ channel = new MethodChannel(messenger, "my_plugin");
169
+ channel.setMethodCallHandler(this);
170
+ }
171
+
172
+ @Override
173
+ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
174
+ if (call.method.equals("getPlatformVersion")) {
175
+ result.success("Android " + android.os.Build.VERSION.RELEASE);
176
+ } else {
177
+ result.notImplemented();
178
+ }
179
+ }
180
+
181
+ @Override
182
+ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
183
+ channel.setMethodCallHandler(null);
184
+ }
185
+
186
+ @Override
187
+ public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
188
+ // Handle Activity attachment
189
+ }
190
+
191
+ @Override
192
+ public void onDetachedFromActivityForConfigChanges() {
193
+ // Handle config changes
194
+ }
195
+
196
+ @Override
197
+ public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
198
+ // Handle reattachment
199
+ }
200
+
201
+ @Override
202
+ public void onDetachedFromActivity() {
203
+ // Clean up Activity references
204
+ }
205
+}
206
+```
.agent/skills/flutter-caching-data/SKILL.md
new
+168
@@ -0,0 +1,168 @@
1
+---
2
+name: "flutter-caching-data"
3
+description: "Implements caching strategies for Flutter apps to improve performance and offline support. Use when retaining app data locally to reduce network requests or speed up startup."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:19:54 GMT"
7
+
8
+---
9
+# Implementing Flutter Caching and Offline-First Architectures
10
+
11
+## Contents
12
+- [Selecting a Caching Strategy](#selecting-a-caching-strategy)
13
+- [Implementing Offline-First Data Synchronization](#implementing-offline-first-data-synchronization)
14
+- [Managing File System and SQLite Persistence](#managing-file-system-and-sqlite-persistence)
15
+- [Optimizing UI, Scroll, and Image Caching](#optimizing-ui-scroll-and-image-caching)
16
+- [Caching the FlutterEngine (Android)](#caching-the-flutterengine-android)
17
+- [Workflows](#workflows)
18
+
19
+## Selecting a Caching Strategy
20
+
21
+Apply the appropriate caching mechanism based on the data lifecycle and size requirements.
22
+
23
+* **If storing small, non-critical UI states or preferences:** Use `shared_preferences`.
24
+* **If storing large, structured datasets:** Use on-device databases (SQLite via `sqflite`, Drift, Hive CE, or Isar).
25
+* **If storing binary data or large media:** Use file system caching via `path_provider`.
26
+* **If retaining user session state (navigation, scroll positions):** Implement Flutter's built-in state restoration to sync the Element tree with the engine.
27
+* **If optimizing Android initialization:** Pre-warm and cache the `FlutterEngine`.
28
+
29
+## Implementing Offline-First Data Synchronization
30
+
31
+Design repositories as the single source of truth, combining local databases and remote API clients.
32
+
33
+### Read Operations (Stream Approach)
34
+Yield local data immediately for fast UI rendering, then fetch remote data, update the local cache, and yield the fresh data.
35
+
36
+```dart
37
+Stream<UserProfile> getUserProfile() async* {
38
+ // 1. Yield local cache first
39
+ final localProfile = await _databaseService.fetchUserProfile();
40
+ if (localProfile != null) yield localProfile;
41
+
42
+ // 2. Fetch remote, update cache, yield fresh data
43
+ try {
44
+ final remoteProfile = await _apiClientService.getUserProfile();
45
+ await _databaseService.updateUserProfile(remoteProfile);
46
+ yield remoteProfile;
47
+ } catch (e) {
48
+ // Handle network failure; UI already has local data
49
+ }
50
+}
51
+```
52
+
53
+### Write Operations
54
+Determine the write strategy based on data criticality:
55
+* **If strict server synchronization is required (Online-only):** Attempt the API call first. Only update the local database if the API call succeeds.
56
+* **If offline availability is prioritized (Offline-first):** Write to the local database immediately. Attempt the API call. If the API call fails, flag the local record for background synchronization.
57
+
58
+### Background Synchronization
59
+Add a `synchronized` boolean flag to your data models. Run a periodic background task (e.g., via `workmanager` or a `Timer`) to push unsynchronized local changes to the server.
60
+
61
+## Managing File System and SQLite Persistence
62
+
63
+### File System Caching
64
+Use `path_provider` to locate the correct directory.
65
+* Use `getApplicationDocumentsDirectory()` for persistent data.
66
+* Use `getTemporaryDirectory()` for cache data the OS can clear.
67
+
68
+```dart
69
+Future<File> get _localFile async {
70
+ final directory = await getApplicationDocumentsDirectory();
71
+ return File('${directory.path}/cache.txt');
72
+}
73
+```
74
+
75
+### SQLite Persistence
76
+Use `sqflite` for relational data caching. Always use `whereArgs` to prevent SQL injection.
77
+
78
+```dart
79
+Future<void> updateCachedRecord(Record record) async {
80
+ final db = await database;
81
+ await db.update(
82
+ 'records',
83
+ record.toMap(),
84
+ where: 'id = ?',
85
+ whereArgs: [record.id], // NEVER use string interpolation here
86
+ );
87
+}
88
+```
89
+
90
+## Optimizing UI, Scroll, and Image Caching
91
+
92
+### Image Caching
93
+Image I/O and decompression are expensive.
94
+* Use the `cached_network_image` package to handle file-system caching of remote images.
95
+* **Custom ImageProviders:** If implementing a custom `ImageProvider`, override `createStream()` and `resolveStreamForKey()` instead of the deprecated `resolve()` method.
96
+* **Cache Sizing:** The `ImageCache.maxByteSize` no longer automatically expands for large images. If loading images larger than the default cache size, manually increase `ImageCache.maxByteSize` or subclass `ImageCache` to implement custom eviction logic.
97
+
98
+### Scroll Caching
99
+When configuring caching for scrollable widgets (`ListView`, `GridView`, `Viewport`), use the `scrollCacheExtent` property with a `ScrollCacheExtent` object. Do not use the deprecated `cacheExtent` and `cacheExtentStyle` properties.
100
+
101
+```dart
102
+// Correct implementation
103
+ListView(
104
+ scrollCacheExtent: const ScrollCacheExtent.pixels(500.0),
105
+ children: // ...
106
+)
107
+
108
+Viewport(
109
+ scrollCacheExtent: const ScrollCacheExtent.viewport(0.5),
110
+ slivers: // ...
111
+)
112
+```
113
+
114
+### Widget Caching
115
+* Avoid overriding `operator ==` on `Widget` objects. It causes O(N²) behavior during rebuilds.
116
+* **Exception:** You may override `operator ==` *only* on leaf widgets (no children) where comparing properties is significantly faster than rebuilding, and the properties rarely change.
117
+* Prefer using `const` constructors to allow the framework to short-circuit rebuilds automatically.
118
+
119
+## Caching the FlutterEngine (Android)
120
+
121
+To eliminate the non-trivial warm-up time of a `FlutterEngine` when adding Flutter to an existing Android app, pre-warm and cache the engine.
122
+
123
+1. Instantiate and pre-warm the engine in the `Application` class.
124
+2. Store it in the `FlutterEngineCache`.
125
+3. Retrieve it using `withCachedEngine` in the `FlutterActivity` or `FlutterFragment`.
126
+
127
+```kotlin
128
+// 1. Pre-warm in Application class
129
+val flutterEngine = FlutterEngine(this)
130
+flutterEngine.navigationChannel.setInitialRoute("/cached_route")
131
+flutterEngine.dartExecutor.executeDartEntrypoint(DartEntrypoint.createDefault())
132
+
133
+// 2. Cache the engine
134
+FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine)
135
+
136
+// 3. Use in Activity/Fragment
137
+startActivity(
138
+ FlutterActivity.withCachedEngine("my_engine_id").build(this)
139
+)
140
+```
141
+*Note: You cannot set an initial route via the Activity/Fragment builder when using a cached engine. Set the initial route on the engine's navigation channel before executing the Dart entrypoint.*
142
+
143
+## Workflows
144
+
145
+### Workflow: Implementing an Offline-First Repository
146
+Follow these steps to implement a robust offline-first data layer.
147
+
148
+- [ ] **Task Progress:**
149
+ - [ ] Define the data model with a `synchronized` boolean flag (default `false`).
150
+ - [ ] Implement the local `DatabaseService` (SQLite/Hive) with CRUD operations.
151
+ - [ ] Implement the remote `ApiClientService` for network requests.
152
+ - [ ] Create the `Repository` class combining both services.
153
+ - [ ] Implement the read method returning a `Stream<T>` (yield local, fetch remote, update local, yield remote).
154
+ - [ ] Implement the write method (write local, attempt remote, update `synchronized` flag).
155
+ - [ ] Implement a background sync function to process records where `synchronized == false`.
156
+ - [ ] Run validator -> review errors -> fix (Test offline behavior by disabling network).
157
+
158
+### Workflow: Pre-warming the Android FlutterEngine
159
+Follow these steps to cache the FlutterEngine for seamless Android integration.
160
+
161
+- [ ] **Task Progress:**
162
+ - [ ] Locate the Android `Application` class (create one if it doesn't exist and register in `AndroidManifest.xml`).
163
+ - [ ] Instantiate a new `FlutterEngine`.
164
+ - [ ] (Optional) Set the initial route via `navigationChannel.setInitialRoute()`.
165
+ - [ ] Execute the Dart entrypoint via `dartExecutor.executeDartEntrypoint()`.
166
+ - [ ] Store the engine in `FlutterEngineCache.getInstance().put()`.
167
+ - [ ] Update the target `FlutterActivity` or `FlutterFragment` to use `.withCachedEngine("id")`.
168
+ - [ ] Run validator -> review errors -> fix (Verify no blank screen appears during transition).
.agent/skills/flutter-embedding-native-views/SKILL.md
new
+197
@@ -0,0 +1,197 @@
1
+---
2
+name: "flutter-embedding-native-views"
3
+description: "Embeds native Android, iOS, or macOS views into a Flutter app. Use when integrating complex native components like maps or web views."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:20:28 GMT"
7
+
8
+---
9
+# Integrating Platform Views and Web Content
10
+
11
+## Contents
12
+- [Platform Views Architecture](#platform-views-architecture)
13
+- [Web Embedding Architecture](#web-embedding-architecture)
14
+- [Workflow: Implementing Android Platform Views](#workflow-implementing-android-platform-views)
15
+- [Workflow: Implementing iOS Platform Views](#workflow-implementing-ios-platform-views)
16
+- [Workflow: Embedding Flutter in Web Applications](#workflow-embedding-flutter-in-web-applications)
17
+- [Examples](#examples)
18
+
19
+## Platform Views Architecture
20
+
21
+Platform Views allow embedding native views (Android, iOS, macOS) directly into a Flutter application, enabling the application of transforms, clips, and opacity from Dart.
22
+
23
+### Android Implementations (API 23+)
24
+Choose the appropriate implementation based on your performance and fidelity requirements:
25
+* **Hybrid Composition:** Renders Flutter content into a texture and uses `SurfaceFlinger` to compose both.
26
+ * *Pros:* Best performance and fidelity for Android views.
27
+ * *Cons:* Lowers overall application FPS. Certain Flutter widget transformations will not work.
28
+* **Texture Layer (Texture Layer Hybrid Composition):** Renders Platform Views into a texture. Flutter draws them via the texture and renders its own content directly into a Surface.
29
+ * *Pros:* Best performance for Flutter rendering. All transformations work correctly.
30
+ * *Cons:* Quick scrolling (e.g., WebViews) can be janky. `SurfaceView` is problematic (breaks accessibility). Text magnifiers break unless Flutter is rendered into a `TextureView`.
31
+
32
+### iOS & macOS Implementations
33
+* **iOS:** Uses Hybrid Composition exclusively. The native `UIView` is appended to the view hierarchy.
34
+ * *Limitations:* `ShaderMask` and `ColorFiltered` widgets are not supported. `BackdropFilter` has composition limitations.
35
+* **macOS:** Uses Hybrid Composition (`NSView`).
36
+ * *Limitations:* Not fully functional in current releases (e.g., gesture support is unavailable).
37
+
38
+### Performance Mitigation
39
+Mitigate performance drops during complex Dart animations by rendering a screenshot of the native view as a placeholder texture while the animation runs.
40
+
41
+## Web Embedding Architecture
42
+
43
+Embed Flutter into existing web applications (Vanilla JS, React, Angular, etc.) using either Full Page mode or Embedded (Multi-view) mode.
44
+
45
+* **Full Page Mode:** Flutter takes over the entire browser window. Use an `iframe` if you need to constrain the Flutter app without modifying the Flutter bootstrap process.
46
+* **Embedded Mode (Multi-view):** Render Flutter into specific HTML elements (`div`s). Requires `multiViewEnabled: true` during engine initialization.
47
+ * Manage views from JavaScript using `app.addView()` and `app.removeView()`.
48
+ * In Dart, replace `runApp` with `runWidget`.
49
+ * Manage the dynamic list of views using `WidgetsBinding.instance.platformDispatcher.views` and render them using `ViewCollection` and `View` widgets.
50
+
51
+## Workflow: Implementing Android Platform Views
52
+
53
+Follow this sequential workflow to implement a Platform View on Android.
54
+
55
+**Task Progress:**
56
+- [ ] 1. Determine the composition mode (Hybrid vs. Texture Layer).
57
+- [ ] 2. Implement the Dart widget.
58
+- [ ] 3. Implement the native Android View and Factory.
59
+- [ ] 4. Register the Platform View in the Android host.
60
+- [ ] 5. Run validator -> review rendering -> fix manual invalidation issues.
61
+
62
+### 1. Dart Implementation
63
+If using **Hybrid Composition**, use `PlatformViewLink`, `AndroidViewSurface`, and `PlatformViewsService.initSurfaceAndroidView`.
64
+If using **Texture Layer**, use the `AndroidView` widget.
65
+
66
+### 2. Native Implementation
67
+Create a class implementing `io.flutter.plugin.platform.PlatformView` that returns your native `android.view.View`.
68
+Create a factory extending `PlatformViewFactory` to instantiate your view.
69
+
70
+### 3. Registration
71
+Register the factory in your `MainActivity.kt` (or plugin) using `flutterEngine.platformViewsController.registry.registerViewFactory`.
72
+
73
+*Note: If your native view uses `SurfaceView` or `SurfaceTexture`, manually call `invalidate` on the View or its parent when content changes, as they do not invalidate themselves automatically.*
74
+
75
+## Workflow: Implementing iOS Platform Views
76
+
77
+Follow this sequential workflow to implement a Platform View on iOS.
78
+
79
+**Task Progress:**
80
+- [ ] 1. Implement the Dart widget using `UiKitView`.
81
+- [ ] 2. Implement the native iOS View (`FlutterPlatformView`) and Factory (`FlutterPlatformViewFactory`).
82
+- [ ] 3. Register the Platform View in `AppDelegate.swift` or the plugin registrar.
83
+- [ ] 4. Run validator -> review composition limitations -> fix unsupported filters.
84
+
85
+## Workflow: Embedding Flutter in Web Applications
86
+
87
+Follow this sequential workflow to embed Flutter into an existing web DOM.
88
+
89
+**Task Progress:**
90
+- [ ] 1. Update `flutter_bootstrap.js` to enable multi-view.
91
+- [ ] 2. Update `main.dart` to use `runWidget` and `ViewCollection`.
92
+- [ ] 3. Implement JavaScript logic to add/remove host elements.
93
+- [ ] 4. Run validator -> review view constraints -> fix CSS conflicts.
94
+
95
+### 1. JavaScript Configuration
96
+In `flutter_bootstrap.js`, initialize the engine with `multiViewEnabled: true`.
97
+Use the returned `app` object to add views: `app.addView({ hostElement: document.getElementById('my-div') })`.
98
+
99
+### 2. Dart Configuration
100
+Replace `runApp()` with `runWidget()`.
101
+Create a root widget that listens to `WidgetsBindingObserver.didChangeMetrics`.
102
+Map over `WidgetsBinding.instance.platformDispatcher.views` to create a `View` widget for each attached `FlutterView`, and wrap them all in a `ViewCollection`.
103
+
104
+## Examples
105
+
106
+### Example: Android Texture Layer (Dart)
107
+```dart
108
+import 'package:flutter/material.dart';
109
+import 'package:flutter/services.dart';
110
+
111
+class NativeAndroidView extends StatelessWidget {
112
+ @override
113
+ Widget build(BuildContext context) {
114
+ const String viewType = 'my_native_view';
115
+ final Map<String, dynamic> creationParams = <String, dynamic>{};
116
+
117
+ return AndroidView(
118
+ viewType: viewType,
119
+ layoutDirection: TextDirection.ltr,
120
+ creationParams: creationParams,
121
+ creationParamsCodec: const StandardMessageCodec(),
122
+ );
123
+ }
124
+}
125
+```
126
+
127
+### Example: Web Multi-View Initialization (JavaScript)
128
+```javascript
129
+_flutter.loader.load({
130
+ onEntrypointLoaded: async function(engineInitializer) {
131
+ let engine = await engineInitializer.initializeEngine({
132
+ multiViewEnabled: true,
133
+ });
134
+ let app = await engine.runApp();
135
+
136
+ // Add a view to a specific DOM element
137
+ let viewId = app.addView({
138
+ hostElement: document.querySelector('#flutter-host-container'),
139
+ initialData: { customData: 'Hello from JS' }
140
+ });
141
+ }
142
+});
143
+```
144
+
145
+### Example: Web Multi-View Root Widget (Dart)
146
+```dart
147
+import 'dart:ui' show FlutterView;
148
+import 'package:flutter/widgets.dart';
149
+
150
+void main() {
151
+ runWidget(MultiViewApp(viewBuilder: (context) => const MyEmbeddedWidget()));
152
+}
153
+
154
+class MultiViewApp extends StatefulWidget {
155
+ final WidgetBuilder viewBuilder;
156
+ const MultiViewApp({super.key, required this.viewBuilder});
157
+
158
+ @override
159
+ State<MultiViewApp> createState() => _MultiViewAppState();
160
+}
161
+
162
+class _MultiViewAppState extends State<MultiViewApp> with WidgetsBindingObserver {
163
+ Map<Object, Widget> _views = {};
164
+
165
+ @override
166
+ void initState() {
167
+ super.initState();
168
+ WidgetsBinding.instance.addObserver(this);
169
+ _updateViews();
170
+ }
171
+
172
+ @override
173
+ void didChangeMetrics() => _updateViews();
174
+
175
+ void _updateViews() {
176
+ final newViews = <Object, Widget>{};
177
+ for (final FlutterView view in WidgetsBinding.instance.platformDispatcher.views) {
178
+ newViews[view.viewId] = _views[view.viewId] ?? View(
179
+ view: view,
180
+ child: Builder(builder: widget.viewBuilder),
181
+ );
182
+ }
183
+ setState(() => _views = newViews);
184
+ }
185
+
186
+ @override
187
+ void dispose() {
188
+ WidgetsBinding.instance.removeObserver(this);
189
+ super.dispose();
190
+ }
191
+
192
+ @override
193
+ Widget build(BuildContext context) {
194
+ return ViewCollection(views: _views.values.toList(growable: false));
195
+ }
196
+}
197
+```
.agent/skills/flutter-handling-concurrency/SKILL.md
new
+179
@@ -0,0 +1,179 @@
1
+---
2
+name: "flutter-handling-concurrency"
3
+description: "Executes long-running tasks in background isolates to keep the UI responsive. Use when performing heavy computations or parsing large datasets."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:23:14 GMT"
7
+
8
+---
9
+# Managing Dart Concurrency and Isolates
10
+
11
+## Contents
12
+- [Core Concepts](#core-concepts)
13
+- [Decision Matrix: Async vs. Isolates](#decision-matrix-async-vs-isolates)
14
+- [Workflows](#workflows)
15
+ - [Implementing Standard Asynchronous UI](#implementing-standard-asynchronous-ui)
16
+ - [Offloading Short-Lived Heavy Computation](#offloading-short-lived-heavy-computation)
17
+ - [Establishing Long-Lived Worker Isolates](#establishing-long-lived-worker-isolates)
18
+- [Examples](#examples)
19
+
20
+## Core Concepts
21
+
22
+Dart utilizes a single-threaded execution model driven by an Event Loop (comparable to the iOS main loop). By default, all Flutter application code runs on the Main Isolate.
23
+
24
+* **Asynchronous Operations (`async`/`await`):** Use for non-blocking I/O tasks (network requests, file access). The Event Loop continues processing other events while waiting for the `Future` to complete.
25
+* **Isolates:** Dart's implementation of lightweight threads. Isolates possess their own isolated memory and do not share state. They communicate exclusively via message passing.
26
+* **Main Isolate:** The default thread where UI rendering and event handling occur. Blocking this isolate causes UI freezing (jank).
27
+* **Worker Isolate:** A spawned isolate used to offload CPU-bound tasks (e.g., decoding large JSON blobs) to prevent Main Isolate blockage.
28
+
29
+## Decision Matrix: Async vs. Isolates
30
+
31
+Apply the following conditional logic to determine the correct concurrency approach:
32
+
33
+* **If** the task is I/O bound (e.g., HTTP request, database read) -> **Use `async`/`await`** on the Main Isolate.
34
+* **If** the task is CPU-bound but executes quickly (< 16ms) -> **Use `async`/`await`** on the Main Isolate.
35
+* **If** the task is CPU-bound, takes significant time, and runs once (e.g., parsing a massive JSON payload) -> **Use `Isolate.run()`**.
36
+* **If** the task requires continuous or repeated background processing with multiple messages passed over time -> **Use `Isolate.spawn()` with `ReceivePort` and `SendPort`**.
37
+
38
+## Workflows
39
+
40
+### Implementing Standard Asynchronous UI
41
+
42
+Use this workflow to fetch and display non-blocking asynchronous data.
43
+
44
+**Task Progress:**
45
+- [ ] Mark the data-fetching function with the `async` keyword.
46
+- [ ] Return a `Future<T>` from the function.
47
+- [ ] Use the `await` keyword to yield execution until the operation completes.
48
+- [ ] Wrap the UI component in a `FutureBuilder<T>` (or `StreamBuilder` for streams).
49
+- [ ] Handle `ConnectionState.waiting`, `hasError`, and `hasData` states within the builder.
50
+- [ ] Run validator -> review UI for loading indicators -> fix missing states.
51
+
52
+### Offloading Short-Lived Heavy Computation
53
+
54
+Use this workflow for one-off, CPU-intensive tasks using Dart 2.19+.
55
+
56
+**Task Progress:**
57
+- [ ] Identify the CPU-bound operation blocking the Main Isolate.
58
+- [ ] Extract the computation into a standalone callback function.
59
+- [ ] Ensure the callback function signature accepts exactly one required, unnamed argument (as per specific architectural constraints).
60
+- [ ] Invoke `Isolate.run()` passing the callback.
61
+- [ ] `await` the result of `Isolate.run()` in the Main Isolate.
62
+- [ ] Assign the returned value to the application state.
63
+
64
+### Establishing Long-Lived Worker Isolates
65
+
66
+Use this workflow for persistent background processes requiring continuous bidirectional communication.
67
+
68
+**Task Progress:**
69
+- [ ] Instantiate a `ReceivePort` on the Main Isolate to listen for messages.
70
+- [ ] Spawn the worker isolate using `Isolate.spawn()`, passing the `ReceivePort.sendPort` as the initial message.
71
+- [ ] In the worker isolate, instantiate its own `ReceivePort`.
72
+- [ ] Send the worker's `SendPort` back to the Main Isolate via the initial port.
73
+- [ ] Store the worker's `SendPort` in the Main Isolate for future message dispatching.
74
+- [ ] Implement listeners on both `ReceivePort` instances to handle incoming messages.
75
+- [ ] Run validator -> review memory leaks -> ensure ports are closed when the isolate is no longer needed.
76
+
77
+## Examples
78
+
79
+### Example 1: Asynchronous UI with FutureBuilder
80
+
81
+```dart
82
+// 1. Define the async operation
83
+Future<String> fetchUserData() async {
84
+ await Future.delayed(const Duration(seconds: 2)); // Simulate network I/O
85
+ return "User Data Loaded";
86
+}
87
+
88
+// 2. Consume in the UI
89
+Widget build(BuildContext context) {
90
+ return FutureBuilder<String>(
91
+ future: fetchUserData(),
92
+ builder: (context, snapshot) {
93
+ if (snapshot.connectionState == ConnectionState.waiting) {
94
+ return const CircularProgressIndicator();
95
+ } else if (snapshot.hasError) {
96
+ return Text('Error: ${snapshot.error}');
97
+ } else {
98
+ return Text('Result: ${snapshot.data}');
99
+ }
100
+ },
101
+ );
102
+}
103
+```
104
+
105
+### Example 2: Short-Lived Isolate (`Isolate.run`)
106
+
107
+```dart
108
+import 'dart:isolate';
109
+import 'dart:convert';
110
+
111
+// 1. Define the heavy computation callback
112
+// Note: Adhering to the strict single-argument signature requirement.
113
+List<dynamic> decodeHeavyJson(String jsonString) {
114
+ return jsonDecode(jsonString) as List<dynamic>;
115
+}
116
+
117
+// 2. Offload to a worker isolate
118
+Future<List<dynamic>> processDataInBackground(String rawJson) async {
119
+ // Isolate.run spawns the isolate, runs the computation, returns the value, and exits.
120
+ final result = await Isolate.run(() => decodeHeavyJson(rawJson));
121
+ return result;
122
+}
123
+```
124
+
125
+### Example 3: Long-Lived Isolate (`ReceivePort` / `SendPort`)
126
+
127
+```dart
128
+import 'dart:isolate';
129
+
130
+class WorkerManager {
131
+ late SendPort _workerSendPort;
132
+ final ReceivePort _mainReceivePort = ReceivePort();
133
+ Isolate? _isolate;
134
+
135
+ Future<void> initialize() async {
136
+ // 1. Spawn isolate and pass the Main Isolate's SendPort
137
+ _isolate = await Isolate.spawn(_workerEntry, _mainReceivePort.sendPort);
138
+
139
+ // 2. Listen for messages from the Worker Isolate
140
+ _mainReceivePort.listen((message) {
141
+ if (message is SendPort) {
142
+ // First message is the Worker's SendPort
143
+ _workerSendPort = message;
144
+ _startCommunication();
145
+ } else {
146
+ // Subsequent messages are data payloads
147
+ print('Main Isolate received: $message');
148
+ }
149
+ });
150
+ }
151
+
152
+ void _startCommunication() {
153
+ // Send data to the worker
154
+ _workerSendPort.send("Process this data");
155
+ }
156
+
157
+ // 3. Worker Isolate Entry Point
158
+ static void _workerEntry(SendPort mainSendPort) {
159
+ final workerReceivePort = ReceivePort();
160
+
161
+ // Send the Worker's SendPort back to the Main Isolate
162
+ mainSendPort.send(workerReceivePort.sendPort);
163
+
164
+ // Listen for incoming tasks
165
+ workerReceivePort.listen((message) {
166
+ print('Worker Isolate received: $message');
167
+
168
+ // Perform work and send result back
169
+ final result = "Processed: $message";
170
+ mainSendPort.send(result);
171
+ });
172
+ }
173
+
174
+ void dispose() {
175
+ _mainReceivePort.close();
176
+ _isolate?.kill();
177
+ }
178
+}
179
+```
.agent/skills/flutter-handling-http-and-json/SKILL.md
new
+181
@@ -0,0 +1,181 @@
1
+---
2
+name: "flutter-handling-http-and-json"
3
+description: "Executes HTTP requests and handles JSON serialization in a Flutter app. Use when integrating with REST APIs or parsing structured data from external sources."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:18:44 GMT"
7
+
8
+---
9
+# Handling HTTP and JSON
10
+
11
+## Contents
12
+- [Core Guidelines](#core-guidelines)
13
+- [Workflow: Executing HTTP Operations](#workflow-executing-http-operations)
14
+- [Workflow: Implementing JSON Serialization](#workflow-implementing-json-serialization)
15
+- [Workflow: Parsing Large JSON in the Background](#workflow-parsing-large-json-in-the-background)
16
+- [Examples](#examples)
17
+
18
+## Core Guidelines
19
+
20
+- **Enforce HTTPS:** iOS and Android disable cleartext (HTTP) connections by default. Always use HTTPS endpoints. If HTTP is strictly required for debugging, configure `network_security_config.xml` (Android) and `NSAppTransportSecurity` (iOS).
21
+- **Construct URIs Safely:** Always use `Uri.https(authority, unencodedPath, [queryParameters])` to safely build URLs. This handles encoding and formatting reliably, preventing string concatenation errors.
22
+- **Handle Status Codes:** Always validate the `http.Response.statusCode`. Treat `200` (OK) and `201` (Created) as success. Throw explicit exceptions for other codes (do not return `null`).
23
+- **Prevent UI Jank:** Move expensive JSON parsing operations (taking >16ms) to a background isolate using the `compute()` function.
24
+- **Structured AI Output:** When integrating LLMs, enforce reliable JSON output by specifying a strict JSON schema in the system prompt and setting the response MIME type to `application/json`.
25
+
26
+## Workflow: Executing HTTP Operations
27
+
28
+Use this workflow to implement network requests using the `http` package.
29
+
30
+**Task Progress:**
31
+- [ ] Add the `http` package to `pubspec.yaml`.
32
+- [ ] Configure platform permissions (Internet permission in `AndroidManifest.xml` and macOS `.entitlements`).
33
+- [ ] Construct the target `Uri`.
34
+- [ ] Execute the HTTP method.
35
+- [ ] Validate the response and parse the JSON payload.
36
+
37
+**Conditional Implementation:**
38
+- **If fetching data (GET):** Use `http.get(uri)`.
39
+- **If sending new data (POST):** Use `http.post(uri, headers: {...}, body: jsonEncode(data))`. Ensure `Content-Type` is `application/json; charset=UTF-8`.
40
+- **If updating data (PUT):** Use `http.put(uri, headers: {...}, body: jsonEncode(data))`.
41
+- **If deleting data (DELETE):** Use `http.delete(uri, headers: {...})`.
42
+
43
+**Feedback Loop: Validation & Error Handling**
44
+1. Run the HTTP request.
45
+2. Check `response.statusCode`.
46
+3. If `200` or `201`, call `jsonDecode(response.body)` and map to a Dart object.
47
+4. If any other code, throw an `Exception('Failed to load/update/delete resource')`.
48
+5. Review errors -> fix endpoint, headers, or payload structure.
49
+
50
+## Workflow: Implementing JSON Serialization
51
+
52
+Choose the serialization strategy based on project complexity.
53
+
54
+**Conditional Implementation:**
55
+- **If building a small prototype or simple models:** Use manual serialization with `dart:convert`.
56
+- **If building a production app with complex/nested models:** Use code generation with `json_serializable`.
57
+
58
+### Manual Serialization Setup
59
+**Task Progress:**
60
+- [ ] Import `dart:convert`.
61
+- [ ] Define the Model class with `final` properties.
62
+- [ ] Implement a `factory Model.fromJson(Map<String, dynamic> json)` constructor.
63
+- [ ] Implement a `Map<String, dynamic> toJson()` method.
64
+
65
+### Code Generation Setup (`json_serializable`)
66
+**Task Progress:**
67
+- [ ] Add dependencies: `flutter pub add json_annotation` and `flutter pub add -d build_runner json_serializable`.
68
+- [ ] Import `json_annotation.dart` in the model file.
69
+- [ ] Add the `part 'model_name.g.dart';` directive.
70
+- [ ] Annotate the class with `@JsonSerializable()`. Use `explicitToJson: true` if the class contains nested models.
71
+- [ ] Define the `fromJson` factory and `toJson` method delegating to the generated functions.
72
+- [ ] Run the generator: `dart run build_runner build --delete-conflicting-outputs`.
73
+
74
+## Workflow: Parsing Large JSON in the Background
75
+
76
+Use this workflow to prevent frame drops when parsing large JSON payloads (e.g., lists of 1000+ items).
77
+
78
+**Task Progress:**
79
+- [ ] Create a top-level or static function that takes a `String` (the response body) and returns the parsed Dart object (e.g., `List<Model>`).
80
+- [ ] Inside the function, call `jsonDecode` and map the results to the Model class.
81
+- [ ] In the HTTP fetch method, pass the top-level parsing function and the `response.body` to Flutter's `compute()` function.
82
+
83
+## Examples
84
+
85
+### Example 1: HTTP GET with Manual Serialization
86
+```dart
87
+import 'dart:convert';
88
+import 'package:http/http.dart' as http;
89
+
90
+class Album {
91
+ final int id;
92
+ final String title;
93
+
94
+ const Album({required this.id, required this.title});
95
+
96
+ factory Album.fromJson(Map<String, dynamic> json) {
97
+ return Album(
98
+ id: json['id'] as int,
99
+ title: json['title'] as String,
100
+ );
101
+ }
102
+}
103
+
104
+Future<Album> fetchAlbum() async {
105
+ final uri = Uri.https('jsonplaceholder.typicode.com', '/albums/1');
106
+ final response = await http.get(uri);
107
+
108
+ if (response.statusCode == 200) {
109
+ return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
110
+ } else {
111
+ throw Exception('Failed to load album');
112
+ }
113
+}
114
+```
115
+
116
+### Example 2: HTTP POST Request
117
+```dart
118
+Future<Album> createAlbum(String title) async {
119
+ final uri = Uri.https('jsonplaceholder.typicode.com', '/albums');
120
+ final response = await http.post(
121
+ uri,
122
+ headers: <String, String>{
123
+ 'Content-Type': 'application/json; charset=UTF-8',
124
+ },
125
+ body: jsonEncode(<String, String>{'title': title}),
126
+ );
127
+
128
+ if (response.statusCode == 201) {
129
+ return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
130
+ } else {
131
+ throw Exception('Failed to create album.');
132
+ }
133
+}
134
+```
135
+
136
+### Example 3: Background Parsing with `compute`
137
+```dart
138
+import 'dart:convert';
139
+import 'package:flutter/foundation.dart';
140
+import 'package:http/http.dart' as http;
141
+
142
+// 1. Top-level function for parsing
143
+List<Photo> parsePhotos(String responseBody) {
144
+ final parsed = (jsonDecode(responseBody) as List<Object?>)
145
+ .cast<Map<String, Object?>>();
146
+ return parsed.map<Photo>(Photo.fromJson).toList();
147
+}
148
+
149
+// 2. Fetch function using compute
150
+Future<List<Photo>> fetchPhotos(http.Client client) async {
151
+ final uri = Uri.https('jsonplaceholder.typicode.com', '/photos');
152
+ final response = await client.get(uri);
153
+
154
+ if (response.statusCode == 200) {
155
+ // Run parsePhotos in a separate isolate
156
+ return compute(parsePhotos, response.body);
157
+ } else {
158
+ throw Exception('Failed to load photos');
159
+ }
160
+}
161
+```
162
+
163
+### Example 4: Code Generation (`json_serializable`)
164
+```dart
165
+import 'package:json_annotation/json_annotation.dart';
166
+
167
+part 'user.g.dart';
168
+
169
+@JsonSerializable(explicitToJson: true)
170
+class User {
171
+ final String name;
172
+
173
+ @JsonKey(name: 'registration_date_millis')
174
+ final int registrationDateMillis;
175
+
176
+ User(this.name, this.registrationDateMillis);
177
+
178
+ factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
179
+ Map<String, dynamic> toJson() => _$UserToJson(this);
180
+}
181
+```
.agent/skills/flutter-implementing-navigation-and-routing/SKILL.md
new
+205
@@ -0,0 +1,205 @@
1
+---
2
+name: "flutter-implementing-navigation-and-routing"
3
+description: "Handles routing, navigation, and deep linking in a Flutter application. Use when moving between screens or setting up URL-based navigation."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:30:17 GMT"
7
+
8
+---
9
+# Implementing Navigation and Routing in Flutter
10
+
11
+## Contents
12
+- [Core Concepts](#core-concepts)
13
+- [Implementing Imperative Navigation](#implementing-imperative-navigation)
14
+- [Implementing Declarative Navigation](#implementing-declarative-navigation)
15
+- [Implementing Nested Navigation](#implementing-nested-navigation)
16
+- [Workflows](#workflows)
17
+- [Examples](#examples)
18
+
19
+## Core Concepts
20
+
21
+- **Routes:** In Flutter, screens and pages are referred to as *routes*. A route is simply a widget. This is equivalent to an `Activity` in Android or a `ViewController` in iOS.
22
+- **Navigator vs. Router:**
23
+ - Use `Navigator` (Imperative) for small applications without complex deep linking requirements. It manages a stack of `Route` objects.
24
+ - Use `Router` (Declarative) for applications with advanced navigation, web URL synchronization, and specific deep linking requirements.
25
+- **Deep Linking:** Allows an app to open directly to a specific location based on a URL. Supported on iOS, Android, and Web. Web requires no additional setup.
26
+- **Named Routes:** Avoid using named routes (`MaterialApp.routes` and `Navigator.pushNamed`) for most applications. They have rigid deep linking behavior and do not support the browser forward button. Use a routing package like `go_router` instead.
27
+
28
+## Implementing Imperative Navigation
29
+
30
+Use the `Navigator` widget to push and pop routes using platform-specific transition animations (`MaterialPageRoute` or `CupertinoPageRoute`).
31
+
32
+### Pushing and Popping
33
+- Navigate to a new route using `Navigator.push(context, route)`.
34
+- Return to the previous route using `Navigator.pop(context)`.
35
+- Use `Navigator.pushReplacement()` to replace the current route, or `Navigator.pushAndRemoveUntil()` to clear the stack based on a condition.
36
+
37
+### Passing and Returning Data
38
+- **Sending Data:** Pass data directly into the constructor of the destination widget. Alternatively, pass data via the `settings: RouteSettings(arguments: data)` parameter of the `PageRoute` and extract it using `ModalRoute.of(context)!.settings.arguments`.
39
+- **Returning Data:** Pass the return value to the `pop` method: `Navigator.pop(context, resultData)`. Await the result on the pushing side: `final result = await Navigator.push(...)`.
40
+
41
+## Implementing Declarative Navigation
42
+
43
+For apps requiring deep linking, web URL support, or complex routing, implement the `Router` API via a declarative routing package like `go_router`.
44
+
45
+- Switch from `MaterialApp` to `MaterialApp.router`.
46
+- Define a router configuration that parses route paths and configures the `Navigator` automatically.
47
+- Navigate using package-specific APIs (e.g., `context.go('/path')`).
48
+- **Page-backed vs. Pageless Routes:** Declarative routes are *page-backed* (deep-linkable). Imperative pushes (e.g., dialogs, bottom sheets) are *pageless*. Removing a page-backed route automatically removes all subsequent pageless routes.
49
+
50
+## Implementing Nested Navigation
51
+
52
+Implement nested navigation to manage a sub-flow of screens (e.g., a multi-step setup process or persistent bottom navigation tabs) independently from the top-level global navigator.
53
+
54
+- Instantiate a new `Navigator` widget inside the host widget.
55
+- Assign a `GlobalKey<NavigatorState>` to the nested `Navigator` to control it programmatically.
56
+- Implement the `onGenerateRoute` callback within the nested `Navigator` to resolve sub-routes.
57
+- Intercept hardware back button presses using `PopScope` to prevent the top-level navigator from popping the entire nested flow prematurely.
58
+
59
+## Workflows
60
+
61
+### Workflow: Standard Screen Transition
62
+Copy this checklist to track progress when implementing a basic screen transition:
63
+- [ ] Create the destination widget (Route).
64
+- [ ] Define required data parameters in the destination widget's constructor.
65
+- [ ] Implement `Navigator.push()` in the source widget.
66
+- [ ] Wrap the destination widget in a `MaterialPageRoute` or `CupertinoPageRoute`.
67
+- [ ] Implement `Navigator.pop()` in the destination widget to return.
68
+
69
+### Workflow: Implementing Deep-Linkable Routing
70
+Use this conditional workflow when setting up app-wide routing:
71
+- [ ] **If** the app is simple and requires no deep linking:
72
+ - [ ] Use standard `MaterialApp` and `Navigator.push()`.
73
+- [ ] **If** the app requires deep linking, web support, or complex flows:
74
+ - [ ] Add the `go_router` package.
75
+ - [ ] Change `MaterialApp` to `MaterialApp.router`.
76
+ - [ ] Define the `GoRouter` configuration with all top-level routes.
77
+ - [ ] Replace `Navigator.push()` with `context.go()` or `context.push()`.
78
+
79
+### Workflow: Creating a Nested Navigation Flow
80
+Run this workflow when building a multi-step sub-flow (e.g., IoT device setup):
81
+- [ ] Define string constants for the nested route paths.
82
+- [ ] Create a `GlobalKey<NavigatorState>` in the host widget's state.
83
+- [ ] Return a `Navigator` widget in the host's `build` method, passing the key.
84
+- [ ] Implement `onGenerateRoute` in the nested `Navigator` to map string paths to specific step widgets.
85
+- [ ] Wrap the host `Scaffold` in a `PopScope` to handle back-button interceptions (e.g., prompting "Are you sure you want to exit setup?").
86
+- [ ] Use `navigatorKey.currentState!.pushNamed()` to advance steps within the flow.
87
+
88
+## Examples
89
+
90
+### Example: Passing Data via Constructor (Imperative)
91
+
92
+```dart
93
+// 1. Define the data model
94
+class Todo {
95
+ final String title;
96
+ final String description;
97
+ const Todo(this.title, this.description);
98
+}
99
+
100
+// 2. Source Screen
101
+class TodosScreen extends StatelessWidget {
102
+ final List<Todo> todos;
103
+ const TodosScreen({super.key, required this.todos});
104
+
105
+ @override
106
+ Widget build(BuildContext context) {
107
+ return Scaffold(
108
+ appBar: AppBar(title: const Text('Todos')),
109
+ body: ListView.builder(
110
+ itemCount: todos.length,
111
+ itemBuilder: (context, index) {
112
+ return ListTile(
113
+ title: Text(todos[index].title),
114
+ onTap: () {
115
+ // Push and pass data via constructor
116
+ Navigator.push(
117
+ context,
118
+ MaterialPageRoute(
119
+ builder: (context) => DetailScreen(todo: todos[index]),
120
+ ),
121
+ );
122
+ },
123
+ );
124
+ },
125
+ ),
126
+ );
127
+ }
128
+}
129
+
130
+// 3. Destination Screen
131
+class DetailScreen extends StatelessWidget {
132
+ final Todo todo;
133
+ const DetailScreen({super.key, required this.todo});
134
+
135
+ @override
136
+ Widget build(BuildContext context) {
137
+ return Scaffold(
138
+ appBar: AppBar(title: Text(todo.title)),
139
+ body: Padding(
140
+ padding: const EdgeInsets.all(16),
141
+ child: Text(todo.description),
142
+ ),
143
+ );
144
+ }
145
+}
146
+```
147
+
148
+### Example: Nested Navigation Flow
149
+
150
+```dart
151
+class SetupFlow extends StatefulWidget {
152
+ final String initialRoute;
153
+ const SetupFlow({super.key, required this.initialRoute});
154
+
155
+ @override
156
+ State<SetupFlow> createState() => _SetupFlowState();
157
+}
158
+
159
+class _SetupFlowState extends State<SetupFlow> {
160
+ final _navigatorKey = GlobalKey<NavigatorState>();
161
+
162
+ void _exitSetup() => Navigator.of(context).pop();
163
+
164
+ @override
165
+ Widget build(BuildContext context) {
166
+ return PopScope(
167
+ canPop: false,
168
+ onPopInvokedWithResult: (didPop, _) async {
169
+ if (didPop) return;
170
+ // Intercept back button to prevent accidental exit
171
+ _exitSetup();
172
+ },
173
+ child: Scaffold(
174
+ appBar: AppBar(title: const Text('Setup')),
175
+ body: Navigator(
176
+ key: _navigatorKey,
177
+ initialRoute: widget.initialRoute,
178
+ onGenerateRoute: _onGenerateRoute,
179
+ ),
180
+ ),
181
+ );
182
+ }
183
+
184
+ Route<Widget> _onGenerateRoute(RouteSettings settings) {
185
+ Widget page;
186
+ switch (settings.name) {
187
+ case 'step1':
188
+ page = StepOnePage(
189
+ onComplete: () => _navigatorKey.currentState!.pushNamed('step2'),
190
+ );
191
+ break;
192
+ case 'step2':
193
+ page = StepTwoPage(onComplete: _exitSetup);
194
+ break;
195
+ default:
196
+ throw StateError('Unexpected route name: ${settings.name}!');
197
+ }
198
+
199
+ return MaterialPageRoute(
200
+ builder: (context) => page,
201
+ settings: settings,
202
+ );
203
+ }
204
+}
205
+```
.agent/skills/flutter-improving-accessibility/SKILL.md
new
+110
@@ -0,0 +1,110 @@
1
+---
2
+name: "flutter-improving-accessibility"
3
+description: "Configures a Flutter app to support assistive technologies like Screen Readers. Use when ensuring an application is usable for people with disabilities."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:17:37 GMT"
7
+
8
+---
9
+# Implementing Flutter Accessibility
10
+
11
+## Contents
12
+- [UI Design and Styling](#ui-design-and-styling)
13
+- [Accessibility Widgets](#accessibility-widgets)
14
+- [Web Accessibility](#web-accessibility)
15
+- [Adaptive and Responsive Design](#adaptive-and-responsive-design)
16
+- [Workflows](#workflows)
17
+- [Examples](#examples)
18
+
19
+## UI Design and Styling
20
+Design layouts to accommodate dynamic scaling and high visibility. Flutter automatically calculates font sizes based on OS-level accessibility settings.
21
+
22
+* **Font Scaling:** Ensure layouts provide sufficient room to render all contents when font sizes are increased to their maximum OS settings. Avoid hardcoding fixed heights on text containers.
23
+* **Color Contrast:** Maintain a contrast ratio of at least 4.5:1 for small text and 3.0:1 for large text (18pt+ regular or 14pt+ bold) to meet W3C standards.
24
+* **Tap Targets:** Enforce a minimum tap target size of 48x48 logical pixels to accommodate users with limited dexterity.
25
+
26
+## Accessibility Widgets
27
+Utilize Flutter's catalog of accessibility widgets to manipulate the semantics tree exposed to assistive technologies (like TalkBack or VoiceOver).
28
+
29
+* **`Semantics`**: Use this to annotate the widget tree with a description of the meaning of the widgets. Assign specific roles using the `SemanticsRole` enum (e.g., button, link, heading) when building custom components.
30
+* **`MergeSemantics`**: Wrap composite widgets to merge the semantics of all descendants into a single selectable node for screen readers.
31
+* **`ExcludeSemantics`**: Use this to drop the semantics of all descendants, hiding redundant or purely decorative sub-widgets from accessibility tools.
32
+
33
+## Web Accessibility
34
+Flutter web renders UI on a single canvas, requiring a specialized DOM layer to expose structure to browsers.
35
+
36
+* **Enable Semantics:** Web accessibility is disabled by default for performance. Users can enable it via an invisible button (`aria-label="Enable accessibility"`).
37
+* **Programmatic Enablement:** If building a web-first application requiring default accessibility, force the semantics tree generation at startup.
38
+* **Semantic Roles:** Rely on standard widgets (`TabBar`, `MenuAnchor`, `Table`) for automatic ARIA role mapping. For custom components, explicitly assign `SemanticsRole` values to ensure screen readers interpret the elements correctly.
39
+
40
+## Adaptive and Responsive Design
41
+Differentiate between adaptive and responsive paradigms to build universal applications.
42
+
43
+* **Responsive Design:** Adjust the placement, sizing, and reflowing of design elements to fit the available screen space.
44
+* **Adaptive Design:** Select appropriate layouts (e.g., bottom navigation vs. side panel) and input mechanisms (e.g., touch vs. mouse/keyboard) to make the UI usable within the current device context. Design to the strengths of each form factor.
45
+
46
+## Workflows
47
+
48
+### Task Progress: Accessibility Implementation
49
+Copy this checklist to track accessibility compliance during UI development:
50
+
51
+- [ ] Verify all interactive elements have a minimum tap target of 48x48 pixels.
52
+- [ ] Test layout with maximum OS font size settings to ensure no text clipping or overflow occurs.
53
+- [ ] Validate color contrast ratios (4.5:1 for normal text, 3.0:1 for large text).
54
+- [ ] Wrap custom interactive widgets in `Semantics` and assign the appropriate `SemanticsRole`.
55
+- [ ] Group complex composite widgets using `MergeSemantics` to prevent screen reader fatigue.
56
+- [ ] Hide decorative elements from screen readers using `ExcludeSemantics`.
57
+- [ ] If targeting web, verify ARIA roles are correctly mapped and consider programmatic enablement of the semantics tree.
58
+
59
+### Feedback Loop: Accessibility Validation
60
+Run this loop when finalizing a view or component:
61
+1. **Run validator:** Execute accessibility tests or use OS-level screen readers (VoiceOver/TalkBack) to navigate the view.
62
+2. **Review errors:** Identify unannounced interactive elements, trapped focus, or clipped text.
63
+3. **Fix:** Apply `Semantics`, adjust constraints, or modify colors. Repeat until the screen reader provides a clear, logical traversal of the UI.
64
+
65
+## Examples
66
+
67
+### Programmatic Web Accessibility Enablement
68
+If targeting web and requiring accessibility by default, initialize the semantics binding before running the app.
69
+
70
+```dart
71
+import 'package:flutter/material.dart';
72
+import 'package:flutter/semantics.dart';
73
+import 'package:flutter/foundation.dart';
74
+
75
+void main() {
76
+ if (kIsWeb) {
77
+ SemanticsBinding.instance.ensureSemantics();
78
+ }
79
+ runApp(const MyApp());
80
+}
81
+```
82
+
83
+### Custom Component Semantics
84
+If building a custom widget that acts as a list item, explicitly define its semantic role so assistive technologies and web ARIA mappings interpret it correctly.
85
+
86
+```dart
87
+import 'package:flutter/material.dart';
88
+import 'package:flutter/semantics.dart';
89
+
90
+class CustomListItem extends StatelessWidget {
91
+ final String text;
92
+
93
+ const CustomListItem({super.key, required this.text});
94
+
95
+ @override
96
+ Widget build(BuildContext context) {
97
+ return Semantics(
98
+ role: SemanticsRole.listItem,
99
+ label: text,
100
+ child: Padding(
101
+ padding: const EdgeInsets.all(12.0), // Ensures > 48px tap target if interactive
102
+ child: Text(
103
+ text,
104
+ style: const TextStyle(fontSize: 16), // Ensure contrast ratio > 4.5:1
105
+ ),
106
+ ),
107
+ );
108
+ }
109
+}
110
+```
.agent/skills/flutter-interoperating-with-native-apis/SKILL.md
new
+127
@@ -0,0 +1,127 @@
1
+---
2
+name: "flutter-interoperating-with-native-apis"
3
+description: "Interoperates with native platform APIs on Android, iOS, and the web. Use when accessing device-specific features not available in Dart or calling existing native code."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:21:02 GMT"
7
+
8
+---
9
+# Integrating Platform-Specific Code in Flutter
10
+
11
+## Contents
12
+- [Core Concepts & Terminology](#core-concepts--terminology)
13
+- [Binding to Native C/C++ Code (FFI)](#binding-to-native-cc-code-ffi)
14
+- [Implementing Platform Channels & Pigeon](#implementing-platform-channels--pigeon)
15
+- [Hosting Native Platform Views](#hosting-native-platform-views)
16
+- [Integrating Web Content & Wasm](#integrating-web-content--wasm)
17
+- [Workflows](#workflows)
18
+
19
+## Core Concepts & Terminology
20
+- **FFI (Foreign Function Interface):** The `dart:ffi` library used to bind Dart directly to native C/C++ APIs.
21
+- **Platform Channel:** The asynchronous message-passing system (`MethodChannel`, `BasicMessageChannel`) connecting the Dart client (UI) to the host platform (Kotlin/Java, Swift/Objective-C, C++).
22
+- **Pigeon:** A code-generation tool that creates type-safe Platform Channels.
23
+- **Platform View:** A mechanism to embed native UI components (e.g., Android `View`, iOS `UIView`) directly into the Flutter widget tree.
24
+- **JS Interop:** The modern, Wasm-compatible approach to interacting with JavaScript and DOM APIs using `package:web` and `dart:js_interop`.
25
+
26
+## Binding to Native C/C++ Code (FFI)
27
+Use FFI to execute high-performance native code or utilize existing C/C++ libraries without the overhead of asynchronous Platform Channels.
28
+
29
+### Project Setup
30
+* **If creating a standard C/C++ integration (Recommended since Flutter 3.38):** Use the `package_ffi` template. This utilizes `build.dart` hooks to compile native code, eliminating the need for OS-specific build files (CMake, build.gradle, podspec).
31
+ ```bash
32
+ flutter create --template=package_ffi <package_name>
33
+ ```
34
+* **If requiring access to the Flutter Plugin API or Play Services:** Use the legacy `plugin_ffi` template.
35
+ ```bash
36
+ flutter create --template=plugin_ffi <plugin_name>
37
+ ```
38
+
39
+### Implementation Rules
40
+* **Symbol Visibility:** Always mark C++ symbols with `extern "C"` and prevent linker discarding during link-time optimization (LTO).
41
+ ```cpp
42
+ extern "C" __attribute__((visibility("default"))) __attribute__((used))
43
+ ```
44
+* **Dynamic Library Naming (Apple Platforms):** Ensure your `build.dart` hook produces the exact same filename across all target architectures (e.g., `arm64` vs `x86_64`) and SDKs (`iphoneos` vs `iphonesimulator`). Do not append architecture suffixes to the `.dylib` or `.framework` names.
45
+* **Binding Generation:** Always use `package:ffigen` to generate Dart bindings from your C headers (`.h`). Configure this in `ffigen.yaml`.
46
+
47
+## Implementing Platform Channels & Pigeon
48
+Use Platform Channels when you need to interact with platform-specific APIs (e.g., Battery, Bluetooth, OS-level services) using the platform's native language.
49
+
50
+### Pigeon (Type-Safe Channels)
51
+Always prefer `package:pigeon` over raw `MethodChannel` implementations for complex or frequently used APIs.
52
+1. Define the messaging protocol in a standalone Dart file using Pigeon annotations (`@HostApi()`).
53
+2. Generate the host (Kotlin/Swift/C++) and client (Dart) code.
54
+3. Implement the generated interfaces on the native side.
55
+
56
+### Threading Rules
57
+* **Main Thread Requirement:** Always invoke channel methods destined for Flutter on the platform's main thread (UI thread).
58
+* **Background Execution:** If executing channel handlers on a background thread (Android/iOS), you must use the Task Queue API (`makeBackgroundTaskQueue()`).
59
+* **Isolates:** To use plugins/channels from a Dart background `Isolate`, ensure it is registered using `BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken)`.
60
+
61
+## Hosting Native Platform Views
62
+Use Platform Views to embed native UI components (e.g., Google Maps, native video players) into the Flutter widget tree.
63
+
64
+### Android Platform Views
65
+Evaluate the trade-offs between the two rendering modes and select the appropriate one:
66
+* **If requiring perfect fidelity, accessibility, or SurfaceView support:** Use **Hybrid Composition** (`PlatformViewLink` + `AndroidViewSurface`). This appends the native view to the hierarchy but may reduce Flutter's rendering performance.
67
+* **If prioritizing Flutter rendering performance and transformations:** Use **Texture Layer** (`AndroidView`). This renders the native view into a texture. Note: Quick scrolling may drop frames, and `SurfaceView` is problematic.
68
+
69
+### iOS Platform Views
70
+* iOS exclusively uses Hybrid Composition.
71
+* Implement `FlutterPlatformViewFactory` and `FlutterPlatformView` in Swift or Objective-C.
72
+* Use the `UiKitView` widget on the Dart side.
73
+* *Limitation:* `ShaderMask` and `ColorFiltered` widgets cannot be applied to iOS Platform Views.
74
+
75
+## Integrating Web Content & Wasm
76
+Flutter Web supports compiling to WebAssembly (Wasm) for improved performance and multi-threading.
77
+
78
+### Wasm Compilation
79
+* Compile to Wasm using: `flutter build web --wasm`.
80
+* **Server Configuration:** To enable multi-threading, configure your HTTP server to emit the following headers:
81
+ * `Cross-Origin-Embedder-Policy: credentialless` (or `require-corp`)
82
+ * `Cross-Origin-Opener-Policy: same-origin`
83
+* *Limitation:* WasmGC is not currently supported on iOS browsers (WebKit limitation). Flutter will automatically fall back to JavaScript if WasmGC is unavailable.
84
+
85
+### Web Interop
86
+* **If writing new web-specific code:** Strictly use `package:web` and `dart:js_interop`.
87
+* **Do NOT use:** `dart:html`, `dart:js`, or `package:js`. These are incompatible with Wasm compilation.
88
+* **Embedding HTML:** Use `HtmlElementView.fromTagName` to inject arbitrary HTML elements (like `<video>`) into the Flutter Web DOM.
89
+
90
+---
91
+
92
+## Workflows
93
+
94
+### Workflow: Creating a Native FFI Integration
95
+Use this workflow when binding to a C/C++ library.
96
+
97
+- [ ] **Task Progress:**
98
+ - [ ] 1. Run `flutter create --template=package_ffi <name>`.
99
+ - [ ] 2. Place C/C++ source code in the `src/` directory.
100
+ - [ ] 3. Ensure all exported C++ functions are wrapped in `extern "C"` and visibility attributes.
101
+ - [ ] 4. Configure `ffigen.yaml` to point to your header files.
102
+ - [ ] 5. Run `dart run ffigen` to generate Dart bindings.
103
+ - [ ] 6. Modify `hook/build.dart` if linking against pre-compiled or system libraries.
104
+ - [ ] 7. Run validator -> `flutter test` -> review errors -> fix.
105
+
106
+### Workflow: Implementing a Type-Safe Platform Channel (Pigeon)
107
+Use this workflow when you need to call Kotlin/Swift APIs from Dart.
108
+
109
+- [ ] **Task Progress:**
110
+ - [ ] 1. Add `pigeon` to `dev_dependencies`.
111
+ - [ ] 2. Create `pigeons/messages.dart` and define data classes and `@HostApi()` abstract classes.
112
+ - [ ] 3. Run the Pigeon generator script to output Dart, Kotlin, and Swift files.
113
+ - [ ] 4. **Android:** Implement the generated interface in `MainActivity.kt` or your Plugin class.
114
+ - [ ] 5. **iOS:** Implement the generated protocol in `AppDelegate.swift` or your Plugin class.
115
+ - [ ] 6. **Dart:** Import the generated Dart file and call the API methods.
116
+ - [ ] 7. Run validator -> verify cross-platform compilation -> review errors -> fix.
117
+
118
+### Workflow: Embedding a Native Platform View
119
+Use this workflow when embedding a native UI component (e.g., a native map or camera view).
120
+
121
+- [ ] **Task Progress:**
122
+ - [ ] 1. **Dart:** Create a widget that conditionally returns `AndroidView` (or `PlatformViewLink`) for Android, and `UiKitView` for iOS based on `defaultTargetPlatform`.
123
+ - [ ] 2. **Android:** Create a class implementing `PlatformView` that returns the native Android `View`.
124
+ - [ ] 3. **Android:** Create a `PlatformViewFactory` and register it in `configureFlutterEngine`.
125
+ - [ ] 4. **iOS:** Create a class implementing `FlutterPlatformView` that returns the native `UIView`.
126
+ - [ ] 5. **iOS:** Create a `FlutterPlatformViewFactory` and register it in `application:didFinishLaunchingWithOptions:`.
127
+ - [ ] 6. Run validator -> test on physical Android and iOS devices -> review UI clipping/scrolling issues -> fix.
.agent/skills/flutter-localizing-apps/SKILL.md
new
+204
@@ -0,0 +1,204 @@
1
+---
2
+name: "flutter-localizing-apps"
3
+description: "Configures a Flutter app to support multiple languages and regions. Use when preparing an application for international markets and diverse user locales."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:17:07 GMT"
7
+
8
+---
9
+# Localizing Flutter Applications
10
+
11
+## Contents
12
+- [Core Configuration](#core-configuration)
13
+- [Defining ARB Resources](#defining-arb-resources)
14
+- [App Integration](#app-integration)
15
+- [Advanced Formatting](#advanced-formatting)
16
+- [Workflows](#workflows)
17
+- [Troubleshooting & Gotchas](#troubleshooting--gotchas)
18
+
19
+## Core Configuration
20
+
21
+Configure the project to support code generation for localizations.
22
+
23
+1. Add required dependencies to `pubspec.yaml`:
24
+```yaml
25
+dependencies:
26
+ flutter:
27
+ sdk: flutter
28
+ flutter_localizations:
29
+ sdk: flutter
30
+ intl: any
31
+
32
+flutter:
33
+ generate: true # Required for l10n code generation
34
+```
35
+
36
+2. Create an `l10n.yaml` file in the project root to configure the `gen-l10n` tool:
37
+```yaml
38
+arb-dir: lib/l10n
39
+template-arb-file: app_en.arb
40
+output-localization-file: app_localizations.dart
41
+# Optional: Set to false to generate files into lib/ instead of the synthetic package
42
+# synthetic-package: false
43
+```
44
+
45
+## Defining ARB Resources
46
+
47
+Store localized strings in Application Resource Bundle (`.arb`) files within the configured `arb-dir`.
48
+
49
+Create the template file (e.g., `lib/l10n/app_en.arb`):
50
+```json
51
+{
52
+ "helloWorld": "Hello World!",
53
+ "@helloWorld": {
54
+ "description": "The conventional newborn programmer greeting"
55
+ }
56
+}
57
+```
58
+
59
+Create translation files (e.g., `lib/l10n/app_es.arb`):
60
+```json
61
+{
62
+ "helloWorld": "¡Hola Mundo!"
63
+}
64
+```
65
+
66
+## App Integration
67
+
68
+Initialize the `Localizations` widget by configuring the root `MaterialApp` or `CupertinoApp`.
69
+
70
+```dart
71
+import 'package:flutter_localizations/flutter_localizations.dart';
72
+import 'package:flutter_gen/gen_l10n/app_localizations.dart'; // Adjust import if synthetic-package is false
73
+
74
+return MaterialApp(
75
+ title: 'Localized App',
76
+ localizationsDelegates: const [
77
+ AppLocalizations.delegate,
78
+ GlobalMaterialLocalizations.delegate,
79
+ GlobalWidgetsLocalizations.delegate,
80
+ GlobalCupertinoLocalizations.delegate,
81
+ ],
82
+ supportedLocales: const [
83
+ Locale('en'), // English
84
+ Locale('es'), // Spanish
85
+ ],
86
+ home: const MyHomePage(),
87
+);
88
+```
89
+
90
+Access localized values in the widget tree using the generated `AppLocalizations` class:
91
+```dart
92
+Text(AppLocalizations.of(context)!.helloWorld)
93
+```
94
+
95
+*Note: If using `WidgetsApp` instead of `MaterialApp`, omit `GlobalMaterialLocalizations.delegate`.*
96
+
97
+## Advanced Formatting
98
+
99
+Use placeholders, plurals, and selects for dynamic content. Define parameters in the `@key` metadata.
100
+
101
+### Placeholders
102
+```json
103
+"hello": "Hello {userName}",
104
+"@hello": {
105
+ "description": "A message with a single parameter",
106
+ "placeholders": {
107
+ "userName": {
108
+ "type": "String",
109
+ "example": "Bob"
110
+ }
111
+ }
112
+}
113
+```
114
+
115
+### Plurals
116
+```json
117
+"nWombats": "{count, plural, =0{no wombats} =1{1 wombat} other{{count} wombats}}",
118
+"@nWombats": {
119
+ "placeholders": {
120
+ "count": {
121
+ "type": "num",
122
+ "format": "compact"
123
+ }
124
+ }
125
+}
126
+```
127
+
128
+### Selects (Gender/Enums)
129
+```json
130
+"pronoun": "{gender, select, male{he} female{she} other{they}}",
131
+"@pronoun": {
132
+ "placeholders": {
133
+ "gender": {
134
+ "type": "String"
135
+ }
136
+ }
137
+}
138
+```
139
+
140
+### Dates and Numbers
141
+Use `format` and `optionalParameters` to leverage `intl` formatting.
142
+```json
143
+"dateMessage": "Date: {date}",
144
+"@dateMessage": {
145
+ "placeholders": {
146
+ "date": {
147
+ "type": "DateTime",
148
+ "format": "yMd"
149
+ }
150
+ }
151
+}
152
+```
153
+
154
+## Workflows
155
+
156
+### Task Progress: Adding a New Language
157
+Copy this checklist to track progress when introducing a new locale.
158
+
159
+- [ ] Create a new `.arb` file in the `arb-dir` (e.g., `app_fr.arb`).
160
+- [ ] Translate all keys present in the template `.arb` file.
161
+- [ ] Add the new `Locale` to the `supportedLocales` list in `MaterialApp`.
162
+- [ ] Run validator -> Execute `flutter gen-l10n` to verify ARB syntax and regenerate `AppLocalizations`.
163
+- [ ] Review errors -> Fix any missing placeholders or malformed plural/select statements.
164
+- [ ] If targeting iOS, complete the "Configuring iOS App Bundle" workflow.
165
+
166
+### Task Progress: Configuring iOS App Bundle
167
+Flutter handles runtime localization, but iOS requires bundle-level configuration for the App Store and system settings.
168
+
169
+- [ ] Open `ios/Runner.xcodeproj` in Xcode.
170
+- [ ] Select the `Runner` project in the Project Navigator.
171
+- [ ] Navigate to the `Info` tab.
172
+- [ ] Under the **Localizations** section, click the `+` button.
173
+- [ ] Add the newly supported languages/regions.
174
+- [ ] Run validator -> Build the iOS app to ensure `project.pbxproj` is correctly updated.
175
+
176
+## Troubleshooting & Gotchas
177
+
178
+### Missing Localizations Ancestor
179
+Widgets like `TextField` and `CupertinoTabBar` require a `Localizations` ancestor with specific delegates (`MaterialLocalizations` or `CupertinoLocalizations`).
180
+
181
+**Error:** `No MaterialLocalizations found.` or `CupertinoTabBar requires a Localizations parent...`
182
+**Fix:** Ensure the widget is a descendant of `MaterialApp`/`CupertinoApp`. If building a standalone widget tree (e.g., in tests or a custom `WidgetsApp`), wrap the widget in a `Localizations` widget:
183
+
184
+```dart
185
+Localizations(
186
+ locale: const Locale('en', 'US'),
187
+ delegates: const [
188
+ DefaultWidgetsLocalizations.delegate,
189
+ DefaultMaterialLocalizations.delegate, // Required for TextField
190
+ DefaultCupertinoLocalizations.delegate, // Required for CupertinoTabBar
191
+ ],
192
+ child: child,
193
+)
194
+```
195
+
196
+### Advanced Locale Definition
197
+If supporting languages with multiple scripts (e.g., Chinese), use `Locale.fromSubtags` to explicitly define the `scriptCode` and `countryCode` to prevent Flutter from resolving to an unexpected variant.
198
+
199
+```dart
200
+supportedLocales: const [
201
+ Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans', countryCode: 'CN'),
202
+ Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant', countryCode: 'TW'),
203
+]
204
+```
.agent/skills/flutter-managing-state/SKILL.md
new
+203
@@ -0,0 +1,203 @@
1
+---
2
+name: "flutter-managing-state"
3
+description: "Manages application and ephemeral state in a Flutter app. Use when sharing data between widgets or handling complex UI state transitions."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:18:06 GMT"
7
+
8
+---
9
+# Managing State in Flutter
10
+
11
+## Contents
12
+- [Core Concepts](#core-concepts)
13
+- [Architecture and Data Flow](#architecture-and-data-flow)
14
+- [Workflow: Selecting a State Management Approach](#workflow-selecting-a-state-management-approach)
15
+- [Workflow: Implementing MVVM with Provider](#workflow-implementing-mvvm-with-provider)
16
+- [Examples](#examples)
17
+
18
+## Core Concepts
19
+
20
+Flutter's UI is declarative; it is built to reflect the current state of the app (`UI = f(state)`). When state changes, trigger a rebuild of the UI that depends on that state.
21
+
22
+Distinguish between two primary types of state to determine your management strategy:
23
+* **Ephemeral State (Local State):** State contained neatly within a single widget (e.g., current page in a `PageView`, current selected tab, animation progress). Manage this using a `StatefulWidget` and `setState()`.
24
+* **App State (Shared State):** State shared across multiple parts of the app and maintained between user sessions (e.g., user preferences, login info, shopping cart contents). Manage this using advanced approaches like `InheritedWidget`, the `provider` package, and the MVVM architecture.
25
+
26
+## Architecture and Data Flow
27
+
28
+Implement the **Model-View-ViewModel (MVVM)** design pattern combined with **Unidirectional Data Flow (UDF)** for scalable app state management.
29
+
30
+* **Unidirectional Data Flow (UDF):** Enforce a strict flow where state flows *down* from the data layer, through the logic layer, to the UI layer. Events from user interactions flow *up* from the UI layer, to the logic layer, to the data layer.
31
+* **Single Source of Truth (SSOT):** Ensure data changes always happen in the data layer (Repositories). The SSOT class must be the only class capable of modifying its respective data.
32
+* **Model (Data Layer):** Handle low-level tasks like HTTP requests, data caching, and system resources using Repository classes.
33
+* **ViewModel (Logic Layer):** Manage the UI state. Convert app data from the Model into UI State. Extend `ChangeNotifier` and call `notifyListeners()` to trigger UI rebuilds when data changes.
34
+* **View (UI Layer):** Display the state provided by the ViewModel. Keep views lean; they should contain minimal logic (only routing, animations, or simple UI conditionals).
35
+
36
+## Workflow: Selecting a State Management Approach
37
+
38
+Evaluate the scope of the state to determine the correct implementation strategy.
39
+
40
+* **If managing Ephemeral State (single widget scope):**
41
+ 1. Subclass `StatefulWidget` and `State`.
42
+ 2. Store mutable state as private fields within the `State` class.
43
+ 3. Mutate state exclusively inside a `setState()` callback to mark the widget as dirty and schedule a rebuild.
44
+* **If managing App State (shared across widgets):**
45
+ 1. Implement the MVVM pattern.
46
+ 2. Use the `provider` package (a wrapper around `InheritedWidget`) to inject state into the widget tree.
47
+ 3. Use `ChangeNotifier` to emit state updates.
48
+
49
+## Workflow: Implementing MVVM with Provider
50
+
51
+Follow this sequential workflow to implement app-level state management using MVVM and `provider`.
52
+
53
+**Task Progress:**
54
+- [ ] 1. Define the Model (Repository).
55
+- [ ] 2. Create the ViewModel (`ChangeNotifier`).
56
+- [ ] 3. Inject the ViewModel into the Widget Tree.
57
+- [ ] 4. Consume the State in the View.
58
+- [ ] 5. Validate the implementation.
59
+
60
+### 1. Define the Model (Repository)
61
+Create a repository class to act as the Single Source of Truth (SSOT) for the specific data domain. Handle all external API calls or database queries here.
62
+
63
+### 2. Create the ViewModel (`ChangeNotifier`)
64
+Create a ViewModel class that extends `ChangeNotifier`.
65
+* Pass the Repository into the ViewModel via dependency injection.
66
+* Define properties for the UI state (e.g., `isLoading`, `data`, `errorMessage`).
67
+* Implement methods to handle UI events. Inside these methods, mutate the state and call `notifyListeners()` to trigger UI rebuilds.
68
+
69
+### 3. Inject the ViewModel into the Widget Tree
70
+Use `ChangeNotifierProvider` from the `provider` package to provide the ViewModel to the widget subtree that requires it. Place the provider as low in the widget tree as possible to avoid polluting the scope.
71
+
72
+### 4. Consume the State in the View
73
+Access the ViewModel in your `StatelessWidget` or `StatefulWidget`.
74
+* Use `Consumer<MyViewModel>` to rebuild specific parts of the UI when `notifyListeners()` is called.
75
+* Use `context.read<MyViewModel>()` (or `Provider.of<MyViewModel>(context, listen: false)`) inside event handlers (like `onPressed`) to call ViewModel methods without triggering a rebuild of the calling widget.
76
+
77
+### 5. Validate the implementation
78
+Run the following feedback loop to ensure data flows correctly:
79
+1. Trigger a user action in the View.
80
+2. Verify the ViewModel receives the event and calls the Repository.
81
+3. Verify the Repository updates the SSOT and returns data.
82
+4. Verify the ViewModel updates its state and calls `notifyListeners()`.
83
+5. Verify the View rebuilds with the new state.
84
+*Run validator -> review errors -> fix missing `notifyListeners()` calls or incorrect `Provider` scopes.*
85
+
86
+## Examples
87
+
88
+### Ephemeral State Implementation (`setState`)
89
+
90
+Use this pattern strictly for local, UI-only state.
91
+
92
+```dart
93
+class EphemeralCounter extends StatefulWidget {
94
+ const EphemeralCounter({super.key});
95
+
96
+ @override
97
+ State<EphemeralCounter> createState() => _EphemeralCounterState();
98
+}
99
+
100
+class _EphemeralCounterState extends State<EphemeralCounter> {
101
+ int _counter = 0; // Local state
102
+
103
+ void _increment() {
104
+ setState(() {
105
+ _counter++; // Mutate state and schedule rebuild
106
+ });
107
+ }
108
+
109
+ @override
110
+ Widget build(BuildContext context) {
111
+ return ElevatedButton(
112
+ onPressed: _increment,
113
+ child: Text('Count: $_counter'),
114
+ );
115
+ }
116
+}
117
+```
118
+
119
+### App State Implementation (MVVM + Provider)
120
+
121
+Use this pattern for shared data and complex business logic.
122
+
123
+```dart
124
+// 1. Model (Repository)
125
+class CartRepository {
126
+ Future<void> saveItemToCart(String item) async {
127
+ // Simulate network/database call
128
+ await Future.delayed(const Duration(milliseconds: 500));
129
+ }
130
+}
131
+
132
+// 2. ViewModel (ChangeNotifier)
133
+class CartViewModel extends ChangeNotifier {
134
+ final CartRepository repository;
135
+
136
+ CartViewModel({required this.repository});
137
+
138
+ final List<String> _items = [];
139
+ bool isLoading = false;
140
+ String? errorMessage;
141
+
142
+ List<String> get items => List.unmodifiable(_items);
143
+
144
+ Future<void> addItem(String item) async {
145
+ isLoading = true;
146
+ errorMessage = null;
147
+ notifyListeners(); // Trigger loading UI
148
+
149
+ try {
150
+ await repository.saveItemToCart(item);
151
+ _items.add(item);
152
+ } catch (e) {
153
+ errorMessage = 'Failed to add item';
154
+ } finally {
155
+ isLoading = false;
156
+ notifyListeners(); // Trigger success/error UI
157
+ }
158
+ }
159
+}
160
+
161
+// 3. Injection & 4. View (UI)
162
+class CartApp extends StatelessWidget {
163
+ const CartApp({super.key});
164
+
165
+ @override
166
+ Widget build(BuildContext context) {
167
+ // Inject ViewModel
168
+ return ChangeNotifierProvider(
169
+ create: (_) => CartViewModel(repository: CartRepository()),
170
+ child: const CartScreen(),
171
+ );
172
+ }
173
+}
174
+
175
+class CartScreen extends StatelessWidget {
176
+ const CartScreen({super.key});
177
+
178
+ @override
179
+ Widget build(BuildContext context) {
180
+ return Scaffold(
181
+ body: Consumer<CartViewModel>(
182
+ builder: (context, viewModel, child) {
183
+ if (viewModel.isLoading) {
184
+ return const CircularProgressIndicator();
185
+ }
186
+ if (viewModel.errorMessage != null) {
187
+ return Text(viewModel.errorMessage!);
188
+ }
189
+ return ListView.builder(
190
+ itemCount: viewModel.items.length,
191
+ itemBuilder: (_, index) => Text(viewModel.items[index]),
192
+ );
193
+ },
194
+ ),
195
+ floatingActionButton: FloatingActionButton(
196
+ // Use read() to access methods without listening for rebuilds
197
+ onPressed: () => context.read<CartViewModel>().addItem('New Item'),
198
+ child: const Icon(Icons.add),
199
+ ),
200
+ );
201
+ }
202
+}
203
+```
.agent/skills/flutter-reducing-app-size/SKILL.md
new
+100
@@ -0,0 +1,100 @@
1
+---
2
+name: "flutter-reducing-app-size"
3
+description: "Measures and optimizes the size of Flutter application bundles for deployment. Use when minimizing download size or meeting app store package constraints."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:22:44 GMT"
7
+
8
+---
9
+# Reducing Flutter App Size
10
+
11
+## Contents
12
+- [Core Concepts](#core-concepts)
13
+- [Workflow: Generating Size Analysis Files](#workflow-generating-size-analysis-files)
14
+- [Workflow: Analyzing Size Data in DevTools](#workflow-analyzing-size-data-in-devtools)
15
+- [Workflow: Estimating iOS Download Size](#workflow-estimating-ios-download-size)
16
+- [Workflow: Implementing Size Reduction Strategies](#workflow-implementing-size-reduction-strategies)
17
+- [Examples](#examples)
18
+
19
+## Core Concepts
20
+- **Debug vs. Release:** Never use debug builds to measure app size. Debug builds include VM overhead and lack Ahead-Of-Time (AOT) compilation and tree-shaking.
21
+- **Upload vs. Download Size:** The size of an upload package (APK, AAB, IPA) does not represent the end-user download size. App stores filter redundant native library architectures and asset densities based on the target device.
22
+- **AOT Tree-Shaking:** The Dart AOT compiler automatically removes unused or unreachable code in profile and release modes.
23
+- **Size Analysis JSON:** The `--analyze-size` flag generates a `*-code-size-analysis_*.json` file detailing the byte size of packages, libraries, classes, and functions.
24
+
25
+## Workflow: Generating Size Analysis Files
26
+
27
+Use this workflow to generate the raw data required for size analysis.
28
+
29
+**Task Progress:**
30
+- [ ] Determine the target platform (apk, appbundle, ios, linux, macos, windows).
31
+- [ ] Run the Flutter build command with the `--analyze-size` flag.
32
+- [ ] Locate the generated `*-code-size-analysis_*.json` file in the `build/` directory.
33
+
34
+**Conditional Logic:**
35
+- **If targeting Android:** Run `flutter build apk --analyze-size` or `flutter build appbundle --analyze-size`.
36
+- **If targeting iOS:** Run `flutter build ios --analyze-size`. *Note: This creates a `.app` file useful for relative content sizing, but not for estimating final App Store download size. Use the [Estimating iOS Download Size](#workflow-estimating-ios-download-size) workflow for accurate iOS metrics.*
37
+- **If targeting Desktop:** Run `flutter build [windows|macos|linux] --analyze-size`.
38
+
39
+## Workflow: Analyzing Size Data in DevTools
40
+
41
+Use this workflow to visualize and drill down into the Size Analysis JSON.
42
+
43
+**Task Progress:**
44
+- [ ] Launch DevTools by running `dart devtools` in the terminal.
45
+- [ ] Select "Open app size tool" from the DevTools landing page.
46
+- [ ] Upload the generated `*-code-size-analysis_*.json` file.
47
+- [ ] Inspect the treemap or tree view to identify large packages, libraries, or assets.
48
+- [ ] **Feedback Loop:**
49
+ 1. Identify the largest contributors to app size.
50
+ 2. Determine if the dependency or asset is strictly necessary.
51
+ 3. Remove, replace, or optimize the identified component.
52
+ 4. Regenerate the Size Analysis JSON and compare the new build against the old build using the DevTools "Diff" tab.
53
+
54
+## Workflow: Estimating iOS Download Size
55
+
56
+Use this workflow to get an accurate projection of iOS download and installation sizes across different devices.
57
+
58
+**Task Progress:**
59
+- [ ] Configure the app version and build number in `pubspec.yaml`.
60
+- [ ] Generate an Xcode archive by running `flutter build ipa --export-method development`.
61
+- [ ] Open the generated archive (`build/ios/archive/*.xcarchive`) in Xcode.
62
+- [ ] Click **Distribute App** and select **Development**.
63
+- [ ] In the App Thinning configuration, select **All compatible device variants**.
64
+- [ ] Check the option to **Strip Swift symbols**.
65
+- [ ] Sign and export the IPA.
66
+- [ ] Open the exported directory and review the `App Thinning Size Report.txt` file to evaluate projected sizes per device.
67
+
68
+## Workflow: Implementing Size Reduction Strategies
69
+
70
+Apply these strategies to actively reduce the compiled footprint of the application.
71
+
72
+**Task Progress:**
73
+- [ ] **Split Debug Info:** Strip debug symbols from the compiled binary and store them in separate files.
74
+- [ ] **Remove Unused Resources:** Audit the `pubspec.yaml` and `assets/` directory. Delete any images, fonts, or files not actively referenced in the codebase.
75
+- [ ] **Minimize Library Resources:** Review third-party packages. If a package imports massive resource files (e.g., large icon sets or localization files) but only a fraction is used, consider alternative packages or custom implementations.
76
+- [ ] **Compress Media:** Compress all PNG and JPEG assets using tools like `pngquant`, `imageoptim`, or WebP conversion before bundling them into the app.
77
+
78
+## Examples
79
+
80
+### Generating Size Analysis (Android)
81
+```bash
82
+# Generate the size analysis JSON for an Android App Bundle
83
+flutter build appbundle --analyze-size --target-platform=android-arm64
84
+```
85
+
86
+### Splitting Debug Info (Release Build)
87
+```bash
88
+# Build an APK while stripping debug info to reduce binary size
89
+flutter build apk --obfuscate --split-debug-info=build/app/outputs/symbols
90
+```
91
+
92
+### Reading the iOS App Thinning Size Report
93
+When reviewing `App Thinning Size Report.txt`, look for the specific target device to understand the true impact on the user:
94
+```text
95
+Variant: Runner-7433FC8E-1DF4-4299-A7E8-E00768671BEB.ipa
96
+Supported variant descriptors: [device: iPhone12,1, os-version: 13.0]
97
+App + On Demand Resources size: 5.4 MB compressed, 13.7 MB uncompressed
98
+App size: 5.4 MB compressed, 13.7 MB uncompressed
99
+```
100
+*Interpretation: The end-user download size (compressed) is 5.4 MB, and the on-device footprint (uncompressed) is 13.7 MB.*
.agent/skills/flutter-setting-up-on-linux/SKILL.md
new
+138
@@ -0,0 +1,138 @@
1
+---
2
+name: "flutter-setting-up-on-linux"
3
+description: "Sets up a Linux environment for Flutter development. Use when configuring a Linux machine to run, build, or deploy Flutter applications."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:12:22 GMT"
7
+
8
+---
9
+# Setting Up a Linux Environment for Flutter Development
10
+
11
+## Contents
12
+- [System Dependencies](#system-dependencies)
13
+- [Workflow: Configuring the Linux Toolchain](#workflow-configuring-the-linux-toolchain)
14
+- [Workflow: Validating the Environment](#workflow-validating-the-environment)
15
+- [Workflow: Preparing for Distribution (Snapcraft)](#workflow-preparing-for-distribution-snapcraft)
16
+- [Examples](#examples)
17
+
18
+## System Dependencies
19
+To build and run Flutter applications on a Linux desktop, install the required C/C++ toolchain and system libraries. Flutter relies on `dart:ffi` to interface with Linux system calls and the GTK framework for UI rendering.
20
+
21
+Required packages for Debian/Ubuntu-based distributions:
22
+* **Core Utilities:** `curl`, `git`, `unzip`, `xz-utils`, `zip`
23
+* **Build Tools:** `clang`, `cmake`, `ninja-build`, `pkg-config`
24
+* **Libraries:** `libglu1-mesa`, `libgtk-3-dev`, `libstdc++-12-dev`
25
+
26
+## Workflow: Configuring the Linux Toolchain
27
+
28
+Follow this sequential workflow to prepare the Linux host for Flutter desktop development.
29
+
30
+**Task Progress:**
31
+- [ ] 1. Update package lists and upgrade existing packages.
32
+- [ ] 2. Install core utilities and build dependencies.
33
+- [ ] 3. Configure IDE/Editor with Flutter support.
34
+- [ ] 4. (Conditional) Enable Linux support on ChromeOS.
35
+
36
+**1. Update and Install Dependencies**
37
+Execute the following command to install all required packages on Debian/Ubuntu systems:
38
+```bash
39
+sudo apt-get update -y && sudo apt-get upgrade -y
40
+sudo apt-get install -y curl git unzip xz-utils zip libglu1-mesa clang cmake ninja-build pkg-config libgtk-3-dev libstdc++-12-dev
41
+```
42
+
43
+**2. Conditional: ChromeOS Setup**
44
+* **If developing on a Chromebook:** Turn on Linux support in the ChromeOS settings. Ensure the Linux container is fully updated using the `apt-get` commands above before proceeding.
45
+
46
+**3. IDE Configuration**
47
+Install Visual Studio Code, Android Studio, or an IntelliJ-based IDE. Install the official Dart and Flutter extensions/plugins to enable language server features and debugging capabilities.
48
+
49
+## Workflow: Validating the Environment
50
+
51
+Run this feedback loop to ensure the toolchain is correctly recognized by the Flutter SDK.
52
+
53
+**Task Progress:**
54
+- [ ] 1. Run environment validator.
55
+- [ ] 2. Verify connected Linux devices.
56
+- [ ] 3. Resolve toolchain errors.
57
+
58
+**1. Run Validator**
59
+Execute the Flutter diagnostic tool with verbose output:
60
+```bash
61
+flutter doctor -v
62
+```
63
+
64
+**2. Review and Fix (Feedback Loop)**
65
+* **If errors exist under the "Linux toolchain" section:** Review the missing dependencies, install the flagged packages, and re-run `flutter doctor -v`. Repeat until the Linux toolchain section passes.
66
+
67
+**3. Verify Device Availability**
68
+Ensure the Linux desktop is recognized as a valid deployment target:
69
+```bash
70
+flutter devices
71
+```
72
+*Expected Output:* At least one entry must display with the platform marked as **linux**.
73
+
74
+## Workflow: Preparing for Distribution (Snapcraft)
75
+
76
+When preparing a release build for the Snap Store, configure the Snapcraft build environment.
77
+
78
+**Task Progress:**
79
+- [ ] 1. Install Snapcraft and LXD.
80
+- [ ] 2. Configure LXD.
81
+- [ ] 3. Build the Snap package.
82
+
83
+**1. Install Build Tools**
84
+```bash
85
+sudo snap install snapcraft --classic
86
+sudo snap install lxd
87
+```
88
+
89
+**2. Configure LXD**
90
+Initialize LXD and add the current user to the `lxd` group:
91
+```bash
92
+sudo lxd init
93
+sudo usermod -a -G lxd <your_username>
94
+```
95
+*Note: Log out and log back in to apply the group changes.*
96
+
97
+**3. Build the Snap**
98
+Navigate to the project root containing the `snap/snapcraft.yaml` file and execute the build:
99
+```bash
100
+snapcraft --use-lxd
101
+```
102
+
103
+## Examples
104
+
105
+### Baseline `snapcraft.yaml` Configuration
106
+Use this template for the `<project_root>/snap/snapcraft.yaml` file when packaging a Flutter Linux app for the Snap Store.
107
+
108
+```yaml
109
+name: super-cool-app
110
+version: 0.1.0
111
+summary: Super Cool App
112
+description: Super Cool App that does everything!
113
+
114
+confinement: strict
115
+base: core22
116
+grade: stable
117
+
118
+slots:
119
+ dbus-super-cool-app:
120
+ interface: dbus
121
+ bus: session
122
+ name: org.bar.super_cool_app
123
+
124
+apps:
125
+ super-cool-app:
126
+ command: super_cool_app
127
+ extensions: [gnome]
128
+ plugs:
129
+ - network
130
+ slots:
131
+ - dbus-super-cool-app
132
+
133
+parts:
134
+ super-cool-app:
135
+ source: .
136
+ plugin: flutter
137
+ flutter-target: lib/main.dart
138
+```
.agent/skills/flutter-setting-up-on-macos/SKILL.md
new
+75
@@ -0,0 +1,75 @@
1
+---
2
+name: "flutter-setting-up-on-macos"
3
+description: "Sets up a macOS environment for Flutter development. Use when configuring a macOS machine to run, build, or deploy Flutter applications for iOS or macOS."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:12:50 GMT"
7
+
8
+---
9
+# Setting Up a macOS Environment for Flutter Development
10
+
11
+## Contents
12
+- [Prerequisites](#prerequisites)
13
+- [Tooling Configuration](#tooling-configuration)
14
+- [Workflow: Configuring macOS Tooling](#workflow-configuring-macos-tooling)
15
+- [Workflow: Validating the Environment](#workflow-validating-the-environment)
16
+- [Troubleshooting](#troubleshooting)
17
+
18
+## Prerequisites
19
+Ensure the following baseline requirements are met before configuring the macOS-specific toolchain:
20
+- macOS operating system.
21
+- Flutter SDK installed and added to the system `PATH`.
22
+- Active internet connection for downloading toolchains and dependencies.
23
+
24
+## Tooling Configuration
25
+macOS desktop development requires specific Apple toolchains to compile and debug native Swift and Objective-C code.
26
+- **Xcode**: Required for compiling macOS desktop applications.
27
+- **CocoaPods**: Required for managing native dependencies used by Flutter plugins.
28
+
29
+## Workflow: Configuring macOS Tooling
30
+
31
+Copy and follow this checklist to configure the macOS build environment.
32
+
33
+- [ ] **Install Xcode**: Install the latest version of Xcode from the Mac App Store or the Apple Developer portal.
34
+- [ ] **Configure Command-Line Tools**: Link the Xcode command-line tools to the installed Xcode version. Run the following command in the terminal:
35
+ ```bash
36
+ sudo sh -c 'xcode-select -s /Applications/Xcode.app/Contents/Developer && xcodebuild -runFirstLaunch'
37
+ ```
38
+ *Conditional*: If Xcode is installed in a custom directory, replace `/Applications/Xcode.app` with the correct absolute path.
39
+- [ ] **Accept Xcode Licenses**: Accept the required developer licenses by running:
40
+ ```bash
41
+ sudo xcodebuild -license
42
+ ```
43
+ Read and agree to the prompts.
44
+- [ ] **Install CocoaPods**: Install CocoaPods to handle native macOS plugin dependencies.
45
+ ```bash
46
+ sudo gem install cocoapods
47
+ ```
48
+ *Conditional*: If CocoaPods is already installed, ensure it is updated to the latest version (`sudo gem update cocoapods`).
49
+
50
+## Workflow: Validating the Environment
51
+
52
+Execute this feedback loop to ensure the environment is correctly configured for macOS desktop development.
53
+
54
+- [ ] **Run Validator**: Execute the Flutter diagnostic tool with verbose output:
55
+ ```bash
56
+ flutter doctor -v
57
+ ```
58
+- [ ] **Review Errors**: Check the `Xcode` section in the output.
59
+- [ ] **Fix & Retry**: If errors or missing components are reported under the Xcode section, resolve them according to the output instructions, then re-run `flutter doctor -v` until the Xcode section passes.
60
+- [ ] **Verify Device Availability**: Confirm that Flutter recognizes the macOS desktop as a valid deployment target:
61
+ ```bash
62
+ flutter devices
63
+ ```
64
+ *Success Criteria*: The output must contain at least one entry with `macos` listed as the platform.
65
+
66
+## Troubleshooting
67
+
68
+If the validation workflow fails, apply the following resolutions:
69
+
70
+- **Missing Command-Line Tools**: If `flutter doctor` reports missing tools, ensure the `xcode-select` command was run with `sudo` and points to the correct `.app` directory.
71
+- **CocoaPods Not Found**: If CocoaPods is installed but not detected, verify that your Ruby `gem` binary path is included in your shell's `PATH` environment variable.
72
+- **Device Not Listed**: If `flutter devices` does not list `macos`, ensure desktop support is enabled in your Flutter configuration:
73
+ ```bash
74
+ flutter config --enable-macos-desktop
75
+ ```
.agent/skills/flutter-setting-up-on-windows/SKILL.md
new
+103
@@ -0,0 +1,103 @@
1
+---
2
+name: "flutter-setting-up-on-windows"
3
+description: "Sets up a Windows environment for Flutter development. Use when configuring a Windows machine to run, build, or deploy Flutter applications for Windows desktop or Android."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:13:13 GMT"
7
+
8
+---
9
+# Setting Up Flutter for Windows Development
10
+
11
+## Contents
12
+- [Core Requirements](#core-requirements)
13
+- [Workflow: Installing and Configuring the SDK](#workflow-installing-and-configuring-the-sdk)
14
+- [Workflow: Configuring Tooling and IDEs](#workflow-configuring-tooling-and-ides)
15
+- [Workflow: Configuring Target Platforms](#workflow-configuring-target-platforms)
16
+- [Workflow: Building and Packaging for Windows](#workflow-building-and-packaging-for-windows)
17
+- [Workflow: Generating and Installing Certificates](#workflow-generating-and-installing-certificates)
18
+- [Examples](#examples)
19
+
20
+## Core Requirements
21
+Configure the Windows environment to support both Flutter framework execution and native C/C++ compilation. Differentiate strictly between **Visual Studio** (required for Windows desktop C++ compilation) and **VS Code** (the recommended Dart/Flutter code editor).
22
+
23
+## Workflow: Installing and Configuring the SDK
24
+
25
+Follow this sequential workflow to initialize the Flutter SDK on a Windows machine.
26
+
27
+- [ ] Download the latest stable Flutter SDK for Windows.
28
+- [ ] Extract the SDK to a directory with standard user privileges (e.g., `C:\src\flutter`). Do not install in protected directories like `C:\Program Files\`.
29
+- [ ] Copy the absolute path to the Flutter SDK's `bin` directory.
30
+- [ ] Open Windows Environment Variables settings and append the `bin` directory path to the system or user `PATH` variable.
31
+- [ ] Open a new terminal session to apply the `PATH` changes.
32
+- [ ] **Feedback Loop:** Run validator -> review errors -> fix.
33
+ 1. Execute `flutter doctor -v`.
34
+ 2. Review the output for missing dependencies or path issues.
35
+ 3. Resolve any flagged errors before proceeding to tooling setup.
36
+
37
+## Workflow: Configuring Tooling and IDEs
38
+
39
+- [ ] Install **Visual Studio** (not VS Code).
40
+- [ ] Select and install the **Desktop development with C++** workload during the Visual Studio installation process. This is mandatory for compiling Windows desktop applications.
41
+- [ ] Install your preferred code editor (VS Code, Android Studio, or IntelliJ).
42
+- [ ] Install the official Flutter and Dart extensions/plugins within your chosen editor.
43
+
44
+## Workflow: Configuring Target Platforms
45
+
46
+Apply conditional logic based on the specific platform you are targeting for development.
47
+
48
+**If targeting Windows Desktop:**
49
+- [ ] Ensure the Visual Studio C++ workload is fully updated.
50
+- [ ] Restart your IDE so it detects the Windows desktop device.
51
+- [ ] To disable platforms you do not intend to compile for, execute `flutter config --no-enable-<platform>` (e.g., `flutter config --no-enable-windows-desktop`).
52
+
53
+**If targeting Android on Windows:**
54
+- [ ] **For physical devices:** Enable Developer Options and USB debugging on the device. Install the specific OEM USB drivers for Windows.
55
+- [ ] **For emulators:** Open the Android Virtual Device (AVD) manager. Under "Emulated Performance" -> "Graphics acceleration", select an option specifying "Hardware" to enable hardware acceleration.
56
+- [ ] Verify the device connection by running `flutter devices`.
57
+
58
+## Workflow: Building and Packaging for Windows
59
+
60
+To distribute a Windows desktop application, assemble the compiled executable and its required dependencies into a single distributable archive.
61
+
62
+- [ ] Execute `flutter build windows` to compile the release build.
63
+- [ ] Navigate to `build\windows\runner\Release\`.
64
+- [ ] Create a new staging directory for the distribution zip.
65
+- [ ] Copy the following assets from the `Release` directory into the staging directory:
66
+ - The application executable (`.exe`).
67
+ - All generated `.dll` files.
68
+ - The entire `data` directory.
69
+- [ ] Copy the required Visual C++ redistributables into the staging directory alongside the executable:
70
+ - `msvcp140.dll`
71
+ - `vcruntime140.dll`
72
+ - `vcruntime140_1.dll`
73
+- [ ] Compress the staging directory into a `.zip` file for distribution.
74
+
75
+## Workflow: Generating and Installing Certificates
76
+
77
+If you require a self-signed certificate for MSIX packaging or local testing, use OpenSSL.
78
+
79
+- [ ] Install OpenSSL and add its `bin` directory to your `PATH` environment variable.
80
+- [ ] Generate a private key: `openssl genrsa -out mykeyname.key 2048`
81
+- [ ] Generate a Certificate Signing Request (CSR): `openssl req -new -key mykeyname.key -out mycsrname.csr`
82
+- [ ] Generate the signed certificate (CRT): `openssl x509 -in mycsrname.csr -out mycrtname.crt -req -signkey mykeyname.key -days 10000`
83
+- [ ] Generate the `.pfx` file: `openssl pkcs12 -export -out CERTIFICATE.pfx -inkey mykeyname.key -in mycrtname.crt`
84
+- [ ] Install the `.pfx` certificate on the local Windows machine. Place it in the Certificate Store under **Trusted Root Certification Authorities** prior to installing the application.
85
+
86
+## Examples
87
+
88
+### Windows Distribution Directory Structure
89
+When assembling your Windows build for distribution, ensure the directory structure strictly matches the following layout before zipping:
90
+
91
+```text
92
+Release_Archive/
93
+│ my_flutter_app.exe
94
+│ flutter_windows.dll
95
+│ msvcp140.dll
96
+│ vcruntime140.dll
97
+│ vcruntime140_1.dll
98
+│
99
+└───data/
100
+ │ app.so
101
+ │ icudtl.dat
102
+ │ ...
103
+```
.agent/skills/flutter-testing-apps/SKILL.md
new
+182
@@ -0,0 +1,182 @@
1
+---
2
+name: "flutter-testing-apps"
3
+description: "Implements unit, widget, and integration tests for a Flutter app. Use when ensuring code quality and preventing regressions through automated testing."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:22:10 GMT"
7
+
8
+---
9
+# Testing Flutter Applications
10
+
11
+## Contents
12
+- [Core Testing Strategies](#core-testing-strategies)
13
+- [Architectural Testing Guidelines](#architectural-testing-guidelines)
14
+- [Plugin Testing Guidelines](#plugin-testing-guidelines)
15
+- [Workflows](#workflows)
16
+- [Examples](#examples)
17
+
18
+## Core Testing Strategies
19
+
20
+Balance your testing suite across three main categories to optimize for confidence, maintenance cost, dependencies, and execution speed.
21
+
22
+### Unit Tests
23
+Use unit tests to verify the correctness of a single function, method, or class under various conditions.
24
+- Mock all external dependencies.
25
+- Do not involve disk I/O, screen rendering, or user actions from outside the test process.
26
+- Execute using the `test` or `flutter_test` package.
27
+
28
+### Widget Tests
29
+Use widget tests (component tests) to ensure a single widget's UI looks and interacts as expected.
30
+- Provide the appropriate widget lifecycle context using `WidgetTester`.
31
+- Use `Finder` classes to locate widgets and `Matcher` constants to verify their existence and state.
32
+- Test views and UI interactions without spinning up the full application.
33
+
34
+### Integration Tests
35
+Use integration tests (end-to-end or GUI testing) to validate how individual pieces of an app work together and to capture performance metrics on real devices.
36
+- Add the `integration_test` package as a dependency.
37
+- Run on physical devices, OS emulators, or Firebase Test Lab.
38
+- Prioritize integration tests for routing, dependency injection, and critical user flows.
39
+
40
+## Architectural Testing Guidelines
41
+
42
+Design your application for observability and testability. Ensure all components can be tested both in isolation and together.
43
+
44
+- **ViewModels**: Write unit tests for every ViewModel class. Test the UI logic without relying on Flutter libraries or testing frameworks.
45
+- **Repositories & Services**: Write unit tests for every service and repository. Mock the underlying data sources (e.g., HTTP clients, local databases).
46
+- **Views**: Write widget tests for all views. Pass faked or mocked ViewModels and Repositories into the widget tree to isolate the UI.
47
+- **Fakes over Mocks**: Prefer creating `Fake` implementations of your repositories (e.g., `FakeUserRepository`) over using mocking libraries when testing ViewModels and Views to ensure well-defined inputs and outputs.
48
+
49
+## Plugin Testing Guidelines
50
+
51
+When testing plugins, combine Dart tests with native platform tests to ensure full coverage across the method channel.
52
+
53
+- **Dart Tests**: Use Dart unit and widget tests for the Dart-facing API. Mock the platform channel to validate Dart logic.
54
+- **Native Unit Tests**: Implement native unit tests for isolated platform logic.
55
+ - Android: Configure JUnit tests in `android/src/test/`.
56
+ - iOS/macOS: Configure XCTest tests in `example/ios/RunnerTests/` and `example/macos/RunnerTests/`.
57
+ - Linux/Windows: Configure GoogleTest tests in `linux/test/` and `windows/test/`.
58
+- **Native UI Tests**: Use Espresso (Android) or XCUITest (iOS) if the plugin requires native UI interactions.
59
+- **Integration Tests**: Write at least one integration test for each platform channel call to verify Dart-to-Native communication.
60
+- **End-to-End Fallback**: If integration tests cannot cover a flow (e.g., mocking device state), synthesize calls to the method channel entry point using native unit tests, and test the Dart public API using Dart unit tests.
61
+
62
+## Workflows
63
+
64
+### Workflow: Implementing a Component Test Suite
65
+Copy and track this checklist when implementing tests for a new architectural feature.
66
+
67
+- [ ] **Task Progress**
68
+ - [ ] Create `Fake` implementations for any new Repositories or Services.
69
+ - [ ] Write Unit Tests for the Repository (mocking the API/Database).
70
+ - [ ] Write Unit Tests for the ViewModel (injecting the Fake Repositories).
71
+ - [ ] Write Widget Tests for the View (injecting the ViewModel and Fake Repositories).
72
+ - [ ] Write an Integration Test for the critical path involving this feature.
73
+ - [ ] Run validator -> review coverage -> fix missing edge cases.
74
+
75
+### Workflow: Running Integration Tests
76
+Follow conditional logic based on the target platform when executing integration tests.
77
+
78
+1. **If testing on Mobile (Local)**:
79
+ - Connect the Android/iOS device or emulator.
80
+ - Run: `flutter test integration_test/app_test.dart`
81
+2. **If testing on Web**:
82
+ - Install and launch ChromeDriver: `chromedriver --port=4444`
83
+ - Run: `flutter drive --driver=test_driver/integration_test.dart --target=integration_test/app_test.dart -d chrome`
84
+3. **If testing on Linux (CI System)**:
85
+ - Invoke an X server using `xvfb-run` to provide a display environment.
86
+ - Run: `xvfb-run flutter test integration_test/app_test.dart -d linux`
87
+4. **If testing via Firebase Test Lab**:
88
+ - Build the Android test APKs: `flutter build apk --debug` and `./gradlew app:assembleAndroidTest`
89
+ - Upload the App APK and Test APK to the Firebase Console.
90
+
91
+## Examples
92
+
93
+### Example: ViewModel Unit Test
94
+Demonstrates testing a ViewModel using a Fake Repository.
95
+
96
+```dart
97
+import 'package:flutter_test/flutter_test.dart';
98
+
99
+void main() {
100
+ group('HomeViewModel tests', () {
101
+ test('Load bookings successfully', () {
102
+ // Inject fake dependencies
103
+ final viewModel = HomeViewModel(
104
+ bookingRepository: FakeBookingRepository()..createBooking(kBooking),
105
+ userRepository: FakeUserRepository(),
106
+ );
107
+
108
+ // Verify state
109
+ expect(viewModel.bookings.isNotEmpty, true);
110
+ });
111
+ });
112
+}
113
+```
114
+
115
+### Example: View Widget Test
116
+Demonstrates testing a View by pumping a localized widget tree with fake dependencies.
117
+
118
+```dart
119
+import 'package:flutter/material.dart';
120
+import 'package:flutter_test/flutter_test.dart';
121
+
122
+void main() {
123
+ group('HomeScreen tests', () {
124
+ late HomeViewModel viewModel;
125
+ late FakeBookingRepository bookingRepository;
126
+
127
+ setUp(() {
128
+ bookingRepository = FakeBookingRepository()..createBooking(kBooking);
129
+ viewModel = HomeViewModel(
130
+ bookingRepository: bookingRepository,
131
+ userRepository: FakeUserRepository(),
132
+ );
133
+ });
134
+
135
+ testWidgets('renders bookings list', (WidgetTester tester) async {
136
+ await tester.pumpWidget(
137
+ MaterialApp(
138
+ home: HomeScreen(viewModel: viewModel),
139
+ ),
140
+ );
141
+
142
+ // Verify UI state
143
+ expect(find.byType(ListView), findsOneWidget);
144
+ expect(find.text('Booking 1'), findsOneWidget);
145
+ });
146
+ });
147
+}
148
+```
149
+
150
+### Example: Integration Test
151
+Demonstrates a full end-to-end test using the `integration_test` package.
152
+
153
+```dart
154
+import 'package:flutter/material.dart';
155
+import 'package:flutter_test/flutter_test.dart';
156
+import 'package:integration_test/integration_test.dart';
157
+import 'package:my_app/main.dart';
158
+
159
+void main() {
160
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
161
+
162
+ group('end-to-end test', () {
163
+ testWidgets('tap on the floating action button, verify counter', (tester) async {
164
+ // Load app widget
165
+ await tester.pumpWidget(const MyApp());
166
+
167
+ // Verify initial state
168
+ expect(find.text('0'), findsOneWidget);
169
+
170
+ // Find and tap the button
171
+ final fab = find.byKey(const ValueKey('increment'));
172
+ await tester.tap(fab);
173
+
174
+ // Trigger a frame to allow animations/state to settle
175
+ await tester.pumpAndSettle();
176
+
177
+ // Verify updated state
178
+ expect(find.text('1'), findsOneWidget);
179
+ });
180
+ });
181
+}
182
+```
.agent/skills/flutter-theming-apps/SKILL.md
new
+174
@@ -0,0 +1,174 @@
1
+---
2
+name: "flutter-theming-apps"
3
+description: "Customizes the visual appearance of a Flutter app using the theming system. Use when defining global styles, colors, or typography for an application."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:14:47 GMT"
7
+---
8
+
9
+# Implementing Flutter Theming and Adaptive Design
10
+
11
+## Contents
12
+
13
+- [Core Theming Concepts](#core-theming-concepts)
14
+- [Material 3 Guidelines](#material-3-guidelines)
15
+- [Component Theme Normalization](#component-theme-normalization)
16
+- [Button Styling](#button-styling)
17
+- [Platform Idioms & Adaptive Design](#platform-idioms--adaptive-design)
18
+- [Workflows](#workflows)
19
+- [Examples](#examples)
20
+
21
+## Core Theming Concepts
22
+
23
+Flutter applies styling in a strict hierarchy: styles applied to the specific widget -> themes that override the immediate parent theme -> the main app theme.
24
+
25
+- Define app-wide themes using the `theme` property of `MaterialApp` with a `ThemeData` instance.
26
+- Override themes for specific widget subtrees by wrapping them in a `Theme` widget and using `Theme.of(context).copyWith(...)`.
27
+- **Do not** use deprecated `ThemeData` properties:
28
+ - Replace `accentColor` with `colorScheme.secondary`.
29
+ - Replace `accentTextTheme` with `textTheme` (using `colorScheme.onSecondary` for contrast).
30
+ - Replace `AppBarTheme.color` with `AppBarTheme.backgroundColor`.
31
+
32
+## Material 3 Guidelines
33
+
34
+Material 3 is the default theme as of Flutter 3.16.
35
+
36
+- **Colors:** Generate color schemes using `ColorScheme.fromSeed(seedColor: Colors.blue)`. This ensures accessible contrast ratios.
37
+- **Elevation:** Material 3 uses `ColorScheme.surfaceTint` to indicate elevation instead of just drop shadows. To revert to M2 shadow behavior, set `surfaceTint: Colors.transparent` and define a `shadowColor`.
38
+- **Typography:** Material 3 updates font sizes, weights, and line heights. If text wrapping breaks legacy layouts, adjust `letterSpacing` on the specific `TextStyle`.
39
+- **Modern Components:**
40
+ - Replace `BottomNavigationBar` with `NavigationBar`.
41
+ - Replace `Drawer` with `NavigationDrawer`.
42
+ - Replace `ToggleButtons` with `SegmentedButton`.
43
+ - Use `FilledButton` for a high-emphasis button without the elevation of `ElevatedButton`.
44
+
45
+## Component Theme Normalization
46
+
47
+Component themes in `ThemeData` have been normalized to use `*ThemeData` classes rather than `*Theme` widgets.
48
+
49
+When defining `ThemeData`, strictly use the `*ThemeData` suffix for the following properties:
50
+
51
+- `cardTheme`: Use `CardThemeData` (Not `CardTheme`)
52
+- `dialogTheme`: Use `DialogThemeData` (Not `DialogTheme`)
53
+- `tabBarTheme`: Use `TabBarThemeData` (Not `TabBarTheme`)
54
+- `appBarTheme`: Use `AppBarThemeData` (Not `AppBarTheme`)
55
+- `bottomAppBarTheme`: Use `BottomAppBarThemeData` (Not `BottomAppBarTheme`)
56
+- `inputDecorationTheme`: Use `InputDecorationThemeData` (Not `InputDecorationTheme`)
57
+
58
+## Button Styling
59
+
60
+Legacy button classes (`FlatButton`, `RaisedButton`, `OutlineButton`) are obsolete.
61
+
62
+- Use `TextButton`, `ElevatedButton`, and `OutlinedButton`.
63
+- Configure button appearance using a `ButtonStyle` object.
64
+- For simple overrides based on the theme's color scheme, use the static utility method: `TextButton.styleFrom(foregroundColor: Colors.blue)`.
65
+- For state-dependent styling (hovered, focused, pressed, disabled), use `MaterialStateProperty.resolveWith`.
66
+
67
+## Platform Idioms & Adaptive Design
68
+
69
+When building adaptive apps, respect platform-specific norms to reduce cognitive load and build user trust.
70
+
71
+- **Scrollbars:** Desktop users expect omnipresent scrollbars; mobile users expect them only during scrolling. Toggle `thumbVisibility` on the `Scrollbar` widget based on the platform.
72
+- **Selectable Text:** Web and desktop users expect text to be selectable. Wrap text in `SelectableText` or `SelectableText.rich`.
73
+- **Horizontal Button Order:** Windows places confirmation buttons on the left; macOS/Linux place them on the right. Use a `Row` with `TextDirection.rtl` for Windows and `TextDirection.ltr` for others.
74
+- **Context Menus & Tooltips:** Desktop users expect hover and right-click interactions. Implement `Tooltip` for hover states and use context menu packages for right-click actions.
75
+
76
+## Workflows
77
+
78
+### Workflow: Migrating Legacy Themes to Material 3
79
+
80
+Use this workflow when updating an older Flutter codebase.
81
+
82
+**Task Progress:**
83
+
84
+- [ ] 1. Remove `useMaterial3: false` from `ThemeData` (it is true by default).
85
+- [ ] 2. Replace manual `ColorScheme` definitions with `ColorScheme.fromSeed()`.
86
+- [ ] 3. Run validator -> review errors -> fix: Search for and replace deprecated `accentColor`, `accentColorBrightness`, `accentIconTheme`, and `accentTextTheme`.
87
+- [ ] 4. Run validator -> review errors -> fix: Search for `AppBarTheme(color: ...)` and replace with `backgroundColor`.
88
+- [ ] 5. Update `ThemeData` component properties to use `*ThemeData` classes (e.g., `cardTheme: CardThemeData()`).
89
+- [ ] 6. Replace legacy buttons (`FlatButton` -> `TextButton`, `RaisedButton` -> `ElevatedButton`, `OutlineButton` -> `OutlinedButton`).
90
+- [ ] 7. Replace legacy navigation components (`BottomNavigationBar` -> `NavigationBar`, `Drawer` -> `NavigationDrawer`).
91
+
92
+### Workflow: Implementing Adaptive UI Components
93
+
94
+Use this workflow when building a widget intended for both mobile and desktop/web.
95
+
96
+**Task Progress:**
97
+
98
+- [ ] 1. If displaying a list/grid, wrap it in a `Scrollbar` and set `thumbVisibility: DeviceType.isDesktop`.
99
+- [ ] 2. If displaying read-only data, use `SelectableText` instead of `Text`.
100
+- [ ] 3. If implementing a dialog with action buttons, check the platform. If Windows, set `TextDirection.rtl` on the button `Row`.
101
+- [ ] 4. If implementing interactive elements, wrap them in `Tooltip` widgets to support mouse hover states.
102
+
103
+## Examples
104
+
105
+### Example: Modern Material 3 ThemeData Setup
106
+
107
+```dart
108
+MaterialApp(
109
+ title: 'Adaptive App',
110
+ theme: ThemeData(
111
+ colorScheme: ColorScheme.fromSeed(
112
+ seedColor: Colors.deepPurple,
113
+ brightness: Brightness.light,
114
+ ),
115
+ // Use *ThemeData classes for component normalization
116
+ appBarTheme: const AppBarThemeData(
117
+ backgroundColor: Colors.deepPurple, // Do not use 'color'
118
+ elevation: 0,
119
+ ),
120
+ cardTheme: const CardThemeData(
121
+ elevation: 2,
122
+ ),
123
+ textTheme: const TextTheme(
124
+ bodyMedium: TextStyle(letterSpacing: 0.2),
125
+ ),
126
+ ),
127
+ home: const MyHomePage(),
128
+);
129
+```
130
+
131
+### Example: State-Dependent ButtonStyle
132
+
133
+```dart
134
+TextButton(
135
+ style: ButtonStyle(
136
+ // Default color
137
+ foregroundColor: MaterialStateProperty.all<Color>(Colors.blue),
138
+ // State-dependent overlay color
139
+ overlayColor: MaterialStateProperty.resolveWith<Color?>(
140
+ (Set<MaterialState> states) {
141
+ if (states.contains(MaterialState.hovered)) {
142
+ return Colors.blue.withValues(alpha:0.04);
143
+ }
144
+ if (states.contains(MaterialState.focused) || states.contains(MaterialState.pressed)) {
145
+ return Colors.blue.withValues(alpha:0.12);
146
+ }
147
+ return null; // Defer to the widget's default.
148
+ },
149
+ ),
150
+ ),
151
+ onPressed: () {},
152
+ child: const Text('Adaptive Button'),
153
+)
154
+```
155
+
156
+### Example: Adaptive Dialog Button Order
157
+
158
+```dart
159
+Row(
160
+ // Windows expects confirmation on the left (RTL reverses the standard LTR Row)
161
+ textDirection: Platform.isWindows ? TextDirection.rtl : TextDirection.ltr,
162
+ mainAxisAlignment: MainAxisAlignment.end,
163
+ children: [
164
+ TextButton(
165
+ onPressed: () => Navigator.pop(context, false),
166
+ child: const Text('Cancel'),
167
+ ),
168
+ FilledButton(
169
+ onPressed: () => Navigator.pop(context, true),
170
+ child: const Text('Confirm'),
171
+ ),
172
+ ],
173
+)
174
+```
.agent/skills/flutter-working-with-databases/SKILL.md
new
+200
@@ -0,0 +1,200 @@
1
+---
2
+name: "flutter-working-with-databases"
3
+description: "Manages local data persistence using SQLite or other database solutions. Use when a Flutter app needs to store, query, or synchronize large amounts of structured data on the device."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:19:15 GMT"
7
+
8
+---
9
+# Architecting the Data Layer
10
+
11
+## Contents
12
+- [Core Architecture](#core-architecture)
13
+- [Services Implementation](#services-implementation)
14
+- [Repository Implementation](#repository-implementation)
15
+- [Caching Strategies](#caching-strategies)
16
+- [Workflows](#workflows)
17
+- [Examples](#examples)
18
+
19
+## Core Architecture
20
+
21
+Construct the data layer as the Single Source of Truth (SSOT) for all application data. In an MVVM architecture, the data layer represents the Model. Never update application data outside of this layer.
22
+
23
+Separate the data layer into two distinct components: **Repositories** and **Services**.
24
+
25
+### Repositories
26
+* Act as the SSOT for a specific domain entity.
27
+* Contain business logic for data mutation, polling, caching, and offline synchronization.
28
+* Transform raw data models (API/DB models) into Domain Models (clean data classes containing only what the UI needs).
29
+* Inject Services as private members to prevent the UI layer from bypassing the repository.
30
+
31
+### Services
32
+* Act as stateless wrappers around external data sources (HTTP clients, SQLite databases, platform plugins).
33
+* Perform no business logic or data transformation beyond basic JSON serialization.
34
+* Return raw data models or `Result` wrappers to the calling repository.
35
+
36
+## Services Implementation
37
+
38
+### Database Services (SQLite)
39
+Use databases to persist and query large amounts of structured data locally.
40
+* Add `sqflite` and `path` packages to `pubspec.yaml`.
41
+* Use the `path` package to define the storage location on disk safely across platforms.
42
+* Define table schemas using constants to prevent typos.
43
+* Use `id` as the primary key with `AUTOINCREMENT` to improve query and update times.
44
+* Always use `whereArgs` in SQL queries to prevent SQL injection (e.g., `where: 'id = ?', whereArgs: [id]`).
45
+
46
+### API Services
47
+* Wrap HTTP calls (e.g., using the `http` package) in dedicated client classes.
48
+* Return asynchronous response objects (`Future` or `Stream`).
49
+* Handle raw JSON serialization at this level, returning API-specific data models.
50
+
51
+## Repository Implementation
52
+
53
+### Domain Models
54
+* Define immutable data classes (using `freezed` or `built_value`) for Domain Models.
55
+* Strip out backend-specific fields (like metadata or pagination tokens) that the UI does not need.
56
+
57
+### Offline-First Synchronization
58
+Combine local and remote data sources within the repository to provide seamless offline support.
59
+
60
+* **If reading data:** Return a `Stream` that immediately yields the cached local data from the Database Service, performs the network request via the API Service, updates the Database Service, and then yields the fresh data.
61
+* **If writing data (Online-only):** Attempt the API Service mutation first. If successful, update the Database Service.
62
+* **If writing data (Offline-first):** Update the Database Service immediately. Attempt the API Service mutation. If the network fails, flag the local database record as `synchronized: false` and queue a background synchronization task.
63
+
64
+## Caching Strategies
65
+
66
+Select the appropriate caching strategy based on the data payload:
67
+* **Small Key-Value Data:** Use `shared_preferences` for simple app configurations, theme settings, or user preferences.
68
+* **Large Datasets:** Use relational (`sqflite`, `drift`) or non-relational (`hive_ce`, `isar_community`) on-device databases.
69
+* **Images:** Use the `cached_network_image` package to automatically cache remote images to the device's file system.
70
+* **API Responses:** Implement lightweight remote caching within the API Service or Repository using in-memory maps or temporary file storage.
71
+
72
+## Workflows
73
+
74
+### Workflow: Implementing a New Data Feature
75
+Copy and track this checklist when adding a new data entity to the application.
76
+
77
+- [ ] **Task Progress**
78
+ - [ ] Define the Domain Model (immutable, UI-focused).
79
+ - [ ] Define the API/DB Models (raw data structures).
80
+ - [ ] Create or update the Service(s) to handle raw data fetching/storage.
81
+ - [ ] Create the Repository interface (abstract class).
82
+ - [ ] Implement the Repository, injecting the required Service(s) as private dependencies.
83
+ - [ ] Map raw Service models to the Domain Model within the Repository.
84
+ - [ ] Expose Repository methods to the View Model.
85
+ - [ ] Run validator -> review errors -> fix.
86
+
87
+### Workflow: Implementing SQLite Persistence
88
+Follow this sequence to add a new SQLite table and integrate it.
89
+
90
+- [ ] **Task Progress**
91
+ - [ ] Add `sqflite` and `path` dependencies.
92
+ - [ ] Define table name and column constants.
93
+ - [ ] Update the `onCreate` or `onUpgrade` method in the Database Service to execute the `CREATE TABLE` statement.
94
+ - [ ] Implement `insert`, `query`, `update`, and `delete` methods in the Database Service.
95
+ - [ ] Inject the Database Service into the target Repository.
96
+ - [ ] Ensure the Repository calls `database.open()` before executing queries.
97
+
98
+## Examples
99
+
100
+### Offline-First Repository Implementation
101
+This example demonstrates a Repository coordinating between a Database Service and an API Service using a Stream for offline-first reads.
102
+
103
+```dart
104
+import 'dart:async';
105
+
106
+class TodoRepository {
107
+ TodoRepository({
108
+ required DatabaseService databaseService,
109
+ required ApiClientService apiClientService,
110
+ }) : _databaseService = databaseService,
111
+ _apiClientService = apiClientService;
112
+
113
+ final DatabaseService _databaseService;
114
+ final ApiClientService _apiClientService;
115
+
116
+ /// Yields local data immediately, then fetches remote data, updates local, and yields fresh data.
117
+ Stream<List<Todo>> observeTodos() async* {
118
+ // 1. Yield local cached data first
119
+ final localTodos = await _databaseService.getAllTodos();
120
+ if (localTodos.isNotEmpty) {
121
+ yield localTodos.map((model) => Todo.fromDbModel(model)).toList();
122
+ }
123
+
124
+ try {
125
+ // 2. Fetch fresh data from API
126
+ final remoteTodos = await _apiClientService.fetchTodos();
127
+
128
+ // 3. Update local database
129
+ await _databaseService.replaceAllTodos(remoteTodos);
130
+
131
+ // 4. Yield fresh data
132
+ yield remoteTodos.map((model) => Todo.fromApiModel(model)).toList();
133
+ } on Exception catch (e) {
134
+ // Handle network errors (UI will still have local data)
135
+ // Log error or yield a specific error state if required
136
+ }
137
+ }
138
+
139
+ /// Offline-first write: Save locally, then attempt remote sync.
140
+ Future<void> createTodo(Todo todo) async {
141
+ final dbModel = todo.toDbModel().copyWith(isSynced: false);
142
+
143
+ // 1. Save locally immediately
144
+ await _databaseService.insertTodo(dbModel);
145
+
146
+ try {
147
+ // 2. Attempt remote sync
148
+ final apiModel = await _apiClientService.postTodo(todo.toApiModel());
149
+
150
+ // 3. Mark as synced locally
151
+ await _databaseService.updateTodo(
152
+ dbModel.copyWith(id: apiModel.id, isSynced: true)
153
+ );
154
+ } on Exception catch (_) {
155
+ // Leave as isSynced: false for background sync task to pick up later
156
+ }
157
+ }
158
+}
159
+```
160
+
161
+### SQLite Database Service Implementation
162
+Demonstrates safe query construction using `whereArgs`.
163
+
164
+```dart
165
+class DatabaseService {
166
+ static const String _tableName = 'todos';
167
+ static const String _colId = 'id';
168
+ static const String _colTask = 'task';
169
+ static const String _colIsSynced = 'is_synced';
170
+
171
+ Database? _database;
172
+
173
+ Future<void> open() async {
174
+ if (_database != null) return;
175
+
176
+ final dbPath = join(await getDatabasesPath(), 'app_database.db');
177
+ _database = await openDatabase(
178
+ dbPath,
179
+ version: 1,
180
+ onCreate: (db, version) {
181
+ return db.execute(
182
+ 'CREATE TABLE $_tableName('
183
+ '$_colId INTEGER PRIMARY KEY AUTOINCREMENT, '
184
+ '$_colTask TEXT, '
185
+ '$_colIsSynced INTEGER)'
186
+ );
187
+ },
188
+ );
189
+ }
190
+
191
+ Future<void> updateTodo(TodoDbModel todo) async {
192
+ await _database!.update(
193
+ _tableName,
194
+ todo.toMap(),
195
+ where: '$_colId = ?',
196
+ whereArgs: [todo.id], // Prevents SQL injection
197
+ );
198
+ }
199
+}
200
+```
.agent/skills/riverpod-3-0-migration/SKILL.md
new
+74
@@ -0,0 +1,74 @@
1
+---
2
+name: riverpod-3-0-migration
3
+description: Migrate Riverpod from 2.0 to 3.0; automatic retry, paused listeners, legacy providers import, Ref simplification, FamilyNotifier removal, ProviderException, updateShouldNotify. Use when the user asks about Riverpod 3 migration, upgrading to Riverpod 3, or breaking changes in 3.0.
4
+---
5
+
6
+# Riverpod — Migrating from 2.0 to 3.0
7
+
8
+For a full list of changes and highlights, see the official Riverpod docs (What's new in Riverpod 3.0). This skill summarizes the main migration steps.
9
+
10
+## Automatic retry
11
+
12
+Failing providers are **retried by default** in 3.0. To disable globally:
13
+
14
+```dart
15
+ProviderScope(retry: (retryCount, error) => null, child: MyApp())
16
+// or ProviderContainer(retry: (retryCount, error) => null)
17
+```
18
+
19
+To disable per provider, pass **retry: (retryCount, error) => null** when defining the provider (or `@Riverpod(retry: ...)` with codegen). See riverpod-retry.
20
+
21
+## Out-of-view providers are paused
22
+
23
+Listeners in widgets that are not visible (e.g. off-screen) are **paused** by default. There is no global switch. To keep a subtree from pausing, wrap it in **TickerMode(enabled: true, child: ...)** so the Consumer(s) inside do not follow the pause behavior.
24
+
25
+## Legacy providers (StateProvider, StateNotifierProvider, ChangeNotifierProvider)
26
+
27
+These are **moved to a legacy import**. They are not removed but discouraged. To keep using them:
28
+
29
+```dart
30
+import 'package:flutter_riverpod/legacy.dart';
31
+// or hooks_riverpod/legacy.dart, riverpod/legacy.dart
32
+```
33
+
34
+## Updates use ==
35
+
36
+All providers now use **==** to filter updates (no longer **identical**). **StreamProvider** / **StreamNotifier** values are compared with ==. To customize, override **updateShouldNotify(previous, next)** on the Notifier (e.g. return true to always notify).
37
+
38
+## ProviderObserver API change
39
+
40
+Observer methods now receive a single **ProviderObserverContext** (with container, provider, and optional mutation) instead of separate (provider, value, container) parameters. Update signatures, e.g.:
41
+
42
+- **didAddProvider(ProviderObserverContext context, Object? value)** (and similar for other callbacks).
43
+
44
+## Ref simplified; Ref subclasses removed
45
+
46
+- **Ref** no longer has a type parameter. Use **Ref** everywhere (e.g. replace **MyProviderRef** with **Ref** in codegen).
47
+- **ProviderRef.state**, **Ref.listenSelf**, **FutureProviderRef.future** are **moved onto Notifier/AsyncNotifier**: use **Notifier.state**, **Notifier.listenSelf**, **AsyncNotifier.future** instead of on ref. If you need these, use a class-based Notifier/AsyncNotifier and call them on the notifier.
48
+
49
+## AutoDispose type names removed
50
+
51
+AutoDispose is still supported, but the separate type names (e.g. **AutoDisposeProvider**, **AutoDisposeNotifier**) are unified. Use **Provider**, **Notifier**, etc. with the same behavior. Migration: case-sensitive replace **AutoDispose** with a space (empty string) in type names.
52
+
53
+## Family Notifier types removed
54
+
55
+**FamilyNotifier**, **FamilyAsyncNotifier**, **FamilyStreamNotifier** are removed. Use **Notifier** / **AsyncNotifier** / **StreamNotifier** and pass the parameter via the **constructor** (and store it in a field). Remove the parameter from **build**; **build()** takes no family argument. Example:
56
+
57
+- Before: `FamilyNotifier<int, String>` with `int build(String arg)`.
58
+- After: `Notifier<int>` with constructor `MyNotifier(this.arg)` and `final String arg;`, and **int build()** using **arg**.
59
+
60
+## ProviderException
61
+
62
+When a provider throws, the exception is wrapped in **ProviderException**. If you catch the original type (e.g. **on NotFoundException**), change to **on ProviderException catch (e)** and check **e.exception is NotFoundException**. If you only use **AsyncValue** and **value.hasError** / **value.error**, no change needed.
63
+
64
+## Summary
65
+
66
+- Retry: disable via **retry** on scope/container or provider.
67
+- Pause: use **TickerMode(enabled: true)** where you don’t want pausing.
68
+- Legacy: use **flutter_riverpod/legacy.dart** (or equivalent) for StateProvider / StateNotifierProvider / ChangeNotifierProvider.
69
+- Ref: use plain **Ref**; move **state** / **listenSelf** / **future** to Notifier/AsyncNotifier.
70
+- AutoDispose: remove “AutoDispose” from type names.
71
+- Family notifiers: use Notifier/AsyncNotifier with constructor argument; **build()** has no parameter.
72
+- Errors: catch **ProviderException** and inspect **exception** when you need the original type.
73
+
74
+For version highlights and more detail, see the official Riverpod documentation.
.agent/skills/riverpod-auto-dispose/SKILL.md
new
+65
@@ -0,0 +1,65 @@
1
+---
2
+name: riverpod-auto-dispose
3
+description: Enable automatic disposal of Riverpod providers when they have no listeners; keepAlive, onDispose, invalidate, ref.keepAlive. Use when preventing memory leaks, caching only while used, or cleaning up resources when a provider is no longer needed. Use this skill when the user asks about auto-dispose, keepAlive, or when to dispose Riverpod state.
4
+---
5
+
6
+# Riverpod — Automatic disposal
7
+
8
+## Instructions
9
+
10
+With **automatic disposal** enabled, Riverpod destroys a provider's state when it has no listeners for one frame. This frees memory and stops work (e.g. network requests) when the provider is no longer used.
11
+
12
+### Enabling
13
+
14
+- **Code generation:** Enabled by default. Disable with `@Riverpod(keepAlive: true)`.
15
+- **Manual:** Add `isAutoDispose: true` when creating the provider (e.g. `Provider.autoDispose(...)` or `FutureProvider.autoDispose(...)`).
16
+
17
+```dart
18
+// Codegen: disable auto-dispose
19
+@Riverpod(keepAlive: true)
20
+String helloWorld(Ref ref) => 'Hello world!';
21
+
22
+// Manual: enable
23
+final helloWorldProvider = Provider<String>(
24
+ isAutoDispose: true,
25
+ (ref) => 'Hello world!',
26
+);
27
+```
28
+
29
+When a provider has **parameters** (family), enable auto-dispose to avoid caching every parameter combination forever and causing memory leaks.
30
+
31
+### When disposal runs
32
+
33
+Riverpod counts listeners (from ref.watch, ref.listen, etc.). When the count reaches zero, it waits one frame; if still zero, **ref.onCancel** runs, then the state is destroyed and **ref.onDispose** runs.
34
+
35
+### Reacting to disposal: ref.onDispose
36
+
37
+Register a callback when the state is destroyed (e.g. close a StreamController):
38
+
39
+```dart
40
+final provider = StreamProvider<int>((ref) {
41
+ final controller = StreamController<int>();
42
+ ref.onDispose(controller.close);
43
+ return controller.stream;
44
+});
45
+```
46
+
47
+Do not trigger side effects that modify other providers inside onDispose. You can call onDispose multiple times. Other lifecycles: **ref.onCancel** (last listener removed), **ref.onResume** (new listener added after cancel).
48
+
49
+### Forcing destruction: ref.invalidate
50
+
51
+From a widget or another provider, call **ref.invalidate(provider)** to destroy the current state. If the provider is still listened to, it will recompute; otherwise it stays destroyed until read again.
52
+
53
+```dart
54
+onPressed: () => ref.invalidate(someProvider),
55
+```
56
+
57
+For family providers you can invalidate one parameter combination or all (see riverpod-family and docs).
58
+
59
+### Keeping state alive: ref.keepAlive
60
+
61
+When auto-dispose is enabled, you can call **ref.keepAlive()** to prevent disposal until the next recomputation. Use to keep successful results alive while allowing failed or unused state to be disposed. The return value can be called to revert to automatic disposal. If the provider is recomputed, auto-dispose is re-enabled.
62
+
63
+### Caching for a duration
64
+
65
+Riverpod does not provide a built-in "cache for X seconds". You can implement it with a Timer and ref.keepAlive, or use ref.onCancel/onResume to dispose after a period of no listeners.
.agent/skills/riverpod-cancel/SKILL.md
new
+36
@@ -0,0 +1,36 @@
1
+---
2
+name: riverpod-cancel
3
+description: Cancel or debounce Riverpod async requests with ref.onDispose; cancel when user leaves the page, debounce rapid refreshes. Use when the user asks about cancelling requests, debouncing, or cleaning up when a provider is disposed.
4
+---
5
+
6
+# Riverpod — Cancelling and debouncing requests
7
+
8
+## Instructions
9
+
10
+Use **ref.onDispose** together with **auto-dispose** (or ref.watch) to cancel in-flight requests when the user leaves the page, or to debounce rapid refreshes.
11
+
12
+### Cancelling when leaving the page
13
+
14
+Ensure the provider is **auto-dispose**. When the user navigates away, the provider loses its listeners and is disposed; **ref.onDispose** runs. In that callback, cancel the request (e.g. close the HTTP client used for the request).
15
+
16
+Example pattern: create an `http.Client()` in the provider, pass it to your fetch logic, and call **ref.onDispose(client.close)**. When the provider is disposed, the client closes and the request is cancelled. Exact API depends on your HTTP package.
17
+
18
+### Debouncing refreshes
19
+
20
+If the user triggers refresh multiple times quickly, you can delay the actual request until they stop (e.g. 500ms). Pattern:
21
+
22
+1. In the provider, set a flag (e.g. `didDispose = false`) and **ref.onDispose(() => didDispose = true)**.
23
+2. **await Future.delayed(duration)**.
24
+3. If **didDispose** is true, the user triggered another refresh during the delay; **throw** to abort (Riverpod catches and ignores). Otherwise create the client, register **onDispose(client.close)**, and run the request.
25
+
26
+So: delay first, then create client and run request; disposal during the delay aborts, disposal after creates client cancels the request.
27
+
28
+### Reusable extension
29
+
30
+You can implement an extension on **Ref** that returns a debounced/cancellable client:
31
+
32
+- Use **onDispose** to set a "cancelled" flag.
33
+- Wait for the debounce duration; if disposed, throw.
34
+- Create the client, **onDispose(client.close)**, return the client.
35
+
36
+Use this extension in your activity/provider so all call sites get debounce + cancel behavior. See the official Riverpod "cancel" how-to for a full extension example.
.agent/skills/riverpod-codegen-and-hooks/SKILL.md
new
+105
@@ -0,0 +1,105 @@
1
+---
2
+name: riverpod-codegen-and-hooks
3
+description: Use Riverpod code generation (@riverpod, riverpod_generator) and hooks (hooks_riverpod, HookConsumerWidget, flutter_hooks with Riverpod). Use when the user asks about @riverpod, code generation, riverpod_generator, when to use codegen, or using flutter_hooks with Riverpod (HookConsumerWidget, HookConsumer).
4
+---
5
+
6
+# Riverpod — Code generation and hooks
7
+
8
+## Code generation
9
+
10
+Code generation is **optional** in Riverpod. It changes the syntax for defining providers: you write an annotated function or class and the generator produces the provider. Use it if you already use code generation (e.g. Freezed, json_serializable); otherwise the extra build step may not be worth it. See riverpod-getting-started for setup (build_runner, riverpod_generator).
11
+
12
+### Benefits
13
+
14
+- Clearer syntax: no manual provider type (Provider vs FutureProvider etc.); Riverpod infers it.
15
+- Parameters: any parameters (named, optional, defaults) instead of a single family parameter.
16
+- Stateful hot-reload for Riverpod code.
17
+- Better debugging via generated metadata.
18
+
19
+### Syntax
20
+
21
+**Functional provider** (sync):
22
+
23
+```dart
24
+@riverpod
25
+String example(Ref ref) {
26
+ return 'foo';
27
+}
28
+```
29
+
30
+**Class-based provider** (sync, with methods for side effects):
31
+
32
+```dart
33
+@riverpod
34
+class Example extends _$Example {
35
+ @override
36
+ String build() => 'foo';
37
+ // Add methods to mutate state
38
+}
39
+```
40
+
41
+**Async:** Use `Future`/`FutureOr`/`Stream`; async functions get AsyncValue and loading/error handling automatically. **Auto-dispose:** On by default with codegen. Disable with `@Riverpod(keepAlive: true)`.
42
+
43
+```dart
44
+@riverpod
45
+String example1(Ref ref) => 'foo';
46
+
47
+@Riverpod(keepAlive: true)
48
+String example2(Ref ref) => 'foo';
49
+```
50
+
51
+**Parameters:** Add parameters to the function (consistent == required):
52
+
53
+```dart
54
+@riverpod
55
+Future<User> fetchUser(Ref ref, {required int userId}) async {
56
+ final json = await http.get('api/user/$userId');
57
+ return User.fromJson(json);
58
+}
59
+```
60
+
61
+Manual equivalent would be `FutureProvider.autoDispose.family<User, int>(...)`. See riverpod-providers and riverpod-family for concepts.
62
+
63
+---
64
+
65
+## Hooks
66
+
67
+**Hooks** come from [flutter_hooks] (separate from Riverpod). They are for **local** widget state (e.g. TextEditingController, AnimationController) and can replace StatefulWidget or builder nesting. Use them only if you want hooks; they are not required for Riverpod. Newcomers should avoid hooks at first.
68
+
69
+### Using hooks and Riverpod together
70
+
71
+You need both **flutter_hooks** and **hooks_riverpod**. Then either:
72
+
73
+**Option 1: HookConsumerWidget** — One base class that supports both hooks and ref:
74
+
75
+```dart
76
+class Example extends HookConsumerWidget {
77
+ @override
78
+ Widget build(BuildContext context, WidgetRef ref) {
79
+ final counter = useState(0);
80
+ final value = ref.watch(myProvider);
81
+ return Text('Hello $counter $value');
82
+ }
83
+}
84
+```
85
+
86
+**Option 2: HookConsumer** — Builder that combines HookBuilder + Consumer (works with flutter_riverpod only if you nest them; hooks_riverpod provides HookConsumer):
87
+
88
+```dart
89
+return HookConsumer(
90
+ builder: (context, ref, child) {
91
+ final counter = useState(0);
92
+ final value = ref.watch(myProvider);
93
+ return Text('Hello $counter $value');
94
+ },
95
+);
96
+```
97
+
98
+**Option 3:** Nest `Consumer` and `HookBuilder` (no hooks_riverpod needed).
99
+
100
+### Rules of hooks
101
+
102
+- Use hooks only in the **build** method of a widget that extends **HookWidget** (or HookConsumerWidget).
103
+- Do not use hooks conditionally or in loops; call order must be stable.
104
+
105
+See riverpod-consumers for Ref in widgets and the official docs for full codegen/hooks details.
.agent/skills/riverpod-consumers/SKILL.md
new
+71
@@ -0,0 +1,71 @@
1
+---
2
+name: riverpod-consumers
3
+description: Use Riverpod Consumer, ConsumerWidget, and ConsumerStatefulWidget to read and watch providers in widgets; WidgetRef, builder ref parameter. Use when building widgets that need to access Riverpod providers, ref.watch or ref.read in the UI, or converting StatelessWidget to ConsumerWidget. Prefer this skill when the user asks how to use providers in Flutter widgets or why ConsumerWidget is required.
4
+---
5
+
6
+# Riverpod — Consumers
7
+
8
+## Instructions
9
+
10
+Consumers are widgets that give you a **Ref** (here, **WidgetRef**) so you can read and listen to providers. Without a Consumer, widgets cannot access the provider tree.
11
+
12
+### Consumer (builder)
13
+
14
+Use `Consumer` when you want to keep extending `StatelessWidget` or `StatefulWidget`. The builder callback receives `(context, ref, child)`.
15
+
16
+```dart
17
+class MyWidget extends StatelessWidget {
18
+ @override
19
+ Widget build(BuildContext context) {
20
+ return Consumer(
21
+ builder: (context, ref, _) {
22
+ final value = ref.watch(myProvider);
23
+ return Text(value.toString());
24
+ },
25
+ );
26
+ }
27
+}
28
+```
29
+
30
+### ConsumerWidget
31
+
32
+Subclass `ConsumerWidget` instead of `StatelessWidget`. The `build` method receives `(BuildContext context, WidgetRef ref)`.
33
+
34
+```dart
35
+class MyWidget extends ConsumerWidget {
36
+ @override
37
+ Widget build(BuildContext context, WidgetRef ref) {
38
+ final value = ref.watch(myProvider);
39
+ return Text(value.toString());
40
+ }
41
+}
42
+```
43
+
44
+### ConsumerStatefulWidget + ConsumerState
45
+
46
+When you need a `State` (e.g. for lifecycle or local state), use `ConsumerStatefulWidget` and `ConsumerState`. The state object has a `ref` property.
47
+
48
+```dart
49
+class MyWidget extends ConsumerStatefulWidget {
50
+ @override
51
+ ConsumerState<MyWidget> createState() => _MyWidgetState();
52
+}
53
+
54
+class _MyWidgetState extends ConsumerState<MyWidget> {
55
+ @override
56
+ Widget build(BuildContext context) {
57
+ final value = ref.watch(myProvider);
58
+ return Text(value.toString());
59
+ }
60
+}
61
+```
62
+
63
+### Which to use
64
+
65
+- **Consumer** — Use for everything if you prefer not to change your widget base class; slightly more verbose.
66
+- **ConsumerWidget** — Recommended when you don't need State; one less nesting level.
67
+- **ConsumerStatefulWidget** — Use when you need State (e.g. TabController, animations).
68
+
69
+### Why not StatelessWidget + context.watch?
70
+
71
+Riverpod does not use `BuildContext` to watch providers because that would break **auto-dispose** and other features that rely on knowing when a widget stops listening. Ref-based consumers allow reliable disposal and correct behavior. The hooks_riverpod package also offers HookConsumerWidget etc. for use with flutter_hooks. Enable riverpod_lint for IDE refactors (e.g. "Convert to ConsumerWidget").
.agent/skills/riverpod-containers/SKILL.md
new
+66
@@ -0,0 +1,66 @@
1
+---
2
+name: riverpod-containers
3
+description: Use ProviderScope in Flutter and ProviderContainer in Dart for Riverpod; where provider state is stored, overrides, observers, testing with ProviderContainer.test. Use when setting up Riverpod in Flutter or pure Dart, understanding where state lives, or writing tests with a fresh container. Use this skill when the user asks about ProviderScope, ProviderContainer, or where Riverpod stores state.
4
+---
5
+
6
+# Riverpod — Containers / Scopes
7
+
8
+## Instructions
9
+
10
+Providers themselves hold **no state**. State is stored in a **ProviderContainer**. In Flutter you use the **ProviderScope** widget, which creates and exposes a container to the widget tree.
11
+
12
+### Flutter: ProviderScope
13
+
14
+Wrap your app at the root so widgets can read providers:
15
+
16
+```dart
17
+void main() {
18
+ runApp(
19
+ ProviderScope(
20
+ child: MyApp(),
21
+ ),
22
+ );
23
+}
24
+```
25
+
26
+After that, use Consumer / ConsumerWidget / ConsumerStatefulWidget to get a `ref` and call `ref.watch` / `ref.read`. You can also pass `overrides` or `observers` to ProviderScope.
27
+
28
+### Pure Dart: ProviderContainer
29
+
30
+In command-line or server-side Dart, create a container, use it, then dispose it:
31
+
32
+```dart
33
+void main() {
34
+ final container = ProviderContainer();
35
+ try {
36
+ final sub = container.listen(counterProvider, (previous, next) {
37
+ print('Counter changed from $previous to $next');
38
+ });
39
+ print('Counter starts at ${sub.read()}');
40
+ } finally {
41
+ container.dispose();
42
+ }
43
+}
44
+```
45
+
46
+### Why state lives in a container
47
+
48
+- **Separation of concerns** — Only the provider (and ref) can change its state; the UI typically calls notifier methods.
49
+- **Testing** — Each test can create a new container (e.g. `ProviderContainer.test()`) for a fresh state; no shared global state.
50
+- **Configuration** — Overrides and observers are set on the container/scope. See riverpod-overrides and riverpod-observers.
51
+- **Scoping** — The same provider can resolve to different state in different parts of the tree (advanced). See riverpod-scoping.
52
+
53
+### Testing
54
+
55
+Do not use `ProviderContainer()` directly in tests. Use **ProviderContainer.test()** so the container is disposed when the test ends:
56
+
57
+```dart
58
+test('Counter starts at 0 and can be incremented', () {
59
+ final container = ProviderContainer.test();
60
+ expect(container.read(counterProvider), 0);
61
+ container.read(counterProvider.notifier).increment();
62
+ expect(container.read(counterProvider), 1);
63
+});
64
+```
65
+
66
+In widget tests, wrap the widget in `ProviderScope` and use `tester.container()` to get the container if needed (see riverpod-testing).
.agent/skills/riverpod-eager-initialization/SKILL.md
new
+46
@@ -0,0 +1,46 @@
1
+---
2
+name: riverpod-eager-initialization
3
+description: Eagerly initialize Riverpod providers at app startup by watching them in a root Consumer; handle loading/error in the initializer, AsyncValue.requireValue. Use when a provider must be ready before the rest of the app is used. Use this skill when the user asks about eager initialization or preloading providers.
4
+---
5
+
6
+# Riverpod — Eager initialization
7
+
8
+## Instructions
9
+
10
+Providers are **lazy** by default: they initialize on first use. There is no built-in "eager" flag (for tree-shaking). To force early initialization, **watch** the provider at the root of your app so it stays alive and runs immediately.
11
+
12
+### Approach
13
+
14
+Place a **Consumer** (or ConsumerWidget) directly under **ProviderScope** that only watches the providers you want to initialize and returns your real app as **child**:
15
+
16
+```dart
17
+void main() {
18
+ runApp(ProviderScope(child: MyApp()));
19
+}
20
+
21
+class MyApp extends StatelessWidget {
22
+ @override
23
+ Widget build(BuildContext context) {
24
+ return const _EagerInitialization(child: MaterialApp());
25
+ }
26
+}
27
+
28
+class _EagerInitialization extends ConsumerWidget {
29
+ const _EagerInitialization({required this.child});
30
+ final Widget child;
31
+
32
+ @override
33
+ Widget build(BuildContext context, WidgetRef ref) {
34
+ ref.watch(myProvider); // Eagerly initialize
35
+ return child;
36
+ }
37
+}
38
+```
39
+
40
+Only the initializer widget rebuilds when the provider changes; **child** is unchanged so Flutter does not rebuild the rest of the tree unless something else is listening.
41
+
42
+### Async: loading and error
43
+
44
+If the provider is async, handle loading/error in the initializer (e.g. show CircularProgressIndicator or error until ready, then return child). Other widgets can use **AsyncValue.requireValue** to read the data without pattern matching; if they run before the value is ready, it throws with a clear message.
45
+
46
+Put the initializer in a shared widget (e.g. MyApp) so tests can reuse the same setup by using that widget.
.agent/skills/riverpod-family/SKILL.md
new
+66
@@ -0,0 +1,66 @@
1
+---
2
+name: riverpod-family
3
+description: Use Riverpod family providers to pass parameters and cache per parameter; FutureProvider.family, NotifierProvider.family, autoDispose with family, overriding in tests. Use when fetching data by ID, pagination, or any provider that depends on a parameter. Use this skill when the user asks about family, provider parameters, or caching by ID.
4
+---
5
+
6
+# Riverpod — Family
7
+
8
+## Instructions
9
+
10
+**Family** lets a single provider have multiple independent states, one per parameter combination (like a Map from parameter to state). Use it for API calls that depend on an ID, query, or page number.
11
+
12
+### Creating a family (functional)
13
+
14
+Add `.family` and an extra type parameter for the argument. The provider function receives `(ref, param)`:
15
+
16
+```dart
17
+final userProvider = FutureProvider.autoDispose.family<User, String>((ref, id) async {
18
+ final dio = Dio();
19
+ final response = await dio.get('https://api.example.com/users/$id');
20
+ return User.fromJson(response.data);
21
+});
22
+```
23
+
24
+With code generation, add parameters to the function; the generated provider accepts arguments: `userProvider(id)`.
25
+
26
+### Creating a family (Notifier)
27
+
28
+Use `NotifierProvider.family` / `AsyncNotifierProvider.family` (and `.autoDispose.family`). The Notifier must have a constructor that stores the parameter:
29
+
30
+```dart
31
+final userProvider = AsyncNotifierProvider.autoDispose.family<UserNotifier, User, String>(
32
+ UserNotifier.new,
33
+);
34
+
35
+class UserNotifier extends AsyncNotifier<User> {
36
+ UserNotifier(this.id);
37
+ final String id;
38
+
39
+ @override
40
+ Future<User> build() async {
41
+ final dio = Dio();
42
+ final response = await dio.get('https://api.example.com/users/$id');
43
+ return User.fromJson(response.data);
44
+ }
45
+}
46
+```
47
+
48
+### Using a family
49
+
50
+Pass the parameter when watching or reading:
51
+
52
+```dart
53
+final user = ref.watch(userProvider('123'));
54
+final other = ref.watch(userProvider('456')); // independent state
55
+```
56
+
57
+Parameters must have consistent **==** and **hashCode**. Do not use mutable or list/map literals as parameters (e.g. `ref.watch(myProvider([1,2,3]))` is wrong because `[1,2,3] != [1,2,3]`). Prefer primitives or custom classes with ==/hashCode. Enable riverpod_lint provider_parameters rule to catch mistakes.
58
+
59
+### Auto-dispose with family
60
+
61
+Enable auto-dispose when using family so that when the parameter goes out of use (e.g. user navigates away), that parameter's state can be disposed and you avoid unbounded cache growth.
62
+
63
+### Overriding in tests
64
+
65
+- Override a single parameter: pass the same parameter to the override.
66
+- Override all parameters: override with a provider that ignores the parameter or returns a fixed value. See riverpod-overrides and the docs for exact syntax.
.agent/skills/riverpod-faq-and-practices/SKILL.md
new
+58
@@ -0,0 +1,58 @@
1
+---
2
+name: riverpod-faq-and-practices
3
+description: Answers Riverpod FAQ (ref.refresh vs invalidate, ConsumerWidget vs StatelessWidget, Ref vs WidgetRef, reset all providers, ref after unmount) and do/don't best practices (avoid init in widgets, avoid ephemeral state in providers, avoid side effects in provider init, static providers, riverpod_lint). Use when the user asks Riverpod FAQ, best practices, or do/don't guidelines.
4
+---
5
+
6
+# Riverpod — FAQ and best practices
7
+
8
+## FAQ
9
+
10
+### ref.refresh vs ref.invalidate
11
+
12
+**ref.refresh** = **ref.invalidate** + **ref.read**: it invalidates the provider and returns the new value. Use **invalidate** when you don't need the new value (e.g. "recompute when the user pulls to refresh"). Use **refresh** when you need the result right away. After invalidate alone, recomputation can happen at the next frame or when the provider is next read; refresh forces immediate recomputation by reading.
13
+
14
+### Why no shared interface between Ref and WidgetRef?
15
+
16
+Ref (in providers) and WidgetRef (in widgets) are kept separate on purpose so you don't write code that conditionally depends on both; they have subtle differences and mixing would be error-prone. Prefer **Ref**: put logic in a Notifier and call **ref.read(notifierProvider.notifier).yourMethod()** from the UI; the method uses the Notifier's Ref.
17
+
18
+### Why ConsumerWidget instead of StatelessWidget?
19
+
20
+InheritedWidget (and thus BuildContext) cannot support "on change" listeners like **ref.listen**, cannot know when widgets stop listening (needed for auto-dispose and family), and has lifecycle quirks (e.g. with GlobalKeys). Riverpod needs a Ref that isn't tied to BuildContext for these features. Hence ConsumerWidget (and Consumer) provide a Ref.
21
+
22
+### Why doesn't hooks_riverpod export flutter_hooks?
23
+
24
+So each package can be versioned independently; a breaking change in one doesn't force the other to break.
25
+
26
+### Reset all providers at once?
27
+
28
+There is no API to reset all providers; it's considered an anti-pattern. For "logout and clear state", have providers that depend on the current user **ref.watch** a user provider; when the user logs out, that provider changes and dependents recompute or clear. Only user-dependent state resets; the rest stays.
29
+
30
+### "Using ref when a widget is about to or has been unmounted is unsafe"
31
+
32
+This (or "No ProviderScope found") happens when **ref** is used after an **await** in a widget that may have been disposed. After any **await**, check **if (!context.mounted) return;** before using **ref** (same pattern as with BuildContext).
33
+
34
+---
35
+
36
+## Do / Don't
37
+
38
+### AVOID initializing providers in a widget
39
+
40
+Providers should initialize themselves. Don't call something like **ref.read(provider).init()** from **initState**. If initialization depends on user action (e.g. navigation), trigger it from the action (e.g. **onPressed** before **Navigator.push**).
41
+
42
+### AVOID using providers for ephemeral state
43
+
44
+Use providers for **shared business state**, not for: selected tab/item, form state (which should reset on leave/back), animations, or controller-like state (e.g. TextEditingController). For local widget state use **flutter_hooks** or local state. Storing "selected item" in a global provider can break back navigation (e.g. back from /books/21 should show /books/42, but the provider still holds 21).
45
+
46
+### DON'T perform side effects during provider initialization
47
+
48
+Providers should represent "read" operations. Don't use a provider's build to perform "write" operations (e.g. submitting a form). That can lead to skipped or duplicated side effects. For loading/error of a side effect use **mutations** (see riverpod-mutations).
49
+
50
+### PREFER statically known providers with ref.watch/read/listen
51
+
52
+Use **ref.watch(provider)** where **provider** is a top-level final (or otherwise statically known). Avoid passing a provider as a parameter and then watching it so that static analysis and riverpod_lint can work.
53
+
54
+### AVOID dynamically creating providers
55
+
56
+Define providers as **top-level final** variables. Don't create them inside classes or as instance fields; that can cause memory leaks and unsupported behavior. Static final in a class is allowed but not supported by code generation.
57
+
58
+Enable **riverpod_lint** (see riverpod-getting-started) to enforce many of these practices.
.agent/skills/riverpod-from-provider/SKILL.md
new
+97
@@ -0,0 +1,97 @@
1
+---
2
+name: riverpod-from-provider
3
+description: Migrate from package:provider to Riverpod; ChangeNotifierProvider, ProxyProvider to ref.watch, context.watch to ref.watch, ConsumerWidget, incremental migration, family and autoDispose. Use when the user is migrating from Provider to Riverpod, or asks about Provider vs Riverpod, or how to replace ProxyProvider/ChangeNotifierProvider.
4
+---
5
+
6
+# Riverpod — Migrating from Provider
7
+
8
+## Motivation
9
+
10
+Riverpod was created as a successor to Provider, addressing InheritedWidget limitations:
11
+
12
+- **Same type:** Provider can't have two `Provider<Item>` in the tree (only the nearest is found). Riverpod has no such limit; providers are identified by variable, not type.
13
+- **Combining providers:** ProxyProvider is tedious and error-prone. In Riverpod, use **ref.watch** inside a provider to depend on others; composition is straightforward.
14
+- **AsyncValue:** Riverpod can expose previous data while loading (e.g. show old list + loading indicator). Provider doesn't offer this cleanly.
15
+- **Safety:** Provider can throw `ProviderNotFoundException` at runtime. Riverpod avoids this by design.
16
+- **Disposal:** Provider can't react when consumers stop listening; scoping is tricky. Riverpod offers **autoDispose** and **ref.keepAlive** for clear lifecycle and caching.
17
+- **Parameters:** Riverpod's **.family** gives type-safe, parameterized providers with per-parameter state; with autoDispose, state is disposed when unused. Equivalent in Provider is impractical.
18
+- **Testing:** With Provider you must re-define providers per test. With Riverpod, override with **overrides** to mock.
19
+- **Side effects:** Riverpod offers **ref.listen** for reacting to changes (e.g. navigation, snackbars). Provider has no built-in equivalent.
20
+
21
+The main API change: use **ConsumerWidget** (and **WidgetRef**) instead of StatelessWidget, and **ref.watch** / **ref.read** instead of **context.watch** / **context.read**.
22
+
23
+---
24
+
25
+## Quickstart
26
+
27
+- Read the Riverpod getting started guide (riverpod-getting-started) and try a small example.
28
+- Migrate **incrementally**. You can run Provider and Riverpod side by side (use import aliases if needed).
29
+
30
+### Start with ChangeNotifierProvider
31
+
32
+Keep existing **ChangeNotifier** classes and wrap them in Riverpod's **ChangeNotifierProvider**:
33
+
34
+```dart
35
+final myNotifierProvider = ChangeNotifierProvider<MyNotifier>((ref) => MyNotifier());
36
+```
37
+
38
+Use **ProviderScope** at the root. Replace **context.watch** with **ref.watch** where this provider is used (e.g. in a ConsumerWidget). No need to convert every ChangeNotifier to a Notifier immediately.
39
+
40
+### Start with leaves
41
+
42
+Migrate providers that have no dependencies first (the "leaves"), then those that depend on them. Avoid migrating ProxyProviders until their dependencies are migrated.
43
+
44
+### One provider at a time
45
+
46
+Migrate and test one provider at a time. Full migration of a ChangeNotifier means: (1) convert to **Notifier** + **NotifierProvider**, (2) replace every **context.watch** for it with **ref.watch**.
47
+
48
+### ProxyProvider → ref.watch
49
+
50
+In Riverpod, combining providers is done with **ref.watch** inside another provider:
51
+
52
+```dart
53
+final labelProvider = Provider<String>((ref) {
54
+ final userIdNotifier = ref.watch(userIdNotifierProvider);
55
+ return 'The user ID is ${userIdNotifier.userId}';
56
+});
57
+```
58
+
59
+For stateful combined objects (like ChangeNotifierProxyProvider), use **ref.listen** in the provider to react to another provider and update your notifier.
60
+
61
+### Eager initialization
62
+
63
+Riverpod providers are lazy. To warm data at startup, watch the provider at the root (e.g. in a Consumer under ProviderScope that returns your app as child). See riverpod-eager-initialization.
64
+
65
+### Code generation
66
+
67
+Code gen doesn't generate ChangeNotifierProvider. You can use a small extension (listenAndDisposeChangeNotifier) to expose ChangeNotifier with @riverpod during migration; once you switch to Notifier, remove the extension. See the official quickstart for the snippet.
68
+
69
+---
70
+
71
+## Provider vs Riverpod
72
+
73
+### Defining providers
74
+
75
+- **Provider:** Providers are widgets (e.g. inside MultiProvider).
76
+- **Riverpod:** Providers are top-level **final** variables. No widget tree for definitions. Add **ProviderScope** at the root of the app.
77
+
78
+### Reading
79
+
80
+- **Provider:** `context.watch<T>()`, `context.read<T>()`, `context.select<T,R>(...)`.
81
+- **Riverpod:** Use **ConsumerWidget** (or Consumer) to get **WidgetRef ref**; then **ref.watch(provider)**, **ref.read(provider)**, **ref.watch(provider.select(...))**.
82
+- Use **watch** in build, **read** in event handlers. Same mental model as Provider.
83
+
84
+### Consumer
85
+
86
+Riverpod has **Consumer** with `(context, ref, child)`. No need for Consumer2, Consumer3, etc.: just multiple **ref.watch** calls in one builder.
87
+
88
+### Scoping vs family + autoDispose
89
+
90
+Provider uses scoping to destroy state or have per-page state. In Riverpod:
91
+
92
+- **autoDispose** destroys state when there are no listeners (ref.onCancel / ref.onDispose).
93
+- **.family** gives parameterized providers (one state per parameter). Use with autoDispose to avoid unbounded cache.
94
+
95
+So: use **.family** for "state per X" and **.autoDispose** for "destroy when unused" instead of scoping.
96
+
97
+See riverpod-getting-started, riverpod-providers, riverpod-family, and riverpod-auto-dispose for details.
.agent/skills/riverpod-getting-started/SKILL.md
new
+139
@@ -0,0 +1,139 @@
1
+---
2
+name: riverpod-getting-started
3
+description: Install Riverpod (flutter_riverpod or riverpod), wrap the app in ProviderScope, run a hello-world provider, and optionally enable riverpod_lint and code generation. Use when starting a Flutter or Dart project with Riverpod, adding the Riverpod dependency, or setting up ProviderScope and a first provider. For version highlights see the official Riverpod docs.
4
+---
5
+
6
+# Riverpod — Getting Started
7
+
8
+## Instructions
9
+
10
+### Installing the package
11
+
12
+Riverpod is available as a main package (Flutter: `flutter_riverpod`; Dart-only: `riverpod`). Optional packages add code generation and hooks.
13
+
14
+**Flutter:**
15
+
16
+```bash
17
+flutter pub add flutter_riverpod
18
+```
19
+
20
+With code generation:
21
+
22
+```bash
23
+flutter pub add flutter_riverpod riverpod_annotation
24
+flutter pub add dev:riverpod_generator build_runner
25
+```
26
+
27
+**Dart only:**
28
+
29
+```bash
30
+dart pub add riverpod
31
+```
32
+
33
+With code generation:
34
+
35
+```bash
36
+dart pub add riverpod riverpod_annotation
37
+dart pub add dev:riverpod_generator build_runner
38
+```
39
+
40
+**Manual `pubspec.yaml` (Flutter):**
41
+
42
+```yaml
43
+dependencies:
44
+ flutter:
45
+ sdk: flutter
46
+ flutter_riverpod: ^2.0.0 # use latest from pub.dev
47
+
48
+dev_dependencies:
49
+ build_runner:
50
+ riverpod_generator: ^2.0.0 # if using code generation
51
+```
52
+
53
+Then run `flutter pub get` (or `dart pub get` for Dart-only).
54
+
55
+If using code generation, run:
56
+
57
+```sh
58
+dart run build_runner watch -d
59
+```
60
+
61
+**Note:** Some Riverpod packages may require Flutter's beta channel for compatibility with latest `json_serializable`. If `pub get` fails, try `flutter channel beta` or use Riverpod `<=3.1.0`.
62
+
63
+### Wrap the app in ProviderScope
64
+
65
+Widgets need a `ProviderScope` (Flutter) or a `ProviderContainer` (Dart) to read providers.
66
+
67
+**Flutter — ProviderScope:**
68
+
69
+```dart
70
+import 'package:flutter/material.dart';
71
+import 'package:flutter_riverpod/flutter_riverpod.dart';
72
+
73
+void main() {
74
+ runApp(
75
+ ProviderScope(
76
+ child: MyApp(),
77
+ ),
78
+ );
79
+}
80
+```
81
+
82
+**Dart only — ProviderContainer:**
83
+
84
+```dart
85
+final container = ProviderContainer();
86
+final value = container.read(helloWorldProvider);
87
+print(value);
88
+// Don't forget to container.dispose() when done
89
+```
90
+
91
+### Hello world (Flutter with code generation)
92
+
93
+```dart
94
+import 'package:flutter/material.dart';
95
+import 'package:flutter_riverpod/flutter_riverpod.dart';
96
+import 'package:riverpod_annotation/riverpod_annotation.dart';
97
+
98
+part 'main.g.dart';
99
+
100
+@riverpod
101
+String helloWorld(Ref ref) {
102
+ return 'Hello world';
103
+}
104
+
105
+void main() {
106
+ runApp(ProviderScope(child: MyApp()));
107
+}
108
+
109
+class MyApp extends ConsumerWidget {
110
+ @override
111
+ Widget build(BuildContext context, WidgetRef ref) {
112
+ final String value = ref.watch(helloWorldProvider);
113
+ return MaterialApp(
114
+ home: Scaffold(
115
+ appBar: AppBar(title: const Text('Example')),
116
+ body: Center(child: Text(value)),
117
+ ),
118
+ );
119
+ }
120
+}
121
+```
122
+
123
+Run `dart run build_runner build` (or `watch`) to generate `main.g.dart`. Without code generation, use a manual provider (e.g. `final helloWorldProvider = Provider<String>((ref) => 'Hello world');`) and the same `ProviderScope` + `ConsumerWidget` pattern.
124
+
125
+### Enabling riverpod_lint
126
+
127
+Add the optional linter via `analysis_options.yaml`:
128
+
129
+```yaml
130
+plugins:
131
+ riverpod_lint: ^2.0.0 # use latest from https://pub.dev/packages/riverpod_lint
132
+```
133
+
134
+This provides lint rules and refactorings for Riverpod. See the riverpod_lint package page for the full list.
135
+
136
+### Going further
137
+
138
+- **Code generation and hooks:** See the riverpod-codegen-and-hooks skill (or docs: About code generation, About hooks).
139
+- **VS Code / Android Studio:** Consider Flutter Riverpod Snippets extensions for faster boilerplate.
.agent/skills/riverpod-migration/SKILL.md
new
+29
@@ -0,0 +1,29 @@
1
+---
2
+name: riverpod-migration
3
+description: Migrate Riverpod from StateNotifier to Notifier/AsyncNotifier, from ChangeNotifier to AsyncNotifier, or upgrade from 0.13/0.14/1.0; ref.onDispose, family, lifecycle, riverpod migrate CLI. Use when the user asks about migrating from StateNotifier, from ChangeNotifier, upgrading Riverpod 0.13 to 0.14, 0.14 to 1.0, or Riverpod migration guides.
4
+---
5
+
6
+# Riverpod — Migration guides
7
+
8
+## Instructions
9
+
10
+This skill points to detailed migration guides for different scenarios. Use the reference file that matches your case.
11
+
12
+### When to use which guide
13
+
14
+| Scenario | Reference |
15
+|----------|-----------|
16
+| **From StateNotifier** to Notifier / AsyncNotifier | [migration_from_state_notifier.md](reference/migration_from_state_notifier.md) |
17
+| **From ChangeNotifier** to AsyncNotifier | [migration_from_change_notifier.md](reference/migration_from_change_notifier.md) |
18
+| **Upgrade 0.13.x → 0.14.x** (StateNotifierProvider syntax, watch(provider) vs provider.state) | [migration_0_13_0_14.md](reference/migration_0_13_0_14.md) |
19
+| **Upgrade 0.14.x → 1.0** (ScopedReader → WidgetRef, useProvider → ref.watch, StateProvider) | [migration_0_14_1_0.md](reference/migration_0_14_1_0.md) |
20
+
21
+For **Riverpod 2.0 → 3.0** see the **riverpod-3-0-migration** skill.
22
+
23
+### Common themes
24
+
25
+- **StateNotifier → Notifier/AsyncNotifier:** Put init logic in `build`; use `ref.onDispose` instead of overloading `dispose`; no `mounted`—use cancellation (e.g. Completer or HTTP cancel token). Consumers stay the same (ref.watch, methods on notifier).
26
+- **ChangeNotifier → AsyncNotifier:** Replace `isLoading`/`hasError`/manual try-catch with `AsyncValue`; single `state`; mutations by reassigning `state`.
27
+- **CLI:** For 0.14 and 1.0 upgrades you can run `dart pub global activate riverpod_cli` then `riverpod migrate` in the project (do not upgrade Riverpod manually first; the tool will suggest version and changes).
28
+
29
+Open the relevant reference file for step-by-step instructions and code examples.
.agent/skills/riverpod-migration/reference/migration_0_13_0_14.md
new
+23
@@ -0,0 +1,23 @@
1
+# Migration: 0.13.x → 0.14.x
2
+
3
+- In **0.14.0** the way **StateNotifierProvider** and state/notifier reading work changed.
4
+
5
+## Changes
6
+
7
+1. **StateNotifierProvider** gets an extra type parameter: the **state** type.
8
+ - Before: `StateNotifierProvider<MyStateNotifier>(...)`
9
+ - After: `StateNotifierProvider<MyStateNotifier, MyModel>(...)`
10
+
11
+2. **Reading the notifier**
12
+ - Before: `watch(provider)` returned the StateNotifier.
13
+ - After: use **provider.notifier**: `watch(provider.notifier)` to get the StateNotifier.
14
+
15
+3. **Reading the state**
16
+ - Before: `watch(provider.state)` for the state.
17
+ - After: `watch(provider)` returns the state directly.
18
+
19
+## CLI migration
20
+
21
+- Install: `dart pub global activate riverpod_cli`
22
+- In the project (do **not** upgrade Riverpod first): run **riverpod migrate**.
23
+- The tool will suggest edits (e.g. `watch(provider.state)` → `watch(provider)`). Accept with `y` or reject with `n`.
.agent/skills/riverpod-migration/reference/migration_0_14_1_0.md
new
+30
@@ -0,0 +1,30 @@
1
+# Migration: 0.14.x → 1.0
2
+
3
+- **1.0** unifies the API for reading providers and changes some types.
4
+
5
+## CLI migration
6
+
7
+- Install: `dart pub global activate riverpod_cli`
8
+- **Do not** upgrade to 1.0 before running the tool; stay on 0.14.x.
9
+- Run **riverpod migrate** in the project; it will suggest changes and can upgrade the dependency.
10
+
11
+## Syntax unification
12
+
13
+- **Single syntax**: **ref.watch(provider)** (and ref.read). **useProvider**, **watch(provider)** without ref, and **context.read** are removed.
14
+
15
+- **ConsumerWidget / Consumer**
16
+ - Before: `build(BuildContext context, ScopedReader watch)` and `watch(provider)`.
17
+ - After: `build(BuildContext context, WidgetRef ref)` and **ref.watch(provider)**.
18
+ - Consumer: `builder: (context, watch, child)` → `builder: (context, ref, child)` and use **ref.watch(provider)**.
19
+
20
+- **HookWidget**
21
+ - **useProvider(provider)** is removed. Use **HookConsumerWidget** and **ref.watch(provider)** in build.
22
+
23
+- **context.read**
24
+ - Use **ConsumerWidget** (or Consumer) and **ref.read(provider)** instead of **context.read**.
25
+
26
+## StateProvider
27
+
28
+- **ref.watch(StateProvider)** used to return a **StateController**; in 1.0 it returns the **state** value only.
29
+ - If you only displayed the value: change to **ref.watch(provider)** and use the value directly.
30
+ - If you need the controller: use **ref.watch(provider.state)** to keep the old behavior (StateController).
.agent/skills/riverpod-migration/reference/migration_from_change_notifier.md
new
+36
@@ -0,0 +1,36 @@
1
+# Migration: From ChangeNotifier to AsyncNotifier
2
+
3
+- For apps already on Riverpod using **ChangeNotifierProvider**, this guide moves to **AsyncNotifier** (and **Notifier** where state is sync).
4
+
5
+## Why migrate
6
+
7
+- ChangeNotifier leads to **isLoading** / **hasError** flags and lots of **try/catch/finally** and **notifyListeners()**.
8
+- AsyncNotifier uses a single **state** and **AsyncValue**; Riverpod handles loading/error; less boilerplate and fewer inconsistent states.
9
+
10
+## Steps
11
+
12
+1. **Choose the notifier type**
13
+ - Side effects (methods that change state)? → Class-based (Notifier or AsyncNotifier).
14
+ - Async load (e.g. network)? → **AsyncNotifier<T>**.
15
+ - Parameters? → Add them to the notifier (e.g. constructor + field) and use family/provider that passes them.
16
+
17
+2. **Declaration**
18
+ - Replace **ChangeNotifierProvider** with **AsyncNotifierProvider** (or NotifierProvider if sync).
19
+ - State type: e.g. **AsyncNotifier<List<Todo>>**; no separate **todos** / **isLoading** / **hasError**—just **state** and AsyncValue.
20
+
21
+3. **Initialization**
22
+ - Move init from a custom method (e.g. **init**) into **build**.
23
+ - **build** returns **Future<List<Todo>>**; Riverpod exposes **AsyncValue<List<Todo>>** to the UI. No manual try/catch in build.
24
+
25
+4. **Mutations**
26
+ - Replace flag updates and notifyListeners with **state = newValue** (and **state = AsyncValue.data(...)** or **AsyncValue.error(...)** if you construct explicitly). Use **ref** for dependencies (ref.watch, ref.read) inside methods.
27
+
28
+5. **Consumers**
29
+ - **ref.watch(provider)** now gets **AsyncValue<T>**; handle loading/error/data in the UI. Use **ref.read(provider.notifier)** to call methods.
30
+
31
+## Summary
32
+
33
+- Init in **build**; no **todos**/isLoading/hasError.
34
+- No try-catch-finally in build; return the Future.
35
+- Side effects: reassign **state**; use **ref** for other providers.
36
+- See riverpod-providers and riverpod-refs for Notifier/AsyncNotifier and Ref APIs.
.agent/skills/riverpod-migration/reference/migration_from_state_notifier.md
new
+42
@@ -0,0 +1,42 @@
1
+# Migration: From StateNotifier to Notifier / AsyncNotifier
2
+
3
+- **StateNotifier** is deprecated in favor of **Notifier** and **AsyncNotifier** (Riverpod 2.0+).
4
+
5
+## Main differences
6
+
7
+- Dependencies: in StateNotifier they were in the provider; in Notifier they go in **build** via **ref.watch**.
8
+- Initialization: StateNotifier split between provider and constructor; Notifier uses a single **build** method; no logic in the constructor.
9
+- Lifecycle: StateNotifier had **dispose** on the class. Notifier/AsyncNotifier use **ref.onDispose** (and ref.onCancel) in build, like any other provider. Only internal state is disposed on rebuild; the notifier type itself is not disposed the same way.
10
+- **mounted**: Notifier/AsyncNotifier do not have **mounted**. Use cancellation (Completer, Dio CancelToken, etc.) instead of checking mounted after async work.
11
+
12
+## Sync: StateNotifier → Notifier
13
+
14
+- Replace **StateNotifierProvider** with **NotifierProvider**.
15
+- Move reactive dependencies from the provider into **build** with **ref.watch**.
16
+- Put all init logic in **build**; leave constructor minimal.
17
+- Use **ref.onDispose** in build for cleanup (no override of dispose).
18
+
19
+## Async: StateNotifier → AsyncNotifier
20
+
21
+- Replace **StateNotifierProvider** with **AsyncNotifierProvider**.
22
+- Move async init into **build** returning **Future<T>**; remove AsyncValue.guard / try-catch in build.
23
+- Use **future** and **update** on AsyncNotifier for simpler async flows.
24
+- Migrating from **StateNotifier<AsyncValue<T>>** to **AsyncNotifier<T>**: put init in build, remove manual try/catch and AsyncValue handling in build.
25
+
26
+## Family and autoDispose
27
+
28
+- With Notifier/AsyncNotifier, family parameters are passed into the class (e.g. constructor + field) and are available in build; no separate “family” type.
29
+- AutoDispose is expressed the same as other providers (e.g. ref.onDispose in build).
30
+
31
+## Consumers and mutations
32
+
33
+- **Consumers do not change**: ref.watch(provider), ref.read(provider.notifier), and calling notifier methods stay the same.
34
+- **ref.listen** replaces StateNotifier’s **.addListener** / **.stream**: use **ref.listen(provider, (prev, next) { ... })**.
35
+
36
+## Tests
37
+
38
+- **.debugState** is gone. Use **ref.read(provider)** or **ref.read(provider.notifier)** to get the notifier/state in tests; do not instantiate Notifier/AsyncNotifier by hand (ref and family args would be unset).
39
+
40
+## StateProvider
41
+
42
+- **StateProvider** is deprecated with StateNotifierProvider. Migrate to **Notifier**: one Notifier with **build** returning the initial value and methods that set **state**.
.agent/skills/riverpod-mutations/SKILL.md
new
+80
@@ -0,0 +1,80 @@
1
+---
2
+name: riverpod-mutations
3
+description: Use Riverpod mutations for UI-driven async operations like form submit; Mutation.run, MutationIdle/Pending/Success/Error, Mutation.reset, keyed mutations. Use when showing loading/error/success for a single action (e.g. submit button) without polluting provider state. Use this skill when the user asks about mutations, loading state for actions, or form submission with Riverpod.
4
+---
5
+
6
+# Riverpod — Mutations (experimental)
7
+
8
+## Instructions
9
+
10
+**Mutations** represent a single async operation the UI can react to (e.g. "add todo" in progress, succeeded, or failed). They keep loading/error/success out of your main provider state. The API is experimental and may change.
11
+
12
+### Defining a mutation
13
+
14
+Create a `Mutation<T>` instance (T is the result type, optional):
15
+
16
+```dart
17
+final addTodo = Mutation<Todo>();
18
+```
19
+
20
+Typically store it as a global or `static final` on a Notifier.
21
+
22
+### Listening
23
+
24
+Use ref.watch in a Consumer (or inside a provider) to drive the UI:
25
+
26
+```dart
27
+final addTodoState = ref.watch(addTodo);
28
+
29
+// In build:
30
+backgroundColor: switch (addTodoState) {
31
+ MutationError() => const WidgetStatePropertyAll(Colors.red),
32
+ _ => null,
33
+},
34
+if (addTodoState is MutationPending) ...[
35
+ const SizedBox(width: 8),
36
+ const CircularProgressIndicator(),
37
+],
38
+```
39
+
40
+### States
41
+
42
+- **MutationIdle** — Not started or reset.
43
+- **MutationPending** — In progress.
44
+- **MutationSuccess** — Succeeded; result available.
45
+- **MutationError** — Failed; error available.
46
+
47
+Use a `switch` on the state to handle each case.
48
+
49
+### Triggering
50
+
51
+Call **Mutation.run(ref, callback)**. The callback receives a transaction object (e.g. `tsx`) to access providers and must return a value of type T:
52
+
53
+```dart
54
+addTodo.run(ref, (tsx) async {
55
+ final todoNotifier = tsx.get(todoNotifierProvider.notifier);
56
+ final createdTodo = await todoNotifier.addTodo('Eat a cookie');
57
+ return createdTodo;
58
+});
59
+```
60
+
61
+Use `tsx.get(provider)` to keep providers alive for the duration of the mutation.
62
+
63
+### Keyed mutations
64
+
65
+Pass a key to have multiple instances (e.g. one mutation per list item):
66
+
67
+```dart
68
+ref.watch(deleteItemMutation(itemId));
69
+deleteItemMutation(itemId).run(ref, (tsx) async { ... });
70
+```
71
+
72
+### Resetting
73
+
74
+Mutations reset to Idle when they complete or when all listeners are removed. To reset manually:
75
+
76
+```dart
77
+addTodo.reset(ref);
78
+```
79
+
80
+See the official Riverpod docs for the latest mutation API and generics.
.agent/skills/riverpod-observers/SKILL.md
new
+52
@@ -0,0 +1,52 @@
1
+---
2
+name: riverpod-observers
3
+description: Use ProviderObserver to log or debug Riverpod provider lifecycle; didUpdateProvider, ProviderScope observers, naming providers. Use when adding logging, analytics, or debugging for provider state changes. Use this skill when the user asks about ProviderObserver, logging Riverpod, or debugging provider updates.
4
+---
5
+
6
+# Riverpod — ProviderObservers
7
+
8
+## Instructions
9
+
10
+A **ProviderObserver** receives lifecycle events for providers (e.g. when a provider's value changes). Use it for logging, analytics, or debugging.
11
+
12
+### Implementing an observer
13
+
14
+Extend **ProviderObserver** and override the methods you need (e.g. **didUpdateProvider**). Pass instances to **ProviderScope** or **ProviderContainer** via the `observers` parameter.
15
+
16
+### Example: logger
17
+
18
+```dart
19
+final class Logger extends ProviderObserver {
20
+ @override
21
+ void didUpdateProvider(
22
+ ProviderObserverContext context,
23
+ Object? previousValue,
24
+ Object? newValue,
25
+ ) {
26
+ print('{"provider": "${context.provider}", "newValue": "$newValue"}');
27
+ }
28
+}
29
+
30
+void main() {
31
+ runApp(
32
+ ProviderScope(
33
+ observers: [Logger()],
34
+ child: const MyApp(),
35
+ ),
36
+ );
37
+}
38
+```
39
+
40
+### Naming providers
41
+
42
+For clearer logs, give providers a **name**:
43
+
44
+```dart
45
+final myProvider = Provider<int>((ref) => 0, name: 'MyProvider');
46
+```
47
+
48
+With code generation, a name is usually assigned automatically.
49
+
50
+### Note on mutations
51
+
52
+If state is mutated in place (e.g. a List with ref.notifyListeners), `previousValue` and `newValue` in didUpdateProvider may be the same reference. For distinct values, clone before mutating.
.agent/skills/riverpod-offline/SKILL.md
new
+111
@@ -0,0 +1,111 @@
1
+---
2
+name: riverpod-offline
3
+description: Persist Riverpod notifier state offline with Storage and persist(); riverpod_sqflite, JsonPersist, key, destroyKey, cache duration, testing with in-memory storage. Use when saving state across app restarts or offline. Use this skill when the user asks about offline persistence, persisting state, or Riverpod storage.
4
+---
5
+
6
+# Riverpod — Offline persistence (experimental)
7
+
8
+## Instructions
9
+
10
+**Offline persistence** stores provider state on device so it survives restarts and works offline. Riverpod is storage-agnostic; packages like **riverpod_sqflite** provide a **Storage** implementation. Only **Notifier**-based providers can be persisted. The feature is experimental.
11
+
12
+### Creating a Storage
13
+
14
+Install a package (e.g. riverpod_sqflite + sqflite) and create a Storage. With SQFlite:
15
+
16
+```dart
17
+final storageProvider = FutureProvider<Storage<String, String>>((ref) async {
18
+ return JsonSqFliteStorage.open(
19
+ join(await getDatabasesPath(), 'riverpod.db'),
20
+ );
21
+});
22
+```
23
+
24
+### Persisting a notifier
25
+
26
+Inside the notifier's `build`, call **persist** with: the Storage (e.g. `ref.watch(storageProvider.future)`), a unique **key**, and **encode**/ **decode** for your state. Do not await persist; Riverpod handles it.
27
+
28
+```dart
29
+class TodoList extends AsyncNotifier<List<Todo>> {
30
+ @override
31
+ Future<List<Todo>> build() async {
32
+ persist(
33
+ ref.watch(storageProvider.future),
34
+ key: 'todo_list',
35
+ encode: (todos) => todos.map((todo) => {'task': todo.task}).toList(),
36
+ decode: (json) => (json as List).map((todo) => Todo(task: todo['task'] as String)).toList(),
37
+ );
38
+ return fetchTodosFromServer();
39
+ }
40
+}
41
+```
42
+
43
+### Keys
44
+
45
+- **Unique** across all persisted providers (same key = same row, risk of corruption).
46
+- **Stable** across restarts (changing the key loses restored state).
47
+- For **family** providers, include the parameter in the key.
48
+
49
+### JsonPersist (code generation)
50
+
51
+With riverpod_sqflite and codegen, use **@JsonPersist()** so key/encode/decode are generated:
52
+
53
+```dart
54
+@riverpod
55
+@JsonPersist()
56
+class TodoList extends _$TodoList {
57
+ @override
58
+ Future<List<Todo>> build() async {
59
+ persist(ref.watch(storageProvider.future));
60
+ return fetchTodosFromServer();
61
+ }
62
+}
63
+```
64
+
65
+### Cache duration
66
+
67
+By default state is cached for a short time (e.g. 2 days). For long-lived data (e.g. user preferences), set **StorageOptions**:
68
+
69
+```dart
70
+persist(
71
+ ref.watch(storageProvider.future),
72
+ options: const StorageOptions(cacheTime: StorageCacheTime.unsafe_forever),
73
+ // ...
74
+);
75
+```
76
+
77
+If using forever, plan to delete or migrate data when the app changes; Riverpod does not do migrations.
78
+
79
+### Destroy key (simple migration)
80
+
81
+When the data shape changes, use **destroyKey** so old data is discarded:
82
+
83
+```dart
84
+options: const StorageOptions(destroyKey: '1.0'),
85
+```
86
+
87
+Bump the string in new releases; old persisted state is then ignored and the provider starts fresh.
88
+
89
+### Waiting for decode
90
+
91
+To initialize from persisted state instead of a network call, await the persist future:
92
+
93
+```dart
94
+await persist(ref.watch(storageProvider.future), key: 'todo_list', ...).future;
95
+return state.value ?? <Todo>[];
96
+```
97
+
98
+### Testing
99
+
100
+Override the storage provider with **Storage.inMemory()** so tests don't need a real database:
101
+
102
+```dart
103
+ProviderScope(
104
+ overrides: [
105
+ storageProvider.overrideWith((ref) => Storage<String, String>.inMemory()),
106
+ ],
107
+ child: const MyApp(),
108
+)
109
+```
110
+
111
+For advanced migrations or custom storage strategies, you may still need to work with the database directly.
.agent/skills/riverpod-overrides/SKILL.md
new
+55
@@ -0,0 +1,55 @@
1
+---
2
+name: riverpod-overrides
3
+description: Override Riverpod providers for tests, debugging, or environment-specific behavior; ProviderScope and ProviderContainer overrides, overrideWith, overrideWithValue. Use when mocking providers in tests, injecting different implementations, or scoping provider behavior. Use this skill when the user asks about overrides, testing Riverpod, or mocking providers.
4
+---
5
+
6
+# Riverpod — Provider overrides
7
+
8
+## Instructions
9
+
10
+All providers can be **overridden** to change their behavior. Overrides are set on the **container** (ProviderScope in Flutter, ProviderContainer in Dart) via the `overrides` parameter. Use overrides for testing, debugging, different environments, or scoping (see riverpod-scoping).
11
+
12
+### Where to set overrides
13
+
14
+- **Flutter:** Pass `overrides` to `ProviderScope` (usually at the root or around a subtree).
15
+- **Dart:** Pass `overrides` to `ProviderContainer`.
16
+
17
+### Override methods
18
+
19
+Each provider type exposes methods whose names start with `overrideWith`, for example:
20
+
21
+- **overrideWith** — Provide a new implementation (e.g. a function that returns a value or builds state).
22
+- **overrideWithValue** — Replace with a fixed value (e.g. for FutureProvider/StreamProvider).
23
+- **overrideWithBuild** — Custom build logic (e.g. for StreamProvider).
24
+
25
+Check the provider's API for the exact method (e.g. `Provider.overrideWith`, `FutureProvider.overrideWithValue`).
26
+
27
+### Example: override a counter
28
+
29
+```dart
30
+// Flutter
31
+void main() {
32
+ runApp(
33
+ ProviderScope(
34
+ overrides: [
35
+ counterProvider.overrideWith((ref) => 42),
36
+ ],
37
+ child: MyApp(),
38
+ ),
39
+ );
40
+}
41
+
42
+// Dart
43
+final container = ProviderContainer(
44
+ overrides: [
45
+ counterProvider.overrideWith((ref) => 42),
46
+ ],
47
+);
48
+```
49
+
50
+### Family overrides
51
+
52
+- **Single parameter:** Override one specific argument: `userProvider('123').overrideWith((ref) => User(name: 'User 123'))`.
53
+- **All parameters:** Override the whole family: `userProvider.overrideWith((ref, arg) => User(name: 'User $arg'))`.
54
+
55
+See riverpod-family and riverpod-testing for more test patterns.
.agent/skills/riverpod-providers/SKILL.md
new
+92
@@ -0,0 +1,92 @@
1
+---
2
+name: riverpod-providers
3
+description: Declare and use Riverpod providers (Provider, FutureProvider, StreamProvider, NotifierProvider, AsyncNotifierProvider, StreamNotifierProvider); unmodifiable vs modifiable, top-level declaration, Ref, Notifier build method. Use when creating providers, choosing provider type, writing Notifier classes, or understanding Riverpod state. Use this skill whenever the user asks about Riverpod providers, provider types, or notifiers.
4
+---
5
+
6
+# Riverpod — Providers
7
+
8
+## Instructions
9
+
10
+Providers are the central feature of Riverpod: memoized functions that cache their result and let multiple widgets (or other providers) access the same value. They are declared as **top-level** `final` variables. State lives in a **ProviderContainer** (or ProviderScope in Flutter), not in the provider itself.
11
+
12
+### Provider variants
13
+
14
+| | Synchronous | Future | Stream |
15
+|--------------|---------------|------------------------|-------------------------|
16
+| Unmodifiable | Provider | FutureProvider | StreamProvider |
17
+| Modifiable | NotifierProvider | AsyncNotifierProvider | StreamNotifierProvider |
18
+
19
+- **Sync vs Future vs Stream**: Match the return type of your function (`int`, `Future<T>`, `Stream<T>`).
20
+- **Unmodifiable**: Widgets only read the value. Use for computed or fetched data.
21
+- **Modifiable**: Expose a Notifier with a `build()` method and custom methods; widgets call `ref.read(provider.notifier).someMethod()`. Use when the UI must update state.
22
+
23
+Pick the type based on what you want to return; the provider type follows naturally. FutureProvider / AsyncNotifierProvider are the most common.
24
+
25
+### Creating a provider (unmodifiable / functional)
26
+
27
+**Manual:**
28
+
29
+```dart
30
+final userProvider = FutureProvider<User>((ref) async {
31
+ final response = await http.get('https://api.example.com/user/123');
32
+ return User.fromJson(response.body);
33
+});
34
+```
35
+
36
+**Code generation:** Annotate a top-level function with `@riverpod`. First parameter must be `Ref ref`. The generated provider name is `functionNameProvider`.
37
+
38
+```dart
39
+@riverpod
40
+Future<User> user(Ref ref) async {
41
+ final response = await http.get('https://api.example.com/user/123');
42
+ return User.fromJson(response.body);
43
+}
44
+```
45
+
46
+Modifiers: `.autoDispose` (or `@Riverpod(keepAlive: true)` to disable) and `.family` for parameters. See riverpod-auto-dispose and riverpod-family.
47
+
48
+### Creating a provider (modifiable / Notifier)
49
+
50
+**Manual:** Use a NotifierProvider (or AsyncNotifierProvider / StreamNotifierProvider) with a Notifier class that extends `Notifier<T>`, `AsyncNotifier<T>`, or `StreamNotifier<T>`. Override `build()`; do not put logic in the constructor (ref/state are not available yet). Public methods are called via `ref.read(provider.notifier).method()`.
51
+
52
+```dart
53
+final counterProvider = NotifierProvider<CounterNotifier, int>(CounterNotifier.new);
54
+
55
+class CounterNotifier extends Notifier<int> {
56
+ @override
57
+ int build() => 0;
58
+
59
+ void increment() => state++;
60
+}
61
+```
62
+
63
+**Code generation:** Annotate a class that extends `_$ClassName`. Override `build()`; add custom methods. Same rule: no logic in the constructor.
64
+
65
+```dart
66
+@riverpod
67
+class CounterNotifier extends _$CounterNotifier {
68
+ @override
69
+ int build() => 0;
70
+
71
+ void increment() => state++;
72
+}
73
+```
74
+
75
+### Using providers
76
+
77
+1. **Flutter:** Wrap the app in `ProviderScope`. Use Consumer, ConsumerWidget, or ConsumerStatefulWidget to get a `ref` and call `ref.watch(provider)` or `ref.read(provider)`.
78
+2. **Dart only:** Create a `ProviderContainer()`, then `container.read(provider)` or `container.listen(...)`. Call `container.dispose()` when done.
79
+3. **Inside another provider:** Use the `Ref` passed to the provider function (or `this.ref` in a Notifier) to `ref.watch` / `ref.read` other providers.
80
+
81
+Example in a widget:
82
+
83
+```dart
84
+return Consumer(
85
+ builder: (context, ref, _) {
86
+ final helloWorld = ref.watch(helloWorldProvider);
87
+ return Text(helloWorld);
88
+ },
89
+);
90
+```
91
+
92
+You can declare multiple providers that return the same type; they are independent. For more detail on Ref, containers, modifiers, and notifier rules, see [reference.md](reference.md).
.agent/skills/riverpod-providers/reference.md
new
+34
@@ -0,0 +1,34 @@
1
+# Riverpod Providers — Reference
2
+
3
+## Provider type summary
4
+
5
+- **Provider** — sync, unmodifiable. Return type `T`.
6
+- **FutureProvider** — async, unmodifiable. Return type `Future<T>`.
7
+- **StreamProvider** — stream, unmodifiable. Return type `Stream<T>`.
8
+- **NotifierProvider** — sync, modifiable. Notifier extends `Notifier<T>`, `build()` returns `T`.
9
+- **AsyncNotifierProvider** — async, modifiable. Notifier extends `AsyncNotifier<T>`, `build()` returns `Future<T>`.
10
+- **StreamNotifierProvider** — stream, modifiable. Notifier extends `StreamNotifier<T>`, `build()` returns `Stream<T>`.
11
+
12
+## Modifiers
13
+
14
+- **autoDispose** — Cache is cleared when the provider has no listeners. Use with family to avoid memory leaks. Disable with `@Riverpod(keepAlive: true)` in codegen.
15
+- **family** — Pass parameters to the provider (e.g. `provider(id)`). See riverpod-family.
16
+
17
+## Notifier rules
18
+
19
+- Do not put logic in the Notifier constructor; `ref` and `state` are not ready. Put initialization in `build()`.
20
+- `build()` is called by the framework; do not call it directly.
21
+- Consumers modify state by calling methods on the notifier: `ref.read(myProvider.notifier).myMethod()`.
22
+
23
+## Multiple providers, same type
24
+
25
+Riverpod allows multiple providers that expose the same type (e.g. two different `Provider<String>`). They are independent; no conflict.
26
+
27
+## Links to other concepts
28
+
29
+- Ref (read, watch, listen, invalidate): riverpod-refs
30
+- Containers / ProviderScope: riverpod-containers
31
+- Auto-dispose: riverpod-auto-dispose
32
+- Family: riverpod-family
33
+- Overrides (testing): riverpod-overrides
34
+- Consumers (widgets): riverpod-consumers
.agent/skills/riverpod-pull-to-refresh/SKILL.md
new
+51
@@ -0,0 +1,51 @@
1
+---
2
+name: riverpod-pull-to-refresh
3
+description: Implement pull-to-refresh with Riverpod using RefreshIndicator and ref.refresh; show spinner on initial load, show previous data during refresh, AsyncValue pattern matching. Use when the user asks about pull-to-refresh, RefreshIndicator with Riverpod, or refreshing async providers.
4
+---
5
+
6
+# Riverpod — Pull-to-refresh
7
+
8
+## Instructions
9
+
10
+Riverpod fits pull-to-refresh well: refresh by invalidating the provider and let AsyncValue handle loading vs data vs error.
11
+
12
+### 1. Provider and UI
13
+
14
+Define an async provider (e.g. FutureProvider or AsyncNotifierProvider) that fetches data. Display it with **ref.watch(provider)** so you get an **AsyncValue**.
15
+
16
+### 2. RefreshIndicator
17
+
18
+Wrap your scrollable (ListView, SingleChildScrollView, etc.) in **RefreshIndicator**. In **onRefresh**, call **ref.refresh(provider.future)** so the indicator stays until the new data is loaded:
19
+
20
+```dart
21
+RefreshIndicator(
22
+ onRefresh: () => ref.refresh(activityProvider.future),
23
+ child: ListView(
24
+ children: [
25
+ // content
26
+ ],
27
+ ),
28
+)
29
+```
30
+
31
+### 3. Loading and error states
32
+
33
+Use **AsyncValue** pattern matching:
34
+
35
+- **Initial load:** No data and no error → show full-screen spinner.
36
+- **Refresh:** Show the refresh indicator and keep showing previous data (or previous error) until the new result arrives.
37
+- **Data:** `AsyncValue(:final value?)` or similar to show the value.
38
+- **Error:** `AsyncValue(:final error?)` to show the error.
39
+
40
+Example (syntax may vary by Riverpod version):
41
+
42
+```dart
43
+final activity = ref.watch(activityProvider);
44
+switch (activity) {
45
+ AsyncValue<Activity>(:final value?) => Text(value.activity),
46
+ AsyncValue(:final error?) => Text('Error: $error'),
47
+ _ => const CircularProgressIndicator(),
48
+}
49
+```
50
+
51
+Use **valueOrNull** (or equivalent) if your API exposes it for nullable data. See the docs for the exact AsyncValue API in your version.
.agent/skills/riverpod-refs/SKILL.md
new
+70
@@ -0,0 +1,70 @@
1
+---
2
+name: riverpod-refs
3
+description: Use Ref and WidgetRef to read, watch, listen, invalidate, and refresh providers; onDispose and onCancel lifecycle; ref.read vs ref.watch vs ref.listen, ref.invalidate and ref.refresh. Use when interacting with Riverpod providers from widgets or other providers, when to use watch vs read, or when resetting provider state. Use this skill whenever the user asks about ref.watch, ref.read, ref.listen, ref.invalidate, or Riverpod lifecycle.
4
+---
5
+
6
+# Riverpod — Refs
7
+
8
+## Instructions
9
+
10
+**Ref** (and **WidgetRef** in widgets) is how you interact with providers: read state, listen to changes, reset state, and register lifecycle callbacks. Providers get a `Ref` as the first parameter of their function or as `this.ref` in a Notifier. Widgets get a **WidgetRef** via Consumer / ConsumerWidget / ConsumerState.
11
+
12
+### Obtaining a Ref
13
+
14
+- **Inside a provider:** First parameter of the provider function, or `ref` property in a Notifier.
15
+- **Inside a widget:** Use a Consumer (builder gives `ref`), ConsumerWidget (`build(context, ref)`), or ConsumerStatefulWidget (state has `ref`). Pass `ref` to other functions if needed.
16
+
17
+### Listening to state
18
+
19
+- **ref.watch(provider)** — Declarative. Your widget/provider rebuilds when the provider changes. Use this by default in build methods.
20
+- **ref.listen(provider, (prev, next) { ... })** — Imperative. Run side effects when the provider changes (e.g. show a dialog, navigate). Safe to use in build; for initState use `ref.listenManual` and manage the subscription.
21
+
22
+```dart
23
+// In a widget
24
+final tick = ref.watch(tickProvider);
25
+return Text('Tick: $tick');
26
+
27
+// Side effect when provider changes
28
+ref.listen(tickProvider, (previous, next) {
29
+ print('Tick changed from $previous to $next');
30
+});
31
+```
32
+
33
+### Reading without listening
34
+
35
+- **ref.read(provider)** — Get current value without subscribing. Use in event handlers (e.g. onPressed), not to "optimize" by avoiding watch. For selective rebuilds use `ref.watch(provider.select((value) => ...))`.
36
+
37
+```dart
38
+onPressed: () {
39
+ final tick = ref.read(tickProvider);
40
+ print('Current tick: $tick');
41
+}
42
+```
43
+
44
+### Resetting state
45
+
46
+- **ref.invalidate(provider)** — Discard current state; provider will recompute on next read. If the provider is listened to, a new state is created.
47
+- **ref.refresh(provider)** — Same as invalidate + read: invalidates and returns the new value. Use when you need the new value immediately.
48
+
49
+```dart
50
+ref.invalidate(tickProvider);
51
+// or
52
+final newTick = ref.refresh(tickProvider);
53
+```
54
+
55
+### Lifecycle: onDispose, onCancel
56
+
57
+Inside a provider you can register callbacks:
58
+
59
+- **ref.onDispose(callback)** — Called when the provider state is destroyed (e.g. auto-dispose or recomputation). Use to close StreamControllers, cancel timers, etc. Do not trigger side effects that modify other providers inside onDispose.
60
+- **ref.onCancel(callback)** — Called when the last listener is removed (before dispose). **ref.onResume(callback)** — Called when a listener is added again after onCancel.
61
+
62
+```dart
63
+final provider = StreamProvider<int>((ref) {
64
+ final controller = StreamController<int>();
65
+ ref.onDispose(controller.close);
66
+ return controller.stream;
67
+});
68
+```
69
+
70
+You can call onDispose multiple times (e.g. one per disposable resource). Return value of onDispose/onCancel can be called to unregister. For more detail and select/listenManual, see [reference.md](reference.md).
.agent/skills/riverpod-refs/reference.md
new
+30
@@ -0,0 +1,30 @@
1
+# Riverpod Refs — Reference
2
+
3
+## ref.watch
4
+
5
+Subscribes to the provider; the current widget/provider rebuilds when the value changes. Use in build. Providers can watch other providers to compose state.
6
+
7
+## ref.read
8
+
9
+Returns current value without subscribing. Use in event handlers (onPressed, callbacks). Do not use read instead of watch to "optimize" — use watch or use select to limit rebuilds.
10
+
11
+## ref.listen
12
+
13
+Registers a one-off listener for side effects (navigation, dialogs, logging). Signature: `(previous, next) => ...`. Safe in build. For listening outside build (e.g. initState), use **ref.listenManual** and store the subscription to cancel later.
14
+
15
+## ref.invalidate / ref.refresh
16
+
17
+- **invalidate(provider)** — Discard state; next read will recompute.
18
+- **refresh(provider)** — invalidate + read in one call; returns the new value.
19
+
20
+## ref.select
21
+
22
+Reduce rebuilds by watching only a part of the state: `ref.watch(provider.select((value) => value.someField))`. Rebuilds only when the selected value changes (by ==).
23
+
24
+## WidgetRef.listenManual
25
+
26
+When you need to listen from initState or outside build, use listenManual and call the returned subscription's `close()` in dispose.
27
+
28
+## Ref vs WidgetRef
29
+
30
+Ref is used inside providers; WidgetRef is used inside widgets. There is no shared interface by design — keep logic in providers and call notifier methods from the UI so only Ref is needed in business logic.
.agent/skills/riverpod-retry/SKILL.md
new
+51
@@ -0,0 +1,51 @@
1
+---
2
+name: riverpod-retry
3
+description: Customize Riverpod automatic retry on provider failure; retry function, per-provider and global retry, disabling retry, ProviderException, awaiting retries. Use when a provider can fail transiently and should retry, or when you need to disable or customize retry logic. Use this skill when the user asks about retry, failed providers, or exponential backoff in Riverpod.
4
+---
5
+
6
+# Riverpod — Automatic retry
7
+
8
+## Instructions
9
+
10
+Riverpod **retries** providers when their computation throws. Retry can be customized per provider or globally.
11
+
12
+### Default behavior
13
+
14
+By default a provider is retried up to a limit (e.g. 10 times) with exponential backoff (e.g. 200ms to 6.4s). **Error** and **ProviderException** are not retried: Error indicates a bug; ProviderException means a dependency failed, so the dependent provider is not retried (the underlying one is).
15
+
16
+### Custom retry function
17
+
18
+A retry function has signature `Duration? Function(int retryCount, Object error)`. Return a **Duration** for the delay before the next retry, or **null** to stop.
19
+
20
+```dart
21
+Duration? myRetry(int retryCount, Object error) {
22
+ if (retryCount >= 5) return null;
23
+ if (error is ProviderException) return null;
24
+ return Duration(milliseconds: 200 * (1 << retryCount));
25
+}
26
+```
27
+
28
+### Where to set it
29
+
30
+- **Per provider:** Pass `retry: myRetry` when defining the provider (or `@Riverpod(retry: myRetry)` with codegen).
31
+- **Globally:** Pass `retry: myRetry` to **ProviderScope** or **ProviderContainer**.
32
+
33
+```dart
34
+// Global
35
+ProviderScope(retry: myRetry, child: MyApp())
36
+
37
+// Per provider (manual)
38
+final myProvider = Provider<int>(retry: myRetry, (ref) => 0);
39
+```
40
+
41
+### Disabling retry
42
+
43
+Return **null** always:
44
+
45
+```dart
46
+ProviderScope(retry: (retryCount, error) => null, child: MyApp())
47
+```
48
+
49
+### Awaiting async providers with retry
50
+
51
+`ref.watch(myProvider.future)` (or equivalent) keeps waiting until either all retries are exhausted or the provider succeeds. So `await ref.watch(myProvider.future)` skips intermediate failures and completes when the provider finally succeeds or gives up.
.agent/skills/riverpod-scoping/SKILL.md
new
+55
@@ -0,0 +1,55 @@
1
+---
2
+name: riverpod-scoping
3
+description: Scope Riverpod providers to a subtree using overrides and dependencies; page-specific behavior, ListView optimization, avoiding passing family parameters. Use when changing provider behavior for one page or widget, or when optimizing rebuilds. Use this skill when the user asks about scoping providers or provider scope.
4
+---
5
+
6
+# Riverpod — Scoping providers
7
+
8
+## Instructions
9
+
10
+**Scoping** means changing a provider's behavior for only part of the app by overriding it in a **non-root** ProviderScope. Use cases: page-specific customization (e.g. theme for one page), performance (e.g. rebuild only the item that changed in a ListView), or avoiding passing family parameters through the tree.
11
+
12
+**Note:** Scoping is complex and may be reworked in future Riverpod versions. Use with care.
13
+
14
+### Opt-in: dependencies
15
+
16
+By default you cannot scope a provider. Opt in by setting **dependencies** on the provider. The first scoped provider usually has `dependencies: const []`.
17
+
18
+```dart
19
+final currentItemIdProvider = Provider<String?>(
20
+ dependencies: const [],
21
+ (ref) => null,
22
+);
23
+```
24
+
25
+### Listening to a scoped provider
26
+
27
+Use ref.watch/ref.read as usual. If another **provider** listens to a scoped provider, that listening provider must list the scoped provider in its **dependencies**:
28
+
29
+```dart
30
+final currentItemProvider = FutureProvider<Item?>(
31
+ dependencies: [currentItemIdProvider],
32
+ (ref) async {
33
+ final currentItemId = ref.watch(currentItemIdProvider);
34
+ if (currentItemId == null) return null;
35
+ return fetchItem(id: currentItemId);
36
+ },
37
+);
38
+```
39
+
40
+Only scoped providers need to be listed in dependencies; non-scoped providers do not.
41
+
42
+### Setting the scoped value: overrides
43
+
44
+Use **overrides** on a ProviderScope that wraps the subtree where the scoped value should apply:
45
+
46
+```dart
47
+ProviderScope(
48
+ overrides: [
49
+ currentItemIdProvider.overrideWithValue('123'),
50
+ ],
51
+ child: const DetailPageView(),
52
+)
53
+```
54
+
55
+That way the detail page sees `'123'` as the current item ID without it being passed down manually. See riverpod-overrides for override syntax.
.agent/skills/riverpod-select/SKILL.md
new
+30
@@ -0,0 +1,30 @@
1
+---
2
+name: riverpod-select
3
+description: Reduce rebuilds with ref.watch(provider.select(...)); select a subset of state so widgets rebuild only when that part changes. Use when optimizing performance or when a widget uses only one field of a large object. Use this skill when the user asks about select, reducing rebuilds, or optimizing Riverpod watch.
4
+---
5
+
6
+# Riverpod — Reducing rebuilds with select
7
+
8
+## Instructions
9
+
10
+By default, **ref.watch(provider)** rebuilds whenever the provider's value changes (by reference/equality). If you only use a subset of the state (e.g. one field of a User), the widget still rebuilds when other fields change.
11
+
12
+Use **select** to watch only a part of the state. Rebuilds happen only when the **selected** value changes (by ==).
13
+
14
+### Syntax
15
+
16
+```dart
17
+// Rebuild only when firstName changes
18
+String name = ref.watch(provider.select((it) => it.firstName));
19
+return Text('Hello $name');
20
+```
21
+
22
+You can call select multiple times for different properties. The selected value should be **immutable**; mutating a returned List/object in place will not trigger a rebuild.
23
+
24
+### Async providers: selectAsync
25
+
26
+For async providers you often use **ref.watch(provider.future)**. To select on the resolved data instead of the whole AsyncValue, use **selectAsync** (when available): it applies a select on the data and returns a Future. See the Riverpod docs for the exact API in your version.
27
+
28
+### When to use
29
+
30
+- Only when the "other" properties change often and you have measured that rebuilds are a bottleneck. Select adds a small cost and extra complexity. Prefer normal watch unless you need the optimization.
.agent/skills/riverpod-testing/SKILL.md
new
+93
@@ -0,0 +1,93 @@
1
+---
2
+name: riverpod-testing
3
+description: Test Riverpod providers and widgets; ProviderContainer.test, unit tests, widget tests with ProviderScope, tester.container(), mocking with overrides, container.listen for auto-dispose, awaiting .future. Use when writing unit or widget tests for Riverpod code, mocking providers, or testing with overrides. Use this skill when the user asks about testing Riverpod, mocking providers, or ProviderContainer in tests.
4
+---
5
+
6
+# Riverpod — Testing
7
+
8
+## Instructions
9
+
10
+Riverpod is designed for testability: isolate state per test, mock via overrides, and keep the test environment close to production.
11
+
12
+### Unit tests (no Flutter)
13
+
14
+Use **ProviderContainer.test()** to create a container for the test. Do not share containers between tests.
15
+
16
+```dart
17
+void main() {
18
+ test('Some description', () {
19
+ final container = ProviderContainer.test();
20
+ expect(container.read(provider), equals('some value'));
21
+ });
22
+}
23
+```
24
+
25
+- **container.read(provider)** — Read current value.
26
+- **container.listen(provider, (prev, next) {})** — Listen and get a subscription; use **subscription.read()** to read. Prefer listen when the provider is auto-dispose so it is not disposed mid-test.
27
+
28
+```dart
29
+final subscription = container.listen<String>(provider, (_, _) {});
30
+expect(subscription.read(), 'Some value');
31
+```
32
+
33
+### Widget tests
34
+
35
+Wrap the widget under test in **ProviderScope**:
36
+
37
+```dart
38
+testWidgets('Some description', (tester) async {
39
+ await tester.pumpWidget(
40
+ const ProviderScope(child: YourWidgetYouWantToTest()),
41
+ );
42
+});
43
+```
44
+
45
+To interact with providers in the test, get the container with **tester.container()**:
46
+
47
+```dart
48
+final container = tester.container();
49
+expect(container.read(provider), 'some value');
50
+```
51
+
52
+### Mocking providers
53
+
54
+Use **overrides** on ProviderContainer or ProviderScope. All providers can be overridden without extra setup.
55
+
56
+```dart
57
+final container = ProviderContainer.test(
58
+ overrides: [
59
+ exampleProvider.overrideWith((ref) => 'Hello from tests'),
60
+ ],
61
+);
62
+
63
+// Or in widget tests:
64
+await tester.pumpWidget(
65
+ ProviderScope(
66
+ overrides: [exampleProvider.overrideWith((ref) => 'Hello from tests')],
67
+ child: const YourWidgetYouWantToTest(),
68
+ ),
69
+);
70
+```
71
+
72
+See riverpod-overrides for family overrides and other override methods.
73
+
74
+### Awaiting async providers
75
+
76
+Read **provider.future** to get a Future that completes with the provider value; use with expectLater:
77
+
78
+```dart
79
+await expectLater(
80
+ container.read(provider.future),
81
+ completion('some value'),
82
+);
83
+```
84
+
85
+### Listening / spying
86
+
87
+Use **container.listen(provider, callback)** and assert on the callback arguments or collect values in a list for assertions. Works with mockito/mocktail verify patterns.
88
+
89
+### Mocking Notifiers
90
+
91
+Prefer mocking a dependency (e.g. repository) the Notifier uses rather than mocking the Notifier. If you must mock a Notifier, **subclass** it (do not implement), so the mock extends the original base class. With code generation, the mock usually needs to live in the same file as the Notifier to access the generated base class.
92
+
93
+See riverpod-overrides and the official docs for more examples.
.agent/skills/shadcn_ui-accordion/SKILL.md
new
+46
@@ -0,0 +1,46 @@
1
+---
2
+name: shadcn_ui-accordion
3
+description: Build collapsible accordions with ShadAccordion and ShadAccordionItem; single or multiple open items. Use when adding expandable sections, FAQs, or vertically stacked reveal content in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Accordion
7
+
8
+## Instructions
9
+
10
+Use `ShadAccordion` for a vertically stacked set of headings that each reveal content. Each item is a `ShadAccordionItem` with `value`, `title`, and `child`.
11
+
12
+### Single (one open at a time)
13
+
14
+```dart
15
+final details = [
16
+ (title: 'Is it acceptable?', content: 'Yes. It adheres to the WAI-ARIA design pattern.'),
17
+ (title: 'Is it styled?', content: "Yes. It comes with default styles that matches the other components' aesthetic."),
18
+ (title: 'Is it animated?', content: "Yes. It's animated by default, but you can disable it if you prefer."),
19
+];
20
+
21
+ShadAccordion<({String content, String title})>(
22
+ children: details.map(
23
+ (detail) => ShadAccordionItem(
24
+ value: detail,
25
+ title: Text(detail.title),
26
+ child: Text(detail.content),
27
+ ),
28
+ ),
29
+)
30
+```
31
+
32
+### Multiple (several open at once)
33
+
34
+Use `ShadAccordion.multiple`:
35
+
36
+```dart
37
+ShadAccordion<({String content, String title})>.multiple(
38
+ children: details.map(
39
+ (detail) => ShadAccordionItem(
40
+ value: detail,
41
+ title: Text(detail.title),
42
+ child: Text(detail.content),
43
+ ),
44
+ ),
45
+)
46
+```
.agent/skills/shadcn_ui-alert/SKILL.md
new
+30
@@ -0,0 +1,30 @@
1
+---
2
+name: shadcn_ui-alert
3
+description: Show callout alerts with ShadAlert and ShadAlert.destructive; icon, title, description. Use when displaying warnings, errors, or important messages in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Alert
7
+
8
+## Instructions
9
+
10
+`ShadAlert` displays a callout for user attention. Use default constructor for standard alerts, `ShadAlert.destructive` for errors or destructive actions.
11
+
12
+### Primary / default
13
+
14
+```dart
15
+ShadAlert(
16
+ icon: Icon(LucideIcons.terminal),
17
+ title: Text('Heads up!'),
18
+ description: Text('You can add components to your app using the cli.'),
19
+)
20
+```
21
+
22
+### Destructive
23
+
24
+```dart
25
+ShadAlert.destructive(
26
+ icon: Icon(LucideIcons.circleAlert),
27
+ title: Text('Error'),
28
+ description: Text('Your session has expired. Please log in again.'),
29
+)
30
+```
.agent/skills/shadcn_ui-avatar/SKILL.md
new
+21
@@ -0,0 +1,21 @@
1
+---
2
+name: shadcn_ui-avatar
3
+description: Display user avatars with ShadAvatar; image URL with optional placeholder. Use when showing user profile images or fallback initials in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Avatar
7
+
8
+## Instructions
9
+
10
+`ShadAvatar` is an image with a placeholder for representing the user. Uses the `universal_image` package for multiple image formats.
11
+
12
+### Basic usage
13
+
14
+```dart
15
+ShadAvatar(
16
+ 'https://example.com/avatar.png',
17
+ placeholder: Text('CN'),
18
+)
19
+```
20
+
21
+Pass the image URL as the first argument; `placeholder` is shown while loading or if the image fails (e.g. initials).
.agent/skills/shadcn_ui-badge/SKILL.md
new
+24
@@ -0,0 +1,24 @@
1
+---
2
+name: shadcn_ui-badge
3
+description: Display badges with ShadBadge; primary, secondary, destructive, outline variants. Use when showing tags, status labels, or count indicators in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Badge
7
+
8
+## Instructions
9
+
10
+`ShadBadge` displays a badge or badge-like component. Variants: default (primary), `ShadBadge.secondary`, `ShadBadge.destructive`, `ShadBadge.outline`.
11
+
12
+### Variants
13
+
14
+```dart
15
+ShadBadge(child: const Text('Primary'))
16
+
17
+ShadBadge.secondary(child: const Text('Secondary'))
18
+
19
+ShadBadge.destructive(child: const Text('Destructive'))
20
+
21
+ShadBadge.outline(child: const Text('Outline'))
22
+```
23
+
24
+Wrap any `child` (typically `Text`) with the desired constructor.
.agent/skills/shadcn_ui-breadcrumb/SKILL.md
new
+58
@@ -0,0 +1,58 @@
1
+---
2
+name: shadcn_ui-breadcrumb
3
+description: Build breadcrumb navigation with ShadBreadcrumb, ShadBreadcrumbLink, ShadBreadcrumbDropdown, custom separator. Use when showing navigation path, hierarchy of links, or dropdown breadcrumb in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Breadcrumb
7
+
8
+## Instructions
9
+
10
+`ShadBreadcrumb` displays the path to the current resource using a hierarchy of links. Children can be `ShadBreadcrumbLink`, `ShadBreadcrumbDropdown`, or plain widgets (e.g. `Text` for current page).
11
+
12
+### Basic breadcrumb
13
+
14
+```dart
15
+ShadBreadcrumb(
16
+ children: [
17
+ ShadBreadcrumbLink(
18
+ onPressed: () => print('Navigating to Home'),
19
+ child: const Text('Home'),
20
+ ),
21
+ ShadBreadcrumbDropdown(
22
+ items: [
23
+ ShadBreadcrumbDropMenuItem(
24
+ onPressed: () => print('Navigating to Documentation'),
25
+ child: const Text('Documentation'),
26
+ ),
27
+ ShadBreadcrumbDropMenuItem(
28
+ onPressed: () => print('Navigating to Themes'),
29
+ child: const Text('Themes'),
30
+ ),
31
+ ],
32
+ showDropdownArrow: false,
33
+ child: ShadBreadcrumbEllipsis(),
34
+ ),
35
+ Text('Components'),
36
+ Text('Breadcrumb'),
37
+ ],
38
+)
39
+```
40
+
41
+### Custom separator
42
+
43
+Default separator is `>`. Override with `separator`:
44
+
45
+```dart
46
+ShadBreadcrumb(
47
+ separator: const Icon(LucideIcons.slash),
48
+ children: [
49
+ ShadBreadcrumbLink(onPressed: () {}, child: const Text('Home')),
50
+ ShadBreadcrumbLink(onPressed: () {}, child: const Text('Components')),
51
+ Text('Breadcrumb'),
52
+ ],
53
+)
54
+```
55
+
56
+### Dropdown in breadcrumb
57
+
58
+Use `ShadBreadcrumbDropdown` with `items` (list of `ShadBreadcrumbDropMenuItem`) and `child` (trigger widget). Use `ShadBreadcrumbEllipsis()` for an ellipsis trigger.
.agent/skills/shadcn_ui-button/SKILL.md
new
+72
@@ -0,0 +1,72 @@
1
+---
2
+name: shadcn_ui-button
3
+description: Use Shadcn UI buttons (ShadButton variants: primary, secondary, destructive, outline, ghost, link); leading icon, loading state, gradient and shadow. Use when adding buttons, CTAs, or button-style widgets in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Button
7
+
8
+## Instructions
9
+
10
+`ShadButton` displays a button or component that looks like a button. Variants: default (primary), `ShadButton.secondary`, `ShadButton.destructive`, `ShadButton.outline`, `ShadButton.ghost`, `ShadButton.link`. Use `leading` (and optionally `trailing`) for icons; support gradient and shadows for custom styling.
11
+
12
+### Variants
13
+
14
+```dart
15
+ShadButton(child: const Text('Primary'), onPressed: () {})
16
+
17
+ShadButton.secondary(child: const Text('Secondary'), onPressed: () {})
18
+
19
+ShadButton.destructive(child: const Text('Destructive'), onPressed: () {})
20
+
21
+ShadButton.outline(child: const Text('Outline'), onPressed: () {})
22
+
23
+ShadButton.ghost(child: const Text('Ghost'), onPressed: () {})
24
+
25
+ShadButton.link(child: const Text('Link'), onPressed: () {})
26
+```
27
+
28
+### Text and icon
29
+
30
+```dart
31
+ShadButton(
32
+ onPressed: () {},
33
+ leading: const Icon(LucideIcons.mail),
34
+ child: const Text('Login with Email'),
35
+)
36
+```
37
+
38
+### Loading state
39
+
40
+Use `leading` with a small `CircularProgressIndicator`:
41
+
42
+```dart
43
+ShadButton(
44
+ onPressed: () {},
45
+ leading: SizedBox.square(
46
+ dimension: 16,
47
+ child: CircularProgressIndicator(
48
+ strokeWidth: 2,
49
+ color: ShadTheme.of(context).colorScheme.primaryForeground,
50
+ ),
51
+ ),
52
+ child: const Text('Please wait'),
53
+)
54
+```
55
+
56
+### Gradient and shadow
57
+
58
+```dart
59
+ShadButton(
60
+ onPressed: () {},
61
+ gradient: const LinearGradient(colors: [Colors.cyan, Colors.indigo]),
62
+ shadows: [
63
+ BoxShadow(
64
+ color: Colors.blue.withValues(alpha:.4),
65
+ spreadRadius: 4,
66
+ blurRadius: 10,
67
+ offset: const Offset(0, 2),
68
+ ),
69
+ ],
70
+ child: const Text('Gradient with Shadow'),
71
+)
72
+```
.agent/skills/shadcn_ui-calendar/SKILL.md
new
+63
@@ -0,0 +1,63 @@
1
+---
2
+name: shadcn_ui-calendar
3
+description: Use ShadCalendar for single, multiple, or range date selection; caption layout, hide navigation, week numbers, outside days, fixed weeks. Use when building date pickers or calendar widgets in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Calendar
7
+
8
+## Instructions
9
+
10
+`ShadCalendar` is a date field that lets users enter and edit dates. Modes: single date, multiple dates (`ShadCalendar.multiple`), or range (`ShadCalendar.range`). Use `fromMonth`/`toMonth` to limit range; `captionLayout` for dropdown months/years.
11
+
12
+### Single
13
+
14
+```dart
15
+final today = DateTime.now();
16
+
17
+ShadCalendar(
18
+ selected: today,
19
+ fromMonth: DateTime(today.year - 1),
20
+ toMonth: DateTime(today.year, 12),
21
+)
22
+```
23
+
24
+### Multiple
25
+
26
+```dart
27
+ShadCalendar.multiple(
28
+ numberOfMonths: 2,
29
+ fromMonth: DateTime(today.year),
30
+ toMonth: DateTime(today.year + 1, 12),
31
+ min: 5,
32
+ max: 10,
33
+)
34
+```
35
+
36
+### Range
37
+
38
+```dart
39
+const ShadCalendar.range(
40
+ min: 2,
41
+ max: 5,
42
+)
43
+```
44
+
45
+### Caption layout
46
+
47
+```dart
48
+ShadCalendar(
49
+ captionLayout: ShadCalendarCaptionLayout.dropdownMonths,
50
+);
51
+
52
+ShadCalendar(
53
+ captionLayout: ShadCalendarCaptionLayout.dropdownYears,
54
+);
55
+```
56
+
57
+### Options
58
+
59
+- `hideNavigation: true` — Hide month navigation.
60
+- `showWeekNumbers: true` — Show week numbers.
61
+- `showOutsideDays: false` — Hide days from adjacent months.
62
+- `fixedWeeks: true` — Fixed number of rows.
63
+- `hideWeekdayNames: true` — Hide weekday headers.
.agent/skills/shadcn_ui-card/SKILL.md
new
+53
@@ -0,0 +1,53 @@
1
+---
2
+name: shadcn_ui-card
3
+description: Build cards with ShadCard; title, description, footer, and child content. Use when creating content cards, project cards, or notification panels in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Card
7
+
8
+## Instructions
9
+
10
+`ShadCard` displays a card with optional header (title, description), body (`child`), and footer. Use `width` to constrain width.
11
+
12
+### Basic card
13
+
14
+```dart
15
+final theme = ShadTheme.of(context);
16
+ShadCard(
17
+ width: 350,
18
+ title: Text('Create project', style: theme.textTheme.h4),
19
+ description: const Text('Deploy your new project in one-click.'),
20
+ footer: Row(
21
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
22
+ children: [
23
+ ShadButton.outline(child: const Text('Cancel'), onPressed: () {}),
24
+ ShadButton(child: const Text('Deploy'), onPressed: () {}),
25
+ ],
26
+ ),
27
+ child: Padding(
28
+ padding: const EdgeInsets.symmetric(vertical: 16),
29
+ child: Column(
30
+ mainAxisSize: MainAxisSize.min,
31
+ crossAxisAlignment: CrossAxisAlignment.stretch,
32
+ children: [
33
+ const Text('Name'),
34
+ const SizedBox(height: 6),
35
+ const ShadInput(placeholder: Text('Name of your project')),
36
+ const SizedBox(height: 16),
37
+ const Text('Framework'),
38
+ const SizedBox(height: 6),
39
+ ShadSelect<String>(
40
+ placeholder: const Text('Select'),
41
+ options: frameworks.entries
42
+ .map((e) => ShadOption(value: e.key, child: Text(e.value)))
43
+ .toList(),
44
+ selectedOptionBuilder: (context, value) => Text(frameworks[value]!),
45
+ onChanged: (value) {},
46
+ ),
47
+ ],
48
+ ),
49
+ ),
50
+)
51
+```
52
+
53
+Title and description are optional. Footer can be any widget (e.g. buttons). Put main content in `child`.
.agent/skills/shadcn_ui-checkbox/SKILL.md
new
+39
@@ -0,0 +1,39 @@
1
+---
2
+name: shadcn_ui-checkbox
3
+description: Add checkboxes with ShadCheckbox and ShadCheckboxFormField; label, sublabel, form validation. Use when adding boolean toggles or terms acceptance in a Flutter shadcn_ui app or ShadForm.
4
+---
5
+
6
+# Shadcn UI — Checkbox
7
+
8
+## Instructions
9
+
10
+`ShadCheckbox` is a control that toggles between checked and unchecked. Use `value` and `onChanged`; optional `label` and `sublabel`. For forms use `ShadCheckboxFormField` with `id`, `validator`, and form state.
11
+
12
+### Standalone
13
+
14
+```dart
15
+ShadCheckbox(
16
+ value: value,
17
+ onChanged: (v) => setState(() => value = v),
18
+ label: const Text('Accept terms and conditions'),
19
+ sublabel: const Text(
20
+ 'You agree to our Terms of Service and Privacy Policy.',
21
+ ),
22
+)
23
+```
24
+
25
+### Form field
26
+
27
+```dart
28
+ShadCheckboxFormField(
29
+ id: 'terms',
30
+ initialValue: false,
31
+ inputLabel: const Text('I accept the terms and conditions'),
32
+ onChanged: (v) {},
33
+ inputSublabel: const Text('You agree to our Terms and Conditions'),
34
+ validator: (v) {
35
+ if (!v) return 'You must accept the terms and conditions';
36
+ return null;
37
+ },
38
+)
39
+```
.agent/skills/shadcn_ui-context-menu/SKILL.md
new
+65
@@ -0,0 +1,65 @@
1
+---
2
+name: shadcn_ui-context-menu
3
+description: Show context menus on right-click with ShadContextMenuRegion, ShadContextMenuItem, submenus, dividers. Use when adding right-click menus, actions, or nested menu items in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Context Menu
7
+
8
+## Instructions
9
+
10
+`ShadContextMenuRegion` wraps a `child` and shows a menu on right-click. Set `items` to a list of `ShadContextMenuItem` or `ShadContextMenuItem.inset`. Use `ShadContextMenuItem` with `items` for submenus; use `Divider(height: 8)` and `Padding` for sections and labels.
11
+
12
+### Basic usage
13
+
14
+```dart
15
+ShadContextMenuRegion(
16
+ constraints: const BoxConstraints(minWidth: 300),
17
+ items: [
18
+ const ShadContextMenuItem.inset(child: Text('Back')),
19
+ const ShadContextMenuItem.inset(
20
+ enabled: false,
21
+ child: Text('Forward'),
22
+ ),
23
+ const ShadContextMenuItem.inset(child: Text('Reload')),
24
+ const ShadContextMenuItem.inset(
25
+ trailing: Icon(LucideIcons.chevronRight),
26
+ items: [
27
+ ShadContextMenuItem(child: Text('Save Page As...')),
28
+ ShadContextMenuItem(child: Text('Create Shortcut...')),
29
+ ShadContextMenuItem(child: Text('Developer Tools')),
30
+ Divider(height: 8),
31
+ ShadContextMenuItem(child: Text('Developer Tools')),
32
+ ],
33
+ child: Text('More Tools'),
34
+ ),
35
+ const Divider(height: 8),
36
+ const ShadContextMenuItem(
37
+ leading: Icon(LucideIcons.check),
38
+ child: Text('Show Bookmarks Bar'),
39
+ ),
40
+ const ShadContextMenuItem.inset(child: Text('Show Full URLs')),
41
+ const Divider(height: 8),
42
+ Padding(
43
+ padding: const EdgeInsets.fromLTRB(36, 8, 8, 8),
44
+ child: Text('People', style: theme.textTheme.small),
45
+ ),
46
+ ShadContextMenuItem(
47
+ leading: /* avatar or icon */,
48
+ child: const Text('Pedro Duarte'),
49
+ ),
50
+ const ShadContextMenuItem.inset(child: Text('Colm Tuite')),
51
+ ],
52
+ child: Container(
53
+ width: 300,
54
+ height: 200,
55
+ alignment: Alignment.center,
56
+ decoration: BoxDecoration(
57
+ border: Border.all(color: theme.colorScheme.border),
58
+ borderRadius: BorderRadius.circular(8),
59
+ ),
60
+ child: const Text('Right click here'),
61
+ ),
62
+)
63
+```
64
+
65
+Use `ShadContextMenuItem` for items with leading/trailing; `ShadContextMenuItem.inset` for indented items. Nest `items` for submenus.
.agent/skills/shadcn_ui-date-picker/SKILL.md
new
+85
@@ -0,0 +1,85 @@
1
+---
2
+name: shadcn_ui-date-picker
3
+description: Use ShadDatePicker and ShadDatePicker.range for single or range date selection; presets, ShadDatePickerFormField, ShadDateRangePickerFormField. Use when adding date pickers or date range fields in a Flutter shadcn_ui app or ShadForm.
4
+---
5
+
6
+# Shadcn UI — Date Picker
7
+
8
+## Instructions
9
+
10
+`ShadDatePicker` is a date picker with optional range and presets. Use `ShadDatePicker.range()` for range selection. For forms use `ShadDatePickerFormField` and `ShadDateRangePickerFormField` with `id`, `label`, `validator`, and optional `fromValueTransformer`/`toValueTransformer`.
11
+
12
+### Single
13
+
14
+```dart
15
+ConstrainedBox(
16
+ constraints: const BoxConstraints(maxWidth: 600),
17
+ child: const ShadDatePicker(),
18
+)
19
+```
20
+
21
+### Range
22
+
23
+```dart
24
+ConstrainedBox(
25
+ constraints: const BoxConstraints(maxWidth: 600),
26
+ child: const ShadDatePicker.range(),
27
+)
28
+```
29
+
30
+### With presets
31
+
32
+Use a `header` widget (e.g. `ShadSelect`) and shared `groupId` so the date picker popover stays open when the select popover closes. Set `selected` from preset choice (e.g. today + days offset).
33
+
34
+```dart
35
+ShadDatePicker(
36
+ groupId: groupId,
37
+ header: Padding(
38
+ padding: const EdgeInsets.only(bottom: 4),
39
+ child: ShadSelect<int>(
40
+ groupId: groupId,
41
+ minWidth: 276,
42
+ placeholder: const Text('Select'),
43
+ options: presets.entries
44
+ .map((e) => ShadOption(value: e.key, child: Text(e.value)))
45
+ .toList(),
46
+ selectedOptionBuilder: (context, value) => Text(presets[value]!),
47
+ onChanged: (value) {
48
+ if (value == null) return;
49
+ setState(() => selected = today.add(Duration(days: value)));
50
+ },
51
+ ),
52
+ ),
53
+ selected: selected,
54
+ calendarDecoration: theme.calendarTheme.decoration,
55
+ popoverPadding: const EdgeInsets.all(4),
56
+)
57
+```
58
+
59
+### Form fields
60
+
61
+```dart
62
+ShadDatePickerFormField(
63
+ label: const Text('Date of birth'),
64
+ onChanged: print,
65
+ description: const Text('Your date of birth is used to calculate your age.'),
66
+ validator: (v) {
67
+ if (v == null) return 'A date of birth is required.';
68
+ return null;
69
+ },
70
+)
71
+
72
+ShadDateRangePickerFormField(
73
+ label: const Text('Range of dates'),
74
+ onChanged: print,
75
+ description: const Text('Select the range of dates you want to search between.'),
76
+ validator: (v) {
77
+ if (v == null) return 'A range of dates is required.';
78
+ if (v.start == null) return 'The start date is required.';
79
+ if (v.end == null) return 'The end date is required.';
80
+ return null;
81
+ },
82
+)
83
+```
84
+
85
+For form initial value as string (e.g. `'date': '2024-02-01'`), use `fromValueTransformer` and `toValueTransformer` on `ShadDatePickerFormField` to convert to/from `DateTime`. See [reference.md](reference.md) or the Form skill for value transformers.
.agent/skills/shadcn_ui-decorator/SKILL.md
new
+57
@@ -0,0 +1,57 @@
1
+---
2
+name: shadcn_ui-decorator
3
+description: Configure ShadDecoration and ShadDecorator for shadcn_ui components; customize secondary border and disableSecondaryBorder. Use when styling form controls, focus rings, or component borders in Flutter shadcn_ui.
4
+---
5
+
6
+# Shadcn UI — Decorator
7
+
8
+## Instructions
9
+
10
+Most shadcn_ui components use a `ShadDecoration` handled by `ShadDecorator` for borders, focus state, labels, and error/description styles.
11
+
12
+### Default decoration
13
+
14
+Default includes secondary border, focused ring, label/error/description styles and padding:
15
+
16
+```dart
17
+ShadDecoration(
18
+ secondaryBorder: ShadBorder.all(
19
+ padding: const EdgeInsets.all(4),
20
+ width: 0,
21
+ ),
22
+ secondaryFocusedBorder: ShadBorder.all(
23
+ width: 2,
24
+ color: colorScheme.ring,
25
+ radius: radius.add(radius / 2),
26
+ padding: const EdgeInsets.all(2),
27
+ ),
28
+ labelStyle: textTheme.muted.copyWith(
29
+ fontWeight: FontWeight.w500,
30
+ color: colorScheme.foreground,
31
+ ),
32
+ errorStyle: textTheme.muted.copyWith(
33
+ fontWeight: FontWeight.w500,
34
+ color: colorScheme.destructive,
35
+ ),
36
+ labelPadding: const EdgeInsets.only(bottom: 8),
37
+ descriptionStyle: textTheme.muted,
38
+ descriptionPadding: const EdgeInsets.only(top: 8),
39
+ errorPadding: const EdgeInsets.only(top: 8),
40
+ errorLabelStyle: textTheme.muted.copyWith(
41
+ fontWeight: FontWeight.w500,
42
+ color: colorScheme.destructive,
43
+ ),
44
+);
45
+```
46
+
47
+### Disable secondary border
48
+
49
+By default a secondary border is drawn around focusable components. To disable it and rely on a bolder primary border:
50
+
51
+```dart
52
+ShadThemeData(
53
+ disableSecondaryBorder: true,
54
+),
55
+```
56
+
57
+**Note:** Disabling the secondary border is not recommended; it can hurt accessibility because the focus ring helps users see which component is focused.
.agent/skills/shadcn_ui-dialog/SKILL.md
new
+83
@@ -0,0 +1,83 @@
1
+---
2
+name: shadcn_ui-dialog
3
+description: Show modal dialogs with showShadDialog, ShadDialog and ShadDialog.alert; title, description, actions, child content. Use when adding modals, confirmations, or alert dialogs in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Dialog
7
+
8
+## Instructions
9
+
10
+A modal dialog that interrupts the user. Use `showShadDialog(context: context, builder: ...)` and return a `ShadDialog` or `ShadDialog.alert`. Provide `title`, optional `description`, optional `actions` (list of widgets, e.g. buttons), and optional `child` for body content. Dismiss with `Navigator.of(context).pop(...)`.
11
+
12
+### Standard dialog
13
+
14
+```dart
15
+showShadDialog(
16
+ context: context,
17
+ builder: (context) => ShadDialog(
18
+ title: const Text('Edit Profile'),
19
+ description: const Text(
20
+ "Make changes to your profile here. Click save when you're done",
21
+ ),
22
+ actions: const [ShadButton(child: Text('Save changes'))],
23
+ child: Container(
24
+ width: 375,
25
+ padding: const EdgeInsets.symmetric(vertical: 20),
26
+ child: Column(
27
+ mainAxisSize: MainAxisSize.min,
28
+ crossAxisAlignment: CrossAxisAlignment.end,
29
+ spacing: 16,
30
+ children: profile
31
+ .map(
32
+ (p) => Row(
33
+ children: [
34
+ Expanded(
35
+ child: Text(
36
+ p.title,
37
+ textAlign: TextAlign.end,
38
+ style: theme.textTheme.small,
39
+ ),
40
+ ),
41
+ const SizedBox(width: 16),
42
+ Expanded(
43
+ flex: 3,
44
+ child: ShadInput(initialValue: p.value),
45
+ ),
46
+ ],
47
+ ),
48
+ )
49
+ .toList(),
50
+ ),
51
+ ),
52
+ ),
53
+);
54
+```
55
+
56
+### Alert dialog
57
+
58
+Use `ShadDialog.alert` for confirmations; pass `actions` with Cancel and Continue (or similar) that call `Navigator.of(context).pop(false)` and `Navigator.of(context).pop(true)`.
59
+
60
+```dart
61
+showShadDialog(
62
+ context: context,
63
+ builder: (context) => ShadDialog.alert(
64
+ title: const Text('Are you absolutely sure?'),
65
+ description: const Padding(
66
+ padding: EdgeInsets.only(bottom: 8),
67
+ child: Text(
68
+ 'This action cannot be undone. This will permanently delete your account and remove your data from our servers.',
69
+ ),
70
+ ),
71
+ actions: [
72
+ ShadButton.outline(
73
+ child: const Text('Cancel'),
74
+ onPressed: () => Navigator.of(context).pop(false),
75
+ ),
76
+ ShadButton(
77
+ child: const Text('Continue'),
78
+ onPressed: () => Navigator.of(context).pop(true),
79
+ ),
80
+ ],
81
+ ),
82
+);
83
+```
.agent/skills/shadcn_ui-form/SKILL.md
new
+100
@@ -0,0 +1,100 @@
1
+---
2
+name: shadcn_ui-form
3
+description: Build forms with ShadForm, validation, and form field values; use ShadInputFormField, ShadCheckboxFormField, ShadSelectFormField, ShadDatePickerFormField, etc. Use when building validated forms, getting form values, or using dot notation for nested fields in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Form
7
+
8
+## Instructions
9
+
10
+`ShadForm` provides centralized form state, a single `Map<String, dynamic>` for all field values, and no need to manage individual controllers. Use a `GlobalKey<ShadFormState>` to call `saveAndValidate()` and read `value`. Give each form field an `id`; use `Shad*FormField` widgets (e.g. `ShadInputFormField`, `ShadCheckboxFormField`, `ShadSelectFormField`, `ShadDatePickerFormField`, `ShadTimePickerFormField`, `ShadRadioGroupFormField`) inside `ShadForm`.
11
+
12
+### Basic form
13
+
14
+```dart
15
+final formKey = GlobalKey<ShadFormState>();
16
+
17
+ShadForm(
18
+ key: formKey,
19
+ child: Column(
20
+ children: [
21
+ ShadInputFormField(
22
+ id: 'username',
23
+ label: const Text('Username'),
24
+ placeholder: const Text('Enter your username'),
25
+ description: const Text('This is your public display name.'),
26
+ validator: (v) {
27
+ if (v.length < 2) return 'Username must be at least 2 characters.';
28
+ return null;
29
+ },
30
+ ),
31
+ const SizedBox(height: 16),
32
+ ShadButton(
33
+ child: const Text('Submit'),
34
+ onPressed: () {
35
+ if (formKey.currentState!.saveAndValidate()) {
36
+ print('value: ${formKey.currentState!.value}');
37
+ } else {
38
+ print('validation failed');
39
+ }
40
+ },
41
+ ),
42
+ ],
43
+ ),
44
+)
45
+```
46
+
47
+### Submit and get value
48
+
49
+```dart
50
+ShadButton(
51
+ child: const Text('Submit'),
52
+ onPressed: () {
53
+ final formState = formKey.currentState!;
54
+ if (!formState.saveAndValidate()) return;
55
+ print('Form value: ${formState.value}');
56
+ },
57
+),
58
+```
59
+
60
+### Initial value
61
+
62
+Set default values with `initialValue` on `ShadForm`:
63
+
64
+```dart
65
+ShadForm(
66
+ initialValue: {
67
+ 'username': 'john_doe',
68
+ 'email': 'john_doe@example.com',
69
+ },
70
+ child: // form fields with matching ids
71
+)
72
+```
73
+
74
+### Set field or full value
75
+
76
+- Single field: `formKey.currentState!.setFieldValue('username', 'new_username');` (use `notifyField: false` to skip updating the field UI).
77
+- Entire form: `formKey.currentState!.setValue({...});` (use `notifyFields: false` to skip updating fields).
78
+
79
+### Value transformers
80
+
81
+Use `fromValueTransformer` and `toValueTransformer` on form fields when the form value type differs from the field type (e.g. form stores string, field uses `DateTime`):
82
+
83
+```dart
84
+ShadDatePickerFormField(
85
+ id: 'date',
86
+ fromValueTransformer: (value) => DateTime.tryParse(value ?? ''),
87
+ toValueTransformer: (date) =>
88
+ date == null ? null : DateFormat('yyyy-MM-dd').format(date),
89
+ ...
90
+)
91
+```
92
+
93
+### Dot notation for nested values
94
+
95
+Field IDs like `user.name` or `profile.settings.theme` produce nested maps in `formKey.currentState!.value`, e.g. `{'user': {'name': '...', 'email': '...'}}`. Provide `initialValue` as a nested map (not dot-notation keys). Customize separator with `fieldIdSeparator` (e.g. `'/'`); set `fieldIdSeparator: null` to disable nesting and keep flat keys.
96
+
97
+## Additional resources
98
+
99
+- Full details on initial value, setFieldValue/setValue, value transformers, and dot notation: [reference.md](reference.md)
100
+- Form field examples: Checkbox, Switch, Input, Select, RadioGroup, DatePicker, TimePicker component docs/skills.
.agent/skills/shadcn_ui-form/reference.md
new
+47
@@ -0,0 +1,47 @@
1
+# Shadcn UI Form — Reference
2
+
3
+## Initial form value
4
+
5
+All form fields with matching `id`s are initialized from `ShadForm`'s `initialValue` map. Fields with their own `initialValue` override the form's.
6
+
7
+## Get the form value
8
+
9
+`formKey.currentState!.value` returns `Map<String, dynamic>`. Use after a successful `saveAndValidate()`.
10
+
11
+## setFieldValue
12
+
13
+`formKey.currentState!.setFieldValue('username', 'new_username');`
14
+
15
+- Pass `notifyField: false` to update only the form value, not the field UI.
16
+
17
+## setValue
18
+
19
+`formKey.currentState!.setValue({ 'username': '...', 'email': '...' });`
20
+
21
+- Pass `notifyFields: false` to update only the form value, not the fields UI.
22
+
23
+## Value transformers
24
+
25
+- **fromValueTransformer**: Converts form value (e.g. string) to field value (e.g. DateTime).
26
+- **toValueTransformer**: Converts field value back to form value for `value` and validation.
27
+
28
+Example: form has `{'date': '2024-02-01'}`; `ShadDatePickerFormField` needs `DateTime`. Use `fromValueTransformer: (value) => DateTime.tryParse(value ?? '')` and `toValueTransformer: (date) => date == null ? null : DateFormat('yyyy-MM-dd').format(date)`.
29
+
30
+## Dot notation
31
+
32
+- IDs like `user.email` or `profile.settings.theme` create nested maps in `value`.
33
+- `initialValue` must be nested: `{'user': {'name': 'John', 'email': '...'}}`.
34
+- Custom separator: `ShadForm(fieldIdSeparator: '/', ...)` then use `user/name`.
35
+- Disable: `ShadForm(fieldIdSeparator: null, ...)` so `user.email` stays a flat key.
36
+
37
+## Form field components
38
+
39
+Use inside `ShadForm` with unique `id` and optional `validator`:
40
+
41
+- ShadInputFormField
42
+- ShadCheckboxFormField
43
+- ShadSwitchFormField
44
+- ShadSelectFormField
45
+- ShadRadioGroupFormField
46
+- ShadDatePickerFormField / ShadDateRangePickerFormField
47
+- ShadTimePickerFormField
.agent/skills/shadcn_ui-getting-started/SKILL.md
new
+122
@@ -0,0 +1,122 @@
1
+---
2
+name: shadcn_ui-getting-started
3
+description: Install and set up Flutter Shadcn UI (shadcn_ui), configure ShadApp with Material or Cupertino, and use bundled packages. Use when starting a Flutter project with shadcn_ui, adding shadcn_ui dependency, or integrating Shadcn with MaterialApp/CupertinoApp.
4
+---
5
+
6
+# Shadcn UI for Flutter — Getting Started
7
+
8
+## Instructions
9
+
10
+### Installation
11
+
12
+From the project root:
13
+
14
+```bash
15
+flutter pub add shadcn_ui
16
+```
17
+
18
+Or add to `pubspec.yaml`:
19
+
20
+```yaml
21
+dependencies:
22
+ shadcn_ui: ^0.2.4 # use latest version
23
+```
24
+
25
+### Shadcn only (pure)
26
+
27
+Use `ShadApp` for apps that use only Shadcn UI components (no Material/Cupertino):
28
+
29
+```dart
30
+import 'package:shadcn_ui/shadcn_ui.dart';
31
+
32
+void main() {
33
+ runApp(const MyApp());
34
+}
35
+
36
+class MyApp extends StatelessWidget {
37
+ const MyApp({super.key});
38
+
39
+ @override
40
+ Widget build(BuildContext context) {
41
+ return ShadApp();
42
+ }
43
+}
44
+```
45
+
46
+For declarative routing use `ShadApp.router`.
47
+
48
+### Shadcn + Material
49
+
50
+Use `ShadApp.custom` with `appBuilder` to wrap `MaterialApp` and use shadcn and Material together. Wrap the app with `ShadAppBuilder` in the builder:
51
+
52
+```dart
53
+import 'package:shadcn_ui/shadcn_ui.dart';
54
+import 'package:flutter/material.dart';
55
+
56
+return ShadApp.custom(
57
+ themeMode: ThemeMode.dark,
58
+ darkTheme: ShadThemeData(
59
+ brightness: Brightness.dark,
60
+ colorScheme: const ShadSlateColorScheme.dark(),
61
+ ),
62
+ appBuilder: (context) {
63
+ return MaterialApp(
64
+ theme: Theme.of(context),
65
+ builder: (context, child) {
66
+ return ShadAppBuilder(child: child!);
67
+ },
68
+ );
69
+ },
70
+);
71
+```
72
+
73
+Use `MaterialApp.router` when using the Router API.
74
+
75
+ShadApp builds a default Material `ThemeData` from `ShadThemeData` (fontFamily, extensions, colorScheme mapping, dividerTheme, textSelectionTheme, iconTheme, scrollbarTheme). Override with `Theme.of(context).copyWith(...)` without losing shadcn defaults.
76
+
77
+### Shadcn + Cupertino
78
+
79
+Use `CupertinoApp` inside `appBuilder` for Cupertino + shadcn:
80
+
81
+```dart
82
+import 'package:shadcn_ui/shadcn_ui.dart';
83
+import 'package:flutter/cupertino.dart';
84
+import 'package:flutter_localizations/flutter_localizations.dart';
85
+
86
+return ShadApp.custom(
87
+ themeMode: ThemeMode.dark,
88
+ darkTheme: ShadThemeData(
89
+ brightness: Brightness.dark,
90
+ colorScheme: const ShadSlateColorScheme.dark(),
91
+ ),
92
+ appBuilder: (context) {
93
+ return CupertinoApp(
94
+ theme: CupertinoTheme.of(context),
95
+ localizationsDelegates: const [
96
+ DefaultMaterialLocalizations.delegate,
97
+ DefaultCupertinoLocalizations.delegate,
98
+ DefaultWidgetsLocalizations.delegate,
99
+ ],
100
+ builder: (context, child) {
101
+ return ShadAppBuilder(child: child!);
102
+ },
103
+ );
104
+ },
105
+);
106
+```
107
+
108
+Use `CupertinoApp.router` for Router-based navigation. Override Cupertino theme with `CupertinoTheme.of(context).copyWith(...)`.
109
+
110
+### Bundled packages
111
+
112
+Shadcn UI re-exports these packages; no extra imports needed:
113
+
114
+- **flutter_animate** — Animations; components accept `List<Effect>` for customization.
115
+- **lucide_icons_flutter** — Icons via `LucideIcons` (e.g. `LucideIcons.activity`). Browse at https://lucide.dev/icons/
116
+- **two_dimensional_scrollables** — Used by `ShadTable`.
117
+- **intl** — i18n and message translation.
118
+- **universal_image** — Multiple image formats; used by `ShadAvatar`.
119
+
120
+### Submit your app
121
+
122
+To add your app to the showcase: [open the GitHub template](https://github.com/nank1ro/flutter-shadcn-ui/issues/new?template=docs-add-app.yaml) and fill the form.
.agent/skills/shadcn_ui-icon-button/SKILL.md
new
+60
@@ -0,0 +1,60 @@
1
+---
2
+name: shadcn_ui-icon-button
3
+description: Use ShadIconButton and variants (primary, secondary, destructive, outline, ghost); loading state, gradient and shadow. Use when adding icon-only buttons or icon CTAs in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Icon Button
7
+
8
+## Instructions
9
+
10
+`ShadIconButton` displays an icon button. Variants: default (primary), `ShadIconButton.secondary`, `ShadIconButton.destructive`, `ShadIconButton.outline`, `ShadIconButton.ghost`. Use `icon` and `onPressed`; optional `iconSize`, `padding`; support gradient and shadows.
11
+
12
+### Variants
13
+
14
+```dart
15
+ShadIconButton(
16
+ onPressed: () => print('Primary'),
17
+ icon: const Icon(LucideIcons.rocket),
18
+)
19
+
20
+ShadIconButton.secondary(icon: const Icon(LucideIcons.rocket), onPressed: () {})
21
+
22
+ShadIconButton.destructive(icon: const Icon(LucideIcons.rocket), onPressed: () {})
23
+
24
+ShadIconButton.outline(icon: const Icon(LucideIcons.rocket), onPressed: () {})
25
+
26
+ShadIconButton.ghost(icon: const Icon(LucideIcons.rocket), onPressed: () {})
27
+```
28
+
29
+### Loading
30
+
31
+Use a small `CircularProgressIndicator` as `icon`:
32
+
33
+```dart
34
+ShadIconButton(
35
+ icon: SizedBox.square(
36
+ dimension: 16,
37
+ child: CircularProgressIndicator(
38
+ strokeWidth: 2,
39
+ color: ShadTheme.of(context).colorScheme.primaryForeground,
40
+ ),
41
+ ),
42
+)
43
+```
44
+
45
+### Gradient and shadow
46
+
47
+```dart
48
+ShadIconButton(
49
+ gradient: const LinearGradient(colors: [Colors.cyan, Colors.indigo]),
50
+ shadows: [
51
+ BoxShadow(
52
+ color: Colors.blue.withValues(alpha: .4),
53
+ spreadRadius: 4,
54
+ blurRadius: 10,
55
+ offset: const Offset(0, 2),
56
+ ),
57
+ ],
58
+ icon: const Icon(LucideIcons.rocket),
59
+)
60
+```
.agent/skills/shadcn_ui-input-otp/SKILL.md
new
+93
@@ -0,0 +1,93 @@
1
+---
2
+name: shadcn_ui-input-otp
3
+description: Build OTP input with ShadInputOTP, ShadInputOTPGroup, ShadInputOTPSlot; maxLength, inputFormatters, ShadInputOTPFormField. Use when adding one-time password or verification code inputs in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Input OTP
7
+
8
+## Instructions
9
+
10
+`ShadInputOTP` is an accessible one-time password component with copy-paste support. Use `children` to build the layout: `ShadInputOTPGroup` containing `ShadInputOTPSlot`s, with optional separators (e.g. `Icon(LucideIcons.dot)`). Set `maxLength`; use `onChanged` for the full OTP string. Restrict input with `keyboardType` and `inputFormatters` (e.g. `FilteringTextInputFormatter.digitsOnly`). For forms use `ShadInputOTPFormField` with `id`, `label`, `validator`.
11
+
12
+### Basic
13
+
14
+```dart
15
+ShadInputOTP(
16
+ onChanged: (v) => print('OTP: $v'),
17
+ maxLength: 6,
18
+ children: const [
19
+ ShadInputOTPGroup(
20
+ children: [
21
+ ShadInputOTPSlot(),
22
+ ShadInputOTPSlot(),
23
+ ShadInputOTPSlot(),
24
+ ],
25
+ ),
26
+ Icon(size: 24, LucideIcons.dot),
27
+ ShadInputOTPGroup(
28
+ children: [
29
+ ShadInputOTPSlot(),
30
+ ShadInputOTPSlot(),
31
+ ShadInputOTPSlot(),
32
+ ],
33
+ ),
34
+ ],
35
+)
36
+```
37
+
38
+### With input formatters (digits only)
39
+
40
+```dart
41
+ShadInputOTP(
42
+ onChanged: (v) => print('OTP: $v'),
43
+ maxLength: 4,
44
+ keyboardType: TextInputType.number,
45
+ inputFormatters: [
46
+ FilteringTextInputFormatter.digitsOnly,
47
+ ],
48
+ children: const [
49
+ ShadInputOTPGroup(
50
+ children: [
51
+ ShadInputOTPSlot(),
52
+ ShadInputOTPSlot(),
53
+ ShadInputOTPSlot(),
54
+ ShadInputOTPSlot(),
55
+ ],
56
+ ),
57
+ ],
58
+)
59
+```
60
+
61
+The package also provides `UpperCaseTextInputFormatter` and `LowerCaseTextInputFormatter`.
62
+
63
+### Form field
64
+
65
+```dart
66
+ShadInputOTPFormField(
67
+ id: 'otp',
68
+ maxLength: 6,
69
+ label: const Text('OTP'),
70
+ description: const Text('Enter your OTP.'),
71
+ validator: (v) {
72
+ if (v.contains(' ')) return 'Fill the whole OTP code';
73
+ return null;
74
+ },
75
+ children: const [
76
+ ShadInputOTPGroup(
77
+ children: [
78
+ ShadInputOTPSlot(),
79
+ ShadInputOTPSlot(),
80
+ ShadInputOTPSlot(),
81
+ ],
82
+ ),
83
+ Icon(size: 24, LucideIcons.dot),
84
+ ShadInputOTPGroup(
85
+ children: [
86
+ ShadInputOTPSlot(),
87
+ ShadInputOTPSlot(),
88
+ ShadInputOTPSlot(),
89
+ ],
90
+ ),
91
+ ],
92
+)
93
+```
.agent/skills/shadcn_ui-input/SKILL.md
new
+59
@@ -0,0 +1,59 @@
1
+---
2
+name: shadcn_ui-input
3
+description: Use ShadInput for text fields; placeholder, leading/trailing widgets, obscureText; ShadInputFormField for forms with validation. Use when adding text inputs, email/password fields, or form input fields in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Input
7
+
8
+## Instructions
9
+
10
+`ShadInput` displays a form input or input-like component. Use `placeholder`, `initialValue`, `keyboardType`, `obscureText`; optional `leading` and `trailing` widgets. For forms use `ShadInputFormField` with `id`, `label`, `description`, `validator`.
11
+
12
+### Basic
13
+
14
+```dart
15
+const ShadInput(
16
+ placeholder: Text('Email'),
17
+ keyboardType: TextInputType.emailAddress,
18
+)
19
+```
20
+
21
+### With leading and trailing
22
+
23
+Example: password with toggle visibility:
24
+
25
+```dart
26
+ShadInput(
27
+ placeholder: const Text('Password'),
28
+ obscureText: obscure,
29
+ leading: Icon(LucideIcons.lock),
30
+ trailing: SizedBox.square(
31
+ dimension: 24,
32
+ child: OverflowBox(
33
+ maxWidth: 28,
34
+ maxHeight: 28,
35
+ child: ShadIconButton(
36
+ iconSize: 20,
37
+ padding: EdgeInsets.all(2),
38
+ icon: Icon(obscure ? LucideIcons.eyeOff : LucideIcons.eye),
39
+ onPressed: () => setState(() => obscure = !obscure),
40
+ ),
41
+ ),
42
+ ),
43
+)
44
+```
45
+
46
+### Form field
47
+
48
+```dart
49
+ShadInputFormField(
50
+ id: 'username',
51
+ label: const Text('Username'),
52
+ placeholder: const Text('Enter your username'),
53
+ description: const Text('This is your public display name.'),
54
+ validator: (v) {
55
+ if (v.length < 2) return 'Username must be at least 2 characters.';
56
+ return null;
57
+ },
58
+)
59
+```
.agent/skills/shadcn_ui-menubar/SKILL.md
new
+70
@@ -0,0 +1,70 @@
1
+---
2
+name: shadcn_ui-menubar
3
+description: Build desktop-style menubars with ShadMenubar and ShadMenubarItem; nested items, ShadContextMenuItem, dividers. Use when adding a persistent top menu (File, Edit, View, etc.) in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Menubar
7
+
8
+## Instructions
9
+
10
+`ShadMenubar` is a visually persistent menu (e.g. File, Edit, View). Each top-level entry is a `ShadMenubarItem` with `child` (label) and `items` (list of menu entries). Use `ShadContextMenuItem` and `ShadContextMenuItem.inset` for entries; use `ShadSeparator.horizontal` or `Divider(height: 8)` for dividers. Nest `ShadContextMenuItem` with `items` for submenus; use `trailing: Icon(LucideIcons.chevronRight)` on parent. Set `enabled: false` to disable an item.
11
+
12
+### Structure
13
+
14
+```dart
15
+ShadMenubar(
16
+ items: [
17
+ ShadMenubarItem(
18
+ items: [
19
+ const ShadContextMenuItem(child: Text('New Tab')),
20
+ const ShadContextMenuItem(child: Text('New Window')),
21
+ const ShadContextMenuItem(
22
+ enabled: false,
23
+ child: Text('New Incognito Window'),
24
+ ),
25
+ divider,
26
+ const ShadContextMenuItem(
27
+ trailing: Icon(LucideIcons.chevronRight),
28
+ items: [
29
+ ShadContextMenuItem(child: Text('Email Link')),
30
+ ShadContextMenuItem(child: Text('Messages')),
31
+ ],
32
+ child: Text('Share'),
33
+ ),
34
+ divider,
35
+ const ShadContextMenuItem(child: Text('Print...')),
36
+ ],
37
+ child: const Text('File'),
38
+ ),
39
+ ShadMenubarItem(
40
+ items: [
41
+ const ShadContextMenuItem(child: Text('Undo')),
42
+ const ShadContextMenuItem(child: Text('Redo')),
43
+ divider,
44
+ ShadContextMenuItem(
45
+ trailing: const Icon(LucideIcons.chevronRight),
46
+ items: [
47
+ const ShadContextMenuItem(child: Text('Find...')),
48
+ const ShadContextMenuItem(child: Text('Find Next')),
49
+ ],
50
+ child: const Text('Find'),
51
+ ),
52
+ divider,
53
+ const ShadContextMenuItem(child: Text('Cut')),
54
+ const ShadContextMenuItem(child: Text('Copy')),
55
+ const ShadContextMenuItem(child: Text('Paste')),
56
+ ],
57
+ child: const Text('Edit'),
58
+ ),
59
+ ShadMenubarItem(
60
+ items: [
61
+ const ShadContextMenuItem.inset(child: Text('Reload')),
62
+ const ShadContextMenuItem.inset(child: Text('Toggle Full Screen')),
63
+ ],
64
+ child: const Text('View'),
65
+ ),
66
+ ],
67
+)
68
+```
69
+
70
+Create a reusable `divider` with `ShadSeparator.horizontal(margin: const EdgeInsets.symmetric(vertical: 4), color: theme.colorScheme.muted)`.
.agent/skills/shadcn_ui-popover/SKILL.md
new
+41
@@ -0,0 +1,41 @@
1
+---
2
+name: shadcn_ui-popover
3
+description: Show rich content in a popover with ShadPopover, ShadPopoverController; trigger button, toggle. Use when adding popovers, floating panels, or button-triggered overlay content in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Popover
7
+
8
+## Instructions
9
+
10
+`ShadPopover` displays rich content in a portal, triggered by a button (or other trigger). Use a `ShadPopoverController()` to control open/close; call `popoverController.toggle` from the trigger. Pass `popover: (context) => ...` returning the overlay widget (e.g. a `SizedBox` with form fields); `child` is the trigger widget.
11
+
12
+### Basic usage
13
+
14
+```dart
15
+final popoverController = ShadPopoverController();
16
+
17
+// dispose: popoverController.dispose();
18
+
19
+ShadPopover(
20
+ controller: popoverController,
21
+ popover: (context) => SizedBox(
22
+ width: 288,
23
+ child: Column(
24
+ crossAxisAlignment: CrossAxisAlignment.start,
25
+ mainAxisSize: MainAxisSize.min,
26
+ children: [
27
+ Text('Dimensions', style: textTheme.h4),
28
+ Text('Set the dimensions for the layer.', style: textTheme.p),
29
+ const SizedBox(height: 4),
30
+ // ... form rows with ShadInput, etc.
31
+ ],
32
+ ),
33
+ ),
34
+ child: ShadButton.outline(
35
+ onPressed: popoverController.toggle,
36
+ child: const Text('Open popover'),
37
+ ),
38
+)
39
+```
40
+
41
+Dispose the controller in `State.dispose`.
.agent/skills/shadcn_ui-progress/SKILL.md
new
+29
@@ -0,0 +1,29 @@
1
+---
2
+name: shadcn_ui-progress
3
+description: Show progress with ShadProgress; determinate (value 0–1) or indeterminate. Use when displaying task completion or loading progress in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Progress
7
+
8
+## Instructions
9
+
10
+`ShadProgress` displays completion progress, typically as a progress bar. Use determinate mode with a `value` (0.0–1.0); omit `value` for indeterminate (animated) progress.
11
+
12
+### Determinate
13
+
14
+```dart
15
+ConstrainedBox(
16
+ constraints: BoxConstraints(
17
+ maxWidth: MediaQuery.sizeOf(context).width * 0.6,
18
+ ),
19
+ child: const ShadProgress(value: 0.5),
20
+)
21
+```
22
+
23
+### Indeterminate
24
+
25
+```dart
26
+const ShadProgress()
27
+```
28
+
29
+Constrain width with `ConstrainedBox` or parent layout as needed.
.agent/skills/shadcn_ui-radio-group/SKILL.md
new
+42
@@ -0,0 +1,42 @@
1
+---
2
+name: shadcn_ui-radio-group
3
+description: Use ShadRadioGroup and ShadRadio for single-choice options; ShadRadioGroupFormField for forms. Use when adding radio buttons or single-select options in a Flutter shadcn_ui app or ShadForm.
4
+---
5
+
6
+# Shadcn UI — Radio Group
7
+
8
+## Instructions
9
+
10
+`ShadRadioGroup<T>` is a set of radio buttons where at most one can be selected. Provide `items` as a list of `ShadRadio<T>` with `value` and `label`. Use `ShadRadioGroupFormField<T>` inside `ShadForm` with `id`, `label`, `items`, and `validator`.
11
+
12
+### Standalone
13
+
14
+```dart
15
+ShadRadioGroup<String>(
16
+ items: [
17
+ ShadRadio(label: Text('Default'), value: 'default'),
18
+ ShadRadio(label: Text('Comfortable'), value: 'comfortable'),
19
+ ShadRadio(label: Text('Nothing'), value: 'nothing'),
20
+ ],
21
+)
22
+```
23
+
24
+### Form field
25
+
26
+```dart
27
+enum NotifyAbout { all, mentions, nothing; }
28
+
29
+ShadRadioGroupFormField<NotifyAbout>(
30
+ label: const Text('Notify me about'),
31
+ items: NotifyAbout.values.map(
32
+ (e) => ShadRadio(
33
+ value: e,
34
+ label: Text(e.message),
35
+ ),
36
+ ),
37
+ validator: (v) {
38
+ if (v == null) return 'You need to select a notification type.';
39
+ return null;
40
+ },
41
+)
42
+```
.agent/skills/shadcn_ui-resizable/SKILL.md
new
+81
@@ -0,0 +1,81 @@
1
+---
2
+name: shadcn_ui-resizable
3
+description: Build resizable panel layouts with ShadResizablePanelGroup and ShadResizablePanel; horizontal/vertical axis, defaultSize, minSize, maxSize, showHandle. Use when adding resizable split panes or panel groups in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Resizable
7
+
8
+## Instructions
9
+
10
+`ShadResizablePanelGroup` contains `ShadResizablePanel` children. Each panel has `id`, `defaultSize` (fraction 0–1), optional `minSize` and `maxSize`, and `child`. Use `axis: Axis.vertical` for vertical splits. Nest `ShadResizablePanelGroup` inside a panel for nested layouts. Set `showHandle: true` to show the resize handle; optional `handleIcon` or `handleIconSrc` to customize. Double-click the handle to reset to default size.
11
+
12
+### Basic horizontal
13
+
14
+```dart
15
+ShadResizablePanelGroup(
16
+ children: [
17
+ ShadResizablePanel(
18
+ id: 0,
19
+ defaultSize: .5,
20
+ minSize: .2,
21
+ maxSize: .8,
22
+ child: Center(child: Text('One', style: theme.textTheme.large)),
23
+ ),
24
+ ShadResizablePanel(
25
+ id: 1,
26
+ defaultSize: .5,
27
+ child: ShadResizablePanelGroup(
28
+ axis: Axis.vertical,
29
+ children: [
30
+ ShadResizablePanel(
31
+ id: 0,
32
+ defaultSize: .3,
33
+ child: Center(child: Text('Two', style: theme.textTheme.large)),
34
+ ),
35
+ ShadResizablePanel(
36
+ id: 1,
37
+ defaultSize: .7,
38
+ child: Align(child: Text('Three', style: theme.textTheme.large)),
39
+ ),
40
+ ],
41
+ ),
42
+ ),
43
+ ],
44
+)
45
+```
46
+
47
+### Vertical
48
+
49
+```dart
50
+ShadResizablePanelGroup(
51
+ axis: Axis.vertical,
52
+ children: [
53
+ ShadResizablePanel(
54
+ id: 0,
55
+ defaultSize: 0.3,
56
+ minSize: 0.1,
57
+ child: Center(child: Text('Header', style: theme.textTheme.large)),
58
+ ),
59
+ ShadResizablePanel(
60
+ id: 1,
61
+ defaultSize: 0.7,
62
+ minSize: 0.1,
63
+ child: Center(child: Text('Footer', style: theme.textTheme.large)),
64
+ ),
65
+ ],
66
+)
67
+```
68
+
69
+### With visible handle
70
+
71
+```dart
72
+ShadResizablePanelGroup(
73
+ showHandle: true,
74
+ children: [
75
+ ShadResizablePanel(id: 0, defaultSize: .5, minSize: .2, child: ...),
76
+ ShadResizablePanel(id: 1, defaultSize: .5, minSize: .2, child: ...),
77
+ ],
78
+)
79
+```
80
+
81
+Wrap the group in a bordered container (e.g. `DecoratedBox` + `ClipRRect` with `theme.radius`) for a clean look.
.agent/skills/shadcn_ui-responsive/SKILL.md
new
+65
@@ -0,0 +1,65 @@
1
+---
2
+name: shadcn_ui-responsive
3
+description: Use Shadcn UI breakpoints and responsive layout with ShadBreakpoints, ShadResponsiveBuilder, and context.breakpoint. Use when building responsive Flutter shadcn_ui layouts, adapting UI to screen size, or checking current breakpoint (tn, sm, md, lg, xl, xxl).
4
+---
5
+
6
+# Shadcn UI — Responsive
7
+
8
+## Instructions
9
+
10
+Responsiveness is built into shadcn_ui via `ShadThemeData.breakpoints`. Use `ShadResponsiveBuilder` or `context.breakpoint` to branch on the current breakpoint.
11
+
12
+### Default breakpoints
13
+
14
+```dart
15
+ShadThemeData(
16
+ breakpoints: ShadBreakpoints(
17
+ tn: 0, // tiny
18
+ sm: 640, // small
19
+ md: 768, // medium
20
+ lg: 1024, // large
21
+ xl: 1280, // extra large
22
+ xxl: 1536, // extra extra large
23
+ ),
24
+);
25
+```
26
+
27
+### Current breakpoint
28
+
29
+**With ShadResponsiveBuilder:**
30
+
31
+```dart
32
+ShadResponsiveBuilder(
33
+ builder: (context, breakpoint) {
34
+ final sm = breakpoint >= ShadTheme.of(context).breakpoints.sm;
35
+ // ...
36
+ },
37
+),
38
+```
39
+
40
+**With context (equivalent):**
41
+
42
+```dart
43
+final sm = context.breakpoint >= ShadTheme.of(context).breakpoints.sm;
44
+```
45
+
46
+Use `>=` so that e.g. "sm" applies to small and larger viewports (Tailwind-style). Use `==` only when you need to match a single breakpoint.
47
+
48
+### Sealed class switch
49
+
50
+The breakpoint is a sealed type; you can switch on it:
51
+
52
+```dart
53
+ShadResponsiveBuilder(
54
+ builder: (context, breakpoint) {
55
+ return switch (breakpoint) {
56
+ ShadBreakpointTN() => const Text('Tiny'),
57
+ ShadBreakpointSM() => const Text('Small'),
58
+ ShadBreakpointMD() => const Text('Medium'),
59
+ ShadBreakpointLG() => const Text('Large'),
60
+ ShadBreakpointXL() => const Text('Extra Large'),
61
+ ShadBreakpointXXL() => const Text('Extra Extra Large'),
62
+ };
63
+ },
64
+),
65
+```
.agent/skills/shadcn_ui-select/SKILL.md
new
+68
@@ -0,0 +1,68 @@
1
+---
2
+name: shadcn_ui-select
3
+description: Use ShadSelect for dropdown option lists; ShadOption, placeholder, selectedOptionBuilder, scrollable groups, ShadSelect.withSearch, ShadSelect.multiple, ShadSelectFormField. Use when adding dropdowns, single/multi select, or searchable select in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Select
7
+
8
+## Instructions
9
+
10
+`ShadSelect<T>` displays a list of options triggered by a button. Provide `options` (list of `ShadOption<T>` or section headers like `Padding` with text), `placeholder`, `selectedOptionBuilder: (context, value) => ...`, and `onChanged`. Use `initialValue` or controlled state for selected value. Set `minWidth`/`maxWidth`/`maxHeight` as needed. For forms use `ShadSelectFormField<T>` with `id`, `validator`. Use `allowDeselection: true` to allow clearing the selection.
11
+
12
+### Basic
13
+
14
+```dart
15
+final fruits = {'apple': 'Apple', 'banana': 'Banana', ...};
16
+
17
+ShadSelect<String>(
18
+ placeholder: const Text('Select a fruit'),
19
+ options: [
20
+ Padding(
21
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
22
+ child: Text(
23
+ 'Fruits',
24
+ style: theme.textTheme.muted.copyWith(
25
+ fontWeight: FontWeight.w600,
26
+ color: theme.colorScheme.popoverForeground,
27
+ ),
28
+ textAlign: TextAlign.start,
29
+ ),
30
+ ),
31
+ ...fruits.entries.map((e) => ShadOption(value: e.key, child: Text(e.value))),
32
+ ],
33
+ selectedOptionBuilder: (context, value) => Text(fruits[value]!),
34
+ onChanged: print,
35
+)
36
+```
37
+
38
+### Form field
39
+
40
+```dart
41
+ShadSelectFormField<String>(
42
+ id: 'email',
43
+ minWidth: 350,
44
+ initialValue: null,
45
+ options: verifiedEmails
46
+ .map((email) => ShadOption(value: email, child: Text(email)))
47
+ .toList(),
48
+ selectedOptionBuilder: (context, value) =>
49
+ value == 'none' ? const Text('Select a verified email to display') : Text(value),
50
+ placeholder: const Text('Select a verified email to display'),
51
+ validator: (v) {
52
+ if (v == null) return 'Please select an email to display';
53
+ return null;
54
+ },
55
+)
56
+```
57
+
58
+### With search
59
+
60
+Use `ShadSelect.withSearch`. Pass `onSearchChanged`, `searchPlaceholder`, and filter options in state (e.g. wrap options in `Offstage(offstage: !filtered.containsKey(key), child: ShadOption(...))` to avoid focus loss when results change).
61
+
62
+### Multiple
63
+
64
+Use `ShadSelect.multiple`. Pass `selectedOptionsBuilder: (context, values) => ...`, `onChanged`, and optionally `allowDeselection: true`, `closeOnSelect: false` to keep popover open after selection.
65
+
66
+## Additional resources
67
+
68
+Scrollable option lists (grouped sections) and full search/multiple examples: [reference.md](reference.md).
.agent/skills/shadcn_ui-select/reference.md
new
+48
@@ -0,0 +1,48 @@
1
+# Shadcn UI Select — Reference
2
+
3
+## Scrollable / grouped options
4
+
5
+Build `options` as a list of section headers (`Padding` with styled `Text`) followed by `ShadOption` widgets for that section. For large lists the select popover scrolls. Example for timezones by region:
6
+
7
+```dart
8
+List<Widget> getTimezonesWidgets(ShadThemeData theme) {
9
+ final widgets = <Widget>[];
10
+ for (final zone in timezones.entries) {
11
+ widgets.add(
12
+ Padding(
13
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
14
+ child: Text(
15
+ zone.key,
16
+ style: theme.textTheme.muted.copyWith(
17
+ fontWeight: FontWeight.w600,
18
+ color: theme.colorScheme.popoverForeground,
19
+ ),
20
+ textAlign: TextAlign.start,
21
+ ),
22
+ ),
23
+ );
24
+ widgets.addAll(zone.value.entries
25
+ .map((e) => ShadOption(value: e.key, child: Text(e.value))));
26
+ }
27
+ return widgets;
28
+}
29
+
30
+ShadSelect<String>(
31
+ placeholder: const Text('Select a timezone'),
32
+ options: getTimezonesWidgets(theme),
33
+ selectedOptionBuilder: (context, value) {
34
+ final timezone = timezones.entries
35
+ .firstWhere((element) => element.value.containsKey(value))
36
+ .value[value];
37
+ return Text(timezone!);
38
+ },
39
+)
40
+```
41
+
42
+## With search
43
+
44
+Use `ShadSelect<String>.withSearch`, maintain `searchValue` in state, compute `filteredFrameworks` (or similar), and pass `onSearchChanged: (value) => setState(() => searchValue = value)`. In `options`, show "No results" when filtered is empty; otherwise map entries and wrap each `ShadOption` in `Offstage(offstage: !filtered.containsKey(key), child: ShadOption(...))` so the widget stays in the tree and focus is preserved.
45
+
46
+## Multiple selection
47
+
48
+Use `ShadSelect<T>.multiple`. Provide `selectedOptionsBuilder: (context, values) => Text(values.join(', '))` (or similar), `onChanged`, and optionally `allowDeselection: true`, `closeOnSelect: false`. Tap outside the popover to close it.
.agent/skills/shadcn_ui-separator/SKILL.md
new
+30
@@ -0,0 +1,30 @@
1
+---
2
+name: shadcn_ui-separator
3
+description: Visually separate content with ShadSeparator.horizontal and ShadSeparator.vertical; thickness, margin, radius. Use when adding dividers or visual separation in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Separator
7
+
8
+## Instructions
9
+
10
+`ShadSeparator` visually or semantically separates content. Use `ShadSeparator.horizontal` for a horizontal line (e.g. between rows); use `ShadSeparator.vertical` for a vertical line (e.g. between columns). Optional: `thickness`, `margin`, `radius`, `color`.
11
+
12
+### Horizontal
13
+
14
+```dart
15
+const ShadSeparator.horizontal(
16
+ thickness: 4,
17
+ margin: EdgeInsets.symmetric(horizontal: 20),
18
+ radius: BorderRadius.all(Radius.circular(4)),
19
+)
20
+```
21
+
22
+### Vertical
23
+
24
+```dart
25
+const ShadSeparator.vertical(
26
+ thickness: 4,
27
+ margin: EdgeInsets.symmetric(vertical: 20),
28
+ radius: BorderRadius.all(Radius.circular(4)),
29
+)
30
+```
.agent/skills/shadcn_ui-sheet/SKILL.md
new
+61
@@ -0,0 +1,61 @@
1
+---
2
+name: shadcn_ui-sheet
3
+description: Show slide-out panels with showShadSheet and ShadSheet; side (top, right, bottom, left), title, description, actions, child. Use when adding side panels, drawers, or slide-over content in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Sheet
7
+
8
+## Instructions
9
+
10
+Sheets extend the dialog pattern to display content from an edge of the screen. Use `showShadSheet(context: context, side: ShadSheetSide.*, builder: (context) => ShadSheet(...))`. `side` can be `ShadSheetSide.top`, `ShadSheetSide.right`, `ShadSheetSide.bottom`, or `ShadSheetSide.left`. Return a `ShadSheet` with optional `title`, `description`, `child` (body), and `actions`. Use `constraints` (e.g. `BoxConstraints(maxWidth: 512)` for left/right) to limit width or height.
11
+
12
+### Basic (right side)
13
+
14
+```dart
15
+showShadSheet(
16
+ side: ShadSheetSide.right,
17
+ context: context,
18
+ builder: (context) => ShadSheet(
19
+ constraints: const BoxConstraints(maxWidth: 512),
20
+ title: const Text('Edit Profile'),
21
+ description: const Text(
22
+ "Make changes to your profile here. Click save when you're done",
23
+ ),
24
+ child: Padding(
25
+ padding: const EdgeInsets.symmetric(vertical: 20),
26
+ child: Column(
27
+ mainAxisSize: MainAxisSize.min,
28
+ crossAxisAlignment: CrossAxisAlignment.stretch,
29
+ spacing: 16,
30
+ children: profile
31
+ .map(
32
+ (p) => Row(
33
+ children: [
34
+ Expanded(
35
+ child: Text(
36
+ p.title,
37
+ textAlign: TextAlign.end,
38
+ style: theme.textTheme.small,
39
+ ),
40
+ ),
41
+ const SizedBox(width: 16),
42
+ Expanded(
43
+ flex: 5,
44
+ child: ShadInput(initialValue: p.value),
45
+ ),
46
+ ],
47
+ ),
48
+ )
49
+ .toList(),
50
+ ),
51
+ ),
52
+ actions: const [
53
+ ShadButton(child: Text('Save changes')),
54
+ ],
55
+ ),
56
+);
57
+```
58
+
59
+### All sides
60
+
61
+Use `side: ShadSheetSide.top`, `ShadSheetSide.bottom`, `ShadSheetSide.left`, or `ShadSheetSide.right` in `showShadSheet`. For left/right sheets pass `constraints: const BoxConstraints(maxWidth: 512)` to the `ShadSheet` so content doesn’t stretch full width.
.agent/skills/shadcn_ui-slider/SKILL.md
new
+21
@@ -0,0 +1,21 @@
1
+---
2
+name: shadcn_ui-slider
3
+description: Use ShadSlider for numeric input within a range; initialValue, max (and min). Use when adding range sliders or numeric selection in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Slider
7
+
8
+## Instructions
9
+
10
+`ShadSlider` lets the user select a value from a range. Set `initialValue` and `max` (and optionally `min`). Use with state or a controller if you need to read the value on change.
11
+
12
+### Basic
13
+
14
+```dart
15
+ShadSlider(
16
+ initialValue: 33,
17
+ max: 100,
18
+)
19
+```
20
+
21
+Use callbacks or state management to react to value changes when needed.
.agent/skills/shadcn_ui-sonner/SKILL.md
new
+36
@@ -0,0 +1,36 @@
1
+---
2
+name: shadcn_ui-sonner
3
+description: Show toasts with ShadSonner and ShadToast; ShadSonner.of(context), show/hide, title, description, action. Use when showing toast notifications or ephemeral feedback in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Sonner
7
+
8
+## Instructions
9
+
10
+Sonner is an opinionated toast component. Ensure `ShadSonner` is in the widget tree (e.g. in app builder). Get the controller with `ShadSonner.of(context)`. Call `sonner.show(ShadToast(...))` to show a toast; use `sonner.hide(id)` to dismiss. Each toast can have `id`, `title`, `description`, and optional `action` (e.g. an Undo button).
11
+
12
+### Show toast
13
+
14
+```dart
15
+ShadButton.outline(
16
+ child: const Text('Show Toast'),
17
+ onPressed: () {
18
+ final sonner = ShadSonner.of(context);
19
+ final id = Random().nextInt(1000);
20
+ final now = DateTime.now();
21
+ sonner.show(
22
+ ShadToast(
23
+ id: id,
24
+ title: const Text('Event has been created'),
25
+ description: Text(DateFormat.yMd().add_jms().format(now)),
26
+ action: ShadButton(
27
+ child: const Text('Undo'),
28
+ onPressed: () => sonner.hide(id),
29
+ ),
30
+ ),
31
+ );
32
+ },
33
+)
34
+```
35
+
36
+Use a unique `id` per toast so you can hide it from the action button or elsewhere.
.agent/skills/shadcn_ui-switch/SKILL.md
new
+36
@@ -0,0 +1,36 @@
1
+---
2
+name: shadcn_ui-switch
3
+description: Add toggle switches with ShadSwitch and ShadSwitchFormField; label, form validation. Use when adding on/off toggles or switch form fields in a Flutter shadcn_ui app or ShadForm.
4
+---
5
+
6
+# Shadcn UI — Switch
7
+
8
+## Instructions
9
+
10
+`ShadSwitch` is a control that toggles between checked and unchecked. Use `value` and `onChanged`; optional `label`. For forms use `ShadSwitchFormField` with `id`, `initialValue`, `inputLabel`, `inputSublabel`, `validator`.
11
+
12
+### Standalone
13
+
14
+```dart
15
+ShadSwitch(
16
+ value: value,
17
+ onChanged: (v) => setState(() => value = v),
18
+ label: const Text('Airplane Mode'),
19
+)
20
+```
21
+
22
+### Form field
23
+
24
+```dart
25
+ShadSwitchFormField(
26
+ id: 'terms',
27
+ initialValue: false,
28
+ inputLabel: const Text('I accept the terms and conditions'),
29
+ onChanged: (v) {},
30
+ inputSublabel: const Text('You agree to our Terms and Conditions'),
31
+ validator: (v) {
32
+ if (!v) return 'You must accept the terms and conditions';
33
+ return null;
34
+ },
35
+)
36
+```
.agent/skills/shadcn_ui-table/SKILL.md
new
+69
@@ -0,0 +1,69 @@
1
+---
2
+name: shadcn_ui-table
3
+description: Build tables with ShadTable.list or ShadTable builder; ShadTableCell.header, ShadTableCell.footer, columnSpanExtent (FixedTableSpanExtent, RemainingTableSpanExtent). Use when displaying tabular data or responsive tables in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Table
7
+
8
+## Instructions
9
+
10
+`ShadTable` is a responsive table. Use `ShadTable.list` for small tables (all rows built at once): pass `header`, `footer` (lists of `ShadTableCell.header` / `ShadTableCell.footer`), and `children` (list of row lists of `ShadTableCell`). Use `ShadTable` with `columnCount`, `rowCount`, `header`, `builder`, `footer` for large tables (on-demand building). Use `columnSpanExtent` to set column widths: `FixedTableSpanExtent(130)` or `MaxTableSpanExtent(FixedTableSpanExtent(120), RemainingTableSpanExtent())`.
11
+
12
+### List (small tables)
13
+
14
+```dart
15
+ShadTable.list(
16
+ header: const [
17
+ ShadTableCell.header(child: Text('Invoice')),
18
+ ShadTableCell.header(child: Text('Status')),
19
+ ShadTableCell.header(child: Text('Method')),
20
+ ShadTableCell.header(
21
+ alignment: Alignment.centerRight,
22
+ child: Text('Amount'),
23
+ ),
24
+ ],
25
+ footer: const [
26
+ ShadTableCell.footer(child: Text('Total')),
27
+ ShadTableCell.footer(child: Text('')),
28
+ ShadTableCell.footer(child: Text('')),
29
+ ShadTableCell.footer(
30
+ alignment: Alignment.centerRight,
31
+ child: Text(r'$2500.00'),
32
+ ),
33
+ ],
34
+ columnSpanExtent: (index) {
35
+ if (index == 2) return const FixedTableSpanExtent(130);
36
+ if (index == 3) {
37
+ return const MaxTableSpanExtent(
38
+ FixedTableSpanExtent(120),
39
+ RemainingTableSpanExtent(),
40
+ );
41
+ }
42
+ return null;
43
+ },
44
+ children: invoices.map(
45
+ (invoice) => [
46
+ ShadTableCell(
47
+ child: Text(
48
+ invoice.invoice,
49
+ style: const TextStyle(fontWeight: FontWeight.w500),
50
+ ),
51
+ ),
52
+ ShadTableCell(child: Text(invoice.paymentStatus)),
53
+ ShadTableCell(child: Text(invoice.paymentMethod)),
54
+ ShadTableCell(
55
+ alignment: Alignment.centerRight,
56
+ child: Text(invoice.totalAmount),
57
+ ),
58
+ ],
59
+ ),
60
+)
61
+```
62
+
63
+### Builder (large tables)
64
+
65
+Use `ShadTable(columnCount:, rowCount:, header: (context, column) => ..., columnSpanExtent: (index) => ..., builder: (context, index) => ShadTableCell(...), footer: (context, column) => ...)`. The builder receives a table index with `row` and `column`. Prefer builder for large data so only visible rows are built.
66
+
67
+## Additional resources
68
+
69
+Detailed column span and builder patterns: [reference.md](reference.md).
.agent/skills/shadcn_ui-table/reference.md
new
+19
@@ -0,0 +1,19 @@
1
+# Shadcn UI Table — Reference
2
+
3
+## columnSpanExtent
4
+
5
+Return `TableSpanExtent?` per column index:
6
+- `FixedTableSpanExtent(130)` — fixed width.
7
+- `MaxTableSpanExtent(FixedTableSpanExtent(120), RemainingTableSpanExtent())` — min 120, then remaining space.
8
+- `null` — default extent.
9
+
10
+## Builder API
11
+
12
+`ShadTable` with:
13
+- `columnCount`, `rowCount`
14
+- `header: (context, column) => ShadTableCell.header(...)`
15
+- `columnSpanExtent: (index) => ...`
16
+- `builder: (context, index) => ShadTableCell(...)` where `index` has `index.row`, `index.column`
17
+- `footer: (context, column) => ShadTableCell.footer(...)` or empty cell
18
+
19
+Use for large tables so widgets are created on demand.
.agent/skills/shadcn_ui-tabs/SKILL.md
new
+63
@@ -0,0 +1,63 @@
1
+---
2
+name: shadcn_ui-tabs
3
+description: Build tabbed UIs with ShadTabs and ShadTab; value, tabs (content + child label), tabBarConstraints, contentConstraints. Use when adding tab panels or layered sections in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Tabs
7
+
8
+## Instructions
9
+
10
+`ShadTabs<T>` displays layered sections (tab panels) one at a time. Provide `value` (currently selected tab value), `tabs` (list of `ShadTab<T>`), and optional `tabBarConstraints` and `contentConstraints`. Each `ShadTab` has `value`, `child` (tab label), and `content` (panel widget).
11
+
12
+### Basic
13
+
14
+```dart
15
+ShadTabs<String>(
16
+ value: 'account',
17
+ tabBarConstraints: const BoxConstraints(maxWidth: 400),
18
+ contentConstraints: const BoxConstraints(maxWidth: 400),
19
+ tabs: [
20
+ ShadTab(
21
+ value: 'account',
22
+ content: ShadCard(
23
+ title: const Text('Account'),
24
+ description: const Text(
25
+ "Make changes to your account here. Click save when you're done."),
26
+ footer: const ShadButton(child: Text('Save changes')),
27
+ child: Column(
28
+ mainAxisSize: MainAxisSize.min,
29
+ children: [
30
+ const SizedBox(height: 16),
31
+ ShadInputFormField(label: const Text('Name'), initialValue: 'Ale'),
32
+ const SizedBox(height: 8),
33
+ ShadInputFormField(label: const Text('Username'), initialValue: 'nank1ro'),
34
+ const SizedBox(height: 16),
35
+ ],
36
+ ),
37
+ ),
38
+ child: const Text('Account'),
39
+ ),
40
+ ShadTab(
41
+ value: 'password',
42
+ content: ShadCard(
43
+ title: const Text('Password'),
44
+ description: const Text(
45
+ "Change your password here. After saving, you'll be logged out."),
46
+ footer: const ShadButton(child: Text('Save password')),
47
+ child: Column(
48
+ children: [
49
+ const SizedBox(height: 16),
50
+ ShadInputFormField(label: const Text('Current password'), obscureText: true),
51
+ const SizedBox(height: 8),
52
+ ShadInputFormField(label: const Text('New password'), obscureText: true),
53
+ const SizedBox(height: 16),
54
+ ],
55
+ ),
56
+ ),
57
+ child: const Text('Password'),
58
+ ),
59
+ ],
60
+)
61
+```
62
+
63
+Use state or a controller to change `value` when the user switches tabs if needed.
.agent/skills/shadcn_ui-textarea/SKILL.md
new
+34
@@ -0,0 +1,34 @@
1
+---
2
+name: shadcn_ui-textarea
3
+description: Use ShadTextarea for multi-line text input; placeholder. ShadTextareaFormField for forms with validation. Use when adding multi-line text fields or bio/description inputs in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Textarea
7
+
8
+## Instructions
9
+
10
+`ShadTextarea` displays a multi-line form field or textarea-like component. Use `placeholder` and optional `initialValue`. For forms use `ShadTextareaFormField` with `id`, `label`, `placeholder`, `description`, and `validator`.
11
+
12
+### Standalone
13
+
14
+```dart
15
+const ShadTextarea(
16
+ placeholder: Text('Type your message here'),
17
+)
18
+```
19
+
20
+### Form field
21
+
22
+```dart
23
+ShadTextareaFormField(
24
+ id: 'bio',
25
+ label: const Text('Bio'),
26
+ placeholder: const Text('Tell us a little bit about yourself'),
27
+ description: const Text(
28
+ 'You can @mention other users and organizations.'),
29
+ validator: (v) {
30
+ if (v.length < 10) return 'Bio must be at least 10 characters.';
31
+ return null;
32
+ },
33
+)
34
+```
.agent/skills/shadcn_ui-theme/SKILL.md
new
+89
@@ -0,0 +1,89 @@
1
+---
2
+name: shadcn_ui-theme
3
+description: Configure Shadcn UI theme and color schemes (ShadThemeData, ShadColorScheme), override colors, use ShadColorScheme.fromName for theme switchers, and add custom colors. Use when theming a Flutter shadcn_ui app, changing color scheme, or adding custom theme colors.
4
+---
5
+
6
+# Shadcn UI — Theme Data
7
+
8
+## Instructions
9
+
10
+Theme and color scheme are defined by `ShadThemeData` and `ShadColorScheme`. Supported scheme names: blue, gray, green, neutral, orange, red, rose, slate, stone, violet, yellow, zinc.
11
+
12
+### Basic usage
13
+
14
+```dart
15
+import 'package:shadcn_ui/shadcn_ui.dart';
16
+
17
+return ShadApp(
18
+ darkTheme: ShadThemeData(
19
+ brightness: Brightness.dark,
20
+ colorScheme: const ShadSlateColorScheme.dark(),
21
+ ),
22
+ child: ...,
23
+);
24
+```
25
+
26
+### Override theme properties
27
+
28
+Override specific properties of the color scheme or component themes:
29
+
30
+```dart
31
+ShadApp(
32
+ darkTheme: ShadThemeData(
33
+ brightness: Brightness.dark,
34
+ colorScheme: const ShadSlateColorScheme.dark(
35
+ background: Colors.blue,
36
+ ),
37
+ primaryButtonTheme: const ShadButtonTheme(
38
+ backgroundColor: Colors.cyan,
39
+ ),
40
+ ),
41
+ ...
42
+);
43
+```
44
+
45
+For fully custom schemes, extend `ShadColorScheme` and pass all required properties.
46
+
47
+### Theme switcher with ShadColorScheme.fromName
48
+
49
+Use `ShadColorScheme.fromName` to let users change the color scheme:
50
+
51
+```dart
52
+final shadThemeColors = [
53
+ 'blue', 'gray', 'green', 'neutral', 'orange', 'red', 'rose',
54
+ 'slate', 'stone', 'violet', 'yellow', 'zinc',
55
+];
56
+
57
+final lightColorScheme = ShadColorScheme.fromName('blue');
58
+final darkColorScheme = ShadColorScheme.fromName('slate', brightness: Brightness.dark);
59
+```
60
+
61
+Use a `ShadSelect<String>` (or similar) with these names and rebuild the app with the chosen scheme via your state management.
62
+
63
+### Custom colors
64
+
65
+Add custom colors via the `custom` parameter on the color scheme:
66
+
67
+```dart
68
+return ShadApp(
69
+ theme: ShadThemeData(
70
+ colorScheme: const ShadZincColorScheme.light(
71
+ custom: {
72
+ 'myCustomColor': Color.fromARGB(255, 177, 4, 196),
73
+ },
74
+ ),
75
+ ),
76
+);
77
+```
78
+
79
+Access: `ShadTheme.of(context).colorScheme.custom['myCustomColor']!`
80
+
81
+Or add an extension:
82
+
83
+```dart
84
+extension CustomColorExtension on ShadColorScheme {
85
+ Color get myCustomColor => custom['myCustomColor']!;
86
+}
87
+```
88
+
89
+Then use: `ShadTheme.of(context).colorScheme.myCustomColor`
.agent/skills/shadcn_ui-time-picker/SKILL.md
new
+43
@@ -0,0 +1,43 @@
1
+---
2
+name: shadcn_ui-time-picker
3
+description: Use ShadTimePicker and ShadTimePickerFormField for time selection; trailing widget, period variant. Use when adding time pickers or time form fields in a Flutter shadcn_ui app or ShadForm.
4
+---
5
+
6
+# Shadcn UI — Time Picker
7
+
8
+## Instructions
9
+
10
+`ShadTimePicker` is a time picker component. Optional `trailing` widget (e.g. icon). For forms use `ShadTimePickerFormField` with `label`, `description`, `onChanged`, and `validator`. Use `ShadTimePickerFormField.period` for AM/PM period selection.
11
+
12
+### Standalone
13
+
14
+```dart
15
+const ShadTimePicker(
16
+ trailing: Padding(
17
+ padding: EdgeInsets.only(left: 8, top: 14),
18
+ child: Icon(LucideIcons.clock4),
19
+ ),
20
+)
21
+```
22
+
23
+### Form field
24
+
25
+```dart
26
+ShadTimePickerFormField(
27
+ label: const Text('Pick a time'),
28
+ onChanged: print,
29
+ description: const Text('The time of the day you want to pick'),
30
+ validator: (v) => v == null ? 'A time is required' : null,
31
+)
32
+```
33
+
34
+### Period (AM/PM)
35
+
36
+```dart
37
+ShadTimePickerFormField.period(
38
+ label: const Text('Pick a time'),
39
+ onChanged: print,
40
+ description: const Text('The time of the day you want to pick'),
41
+ validator: (v) => v == null ? 'A time is required' : null,
42
+)
43
+```
.agent/skills/shadcn_ui-toast/SKILL.md
new
+68
@@ -0,0 +1,68 @@
1
+---
2
+name: shadcn_ui-toast
3
+description: Show toasts with ShadToaster.of(context).show(ShadToast); title, description, action, ShadToast.destructive. Use when displaying temporary messages or feedback in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Toast
7
+
8
+## Instructions
9
+
10
+Toasts are succinct temporary messages. Ensure a toaster is in the tree (e.g. `ShadToaster`). Get it with `ShadToaster.of(context)`. Call `toaster.show(ShadToast(...))` to show a toast; use `toaster.hide()` to dismiss (e.g. from an action button). Use `ShadToast` with optional `title`, `description`, and `action`. Use `ShadToast.destructive` for error-style toasts with a destructive action button (use `ShadDecoration` to style the action border).
11
+
12
+### Simple
13
+
14
+```dart
15
+ShadToaster.of(context).show(
16
+ const ShadToast(
17
+ description: Text('Your message has been sent.'),
18
+ ),
19
+);
20
+```
21
+
22
+### With title
23
+
24
+```dart
25
+ShadToaster.of(context).show(
26
+ const ShadToast(
27
+ title: Text('Uh oh! Something went wrong'),
28
+ description: Text('There was a problem with your request'),
29
+ ),
30
+);
31
+```
32
+
33
+### With action
34
+
35
+```dart
36
+ShadToaster.of(context).show(
37
+ ShadToast(
38
+ title: const Text('Uh oh! Something went wrong'),
39
+ description: const Text('There was a problem with your request'),
40
+ action: ShadButton.outline(
41
+ child: const Text('Try again'),
42
+ onPressed: () => ShadToaster.of(context).hide(),
43
+ ),
44
+ ),
45
+);
46
+```
47
+
48
+### Destructive
49
+
50
+```dart
51
+final theme = ShadTheme.of(context);
52
+ShadToaster.of(context).show(
53
+ ShadToast.destructive(
54
+ title: const Text('Uh oh! Something went wrong'),
55
+ description: const Text('There was a problem with your request'),
56
+ action: ShadButton.destructive(
57
+ child: const Text('Try again'),
58
+ decoration: ShadDecoration(
59
+ border: ShadBorder.all(
60
+ color: theme.colorScheme.destructiveForeground,
61
+ width: 1,
62
+ ),
63
+ ),
64
+ onPressed: () => ShadToaster.of(context).hide(),
65
+ ),
66
+ ),
67
+);
68
+```
.agent/skills/shadcn_ui-tooltip/SKILL.md
new
+33
@@ -0,0 +1,33 @@
1
+---
2
+name: shadcn_ui-tooltip
3
+description: Show tooltips on hover or focus with ShadTooltip; builder returns tooltip content, child is trigger. Use when adding hover/focus tooltips in a Flutter shadcn_ui app. Child must use ShadGestureDetector for hover (e.g. ShadButton).
4
+---
5
+
6
+# Shadcn UI — Tooltip
7
+
8
+## Instructions
9
+
10
+`ShadTooltip` shows a popup when the element receives keyboard focus or the mouse hovers over it. Use `builder: (context) => ...` to build the tooltip content (e.g. `Text('Add to library')`) and `child` for the trigger widget. Hover works only if the child uses a `ShadGestureDetector`; `ShadButton` and similar components implement it. For a plain widget (e.g. image), wrap the child with `ShadGestureDetector` so the tooltip shows on hover.
11
+
12
+### Basic
13
+
14
+```dart
15
+ShadTooltip(
16
+ builder: (context) => const Text('Add to library'),
17
+ child: ShadButton.outline(
18
+ child: const Text('Hover/Focus'),
19
+ onPressed: () {},
20
+ ),
21
+)
22
+```
23
+
24
+For a non-interactive child (e.g. `Image`), wrap with `ShadGestureDetector`:
25
+
26
+```dart
27
+ShadTooltip(
28
+ builder: (context) => const Text('Description'),
29
+ child: ShadGestureDetector(
30
+ child: Image.network('...'),
31
+ ),
32
+)
33
+```
.agent/skills/shadcn_ui-typography/SKILL.md
new
+110
@@ -0,0 +1,110 @@
1
+---
2
+name: shadcn_ui-typography
3
+description: Apply Shadcn UI text styles (h1, h2, p, lead, muted, etc.), set custom font family or Google Fonts, and extend ShadTextTheme with custom styles. Use when styling text, headings, or paragraphs in a Flutter shadcn_ui app or when changing the default font.
4
+---
5
+
6
+# Shadcn UI — Typography
7
+
8
+## Instructions
9
+
10
+Use `ShadTheme.of(context).textTheme` for consistent text styles. Default font family is Geist.
11
+
12
+### Text style names
13
+
14
+- **h1Large**, **h1**, **h2**, **h3**, **h4** — Headings
15
+- **p** — Body paragraph
16
+- **blockquote** — Quote style
17
+- **table** — Table header/cell text
18
+- **list** — List item text
19
+- **lead** — Lead/subtitle
20
+- **large** — Large body
21
+- **small** — Small/caption
22
+- **muted** — Muted/secondary text
23
+
24
+### Examples
25
+
26
+```dart
27
+Text(
28
+ 'Taxing Laughter: The Joke Tax Chronicles',
29
+ style: ShadTheme.of(context).textTheme.h1,
30
+)
31
+
32
+Text(
33
+ 'The king, seeing how much happier his subjects were...',
34
+ style: ShadTheme.of(context).textTheme.p,
35
+)
36
+
37
+Text(
38
+ 'Email address',
39
+ style: ShadTheme.of(context).textTheme.small,
40
+)
41
+
42
+Text(
43
+ 'Enter your email address.',
44
+ style: ShadTheme.of(context).textTheme.muted,
45
+)
46
+```
47
+
48
+### Custom font family
49
+
50
+1. Add font assets (e.g. under `/fonts`) and register in `pubspec.yaml`:
51
+
52
+```yaml
53
+flutter:
54
+ fonts:
55
+ - family: UbuntuMono
56
+ fonts:
57
+ - asset: fonts/UbuntuMono-Regular.ttf
58
+ - asset: fonts/UbuntuMono-Italic.ttf
59
+ style: italic
60
+ - asset: fonts/UbuntuMono-Bold.ttf
61
+ weight: 700
62
+```
63
+
64
+2. Set `ShadTextTheme` in `ShadThemeData`:
65
+
66
+```dart
67
+return ShadApp(
68
+ theme: ShadThemeData(
69
+ brightness: Brightness.light,
70
+ colorScheme: const ShadZincColorScheme.light(),
71
+ textTheme: ShadTextTheme(
72
+ colorScheme: const ShadZincColorScheme.light(),
73
+ family: 'UbuntuMono',
74
+ ),
75
+ ),
76
+ ...
77
+);
78
+```
79
+
80
+### Google Fonts
81
+
82
+Install `google_fonts`, then:
83
+
84
+```dart
85
+theme: ShadThemeData(
86
+ brightness: Brightness.light,
87
+ colorScheme: const ShadZincColorScheme.light(),
88
+ textTheme: ShadTextTheme.fromGoogleFont(GoogleFonts.poppins),
89
+),
90
+```
91
+
92
+### Custom text styles
93
+
94
+Extend `ShadTextTheme` with the `custom` parameter:
95
+
96
+```dart
97
+textTheme: ShadTextTheme(
98
+ custom: {
99
+ 'myCustomStyle': const TextStyle(
100
+ fontSize: 16,
101
+ fontWeight: FontWeight.w400,
102
+ color: Colors.blue,
103
+ ),
104
+ },
105
+),
106
+```
107
+
108
+Access: `ShadTheme.of(context).textTheme.custom['myCustomStyle']!`
109
+
110
+Or define an extension on `ShadTextTheme` and use `ShadTheme.of(context).textTheme.myCustomStyle`.
.claude/skills/flutter-adding-home-screen-widgets/SKILL.md
new
+202
@@ -0,0 +1,202 @@
1
+---
2
+name: "flutter-adding-home-screen-widgets"
3
+description: "Adds home screen widgets to a Flutter app for Android and iOS. Use when providing glanceable app information or quick actions on the device home screen."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:23:50 GMT"
7
+
8
+---
9
+# Implementing Flutter Home Screen Widgets
10
+
11
+## Contents
12
+- [Architecture & Data Flow](#architecture--data-flow)
13
+- [Flutter Integration Workflow](#flutter-integration-workflow)
14
+- [iOS Implementation Workflow](#ios-implementation-workflow)
15
+- [Android Implementation Workflow](#android-implementation-workflow)
16
+- [Advanced Techniques](#advanced-techniques)
17
+- [Examples](#examples)
18
+
19
+## Architecture & Data Flow
20
+Home Screen Widgets require native UI implementation (SwiftUI for iOS, XML/Kotlin for Android). The Flutter app communicates with these native widgets via shared local storage (`UserDefaults` on iOS, `SharedPreferences` on Android) using the `home_widget` package.
21
+
22
+- **Data Write:** Flutter app writes key-value pairs or renders images to a shared container.
23
+- **Trigger:** Flutter app signals the native OS to update the widget.
24
+- **Data Read:** Native widget wakes up, reads the key-value pairs or images from the shared container, and updates its UI.
25
+
26
+## Flutter Integration Workflow
27
+
28
+Use this checklist to implement the Dart side of the Home Screen Widget integration.
29
+
30
+- [ ] **Step 1: Initialize the App Group.** Call `HomeWidget.setAppGroupId('<YOUR_APP_GROUP>')` in `initState()` or app startup.
31
+- [ ] **Step 2: Save Data.** Use `HomeWidget.saveWidgetData<T>('key', value)` to write data to shared storage.
32
+- [ ] **Step 3: Trigger Update.** Call `HomeWidget.updateWidget(iOSName: 'YourIOSWidget', androidName: 'YourAndroidWidget')` to notify the OS.
33
+- [ ] **Step 4: Validate.** Run Flutter build -> review console for missing plugin registrations -> fix.
34
+
35
+## iOS Implementation Workflow
36
+
37
+If targeting iOS, implement the widget using Xcode and SwiftUI.
38
+
39
+- [ ] **Step 1: Create Target.** Open `ios/Runner.xcworkspace` in Xcode. Add a new **Widget Extension** target. Disable "Include Live Activity" and "Include Configuration Intent" unless explicitly required.
40
+- [ ] **Step 2: Configure App Groups.** Add the **App Groups** capability to *both* the Runner target and the Widget Extension target. Ensure the App Group ID matches the one used in Dart.
41
+- [ ] **Step 3: Define TimelineEntry.** Create a struct conforming to `TimelineEntry` to hold the data passed from shared storage.
42
+- [ ] **Step 4: Implement TimelineProvider.**
43
+ - In `getSnapshot` and `getTimeline`, instantiate `UserDefaults(suiteName: "<YOUR_APP_GROUP>")`.
44
+ - Extract values using `userDefaults?.string(forKey: "your_key")`.
45
+ - Return the populated `TimelineEntry`.
46
+- [ ] **Step 5: Build UI.** Implement the SwiftUI `View` to display the data from the `TimelineEntry`.
47
+- [ ] **Step 6: Validate.** Run Xcode build for the Widget Extension -> review provisioning/App Group errors -> fix.
48
+
49
+## Android Implementation Workflow
50
+
51
+If targeting Android, implement the widget using Android Studio and XML/Kotlin.
52
+
53
+- [ ] **Step 1: Create App Widget.** Open the `android` folder in Android Studio. Right-click the app directory -> **New -> Widget -> App Widget**.
54
+- [ ] **Step 2: Define Layout.** Edit `res/layout/<widget_name>.xml` to define the UI using standard Android XML layouts (e.g., `RelativeLayout`, `TextView`, `ImageView`).
55
+- [ ] **Step 3: Implement AppWidgetProvider.**
56
+ - Open the generated Kotlin class extending `AppWidgetProvider`.
57
+ - In the `onUpdate` method, retrieve shared data using `HomeWidgetPlugin.getData(context)`.
58
+ - Extract values using `widgetData.getString("your_key", null)`.
59
+ - Update the UI using `RemoteViews` and `setTextViewText` or `setImageViewBitmap`.
60
+ - Call `appWidgetManager.updateAppWidget(appWidgetId, views)`.
61
+- [ ] **Step 4: Validate.** Run Android build -> review Manifest registration errors -> fix.
62
+
63
+## Advanced Techniques
64
+
65
+### Rendering Flutter Widgets as Images
66
+If the UI is too complex to recreate natively (e.g., custom charts), render the Flutter widget to an image and display the image in the native widget.
67
+
68
+1. Wrap the target Flutter widget with a `GlobalKey`.
69
+2. Call `HomeWidget.renderFlutterWidget()`, passing the widget, a filename, and the key.
70
+3. **iOS:** Read the file path from `UserDefaults` and render using `UIImage(contentsOfFile:)` inside a SwiftUI `Image`.
71
+4. **Android:** Read the file path from `SharedPreferences`, decode using `BitmapFactory.decodeFile()`, and render using `setImageViewBitmap()`.
72
+
73
+### Using Custom Flutter Fonts (iOS Only)
74
+If utilizing custom fonts defined in Flutter on iOS Home Screen Widgets:
75
+
76
+1. Extract the Flutter asset bundle path in Swift.
77
+2. Register the font using `CTFontManagerRegisterFontsForURL`.
78
+3. Apply the font in SwiftUI using `Font.custom()`.
79
+
80
+## Examples
81
+
82
+### Example: Flutter Data Update
83
+```dart
84
+import 'package:home_widget/home_widget.dart';
85
+
86
+const String appGroupId = 'group.com.example.app';
87
+const String iOSWidgetName = 'NewsWidgets';
88
+const String androidWidgetName = 'NewsWidget';
89
+
90
+Future<void> updateWidgetData(String title, String description) async {
91
+ await HomeWidget.setAppGroupId(appGroupId);
92
+ await HomeWidget.saveWidgetData<String>('headline_title', title);
93
+ await HomeWidget.saveWidgetData<String>('headline_description', description);
94
+ await HomeWidget.updateWidget(
95
+ iOSName: iOSWidgetName,
96
+ androidName: androidWidgetName,
97
+ );
98
+}
99
+```
100
+
101
+### Example: iOS SwiftUI Provider & View
102
+```swift
103
+import WidgetKit
104
+import SwiftUI
105
+
106
+struct NewsArticleEntry: TimelineEntry {
107
+ let date: Date
108
+ let title: String
109
+ let description: String
110
+}
111
+
112
+struct Provider: TimelineProvider {
113
+ func placeholder(in context: Context) -> NewsArticleEntry {
114
+ NewsArticleEntry(date: Date(), title: "Loading...", description: "Loading...")
115
+ }
116
+
117
+ func getSnapshot(in context: Context, completion: @escaping (NewsArticleEntry) -> ()) {
118
+ let userDefaults = UserDefaults(suiteName: "group.com.example.app")
119
+ let title = userDefaults?.string(forKey: "headline_title") ?? "No Title"
120
+ let description = userDefaults?.string(forKey: "headline_description") ?? "No Description"
121
+
122
+ let entry = NewsArticleEntry(date: Date(), title: title, description: description)
123
+ completion(entry)
124
+ }
125
+
126
+ func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
127
+ getSnapshot(in: context) { (entry) in
128
+ let timeline = Timeline(entries: [entry], policy: .atEnd)
129
+ completion(timeline)
130
+ }
131
+ }
132
+}
133
+
134
+struct NewsWidgetsEntryView : View {
135
+ var entry: Provider.Entry
136
+
137
+ var body: some View {
138
+ VStack(alignment: .leading) {
139
+ Text(entry.title).font(.headline)
140
+ Text(entry.description).font(.subheadline)
141
+ }
142
+ }
143
+}
144
+```
145
+
146
+### Example: Android Kotlin Provider
147
+```kotlin
148
+package com.example.app.widgets
149
+
150
+import android.appwidget.AppWidgetManager
151
+import android.appwidget.AppWidgetProvider
152
+import android.content.Context
153
+import android.widget.RemoteViews
154
+import es.antonborri.home_widget.HomeWidgetPlugin
155
+import com.example.app.R
156
+
157
+class NewsWidget : AppWidgetProvider() {
158
+ override fun onUpdate(
159
+ context: Context,
160
+ appWidgetManager: AppWidgetManager,
161
+ appWidgetIds: IntArray,
162
+ ) {
163
+ for (appWidgetId in appWidgetIds) {
164
+ val widgetData = HomeWidgetPlugin.getData(context)
165
+ val views = RemoteViews(context.packageName, R.layout.news_widget).apply {
166
+ val title = widgetData.getString("headline_title", "No Title")
167
+ setTextViewText(R.id.headline_title, title)
168
+
169
+ val description = widgetData.getString("headline_description", "No Description")
170
+ setTextViewText(R.id.headline_description, description)
171
+ }
172
+ appWidgetManager.updateAppWidget(appWidgetId, views)
173
+ }
174
+ }
175
+}
176
+```
177
+
178
+<details>
179
+<summary>Example: iOS Custom Font Registration Helper</summary>
180
+
181
+```swift
182
+// Add this to your SwiftUI View struct
183
+var bundle: URL {
184
+ let bundle = Bundle.main
185
+ if bundle.bundleURL.pathExtension == "appex" {
186
+ var url = bundle.bundleURL.deletingLastPathComponent().deletingLastPathComponent()
187
+ url.append(component: "Frameworks/App.framework/flutter_assets")
188
+ return url
189
+ }
190
+ return bundle.bundleURL
191
+}
192
+
193
+init(entry: Provider.Entry) {
194
+ self.entry = entry
195
+ CTFontManagerRegisterFontsForURL(
196
+ bundle.appending(path: "/fonts/YourCustomFont.ttf") as CFURL,
197
+ CTFontManagerScope.process,
198
+ nil
199
+ )
200
+}
201
+```
202
+</details>
.claude/skills/flutter-animating-apps/SKILL.md
new
+182
@@ -0,0 +1,182 @@
1
+---
2
+name: "flutter-animating-apps"
3
+description: "Implements animated effects, transitions, and motion in a Flutter app. Use when adding visual feedback, shared element transitions, or physics-based animations."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:16:34 GMT"
7
+
8
+---
9
+# Implementing Flutter Animations
10
+
11
+## Contents
12
+- [Core Concepts](#core-concepts)
13
+- [Animation Strategies](#animation-strategies)
14
+- [Workflows](#workflows)
15
+ - [Implementing Implicit Animations](#implementing-implicit-animations)
16
+ - [Implementing Explicit Animations](#implementing-explicit-animations)
17
+ - [Implementing Hero Transitions](#implementing-hero-transitions)
18
+ - [Implementing Physics-Based Animations](#implementing-physics-based-animations)
19
+- [Examples](#examples)
20
+
21
+## Core Concepts
22
+
23
+Manage Flutter animations using the core typed `Animation` system. Do not manually calculate frames; rely on the framework's ticker and interpolation classes.
24
+
25
+* **`Animation<T>`**: Treat this as an abstract representation of a value that changes over time. It holds state (completed, dismissed) and notifies listeners, but knows nothing about the UI.
26
+* **`AnimationController`**: Instantiate this to drive the animation. It generates values (typically 0.0 to 1.0) tied to the screen refresh rate. Always provide a `vsync` (usually via `SingleTickerProviderStateMixin`) to prevent offscreen resource consumption. Always `dispose()` controllers to prevent memory leaks.
27
+* **`Tween<T>`**: Define a stateless mapping from an input range (usually 0.0-1.0) to an output type (e.g., `Color`, `Offset`, `double`). Chain tweens with curves using `.animate()`.
28
+* **`Curve`**: Apply non-linear timing (e.g., `Curves.easeIn`, `Curves.bounceOut`) to an animation using a `CurvedAnimation` or `CurveTween`.
29
+
30
+## Animation Strategies
31
+
32
+Apply conditional logic to select the correct animation approach:
33
+
34
+* **If animating simple property changes (size, color, opacity) without playback control:** Use **Implicit Animations** (e.g., `AnimatedContainer`, `AnimatedOpacity`, `TweenAnimationBuilder`).
35
+* **If requiring playback control (play, pause, reverse, loop) or coordinating multiple properties:** Use **Explicit Animations** (e.g., `AnimationController` with `AnimatedBuilder` or `AnimatedWidget`).
36
+* **If animating elements between two distinct routes:** Use **Hero Animations** (Shared Element Transitions).
37
+* **If modeling real-world motion (e.g., snapping back after a drag):** Use **Physics-Based Animations** (e.g., `SpringSimulation`).
38
+* **If animating a sequence of overlapping or delayed motions:** Use **Staggered Animations** (multiple `Tween`s driven by a single `AnimationController` using `Interval` curves).
39
+
40
+## Workflows
41
+
42
+### Implementing Implicit Animations
43
+
44
+Use this workflow for "fire-and-forget" state-driven animations.
45
+
46
+- [ ] **Task Progress:**
47
+ - [ ] Identify the target properties to animate (e.g., width, color).
48
+ - [ ] Replace the static widget (e.g., `Container`) with its animated counterpart (e.g., `AnimatedContainer`).
49
+ - [ ] Define the `duration` property.
50
+ - [ ] (Optional) Define the `curve` property for non-linear motion.
51
+ - [ ] Trigger the animation by updating the properties inside a `setState()` call.
52
+ - [ ] Run validator -> review UI for jank -> adjust duration/curve if necessary.
53
+
54
+### Implementing Explicit Animations
55
+
56
+Use this workflow when you need granular control over the animation lifecycle.
57
+
58
+- [ ] **Task Progress:**
59
+ - [ ] Add `SingleTickerProviderStateMixin` (or `TickerProviderStateMixin` for multiple controllers) to the `State` class.
60
+ - [ ] Initialize an `AnimationController` in `initState()`, providing `vsync: this` and a `duration`.
61
+ - [ ] Define a `Tween` and chain it to the controller using `.animate()`.
62
+ - [ ] Wrap the target UI in an `AnimatedBuilder` (preferred for complex trees) or subclass `AnimatedWidget`.
63
+ - [ ] Pass the `Animation` object to the `AnimatedBuilder`'s `animation` property.
64
+ - [ ] Control playback using `controller.forward()`, `controller.reverse()`, or `controller.repeat()`.
65
+ - [ ] Call `controller.dispose()` in the `dispose()` method.
66
+ - [ ] Run validator -> check for memory leaks -> ensure `dispose()` is called.
67
+
68
+### Implementing Hero Transitions
69
+
70
+Use this workflow to fly a widget between two routes.
71
+
72
+- [ ] **Task Progress:**
73
+ - [ ] Wrap the source widget in a `Hero` widget.
74
+ - [ ] Assign a unique, data-driven `tag` to the source `Hero`.
75
+ - [ ] Wrap the destination widget in a `Hero` widget.
76
+ - [ ] Assign the *exact same* `tag` to the destination `Hero`.
77
+ - [ ] Ensure the widget trees inside both `Hero` widgets are visually similar to prevent jarring jumps.
78
+ - [ ] Trigger the transition by pushing the destination route via `Navigator`.
79
+
80
+### Implementing Physics-Based Animations
81
+
82
+Use this workflow for gesture-driven, natural motion.
83
+
84
+- [ ] **Task Progress:**
85
+ - [ ] Set up an `AnimationController` (do not set a fixed duration).
86
+ - [ ] Capture gesture velocity using a `GestureDetector` (e.g., `onPanEnd` providing `DragEndDetails`).
87
+ - [ ] Convert the pixel velocity to the coordinate space of the animating property.
88
+ - [ ] Instantiate a `SpringSimulation` with mass, stiffness, damping, and the calculated velocity.
89
+ - [ ] Drive the controller using `controller.animateWith(simulation)`.
90
+
91
+## Examples
92
+
93
+<details>
94
+<summary><b>Example: Explicit Animation (Staggered with AnimatedBuilder)</b></summary>
95
+
96
+```dart
97
+class StaggeredAnimationDemo extends StatefulWidget {
98
+ @override
99
+ State<StaggeredAnimationDemo> createState() => _StaggeredAnimationDemoState();
100
+}
101
+
102
+class _StaggeredAnimationDemoState extends State<StaggeredAnimationDemo> with SingleTickerProviderStateMixin {
103
+ late AnimationController _controller;
104
+ late Animation<double> _widthAnimation;
105
+ late Animation<Color?> _colorAnimation;
106
+
107
+ @override
108
+ void initState() {
109
+ super.initState();
110
+ _controller = AnimationController(
111
+ duration: const Duration(seconds: 2),
112
+ vsync: this,
113
+ );
114
+
115
+ // Staggered width animation (0.0 to 0.5 interval)
116
+ _widthAnimation = Tween<double>(begin: 50.0, end: 200.0).animate(
117
+ CurvedAnimation(
118
+ parent: _controller,
119
+ curve: const Interval(0.0, 0.5, curve: Curves.easeIn),
120
+ ),
121
+ );
122
+
123
+ // Staggered color animation (0.5 to 1.0 interval)
124
+ _colorAnimation = ColorTween(begin: Colors.blue, end: Colors.red).animate(
125
+ CurvedAnimation(
126
+ parent: _controller,
127
+ curve: const Interval(0.5, 1.0, curve: Curves.easeOut),
128
+ ),
129
+ );
130
+
131
+ _controller.forward();
132
+ }
133
+
134
+ @override
135
+ void dispose() {
136
+ _controller.dispose(); // CRITICAL: Prevent memory leaks
137
+ super.dispose();
138
+ }
139
+
140
+ @override
141
+ Widget build(BuildContext context) {
142
+ return AnimatedBuilder(
143
+ animation: _controller,
144
+ builder: (context, child) {
145
+ return Container(
146
+ width: _widthAnimation.value,
147
+ height: 50.0,
148
+ color: _colorAnimation.value,
149
+ );
150
+ },
151
+ );
152
+ }
153
+}
154
+```
155
+</details>
156
+
157
+<details>
158
+<summary><b>Example: Custom Page Route Transition</b></summary>
159
+
160
+```dart
161
+Route createCustomRoute(Widget destination) {
162
+ return PageRouteBuilder(
163
+ pageBuilder: (context, animation, secondaryAnimation) => destination,
164
+ transitionsBuilder: (context, animation, secondaryAnimation, child) {
165
+ const begin = Offset(0.0, 1.0); // Start from bottom
166
+ const end = Offset.zero;
167
+ const curve = Curves.easeOut;
168
+
169
+ final tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
170
+ final offsetAnimation = animation.drive(tween);
171
+
172
+ return SlideTransition(
173
+ position: offsetAnimation,
174
+ child: child,
175
+ );
176
+ },
177
+ );
178
+}
179
+
180
+// Usage: Navigator.of(context).push(createCustomRoute(const NextPage()));
181
+```
182
+</details>
.claude/skills/flutter-architecting-apps/SKILL.md
new
+163
@@ -0,0 +1,163 @@
1
+---
2
+name: "flutter-architecting-apps"
3
+description: "Architects a Flutter application using the recommended layered approach (UI, Logic, Data). Use when structuring a new project or refactoring for scalability."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:13:42 GMT"
7
+
8
+---
9
+# Architecting Flutter Applications
10
+
11
+## Contents
12
+- [Core Architectural Principles](#core-architectural-principles)
13
+- [Structuring the Layers](#structuring-the-layers)
14
+- [Implementing the Data Layer](#implementing-the-data-layer)
15
+- [Feature Implementation Workflow](#feature-implementation-workflow)
16
+- [Examples](#examples)
17
+
18
+## Core Architectural Principles
19
+
20
+Design Flutter applications to scale by strictly adhering to the following principles:
21
+
22
+* **Enforce Separation of Concerns:** Decouple UI rendering from business logic and data fetching. Organize the codebase into distinct layers (UI, Logic, Data) and further separate by feature within those layers.
23
+* **Maintain a Single Source of Truth (SSOT):** Centralize application state and data in the Data layer. Ensure the SSOT is the only component authorized to mutate its respective data.
24
+* **Implement Unidirectional Data Flow (UDF):** Flow state downwards from the Data layer to the UI layer. Flow events upwards from the UI layer to the Data layer.
25
+* **Treat UI as a Function of State:** Drive the UI entirely via immutable state objects. Rebuild widgets reactively when the underlying state changes.
26
+
27
+## Structuring the Layers
28
+
29
+Separate the application into 2 to 3 distinct layers depending on complexity. Restrict communication so that a layer only interacts with the layer directly adjacent to it.
30
+
31
+### 1. UI Layer (Presentation)
32
+* **Views (Widgets):** Build reusable, lean widgets. Strip all business and data-fetching logic from the widget tree. Restrict widget logic to UI-specific concerns (e.g., animations, routing, layout constraints).
33
+* **ViewModels:** Manage the UI state. Consume domain models from the Data/Logic layers and transform them into presentation-friendly formats. Expose state to the Views and handle user interaction events.
34
+
35
+### 2. Logic Layer (Domain) - *Conditional*
36
+* **If the application requires complex client-side business logic:** Implement a Logic layer containing Use Cases or Interactors. Use this layer to orchestrate interactions between multiple repositories before passing data to the UI layer.
37
+* **If the application is a standard CRUD app:** Omit this layer. Allow ViewModels to interact directly with Repositories.
38
+
39
+### 3. Data Layer (Model)
40
+* **Responsibilities:** Act as the SSOT for all application data. Handle business data, external API consumption, event processing, and data synchronization.
41
+* **Components:** Divide the Data layer strictly into **Repositories** and **Services**.
42
+
43
+## Implementing the Data Layer
44
+
45
+### Services
46
+* **Role:** Wrap external APIs (HTTP servers, local databases, platform plugins).
47
+* **Implementation:** Write Services as stateless Dart classes. Do not store application state here.
48
+* **Mapping:** Create exactly one Service class per external data source.
49
+
50
+### Repositories
51
+* **Role:** Act as the SSOT for domain data.
52
+* **Implementation:** Consume raw data from Services. Handle caching, offline synchronization, and retry logic.
53
+* **Transformation:** Transform raw API/Service data into clean Domain Models formatted for consumption by ViewModels.
54
+
55
+## Feature Implementation Workflow
56
+
57
+Follow this sequential workflow when adding a new feature to the application.
58
+
59
+**Task Progress:**
60
+- [ ] **Step 1: Define Domain Models.** Create immutable Dart classes representing the core data structures required by the feature.
61
+- [ ] **Step 2: Implement Services.** Create stateless Service classes to handle raw data fetching (e.g., HTTP GET/POST).
62
+- [ ] **Step 3: Implement Repositories.** Create Repository classes that consume the Services, handle caching, and return Domain Models.
63
+- [ ] **Step 4: Implement ViewModels.** Create ViewModels that consume the Repositories. Expose immutable state and define methods (commands) for user actions.
64
+- [ ] **Step 5: Implement Views.** Create Flutter Widgets that bind to the ViewModel state and trigger ViewModel methods on user interaction.
65
+- [ ] **Step 6: Run Validator.** Execute unit tests for Services, Repositories, and ViewModels. Execute widget tests for Views.
66
+ * *Feedback Loop:* Review test failures -> Fix logic/mocking errors -> Re-run tests until passing.
67
+
68
+## Examples
69
+
70
+### Data Layer: Service and Repository
71
+
72
+```dart
73
+// 1. Service (Stateless API Wrapper)
74
+class UserApiService {
75
+ final HttpClient _client;
76
+
77
+ UserApiService(this._client);
78
+
79
+ Future<Map<String, dynamic>> fetchUserRaw(String userId) async {
80
+ final response = await _client.get('/users/$userId');
81
+ return response.data;
82
+ }
83
+}
84
+
85
+// 2. Domain Model (Immutable)
86
+class User {
87
+ final String id;
88
+ final String name;
89
+
90
+ const User({required this.id, required this.name});
91
+}
92
+
93
+// 3. Repository (SSOT & Data Transformer)
94
+class UserRepository {
95
+ final UserApiService _apiService;
96
+ User? _cachedUser;
97
+
98
+ UserRepository(this._apiService);
99
+
100
+ Future<User> getUser(String userId) async {
101
+ if (_cachedUser != null && _cachedUser!.id == userId) {
102
+ return _cachedUser!;
103
+ }
104
+
105
+ final rawData = await _apiService.fetchUserRaw(userId);
106
+ final user = User(id: rawData['id'], name: rawData['name']);
107
+
108
+ _cachedUser = user; // Cache data
109
+ return user;
110
+ }
111
+}
112
+```
113
+
114
+### UI Layer: ViewModel and View
115
+
116
+```dart
117
+// 4. ViewModel (State Management)
118
+class UserViewModel extends ChangeNotifier {
119
+ final UserRepository _userRepository;
120
+
121
+ User? user;
122
+ bool isLoading = false;
123
+ String? error;
124
+
125
+ UserViewModel(this._userRepository);
126
+
127
+ Future<void> loadUser(String userId) async {
128
+ isLoading = true;
129
+ error = null;
130
+ notifyListeners();
131
+
132
+ try {
133
+ user = await _userRepository.getUser(userId);
134
+ } catch (e) {
135
+ error = e.toString();
136
+ } finally {
137
+ isLoading = false;
138
+ notifyListeners();
139
+ }
140
+ }
141
+}
142
+
143
+// 5. View (Lean UI)
144
+class UserProfileView extends StatelessWidget {
145
+ final UserViewModel viewModel;
146
+
147
+ const UserProfileView({Key? key, required this.viewModel}) : super(key: key);
148
+
149
+ @override
150
+ Widget build(BuildContext context) {
151
+ return ListenableBuilder(
152
+ listenable: viewModel,
153
+ builder: (context, child) {
154
+ if (viewModel.isLoading) return const CircularProgressIndicator();
155
+ if (viewModel.error != null) return Text('Error: ${viewModel.error}');
156
+ if (viewModel.user == null) return const Text('No user data.');
157
+
158
+ return Text('Hello, ${viewModel.user!.name}');
159
+ },
160
+ );
161
+ }
162
+}
163
+```
.claude/skills/flutter-building-forms/SKILL.md
new
+122
@@ -0,0 +1,122 @@
1
+---
2
+name: "flutter-building-forms"
3
+description: "Builds Flutter forms with validation and user input handling. Use when creating login screens, data entry forms, or any multi-field user input."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:15:24 GMT"
7
+
8
+---
9
+# Building Validated Forms
10
+
11
+## Contents
12
+- [Form Architecture](#form-architecture)
13
+- [Field Validation](#field-validation)
14
+- [Workflow: Implementing a Validated Form](#workflow-implementing-a-validated-form)
15
+- [Examples](#examples)
16
+
17
+## Form Architecture
18
+
19
+Implement forms using a `Form` widget to group and validate multiple input fields together.
20
+
21
+- **Use a StatefulWidget:** Always host your `Form` inside a `StatefulWidget`.
22
+- **Persist the GlobalKey:** Instantiate a `GlobalKey<FormState>` exactly once as a final variable within the `State` class. Do not generate a new `GlobalKey` inside the `build` method; doing so is resource-expensive and destroys the form's state on every rebuild.
23
+- **Bind the Key:** Pass the `GlobalKey<FormState>` to the `key` property of the `Form` widget. This uniquely identifies the form and provides access to the `FormState` for validation and submission.
24
+- **Alternative Access:** If dealing with highly complex widget trees where passing the key is impractical, use `Form.of(context)` to access the `FormState` from a descendant widget.
25
+
26
+## Field Validation
27
+
28
+Use `TextFormField` to render Material Design text inputs with built-in validation support. `TextFormField` is a convenience widget that automatically wraps a standard `TextField` inside a `FormField`.
29
+
30
+- **Implement the Validator:** Provide a `validator()` callback function to each `TextFormField`.
31
+- **Return Error Messages:** If the user's input is invalid, return a `String` containing the specific error message. The `Form` will automatically rebuild to display this text below the field.
32
+- **Return Null for Success:** If the input passes validation, you must return `null`.
33
+
34
+## Workflow: Implementing a Validated Form
35
+
36
+Follow this sequential workflow to implement and validate a form. Copy the checklist to track your progress.
37
+
38
+**Task Progress:**
39
+- [ ] 1. Create a `StatefulWidget` and its corresponding `State` class.
40
+- [ ] 2. Instantiate `final _formKey = GlobalKey<FormState>();` in the `State` class.
41
+- [ ] 3. Return a `Form` widget in the `build` method and assign `key: _formKey`.
42
+- [ ] 4. Add `TextFormField` widgets as descendants of the `Form`.
43
+- [ ] 5. Write a `validator` function for each `TextFormField` (return `String` on error, `null` on success).
44
+- [ ] 6. Add a submit button (e.g., `ElevatedButton`).
45
+- [ ] 7. Implement the validation check in the button's `onPressed` callback using `_formKey.currentState!.validate()`.
46
+
47
+### Validation Decision Logic
48
+
49
+When the user triggers the submit action, execute the following conditional logic:
50
+
51
+1. Call `_formKey.currentState!.validate()`.
52
+2. **If `true` (Valid):** All validators returned `null`. Proceed with form submission (e.g., save data, make API call) and display a success indicator (e.g., a `SnackBar`).
53
+3. **If `false` (Invalid):** One or more validators returned an error string. The `FormState` automatically rebuilds the UI to display the error messages.
54
+4. **Feedback Loop:** Run validator -> review errors -> fix. The user must adjust their input and resubmit until `validate()` returns `true`.
55
+
56
+## Examples
57
+
58
+### Complete Validated Form Implementation
59
+
60
+Use the following pattern to implement a robust, validated form.
61
+
62
+```dart
63
+import 'package:flutter/material.dart';
64
+
65
+class UserRegistrationForm extends StatefulWidget {
66
+ const UserRegistrationForm({super.key});
67
+
68
+ @override
69
+ State<UserRegistrationForm> createState() => _UserRegistrationFormState();
70
+}
71
+
72
+class _UserRegistrationFormState extends State<UserRegistrationForm> {
73
+ // 1. Persist the GlobalKey in the State class
74
+ final _formKey = GlobalKey<FormState>();
75
+
76
+ @override
77
+ Widget build(BuildContext context) {
78
+ // 2. Bind the key to the Form
79
+ return Form(
80
+ key: _formKey,
81
+ child: Column(
82
+ crossAxisAlignment: CrossAxisAlignment.start,
83
+ children: [
84
+ // 3. Add TextFormFields with validators
85
+ TextFormField(
86
+ decoration: const InputDecoration(
87
+ labelText: 'Username',
88
+ hintText: 'Enter your username',
89
+ ),
90
+ validator: (value) {
91
+ if (value == null || value.isEmpty) {
92
+ return 'Please enter a username'; // Error state
93
+ }
94
+ if (value.length < 4) {
95
+ return 'Username must be at least 4 characters'; // Error state
96
+ }
97
+ return null; // Valid state
98
+ },
99
+ ),
100
+ const SizedBox(height: 16),
101
+ // 4. Add the submit button
102
+ ElevatedButton(
103
+ onPressed: () {
104
+ // 5. Trigger validation logic
105
+ if (_formKey.currentState!.validate()) {
106
+ // Form is valid: Process data
107
+ ScaffoldMessenger.of(context).showSnackBar(
108
+ const SnackBar(content: Text('Processing Data')),
109
+ );
110
+ } else {
111
+ // Form is invalid: Errors are automatically displayed
112
+ debugPrint('Form validation failed.');
113
+ }
114
+ },
115
+ child: const Text('Submit'),
116
+ ),
117
+ ],
118
+ ),
119
+ );
120
+ }
121
+}
122
+```
.claude/skills/flutter-building-layouts/SKILL.md
new
+128
@@ -0,0 +1,128 @@
1
+---
2
+name: "flutter-building-layouts"
3
+description: "Builds Flutter layouts using the constraint system and layout widgets. Use when creating or refining the UI structure of a Flutter application."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:14:15 GMT"
7
+
8
+---
9
+# Architecting Flutter Layouts
10
+
11
+## Contents
12
+- [Core Layout Principles](#core-layout-principles)
13
+- [Structural Widgets](#structural-widgets)
14
+- [Adaptive and Responsive Design](#adaptive-and-responsive-design)
15
+- [Workflow: Implementing a Complex Layout](#workflow-implementing-a-complex-layout)
16
+- [Examples](#examples)
17
+
18
+## Core Layout Principles
19
+
20
+Master the fundamental Flutter layout rule: **Constraints go down. Sizes go up. Parent sets position.**
21
+
22
+* **Pass Constraints Down:** Always pass constraints (minimum/maximum width and height) from the parent Widget to its children. A Widget cannot choose its own size independently of its parent's constraints.
23
+* **Pass Sizes Up:** Calculate the child Widget's desired size within the given constraints and pass this size back up to the parent.
24
+* **Set Position via Parent:** Define the `x` and `y` coordinates of a child Widget exclusively within the parent Widget. Children do not know their own position on the screen.
25
+* **Avoid Unbounded Constraints:** Never pass unbounded constraints (e.g., `double.infinity`) in the cross-axis of a flex box (`Row` or `Column`) or within scrollable regions (`ListView`). This causes render exceptions.
26
+
27
+## Structural Widgets
28
+
29
+Select the appropriate structural Widget based on the required spatial arrangement.
30
+
31
+* **Use `Row` and `Column`:** Implement `Row` for horizontal linear layouts and `Column` for vertical linear layouts. Control child alignment using `mainAxisAlignment` and `crossAxisAlignment`.
32
+* **Use `Expanded` and `Flexible`:** Wrap children of `Row` or `Column` in `Expanded` to force them to fill available space, or `Flexible` to allow them to size themselves up to the available space.
33
+* **Use `Container`:** Wrap Widgets in a `Container` when you need to apply padding, margins, borders, or background colors.
34
+* **Use `Stack`:** Implement `Stack` when Widgets must overlap on the Z-axis. Use `Positioned` to anchor children to specific edges of the `Stack`.
35
+* **Use `SizedBox`:** Enforce strict, tight constraints on a child Widget by wrapping it in a `SizedBox` with explicit `width` and `height` values.
36
+
37
+## Adaptive and Responsive Design
38
+
39
+Apply conditional logic to handle varying screen sizes and form factors.
40
+
41
+* **If fitting UI into available space (Responsive):** Use `LayoutBuilder`, `Expanded`, and `Flexible` to dynamically adjust the size and placement of elements based on the parent's constraints.
42
+* **If adjusting UI usability for a specific form factor (Adaptive):** Use conditional rendering to swap entire layout structures. For example, render a bottom navigation bar on mobile, but a side navigation rail on tablets/desktop.
43
+
44
+## Workflow: Implementing a Complex Layout
45
+
46
+Follow this sequential workflow to architect and implement robust Flutter layouts.
47
+
48
+### Task Progress
49
+- [ ] **Phase 1: Visual Deconstruction**
50
+ - [ ] Break down the target UI into a hierarchy of rows, columns, and grids.
51
+ - [ ] Identify overlapping elements (requiring `Stack`).
52
+ - [ ] Identify scrolling regions (requiring `ListView` or `SingleChildScrollView`).
53
+- [ ] **Phase 2: Constraint Planning**
54
+ - [ ] Determine which Widgets require tight constraints (fixed size) vs. loose constraints (flexible size).
55
+ - [ ] Identify potential unbounded constraint risks (e.g., a `ListView` inside a `Column`).
56
+- [ ] **Phase 3: Implementation**
57
+ - [ ] Build the layout from the outside in, starting with the `Scaffold` and primary structural Widgets.
58
+ - [ ] Extract deeply nested layout sections into separate, stateless Widgets to maintain readability.
59
+- [ ] **Phase 4: Validation and Feedback Loop**
60
+ - [ ] Run the application on target devices/simulators.
61
+ - [ ] **Run validator -> review errors -> fix:** Open the Flutter Inspector. Enable "Debug Paint" to visualize render boxes.
62
+ - [ ] Check for yellow/black striped overflow warnings.
63
+ - [ ] If overflow occurs: Wrap the overflowing Widget in `Expanded` (if inside a flex box) or wrap the parent in a scrollable Widget.
64
+
65
+## Examples
66
+
67
+### Example: Resolving Unbounded Constraints in Flex Boxes
68
+
69
+**Anti-pattern:** Placing a `ListView` directly inside a `Column` causes an unbounded height exception because the `Column` provides infinite vertical space to the `ListView`.
70
+
71
+```dart
72
+// BAD: Throws unbounded height exception
73
+Column(
74
+ children: [
75
+ Text('Header'),
76
+ ListView(
77
+ children: [/* items */],
78
+ ),
79
+ ],
80
+)
81
+```
82
+
83
+**Implementation:** Wrap the `ListView` in an `Expanded` Widget to bound its height to the remaining space in the `Column`.
84
+
85
+```dart
86
+// GOOD: ListView is constrained to remaining space
87
+Column(
88
+ children: [
89
+ Text('Header'),
90
+ Expanded(
91
+ child: ListView(
92
+ children: [/* items */],
93
+ ),
94
+ ),
95
+ ],
96
+)
97
+```
98
+
99
+### Example: Responsive Layout with LayoutBuilder
100
+
101
+Implement `LayoutBuilder` to conditionally render different structural Widgets based on available width.
102
+
103
+```dart
104
+Widget buildAdaptiveLayout(BuildContext context) {
105
+ return LayoutBuilder(
106
+ builder: (context, constraints) {
107
+ // Conditional logic based on screen width
108
+ if (constraints.maxWidth > 600) {
109
+ // Tablet/Desktop: Side-by-side layout
110
+ return Row(
111
+ children: [
112
+ SizedBox(width: 250, child: SidebarWidget()),
113
+ Expanded(child: MainContentWidget()),
114
+ ],
115
+ );
116
+ } else {
117
+ // Mobile: Stacked layout with navigation
118
+ return Column(
119
+ children: [
120
+ Expanded(child: MainContentWidget()),
121
+ BottomNavigationBarWidget(),
122
+ ],
123
+ );
124
+ }
125
+ },
126
+ );
127
+}
128
+```
.claude/skills/flutter-building-plugins/SKILL.md
new
+206
@@ -0,0 +1,206 @@
1
+---
2
+name: "flutter-building-plugins"
3
+description: "Builds Flutter plugins that provide native interop for other apps to use. Use when creating reusable packages that bridge Flutter with platform-specific functionality."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:21:35 GMT"
7
+
8
+---
9
+# Developing Flutter Plugins
10
+
11
+## Contents
12
+- [Architecture & Design Patterns](#architecture--design-patterns)
13
+- [Workflow: Creating a New Plugin](#workflow-creating-a-new-plugin)
14
+- [Workflow: Implementing Android Platform Code](#workflow-implementing-android-platform-code)
15
+- [Workflow: Implementing Windows Platform Code](#workflow-implementing-windows-platform-code)
16
+- [Workflow: Adding Platforms to an Existing Plugin](#workflow-adding-platforms-to-an-existing-plugin)
17
+- [Examples](#examples)
18
+
19
+## Architecture & Design Patterns
20
+
21
+### Federated Plugins
22
+Implement federated plugins to split a plugin's API across multiple packages, allowing independent teams to build platform-specific implementations. Structure federated plugins into three distinct components:
23
+1. **App-facing interface:** The primary package users depend on. It exports the public API.
24
+2. **Platform interface:** The package defining the common interface that all platform implementations must implement.
25
+3. **Platform implementations:** Independent packages containing platform-specific code (e.g., `my_plugin_android`, `my_plugin_windows`).
26
+
27
+### FFI vs. Standard Plugins
28
+Choose the correct plugin template based on your native interoperability requirements:
29
+* **Standard Plugins (`--template=plugin`):** Use for accessing platform-specific APIs (e.g., Android SDK, iOS frameworks) via Method Channels.
30
+* **FFI Plugins (`--template=plugin_ffi`):** Use for accessing C/C++ native libraries, configuring Google Play services on Android, or using static linking on iOS/macOS.
31
+ * *Constraint:* FFI plugin packages support bundling native code and method channel registration code, but *not* method channels themselves. If you require both method channels and FFI, use the standard non-FFI plugin template.
32
+
33
+## Workflow: Creating a New Plugin
34
+
35
+Follow this workflow to initialize a new plugin package.
36
+
37
+**Task Progress:**
38
+- [ ] Determine if the plugin requires FFI or standard Method Channels.
39
+- [ ] Execute the appropriate `flutter create` command.
40
+- [ ] Verify the generated directory structure.
41
+
42
+**Conditional Initialization:**
43
+* **If creating a STANDARD plugin:**
44
+ Run the following command, specifying your supported platforms, organization, and preferred languages (defaults are Swift and Kotlin):
45
+ ```bash
46
+ flutter create --template=plugin \
47
+ --platforms=android,ios,web,linux,macos,windows \
48
+ --org com.example.organization \
49
+ -i objc -a java \
50
+ my_plugin
51
+ ```
52
+* **If creating an FFI plugin:**
53
+ Run the following command to generate a project with Dart code in `lib` (using `dart:ffi`) and native source code in `src` (with a `CMakeLists.txt`):
54
+ ```bash
55
+ flutter create --template=plugin_ffi my_ffi_plugin
56
+ ```
57
+
58
+## Workflow: Implementing Android Platform Code
59
+
60
+Always edit Android platform code using Android Studio to ensure proper code completion and Gradle synchronization.
61
+
62
+**Task Progress:**
63
+- [ ] Run initial build to generate necessary Gradle files.
64
+- [ ] Open the Android module in Android Studio.
65
+- [ ] Implement `FlutterPlugin` and lifecycle-aware interfaces.
66
+- [ ] Refactor legacy `registerWith` logic.
67
+- [ ] Run validator -> review errors -> fix.
68
+
69
+1. **Generate Build Files:**
70
+ Build the code at least once before editing to resolve dependencies.
71
+ ```bash
72
+ cd example
73
+ flutter build apk --config-only
74
+ ```
75
+2. **Open in IDE:**
76
+ Launch Android Studio and open the `example/android/build.gradle` or `example/android/build.gradle.kts` file.
77
+3. **Locate Source:**
78
+ Navigate to your plugin's source code at `java/<organization-path>/<PluginName>`.
79
+4. **Implement V2 Embedding:**
80
+ * Implement the `FlutterPlugin` interface.
81
+ * Ensure your plugin class has a public constructor.
82
+ * Extract shared initialization logic from the legacy `registerWith()` method and the new `onAttachedToEngine()` method into a single private method. Both entry points must call this private method to maintain backward compatibility without duplicating logic.
83
+5. **Implement Lifecycle Interfaces:**
84
+ * **If your plugin requires an `Activity` reference:** Implement the `ActivityAware` interface and handle the `onAttachedToActivity`, `onDetachedFromActivityForConfigChanges`, `onReattachedToActivityForConfigChanges`, and `onDetachedFromActivity` callbacks.
85
+ * **If your plugin runs in a background `Service`:** Implement the `ServiceAware` interface.
86
+6. **Update Example App:**
87
+ Ensure the example app's `MainActivity.java` extends the v2 embedding `io.flutter.embedding.android.FlutterActivity`.
88
+7. **Document API:**
89
+ Document all non-overridden public members in your Android implementation.
90
+
91
+## Workflow: Implementing Windows Platform Code
92
+
93
+Always edit Windows platform code using Visual Studio.
94
+
95
+**Task Progress:**
96
+- [ ] Run initial build to generate the Visual Studio solution.
97
+- [ ] Open the solution in Visual Studio.
98
+- [ ] Implement C++ logic.
99
+- [ ] Rebuild the solution.
100
+
101
+1. **Generate Build Files:**
102
+ ```bash
103
+ cd example
104
+ flutter build windows
105
+ ```
106
+2. **Open in IDE:**
107
+ Launch Visual Studio and open the `example/build/windows/hello_example.sln` file.
108
+3. **Locate Source:**
109
+ Navigate to `hello_plugin/Source Files` and `hello_plugin/Header Files` in the Solution Explorer.
110
+4. **Rebuild:**
111
+ After making changes to the C++ plugin code, you *must* rebuild the solution in Visual Studio before running the app, or the outdated plugin binary will be used.
112
+
113
+## Workflow: Adding Platforms to an Existing Plugin
114
+
115
+Use this workflow to retrofit an existing plugin with support for additional platforms.
116
+
117
+**Task Progress:**
118
+- [ ] Run the platform addition command.
119
+- [ ] Update iOS/macOS podspecs (if applicable).
120
+- [ ] Implement the platform-specific code.
121
+
122
+1. **Run Create Command:**
123
+ Navigate to the root directory of your existing plugin and run:
124
+ ```bash
125
+ flutter create --template=plugin --platforms=web,macos .
126
+ ```
127
+2. **Update Podspecs:**
128
+ If adding iOS or macOS support, open the generated `.podspec` file and configure the required dependencies and deployment targets.
129
+
130
+## Examples
131
+
132
+### Android V2 Embedding Implementation
133
+High-fidelity example of an Android plugin implementing `FlutterPlugin` and `ActivityAware` while maintaining legacy compatibility.
134
+
135
+```java
136
+package com.example.myplugin;
137
+
138
+import androidx.annotation.NonNull;
139
+import io.flutter.embedding.engine.plugins.FlutterPlugin;
140
+import io.flutter.embedding.engine.plugins.activity.ActivityAware;
141
+import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
142
+import io.flutter.plugin.common.MethodCall;
143
+import io.flutter.plugin.common.MethodChannel;
144
+import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
145
+import io.flutter.plugin.common.MethodChannel.Result;
146
+import io.flutter.plugin.common.PluginRegistry.Registrar;
147
+
148
+/** MyPlugin */
149
+public class MyPlugin implements FlutterPlugin, MethodCallHandler, ActivityAware {
150
+ private MethodChannel channel;
151
+
152
+ // Public constructor required for v2 embedding
153
+ public MyPlugin() {}
154
+
155
+ @Override
156
+ public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
157
+ setupChannel(flutterPluginBinding.getBinaryMessenger());
158
+ }
159
+
160
+ // Legacy v1 embedding support
161
+ public static void registerWith(Registrar registrar) {
162
+ MyPlugin plugin = new MyPlugin();
163
+ plugin.setupChannel(registrar.messenger());
164
+ }
165
+
166
+ // Shared initialization logic
167
+ private void setupChannel(BinaryMessenger messenger) {
168
+ channel = new MethodChannel(messenger, "my_plugin");
169
+ channel.setMethodCallHandler(this);
170
+ }
171
+
172
+ @Override
173
+ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
174
+ if (call.method.equals("getPlatformVersion")) {
175
+ result.success("Android " + android.os.Build.VERSION.RELEASE);
176
+ } else {
177
+ result.notImplemented();
178
+ }
179
+ }
180
+
181
+ @Override
182
+ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
183
+ channel.setMethodCallHandler(null);
184
+ }
185
+
186
+ @Override
187
+ public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
188
+ // Handle Activity attachment
189
+ }
190
+
191
+ @Override
192
+ public void onDetachedFromActivityForConfigChanges() {
193
+ // Handle config changes
194
+ }
195
+
196
+ @Override
197
+ public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
198
+ // Handle reattachment
199
+ }
200
+
201
+ @Override
202
+ public void onDetachedFromActivity() {
203
+ // Clean up Activity references
204
+ }
205
+}
206
+```
.claude/skills/flutter-caching-data/SKILL.md
new
+168
@@ -0,0 +1,168 @@
1
+---
2
+name: "flutter-caching-data"
3
+description: "Implements caching strategies for Flutter apps to improve performance and offline support. Use when retaining app data locally to reduce network requests or speed up startup."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:19:54 GMT"
7
+
8
+---
9
+# Implementing Flutter Caching and Offline-First Architectures
10
+
11
+## Contents
12
+- [Selecting a Caching Strategy](#selecting-a-caching-strategy)
13
+- [Implementing Offline-First Data Synchronization](#implementing-offline-first-data-synchronization)
14
+- [Managing File System and SQLite Persistence](#managing-file-system-and-sqlite-persistence)
15
+- [Optimizing UI, Scroll, and Image Caching](#optimizing-ui-scroll-and-image-caching)
16
+- [Caching the FlutterEngine (Android)](#caching-the-flutterengine-android)
17
+- [Workflows](#workflows)
18
+
19
+## Selecting a Caching Strategy
20
+
21
+Apply the appropriate caching mechanism based on the data lifecycle and size requirements.
22
+
23
+* **If storing small, non-critical UI states or preferences:** Use `shared_preferences`.
24
+* **If storing large, structured datasets:** Use on-device databases (SQLite via `sqflite`, Drift, Hive CE, or Isar).
25
+* **If storing binary data or large media:** Use file system caching via `path_provider`.
26
+* **If retaining user session state (navigation, scroll positions):** Implement Flutter's built-in state restoration to sync the Element tree with the engine.
27
+* **If optimizing Android initialization:** Pre-warm and cache the `FlutterEngine`.
28
+
29
+## Implementing Offline-First Data Synchronization
30
+
31
+Design repositories as the single source of truth, combining local databases and remote API clients.
32
+
33
+### Read Operations (Stream Approach)
34
+Yield local data immediately for fast UI rendering, then fetch remote data, update the local cache, and yield the fresh data.
35
+
36
+```dart
37
+Stream<UserProfile> getUserProfile() async* {
38
+ // 1. Yield local cache first
39
+ final localProfile = await _databaseService.fetchUserProfile();
40
+ if (localProfile != null) yield localProfile;
41
+
42
+ // 2. Fetch remote, update cache, yield fresh data
43
+ try {
44
+ final remoteProfile = await _apiClientService.getUserProfile();
45
+ await _databaseService.updateUserProfile(remoteProfile);
46
+ yield remoteProfile;
47
+ } catch (e) {
48
+ // Handle network failure; UI already has local data
49
+ }
50
+}
51
+```
52
+
53
+### Write Operations
54
+Determine the write strategy based on data criticality:
55
+* **If strict server synchronization is required (Online-only):** Attempt the API call first. Only update the local database if the API call succeeds.
56
+* **If offline availability is prioritized (Offline-first):** Write to the local database immediately. Attempt the API call. If the API call fails, flag the local record for background synchronization.
57
+
58
+### Background Synchronization
59
+Add a `synchronized` boolean flag to your data models. Run a periodic background task (e.g., via `workmanager` or a `Timer`) to push unsynchronized local changes to the server.
60
+
61
+## Managing File System and SQLite Persistence
62
+
63
+### File System Caching
64
+Use `path_provider` to locate the correct directory.
65
+* Use `getApplicationDocumentsDirectory()` for persistent data.
66
+* Use `getTemporaryDirectory()` for cache data the OS can clear.
67
+
68
+```dart
69
+Future<File> get _localFile async {
70
+ final directory = await getApplicationDocumentsDirectory();
71
+ return File('${directory.path}/cache.txt');
72
+}
73
+```
74
+
75
+### SQLite Persistence
76
+Use `sqflite` for relational data caching. Always use `whereArgs` to prevent SQL injection.
77
+
78
+```dart
79
+Future<void> updateCachedRecord(Record record) async {
80
+ final db = await database;
81
+ await db.update(
82
+ 'records',
83
+ record.toMap(),
84
+ where: 'id = ?',
85
+ whereArgs: [record.id], // NEVER use string interpolation here
86
+ );
87
+}
88
+```
89
+
90
+## Optimizing UI, Scroll, and Image Caching
91
+
92
+### Image Caching
93
+Image I/O and decompression are expensive.
94
+* Use the `cached_network_image` package to handle file-system caching of remote images.
95
+* **Custom ImageProviders:** If implementing a custom `ImageProvider`, override `createStream()` and `resolveStreamForKey()` instead of the deprecated `resolve()` method.
96
+* **Cache Sizing:** The `ImageCache.maxByteSize` no longer automatically expands for large images. If loading images larger than the default cache size, manually increase `ImageCache.maxByteSize` or subclass `ImageCache` to implement custom eviction logic.
97
+
98
+### Scroll Caching
99
+When configuring caching for scrollable widgets (`ListView`, `GridView`, `Viewport`), use the `scrollCacheExtent` property with a `ScrollCacheExtent` object. Do not use the deprecated `cacheExtent` and `cacheExtentStyle` properties.
100
+
101
+```dart
102
+// Correct implementation
103
+ListView(
104
+ scrollCacheExtent: const ScrollCacheExtent.pixels(500.0),
105
+ children: // ...
106
+)
107
+
108
+Viewport(
109
+ scrollCacheExtent: const ScrollCacheExtent.viewport(0.5),
110
+ slivers: // ...
111
+)
112
+```
113
+
114
+### Widget Caching
115
+* Avoid overriding `operator ==` on `Widget` objects. It causes O(N²) behavior during rebuilds.
116
+* **Exception:** You may override `operator ==` *only* on leaf widgets (no children) where comparing properties is significantly faster than rebuilding, and the properties rarely change.
117
+* Prefer using `const` constructors to allow the framework to short-circuit rebuilds automatically.
118
+
119
+## Caching the FlutterEngine (Android)
120
+
121
+To eliminate the non-trivial warm-up time of a `FlutterEngine` when adding Flutter to an existing Android app, pre-warm and cache the engine.
122
+
123
+1. Instantiate and pre-warm the engine in the `Application` class.
124
+2. Store it in the `FlutterEngineCache`.
125
+3. Retrieve it using `withCachedEngine` in the `FlutterActivity` or `FlutterFragment`.
126
+
127
+```kotlin
128
+// 1. Pre-warm in Application class
129
+val flutterEngine = FlutterEngine(this)
130
+flutterEngine.navigationChannel.setInitialRoute("/cached_route")
131
+flutterEngine.dartExecutor.executeDartEntrypoint(DartEntrypoint.createDefault())
132
+
133
+// 2. Cache the engine
134
+FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine)
135
+
136
+// 3. Use in Activity/Fragment
137
+startActivity(
138
+ FlutterActivity.withCachedEngine("my_engine_id").build(this)
139
+)
140
+```
141
+*Note: You cannot set an initial route via the Activity/Fragment builder when using a cached engine. Set the initial route on the engine's navigation channel before executing the Dart entrypoint.*
142
+
143
+## Workflows
144
+
145
+### Workflow: Implementing an Offline-First Repository
146
+Follow these steps to implement a robust offline-first data layer.
147
+
148
+- [ ] **Task Progress:**
149
+ - [ ] Define the data model with a `synchronized` boolean flag (default `false`).
150
+ - [ ] Implement the local `DatabaseService` (SQLite/Hive) with CRUD operations.
151
+ - [ ] Implement the remote `ApiClientService` for network requests.
152
+ - [ ] Create the `Repository` class combining both services.
153
+ - [ ] Implement the read method returning a `Stream<T>` (yield local, fetch remote, update local, yield remote).
154
+ - [ ] Implement the write method (write local, attempt remote, update `synchronized` flag).
155
+ - [ ] Implement a background sync function to process records where `synchronized == false`.
156
+ - [ ] Run validator -> review errors -> fix (Test offline behavior by disabling network).
157
+
158
+### Workflow: Pre-warming the Android FlutterEngine
159
+Follow these steps to cache the FlutterEngine for seamless Android integration.
160
+
161
+- [ ] **Task Progress:**
162
+ - [ ] Locate the Android `Application` class (create one if it doesn't exist and register in `AndroidManifest.xml`).
163
+ - [ ] Instantiate a new `FlutterEngine`.
164
+ - [ ] (Optional) Set the initial route via `navigationChannel.setInitialRoute()`.
165
+ - [ ] Execute the Dart entrypoint via `dartExecutor.executeDartEntrypoint()`.
166
+ - [ ] Store the engine in `FlutterEngineCache.getInstance().put()`.
167
+ - [ ] Update the target `FlutterActivity` or `FlutterFragment` to use `.withCachedEngine("id")`.
168
+ - [ ] Run validator -> review errors -> fix (Verify no blank screen appears during transition).
.claude/skills/flutter-embedding-native-views/SKILL.md
new
+197
@@ -0,0 +1,197 @@
1
+---
2
+name: "flutter-embedding-native-views"
3
+description: "Embeds native Android, iOS, or macOS views into a Flutter app. Use when integrating complex native components like maps or web views."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:20:28 GMT"
7
+
8
+---
9
+# Integrating Platform Views and Web Content
10
+
11
+## Contents
12
+- [Platform Views Architecture](#platform-views-architecture)
13
+- [Web Embedding Architecture](#web-embedding-architecture)
14
+- [Workflow: Implementing Android Platform Views](#workflow-implementing-android-platform-views)
15
+- [Workflow: Implementing iOS Platform Views](#workflow-implementing-ios-platform-views)
16
+- [Workflow: Embedding Flutter in Web Applications](#workflow-embedding-flutter-in-web-applications)
17
+- [Examples](#examples)
18
+
19
+## Platform Views Architecture
20
+
21
+Platform Views allow embedding native views (Android, iOS, macOS) directly into a Flutter application, enabling the application of transforms, clips, and opacity from Dart.
22
+
23
+### Android Implementations (API 23+)
24
+Choose the appropriate implementation based on your performance and fidelity requirements:
25
+* **Hybrid Composition:** Renders Flutter content into a texture and uses `SurfaceFlinger` to compose both.
26
+ * *Pros:* Best performance and fidelity for Android views.
27
+ * *Cons:* Lowers overall application FPS. Certain Flutter widget transformations will not work.
28
+* **Texture Layer (Texture Layer Hybrid Composition):** Renders Platform Views into a texture. Flutter draws them via the texture and renders its own content directly into a Surface.
29
+ * *Pros:* Best performance for Flutter rendering. All transformations work correctly.
30
+ * *Cons:* Quick scrolling (e.g., WebViews) can be janky. `SurfaceView` is problematic (breaks accessibility). Text magnifiers break unless Flutter is rendered into a `TextureView`.
31
+
32
+### iOS & macOS Implementations
33
+* **iOS:** Uses Hybrid Composition exclusively. The native `UIView` is appended to the view hierarchy.
34
+ * *Limitations:* `ShaderMask` and `ColorFiltered` widgets are not supported. `BackdropFilter` has composition limitations.
35
+* **macOS:** Uses Hybrid Composition (`NSView`).
36
+ * *Limitations:* Not fully functional in current releases (e.g., gesture support is unavailable).
37
+
38
+### Performance Mitigation
39
+Mitigate performance drops during complex Dart animations by rendering a screenshot of the native view as a placeholder texture while the animation runs.
40
+
41
+## Web Embedding Architecture
42
+
43
+Embed Flutter into existing web applications (Vanilla JS, React, Angular, etc.) using either Full Page mode or Embedded (Multi-view) mode.
44
+
45
+* **Full Page Mode:** Flutter takes over the entire browser window. Use an `iframe` if you need to constrain the Flutter app without modifying the Flutter bootstrap process.
46
+* **Embedded Mode (Multi-view):** Render Flutter into specific HTML elements (`div`s). Requires `multiViewEnabled: true` during engine initialization.
47
+ * Manage views from JavaScript using `app.addView()` and `app.removeView()`.
48
+ * In Dart, replace `runApp` with `runWidget`.
49
+ * Manage the dynamic list of views using `WidgetsBinding.instance.platformDispatcher.views` and render them using `ViewCollection` and `View` widgets.
50
+
51
+## Workflow: Implementing Android Platform Views
52
+
53
+Follow this sequential workflow to implement a Platform View on Android.
54
+
55
+**Task Progress:**
56
+- [ ] 1. Determine the composition mode (Hybrid vs. Texture Layer).
57
+- [ ] 2. Implement the Dart widget.
58
+- [ ] 3. Implement the native Android View and Factory.
59
+- [ ] 4. Register the Platform View in the Android host.
60
+- [ ] 5. Run validator -> review rendering -> fix manual invalidation issues.
61
+
62
+### 1. Dart Implementation
63
+If using **Hybrid Composition**, use `PlatformViewLink`, `AndroidViewSurface`, and `PlatformViewsService.initSurfaceAndroidView`.
64
+If using **Texture Layer**, use the `AndroidView` widget.
65
+
66
+### 2. Native Implementation
67
+Create a class implementing `io.flutter.plugin.platform.PlatformView` that returns your native `android.view.View`.
68
+Create a factory extending `PlatformViewFactory` to instantiate your view.
69
+
70
+### 3. Registration
71
+Register the factory in your `MainActivity.kt` (or plugin) using `flutterEngine.platformViewsController.registry.registerViewFactory`.
72
+
73
+*Note: If your native view uses `SurfaceView` or `SurfaceTexture`, manually call `invalidate` on the View or its parent when content changes, as they do not invalidate themselves automatically.*
74
+
75
+## Workflow: Implementing iOS Platform Views
76
+
77
+Follow this sequential workflow to implement a Platform View on iOS.
78
+
79
+**Task Progress:**
80
+- [ ] 1. Implement the Dart widget using `UiKitView`.
81
+- [ ] 2. Implement the native iOS View (`FlutterPlatformView`) and Factory (`FlutterPlatformViewFactory`).
82
+- [ ] 3. Register the Platform View in `AppDelegate.swift` or the plugin registrar.
83
+- [ ] 4. Run validator -> review composition limitations -> fix unsupported filters.
84
+
85
+## Workflow: Embedding Flutter in Web Applications
86
+
87
+Follow this sequential workflow to embed Flutter into an existing web DOM.
88
+
89
+**Task Progress:**
90
+- [ ] 1. Update `flutter_bootstrap.js` to enable multi-view.
91
+- [ ] 2. Update `main.dart` to use `runWidget` and `ViewCollection`.
92
+- [ ] 3. Implement JavaScript logic to add/remove host elements.
93
+- [ ] 4. Run validator -> review view constraints -> fix CSS conflicts.
94
+
95
+### 1. JavaScript Configuration
96
+In `flutter_bootstrap.js`, initialize the engine with `multiViewEnabled: true`.
97
+Use the returned `app` object to add views: `app.addView({ hostElement: document.getElementById('my-div') })`.
98
+
99
+### 2. Dart Configuration
100
+Replace `runApp()` with `runWidget()`.
101
+Create a root widget that listens to `WidgetsBindingObserver.didChangeMetrics`.
102
+Map over `WidgetsBinding.instance.platformDispatcher.views` to create a `View` widget for each attached `FlutterView`, and wrap them all in a `ViewCollection`.
103
+
104
+## Examples
105
+
106
+### Example: Android Texture Layer (Dart)
107
+```dart
108
+import 'package:flutter/material.dart';
109
+import 'package:flutter/services.dart';
110
+
111
+class NativeAndroidView extends StatelessWidget {
112
+ @override
113
+ Widget build(BuildContext context) {
114
+ const String viewType = 'my_native_view';
115
+ final Map<String, dynamic> creationParams = <String, dynamic>{};
116
+
117
+ return AndroidView(
118
+ viewType: viewType,
119
+ layoutDirection: TextDirection.ltr,
120
+ creationParams: creationParams,
121
+ creationParamsCodec: const StandardMessageCodec(),
122
+ );
123
+ }
124
+}
125
+```
126
+
127
+### Example: Web Multi-View Initialization (JavaScript)
128
+```javascript
129
+_flutter.loader.load({
130
+ onEntrypointLoaded: async function(engineInitializer) {
131
+ let engine = await engineInitializer.initializeEngine({
132
+ multiViewEnabled: true,
133
+ });
134
+ let app = await engine.runApp();
135
+
136
+ // Add a view to a specific DOM element
137
+ let viewId = app.addView({
138
+ hostElement: document.querySelector('#flutter-host-container'),
139
+ initialData: { customData: 'Hello from JS' }
140
+ });
141
+ }
142
+});
143
+```
144
+
145
+### Example: Web Multi-View Root Widget (Dart)
146
+```dart
147
+import 'dart:ui' show FlutterView;
148
+import 'package:flutter/widgets.dart';
149
+
150
+void main() {
151
+ runWidget(MultiViewApp(viewBuilder: (context) => const MyEmbeddedWidget()));
152
+}
153
+
154
+class MultiViewApp extends StatefulWidget {
155
+ final WidgetBuilder viewBuilder;
156
+ const MultiViewApp({super.key, required this.viewBuilder});
157
+
158
+ @override
159
+ State<MultiViewApp> createState() => _MultiViewAppState();
160
+}
161
+
162
+class _MultiViewAppState extends State<MultiViewApp> with WidgetsBindingObserver {
163
+ Map<Object, Widget> _views = {};
164
+
165
+ @override
166
+ void initState() {
167
+ super.initState();
168
+ WidgetsBinding.instance.addObserver(this);
169
+ _updateViews();
170
+ }
171
+
172
+ @override
173
+ void didChangeMetrics() => _updateViews();
174
+
175
+ void _updateViews() {
176
+ final newViews = <Object, Widget>{};
177
+ for (final FlutterView view in WidgetsBinding.instance.platformDispatcher.views) {
178
+ newViews[view.viewId] = _views[view.viewId] ?? View(
179
+ view: view,
180
+ child: Builder(builder: widget.viewBuilder),
181
+ );
182
+ }
183
+ setState(() => _views = newViews);
184
+ }
185
+
186
+ @override
187
+ void dispose() {
188
+ WidgetsBinding.instance.removeObserver(this);
189
+ super.dispose();
190
+ }
191
+
192
+ @override
193
+ Widget build(BuildContext context) {
194
+ return ViewCollection(views: _views.values.toList(growable: false));
195
+ }
196
+}
197
+```
.claude/skills/flutter-feature-based-architecture/SKILL.md
new
+529
@@ -0,0 +1,529 @@
1
+---
2
+name: "flutter-feature-based-architecture"
3
+description: "Architects a Flutter application using a Feature-Based structure with Riverpod state management and a shared core layer. Use when structuring a new project, adding features, or refactoring for scalability."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Fri, 28 Mar 2026 04:05:00 GMT"
7
+---
8
+
9
+# Flutter Feature-Based Architecture with Riverpod
10
+
11
+## Contents
12
+
13
+- [Overview](#overview)
14
+- [Project Structure](#project-structure)
15
+- [Core (Global) Layer](#core-global-layer)
16
+- [Feature Layer](#feature-layer)
17
+- [State Management with Riverpod](#state-management-with-riverpod)
18
+- [Feature Implementation Workflow](#feature-implementation-workflow)
19
+- [Naming Conventions](#naming-conventions)
20
+- [Examples](#examples)
21
+
22
+## Overview
23
+
24
+This skill enforces a **Feature-Based Architecture** where code is organized by feature (vertical slices) rather than by technical layer. Shared, cross-cutting concerns live in a `core/` directory. State management is handled exclusively with **Riverpod** (`flutter_riverpod` / `hooks_riverpod`).
25
+
26
+### Core Principles
27
+
28
+- **Feature Isolation:** Each feature owns its data, providers, and views. Features must **never** import from other features. If two features need the same code, move it to `core/`.
29
+- **Riverpod-First State Management:** All state is managed via Riverpod providers. No `ChangeNotifier`, `Bloc`, or raw `setState` for business logic.
30
+- **Core for Shared Code:** Anything used by 2+ features belongs in `core/`. This includes shared models, repositories, providers, components, constants, utilities, and infrastructure.
31
+- **Unidirectional Data Flow:** Data flows from Repository → Provider/StateNotifier → View. User events flow from View → Provider/StateNotifier → Repository.
32
+- **Single Source of Truth (SSOT):** Each piece of data has exactly one authoritative source — the Repository. Providers expose that data reactively to the UI.
33
+
34
+## Project Structure
35
+
36
+```
37
+lib/
38
+├── core/ # Shared, cross-cutting concerns (global)
39
+│ ├── components/ # Reusable UI widgets used across features
40
+│ ├── constants/ # App-wide constants (colors, URLs, config)
41
+│ ├── extensions/ # Dart extension methods on common types
42
+│ ├── locales/ # Localization keys and config
43
+│ ├── logger/ # App-wide logging utility
44
+│ ├── models/ # Shared domain models
45
+│ ├── providers/ # Shared Riverpod providers
46
+│ ├── repository/ # Shared repositories & services
47
+│ ├── routes/ # App routing / navigation config
48
+│ ├── theme/ # App theme data (colors, text styles, etc.)
49
+│ └── utils/ # General-purpose utility functions
50
+│
51
+├── features/ # Feature modules (vertical slices)
52
+│ ├── auth/ # Example feature
53
+│ │ ├── data/
54
+│ │ │ ├── models/
55
+│ │ │ └── repository/
56
+│ │ ├── providers/
57
+│ │ └── views/
58
+│ │ └── components/
59
+│ ├── home/ # Example feature
60
+│ ├── settings/ # Example feature
61
+│ └── ...
62
+│
63
+└── main.dart # App entry point
64
+```
65
+
66
+## Core (Global) Layer
67
+
68
+The `core/` directory holds code that is **shared across multiple features**. The rule is simple: *if it's used by 2+ features, it belongs in `core/`*.
69
+
70
+### `core/constants/`
71
+
72
+App-wide constant values. Use a barrel file (`constants.dart`) to re-export all files.
73
+
74
+| What to put here | Example |
75
+|-----------------|---------|
76
+| Color constants | `AppColors` with static `Color` fields (primary, secondary, error, success, etc.) |
77
+| URL constants | `AppUrls` with static URL strings (API base, privacy policy, CDN paths) |
78
+| Size/spacing constants | `AppSizes` with static doubles for padding, radius, etc. |
79
+| Config values | API keys (non-secret), pagination limits, timeout durations |
80
+
81
+### `core/components/`
82
+
83
+Reusable UI widgets consumed by multiple features. These are **presentational** widgets that receive data via constructor params — they should not contain business logic.
84
+
85
+Examples: custom dropdowns, reusable form fields, loading indicators, empty state widgets, confirmation dialogs.
86
+
87
+### `core/extensions/`
88
+
89
+Dart extension methods on common types (`String`, `DateTime`, `BuildContext`, `Color`, etc.) that are used across the app.
90
+
91
+### `core/locales/`
92
+
93
+Localization configuration and generated key files (e.g., for `easy_localization`, `intl`, or `flutter_localizations`).
94
+
95
+### `core/logger/`
96
+
97
+A singleton logging utility wrapping a logging package (e.g., `logger`). Provides static methods like `Log.debug()`, `Log.info()`, `Log.error()`, `Log.warning()`. Should be debug-only in release builds.
98
+
99
+### `core/models/`
100
+
101
+Domain models shared across features. Use a barrel file (`models.dart`) for re-exports. Only place models here if they are truly cross-feature. Feature-specific models stay in their feature's `data/models/`.
102
+
103
+### `core/providers/`
104
+
105
+Riverpod providers that are consumed by multiple features. Typically these wrap shared repositories or expose shared state (e.g., current user, app settings, connectivity).
106
+
107
+### `core/repository/`
108
+
109
+Repositories and services for shared data domains. Organize by subdomain:
110
+
111
+```
112
+core/repository/
113
+├── api/ # Base API client, HTTP wrapper
114
+├── auth/ # Shared auth repository (if used by multiple features)
115
+├── storage/ # Local storage / secure storage wrapper
116
+└── ...
117
+```
118
+
119
+Each repository file should also declare its own Riverpod `Provider` at the bottom of the file.
120
+
121
+### `core/routes/`
122
+
123
+App-level routing and navigation configuration (e.g., `GoRouter` setup, route constants, route guards).
124
+
125
+### `core/theme/`
126
+
127
+App theme data — `ThemeData`, text styles, component themes. Keep color definitions in `constants/` and reference them here.
128
+
129
+### `core/utils/`
130
+
131
+General-purpose utility classes and functions that don't fit elsewhere. Examples: phone/URL launchers, file helpers, crypto utilities, date formatters, Excel/CSV export helpers.
132
+
133
+## Feature Layer
134
+
135
+Each feature directory under `features/` follows a **consistent internal structure** with 3 primary subdirectories:
136
+
137
+```
138
+features/<feature_name>/
139
+├── data/ # Data layer
140
+│ ├── models/ # Domain models specific to this feature
141
+│ │ ├── <model>.dart
142
+│ │ └── models.dart # Barrel file (optional)
143
+│ └── repository/ # Repositories (Firestore CRUD, API calls, etc.)
144
+│ └── <feature>_repository.dart
145
+│
146
+├── providers/ # State management layer
147
+│ └── <feature>_providers.dart # All Riverpod providers for this feature
148
+│
149
+└── views/ # UI layer
150
+ ├── <feature>_page.dart # Main page widget(s)
151
+ ├── add_<feature>_page.dart # Create/edit page (if applicable)
152
+ └── components/ # Feature-specific UI components
153
+ └── <component>.dart
154
+```
155
+
156
+### Optional Subdirectories
157
+
158
+A feature may add extra subdirectories when needed:
159
+
160
+| Directory | When to add |
161
+|-----------|-------------|
162
+| `utils/` | Feature has its own utility/helper functions |
163
+| `services/` | Feature needs stateless service classes (e.g., scheduling logic, data transformation) |
164
+
165
+### Rules
166
+
167
+1. **Never import from another feature.** If feature A needs something from feature B, extract it to `core/`.
168
+2. **Keep views lean.** Views (`ConsumerWidget` / `ConsumerStatefulWidget`) should only handle UI concerns — layout, animations, navigation. All business logic lives in providers/StateNotifiers.
169
+3. **One provider file per feature** is the default. Split into multiple files only when the file exceeds ~400 lines.
170
+4. **Models must have serialization.** Every model needs `fromMap()` / `fromJson()` and `toMap()` / `toJson()` along with `copyWith()`.
171
+
172
+## State Management with Riverpod
173
+
174
+### Provider Types
175
+
176
+| Provider Type | When to Use | Example |
177
+|---------------|-------------|---------|
178
+| `Provider` | Expose a stateless repository or service instance | `final repoProvider = Provider<Repo>((ref) => Repo());` |
179
+| `StateProvider` | Simple mutable state (filters, toggles, search query) | `final searchProvider = StateProvider<String>((ref) => '');` |
180
+| `StateNotifierProvider` | Complex state with methods (CRUD controllers, paginated lists) | `final listProvider = StateNotifierProvider<Notifier, State>((ref) { ... });` |
181
+| `FutureProvider` | One-shot async data fetching | `final dataProvider = FutureProvider<List<Item>>((ref) async { ... });` |
182
+| `FutureProvider.family` | Parameterized async data fetching | `final itemProvider = FutureProvider.family<Item?, String>((ref, id) async { ... });` |
183
+| `StreamProvider` | Real-time data streams (Firestore snapshots, WebSocket) | `final streamProvider = StreamProvider<List<Item>>((ref) { ... });` |
184
+
185
+### Provider File Organization
186
+
187
+Each feature's `providers/` file should be organized in this order:
188
+
189
+1. **Repository Providers** — `Provider<XRepository>`
190
+2. **Filter/UI State Providers** — `StateProvider<String>`, `StateProvider<bool>`
191
+3. **State Classes** — Immutable state objects with `copyWith()`
192
+4. **StateNotifier Classes** — Business logic (list pagination, CRUD operations)
193
+5. **StateNotifierProvider declarations** — Wiring notifiers to their dependencies
194
+
195
+### Use `autoDispose` by default
196
+
197
+Prefer `.autoDispose` on providers that are scoped to a screen or feature:
198
+
199
+```dart
200
+final featureListProvider = StateNotifierProvider.autoDispose<
201
+ FeatureListNotifier, FeatureListState
202
+>((ref) { ... });
203
+```
204
+
205
+This ensures state is cleaned up when the user navigates away.
206
+
207
+## Feature Implementation Workflow
208
+
209
+Follow this sequential workflow when adding a new feature:
210
+
211
+**Task Progress:**
212
+
213
+- [ ] **Step 1: Create Feature Directory.** Create `lib/features/<feature_name>/` with subdirectories: `data/models/`, `data/repository/`, `providers/`, `views/`, and optionally `views/components/`.
214
+- [ ] **Step 2: Define Domain Models.** Create model classes in `data/models/` with `fromMap()`, `toMap()`, and `copyWith()` methods.
215
+- [ ] **Step 3: Implement Repository.** Create repository class in `data/repository/` for data operations (API calls, database CRUD, etc.).
216
+- [ ] **Step 4: Implement Providers.** Create `providers/<feature>_providers.dart` with:
217
+ - Repository `Provider`
218
+ - Filter `StateProvider`s (if needed)
219
+ - State class + `StateNotifier` + `StateNotifierProvider` for list/pagination
220
+ - State class + `StateNotifier` + `StateNotifierProvider` for CRUD controller
221
+- [ ] **Step 5: Implement Views.** Create pages in `views/` as `ConsumerWidget` or `ConsumerStatefulWidget`. Use `ref.watch()` for reactive state, `ref.read()` for one-shot actions.
222
+- [ ] **Step 6: Extract Shared Code.** If any model, provider, component, or utility is needed by another feature, move it to the appropriate `core/` subdirectory.
223
+- [ ] **Step 7: Wire Navigation.** Add the new feature's routes to `core/routes/` and any navigation entry points.
224
+
225
+## Naming Conventions
226
+
227
+| Item | Convention | Example |
228
+|------|-----------|---------|
229
+| Feature directory | `snake_case` | `user_profile/`, `order_history/` |
230
+| Model files | `snake_case` | `user_model.dart`, `order_item.dart` |
231
+| Repository files | `<feature>_repository.dart` | `auth_repository.dart` |
232
+| Provider files | `<feature>_providers.dart` | `auth_providers.dart` |
233
+| View files | `<feature>_page.dart` | `auth_page.dart`, `login_page.dart` |
234
+| StateNotifier classes | `PascalCase` + `Notifier` or `Controller` | `OrderListNotifier`, `AuthController` |
235
+| State classes | `PascalCase` + `State` | `OrderListState`, `AuthState` |
236
+| Provider variables | `camelCase` + `Provider` | `orderListProvider`, `authRepositoryProvider` |
237
+| Barrel exports | `models.dart`, `constants.dart` | Re-export all files in the directory |
238
+
239
+## Examples
240
+
241
+### Data Layer: Model
242
+
243
+```dart
244
+// features/orders/data/models/order_model.dart
245
+class OrderModel {
246
+ final String id;
247
+ final String customerName;
248
+ final double total;
249
+ final DateTime createdAt;
250
+
251
+ const OrderModel({
252
+ required this.id,
253
+ required this.customerName,
254
+ required this.total,
255
+ required this.createdAt,
256
+ });
257
+
258
+ factory OrderModel.fromMap(Map<String, dynamic> map) {
259
+ return OrderModel(
260
+ id: map['id'] ?? '',
261
+ customerName: map['customerName'] ?? '',
262
+ total: (map['total'] ?? 0).toDouble(),
263
+ createdAt: DateTime.parse(map['createdAt']),
264
+ );
265
+ }
266
+
267
+ Map<String, dynamic> toMap() => {
268
+ 'customerName': customerName,
269
+ 'total': total,
270
+ 'createdAt': createdAt.toIso8601String(),
271
+ };
272
+
273
+ OrderModel copyWith({
274
+ String? id,
275
+ String? customerName,
276
+ double? total,
277
+ DateTime? createdAt,
278
+ }) {
279
+ return OrderModel(
280
+ id: id ?? this.id,
281
+ customerName: customerName ?? this.customerName,
282
+ total: total ?? this.total,
283
+ createdAt: createdAt ?? this.createdAt,
284
+ );
285
+ }
286
+}
287
+```
288
+
289
+### Data Layer: Repository
290
+
291
+```dart
292
+// features/orders/data/repository/order_repository.dart
293
+class OrderRepository {
294
+ // Inject or instantiate your data source (Firestore, REST API, etc.)
295
+
296
+ Future<String> addOrder(OrderModel order) async { /* ... */ }
297
+ Future<void> updateOrder(String id, OrderModel order) async { /* ... */ }
298
+ Future<void> deleteOrder(String id) async { /* ... */ }
299
+ Future<OrderModel?> getOrderById(String id) async { /* ... */ }
300
+
301
+ // Paginated fetch returning items + cursor for next page
302
+ Future<Map<String, dynamic>> getOrdersPaginated(
303
+ dynamic lastCursor, {
304
+ int limit = 10,
305
+ String? searchQuery,
306
+ }) async { /* ... */ }
307
+}
308
+```
309
+
310
+### Provider Layer: Paginated List
311
+
312
+```dart
313
+// features/orders/providers/order_providers.dart
314
+
315
+// 1. Repository Provider
316
+final orderRepositoryProvider = Provider<OrderRepository>((ref) {
317
+ return OrderRepository();
318
+});
319
+
320
+// 2. Filter Providers
321
+final orderSearchQueryProvider = StateProvider<String>((ref) => '');
322
+
323
+// 3. List State
324
+class OrderListState {
325
+ final List<OrderModel> items;
326
+ final bool isLoading;
327
+ final bool isLoadingMore;
328
+ final String? error;
329
+ final bool hasMore;
330
+ final dynamic lastCursor;
331
+
332
+ const OrderListState({
333
+ this.items = const [],
334
+ this.isLoading = false,
335
+ this.isLoadingMore = false,
336
+ this.error,
337
+ this.hasMore = true,
338
+ this.lastCursor,
339
+ });
340
+
341
+ OrderListState copyWith({ /* all fields */ });
342
+}
343
+
344
+// 4. List Notifier
345
+class OrderListNotifier extends StateNotifier<OrderListState> {
346
+ final OrderRepository _repository;
347
+ final Ref _ref;
348
+ final int _pageSize = 10;
349
+
350
+ OrderListNotifier(this._repository, this._ref)
351
+ : super(const OrderListState()) {
352
+ fetchFirstPage();
353
+ }
354
+
355
+ Future<void> fetchFirstPage() async {
356
+ state = state.copyWith(isLoading: true, error: null);
357
+ try {
358
+ final result = await _repository.getOrdersPaginated(
359
+ null,
360
+ limit: _pageSize,
361
+ searchQuery: _ref.read(orderSearchQueryProvider),
362
+ );
363
+ final List<OrderModel> items = result['items'];
364
+ state = state.copyWith(
365
+ items: items,
366
+ isLoading: false,
367
+ lastCursor: result['lastCursor'],
368
+ hasMore: items.length >= _pageSize,
369
+ );
370
+ } catch (e) {
371
+ state = state.copyWith(isLoading: false, error: e.toString());
372
+ }
373
+ }
374
+
375
+ Future<void> fetchNextPage() async {
376
+ if (state.isLoading || state.isLoadingMore || !state.hasMore) return;
377
+ state = state.copyWith(isLoadingMore: true);
378
+ try {
379
+ final result = await _repository.getOrdersPaginated(
380
+ state.lastCursor,
381
+ limit: _pageSize,
382
+ searchQuery: _ref.read(orderSearchQueryProvider),
383
+ );
384
+ final List<OrderModel> newItems = result['items'];
385
+ state = state.copyWith(
386
+ items: [...state.items, ...newItems],
387
+ isLoadingMore: false,
388
+ lastCursor: result['lastCursor'],
389
+ hasMore: newItems.length >= _pageSize,
390
+ );
391
+ } catch (e) {
392
+ state = state.copyWith(isLoadingMore: false, error: e.toString());
393
+ }
394
+ }
395
+
396
+ void refresh() => fetchFirstPage();
397
+}
398
+
399
+// 5. List Provider (autoDispose)
400
+final orderListProvider = StateNotifierProvider.autoDispose<
401
+ OrderListNotifier, OrderListState
402
+>((ref) {
403
+ final repository = ref.watch(orderRepositoryProvider);
404
+ ref.watch(orderSearchQueryProvider); // re-create on filter change
405
+ return OrderListNotifier(repository, ref);
406
+});
407
+```
408
+
409
+### Provider Layer: CRUD Controller
410
+
411
+```dart
412
+// (continued in the same providers file)
413
+
414
+class OrderControllerState {
415
+ final bool isLoading;
416
+ final String? error;
417
+ final bool isSuccess;
418
+
419
+ const OrderControllerState({
420
+ this.isLoading = false,
421
+ this.error,
422
+ this.isSuccess = false,
423
+ });
424
+
425
+ OrderControllerState copyWith({ /* all fields */ });
426
+}
427
+
428
+class OrderController extends StateNotifier<OrderControllerState> {
429
+ final OrderRepository _repository;
430
+ final Ref _ref;
431
+
432
+ OrderController(this._repository, this._ref)
433
+ : super(const OrderControllerState());
434
+
435
+ Future<void> addOrder(OrderModel order) async {
436
+ state = const OrderControllerState(isLoading: true);
437
+ try {
438
+ await _repository.addOrder(order);
439
+ state = const OrderControllerState(isSuccess: true);
440
+ _ref.read(orderListProvider.notifier).refresh();
441
+ } catch (e) {
442
+ state = OrderControllerState(error: e.toString());
443
+ }
444
+ }
445
+
446
+ Future<void> deleteOrder(String id) async {
447
+ state = const OrderControllerState(isLoading: true);
448
+ try {
449
+ await _repository.deleteOrder(id);
450
+ state = const OrderControllerState(isSuccess: true);
451
+ _ref.read(orderListProvider.notifier).refresh();
452
+ } catch (e) {
453
+ state = OrderControllerState(error: e.toString());
454
+ }
455
+ }
456
+}
457
+
458
+final orderControllerProvider = StateNotifierProvider.autoDispose<
459
+ OrderController, OrderControllerState
460
+>((ref) {
461
+ final repository = ref.watch(orderRepositoryProvider);
462
+ return OrderController(repository, ref);
463
+});
464
+```
465
+
466
+### UI Layer: ConsumerWidget View
467
+
468
+```dart
469
+// features/orders/views/order_list_page.dart
470
+class OrderListPage extends ConsumerWidget {
471
+ const OrderListPage({super.key});
472
+
473
+ @override
474
+ Widget build(BuildContext context, WidgetRef ref) {
475
+ final listState = ref.watch(orderListProvider);
476
+
477
+ if (listState.isLoading) {
478
+ return const Center(child: CircularProgressIndicator());
479
+ }
480
+ if (listState.error != null) {
481
+ return Center(child: Text('Error: ${listState.error}'));
482
+ }
483
+
484
+ return ListView.builder(
485
+ itemCount: listState.items.length + (listState.hasMore ? 1 : 0),
486
+ itemBuilder: (context, index) {
487
+ if (index == listState.items.length) {
488
+ // Load more trigger
489
+ ref.read(orderListProvider.notifier).fetchNextPage();
490
+ return const Center(child: CircularProgressIndicator());
491
+ }
492
+ final order = listState.items[index];
493
+ return ListTile(
494
+ title: Text(order.customerName),
495
+ subtitle: Text('\$${order.total.toStringAsFixed(2)}'),
496
+ );
497
+ },
498
+ );
499
+ }
500
+}
501
+```
502
+
503
+### Core Layer: Shared Component
504
+
505
+```dart
506
+// core/components/search_field.dart
507
+class AppSearchField extends StatelessWidget {
508
+ const AppSearchField({
509
+ super.key,
510
+ required this.onChanged,
511
+ this.hint = 'Search...',
512
+ });
513
+
514
+ final ValueChanged<String> onChanged;
515
+ final String hint;
516
+
517
+ @override
518
+ Widget build(BuildContext context) {
519
+ return TextField(
520
+ decoration: InputDecoration(
521
+ hintText: hint,
522
+ prefixIcon: const Icon(Icons.search),
523
+ border: const OutlineInputBorder(),
524
+ ),
525
+ onChanged: onChanged,
526
+ );
527
+ }
528
+}
529
+```
.claude/skills/flutter-handling-concurrency/SKILL.md
new
+179
@@ -0,0 +1,179 @@
1
+---
2
+name: "flutter-handling-concurrency"
3
+description: "Executes long-running tasks in background isolates to keep the UI responsive. Use when performing heavy computations or parsing large datasets."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:23:14 GMT"
7
+
8
+---
9
+# Managing Dart Concurrency and Isolates
10
+
11
+## Contents
12
+- [Core Concepts](#core-concepts)
13
+- [Decision Matrix: Async vs. Isolates](#decision-matrix-async-vs-isolates)
14
+- [Workflows](#workflows)
15
+ - [Implementing Standard Asynchronous UI](#implementing-standard-asynchronous-ui)
16
+ - [Offloading Short-Lived Heavy Computation](#offloading-short-lived-heavy-computation)
17
+ - [Establishing Long-Lived Worker Isolates](#establishing-long-lived-worker-isolates)
18
+- [Examples](#examples)
19
+
20
+## Core Concepts
21
+
22
+Dart utilizes a single-threaded execution model driven by an Event Loop (comparable to the iOS main loop). By default, all Flutter application code runs on the Main Isolate.
23
+
24
+* **Asynchronous Operations (`async`/`await`):** Use for non-blocking I/O tasks (network requests, file access). The Event Loop continues processing other events while waiting for the `Future` to complete.
25
+* **Isolates:** Dart's implementation of lightweight threads. Isolates possess their own isolated memory and do not share state. They communicate exclusively via message passing.
26
+* **Main Isolate:** The default thread where UI rendering and event handling occur. Blocking this isolate causes UI freezing (jank).
27
+* **Worker Isolate:** A spawned isolate used to offload CPU-bound tasks (e.g., decoding large JSON blobs) to prevent Main Isolate blockage.
28
+
29
+## Decision Matrix: Async vs. Isolates
30
+
31
+Apply the following conditional logic to determine the correct concurrency approach:
32
+
33
+* **If** the task is I/O bound (e.g., HTTP request, database read) -> **Use `async`/`await`** on the Main Isolate.
34
+* **If** the task is CPU-bound but executes quickly (< 16ms) -> **Use `async`/`await`** on the Main Isolate.
35
+* **If** the task is CPU-bound, takes significant time, and runs once (e.g., parsing a massive JSON payload) -> **Use `Isolate.run()`**.
36
+* **If** the task requires continuous or repeated background processing with multiple messages passed over time -> **Use `Isolate.spawn()` with `ReceivePort` and `SendPort`**.
37
+
38
+## Workflows
39
+
40
+### Implementing Standard Asynchronous UI
41
+
42
+Use this workflow to fetch and display non-blocking asynchronous data.
43
+
44
+**Task Progress:**
45
+- [ ] Mark the data-fetching function with the `async` keyword.
46
+- [ ] Return a `Future<T>` from the function.
47
+- [ ] Use the `await` keyword to yield execution until the operation completes.
48
+- [ ] Wrap the UI component in a `FutureBuilder<T>` (or `StreamBuilder` for streams).
49
+- [ ] Handle `ConnectionState.waiting`, `hasError`, and `hasData` states within the builder.
50
+- [ ] Run validator -> review UI for loading indicators -> fix missing states.
51
+
52
+### Offloading Short-Lived Heavy Computation
53
+
54
+Use this workflow for one-off, CPU-intensive tasks using Dart 2.19+.
55
+
56
+**Task Progress:**
57
+- [ ] Identify the CPU-bound operation blocking the Main Isolate.
58
+- [ ] Extract the computation into a standalone callback function.
59
+- [ ] Ensure the callback function signature accepts exactly one required, unnamed argument (as per specific architectural constraints).
60
+- [ ] Invoke `Isolate.run()` passing the callback.
61
+- [ ] `await` the result of `Isolate.run()` in the Main Isolate.
62
+- [ ] Assign the returned value to the application state.
63
+
64
+### Establishing Long-Lived Worker Isolates
65
+
66
+Use this workflow for persistent background processes requiring continuous bidirectional communication.
67
+
68
+**Task Progress:**
69
+- [ ] Instantiate a `ReceivePort` on the Main Isolate to listen for messages.
70
+- [ ] Spawn the worker isolate using `Isolate.spawn()`, passing the `ReceivePort.sendPort` as the initial message.
71
+- [ ] In the worker isolate, instantiate its own `ReceivePort`.
72
+- [ ] Send the worker's `SendPort` back to the Main Isolate via the initial port.
73
+- [ ] Store the worker's `SendPort` in the Main Isolate for future message dispatching.
74
+- [ ] Implement listeners on both `ReceivePort` instances to handle incoming messages.
75
+- [ ] Run validator -> review memory leaks -> ensure ports are closed when the isolate is no longer needed.
76
+
77
+## Examples
78
+
79
+### Example 1: Asynchronous UI with FutureBuilder
80
+
81
+```dart
82
+// 1. Define the async operation
83
+Future<String> fetchUserData() async {
84
+ await Future.delayed(const Duration(seconds: 2)); // Simulate network I/O
85
+ return "User Data Loaded";
86
+}
87
+
88
+// 2. Consume in the UI
89
+Widget build(BuildContext context) {
90
+ return FutureBuilder<String>(
91
+ future: fetchUserData(),
92
+ builder: (context, snapshot) {
93
+ if (snapshot.connectionState == ConnectionState.waiting) {
94
+ return const CircularProgressIndicator();
95
+ } else if (snapshot.hasError) {
96
+ return Text('Error: ${snapshot.error}');
97
+ } else {
98
+ return Text('Result: ${snapshot.data}');
99
+ }
100
+ },
101
+ );
102
+}
103
+```
104
+
105
+### Example 2: Short-Lived Isolate (`Isolate.run`)
106
+
107
+```dart
108
+import 'dart:isolate';
109
+import 'dart:convert';
110
+
111
+// 1. Define the heavy computation callback
112
+// Note: Adhering to the strict single-argument signature requirement.
113
+List<dynamic> decodeHeavyJson(String jsonString) {
114
+ return jsonDecode(jsonString) as List<dynamic>;
115
+}
116
+
117
+// 2. Offload to a worker isolate
118
+Future<List<dynamic>> processDataInBackground(String rawJson) async {
119
+ // Isolate.run spawns the isolate, runs the computation, returns the value, and exits.
120
+ final result = await Isolate.run(() => decodeHeavyJson(rawJson));
121
+ return result;
122
+}
123
+```
124
+
125
+### Example 3: Long-Lived Isolate (`ReceivePort` / `SendPort`)
126
+
127
+```dart
128
+import 'dart:isolate';
129
+
130
+class WorkerManager {
131
+ late SendPort _workerSendPort;
132
+ final ReceivePort _mainReceivePort = ReceivePort();
133
+ Isolate? _isolate;
134
+
135
+ Future<void> initialize() async {
136
+ // 1. Spawn isolate and pass the Main Isolate's SendPort
137
+ _isolate = await Isolate.spawn(_workerEntry, _mainReceivePort.sendPort);
138
+
139
+ // 2. Listen for messages from the Worker Isolate
140
+ _mainReceivePort.listen((message) {
141
+ if (message is SendPort) {
142
+ // First message is the Worker's SendPort
143
+ _workerSendPort = message;
144
+ _startCommunication();
145
+ } else {
146
+ // Subsequent messages are data payloads
147
+ print('Main Isolate received: $message');
148
+ }
149
+ });
150
+ }
151
+
152
+ void _startCommunication() {
153
+ // Send data to the worker
154
+ _workerSendPort.send("Process this data");
155
+ }
156
+
157
+ // 3. Worker Isolate Entry Point
158
+ static void _workerEntry(SendPort mainSendPort) {
159
+ final workerReceivePort = ReceivePort();
160
+
161
+ // Send the Worker's SendPort back to the Main Isolate
162
+ mainSendPort.send(workerReceivePort.sendPort);
163
+
164
+ // Listen for incoming tasks
165
+ workerReceivePort.listen((message) {
166
+ print('Worker Isolate received: $message');
167
+
168
+ // Perform work and send result back
169
+ final result = "Processed: $message";
170
+ mainSendPort.send(result);
171
+ });
172
+ }
173
+
174
+ void dispose() {
175
+ _mainReceivePort.close();
176
+ _isolate?.kill();
177
+ }
178
+}
179
+```
.claude/skills/flutter-handling-http-and-json/SKILL.md
new
+181
@@ -0,0 +1,181 @@
1
+---
2
+name: "flutter-handling-http-and-json"
3
+description: "Executes HTTP requests and handles JSON serialization in a Flutter app. Use when integrating with REST APIs or parsing structured data from external sources."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:18:44 GMT"
7
+
8
+---
9
+# Handling HTTP and JSON
10
+
11
+## Contents
12
+- [Core Guidelines](#core-guidelines)
13
+- [Workflow: Executing HTTP Operations](#workflow-executing-http-operations)
14
+- [Workflow: Implementing JSON Serialization](#workflow-implementing-json-serialization)
15
+- [Workflow: Parsing Large JSON in the Background](#workflow-parsing-large-json-in-the-background)
16
+- [Examples](#examples)
17
+
18
+## Core Guidelines
19
+
20
+- **Enforce HTTPS:** iOS and Android disable cleartext (HTTP) connections by default. Always use HTTPS endpoints. If HTTP is strictly required for debugging, configure `network_security_config.xml` (Android) and `NSAppTransportSecurity` (iOS).
21
+- **Construct URIs Safely:** Always use `Uri.https(authority, unencodedPath, [queryParameters])` to safely build URLs. This handles encoding and formatting reliably, preventing string concatenation errors.
22
+- **Handle Status Codes:** Always validate the `http.Response.statusCode`. Treat `200` (OK) and `201` (Created) as success. Throw explicit exceptions for other codes (do not return `null`).
23
+- **Prevent UI Jank:** Move expensive JSON parsing operations (taking >16ms) to a background isolate using the `compute()` function.
24
+- **Structured AI Output:** When integrating LLMs, enforce reliable JSON output by specifying a strict JSON schema in the system prompt and setting the response MIME type to `application/json`.
25
+
26
+## Workflow: Executing HTTP Operations
27
+
28
+Use this workflow to implement network requests using the `http` package.
29
+
30
+**Task Progress:**
31
+- [ ] Add the `http` package to `pubspec.yaml`.
32
+- [ ] Configure platform permissions (Internet permission in `AndroidManifest.xml` and macOS `.entitlements`).
33
+- [ ] Construct the target `Uri`.
34
+- [ ] Execute the HTTP method.
35
+- [ ] Validate the response and parse the JSON payload.
36
+
37
+**Conditional Implementation:**
38
+- **If fetching data (GET):** Use `http.get(uri)`.
39
+- **If sending new data (POST):** Use `http.post(uri, headers: {...}, body: jsonEncode(data))`. Ensure `Content-Type` is `application/json; charset=UTF-8`.
40
+- **If updating data (PUT):** Use `http.put(uri, headers: {...}, body: jsonEncode(data))`.
41
+- **If deleting data (DELETE):** Use `http.delete(uri, headers: {...})`.
42
+
43
+**Feedback Loop: Validation & Error Handling**
44
+1. Run the HTTP request.
45
+2. Check `response.statusCode`.
46
+3. If `200` or `201`, call `jsonDecode(response.body)` and map to a Dart object.
47
+4. If any other code, throw an `Exception('Failed to load/update/delete resource')`.
48
+5. Review errors -> fix endpoint, headers, or payload structure.
49
+
50
+## Workflow: Implementing JSON Serialization
51
+
52
+Choose the serialization strategy based on project complexity.
53
+
54
+**Conditional Implementation:**
55
+- **If building a small prototype or simple models:** Use manual serialization with `dart:convert`.
56
+- **If building a production app with complex/nested models:** Use code generation with `json_serializable`.
57
+
58
+### Manual Serialization Setup
59
+**Task Progress:**
60
+- [ ] Import `dart:convert`.
61
+- [ ] Define the Model class with `final` properties.
62
+- [ ] Implement a `factory Model.fromJson(Map<String, dynamic> json)` constructor.
63
+- [ ] Implement a `Map<String, dynamic> toJson()` method.
64
+
65
+### Code Generation Setup (`json_serializable`)
66
+**Task Progress:**
67
+- [ ] Add dependencies: `flutter pub add json_annotation` and `flutter pub add -d build_runner json_serializable`.
68
+- [ ] Import `json_annotation.dart` in the model file.
69
+- [ ] Add the `part 'model_name.g.dart';` directive.
70
+- [ ] Annotate the class with `@JsonSerializable()`. Use `explicitToJson: true` if the class contains nested models.
71
+- [ ] Define the `fromJson` factory and `toJson` method delegating to the generated functions.
72
+- [ ] Run the generator: `dart run build_runner build --delete-conflicting-outputs`.
73
+
74
+## Workflow: Parsing Large JSON in the Background
75
+
76
+Use this workflow to prevent frame drops when parsing large JSON payloads (e.g., lists of 1000+ items).
77
+
78
+**Task Progress:**
79
+- [ ] Create a top-level or static function that takes a `String` (the response body) and returns the parsed Dart object (e.g., `List<Model>`).
80
+- [ ] Inside the function, call `jsonDecode` and map the results to the Model class.
81
+- [ ] In the HTTP fetch method, pass the top-level parsing function and the `response.body` to Flutter's `compute()` function.
82
+
83
+## Examples
84
+
85
+### Example 1: HTTP GET with Manual Serialization
86
+```dart
87
+import 'dart:convert';
88
+import 'package:http/http.dart' as http;
89
+
90
+class Album {
91
+ final int id;
92
+ final String title;
93
+
94
+ const Album({required this.id, required this.title});
95
+
96
+ factory Album.fromJson(Map<String, dynamic> json) {
97
+ return Album(
98
+ id: json['id'] as int,
99
+ title: json['title'] as String,
100
+ );
101
+ }
102
+}
103
+
104
+Future<Album> fetchAlbum() async {
105
+ final uri = Uri.https('jsonplaceholder.typicode.com', '/albums/1');
106
+ final response = await http.get(uri);
107
+
108
+ if (response.statusCode == 200) {
109
+ return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
110
+ } else {
111
+ throw Exception('Failed to load album');
112
+ }
113
+}
114
+```
115
+
116
+### Example 2: HTTP POST Request
117
+```dart
118
+Future<Album> createAlbum(String title) async {
119
+ final uri = Uri.https('jsonplaceholder.typicode.com', '/albums');
120
+ final response = await http.post(
121
+ uri,
122
+ headers: <String, String>{
123
+ 'Content-Type': 'application/json; charset=UTF-8',
124
+ },
125
+ body: jsonEncode(<String, String>{'title': title}),
126
+ );
127
+
128
+ if (response.statusCode == 201) {
129
+ return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
130
+ } else {
131
+ throw Exception('Failed to create album.');
132
+ }
133
+}
134
+```
135
+
136
+### Example 3: Background Parsing with `compute`
137
+```dart
138
+import 'dart:convert';
139
+import 'package:flutter/foundation.dart';
140
+import 'package:http/http.dart' as http;
141
+
142
+// 1. Top-level function for parsing
143
+List<Photo> parsePhotos(String responseBody) {
144
+ final parsed = (jsonDecode(responseBody) as List<Object?>)
145
+ .cast<Map<String, Object?>>();
146
+ return parsed.map<Photo>(Photo.fromJson).toList();
147
+}
148
+
149
+// 2. Fetch function using compute
150
+Future<List<Photo>> fetchPhotos(http.Client client) async {
151
+ final uri = Uri.https('jsonplaceholder.typicode.com', '/photos');
152
+ final response = await client.get(uri);
153
+
154
+ if (response.statusCode == 200) {
155
+ // Run parsePhotos in a separate isolate
156
+ return compute(parsePhotos, response.body);
157
+ } else {
158
+ throw Exception('Failed to load photos');
159
+ }
160
+}
161
+```
162
+
163
+### Example 4: Code Generation (`json_serializable`)
164
+```dart
165
+import 'package:json_annotation/json_annotation.dart';
166
+
167
+part 'user.g.dart';
168
+
169
+@JsonSerializable(explicitToJson: true)
170
+class User {
171
+ final String name;
172
+
173
+ @JsonKey(name: 'registration_date_millis')
174
+ final int registrationDateMillis;
175
+
176
+ User(this.name, this.registrationDateMillis);
177
+
178
+ factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
179
+ Map<String, dynamic> toJson() => _$UserToJson(this);
180
+}
181
+```
.claude/skills/flutter-implementing-navigation-and-routing/SKILL.md
new
+205
@@ -0,0 +1,205 @@
1
+---
2
+name: "flutter-implementing-navigation-and-routing"
3
+description: "Handles routing, navigation, and deep linking in a Flutter application. Use when moving between screens or setting up URL-based navigation."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:30:17 GMT"
7
+
8
+---
9
+# Implementing Navigation and Routing in Flutter
10
+
11
+## Contents
12
+- [Core Concepts](#core-concepts)
13
+- [Implementing Imperative Navigation](#implementing-imperative-navigation)
14
+- [Implementing Declarative Navigation](#implementing-declarative-navigation)
15
+- [Implementing Nested Navigation](#implementing-nested-navigation)
16
+- [Workflows](#workflows)
17
+- [Examples](#examples)
18
+
19
+## Core Concepts
20
+
21
+- **Routes:** In Flutter, screens and pages are referred to as *routes*. A route is simply a widget. This is equivalent to an `Activity` in Android or a `ViewController` in iOS.
22
+- **Navigator vs. Router:**
23
+ - Use `Navigator` (Imperative) for small applications without complex deep linking requirements. It manages a stack of `Route` objects.
24
+ - Use `Router` (Declarative) for applications with advanced navigation, web URL synchronization, and specific deep linking requirements.
25
+- **Deep Linking:** Allows an app to open directly to a specific location based on a URL. Supported on iOS, Android, and Web. Web requires no additional setup.
26
+- **Named Routes:** Avoid using named routes (`MaterialApp.routes` and `Navigator.pushNamed`) for most applications. They have rigid deep linking behavior and do not support the browser forward button. Use a routing package like `go_router` instead.
27
+
28
+## Implementing Imperative Navigation
29
+
30
+Use the `Navigator` widget to push and pop routes using platform-specific transition animations (`MaterialPageRoute` or `CupertinoPageRoute`).
31
+
32
+### Pushing and Popping
33
+- Navigate to a new route using `Navigator.push(context, route)`.
34
+- Return to the previous route using `Navigator.pop(context)`.
35
+- Use `Navigator.pushReplacement()` to replace the current route, or `Navigator.pushAndRemoveUntil()` to clear the stack based on a condition.
36
+
37
+### Passing and Returning Data
38
+- **Sending Data:** Pass data directly into the constructor of the destination widget. Alternatively, pass data via the `settings: RouteSettings(arguments: data)` parameter of the `PageRoute` and extract it using `ModalRoute.of(context)!.settings.arguments`.
39
+- **Returning Data:** Pass the return value to the `pop` method: `Navigator.pop(context, resultData)`. Await the result on the pushing side: `final result = await Navigator.push(...)`.
40
+
41
+## Implementing Declarative Navigation
42
+
43
+For apps requiring deep linking, web URL support, or complex routing, implement the `Router` API via a declarative routing package like `go_router`.
44
+
45
+- Switch from `MaterialApp` to `MaterialApp.router`.
46
+- Define a router configuration that parses route paths and configures the `Navigator` automatically.
47
+- Navigate using package-specific APIs (e.g., `context.go('/path')`).
48
+- **Page-backed vs. Pageless Routes:** Declarative routes are *page-backed* (deep-linkable). Imperative pushes (e.g., dialogs, bottom sheets) are *pageless*. Removing a page-backed route automatically removes all subsequent pageless routes.
49
+
50
+## Implementing Nested Navigation
51
+
52
+Implement nested navigation to manage a sub-flow of screens (e.g., a multi-step setup process or persistent bottom navigation tabs) independently from the top-level global navigator.
53
+
54
+- Instantiate a new `Navigator` widget inside the host widget.
55
+- Assign a `GlobalKey<NavigatorState>` to the nested `Navigator` to control it programmatically.
56
+- Implement the `onGenerateRoute` callback within the nested `Navigator` to resolve sub-routes.
57
+- Intercept hardware back button presses using `PopScope` to prevent the top-level navigator from popping the entire nested flow prematurely.
58
+
59
+## Workflows
60
+
61
+### Workflow: Standard Screen Transition
62
+Copy this checklist to track progress when implementing a basic screen transition:
63
+- [ ] Create the destination widget (Route).
64
+- [ ] Define required data parameters in the destination widget's constructor.
65
+- [ ] Implement `Navigator.push()` in the source widget.
66
+- [ ] Wrap the destination widget in a `MaterialPageRoute` or `CupertinoPageRoute`.
67
+- [ ] Implement `Navigator.pop()` in the destination widget to return.
68
+
69
+### Workflow: Implementing Deep-Linkable Routing
70
+Use this conditional workflow when setting up app-wide routing:
71
+- [ ] **If** the app is simple and requires no deep linking:
72
+ - [ ] Use standard `MaterialApp` and `Navigator.push()`.
73
+- [ ] **If** the app requires deep linking, web support, or complex flows:
74
+ - [ ] Add the `go_router` package.
75
+ - [ ] Change `MaterialApp` to `MaterialApp.router`.
76
+ - [ ] Define the `GoRouter` configuration with all top-level routes.
77
+ - [ ] Replace `Navigator.push()` with `context.go()` or `context.push()`.
78
+
79
+### Workflow: Creating a Nested Navigation Flow
80
+Run this workflow when building a multi-step sub-flow (e.g., IoT device setup):
81
+- [ ] Define string constants for the nested route paths.
82
+- [ ] Create a `GlobalKey<NavigatorState>` in the host widget's state.
83
+- [ ] Return a `Navigator` widget in the host's `build` method, passing the key.
84
+- [ ] Implement `onGenerateRoute` in the nested `Navigator` to map string paths to specific step widgets.
85
+- [ ] Wrap the host `Scaffold` in a `PopScope` to handle back-button interceptions (e.g., prompting "Are you sure you want to exit setup?").
86
+- [ ] Use `navigatorKey.currentState!.pushNamed()` to advance steps within the flow.
87
+
88
+## Examples
89
+
90
+### Example: Passing Data via Constructor (Imperative)
91
+
92
+```dart
93
+// 1. Define the data model
94
+class Todo {
95
+ final String title;
96
+ final String description;
97
+ const Todo(this.title, this.description);
98
+}
99
+
100
+// 2. Source Screen
101
+class TodosScreen extends StatelessWidget {
102
+ final List<Todo> todos;
103
+ const TodosScreen({super.key, required this.todos});
104
+
105
+ @override
106
+ Widget build(BuildContext context) {
107
+ return Scaffold(
108
+ appBar: AppBar(title: const Text('Todos')),
109
+ body: ListView.builder(
110
+ itemCount: todos.length,
111
+ itemBuilder: (context, index) {
112
+ return ListTile(
113
+ title: Text(todos[index].title),
114
+ onTap: () {
115
+ // Push and pass data via constructor
116
+ Navigator.push(
117
+ context,
118
+ MaterialPageRoute(
119
+ builder: (context) => DetailScreen(todo: todos[index]),
120
+ ),
121
+ );
122
+ },
123
+ );
124
+ },
125
+ ),
126
+ );
127
+ }
128
+}
129
+
130
+// 3. Destination Screen
131
+class DetailScreen extends StatelessWidget {
132
+ final Todo todo;
133
+ const DetailScreen({super.key, required this.todo});
134
+
135
+ @override
136
+ Widget build(BuildContext context) {
137
+ return Scaffold(
138
+ appBar: AppBar(title: Text(todo.title)),
139
+ body: Padding(
140
+ padding: const EdgeInsets.all(16),
141
+ child: Text(todo.description),
142
+ ),
143
+ );
144
+ }
145
+}
146
+```
147
+
148
+### Example: Nested Navigation Flow
149
+
150
+```dart
151
+class SetupFlow extends StatefulWidget {
152
+ final String initialRoute;
153
+ const SetupFlow({super.key, required this.initialRoute});
154
+
155
+ @override
156
+ State<SetupFlow> createState() => _SetupFlowState();
157
+}
158
+
159
+class _SetupFlowState extends State<SetupFlow> {
160
+ final _navigatorKey = GlobalKey<NavigatorState>();
161
+
162
+ void _exitSetup() => Navigator.of(context).pop();
163
+
164
+ @override
165
+ Widget build(BuildContext context) {
166
+ return PopScope(
167
+ canPop: false,
168
+ onPopInvokedWithResult: (didPop, _) async {
169
+ if (didPop) return;
170
+ // Intercept back button to prevent accidental exit
171
+ _exitSetup();
172
+ },
173
+ child: Scaffold(
174
+ appBar: AppBar(title: const Text('Setup')),
175
+ body: Navigator(
176
+ key: _navigatorKey,
177
+ initialRoute: widget.initialRoute,
178
+ onGenerateRoute: _onGenerateRoute,
179
+ ),
180
+ ),
181
+ );
182
+ }
183
+
184
+ Route<Widget> _onGenerateRoute(RouteSettings settings) {
185
+ Widget page;
186
+ switch (settings.name) {
187
+ case 'step1':
188
+ page = StepOnePage(
189
+ onComplete: () => _navigatorKey.currentState!.pushNamed('step2'),
190
+ );
191
+ break;
192
+ case 'step2':
193
+ page = StepTwoPage(onComplete: _exitSetup);
194
+ break;
195
+ default:
196
+ throw StateError('Unexpected route name: ${settings.name}!');
197
+ }
198
+
199
+ return MaterialPageRoute(
200
+ builder: (context) => page,
201
+ settings: settings,
202
+ );
203
+ }
204
+}
205
+```
.claude/skills/flutter-improving-accessibility/SKILL.md
new
+110
@@ -0,0 +1,110 @@
1
+---
2
+name: "flutter-improving-accessibility"
3
+description: "Configures a Flutter app to support assistive technologies like Screen Readers. Use when ensuring an application is usable for people with disabilities."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:17:37 GMT"
7
+
8
+---
9
+# Implementing Flutter Accessibility
10
+
11
+## Contents
12
+- [UI Design and Styling](#ui-design-and-styling)
13
+- [Accessibility Widgets](#accessibility-widgets)
14
+- [Web Accessibility](#web-accessibility)
15
+- [Adaptive and Responsive Design](#adaptive-and-responsive-design)
16
+- [Workflows](#workflows)
17
+- [Examples](#examples)
18
+
19
+## UI Design and Styling
20
+Design layouts to accommodate dynamic scaling and high visibility. Flutter automatically calculates font sizes based on OS-level accessibility settings.
21
+
22
+* **Font Scaling:** Ensure layouts provide sufficient room to render all contents when font sizes are increased to their maximum OS settings. Avoid hardcoding fixed heights on text containers.
23
+* **Color Contrast:** Maintain a contrast ratio of at least 4.5:1 for small text and 3.0:1 for large text (18pt+ regular or 14pt+ bold) to meet W3C standards.
24
+* **Tap Targets:** Enforce a minimum tap target size of 48x48 logical pixels to accommodate users with limited dexterity.
25
+
26
+## Accessibility Widgets
27
+Utilize Flutter's catalog of accessibility widgets to manipulate the semantics tree exposed to assistive technologies (like TalkBack or VoiceOver).
28
+
29
+* **`Semantics`**: Use this to annotate the widget tree with a description of the meaning of the widgets. Assign specific roles using the `SemanticsRole` enum (e.g., button, link, heading) when building custom components.
30
+* **`MergeSemantics`**: Wrap composite widgets to merge the semantics of all descendants into a single selectable node for screen readers.
31
+* **`ExcludeSemantics`**: Use this to drop the semantics of all descendants, hiding redundant or purely decorative sub-widgets from accessibility tools.
32
+
33
+## Web Accessibility
34
+Flutter web renders UI on a single canvas, requiring a specialized DOM layer to expose structure to browsers.
35
+
36
+* **Enable Semantics:** Web accessibility is disabled by default for performance. Users can enable it via an invisible button (`aria-label="Enable accessibility"`).
37
+* **Programmatic Enablement:** If building a web-first application requiring default accessibility, force the semantics tree generation at startup.
38
+* **Semantic Roles:** Rely on standard widgets (`TabBar`, `MenuAnchor`, `Table`) for automatic ARIA role mapping. For custom components, explicitly assign `SemanticsRole` values to ensure screen readers interpret the elements correctly.
39
+
40
+## Adaptive and Responsive Design
41
+Differentiate between adaptive and responsive paradigms to build universal applications.
42
+
43
+* **Responsive Design:** Adjust the placement, sizing, and reflowing of design elements to fit the available screen space.
44
+* **Adaptive Design:** Select appropriate layouts (e.g., bottom navigation vs. side panel) and input mechanisms (e.g., touch vs. mouse/keyboard) to make the UI usable within the current device context. Design to the strengths of each form factor.
45
+
46
+## Workflows
47
+
48
+### Task Progress: Accessibility Implementation
49
+Copy this checklist to track accessibility compliance during UI development:
50
+
51
+- [ ] Verify all interactive elements have a minimum tap target of 48x48 pixels.
52
+- [ ] Test layout with maximum OS font size settings to ensure no text clipping or overflow occurs.
53
+- [ ] Validate color contrast ratios (4.5:1 for normal text, 3.0:1 for large text).
54
+- [ ] Wrap custom interactive widgets in `Semantics` and assign the appropriate `SemanticsRole`.
55
+- [ ] Group complex composite widgets using `MergeSemantics` to prevent screen reader fatigue.
56
+- [ ] Hide decorative elements from screen readers using `ExcludeSemantics`.
57
+- [ ] If targeting web, verify ARIA roles are correctly mapped and consider programmatic enablement of the semantics tree.
58
+
59
+### Feedback Loop: Accessibility Validation
60
+Run this loop when finalizing a view or component:
61
+1. **Run validator:** Execute accessibility tests or use OS-level screen readers (VoiceOver/TalkBack) to navigate the view.
62
+2. **Review errors:** Identify unannounced interactive elements, trapped focus, or clipped text.
63
+3. **Fix:** Apply `Semantics`, adjust constraints, or modify colors. Repeat until the screen reader provides a clear, logical traversal of the UI.
64
+
65
+## Examples
66
+
67
+### Programmatic Web Accessibility Enablement
68
+If targeting web and requiring accessibility by default, initialize the semantics binding before running the app.
69
+
70
+```dart
71
+import 'package:flutter/material.dart';
72
+import 'package:flutter/semantics.dart';
73
+import 'package:flutter/foundation.dart';
74
+
75
+void main() {
76
+ if (kIsWeb) {
77
+ SemanticsBinding.instance.ensureSemantics();
78
+ }
79
+ runApp(const MyApp());
80
+}
81
+```
82
+
83
+### Custom Component Semantics
84
+If building a custom widget that acts as a list item, explicitly define its semantic role so assistive technologies and web ARIA mappings interpret it correctly.
85
+
86
+```dart
87
+import 'package:flutter/material.dart';
88
+import 'package:flutter/semantics.dart';
89
+
90
+class CustomListItem extends StatelessWidget {
91
+ final String text;
92
+
93
+ const CustomListItem({super.key, required this.text});
94
+
95
+ @override
96
+ Widget build(BuildContext context) {
97
+ return Semantics(
98
+ role: SemanticsRole.listItem,
99
+ label: text,
100
+ child: Padding(
101
+ padding: const EdgeInsets.all(12.0), // Ensures > 48px tap target if interactive
102
+ child: Text(
103
+ text,
104
+ style: const TextStyle(fontSize: 16), // Ensure contrast ratio > 4.5:1
105
+ ),
106
+ ),
107
+ );
108
+ }
109
+}
110
+```
.claude/skills/flutter-interoperating-with-native-apis/SKILL.md
new
+127
@@ -0,0 +1,127 @@
1
+---
2
+name: "flutter-interoperating-with-native-apis"
3
+description: "Interoperates with native platform APIs on Android, iOS, and the web. Use when accessing device-specific features not available in Dart or calling existing native code."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:21:02 GMT"
7
+
8
+---
9
+# Integrating Platform-Specific Code in Flutter
10
+
11
+## Contents
12
+- [Core Concepts & Terminology](#core-concepts--terminology)
13
+- [Binding to Native C/C++ Code (FFI)](#binding-to-native-cc-code-ffi)
14
+- [Implementing Platform Channels & Pigeon](#implementing-platform-channels--pigeon)
15
+- [Hosting Native Platform Views](#hosting-native-platform-views)
16
+- [Integrating Web Content & Wasm](#integrating-web-content--wasm)
17
+- [Workflows](#workflows)
18
+
19
+## Core Concepts & Terminology
20
+- **FFI (Foreign Function Interface):** The `dart:ffi` library used to bind Dart directly to native C/C++ APIs.
21
+- **Platform Channel:** The asynchronous message-passing system (`MethodChannel`, `BasicMessageChannel`) connecting the Dart client (UI) to the host platform (Kotlin/Java, Swift/Objective-C, C++).
22
+- **Pigeon:** A code-generation tool that creates type-safe Platform Channels.
23
+- **Platform View:** A mechanism to embed native UI components (e.g., Android `View`, iOS `UIView`) directly into the Flutter widget tree.
24
+- **JS Interop:** The modern, Wasm-compatible approach to interacting with JavaScript and DOM APIs using `package:web` and `dart:js_interop`.
25
+
26
+## Binding to Native C/C++ Code (FFI)
27
+Use FFI to execute high-performance native code or utilize existing C/C++ libraries without the overhead of asynchronous Platform Channels.
28
+
29
+### Project Setup
30
+* **If creating a standard C/C++ integration (Recommended since Flutter 3.38):** Use the `package_ffi` template. This utilizes `build.dart` hooks to compile native code, eliminating the need for OS-specific build files (CMake, build.gradle, podspec).
31
+ ```bash
32
+ flutter create --template=package_ffi <package_name>
33
+ ```
34
+* **If requiring access to the Flutter Plugin API or Play Services:** Use the legacy `plugin_ffi` template.
35
+ ```bash
36
+ flutter create --template=plugin_ffi <plugin_name>
37
+ ```
38
+
39
+### Implementation Rules
40
+* **Symbol Visibility:** Always mark C++ symbols with `extern "C"` and prevent linker discarding during link-time optimization (LTO).
41
+ ```cpp
42
+ extern "C" __attribute__((visibility("default"))) __attribute__((used))
43
+ ```
44
+* **Dynamic Library Naming (Apple Platforms):** Ensure your `build.dart` hook produces the exact same filename across all target architectures (e.g., `arm64` vs `x86_64`) and SDKs (`iphoneos` vs `iphonesimulator`). Do not append architecture suffixes to the `.dylib` or `.framework` names.
45
+* **Binding Generation:** Always use `package:ffigen` to generate Dart bindings from your C headers (`.h`). Configure this in `ffigen.yaml`.
46
+
47
+## Implementing Platform Channels & Pigeon
48
+Use Platform Channels when you need to interact with platform-specific APIs (e.g., Battery, Bluetooth, OS-level services) using the platform's native language.
49
+
50
+### Pigeon (Type-Safe Channels)
51
+Always prefer `package:pigeon` over raw `MethodChannel` implementations for complex or frequently used APIs.
52
+1. Define the messaging protocol in a standalone Dart file using Pigeon annotations (`@HostApi()`).
53
+2. Generate the host (Kotlin/Swift/C++) and client (Dart) code.
54
+3. Implement the generated interfaces on the native side.
55
+
56
+### Threading Rules
57
+* **Main Thread Requirement:** Always invoke channel methods destined for Flutter on the platform's main thread (UI thread).
58
+* **Background Execution:** If executing channel handlers on a background thread (Android/iOS), you must use the Task Queue API (`makeBackgroundTaskQueue()`).
59
+* **Isolates:** To use plugins/channels from a Dart background `Isolate`, ensure it is registered using `BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken)`.
60
+
61
+## Hosting Native Platform Views
62
+Use Platform Views to embed native UI components (e.g., Google Maps, native video players) into the Flutter widget tree.
63
+
64
+### Android Platform Views
65
+Evaluate the trade-offs between the two rendering modes and select the appropriate one:
66
+* **If requiring perfect fidelity, accessibility, or SurfaceView support:** Use **Hybrid Composition** (`PlatformViewLink` + `AndroidViewSurface`). This appends the native view to the hierarchy but may reduce Flutter's rendering performance.
67
+* **If prioritizing Flutter rendering performance and transformations:** Use **Texture Layer** (`AndroidView`). This renders the native view into a texture. Note: Quick scrolling may drop frames, and `SurfaceView` is problematic.
68
+
69
+### iOS Platform Views
70
+* iOS exclusively uses Hybrid Composition.
71
+* Implement `FlutterPlatformViewFactory` and `FlutterPlatformView` in Swift or Objective-C.
72
+* Use the `UiKitView` widget on the Dart side.
73
+* *Limitation:* `ShaderMask` and `ColorFiltered` widgets cannot be applied to iOS Platform Views.
74
+
75
+## Integrating Web Content & Wasm
76
+Flutter Web supports compiling to WebAssembly (Wasm) for improved performance and multi-threading.
77
+
78
+### Wasm Compilation
79
+* Compile to Wasm using: `flutter build web --wasm`.
80
+* **Server Configuration:** To enable multi-threading, configure your HTTP server to emit the following headers:
81
+ * `Cross-Origin-Embedder-Policy: credentialless` (or `require-corp`)
82
+ * `Cross-Origin-Opener-Policy: same-origin`
83
+* *Limitation:* WasmGC is not currently supported on iOS browsers (WebKit limitation). Flutter will automatically fall back to JavaScript if WasmGC is unavailable.
84
+
85
+### Web Interop
86
+* **If writing new web-specific code:** Strictly use `package:web` and `dart:js_interop`.
87
+* **Do NOT use:** `dart:html`, `dart:js`, or `package:js`. These are incompatible with Wasm compilation.
88
+* **Embedding HTML:** Use `HtmlElementView.fromTagName` to inject arbitrary HTML elements (like `<video>`) into the Flutter Web DOM.
89
+
90
+---
91
+
92
+## Workflows
93
+
94
+### Workflow: Creating a Native FFI Integration
95
+Use this workflow when binding to a C/C++ library.
96
+
97
+- [ ] **Task Progress:**
98
+ - [ ] 1. Run `flutter create --template=package_ffi <name>`.
99
+ - [ ] 2. Place C/C++ source code in the `src/` directory.
100
+ - [ ] 3. Ensure all exported C++ functions are wrapped in `extern "C"` and visibility attributes.
101
+ - [ ] 4. Configure `ffigen.yaml` to point to your header files.
102
+ - [ ] 5. Run `dart run ffigen` to generate Dart bindings.
103
+ - [ ] 6. Modify `hook/build.dart` if linking against pre-compiled or system libraries.
104
+ - [ ] 7. Run validator -> `flutter test` -> review errors -> fix.
105
+
106
+### Workflow: Implementing a Type-Safe Platform Channel (Pigeon)
107
+Use this workflow when you need to call Kotlin/Swift APIs from Dart.
108
+
109
+- [ ] **Task Progress:**
110
+ - [ ] 1. Add `pigeon` to `dev_dependencies`.
111
+ - [ ] 2. Create `pigeons/messages.dart` and define data classes and `@HostApi()` abstract classes.
112
+ - [ ] 3. Run the Pigeon generator script to output Dart, Kotlin, and Swift files.
113
+ - [ ] 4. **Android:** Implement the generated interface in `MainActivity.kt` or your Plugin class.
114
+ - [ ] 5. **iOS:** Implement the generated protocol in `AppDelegate.swift` or your Plugin class.
115
+ - [ ] 6. **Dart:** Import the generated Dart file and call the API methods.
116
+ - [ ] 7. Run validator -> verify cross-platform compilation -> review errors -> fix.
117
+
118
+### Workflow: Embedding a Native Platform View
119
+Use this workflow when embedding a native UI component (e.g., a native map or camera view).
120
+
121
+- [ ] **Task Progress:**
122
+ - [ ] 1. **Dart:** Create a widget that conditionally returns `AndroidView` (or `PlatformViewLink`) for Android, and `UiKitView` for iOS based on `defaultTargetPlatform`.
123
+ - [ ] 2. **Android:** Create a class implementing `PlatformView` that returns the native Android `View`.
124
+ - [ ] 3. **Android:** Create a `PlatformViewFactory` and register it in `configureFlutterEngine`.
125
+ - [ ] 4. **iOS:** Create a class implementing `FlutterPlatformView` that returns the native `UIView`.
126
+ - [ ] 5. **iOS:** Create a `FlutterPlatformViewFactory` and register it in `application:didFinishLaunchingWithOptions:`.
127
+ - [ ] 6. Run validator -> test on physical Android and iOS devices -> review UI clipping/scrolling issues -> fix.
.claude/skills/flutter-localizing-apps/SKILL.md
new
+204
@@ -0,0 +1,204 @@
1
+---
2
+name: "flutter-localizing-apps"
3
+description: "Configures a Flutter app to support multiple languages and regions. Use when preparing an application for international markets and diverse user locales."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:17:07 GMT"
7
+
8
+---
9
+# Localizing Flutter Applications
10
+
11
+## Contents
12
+- [Core Configuration](#core-configuration)
13
+- [Defining ARB Resources](#defining-arb-resources)
14
+- [App Integration](#app-integration)
15
+- [Advanced Formatting](#advanced-formatting)
16
+- [Workflows](#workflows)
17
+- [Troubleshooting & Gotchas](#troubleshooting--gotchas)
18
+
19
+## Core Configuration
20
+
21
+Configure the project to support code generation for localizations.
22
+
23
+1. Add required dependencies to `pubspec.yaml`:
24
+```yaml
25
+dependencies:
26
+ flutter:
27
+ sdk: flutter
28
+ flutter_localizations:
29
+ sdk: flutter
30
+ intl: any
31
+
32
+flutter:
33
+ generate: true # Required for l10n code generation
34
+```
35
+
36
+2. Create an `l10n.yaml` file in the project root to configure the `gen-l10n` tool:
37
+```yaml
38
+arb-dir: lib/l10n
39
+template-arb-file: app_en.arb
40
+output-localization-file: app_localizations.dart
41
+# Optional: Set to false to generate files into lib/ instead of the synthetic package
42
+# synthetic-package: false
43
+```
44
+
45
+## Defining ARB Resources
46
+
47
+Store localized strings in Application Resource Bundle (`.arb`) files within the configured `arb-dir`.
48
+
49
+Create the template file (e.g., `lib/l10n/app_en.arb`):
50
+```json
51
+{
52
+ "helloWorld": "Hello World!",
53
+ "@helloWorld": {
54
+ "description": "The conventional newborn programmer greeting"
55
+ }
56
+}
57
+```
58
+
59
+Create translation files (e.g., `lib/l10n/app_es.arb`):
60
+```json
61
+{
62
+ "helloWorld": "¡Hola Mundo!"
63
+}
64
+```
65
+
66
+## App Integration
67
+
68
+Initialize the `Localizations` widget by configuring the root `MaterialApp` or `CupertinoApp`.
69
+
70
+```dart
71
+import 'package:flutter_localizations/flutter_localizations.dart';
72
+import 'package:flutter_gen/gen_l10n/app_localizations.dart'; // Adjust import if synthetic-package is false
73
+
74
+return MaterialApp(
75
+ title: 'Localized App',
76
+ localizationsDelegates: const [
77
+ AppLocalizations.delegate,
78
+ GlobalMaterialLocalizations.delegate,
79
+ GlobalWidgetsLocalizations.delegate,
80
+ GlobalCupertinoLocalizations.delegate,
81
+ ],
82
+ supportedLocales: const [
83
+ Locale('en'), // English
84
+ Locale('es'), // Spanish
85
+ ],
86
+ home: const MyHomePage(),
87
+);
88
+```
89
+
90
+Access localized values in the widget tree using the generated `AppLocalizations` class:
91
+```dart
92
+Text(AppLocalizations.of(context)!.helloWorld)
93
+```
94
+
95
+*Note: If using `WidgetsApp` instead of `MaterialApp`, omit `GlobalMaterialLocalizations.delegate`.*
96
+
97
+## Advanced Formatting
98
+
99
+Use placeholders, plurals, and selects for dynamic content. Define parameters in the `@key` metadata.
100
+
101
+### Placeholders
102
+```json
103
+"hello": "Hello {userName}",
104
+"@hello": {
105
+ "description": "A message with a single parameter",
106
+ "placeholders": {
107
+ "userName": {
108
+ "type": "String",
109
+ "example": "Bob"
110
+ }
111
+ }
112
+}
113
+```
114
+
115
+### Plurals
116
+```json
117
+"nWombats": "{count, plural, =0{no wombats} =1{1 wombat} other{{count} wombats}}",
118
+"@nWombats": {
119
+ "placeholders": {
120
+ "count": {
121
+ "type": "num",
122
+ "format": "compact"
123
+ }
124
+ }
125
+}
126
+```
127
+
128
+### Selects (Gender/Enums)
129
+```json
130
+"pronoun": "{gender, select, male{he} female{she} other{they}}",
131
+"@pronoun": {
132
+ "placeholders": {
133
+ "gender": {
134
+ "type": "String"
135
+ }
136
+ }
137
+}
138
+```
139
+
140
+### Dates and Numbers
141
+Use `format` and `optionalParameters` to leverage `intl` formatting.
142
+```json
143
+"dateMessage": "Date: {date}",
144
+"@dateMessage": {
145
+ "placeholders": {
146
+ "date": {
147
+ "type": "DateTime",
148
+ "format": "yMd"
149
+ }
150
+ }
151
+}
152
+```
153
+
154
+## Workflows
155
+
156
+### Task Progress: Adding a New Language
157
+Copy this checklist to track progress when introducing a new locale.
158
+
159
+- [ ] Create a new `.arb` file in the `arb-dir` (e.g., `app_fr.arb`).
160
+- [ ] Translate all keys present in the template `.arb` file.
161
+- [ ] Add the new `Locale` to the `supportedLocales` list in `MaterialApp`.
162
+- [ ] Run validator -> Execute `flutter gen-l10n` to verify ARB syntax and regenerate `AppLocalizations`.
163
+- [ ] Review errors -> Fix any missing placeholders or malformed plural/select statements.
164
+- [ ] If targeting iOS, complete the "Configuring iOS App Bundle" workflow.
165
+
166
+### Task Progress: Configuring iOS App Bundle
167
+Flutter handles runtime localization, but iOS requires bundle-level configuration for the App Store and system settings.
168
+
169
+- [ ] Open `ios/Runner.xcodeproj` in Xcode.
170
+- [ ] Select the `Runner` project in the Project Navigator.
171
+- [ ] Navigate to the `Info` tab.
172
+- [ ] Under the **Localizations** section, click the `+` button.
173
+- [ ] Add the newly supported languages/regions.
174
+- [ ] Run validator -> Build the iOS app to ensure `project.pbxproj` is correctly updated.
175
+
176
+## Troubleshooting & Gotchas
177
+
178
+### Missing Localizations Ancestor
179
+Widgets like `TextField` and `CupertinoTabBar` require a `Localizations` ancestor with specific delegates (`MaterialLocalizations` or `CupertinoLocalizations`).
180
+
181
+**Error:** `No MaterialLocalizations found.` or `CupertinoTabBar requires a Localizations parent...`
182
+**Fix:** Ensure the widget is a descendant of `MaterialApp`/`CupertinoApp`. If building a standalone widget tree (e.g., in tests or a custom `WidgetsApp`), wrap the widget in a `Localizations` widget:
183
+
184
+```dart
185
+Localizations(
186
+ locale: const Locale('en', 'US'),
187
+ delegates: const [
188
+ DefaultWidgetsLocalizations.delegate,
189
+ DefaultMaterialLocalizations.delegate, // Required for TextField
190
+ DefaultCupertinoLocalizations.delegate, // Required for CupertinoTabBar
191
+ ],
192
+ child: child,
193
+)
194
+```
195
+
196
+### Advanced Locale Definition
197
+If supporting languages with multiple scripts (e.g., Chinese), use `Locale.fromSubtags` to explicitly define the `scriptCode` and `countryCode` to prevent Flutter from resolving to an unexpected variant.
198
+
199
+```dart
200
+supportedLocales: const [
201
+ Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans', countryCode: 'CN'),
202
+ Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant', countryCode: 'TW'),
203
+]
204
+```
.claude/skills/flutter-managing-state/SKILL.md
new
+203
@@ -0,0 +1,203 @@
1
+---
2
+name: "flutter-managing-state"
3
+description: "Manages application and ephemeral state in a Flutter app. Use when sharing data between widgets or handling complex UI state transitions."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:18:06 GMT"
7
+
8
+---
9
+# Managing State in Flutter
10
+
11
+## Contents
12
+- [Core Concepts](#core-concepts)
13
+- [Architecture and Data Flow](#architecture-and-data-flow)
14
+- [Workflow: Selecting a State Management Approach](#workflow-selecting-a-state-management-approach)
15
+- [Workflow: Implementing MVVM with Provider](#workflow-implementing-mvvm-with-provider)
16
+- [Examples](#examples)
17
+
18
+## Core Concepts
19
+
20
+Flutter's UI is declarative; it is built to reflect the current state of the app (`UI = f(state)`). When state changes, trigger a rebuild of the UI that depends on that state.
21
+
22
+Distinguish between two primary types of state to determine your management strategy:
23
+* **Ephemeral State (Local State):** State contained neatly within a single widget (e.g., current page in a `PageView`, current selected tab, animation progress). Manage this using a `StatefulWidget` and `setState()`.
24
+* **App State (Shared State):** State shared across multiple parts of the app and maintained between user sessions (e.g., user preferences, login info, shopping cart contents). Manage this using advanced approaches like `InheritedWidget`, the `provider` package, and the MVVM architecture.
25
+
26
+## Architecture and Data Flow
27
+
28
+Implement the **Model-View-ViewModel (MVVM)** design pattern combined with **Unidirectional Data Flow (UDF)** for scalable app state management.
29
+
30
+* **Unidirectional Data Flow (UDF):** Enforce a strict flow where state flows *down* from the data layer, through the logic layer, to the UI layer. Events from user interactions flow *up* from the UI layer, to the logic layer, to the data layer.
31
+* **Single Source of Truth (SSOT):** Ensure data changes always happen in the data layer (Repositories). The SSOT class must be the only class capable of modifying its respective data.
32
+* **Model (Data Layer):** Handle low-level tasks like HTTP requests, data caching, and system resources using Repository classes.
33
+* **ViewModel (Logic Layer):** Manage the UI state. Convert app data from the Model into UI State. Extend `ChangeNotifier` and call `notifyListeners()` to trigger UI rebuilds when data changes.
34
+* **View (UI Layer):** Display the state provided by the ViewModel. Keep views lean; they should contain minimal logic (only routing, animations, or simple UI conditionals).
35
+
36
+## Workflow: Selecting a State Management Approach
37
+
38
+Evaluate the scope of the state to determine the correct implementation strategy.
39
+
40
+* **If managing Ephemeral State (single widget scope):**
41
+ 1. Subclass `StatefulWidget` and `State`.
42
+ 2. Store mutable state as private fields within the `State` class.
43
+ 3. Mutate state exclusively inside a `setState()` callback to mark the widget as dirty and schedule a rebuild.
44
+* **If managing App State (shared across widgets):**
45
+ 1. Implement the MVVM pattern.
46
+ 2. Use the `provider` package (a wrapper around `InheritedWidget`) to inject state into the widget tree.
47
+ 3. Use `ChangeNotifier` to emit state updates.
48
+
49
+## Workflow: Implementing MVVM with Provider
50
+
51
+Follow this sequential workflow to implement app-level state management using MVVM and `provider`.
52
+
53
+**Task Progress:**
54
+- [ ] 1. Define the Model (Repository).
55
+- [ ] 2. Create the ViewModel (`ChangeNotifier`).
56
+- [ ] 3. Inject the ViewModel into the Widget Tree.
57
+- [ ] 4. Consume the State in the View.
58
+- [ ] 5. Validate the implementation.
59
+
60
+### 1. Define the Model (Repository)
61
+Create a repository class to act as the Single Source of Truth (SSOT) for the specific data domain. Handle all external API calls or database queries here.
62
+
63
+### 2. Create the ViewModel (`ChangeNotifier`)
64
+Create a ViewModel class that extends `ChangeNotifier`.
65
+* Pass the Repository into the ViewModel via dependency injection.
66
+* Define properties for the UI state (e.g., `isLoading`, `data`, `errorMessage`).
67
+* Implement methods to handle UI events. Inside these methods, mutate the state and call `notifyListeners()` to trigger UI rebuilds.
68
+
69
+### 3. Inject the ViewModel into the Widget Tree
70
+Use `ChangeNotifierProvider` from the `provider` package to provide the ViewModel to the widget subtree that requires it. Place the provider as low in the widget tree as possible to avoid polluting the scope.
71
+
72
+### 4. Consume the State in the View
73
+Access the ViewModel in your `StatelessWidget` or `StatefulWidget`.
74
+* Use `Consumer<MyViewModel>` to rebuild specific parts of the UI when `notifyListeners()` is called.
75
+* Use `context.read<MyViewModel>()` (or `Provider.of<MyViewModel>(context, listen: false)`) inside event handlers (like `onPressed`) to call ViewModel methods without triggering a rebuild of the calling widget.
76
+
77
+### 5. Validate the implementation
78
+Run the following feedback loop to ensure data flows correctly:
79
+1. Trigger a user action in the View.
80
+2. Verify the ViewModel receives the event and calls the Repository.
81
+3. Verify the Repository updates the SSOT and returns data.
82
+4. Verify the ViewModel updates its state and calls `notifyListeners()`.
83
+5. Verify the View rebuilds with the new state.
84
+*Run validator -> review errors -> fix missing `notifyListeners()` calls or incorrect `Provider` scopes.*
85
+
86
+## Examples
87
+
88
+### Ephemeral State Implementation (`setState`)
89
+
90
+Use this pattern strictly for local, UI-only state.
91
+
92
+```dart
93
+class EphemeralCounter extends StatefulWidget {
94
+ const EphemeralCounter({super.key});
95
+
96
+ @override
97
+ State<EphemeralCounter> createState() => _EphemeralCounterState();
98
+}
99
+
100
+class _EphemeralCounterState extends State<EphemeralCounter> {
101
+ int _counter = 0; // Local state
102
+
103
+ void _increment() {
104
+ setState(() {
105
+ _counter++; // Mutate state and schedule rebuild
106
+ });
107
+ }
108
+
109
+ @override
110
+ Widget build(BuildContext context) {
111
+ return ElevatedButton(
112
+ onPressed: _increment,
113
+ child: Text('Count: $_counter'),
114
+ );
115
+ }
116
+}
117
+```
118
+
119
+### App State Implementation (MVVM + Provider)
120
+
121
+Use this pattern for shared data and complex business logic.
122
+
123
+```dart
124
+// 1. Model (Repository)
125
+class CartRepository {
126
+ Future<void> saveItemToCart(String item) async {
127
+ // Simulate network/database call
128
+ await Future.delayed(const Duration(milliseconds: 500));
129
+ }
130
+}
131
+
132
+// 2. ViewModel (ChangeNotifier)
133
+class CartViewModel extends ChangeNotifier {
134
+ final CartRepository repository;
135
+
136
+ CartViewModel({required this.repository});
137
+
138
+ final List<String> _items = [];
139
+ bool isLoading = false;
140
+ String? errorMessage;
141
+
142
+ List<String> get items => List.unmodifiable(_items);
143
+
144
+ Future<void> addItem(String item) async {
145
+ isLoading = true;
146
+ errorMessage = null;
147
+ notifyListeners(); // Trigger loading UI
148
+
149
+ try {
150
+ await repository.saveItemToCart(item);
151
+ _items.add(item);
152
+ } catch (e) {
153
+ errorMessage = 'Failed to add item';
154
+ } finally {
155
+ isLoading = false;
156
+ notifyListeners(); // Trigger success/error UI
157
+ }
158
+ }
159
+}
160
+
161
+// 3. Injection & 4. View (UI)
162
+class CartApp extends StatelessWidget {
163
+ const CartApp({super.key});
164
+
165
+ @override
166
+ Widget build(BuildContext context) {
167
+ // Inject ViewModel
168
+ return ChangeNotifierProvider(
169
+ create: (_) => CartViewModel(repository: CartRepository()),
170
+ child: const CartScreen(),
171
+ );
172
+ }
173
+}
174
+
175
+class CartScreen extends StatelessWidget {
176
+ const CartScreen({super.key});
177
+
178
+ @override
179
+ Widget build(BuildContext context) {
180
+ return Scaffold(
181
+ body: Consumer<CartViewModel>(
182
+ builder: (context, viewModel, child) {
183
+ if (viewModel.isLoading) {
184
+ return const CircularProgressIndicator();
185
+ }
186
+ if (viewModel.errorMessage != null) {
187
+ return Text(viewModel.errorMessage!);
188
+ }
189
+ return ListView.builder(
190
+ itemCount: viewModel.items.length,
191
+ itemBuilder: (_, index) => Text(viewModel.items[index]),
192
+ );
193
+ },
194
+ ),
195
+ floatingActionButton: FloatingActionButton(
196
+ // Use read() to access methods without listening for rebuilds
197
+ onPressed: () => context.read<CartViewModel>().addItem('New Item'),
198
+ child: const Icon(Icons.add),
199
+ ),
200
+ );
201
+ }
202
+}
203
+```
.claude/skills/flutter-reducing-app-size/SKILL.md
new
+100
@@ -0,0 +1,100 @@
1
+---
2
+name: "flutter-reducing-app-size"
3
+description: "Measures and optimizes the size of Flutter application bundles for deployment. Use when minimizing download size or meeting app store package constraints."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:22:44 GMT"
7
+
8
+---
9
+# Reducing Flutter App Size
10
+
11
+## Contents
12
+- [Core Concepts](#core-concepts)
13
+- [Workflow: Generating Size Analysis Files](#workflow-generating-size-analysis-files)
14
+- [Workflow: Analyzing Size Data in DevTools](#workflow-analyzing-size-data-in-devtools)
15
+- [Workflow: Estimating iOS Download Size](#workflow-estimating-ios-download-size)
16
+- [Workflow: Implementing Size Reduction Strategies](#workflow-implementing-size-reduction-strategies)
17
+- [Examples](#examples)
18
+
19
+## Core Concepts
20
+- **Debug vs. Release:** Never use debug builds to measure app size. Debug builds include VM overhead and lack Ahead-Of-Time (AOT) compilation and tree-shaking.
21
+- **Upload vs. Download Size:** The size of an upload package (APK, AAB, IPA) does not represent the end-user download size. App stores filter redundant native library architectures and asset densities based on the target device.
22
+- **AOT Tree-Shaking:** The Dart AOT compiler automatically removes unused or unreachable code in profile and release modes.
23
+- **Size Analysis JSON:** The `--analyze-size` flag generates a `*-code-size-analysis_*.json` file detailing the byte size of packages, libraries, classes, and functions.
24
+
25
+## Workflow: Generating Size Analysis Files
26
+
27
+Use this workflow to generate the raw data required for size analysis.
28
+
29
+**Task Progress:**
30
+- [ ] Determine the target platform (apk, appbundle, ios, linux, macos, windows).
31
+- [ ] Run the Flutter build command with the `--analyze-size` flag.
32
+- [ ] Locate the generated `*-code-size-analysis_*.json` file in the `build/` directory.
33
+
34
+**Conditional Logic:**
35
+- **If targeting Android:** Run `flutter build apk --analyze-size` or `flutter build appbundle --analyze-size`.
36
+- **If targeting iOS:** Run `flutter build ios --analyze-size`. *Note: This creates a `.app` file useful for relative content sizing, but not for estimating final App Store download size. Use the [Estimating iOS Download Size](#workflow-estimating-ios-download-size) workflow for accurate iOS metrics.*
37
+- **If targeting Desktop:** Run `flutter build [windows|macos|linux] --analyze-size`.
38
+
39
+## Workflow: Analyzing Size Data in DevTools
40
+
41
+Use this workflow to visualize and drill down into the Size Analysis JSON.
42
+
43
+**Task Progress:**
44
+- [ ] Launch DevTools by running `dart devtools` in the terminal.
45
+- [ ] Select "Open app size tool" from the DevTools landing page.
46
+- [ ] Upload the generated `*-code-size-analysis_*.json` file.
47
+- [ ] Inspect the treemap or tree view to identify large packages, libraries, or assets.
48
+- [ ] **Feedback Loop:**
49
+ 1. Identify the largest contributors to app size.
50
+ 2. Determine if the dependency or asset is strictly necessary.
51
+ 3. Remove, replace, or optimize the identified component.
52
+ 4. Regenerate the Size Analysis JSON and compare the new build against the old build using the DevTools "Diff" tab.
53
+
54
+## Workflow: Estimating iOS Download Size
55
+
56
+Use this workflow to get an accurate projection of iOS download and installation sizes across different devices.
57
+
58
+**Task Progress:**
59
+- [ ] Configure the app version and build number in `pubspec.yaml`.
60
+- [ ] Generate an Xcode archive by running `flutter build ipa --export-method development`.
61
+- [ ] Open the generated archive (`build/ios/archive/*.xcarchive`) in Xcode.
62
+- [ ] Click **Distribute App** and select **Development**.
63
+- [ ] In the App Thinning configuration, select **All compatible device variants**.
64
+- [ ] Check the option to **Strip Swift symbols**.
65
+- [ ] Sign and export the IPA.
66
+- [ ] Open the exported directory and review the `App Thinning Size Report.txt` file to evaluate projected sizes per device.
67
+
68
+## Workflow: Implementing Size Reduction Strategies
69
+
70
+Apply these strategies to actively reduce the compiled footprint of the application.
71
+
72
+**Task Progress:**
73
+- [ ] **Split Debug Info:** Strip debug symbols from the compiled binary and store them in separate files.
74
+- [ ] **Remove Unused Resources:** Audit the `pubspec.yaml` and `assets/` directory. Delete any images, fonts, or files not actively referenced in the codebase.
75
+- [ ] **Minimize Library Resources:** Review third-party packages. If a package imports massive resource files (e.g., large icon sets or localization files) but only a fraction is used, consider alternative packages or custom implementations.
76
+- [ ] **Compress Media:** Compress all PNG and JPEG assets using tools like `pngquant`, `imageoptim`, or WebP conversion before bundling them into the app.
77
+
78
+## Examples
79
+
80
+### Generating Size Analysis (Android)
81
+```bash
82
+# Generate the size analysis JSON for an Android App Bundle
83
+flutter build appbundle --analyze-size --target-platform=android-arm64
84
+```
85
+
86
+### Splitting Debug Info (Release Build)
87
+```bash
88
+# Build an APK while stripping debug info to reduce binary size
89
+flutter build apk --obfuscate --split-debug-info=build/app/outputs/symbols
90
+```
91
+
92
+### Reading the iOS App Thinning Size Report
93
+When reviewing `App Thinning Size Report.txt`, look for the specific target device to understand the true impact on the user:
94
+```text
95
+Variant: Runner-7433FC8E-1DF4-4299-A7E8-E00768671BEB.ipa
96
+Supported variant descriptors: [device: iPhone12,1, os-version: 13.0]
97
+App + On Demand Resources size: 5.4 MB compressed, 13.7 MB uncompressed
98
+App size: 5.4 MB compressed, 13.7 MB uncompressed
99
+```
100
+*Interpretation: The end-user download size (compressed) is 5.4 MB, and the on-device footprint (uncompressed) is 13.7 MB.*
.claude/skills/flutter-setting-up-on-linux/SKILL.md
new
+138
@@ -0,0 +1,138 @@
1
+---
2
+name: "flutter-setting-up-on-linux"
3
+description: "Sets up a Linux environment for Flutter development. Use when configuring a Linux machine to run, build, or deploy Flutter applications."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:12:22 GMT"
7
+
8
+---
9
+# Setting Up a Linux Environment for Flutter Development
10
+
11
+## Contents
12
+- [System Dependencies](#system-dependencies)
13
+- [Workflow: Configuring the Linux Toolchain](#workflow-configuring-the-linux-toolchain)
14
+- [Workflow: Validating the Environment](#workflow-validating-the-environment)
15
+- [Workflow: Preparing for Distribution (Snapcraft)](#workflow-preparing-for-distribution-snapcraft)
16
+- [Examples](#examples)
17
+
18
+## System Dependencies
19
+To build and run Flutter applications on a Linux desktop, install the required C/C++ toolchain and system libraries. Flutter relies on `dart:ffi` to interface with Linux system calls and the GTK framework for UI rendering.
20
+
21
+Required packages for Debian/Ubuntu-based distributions:
22
+* **Core Utilities:** `curl`, `git`, `unzip`, `xz-utils`, `zip`
23
+* **Build Tools:** `clang`, `cmake`, `ninja-build`, `pkg-config`
24
+* **Libraries:** `libglu1-mesa`, `libgtk-3-dev`, `libstdc++-12-dev`
25
+
26
+## Workflow: Configuring the Linux Toolchain
27
+
28
+Follow this sequential workflow to prepare the Linux host for Flutter desktop development.
29
+
30
+**Task Progress:**
31
+- [ ] 1. Update package lists and upgrade existing packages.
32
+- [ ] 2. Install core utilities and build dependencies.
33
+- [ ] 3. Configure IDE/Editor with Flutter support.
34
+- [ ] 4. (Conditional) Enable Linux support on ChromeOS.
35
+
36
+**1. Update and Install Dependencies**
37
+Execute the following command to install all required packages on Debian/Ubuntu systems:
38
+```bash
39
+sudo apt-get update -y && sudo apt-get upgrade -y
40
+sudo apt-get install -y curl git unzip xz-utils zip libglu1-mesa clang cmake ninja-build pkg-config libgtk-3-dev libstdc++-12-dev
41
+```
42
+
43
+**2. Conditional: ChromeOS Setup**
44
+* **If developing on a Chromebook:** Turn on Linux support in the ChromeOS settings. Ensure the Linux container is fully updated using the `apt-get` commands above before proceeding.
45
+
46
+**3. IDE Configuration**
47
+Install Visual Studio Code, Android Studio, or an IntelliJ-based IDE. Install the official Dart and Flutter extensions/plugins to enable language server features and debugging capabilities.
48
+
49
+## Workflow: Validating the Environment
50
+
51
+Run this feedback loop to ensure the toolchain is correctly recognized by the Flutter SDK.
52
+
53
+**Task Progress:**
54
+- [ ] 1. Run environment validator.
55
+- [ ] 2. Verify connected Linux devices.
56
+- [ ] 3. Resolve toolchain errors.
57
+
58
+**1. Run Validator**
59
+Execute the Flutter diagnostic tool with verbose output:
60
+```bash
61
+flutter doctor -v
62
+```
63
+
64
+**2. Review and Fix (Feedback Loop)**
65
+* **If errors exist under the "Linux toolchain" section:** Review the missing dependencies, install the flagged packages, and re-run `flutter doctor -v`. Repeat until the Linux toolchain section passes.
66
+
67
+**3. Verify Device Availability**
68
+Ensure the Linux desktop is recognized as a valid deployment target:
69
+```bash
70
+flutter devices
71
+```
72
+*Expected Output:* At least one entry must display with the platform marked as **linux**.
73
+
74
+## Workflow: Preparing for Distribution (Snapcraft)
75
+
76
+When preparing a release build for the Snap Store, configure the Snapcraft build environment.
77
+
78
+**Task Progress:**
79
+- [ ] 1. Install Snapcraft and LXD.
80
+- [ ] 2. Configure LXD.
81
+- [ ] 3. Build the Snap package.
82
+
83
+**1. Install Build Tools**
84
+```bash
85
+sudo snap install snapcraft --classic
86
+sudo snap install lxd
87
+```
88
+
89
+**2. Configure LXD**
90
+Initialize LXD and add the current user to the `lxd` group:
91
+```bash
92
+sudo lxd init
93
+sudo usermod -a -G lxd <your_username>
94
+```
95
+*Note: Log out and log back in to apply the group changes.*
96
+
97
+**3. Build the Snap**
98
+Navigate to the project root containing the `snap/snapcraft.yaml` file and execute the build:
99
+```bash
100
+snapcraft --use-lxd
101
+```
102
+
103
+## Examples
104
+
105
+### Baseline `snapcraft.yaml` Configuration
106
+Use this template for the `<project_root>/snap/snapcraft.yaml` file when packaging a Flutter Linux app for the Snap Store.
107
+
108
+```yaml
109
+name: super-cool-app
110
+version: 0.1.0
111
+summary: Super Cool App
112
+description: Super Cool App that does everything!
113
+
114
+confinement: strict
115
+base: core22
116
+grade: stable
117
+
118
+slots:
119
+ dbus-super-cool-app:
120
+ interface: dbus
121
+ bus: session
122
+ name: org.bar.super_cool_app
123
+
124
+apps:
125
+ super-cool-app:
126
+ command: super_cool_app
127
+ extensions: [gnome]
128
+ plugs:
129
+ - network
130
+ slots:
131
+ - dbus-super-cool-app
132
+
133
+parts:
134
+ super-cool-app:
135
+ source: .
136
+ plugin: flutter
137
+ flutter-target: lib/main.dart
138
+```
.claude/skills/flutter-setting-up-on-macos/SKILL.md
new
+75
@@ -0,0 +1,75 @@
1
+---
2
+name: "flutter-setting-up-on-macos"
3
+description: "Sets up a macOS environment for Flutter development. Use when configuring a macOS machine to run, build, or deploy Flutter applications for iOS or macOS."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:12:50 GMT"
7
+
8
+---
9
+# Setting Up a macOS Environment for Flutter Development
10
+
11
+## Contents
12
+- [Prerequisites](#prerequisites)
13
+- [Tooling Configuration](#tooling-configuration)
14
+- [Workflow: Configuring macOS Tooling](#workflow-configuring-macos-tooling)
15
+- [Workflow: Validating the Environment](#workflow-validating-the-environment)
16
+- [Troubleshooting](#troubleshooting)
17
+
18
+## Prerequisites
19
+Ensure the following baseline requirements are met before configuring the macOS-specific toolchain:
20
+- macOS operating system.
21
+- Flutter SDK installed and added to the system `PATH`.
22
+- Active internet connection for downloading toolchains and dependencies.
23
+
24
+## Tooling Configuration
25
+macOS desktop development requires specific Apple toolchains to compile and debug native Swift and Objective-C code.
26
+- **Xcode**: Required for compiling macOS desktop applications.
27
+- **CocoaPods**: Required for managing native dependencies used by Flutter plugins.
28
+
29
+## Workflow: Configuring macOS Tooling
30
+
31
+Copy and follow this checklist to configure the macOS build environment.
32
+
33
+- [ ] **Install Xcode**: Install the latest version of Xcode from the Mac App Store or the Apple Developer portal.
34
+- [ ] **Configure Command-Line Tools**: Link the Xcode command-line tools to the installed Xcode version. Run the following command in the terminal:
35
+ ```bash
36
+ sudo sh -c 'xcode-select -s /Applications/Xcode.app/Contents/Developer && xcodebuild -runFirstLaunch'
37
+ ```
38
+ *Conditional*: If Xcode is installed in a custom directory, replace `/Applications/Xcode.app` with the correct absolute path.
39
+- [ ] **Accept Xcode Licenses**: Accept the required developer licenses by running:
40
+ ```bash
41
+ sudo xcodebuild -license
42
+ ```
43
+ Read and agree to the prompts.
44
+- [ ] **Install CocoaPods**: Install CocoaPods to handle native macOS plugin dependencies.
45
+ ```bash
46
+ sudo gem install cocoapods
47
+ ```
48
+ *Conditional*: If CocoaPods is already installed, ensure it is updated to the latest version (`sudo gem update cocoapods`).
49
+
50
+## Workflow: Validating the Environment
51
+
52
+Execute this feedback loop to ensure the environment is correctly configured for macOS desktop development.
53
+
54
+- [ ] **Run Validator**: Execute the Flutter diagnostic tool with verbose output:
55
+ ```bash
56
+ flutter doctor -v
57
+ ```
58
+- [ ] **Review Errors**: Check the `Xcode` section in the output.
59
+- [ ] **Fix & Retry**: If errors or missing components are reported under the Xcode section, resolve them according to the output instructions, then re-run `flutter doctor -v` until the Xcode section passes.
60
+- [ ] **Verify Device Availability**: Confirm that Flutter recognizes the macOS desktop as a valid deployment target:
61
+ ```bash
62
+ flutter devices
63
+ ```
64
+ *Success Criteria*: The output must contain at least one entry with `macos` listed as the platform.
65
+
66
+## Troubleshooting
67
+
68
+If the validation workflow fails, apply the following resolutions:
69
+
70
+- **Missing Command-Line Tools**: If `flutter doctor` reports missing tools, ensure the `xcode-select` command was run with `sudo` and points to the correct `.app` directory.
71
+- **CocoaPods Not Found**: If CocoaPods is installed but not detected, verify that your Ruby `gem` binary path is included in your shell's `PATH` environment variable.
72
+- **Device Not Listed**: If `flutter devices` does not list `macos`, ensure desktop support is enabled in your Flutter configuration:
73
+ ```bash
74
+ flutter config --enable-macos-desktop
75
+ ```
.claude/skills/flutter-setting-up-on-windows/SKILL.md
new
+103
@@ -0,0 +1,103 @@
1
+---
2
+name: "flutter-setting-up-on-windows"
3
+description: "Sets up a Windows environment for Flutter development. Use when configuring a Windows machine to run, build, or deploy Flutter applications for Windows desktop or Android."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:13:13 GMT"
7
+
8
+---
9
+# Setting Up Flutter for Windows Development
10
+
11
+## Contents
12
+- [Core Requirements](#core-requirements)
13
+- [Workflow: Installing and Configuring the SDK](#workflow-installing-and-configuring-the-sdk)
14
+- [Workflow: Configuring Tooling and IDEs](#workflow-configuring-tooling-and-ides)
15
+- [Workflow: Configuring Target Platforms](#workflow-configuring-target-platforms)
16
+- [Workflow: Building and Packaging for Windows](#workflow-building-and-packaging-for-windows)
17
+- [Workflow: Generating and Installing Certificates](#workflow-generating-and-installing-certificates)
18
+- [Examples](#examples)
19
+
20
+## Core Requirements
21
+Configure the Windows environment to support both Flutter framework execution and native C/C++ compilation. Differentiate strictly between **Visual Studio** (required for Windows desktop C++ compilation) and **VS Code** (the recommended Dart/Flutter code editor).
22
+
23
+## Workflow: Installing and Configuring the SDK
24
+
25
+Follow this sequential workflow to initialize the Flutter SDK on a Windows machine.
26
+
27
+- [ ] Download the latest stable Flutter SDK for Windows.
28
+- [ ] Extract the SDK to a directory with standard user privileges (e.g., `C:\src\flutter`). Do not install in protected directories like `C:\Program Files\`.
29
+- [ ] Copy the absolute path to the Flutter SDK's `bin` directory.
30
+- [ ] Open Windows Environment Variables settings and append the `bin` directory path to the system or user `PATH` variable.
31
+- [ ] Open a new terminal session to apply the `PATH` changes.
32
+- [ ] **Feedback Loop:** Run validator -> review errors -> fix.
33
+ 1. Execute `flutter doctor -v`.
34
+ 2. Review the output for missing dependencies or path issues.
35
+ 3. Resolve any flagged errors before proceeding to tooling setup.
36
+
37
+## Workflow: Configuring Tooling and IDEs
38
+
39
+- [ ] Install **Visual Studio** (not VS Code).
40
+- [ ] Select and install the **Desktop development with C++** workload during the Visual Studio installation process. This is mandatory for compiling Windows desktop applications.
41
+- [ ] Install your preferred code editor (VS Code, Android Studio, or IntelliJ).
42
+- [ ] Install the official Flutter and Dart extensions/plugins within your chosen editor.
43
+
44
+## Workflow: Configuring Target Platforms
45
+
46
+Apply conditional logic based on the specific platform you are targeting for development.
47
+
48
+**If targeting Windows Desktop:**
49
+- [ ] Ensure the Visual Studio C++ workload is fully updated.
50
+- [ ] Restart your IDE so it detects the Windows desktop device.
51
+- [ ] To disable platforms you do not intend to compile for, execute `flutter config --no-enable-<platform>` (e.g., `flutter config --no-enable-windows-desktop`).
52
+
53
+**If targeting Android on Windows:**
54
+- [ ] **For physical devices:** Enable Developer Options and USB debugging on the device. Install the specific OEM USB drivers for Windows.
55
+- [ ] **For emulators:** Open the Android Virtual Device (AVD) manager. Under "Emulated Performance" -> "Graphics acceleration", select an option specifying "Hardware" to enable hardware acceleration.
56
+- [ ] Verify the device connection by running `flutter devices`.
57
+
58
+## Workflow: Building and Packaging for Windows
59
+
60
+To distribute a Windows desktop application, assemble the compiled executable and its required dependencies into a single distributable archive.
61
+
62
+- [ ] Execute `flutter build windows` to compile the release build.
63
+- [ ] Navigate to `build\windows\runner\Release\`.
64
+- [ ] Create a new staging directory for the distribution zip.
65
+- [ ] Copy the following assets from the `Release` directory into the staging directory:
66
+ - The application executable (`.exe`).
67
+ - All generated `.dll` files.
68
+ - The entire `data` directory.
69
+- [ ] Copy the required Visual C++ redistributables into the staging directory alongside the executable:
70
+ - `msvcp140.dll`
71
+ - `vcruntime140.dll`
72
+ - `vcruntime140_1.dll`
73
+- [ ] Compress the staging directory into a `.zip` file for distribution.
74
+
75
+## Workflow: Generating and Installing Certificates
76
+
77
+If you require a self-signed certificate for MSIX packaging or local testing, use OpenSSL.
78
+
79
+- [ ] Install OpenSSL and add its `bin` directory to your `PATH` environment variable.
80
+- [ ] Generate a private key: `openssl genrsa -out mykeyname.key 2048`
81
+- [ ] Generate a Certificate Signing Request (CSR): `openssl req -new -key mykeyname.key -out mycsrname.csr`
82
+- [ ] Generate the signed certificate (CRT): `openssl x509 -in mycsrname.csr -out mycrtname.crt -req -signkey mykeyname.key -days 10000`
83
+- [ ] Generate the `.pfx` file: `openssl pkcs12 -export -out CERTIFICATE.pfx -inkey mykeyname.key -in mycrtname.crt`
84
+- [ ] Install the `.pfx` certificate on the local Windows machine. Place it in the Certificate Store under **Trusted Root Certification Authorities** prior to installing the application.
85
+
86
+## Examples
87
+
88
+### Windows Distribution Directory Structure
89
+When assembling your Windows build for distribution, ensure the directory structure strictly matches the following layout before zipping:
90
+
91
+```text
92
+Release_Archive/
93
+│ my_flutter_app.exe
94
+│ flutter_windows.dll
95
+│ msvcp140.dll
96
+│ vcruntime140.dll
97
+│ vcruntime140_1.dll
98
+│
99
+└───data/
100
+ │ app.so
101
+ │ icudtl.dat
102
+ │ ...
103
+```
.claude/skills/flutter-testing-apps/SKILL.md
new
+182
@@ -0,0 +1,182 @@
1
+---
2
+name: "flutter-testing-apps"
3
+description: "Implements unit, widget, and integration tests for a Flutter app. Use when ensuring code quality and preventing regressions through automated testing."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:22:10 GMT"
7
+
8
+---
9
+# Testing Flutter Applications
10
+
11
+## Contents
12
+- [Core Testing Strategies](#core-testing-strategies)
13
+- [Architectural Testing Guidelines](#architectural-testing-guidelines)
14
+- [Plugin Testing Guidelines](#plugin-testing-guidelines)
15
+- [Workflows](#workflows)
16
+- [Examples](#examples)
17
+
18
+## Core Testing Strategies
19
+
20
+Balance your testing suite across three main categories to optimize for confidence, maintenance cost, dependencies, and execution speed.
21
+
22
+### Unit Tests
23
+Use unit tests to verify the correctness of a single function, method, or class under various conditions.
24
+- Mock all external dependencies.
25
+- Do not involve disk I/O, screen rendering, or user actions from outside the test process.
26
+- Execute using the `test` or `flutter_test` package.
27
+
28
+### Widget Tests
29
+Use widget tests (component tests) to ensure a single widget's UI looks and interacts as expected.
30
+- Provide the appropriate widget lifecycle context using `WidgetTester`.
31
+- Use `Finder` classes to locate widgets and `Matcher` constants to verify their existence and state.
32
+- Test views and UI interactions without spinning up the full application.
33
+
34
+### Integration Tests
35
+Use integration tests (end-to-end or GUI testing) to validate how individual pieces of an app work together and to capture performance metrics on real devices.
36
+- Add the `integration_test` package as a dependency.
37
+- Run on physical devices, OS emulators, or Firebase Test Lab.
38
+- Prioritize integration tests for routing, dependency injection, and critical user flows.
39
+
40
+## Architectural Testing Guidelines
41
+
42
+Design your application for observability and testability. Ensure all components can be tested both in isolation and together.
43
+
44
+- **ViewModels**: Write unit tests for every ViewModel class. Test the UI logic without relying on Flutter libraries or testing frameworks.
45
+- **Repositories & Services**: Write unit tests for every service and repository. Mock the underlying data sources (e.g., HTTP clients, local databases).
46
+- **Views**: Write widget tests for all views. Pass faked or mocked ViewModels and Repositories into the widget tree to isolate the UI.
47
+- **Fakes over Mocks**: Prefer creating `Fake` implementations of your repositories (e.g., `FakeUserRepository`) over using mocking libraries when testing ViewModels and Views to ensure well-defined inputs and outputs.
48
+
49
+## Plugin Testing Guidelines
50
+
51
+When testing plugins, combine Dart tests with native platform tests to ensure full coverage across the method channel.
52
+
53
+- **Dart Tests**: Use Dart unit and widget tests for the Dart-facing API. Mock the platform channel to validate Dart logic.
54
+- **Native Unit Tests**: Implement native unit tests for isolated platform logic.
55
+ - Android: Configure JUnit tests in `android/src/test/`.
56
+ - iOS/macOS: Configure XCTest tests in `example/ios/RunnerTests/` and `example/macos/RunnerTests/`.
57
+ - Linux/Windows: Configure GoogleTest tests in `linux/test/` and `windows/test/`.
58
+- **Native UI Tests**: Use Espresso (Android) or XCUITest (iOS) if the plugin requires native UI interactions.
59
+- **Integration Tests**: Write at least one integration test for each platform channel call to verify Dart-to-Native communication.
60
+- **End-to-End Fallback**: If integration tests cannot cover a flow (e.g., mocking device state), synthesize calls to the method channel entry point using native unit tests, and test the Dart public API using Dart unit tests.
61
+
62
+## Workflows
63
+
64
+### Workflow: Implementing a Component Test Suite
65
+Copy and track this checklist when implementing tests for a new architectural feature.
66
+
67
+- [ ] **Task Progress**
68
+ - [ ] Create `Fake` implementations for any new Repositories or Services.
69
+ - [ ] Write Unit Tests for the Repository (mocking the API/Database).
70
+ - [ ] Write Unit Tests for the ViewModel (injecting the Fake Repositories).
71
+ - [ ] Write Widget Tests for the View (injecting the ViewModel and Fake Repositories).
72
+ - [ ] Write an Integration Test for the critical path involving this feature.
73
+ - [ ] Run validator -> review coverage -> fix missing edge cases.
74
+
75
+### Workflow: Running Integration Tests
76
+Follow conditional logic based on the target platform when executing integration tests.
77
+
78
+1. **If testing on Mobile (Local)**:
79
+ - Connect the Android/iOS device or emulator.
80
+ - Run: `flutter test integration_test/app_test.dart`
81
+2. **If testing on Web**:
82
+ - Install and launch ChromeDriver: `chromedriver --port=4444`
83
+ - Run: `flutter drive --driver=test_driver/integration_test.dart --target=integration_test/app_test.dart -d chrome`
84
+3. **If testing on Linux (CI System)**:
85
+ - Invoke an X server using `xvfb-run` to provide a display environment.
86
+ - Run: `xvfb-run flutter test integration_test/app_test.dart -d linux`
87
+4. **If testing via Firebase Test Lab**:
88
+ - Build the Android test APKs: `flutter build apk --debug` and `./gradlew app:assembleAndroidTest`
89
+ - Upload the App APK and Test APK to the Firebase Console.
90
+
91
+## Examples
92
+
93
+### Example: ViewModel Unit Test
94
+Demonstrates testing a ViewModel using a Fake Repository.
95
+
96
+```dart
97
+import 'package:flutter_test/flutter_test.dart';
98
+
99
+void main() {
100
+ group('HomeViewModel tests', () {
101
+ test('Load bookings successfully', () {
102
+ // Inject fake dependencies
103
+ final viewModel = HomeViewModel(
104
+ bookingRepository: FakeBookingRepository()..createBooking(kBooking),
105
+ userRepository: FakeUserRepository(),
106
+ );
107
+
108
+ // Verify state
109
+ expect(viewModel.bookings.isNotEmpty, true);
110
+ });
111
+ });
112
+}
113
+```
114
+
115
+### Example: View Widget Test
116
+Demonstrates testing a View by pumping a localized widget tree with fake dependencies.
117
+
118
+```dart
119
+import 'package:flutter/material.dart';
120
+import 'package:flutter_test/flutter_test.dart';
121
+
122
+void main() {
123
+ group('HomeScreen tests', () {
124
+ late HomeViewModel viewModel;
125
+ late FakeBookingRepository bookingRepository;
126
+
127
+ setUp(() {
128
+ bookingRepository = FakeBookingRepository()..createBooking(kBooking);
129
+ viewModel = HomeViewModel(
130
+ bookingRepository: bookingRepository,
131
+ userRepository: FakeUserRepository(),
132
+ );
133
+ });
134
+
135
+ testWidgets('renders bookings list', (WidgetTester tester) async {
136
+ await tester.pumpWidget(
137
+ MaterialApp(
138
+ home: HomeScreen(viewModel: viewModel),
139
+ ),
140
+ );
141
+
142
+ // Verify UI state
143
+ expect(find.byType(ListView), findsOneWidget);
144
+ expect(find.text('Booking 1'), findsOneWidget);
145
+ });
146
+ });
147
+}
148
+```
149
+
150
+### Example: Integration Test
151
+Demonstrates a full end-to-end test using the `integration_test` package.
152
+
153
+```dart
154
+import 'package:flutter/material.dart';
155
+import 'package:flutter_test/flutter_test.dart';
156
+import 'package:integration_test/integration_test.dart';
157
+import 'package:my_app/main.dart';
158
+
159
+void main() {
160
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
161
+
162
+ group('end-to-end test', () {
163
+ testWidgets('tap on the floating action button, verify counter', (tester) async {
164
+ // Load app widget
165
+ await tester.pumpWidget(const MyApp());
166
+
167
+ // Verify initial state
168
+ expect(find.text('0'), findsOneWidget);
169
+
170
+ // Find and tap the button
171
+ final fab = find.byKey(const ValueKey('increment'));
172
+ await tester.tap(fab);
173
+
174
+ // Trigger a frame to allow animations/state to settle
175
+ await tester.pumpAndSettle();
176
+
177
+ // Verify updated state
178
+ expect(find.text('1'), findsOneWidget);
179
+ });
180
+ });
181
+}
182
+```
.claude/skills/flutter-theming-apps/SKILL.md
new
+174
@@ -0,0 +1,174 @@
1
+---
2
+name: "flutter-theming-apps"
3
+description: "Customizes the visual appearance of a Flutter app using the theming system. Use when defining global styles, colors, or typography for an application."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:14:47 GMT"
7
+---
8
+
9
+# Implementing Flutter Theming and Adaptive Design
10
+
11
+## Contents
12
+
13
+- [Core Theming Concepts](#core-theming-concepts)
14
+- [Material 3 Guidelines](#material-3-guidelines)
15
+- [Component Theme Normalization](#component-theme-normalization)
16
+- [Button Styling](#button-styling)
17
+- [Platform Idioms & Adaptive Design](#platform-idioms--adaptive-design)
18
+- [Workflows](#workflows)
19
+- [Examples](#examples)
20
+
21
+## Core Theming Concepts
22
+
23
+Flutter applies styling in a strict hierarchy: styles applied to the specific widget -> themes that override the immediate parent theme -> the main app theme.
24
+
25
+- Define app-wide themes using the `theme` property of `MaterialApp` with a `ThemeData` instance.
26
+- Override themes for specific widget subtrees by wrapping them in a `Theme` widget and using `Theme.of(context).copyWith(...)`.
27
+- **Do not** use deprecated `ThemeData` properties:
28
+ - Replace `accentColor` with `colorScheme.secondary`.
29
+ - Replace `accentTextTheme` with `textTheme` (using `colorScheme.onSecondary` for contrast).
30
+ - Replace `AppBarTheme.color` with `AppBarTheme.backgroundColor`.
31
+
32
+## Material 3 Guidelines
33
+
34
+Material 3 is the default theme as of Flutter 3.16.
35
+
36
+- **Colors:** Generate color schemes using `ColorScheme.fromSeed(seedColor: Colors.blue)`. This ensures accessible contrast ratios.
37
+- **Elevation:** Material 3 uses `ColorScheme.surfaceTint` to indicate elevation instead of just drop shadows. To revert to M2 shadow behavior, set `surfaceTint: Colors.transparent` and define a `shadowColor`.
38
+- **Typography:** Material 3 updates font sizes, weights, and line heights. If text wrapping breaks legacy layouts, adjust `letterSpacing` on the specific `TextStyle`.
39
+- **Modern Components:**
40
+ - Replace `BottomNavigationBar` with `NavigationBar`.
41
+ - Replace `Drawer` with `NavigationDrawer`.
42
+ - Replace `ToggleButtons` with `SegmentedButton`.
43
+ - Use `FilledButton` for a high-emphasis button without the elevation of `ElevatedButton`.
44
+
45
+## Component Theme Normalization
46
+
47
+Component themes in `ThemeData` have been normalized to use `*ThemeData` classes rather than `*Theme` widgets.
48
+
49
+When defining `ThemeData`, strictly use the `*ThemeData` suffix for the following properties:
50
+
51
+- `cardTheme`: Use `CardThemeData` (Not `CardTheme`)
52
+- `dialogTheme`: Use `DialogThemeData` (Not `DialogTheme`)
53
+- `tabBarTheme`: Use `TabBarThemeData` (Not `TabBarTheme`)
54
+- `appBarTheme`: Use `AppBarThemeData` (Not `AppBarTheme`)
55
+- `bottomAppBarTheme`: Use `BottomAppBarThemeData` (Not `BottomAppBarTheme`)
56
+- `inputDecorationTheme`: Use `InputDecorationThemeData` (Not `InputDecorationTheme`)
57
+
58
+## Button Styling
59
+
60
+Legacy button classes (`FlatButton`, `RaisedButton`, `OutlineButton`) are obsolete.
61
+
62
+- Use `TextButton`, `ElevatedButton`, and `OutlinedButton`.
63
+- Configure button appearance using a `ButtonStyle` object.
64
+- For simple overrides based on the theme's color scheme, use the static utility method: `TextButton.styleFrom(foregroundColor: Colors.blue)`.
65
+- For state-dependent styling (hovered, focused, pressed, disabled), use `MaterialStateProperty.resolveWith`.
66
+
67
+## Platform Idioms & Adaptive Design
68
+
69
+When building adaptive apps, respect platform-specific norms to reduce cognitive load and build user trust.
70
+
71
+- **Scrollbars:** Desktop users expect omnipresent scrollbars; mobile users expect them only during scrolling. Toggle `thumbVisibility` on the `Scrollbar` widget based on the platform.
72
+- **Selectable Text:** Web and desktop users expect text to be selectable. Wrap text in `SelectableText` or `SelectableText.rich`.
73
+- **Horizontal Button Order:** Windows places confirmation buttons on the left; macOS/Linux place them on the right. Use a `Row` with `TextDirection.rtl` for Windows and `TextDirection.ltr` for others.
74
+- **Context Menus & Tooltips:** Desktop users expect hover and right-click interactions. Implement `Tooltip` for hover states and use context menu packages for right-click actions.
75
+
76
+## Workflows
77
+
78
+### Workflow: Migrating Legacy Themes to Material 3
79
+
80
+Use this workflow when updating an older Flutter codebase.
81
+
82
+**Task Progress:**
83
+
84
+- [ ] 1. Remove `useMaterial3: false` from `ThemeData` (it is true by default).
85
+- [ ] 2. Replace manual `ColorScheme` definitions with `ColorScheme.fromSeed()`.
86
+- [ ] 3. Run validator -> review errors -> fix: Search for and replace deprecated `accentColor`, `accentColorBrightness`, `accentIconTheme`, and `accentTextTheme`.
87
+- [ ] 4. Run validator -> review errors -> fix: Search for `AppBarTheme(color: ...)` and replace with `backgroundColor`.
88
+- [ ] 5. Update `ThemeData` component properties to use `*ThemeData` classes (e.g., `cardTheme: CardThemeData()`).
89
+- [ ] 6. Replace legacy buttons (`FlatButton` -> `TextButton`, `RaisedButton` -> `ElevatedButton`, `OutlineButton` -> `OutlinedButton`).
90
+- [ ] 7. Replace legacy navigation components (`BottomNavigationBar` -> `NavigationBar`, `Drawer` -> `NavigationDrawer`).
91
+
92
+### Workflow: Implementing Adaptive UI Components
93
+
94
+Use this workflow when building a widget intended for both mobile and desktop/web.
95
+
96
+**Task Progress:**
97
+
98
+- [ ] 1. If displaying a list/grid, wrap it in a `Scrollbar` and set `thumbVisibility: DeviceType.isDesktop`.
99
+- [ ] 2. If displaying read-only data, use `SelectableText` instead of `Text`.
100
+- [ ] 3. If implementing a dialog with action buttons, check the platform. If Windows, set `TextDirection.rtl` on the button `Row`.
101
+- [ ] 4. If implementing interactive elements, wrap them in `Tooltip` widgets to support mouse hover states.
102
+
103
+## Examples
104
+
105
+### Example: Modern Material 3 ThemeData Setup
106
+
107
+```dart
108
+MaterialApp(
109
+ title: 'Adaptive App',
110
+ theme: ThemeData(
111
+ colorScheme: ColorScheme.fromSeed(
112
+ seedColor: Colors.deepPurple,
113
+ brightness: Brightness.light,
114
+ ),
115
+ // Use *ThemeData classes for component normalization
116
+ appBarTheme: const AppBarThemeData(
117
+ backgroundColor: Colors.deepPurple, // Do not use 'color'
118
+ elevation: 0,
119
+ ),
120
+ cardTheme: const CardThemeData(
121
+ elevation: 2,
122
+ ),
123
+ textTheme: const TextTheme(
124
+ bodyMedium: TextStyle(letterSpacing: 0.2),
125
+ ),
126
+ ),
127
+ home: const MyHomePage(),
128
+);
129
+```
130
+
131
+### Example: State-Dependent ButtonStyle
132
+
133
+```dart
134
+TextButton(
135
+ style: ButtonStyle(
136
+ // Default color
137
+ foregroundColor: MaterialStateProperty.all<Color>(Colors.blue),
138
+ // State-dependent overlay color
139
+ overlayColor: MaterialStateProperty.resolveWith<Color?>(
140
+ (Set<MaterialState> states) {
141
+ if (states.contains(MaterialState.hovered)) {
142
+ return Colors.blue.withValues(alpha:0.04);
143
+ }
144
+ if (states.contains(MaterialState.focused) || states.contains(MaterialState.pressed)) {
145
+ return Colors.blue.withValues(alpha:0.12);
146
+ }
147
+ return null; // Defer to the widget's default.
148
+ },
149
+ ),
150
+ ),
151
+ onPressed: () {},
152
+ child: const Text('Adaptive Button'),
153
+)
154
+```
155
+
156
+### Example: Adaptive Dialog Button Order
157
+
158
+```dart
159
+Row(
160
+ // Windows expects confirmation on the left (RTL reverses the standard LTR Row)
161
+ textDirection: Platform.isWindows ? TextDirection.rtl : TextDirection.ltr,
162
+ mainAxisAlignment: MainAxisAlignment.end,
163
+ children: [
164
+ TextButton(
165
+ onPressed: () => Navigator.pop(context, false),
166
+ child: const Text('Cancel'),
167
+ ),
168
+ FilledButton(
169
+ onPressed: () => Navigator.pop(context, true),
170
+ child: const Text('Confirm'),
171
+ ),
172
+ ],
173
+)
174
+```
.claude/skills/flutter-working-with-databases/SKILL.md
new
+200
@@ -0,0 +1,200 @@
1
+---
2
+name: "flutter-working-with-databases"
3
+description: "Manages local data persistence using SQLite or other database solutions. Use when a Flutter app needs to store, query, or synchronize large amounts of structured data on the device."
4
+metadata:
5
+ model: "models/gemini-3.1-pro-preview"
6
+ last_modified: "Thu, 12 Mar 2026 22:19:15 GMT"
7
+
8
+---
9
+# Architecting the Data Layer
10
+
11
+## Contents
12
+- [Core Architecture](#core-architecture)
13
+- [Services Implementation](#services-implementation)
14
+- [Repository Implementation](#repository-implementation)
15
+- [Caching Strategies](#caching-strategies)
16
+- [Workflows](#workflows)
17
+- [Examples](#examples)
18
+
19
+## Core Architecture
20
+
21
+Construct the data layer as the Single Source of Truth (SSOT) for all application data. In an MVVM architecture, the data layer represents the Model. Never update application data outside of this layer.
22
+
23
+Separate the data layer into two distinct components: **Repositories** and **Services**.
24
+
25
+### Repositories
26
+* Act as the SSOT for a specific domain entity.
27
+* Contain business logic for data mutation, polling, caching, and offline synchronization.
28
+* Transform raw data models (API/DB models) into Domain Models (clean data classes containing only what the UI needs).
29
+* Inject Services as private members to prevent the UI layer from bypassing the repository.
30
+
31
+### Services
32
+* Act as stateless wrappers around external data sources (HTTP clients, SQLite databases, platform plugins).
33
+* Perform no business logic or data transformation beyond basic JSON serialization.
34
+* Return raw data models or `Result` wrappers to the calling repository.
35
+
36
+## Services Implementation
37
+
38
+### Database Services (SQLite)
39
+Use databases to persist and query large amounts of structured data locally.
40
+* Add `sqflite` and `path` packages to `pubspec.yaml`.
41
+* Use the `path` package to define the storage location on disk safely across platforms.
42
+* Define table schemas using constants to prevent typos.
43
+* Use `id` as the primary key with `AUTOINCREMENT` to improve query and update times.
44
+* Always use `whereArgs` in SQL queries to prevent SQL injection (e.g., `where: 'id = ?', whereArgs: [id]`).
45
+
46
+### API Services
47
+* Wrap HTTP calls (e.g., using the `http` package) in dedicated client classes.
48
+* Return asynchronous response objects (`Future` or `Stream`).
49
+* Handle raw JSON serialization at this level, returning API-specific data models.
50
+
51
+## Repository Implementation
52
+
53
+### Domain Models
54
+* Define immutable data classes (using `freezed` or `built_value`) for Domain Models.
55
+* Strip out backend-specific fields (like metadata or pagination tokens) that the UI does not need.
56
+
57
+### Offline-First Synchronization
58
+Combine local and remote data sources within the repository to provide seamless offline support.
59
+
60
+* **If reading data:** Return a `Stream` that immediately yields the cached local data from the Database Service, performs the network request via the API Service, updates the Database Service, and then yields the fresh data.
61
+* **If writing data (Online-only):** Attempt the API Service mutation first. If successful, update the Database Service.
62
+* **If writing data (Offline-first):** Update the Database Service immediately. Attempt the API Service mutation. If the network fails, flag the local database record as `synchronized: false` and queue a background synchronization task.
63
+
64
+## Caching Strategies
65
+
66
+Select the appropriate caching strategy based on the data payload:
67
+* **Small Key-Value Data:** Use `shared_preferences` for simple app configurations, theme settings, or user preferences.
68
+* **Large Datasets:** Use relational (`sqflite`, `drift`) or non-relational (`hive_ce`, `isar_community`) on-device databases.
69
+* **Images:** Use the `cached_network_image` package to automatically cache remote images to the device's file system.
70
+* **API Responses:** Implement lightweight remote caching within the API Service or Repository using in-memory maps or temporary file storage.
71
+
72
+## Workflows
73
+
74
+### Workflow: Implementing a New Data Feature
75
+Copy and track this checklist when adding a new data entity to the application.
76
+
77
+- [ ] **Task Progress**
78
+ - [ ] Define the Domain Model (immutable, UI-focused).
79
+ - [ ] Define the API/DB Models (raw data structures).
80
+ - [ ] Create or update the Service(s) to handle raw data fetching/storage.
81
+ - [ ] Create the Repository interface (abstract class).
82
+ - [ ] Implement the Repository, injecting the required Service(s) as private dependencies.
83
+ - [ ] Map raw Service models to the Domain Model within the Repository.
84
+ - [ ] Expose Repository methods to the View Model.
85
+ - [ ] Run validator -> review errors -> fix.
86
+
87
+### Workflow: Implementing SQLite Persistence
88
+Follow this sequence to add a new SQLite table and integrate it.
89
+
90
+- [ ] **Task Progress**
91
+ - [ ] Add `sqflite` and `path` dependencies.
92
+ - [ ] Define table name and column constants.
93
+ - [ ] Update the `onCreate` or `onUpgrade` method in the Database Service to execute the `CREATE TABLE` statement.
94
+ - [ ] Implement `insert`, `query`, `update`, and `delete` methods in the Database Service.
95
+ - [ ] Inject the Database Service into the target Repository.
96
+ - [ ] Ensure the Repository calls `database.open()` before executing queries.
97
+
98
+## Examples
99
+
100
+### Offline-First Repository Implementation
101
+This example demonstrates a Repository coordinating between a Database Service and an API Service using a Stream for offline-first reads.
102
+
103
+```dart
104
+import 'dart:async';
105
+
106
+class TodoRepository {
107
+ TodoRepository({
108
+ required DatabaseService databaseService,
109
+ required ApiClientService apiClientService,
110
+ }) : _databaseService = databaseService,
111
+ _apiClientService = apiClientService;
112
+
113
+ final DatabaseService _databaseService;
114
+ final ApiClientService _apiClientService;
115
+
116
+ /// Yields local data immediately, then fetches remote data, updates local, and yields fresh data.
117
+ Stream<List<Todo>> observeTodos() async* {
118
+ // 1. Yield local cached data first
119
+ final localTodos = await _databaseService.getAllTodos();
120
+ if (localTodos.isNotEmpty) {
121
+ yield localTodos.map((model) => Todo.fromDbModel(model)).toList();
122
+ }
123
+
124
+ try {
125
+ // 2. Fetch fresh data from API
126
+ final remoteTodos = await _apiClientService.fetchTodos();
127
+
128
+ // 3. Update local database
129
+ await _databaseService.replaceAllTodos(remoteTodos);
130
+
131
+ // 4. Yield fresh data
132
+ yield remoteTodos.map((model) => Todo.fromApiModel(model)).toList();
133
+ } on Exception catch (e) {
134
+ // Handle network errors (UI will still have local data)
135
+ // Log error or yield a specific error state if required
136
+ }
137
+ }
138
+
139
+ /// Offline-first write: Save locally, then attempt remote sync.
140
+ Future<void> createTodo(Todo todo) async {
141
+ final dbModel = todo.toDbModel().copyWith(isSynced: false);
142
+
143
+ // 1. Save locally immediately
144
+ await _databaseService.insertTodo(dbModel);
145
+
146
+ try {
147
+ // 2. Attempt remote sync
148
+ final apiModel = await _apiClientService.postTodo(todo.toApiModel());
149
+
150
+ // 3. Mark as synced locally
151
+ await _databaseService.updateTodo(
152
+ dbModel.copyWith(id: apiModel.id, isSynced: true)
153
+ );
154
+ } on Exception catch (_) {
155
+ // Leave as isSynced: false for background sync task to pick up later
156
+ }
157
+ }
158
+}
159
+```
160
+
161
+### SQLite Database Service Implementation
162
+Demonstrates safe query construction using `whereArgs`.
163
+
164
+```dart
165
+class DatabaseService {
166
+ static const String _tableName = 'todos';
167
+ static const String _colId = 'id';
168
+ static const String _colTask = 'task';
169
+ static const String _colIsSynced = 'is_synced';
170
+
171
+ Database? _database;
172
+
173
+ Future<void> open() async {
174
+ if (_database != null) return;
175
+
176
+ final dbPath = join(await getDatabasesPath(), 'app_database.db');
177
+ _database = await openDatabase(
178
+ dbPath,
179
+ version: 1,
180
+ onCreate: (db, version) {
181
+ return db.execute(
182
+ 'CREATE TABLE $_tableName('
183
+ '$_colId INTEGER PRIMARY KEY AUTOINCREMENT, '
184
+ '$_colTask TEXT, '
185
+ '$_colIsSynced INTEGER)'
186
+ );
187
+ },
188
+ );
189
+ }
190
+
191
+ Future<void> updateTodo(TodoDbModel todo) async {
192
+ await _database!.update(
193
+ _tableName,
194
+ todo.toMap(),
195
+ where: '$_colId = ?',
196
+ whereArgs: [todo.id], // Prevents SQL injection
197
+ );
198
+ }
199
+}
200
+```
.claude/skills/riverpod-3-0-migration/SKILL.md
new
+74
@@ -0,0 +1,74 @@
1
+---
2
+name: riverpod-3-0-migration
3
+description: Migrate Riverpod from 2.0 to 3.0; automatic retry, paused listeners, legacy providers import, Ref simplification, FamilyNotifier removal, ProviderException, updateShouldNotify. Use when the user asks about Riverpod 3 migration, upgrading to Riverpod 3, or breaking changes in 3.0.
4
+---
5
+
6
+# Riverpod — Migrating from 2.0 to 3.0
7
+
8
+For a full list of changes and highlights, see the official Riverpod docs (What's new in Riverpod 3.0). This skill summarizes the main migration steps.
9
+
10
+## Automatic retry
11
+
12
+Failing providers are **retried by default** in 3.0. To disable globally:
13
+
14
+```dart
15
+ProviderScope(retry: (retryCount, error) => null, child: MyApp())
16
+// or ProviderContainer(retry: (retryCount, error) => null)
17
+```
18
+
19
+To disable per provider, pass **retry: (retryCount, error) => null** when defining the provider (or `@Riverpod(retry: ...)` with codegen). See riverpod-retry.
20
+
21
+## Out-of-view providers are paused
22
+
23
+Listeners in widgets that are not visible (e.g. off-screen) are **paused** by default. There is no global switch. To keep a subtree from pausing, wrap it in **TickerMode(enabled: true, child: ...)** so the Consumer(s) inside do not follow the pause behavior.
24
+
25
+## Legacy providers (StateProvider, StateNotifierProvider, ChangeNotifierProvider)
26
+
27
+These are **moved to a legacy import**. They are not removed but discouraged. To keep using them:
28
+
29
+```dart
30
+import 'package:flutter_riverpod/legacy.dart';
31
+// or hooks_riverpod/legacy.dart, riverpod/legacy.dart
32
+```
33
+
34
+## Updates use ==
35
+
36
+All providers now use **==** to filter updates (no longer **identical**). **StreamProvider** / **StreamNotifier** values are compared with ==. To customize, override **updateShouldNotify(previous, next)** on the Notifier (e.g. return true to always notify).
37
+
38
+## ProviderObserver API change
39
+
40
+Observer methods now receive a single **ProviderObserverContext** (with container, provider, and optional mutation) instead of separate (provider, value, container) parameters. Update signatures, e.g.:
41
+
42
+- **didAddProvider(ProviderObserverContext context, Object? value)** (and similar for other callbacks).
43
+
44
+## Ref simplified; Ref subclasses removed
45
+
46
+- **Ref** no longer has a type parameter. Use **Ref** everywhere (e.g. replace **MyProviderRef** with **Ref** in codegen).
47
+- **ProviderRef.state**, **Ref.listenSelf**, **FutureProviderRef.future** are **moved onto Notifier/AsyncNotifier**: use **Notifier.state**, **Notifier.listenSelf**, **AsyncNotifier.future** instead of on ref. If you need these, use a class-based Notifier/AsyncNotifier and call them on the notifier.
48
+
49
+## AutoDispose type names removed
50
+
51
+AutoDispose is still supported, but the separate type names (e.g. **AutoDisposeProvider**, **AutoDisposeNotifier**) are unified. Use **Provider**, **Notifier**, etc. with the same behavior. Migration: case-sensitive replace **AutoDispose** with a space (empty string) in type names.
52
+
53
+## Family Notifier types removed
54
+
55
+**FamilyNotifier**, **FamilyAsyncNotifier**, **FamilyStreamNotifier** are removed. Use **Notifier** / **AsyncNotifier** / **StreamNotifier** and pass the parameter via the **constructor** (and store it in a field). Remove the parameter from **build**; **build()** takes no family argument. Example:
56
+
57
+- Before: `FamilyNotifier<int, String>` with `int build(String arg)`.
58
+- After: `Notifier<int>` with constructor `MyNotifier(this.arg)` and `final String arg;`, and **int build()** using **arg**.
59
+
60
+## ProviderException
61
+
62
+When a provider throws, the exception is wrapped in **ProviderException**. If you catch the original type (e.g. **on NotFoundException**), change to **on ProviderException catch (e)** and check **e.exception is NotFoundException**. If you only use **AsyncValue** and **value.hasError** / **value.error**, no change needed.
63
+
64
+## Summary
65
+
66
+- Retry: disable via **retry** on scope/container or provider.
67
+- Pause: use **TickerMode(enabled: true)** where you don’t want pausing.
68
+- Legacy: use **flutter_riverpod/legacy.dart** (or equivalent) for StateProvider / StateNotifierProvider / ChangeNotifierProvider.
69
+- Ref: use plain **Ref**; move **state** / **listenSelf** / **future** to Notifier/AsyncNotifier.
70
+- AutoDispose: remove “AutoDispose” from type names.
71
+- Family notifiers: use Notifier/AsyncNotifier with constructor argument; **build()** has no parameter.
72
+- Errors: catch **ProviderException** and inspect **exception** when you need the original type.
73
+
74
+For version highlights and more detail, see the official Riverpod documentation.
.claude/skills/riverpod-auto-dispose/SKILL.md
new
+65
@@ -0,0 +1,65 @@
1
+---
2
+name: riverpod-auto-dispose
3
+description: Enable automatic disposal of Riverpod providers when they have no listeners; keepAlive, onDispose, invalidate, ref.keepAlive. Use when preventing memory leaks, caching only while used, or cleaning up resources when a provider is no longer needed. Use this skill when the user asks about auto-dispose, keepAlive, or when to dispose Riverpod state.
4
+---
5
+
6
+# Riverpod — Automatic disposal
7
+
8
+## Instructions
9
+
10
+With **automatic disposal** enabled, Riverpod destroys a provider's state when it has no listeners for one frame. This frees memory and stops work (e.g. network requests) when the provider is no longer used.
11
+
12
+### Enabling
13
+
14
+- **Code generation:** Enabled by default. Disable with `@Riverpod(keepAlive: true)`.
15
+- **Manual:** Add `isAutoDispose: true` when creating the provider (e.g. `Provider.autoDispose(...)` or `FutureProvider.autoDispose(...)`).
16
+
17
+```dart
18
+// Codegen: disable auto-dispose
19
+@Riverpod(keepAlive: true)
20
+String helloWorld(Ref ref) => 'Hello world!';
21
+
22
+// Manual: enable
23
+final helloWorldProvider = Provider<String>(
24
+ isAutoDispose: true,
25
+ (ref) => 'Hello world!',
26
+);
27
+```
28
+
29
+When a provider has **parameters** (family), enable auto-dispose to avoid caching every parameter combination forever and causing memory leaks.
30
+
31
+### When disposal runs
32
+
33
+Riverpod counts listeners (from ref.watch, ref.listen, etc.). When the count reaches zero, it waits one frame; if still zero, **ref.onCancel** runs, then the state is destroyed and **ref.onDispose** runs.
34
+
35
+### Reacting to disposal: ref.onDispose
36
+
37
+Register a callback when the state is destroyed (e.g. close a StreamController):
38
+
39
+```dart
40
+final provider = StreamProvider<int>((ref) {
41
+ final controller = StreamController<int>();
42
+ ref.onDispose(controller.close);
43
+ return controller.stream;
44
+});
45
+```
46
+
47
+Do not trigger side effects that modify other providers inside onDispose. You can call onDispose multiple times. Other lifecycles: **ref.onCancel** (last listener removed), **ref.onResume** (new listener added after cancel).
48
+
49
+### Forcing destruction: ref.invalidate
50
+
51
+From a widget or another provider, call **ref.invalidate(provider)** to destroy the current state. If the provider is still listened to, it will recompute; otherwise it stays destroyed until read again.
52
+
53
+```dart
54
+onPressed: () => ref.invalidate(someProvider),
55
+```
56
+
57
+For family providers you can invalidate one parameter combination or all (see riverpod-family and docs).
58
+
59
+### Keeping state alive: ref.keepAlive
60
+
61
+When auto-dispose is enabled, you can call **ref.keepAlive()** to prevent disposal until the next recomputation. Use to keep successful results alive while allowing failed or unused state to be disposed. The return value can be called to revert to automatic disposal. If the provider is recomputed, auto-dispose is re-enabled.
62
+
63
+### Caching for a duration
64
+
65
+Riverpod does not provide a built-in "cache for X seconds". You can implement it with a Timer and ref.keepAlive, or use ref.onCancel/onResume to dispose after a period of no listeners.
.claude/skills/riverpod-cancel/SKILL.md
new
+36
@@ -0,0 +1,36 @@
1
+---
2
+name: riverpod-cancel
3
+description: Cancel or debounce Riverpod async requests with ref.onDispose; cancel when user leaves the page, debounce rapid refreshes. Use when the user asks about cancelling requests, debouncing, or cleaning up when a provider is disposed.
4
+---
5
+
6
+# Riverpod — Cancelling and debouncing requests
7
+
8
+## Instructions
9
+
10
+Use **ref.onDispose** together with **auto-dispose** (or ref.watch) to cancel in-flight requests when the user leaves the page, or to debounce rapid refreshes.
11
+
12
+### Cancelling when leaving the page
13
+
14
+Ensure the provider is **auto-dispose**. When the user navigates away, the provider loses its listeners and is disposed; **ref.onDispose** runs. In that callback, cancel the request (e.g. close the HTTP client used for the request).
15
+
16
+Example pattern: create an `http.Client()` in the provider, pass it to your fetch logic, and call **ref.onDispose(client.close)**. When the provider is disposed, the client closes and the request is cancelled. Exact API depends on your HTTP package.
17
+
18
+### Debouncing refreshes
19
+
20
+If the user triggers refresh multiple times quickly, you can delay the actual request until they stop (e.g. 500ms). Pattern:
21
+
22
+1. In the provider, set a flag (e.g. `didDispose = false`) and **ref.onDispose(() => didDispose = true)**.
23
+2. **await Future.delayed(duration)**.
24
+3. If **didDispose** is true, the user triggered another refresh during the delay; **throw** to abort (Riverpod catches and ignores). Otherwise create the client, register **onDispose(client.close)**, and run the request.
25
+
26
+So: delay first, then create client and run request; disposal during the delay aborts, disposal after creates client cancels the request.
27
+
28
+### Reusable extension
29
+
30
+You can implement an extension on **Ref** that returns a debounced/cancellable client:
31
+
32
+- Use **onDispose** to set a "cancelled" flag.
33
+- Wait for the debounce duration; if disposed, throw.
34
+- Create the client, **onDispose(client.close)**, return the client.
35
+
36
+Use this extension in your activity/provider so all call sites get debounce + cancel behavior. See the official Riverpod "cancel" how-to for a full extension example.
.claude/skills/riverpod-codegen-and-hooks/SKILL.md
new
+105
@@ -0,0 +1,105 @@
1
+---
2
+name: riverpod-codegen-and-hooks
3
+description: Use Riverpod code generation (@riverpod, riverpod_generator) and hooks (hooks_riverpod, HookConsumerWidget, flutter_hooks with Riverpod). Use when the user asks about @riverpod, code generation, riverpod_generator, when to use codegen, or using flutter_hooks with Riverpod (HookConsumerWidget, HookConsumer).
4
+---
5
+
6
+# Riverpod — Code generation and hooks
7
+
8
+## Code generation
9
+
10
+Code generation is **optional** in Riverpod. It changes the syntax for defining providers: you write an annotated function or class and the generator produces the provider. Use it if you already use code generation (e.g. Freezed, json_serializable); otherwise the extra build step may not be worth it. See riverpod-getting-started for setup (build_runner, riverpod_generator).
11
+
12
+### Benefits
13
+
14
+- Clearer syntax: no manual provider type (Provider vs FutureProvider etc.); Riverpod infers it.
15
+- Parameters: any parameters (named, optional, defaults) instead of a single family parameter.
16
+- Stateful hot-reload for Riverpod code.
17
+- Better debugging via generated metadata.
18
+
19
+### Syntax
20
+
21
+**Functional provider** (sync):
22
+
23
+```dart
24
+@riverpod
25
+String example(Ref ref) {
26
+ return 'foo';
27
+}
28
+```
29
+
30
+**Class-based provider** (sync, with methods for side effects):
31
+
32
+```dart
33
+@riverpod
34
+class Example extends _$Example {
35
+ @override
36
+ String build() => 'foo';
37
+ // Add methods to mutate state
38
+}
39
+```
40
+
41
+**Async:** Use `Future`/`FutureOr`/`Stream`; async functions get AsyncValue and loading/error handling automatically. **Auto-dispose:** On by default with codegen. Disable with `@Riverpod(keepAlive: true)`.
42
+
43
+```dart
44
+@riverpod
45
+String example1(Ref ref) => 'foo';
46
+
47
+@Riverpod(keepAlive: true)
48
+String example2(Ref ref) => 'foo';
49
+```
50
+
51
+**Parameters:** Add parameters to the function (consistent == required):
52
+
53
+```dart
54
+@riverpod
55
+Future<User> fetchUser(Ref ref, {required int userId}) async {
56
+ final json = await http.get('api/user/$userId');
57
+ return User.fromJson(json);
58
+}
59
+```
60
+
61
+Manual equivalent would be `FutureProvider.autoDispose.family<User, int>(...)`. See riverpod-providers and riverpod-family for concepts.
62
+
63
+---
64
+
65
+## Hooks
66
+
67
+**Hooks** come from [flutter_hooks] (separate from Riverpod). They are for **local** widget state (e.g. TextEditingController, AnimationController) and can replace StatefulWidget or builder nesting. Use them only if you want hooks; they are not required for Riverpod. Newcomers should avoid hooks at first.
68
+
69
+### Using hooks and Riverpod together
70
+
71
+You need both **flutter_hooks** and **hooks_riverpod**. Then either:
72
+
73
+**Option 1: HookConsumerWidget** — One base class that supports both hooks and ref:
74
+
75
+```dart
76
+class Example extends HookConsumerWidget {
77
+ @override
78
+ Widget build(BuildContext context, WidgetRef ref) {
79
+ final counter = useState(0);
80
+ final value = ref.watch(myProvider);
81
+ return Text('Hello $counter $value');
82
+ }
83
+}
84
+```
85
+
86
+**Option 2: HookConsumer** — Builder that combines HookBuilder + Consumer (works with flutter_riverpod only if you nest them; hooks_riverpod provides HookConsumer):
87
+
88
+```dart
89
+return HookConsumer(
90
+ builder: (context, ref, child) {
91
+ final counter = useState(0);
92
+ final value = ref.watch(myProvider);
93
+ return Text('Hello $counter $value');
94
+ },
95
+);
96
+```
97
+
98
+**Option 3:** Nest `Consumer` and `HookBuilder` (no hooks_riverpod needed).
99
+
100
+### Rules of hooks
101
+
102
+- Use hooks only in the **build** method of a widget that extends **HookWidget** (or HookConsumerWidget).
103
+- Do not use hooks conditionally or in loops; call order must be stable.
104
+
105
+See riverpod-consumers for Ref in widgets and the official docs for full codegen/hooks details.
.claude/skills/riverpod-consumers/SKILL.md
new
+71
@@ -0,0 +1,71 @@
1
+---
2
+name: riverpod-consumers
3
+description: Use Riverpod Consumer, ConsumerWidget, and ConsumerStatefulWidget to read and watch providers in widgets; WidgetRef, builder ref parameter. Use when building widgets that need to access Riverpod providers, ref.watch or ref.read in the UI, or converting StatelessWidget to ConsumerWidget. Prefer this skill when the user asks how to use providers in Flutter widgets or why ConsumerWidget is required.
4
+---
5
+
6
+# Riverpod — Consumers
7
+
8
+## Instructions
9
+
10
+Consumers are widgets that give you a **Ref** (here, **WidgetRef**) so you can read and listen to providers. Without a Consumer, widgets cannot access the provider tree.
11
+
12
+### Consumer (builder)
13
+
14
+Use `Consumer` when you want to keep extending `StatelessWidget` or `StatefulWidget`. The builder callback receives `(context, ref, child)`.
15
+
16
+```dart
17
+class MyWidget extends StatelessWidget {
18
+ @override
19
+ Widget build(BuildContext context) {
20
+ return Consumer(
21
+ builder: (context, ref, _) {
22
+ final value = ref.watch(myProvider);
23
+ return Text(value.toString());
24
+ },
25
+ );
26
+ }
27
+}
28
+```
29
+
30
+### ConsumerWidget
31
+
32
+Subclass `ConsumerWidget` instead of `StatelessWidget`. The `build` method receives `(BuildContext context, WidgetRef ref)`.
33
+
34
+```dart
35
+class MyWidget extends ConsumerWidget {
36
+ @override
37
+ Widget build(BuildContext context, WidgetRef ref) {
38
+ final value = ref.watch(myProvider);
39
+ return Text(value.toString());
40
+ }
41
+}
42
+```
43
+
44
+### ConsumerStatefulWidget + ConsumerState
45
+
46
+When you need a `State` (e.g. for lifecycle or local state), use `ConsumerStatefulWidget` and `ConsumerState`. The state object has a `ref` property.
47
+
48
+```dart
49
+class MyWidget extends ConsumerStatefulWidget {
50
+ @override
51
+ ConsumerState<MyWidget> createState() => _MyWidgetState();
52
+}
53
+
54
+class _MyWidgetState extends ConsumerState<MyWidget> {
55
+ @override
56
+ Widget build(BuildContext context) {
57
+ final value = ref.watch(myProvider);
58
+ return Text(value.toString());
59
+ }
60
+}
61
+```
62
+
63
+### Which to use
64
+
65
+- **Consumer** — Use for everything if you prefer not to change your widget base class; slightly more verbose.
66
+- **ConsumerWidget** — Recommended when you don't need State; one less nesting level.
67
+- **ConsumerStatefulWidget** — Use when you need State (e.g. TabController, animations).
68
+
69
+### Why not StatelessWidget + context.watch?
70
+
71
+Riverpod does not use `BuildContext` to watch providers because that would break **auto-dispose** and other features that rely on knowing when a widget stops listening. Ref-based consumers allow reliable disposal and correct behavior. The hooks_riverpod package also offers HookConsumerWidget etc. for use with flutter_hooks. Enable riverpod_lint for IDE refactors (e.g. "Convert to ConsumerWidget").
.claude/skills/riverpod-containers/SKILL.md
new
+66
@@ -0,0 +1,66 @@
1
+---
2
+name: riverpod-containers
3
+description: Use ProviderScope in Flutter and ProviderContainer in Dart for Riverpod; where provider state is stored, overrides, observers, testing with ProviderContainer.test. Use when setting up Riverpod in Flutter or pure Dart, understanding where state lives, or writing tests with a fresh container. Use this skill when the user asks about ProviderScope, ProviderContainer, or where Riverpod stores state.
4
+---
5
+
6
+# Riverpod — Containers / Scopes
7
+
8
+## Instructions
9
+
10
+Providers themselves hold **no state**. State is stored in a **ProviderContainer**. In Flutter you use the **ProviderScope** widget, which creates and exposes a container to the widget tree.
11
+
12
+### Flutter: ProviderScope
13
+
14
+Wrap your app at the root so widgets can read providers:
15
+
16
+```dart
17
+void main() {
18
+ runApp(
19
+ ProviderScope(
20
+ child: MyApp(),
21
+ ),
22
+ );
23
+}
24
+```
25
+
26
+After that, use Consumer / ConsumerWidget / ConsumerStatefulWidget to get a `ref` and call `ref.watch` / `ref.read`. You can also pass `overrides` or `observers` to ProviderScope.
27
+
28
+### Pure Dart: ProviderContainer
29
+
30
+In command-line or server-side Dart, create a container, use it, then dispose it:
31
+
32
+```dart
33
+void main() {
34
+ final container = ProviderContainer();
35
+ try {
36
+ final sub = container.listen(counterProvider, (previous, next) {
37
+ print('Counter changed from $previous to $next');
38
+ });
39
+ print('Counter starts at ${sub.read()}');
40
+ } finally {
41
+ container.dispose();
42
+ }
43
+}
44
+```
45
+
46
+### Why state lives in a container
47
+
48
+- **Separation of concerns** — Only the provider (and ref) can change its state; the UI typically calls notifier methods.
49
+- **Testing** — Each test can create a new container (e.g. `ProviderContainer.test()`) for a fresh state; no shared global state.
50
+- **Configuration** — Overrides and observers are set on the container/scope. See riverpod-overrides and riverpod-observers.
51
+- **Scoping** — The same provider can resolve to different state in different parts of the tree (advanced). See riverpod-scoping.
52
+
53
+### Testing
54
+
55
+Do not use `ProviderContainer()` directly in tests. Use **ProviderContainer.test()** so the container is disposed when the test ends:
56
+
57
+```dart
58
+test('Counter starts at 0 and can be incremented', () {
59
+ final container = ProviderContainer.test();
60
+ expect(container.read(counterProvider), 0);
61
+ container.read(counterProvider.notifier).increment();
62
+ expect(container.read(counterProvider), 1);
63
+});
64
+```
65
+
66
+In widget tests, wrap the widget in `ProviderScope` and use `tester.container()` to get the container if needed (see riverpod-testing).
.claude/skills/riverpod-eager-initialization/SKILL.md
new
+46
@@ -0,0 +1,46 @@
1
+---
2
+name: riverpod-eager-initialization
3
+description: Eagerly initialize Riverpod providers at app startup by watching them in a root Consumer; handle loading/error in the initializer, AsyncValue.requireValue. Use when a provider must be ready before the rest of the app is used. Use this skill when the user asks about eager initialization or preloading providers.
4
+---
5
+
6
+# Riverpod — Eager initialization
7
+
8
+## Instructions
9
+
10
+Providers are **lazy** by default: they initialize on first use. There is no built-in "eager" flag (for tree-shaking). To force early initialization, **watch** the provider at the root of your app so it stays alive and runs immediately.
11
+
12
+### Approach
13
+
14
+Place a **Consumer** (or ConsumerWidget) directly under **ProviderScope** that only watches the providers you want to initialize and returns your real app as **child**:
15
+
16
+```dart
17
+void main() {
18
+ runApp(ProviderScope(child: MyApp()));
19
+}
20
+
21
+class MyApp extends StatelessWidget {
22
+ @override
23
+ Widget build(BuildContext context) {
24
+ return const _EagerInitialization(child: MaterialApp());
25
+ }
26
+}
27
+
28
+class _EagerInitialization extends ConsumerWidget {
29
+ const _EagerInitialization({required this.child});
30
+ final Widget child;
31
+
32
+ @override
33
+ Widget build(BuildContext context, WidgetRef ref) {
34
+ ref.watch(myProvider); // Eagerly initialize
35
+ return child;
36
+ }
37
+}
38
+```
39
+
40
+Only the initializer widget rebuilds when the provider changes; **child** is unchanged so Flutter does not rebuild the rest of the tree unless something else is listening.
41
+
42
+### Async: loading and error
43
+
44
+If the provider is async, handle loading/error in the initializer (e.g. show CircularProgressIndicator or error until ready, then return child). Other widgets can use **AsyncValue.requireValue** to read the data without pattern matching; if they run before the value is ready, it throws with a clear message.
45
+
46
+Put the initializer in a shared widget (e.g. MyApp) so tests can reuse the same setup by using that widget.
.claude/skills/riverpod-family/SKILL.md
new
+66
@@ -0,0 +1,66 @@
1
+---
2
+name: riverpod-family
3
+description: Use Riverpod family providers to pass parameters and cache per parameter; FutureProvider.family, NotifierProvider.family, autoDispose with family, overriding in tests. Use when fetching data by ID, pagination, or any provider that depends on a parameter. Use this skill when the user asks about family, provider parameters, or caching by ID.
4
+---
5
+
6
+# Riverpod — Family
7
+
8
+## Instructions
9
+
10
+**Family** lets a single provider have multiple independent states, one per parameter combination (like a Map from parameter to state). Use it for API calls that depend on an ID, query, or page number.
11
+
12
+### Creating a family (functional)
13
+
14
+Add `.family` and an extra type parameter for the argument. The provider function receives `(ref, param)`:
15
+
16
+```dart
17
+final userProvider = FutureProvider.autoDispose.family<User, String>((ref, id) async {
18
+ final dio = Dio();
19
+ final response = await dio.get('https://api.example.com/users/$id');
20
+ return User.fromJson(response.data);
21
+});
22
+```
23
+
24
+With code generation, add parameters to the function; the generated provider accepts arguments: `userProvider(id)`.
25
+
26
+### Creating a family (Notifier)
27
+
28
+Use `NotifierProvider.family` / `AsyncNotifierProvider.family` (and `.autoDispose.family`). The Notifier must have a constructor that stores the parameter:
29
+
30
+```dart
31
+final userProvider = AsyncNotifierProvider.autoDispose.family<UserNotifier, User, String>(
32
+ UserNotifier.new,
33
+);
34
+
35
+class UserNotifier extends AsyncNotifier<User> {
36
+ UserNotifier(this.id);
37
+ final String id;
38
+
39
+ @override
40
+ Future<User> build() async {
41
+ final dio = Dio();
42
+ final response = await dio.get('https://api.example.com/users/$id');
43
+ return User.fromJson(response.data);
44
+ }
45
+}
46
+```
47
+
48
+### Using a family
49
+
50
+Pass the parameter when watching or reading:
51
+
52
+```dart
53
+final user = ref.watch(userProvider('123'));
54
+final other = ref.watch(userProvider('456')); // independent state
55
+```
56
+
57
+Parameters must have consistent **==** and **hashCode**. Do not use mutable or list/map literals as parameters (e.g. `ref.watch(myProvider([1,2,3]))` is wrong because `[1,2,3] != [1,2,3]`). Prefer primitives or custom classes with ==/hashCode. Enable riverpod_lint provider_parameters rule to catch mistakes.
58
+
59
+### Auto-dispose with family
60
+
61
+Enable auto-dispose when using family so that when the parameter goes out of use (e.g. user navigates away), that parameter's state can be disposed and you avoid unbounded cache growth.
62
+
63
+### Overriding in tests
64
+
65
+- Override a single parameter: pass the same parameter to the override.
66
+- Override all parameters: override with a provider that ignores the parameter or returns a fixed value. See riverpod-overrides and the docs for exact syntax.
.claude/skills/riverpod-faq-and-practices/SKILL.md
new
+58
@@ -0,0 +1,58 @@
1
+---
2
+name: riverpod-faq-and-practices
3
+description: Answers Riverpod FAQ (ref.refresh vs invalidate, ConsumerWidget vs StatelessWidget, Ref vs WidgetRef, reset all providers, ref after unmount) and do/don't best practices (avoid init in widgets, avoid ephemeral state in providers, avoid side effects in provider init, static providers, riverpod_lint). Use when the user asks Riverpod FAQ, best practices, or do/don't guidelines.
4
+---
5
+
6
+# Riverpod — FAQ and best practices
7
+
8
+## FAQ
9
+
10
+### ref.refresh vs ref.invalidate
11
+
12
+**ref.refresh** = **ref.invalidate** + **ref.read**: it invalidates the provider and returns the new value. Use **invalidate** when you don't need the new value (e.g. "recompute when the user pulls to refresh"). Use **refresh** when you need the result right away. After invalidate alone, recomputation can happen at the next frame or when the provider is next read; refresh forces immediate recomputation by reading.
13
+
14
+### Why no shared interface between Ref and WidgetRef?
15
+
16
+Ref (in providers) and WidgetRef (in widgets) are kept separate on purpose so you don't write code that conditionally depends on both; they have subtle differences and mixing would be error-prone. Prefer **Ref**: put logic in a Notifier and call **ref.read(notifierProvider.notifier).yourMethod()** from the UI; the method uses the Notifier's Ref.
17
+
18
+### Why ConsumerWidget instead of StatelessWidget?
19
+
20
+InheritedWidget (and thus BuildContext) cannot support "on change" listeners like **ref.listen**, cannot know when widgets stop listening (needed for auto-dispose and family), and has lifecycle quirks (e.g. with GlobalKeys). Riverpod needs a Ref that isn't tied to BuildContext for these features. Hence ConsumerWidget (and Consumer) provide a Ref.
21
+
22
+### Why doesn't hooks_riverpod export flutter_hooks?
23
+
24
+So each package can be versioned independently; a breaking change in one doesn't force the other to break.
25
+
26
+### Reset all providers at once?
27
+
28
+There is no API to reset all providers; it's considered an anti-pattern. For "logout and clear state", have providers that depend on the current user **ref.watch** a user provider; when the user logs out, that provider changes and dependents recompute or clear. Only user-dependent state resets; the rest stays.
29
+
30
+### "Using ref when a widget is about to or has been unmounted is unsafe"
31
+
32
+This (or "No ProviderScope found") happens when **ref** is used after an **await** in a widget that may have been disposed. After any **await**, check **if (!context.mounted) return;** before using **ref** (same pattern as with BuildContext).
33
+
34
+---
35
+
36
+## Do / Don't
37
+
38
+### AVOID initializing providers in a widget
39
+
40
+Providers should initialize themselves. Don't call something like **ref.read(provider).init()** from **initState**. If initialization depends on user action (e.g. navigation), trigger it from the action (e.g. **onPressed** before **Navigator.push**).
41
+
42
+### AVOID using providers for ephemeral state
43
+
44
+Use providers for **shared business state**, not for: selected tab/item, form state (which should reset on leave/back), animations, or controller-like state (e.g. TextEditingController). For local widget state use **flutter_hooks** or local state. Storing "selected item" in a global provider can break back navigation (e.g. back from /books/21 should show /books/42, but the provider still holds 21).
45
+
46
+### DON'T perform side effects during provider initialization
47
+
48
+Providers should represent "read" operations. Don't use a provider's build to perform "write" operations (e.g. submitting a form). That can lead to skipped or duplicated side effects. For loading/error of a side effect use **mutations** (see riverpod-mutations).
49
+
50
+### PREFER statically known providers with ref.watch/read/listen
51
+
52
+Use **ref.watch(provider)** where **provider** is a top-level final (or otherwise statically known). Avoid passing a provider as a parameter and then watching it so that static analysis and riverpod_lint can work.
53
+
54
+### AVOID dynamically creating providers
55
+
56
+Define providers as **top-level final** variables. Don't create them inside classes or as instance fields; that can cause memory leaks and unsupported behavior. Static final in a class is allowed but not supported by code generation.
57
+
58
+Enable **riverpod_lint** (see riverpod-getting-started) to enforce many of these practices.
.claude/skills/riverpod-from-provider/SKILL.md
new
+97
@@ -0,0 +1,97 @@
1
+---
2
+name: riverpod-from-provider
3
+description: Migrate from package:provider to Riverpod; ChangeNotifierProvider, ProxyProvider to ref.watch, context.watch to ref.watch, ConsumerWidget, incremental migration, family and autoDispose. Use when the user is migrating from Provider to Riverpod, or asks about Provider vs Riverpod, or how to replace ProxyProvider/ChangeNotifierProvider.
4
+---
5
+
6
+# Riverpod — Migrating from Provider
7
+
8
+## Motivation
9
+
10
+Riverpod was created as a successor to Provider, addressing InheritedWidget limitations:
11
+
12
+- **Same type:** Provider can't have two `Provider<Item>` in the tree (only the nearest is found). Riverpod has no such limit; providers are identified by variable, not type.
13
+- **Combining providers:** ProxyProvider is tedious and error-prone. In Riverpod, use **ref.watch** inside a provider to depend on others; composition is straightforward.
14
+- **AsyncValue:** Riverpod can expose previous data while loading (e.g. show old list + loading indicator). Provider doesn't offer this cleanly.
15
+- **Safety:** Provider can throw `ProviderNotFoundException` at runtime. Riverpod avoids this by design.
16
+- **Disposal:** Provider can't react when consumers stop listening; scoping is tricky. Riverpod offers **autoDispose** and **ref.keepAlive** for clear lifecycle and caching.
17
+- **Parameters:** Riverpod's **.family** gives type-safe, parameterized providers with per-parameter state; with autoDispose, state is disposed when unused. Equivalent in Provider is impractical.
18
+- **Testing:** With Provider you must re-define providers per test. With Riverpod, override with **overrides** to mock.
19
+- **Side effects:** Riverpod offers **ref.listen** for reacting to changes (e.g. navigation, snackbars). Provider has no built-in equivalent.
20
+
21
+The main API change: use **ConsumerWidget** (and **WidgetRef**) instead of StatelessWidget, and **ref.watch** / **ref.read** instead of **context.watch** / **context.read**.
22
+
23
+---
24
+
25
+## Quickstart
26
+
27
+- Read the Riverpod getting started guide (riverpod-getting-started) and try a small example.
28
+- Migrate **incrementally**. You can run Provider and Riverpod side by side (use import aliases if needed).
29
+
30
+### Start with ChangeNotifierProvider
31
+
32
+Keep existing **ChangeNotifier** classes and wrap them in Riverpod's **ChangeNotifierProvider**:
33
+
34
+```dart
35
+final myNotifierProvider = ChangeNotifierProvider<MyNotifier>((ref) => MyNotifier());
36
+```
37
+
38
+Use **ProviderScope** at the root. Replace **context.watch** with **ref.watch** where this provider is used (e.g. in a ConsumerWidget). No need to convert every ChangeNotifier to a Notifier immediately.
39
+
40
+### Start with leaves
41
+
42
+Migrate providers that have no dependencies first (the "leaves"), then those that depend on them. Avoid migrating ProxyProviders until their dependencies are migrated.
43
+
44
+### One provider at a time
45
+
46
+Migrate and test one provider at a time. Full migration of a ChangeNotifier means: (1) convert to **Notifier** + **NotifierProvider**, (2) replace every **context.watch** for it with **ref.watch**.
47
+
48
+### ProxyProvider → ref.watch
49
+
50
+In Riverpod, combining providers is done with **ref.watch** inside another provider:
51
+
52
+```dart
53
+final labelProvider = Provider<String>((ref) {
54
+ final userIdNotifier = ref.watch(userIdNotifierProvider);
55
+ return 'The user ID is ${userIdNotifier.userId}';
56
+});
57
+```
58
+
59
+For stateful combined objects (like ChangeNotifierProxyProvider), use **ref.listen** in the provider to react to another provider and update your notifier.
60
+
61
+### Eager initialization
62
+
63
+Riverpod providers are lazy. To warm data at startup, watch the provider at the root (e.g. in a Consumer under ProviderScope that returns your app as child). See riverpod-eager-initialization.
64
+
65
+### Code generation
66
+
67
+Code gen doesn't generate ChangeNotifierProvider. You can use a small extension (listenAndDisposeChangeNotifier) to expose ChangeNotifier with @riverpod during migration; once you switch to Notifier, remove the extension. See the official quickstart for the snippet.
68
+
69
+---
70
+
71
+## Provider vs Riverpod
72
+
73
+### Defining providers
74
+
75
+- **Provider:** Providers are widgets (e.g. inside MultiProvider).
76
+- **Riverpod:** Providers are top-level **final** variables. No widget tree for definitions. Add **ProviderScope** at the root of the app.
77
+
78
+### Reading
79
+
80
+- **Provider:** `context.watch<T>()`, `context.read<T>()`, `context.select<T,R>(...)`.
81
+- **Riverpod:** Use **ConsumerWidget** (or Consumer) to get **WidgetRef ref**; then **ref.watch(provider)**, **ref.read(provider)**, **ref.watch(provider.select(...))**.
82
+- Use **watch** in build, **read** in event handlers. Same mental model as Provider.
83
+
84
+### Consumer
85
+
86
+Riverpod has **Consumer** with `(context, ref, child)`. No need for Consumer2, Consumer3, etc.: just multiple **ref.watch** calls in one builder.
87
+
88
+### Scoping vs family + autoDispose
89
+
90
+Provider uses scoping to destroy state or have per-page state. In Riverpod:
91
+
92
+- **autoDispose** destroys state when there are no listeners (ref.onCancel / ref.onDispose).
93
+- **.family** gives parameterized providers (one state per parameter). Use with autoDispose to avoid unbounded cache.
94
+
95
+So: use **.family** for "state per X" and **.autoDispose** for "destroy when unused" instead of scoping.
96
+
97
+See riverpod-getting-started, riverpod-providers, riverpod-family, and riverpod-auto-dispose for details.
.claude/skills/riverpod-getting-started/SKILL.md
new
+139
@@ -0,0 +1,139 @@
1
+---
2
+name: riverpod-getting-started
3
+description: Install Riverpod (flutter_riverpod or riverpod), wrap the app in ProviderScope, run a hello-world provider, and optionally enable riverpod_lint and code generation. Use when starting a Flutter or Dart project with Riverpod, adding the Riverpod dependency, or setting up ProviderScope and a first provider. For version highlights see the official Riverpod docs.
4
+---
5
+
6
+# Riverpod — Getting Started
7
+
8
+## Instructions
9
+
10
+### Installing the package
11
+
12
+Riverpod is available as a main package (Flutter: `flutter_riverpod`; Dart-only: `riverpod`). Optional packages add code generation and hooks.
13
+
14
+**Flutter:**
15
+
16
+```bash
17
+flutter pub add flutter_riverpod
18
+```
19
+
20
+With code generation:
21
+
22
+```bash
23
+flutter pub add flutter_riverpod riverpod_annotation
24
+flutter pub add dev:riverpod_generator build_runner
25
+```
26
+
27
+**Dart only:**
28
+
29
+```bash
30
+dart pub add riverpod
31
+```
32
+
33
+With code generation:
34
+
35
+```bash
36
+dart pub add riverpod riverpod_annotation
37
+dart pub add dev:riverpod_generator build_runner
38
+```
39
+
40
+**Manual `pubspec.yaml` (Flutter):**
41
+
42
+```yaml
43
+dependencies:
44
+ flutter:
45
+ sdk: flutter
46
+ flutter_riverpod: ^2.0.0 # use latest from pub.dev
47
+
48
+dev_dependencies:
49
+ build_runner:
50
+ riverpod_generator: ^2.0.0 # if using code generation
51
+```
52
+
53
+Then run `flutter pub get` (or `dart pub get` for Dart-only).
54
+
55
+If using code generation, run:
56
+
57
+```sh
58
+dart run build_runner watch -d
59
+```
60
+
61
+**Note:** Some Riverpod packages may require Flutter's beta channel for compatibility with latest `json_serializable`. If `pub get` fails, try `flutter channel beta` or use Riverpod `<=3.1.0`.
62
+
63
+### Wrap the app in ProviderScope
64
+
65
+Widgets need a `ProviderScope` (Flutter) or a `ProviderContainer` (Dart) to read providers.
66
+
67
+**Flutter — ProviderScope:**
68
+
69
+```dart
70
+import 'package:flutter/material.dart';
71
+import 'package:flutter_riverpod/flutter_riverpod.dart';
72
+
73
+void main() {
74
+ runApp(
75
+ ProviderScope(
76
+ child: MyApp(),
77
+ ),
78
+ );
79
+}
80
+```
81
+
82
+**Dart only — ProviderContainer:**
83
+
84
+```dart
85
+final container = ProviderContainer();
86
+final value = container.read(helloWorldProvider);
87
+print(value);
88
+// Don't forget to container.dispose() when done
89
+```
90
+
91
+### Hello world (Flutter with code generation)
92
+
93
+```dart
94
+import 'package:flutter/material.dart';
95
+import 'package:flutter_riverpod/flutter_riverpod.dart';
96
+import 'package:riverpod_annotation/riverpod_annotation.dart';
97
+
98
+part 'main.g.dart';
99
+
100
+@riverpod
101
+String helloWorld(Ref ref) {
102
+ return 'Hello world';
103
+}
104
+
105
+void main() {
106
+ runApp(ProviderScope(child: MyApp()));
107
+}
108
+
109
+class MyApp extends ConsumerWidget {
110
+ @override
111
+ Widget build(BuildContext context, WidgetRef ref) {
112
+ final String value = ref.watch(helloWorldProvider);
113
+ return MaterialApp(
114
+ home: Scaffold(
115
+ appBar: AppBar(title: const Text('Example')),
116
+ body: Center(child: Text(value)),
117
+ ),
118
+ );
119
+ }
120
+}
121
+```
122
+
123
+Run `dart run build_runner build` (or `watch`) to generate `main.g.dart`. Without code generation, use a manual provider (e.g. `final helloWorldProvider = Provider<String>((ref) => 'Hello world');`) and the same `ProviderScope` + `ConsumerWidget` pattern.
124
+
125
+### Enabling riverpod_lint
126
+
127
+Add the optional linter via `analysis_options.yaml`:
128
+
129
+```yaml
130
+plugins:
131
+ riverpod_lint: ^2.0.0 # use latest from https://pub.dev/packages/riverpod_lint
132
+```
133
+
134
+This provides lint rules and refactorings for Riverpod. See the riverpod_lint package page for the full list.
135
+
136
+### Going further
137
+
138
+- **Code generation and hooks:** See the riverpod-codegen-and-hooks skill (or docs: About code generation, About hooks).
139
+- **VS Code / Android Studio:** Consider Flutter Riverpod Snippets extensions for faster boilerplate.
.claude/skills/riverpod-migration/SKILL.md
new
+29
@@ -0,0 +1,29 @@
1
+---
2
+name: riverpod-migration
3
+description: Migrate Riverpod from StateNotifier to Notifier/AsyncNotifier, from ChangeNotifier to AsyncNotifier, or upgrade from 0.13/0.14/1.0; ref.onDispose, family, lifecycle, riverpod migrate CLI. Use when the user asks about migrating from StateNotifier, from ChangeNotifier, upgrading Riverpod 0.13 to 0.14, 0.14 to 1.0, or Riverpod migration guides.
4
+---
5
+
6
+# Riverpod — Migration guides
7
+
8
+## Instructions
9
+
10
+This skill points to detailed migration guides for different scenarios. Use the reference file that matches your case.
11
+
12
+### When to use which guide
13
+
14
+| Scenario | Reference |
15
+|----------|-----------|
16
+| **From StateNotifier** to Notifier / AsyncNotifier | [migration_from_state_notifier.md](reference/migration_from_state_notifier.md) |
17
+| **From ChangeNotifier** to AsyncNotifier | [migration_from_change_notifier.md](reference/migration_from_change_notifier.md) |
18
+| **Upgrade 0.13.x → 0.14.x** (StateNotifierProvider syntax, watch(provider) vs provider.state) | [migration_0_13_0_14.md](reference/migration_0_13_0_14.md) |
19
+| **Upgrade 0.14.x → 1.0** (ScopedReader → WidgetRef, useProvider → ref.watch, StateProvider) | [migration_0_14_1_0.md](reference/migration_0_14_1_0.md) |
20
+
21
+For **Riverpod 2.0 → 3.0** see the **riverpod-3-0-migration** skill.
22
+
23
+### Common themes
24
+
25
+- **StateNotifier → Notifier/AsyncNotifier:** Put init logic in `build`; use `ref.onDispose` instead of overloading `dispose`; no `mounted`—use cancellation (e.g. Completer or HTTP cancel token). Consumers stay the same (ref.watch, methods on notifier).
26
+- **ChangeNotifier → AsyncNotifier:** Replace `isLoading`/`hasError`/manual try-catch with `AsyncValue`; single `state`; mutations by reassigning `state`.
27
+- **CLI:** For 0.14 and 1.0 upgrades you can run `dart pub global activate riverpod_cli` then `riverpod migrate` in the project (do not upgrade Riverpod manually first; the tool will suggest version and changes).
28
+
29
+Open the relevant reference file for step-by-step instructions and code examples.
.claude/skills/riverpod-migration/reference/migration_0_13_0_14.md
new
+23
@@ -0,0 +1,23 @@
1
+# Migration: 0.13.x → 0.14.x
2
+
3
+- In **0.14.0** the way **StateNotifierProvider** and state/notifier reading work changed.
4
+
5
+## Changes
6
+
7
+1. **StateNotifierProvider** gets an extra type parameter: the **state** type.
8
+ - Before: `StateNotifierProvider<MyStateNotifier>(...)`
9
+ - After: `StateNotifierProvider<MyStateNotifier, MyModel>(...)`
10
+
11
+2. **Reading the notifier**
12
+ - Before: `watch(provider)` returned the StateNotifier.
13
+ - After: use **provider.notifier**: `watch(provider.notifier)` to get the StateNotifier.
14
+
15
+3. **Reading the state**
16
+ - Before: `watch(provider.state)` for the state.
17
+ - After: `watch(provider)` returns the state directly.
18
+
19
+## CLI migration
20
+
21
+- Install: `dart pub global activate riverpod_cli`
22
+- In the project (do **not** upgrade Riverpod first): run **riverpod migrate**.
23
+- The tool will suggest edits (e.g. `watch(provider.state)` → `watch(provider)`). Accept with `y` or reject with `n`.
.claude/skills/riverpod-migration/reference/migration_0_14_1_0.md
new
+30
@@ -0,0 +1,30 @@
1
+# Migration: 0.14.x → 1.0
2
+
3
+- **1.0** unifies the API for reading providers and changes some types.
4
+
5
+## CLI migration
6
+
7
+- Install: `dart pub global activate riverpod_cli`
8
+- **Do not** upgrade to 1.0 before running the tool; stay on 0.14.x.
9
+- Run **riverpod migrate** in the project; it will suggest changes and can upgrade the dependency.
10
+
11
+## Syntax unification
12
+
13
+- **Single syntax**: **ref.watch(provider)** (and ref.read). **useProvider**, **watch(provider)** without ref, and **context.read** are removed.
14
+
15
+- **ConsumerWidget / Consumer**
16
+ - Before: `build(BuildContext context, ScopedReader watch)` and `watch(provider)`.
17
+ - After: `build(BuildContext context, WidgetRef ref)` and **ref.watch(provider)**.
18
+ - Consumer: `builder: (context, watch, child)` → `builder: (context, ref, child)` and use **ref.watch(provider)**.
19
+
20
+- **HookWidget**
21
+ - **useProvider(provider)** is removed. Use **HookConsumerWidget** and **ref.watch(provider)** in build.
22
+
23
+- **context.read**
24
+ - Use **ConsumerWidget** (or Consumer) and **ref.read(provider)** instead of **context.read**.
25
+
26
+## StateProvider
27
+
28
+- **ref.watch(StateProvider)** used to return a **StateController**; in 1.0 it returns the **state** value only.
29
+ - If you only displayed the value: change to **ref.watch(provider)** and use the value directly.
30
+ - If you need the controller: use **ref.watch(provider.state)** to keep the old behavior (StateController).
.claude/skills/riverpod-migration/reference/migration_from_change_notifier.md
new
+36
@@ -0,0 +1,36 @@
1
+# Migration: From ChangeNotifier to AsyncNotifier
2
+
3
+- For apps already on Riverpod using **ChangeNotifierProvider**, this guide moves to **AsyncNotifier** (and **Notifier** where state is sync).
4
+
5
+## Why migrate
6
+
7
+- ChangeNotifier leads to **isLoading** / **hasError** flags and lots of **try/catch/finally** and **notifyListeners()**.
8
+- AsyncNotifier uses a single **state** and **AsyncValue**; Riverpod handles loading/error; less boilerplate and fewer inconsistent states.
9
+
10
+## Steps
11
+
12
+1. **Choose the notifier type**
13
+ - Side effects (methods that change state)? → Class-based (Notifier or AsyncNotifier).
14
+ - Async load (e.g. network)? → **AsyncNotifier<T>**.
15
+ - Parameters? → Add them to the notifier (e.g. constructor + field) and use family/provider that passes them.
16
+
17
+2. **Declaration**
18
+ - Replace **ChangeNotifierProvider** with **AsyncNotifierProvider** (or NotifierProvider if sync).
19
+ - State type: e.g. **AsyncNotifier<List<Todo>>**; no separate **todos** / **isLoading** / **hasError**—just **state** and AsyncValue.
20
+
21
+3. **Initialization**
22
+ - Move init from a custom method (e.g. **init**) into **build**.
23
+ - **build** returns **Future<List<Todo>>**; Riverpod exposes **AsyncValue<List<Todo>>** to the UI. No manual try/catch in build.
24
+
25
+4. **Mutations**
26
+ - Replace flag updates and notifyListeners with **state = newValue** (and **state = AsyncValue.data(...)** or **AsyncValue.error(...)** if you construct explicitly). Use **ref** for dependencies (ref.watch, ref.read) inside methods.
27
+
28
+5. **Consumers**
29
+ - **ref.watch(provider)** now gets **AsyncValue<T>**; handle loading/error/data in the UI. Use **ref.read(provider.notifier)** to call methods.
30
+
31
+## Summary
32
+
33
+- Init in **build**; no **todos**/isLoading/hasError.
34
+- No try-catch-finally in build; return the Future.
35
+- Side effects: reassign **state**; use **ref** for other providers.
36
+- See riverpod-providers and riverpod-refs for Notifier/AsyncNotifier and Ref APIs.
.claude/skills/riverpod-migration/reference/migration_from_state_notifier.md
new
+42
@@ -0,0 +1,42 @@
1
+# Migration: From StateNotifier to Notifier / AsyncNotifier
2
+
3
+- **StateNotifier** is deprecated in favor of **Notifier** and **AsyncNotifier** (Riverpod 2.0+).
4
+
5
+## Main differences
6
+
7
+- Dependencies: in StateNotifier they were in the provider; in Notifier they go in **build** via **ref.watch**.
8
+- Initialization: StateNotifier split between provider and constructor; Notifier uses a single **build** method; no logic in the constructor.
9
+- Lifecycle: StateNotifier had **dispose** on the class. Notifier/AsyncNotifier use **ref.onDispose** (and ref.onCancel) in build, like any other provider. Only internal state is disposed on rebuild; the notifier type itself is not disposed the same way.
10
+- **mounted**: Notifier/AsyncNotifier do not have **mounted**. Use cancellation (Completer, Dio CancelToken, etc.) instead of checking mounted after async work.
11
+
12
+## Sync: StateNotifier → Notifier
13
+
14
+- Replace **StateNotifierProvider** with **NotifierProvider**.
15
+- Move reactive dependencies from the provider into **build** with **ref.watch**.
16
+- Put all init logic in **build**; leave constructor minimal.
17
+- Use **ref.onDispose** in build for cleanup (no override of dispose).
18
+
19
+## Async: StateNotifier → AsyncNotifier
20
+
21
+- Replace **StateNotifierProvider** with **AsyncNotifierProvider**.
22
+- Move async init into **build** returning **Future<T>**; remove AsyncValue.guard / try-catch in build.
23
+- Use **future** and **update** on AsyncNotifier for simpler async flows.
24
+- Migrating from **StateNotifier<AsyncValue<T>>** to **AsyncNotifier<T>**: put init in build, remove manual try/catch and AsyncValue handling in build.
25
+
26
+## Family and autoDispose
27
+
28
+- With Notifier/AsyncNotifier, family parameters are passed into the class (e.g. constructor + field) and are available in build; no separate “family” type.
29
+- AutoDispose is expressed the same as other providers (e.g. ref.onDispose in build).
30
+
31
+## Consumers and mutations
32
+
33
+- **Consumers do not change**: ref.watch(provider), ref.read(provider.notifier), and calling notifier methods stay the same.
34
+- **ref.listen** replaces StateNotifier’s **.addListener** / **.stream**: use **ref.listen(provider, (prev, next) { ... })**.
35
+
36
+## Tests
37
+
38
+- **.debugState** is gone. Use **ref.read(provider)** or **ref.read(provider.notifier)** to get the notifier/state in tests; do not instantiate Notifier/AsyncNotifier by hand (ref and family args would be unset).
39
+
40
+## StateProvider
41
+
42
+- **StateProvider** is deprecated with StateNotifierProvider. Migrate to **Notifier**: one Notifier with **build** returning the initial value and methods that set **state**.
.claude/skills/riverpod-mutations/SKILL.md
new
+80
@@ -0,0 +1,80 @@
1
+---
2
+name: riverpod-mutations
3
+description: Use Riverpod mutations for UI-driven async operations like form submit; Mutation.run, MutationIdle/Pending/Success/Error, Mutation.reset, keyed mutations. Use when showing loading/error/success for a single action (e.g. submit button) without polluting provider state. Use this skill when the user asks about mutations, loading state for actions, or form submission with Riverpod.
4
+---
5
+
6
+# Riverpod — Mutations (experimental)
7
+
8
+## Instructions
9
+
10
+**Mutations** represent a single async operation the UI can react to (e.g. "add todo" in progress, succeeded, or failed). They keep loading/error/success out of your main provider state. The API is experimental and may change.
11
+
12
+### Defining a mutation
13
+
14
+Create a `Mutation<T>` instance (T is the result type, optional):
15
+
16
+```dart
17
+final addTodo = Mutation<Todo>();
18
+```
19
+
20
+Typically store it as a global or `static final` on a Notifier.
21
+
22
+### Listening
23
+
24
+Use ref.watch in a Consumer (or inside a provider) to drive the UI:
25
+
26
+```dart
27
+final addTodoState = ref.watch(addTodo);
28
+
29
+// In build:
30
+backgroundColor: switch (addTodoState) {
31
+ MutationError() => const WidgetStatePropertyAll(Colors.red),
32
+ _ => null,
33
+},
34
+if (addTodoState is MutationPending) ...[
35
+ const SizedBox(width: 8),
36
+ const CircularProgressIndicator(),
37
+],
38
+```
39
+
40
+### States
41
+
42
+- **MutationIdle** — Not started or reset.
43
+- **MutationPending** — In progress.
44
+- **MutationSuccess** — Succeeded; result available.
45
+- **MutationError** — Failed; error available.
46
+
47
+Use a `switch` on the state to handle each case.
48
+
49
+### Triggering
50
+
51
+Call **Mutation.run(ref, callback)**. The callback receives a transaction object (e.g. `tsx`) to access providers and must return a value of type T:
52
+
53
+```dart
54
+addTodo.run(ref, (tsx) async {
55
+ final todoNotifier = tsx.get(todoNotifierProvider.notifier);
56
+ final createdTodo = await todoNotifier.addTodo('Eat a cookie');
57
+ return createdTodo;
58
+});
59
+```
60
+
61
+Use `tsx.get(provider)` to keep providers alive for the duration of the mutation.
62
+
63
+### Keyed mutations
64
+
65
+Pass a key to have multiple instances (e.g. one mutation per list item):
66
+
67
+```dart
68
+ref.watch(deleteItemMutation(itemId));
69
+deleteItemMutation(itemId).run(ref, (tsx) async { ... });
70
+```
71
+
72
+### Resetting
73
+
74
+Mutations reset to Idle when they complete or when all listeners are removed. To reset manually:
75
+
76
+```dart
77
+addTodo.reset(ref);
78
+```
79
+
80
+See the official Riverpod docs for the latest mutation API and generics.
.claude/skills/riverpod-observers/SKILL.md
new
+52
@@ -0,0 +1,52 @@
1
+---
2
+name: riverpod-observers
3
+description: Use ProviderObserver to log or debug Riverpod provider lifecycle; didUpdateProvider, ProviderScope observers, naming providers. Use when adding logging, analytics, or debugging for provider state changes. Use this skill when the user asks about ProviderObserver, logging Riverpod, or debugging provider updates.
4
+---
5
+
6
+# Riverpod — ProviderObservers
7
+
8
+## Instructions
9
+
10
+A **ProviderObserver** receives lifecycle events for providers (e.g. when a provider's value changes). Use it for logging, analytics, or debugging.
11
+
12
+### Implementing an observer
13
+
14
+Extend **ProviderObserver** and override the methods you need (e.g. **didUpdateProvider**). Pass instances to **ProviderScope** or **ProviderContainer** via the `observers` parameter.
15
+
16
+### Example: logger
17
+
18
+```dart
19
+final class Logger extends ProviderObserver {
20
+ @override
21
+ void didUpdateProvider(
22
+ ProviderObserverContext context,
23
+ Object? previousValue,
24
+ Object? newValue,
25
+ ) {
26
+ print('{"provider": "${context.provider}", "newValue": "$newValue"}');
27
+ }
28
+}
29
+
30
+void main() {
31
+ runApp(
32
+ ProviderScope(
33
+ observers: [Logger()],
34
+ child: const MyApp(),
35
+ ),
36
+ );
37
+}
38
+```
39
+
40
+### Naming providers
41
+
42
+For clearer logs, give providers a **name**:
43
+
44
+```dart
45
+final myProvider = Provider<int>((ref) => 0, name: 'MyProvider');
46
+```
47
+
48
+With code generation, a name is usually assigned automatically.
49
+
50
+### Note on mutations
51
+
52
+If state is mutated in place (e.g. a List with ref.notifyListeners), `previousValue` and `newValue` in didUpdateProvider may be the same reference. For distinct values, clone before mutating.
.claude/skills/riverpod-offline/SKILL.md
new
+111
@@ -0,0 +1,111 @@
1
+---
2
+name: riverpod-offline
3
+description: Persist Riverpod notifier state offline with Storage and persist(); riverpod_sqflite, JsonPersist, key, destroyKey, cache duration, testing with in-memory storage. Use when saving state across app restarts or offline. Use this skill when the user asks about offline persistence, persisting state, or Riverpod storage.
4
+---
5
+
6
+# Riverpod — Offline persistence (experimental)
7
+
8
+## Instructions
9
+
10
+**Offline persistence** stores provider state on device so it survives restarts and works offline. Riverpod is storage-agnostic; packages like **riverpod_sqflite** provide a **Storage** implementation. Only **Notifier**-based providers can be persisted. The feature is experimental.
11
+
12
+### Creating a Storage
13
+
14
+Install a package (e.g. riverpod_sqflite + sqflite) and create a Storage. With SQFlite:
15
+
16
+```dart
17
+final storageProvider = FutureProvider<Storage<String, String>>((ref) async {
18
+ return JsonSqFliteStorage.open(
19
+ join(await getDatabasesPath(), 'riverpod.db'),
20
+ );
21
+});
22
+```
23
+
24
+### Persisting a notifier
25
+
26
+Inside the notifier's `build`, call **persist** with: the Storage (e.g. `ref.watch(storageProvider.future)`), a unique **key**, and **encode**/ **decode** for your state. Do not await persist; Riverpod handles it.
27
+
28
+```dart
29
+class TodoList extends AsyncNotifier<List<Todo>> {
30
+ @override
31
+ Future<List<Todo>> build() async {
32
+ persist(
33
+ ref.watch(storageProvider.future),
34
+ key: 'todo_list',
35
+ encode: (todos) => todos.map((todo) => {'task': todo.task}).toList(),
36
+ decode: (json) => (json as List).map((todo) => Todo(task: todo['task'] as String)).toList(),
37
+ );
38
+ return fetchTodosFromServer();
39
+ }
40
+}
41
+```
42
+
43
+### Keys
44
+
45
+- **Unique** across all persisted providers (same key = same row, risk of corruption).
46
+- **Stable** across restarts (changing the key loses restored state).
47
+- For **family** providers, include the parameter in the key.
48
+
49
+### JsonPersist (code generation)
50
+
51
+With riverpod_sqflite and codegen, use **@JsonPersist()** so key/encode/decode are generated:
52
+
53
+```dart
54
+@riverpod
55
+@JsonPersist()
56
+class TodoList extends _$TodoList {
57
+ @override
58
+ Future<List<Todo>> build() async {
59
+ persist(ref.watch(storageProvider.future));
60
+ return fetchTodosFromServer();
61
+ }
62
+}
63
+```
64
+
65
+### Cache duration
66
+
67
+By default state is cached for a short time (e.g. 2 days). For long-lived data (e.g. user preferences), set **StorageOptions**:
68
+
69
+```dart
70
+persist(
71
+ ref.watch(storageProvider.future),
72
+ options: const StorageOptions(cacheTime: StorageCacheTime.unsafe_forever),
73
+ // ...
74
+);
75
+```
76
+
77
+If using forever, plan to delete or migrate data when the app changes; Riverpod does not do migrations.
78
+
79
+### Destroy key (simple migration)
80
+
81
+When the data shape changes, use **destroyKey** so old data is discarded:
82
+
83
+```dart
84
+options: const StorageOptions(destroyKey: '1.0'),
85
+```
86
+
87
+Bump the string in new releases; old persisted state is then ignored and the provider starts fresh.
88
+
89
+### Waiting for decode
90
+
91
+To initialize from persisted state instead of a network call, await the persist future:
92
+
93
+```dart
94
+await persist(ref.watch(storageProvider.future), key: 'todo_list', ...).future;
95
+return state.value ?? <Todo>[];
96
+```
97
+
98
+### Testing
99
+
100
+Override the storage provider with **Storage.inMemory()** so tests don't need a real database:
101
+
102
+```dart
103
+ProviderScope(
104
+ overrides: [
105
+ storageProvider.overrideWith((ref) => Storage<String, String>.inMemory()),
106
+ ],
107
+ child: const MyApp(),
108
+)
109
+```
110
+
111
+For advanced migrations or custom storage strategies, you may still need to work with the database directly.
.claude/skills/riverpod-overrides/SKILL.md
new
+55
@@ -0,0 +1,55 @@
1
+---
2
+name: riverpod-overrides
3
+description: Override Riverpod providers for tests, debugging, or environment-specific behavior; ProviderScope and ProviderContainer overrides, overrideWith, overrideWithValue. Use when mocking providers in tests, injecting different implementations, or scoping provider behavior. Use this skill when the user asks about overrides, testing Riverpod, or mocking providers.
4
+---
5
+
6
+# Riverpod — Provider overrides
7
+
8
+## Instructions
9
+
10
+All providers can be **overridden** to change their behavior. Overrides are set on the **container** (ProviderScope in Flutter, ProviderContainer in Dart) via the `overrides` parameter. Use overrides for testing, debugging, different environments, or scoping (see riverpod-scoping).
11
+
12
+### Where to set overrides
13
+
14
+- **Flutter:** Pass `overrides` to `ProviderScope` (usually at the root or around a subtree).
15
+- **Dart:** Pass `overrides` to `ProviderContainer`.
16
+
17
+### Override methods
18
+
19
+Each provider type exposes methods whose names start with `overrideWith`, for example:
20
+
21
+- **overrideWith** — Provide a new implementation (e.g. a function that returns a value or builds state).
22
+- **overrideWithValue** — Replace with a fixed value (e.g. for FutureProvider/StreamProvider).
23
+- **overrideWithBuild** — Custom build logic (e.g. for StreamProvider).
24
+
25
+Check the provider's API for the exact method (e.g. `Provider.overrideWith`, `FutureProvider.overrideWithValue`).
26
+
27
+### Example: override a counter
28
+
29
+```dart
30
+// Flutter
31
+void main() {
32
+ runApp(
33
+ ProviderScope(
34
+ overrides: [
35
+ counterProvider.overrideWith((ref) => 42),
36
+ ],
37
+ child: MyApp(),
38
+ ),
39
+ );
40
+}
41
+
42
+// Dart
43
+final container = ProviderContainer(
44
+ overrides: [
45
+ counterProvider.overrideWith((ref) => 42),
46
+ ],
47
+);
48
+```
49
+
50
+### Family overrides
51
+
52
+- **Single parameter:** Override one specific argument: `userProvider('123').overrideWith((ref) => User(name: 'User 123'))`.
53
+- **All parameters:** Override the whole family: `userProvider.overrideWith((ref, arg) => User(name: 'User $arg'))`.
54
+
55
+See riverpod-family and riverpod-testing for more test patterns.
.claude/skills/riverpod-providers/SKILL.md
new
+92
@@ -0,0 +1,92 @@
1
+---
2
+name: riverpod-providers
3
+description: Declare and use Riverpod providers (Provider, FutureProvider, StreamProvider, NotifierProvider, AsyncNotifierProvider, StreamNotifierProvider); unmodifiable vs modifiable, top-level declaration, Ref, Notifier build method. Use when creating providers, choosing provider type, writing Notifier classes, or understanding Riverpod state. Use this skill whenever the user asks about Riverpod providers, provider types, or notifiers.
4
+---
5
+
6
+# Riverpod — Providers
7
+
8
+## Instructions
9
+
10
+Providers are the central feature of Riverpod: memoized functions that cache their result and let multiple widgets (or other providers) access the same value. They are declared as **top-level** `final` variables. State lives in a **ProviderContainer** (or ProviderScope in Flutter), not in the provider itself.
11
+
12
+### Provider variants
13
+
14
+| | Synchronous | Future | Stream |
15
+|--------------|---------------|------------------------|-------------------------|
16
+| Unmodifiable | Provider | FutureProvider | StreamProvider |
17
+| Modifiable | NotifierProvider | AsyncNotifierProvider | StreamNotifierProvider |
18
+
19
+- **Sync vs Future vs Stream**: Match the return type of your function (`int`, `Future<T>`, `Stream<T>`).
20
+- **Unmodifiable**: Widgets only read the value. Use for computed or fetched data.
21
+- **Modifiable**: Expose a Notifier with a `build()` method and custom methods; widgets call `ref.read(provider.notifier).someMethod()`. Use when the UI must update state.
22
+
23
+Pick the type based on what you want to return; the provider type follows naturally. FutureProvider / AsyncNotifierProvider are the most common.
24
+
25
+### Creating a provider (unmodifiable / functional)
26
+
27
+**Manual:**
28
+
29
+```dart
30
+final userProvider = FutureProvider<User>((ref) async {
31
+ final response = await http.get('https://api.example.com/user/123');
32
+ return User.fromJson(response.body);
33
+});
34
+```
35
+
36
+**Code generation:** Annotate a top-level function with `@riverpod`. First parameter must be `Ref ref`. The generated provider name is `functionNameProvider`.
37
+
38
+```dart
39
+@riverpod
40
+Future<User> user(Ref ref) async {
41
+ final response = await http.get('https://api.example.com/user/123');
42
+ return User.fromJson(response.body);
43
+}
44
+```
45
+
46
+Modifiers: `.autoDispose` (or `@Riverpod(keepAlive: true)` to disable) and `.family` for parameters. See riverpod-auto-dispose and riverpod-family.
47
+
48
+### Creating a provider (modifiable / Notifier)
49
+
50
+**Manual:** Use a NotifierProvider (or AsyncNotifierProvider / StreamNotifierProvider) with a Notifier class that extends `Notifier<T>`, `AsyncNotifier<T>`, or `StreamNotifier<T>`. Override `build()`; do not put logic in the constructor (ref/state are not available yet). Public methods are called via `ref.read(provider.notifier).method()`.
51
+
52
+```dart
53
+final counterProvider = NotifierProvider<CounterNotifier, int>(CounterNotifier.new);
54
+
55
+class CounterNotifier extends Notifier<int> {
56
+ @override
57
+ int build() => 0;
58
+
59
+ void increment() => state++;
60
+}
61
+```
62
+
63
+**Code generation:** Annotate a class that extends `_$ClassName`. Override `build()`; add custom methods. Same rule: no logic in the constructor.
64
+
65
+```dart
66
+@riverpod
67
+class CounterNotifier extends _$CounterNotifier {
68
+ @override
69
+ int build() => 0;
70
+
71
+ void increment() => state++;
72
+}
73
+```
74
+
75
+### Using providers
76
+
77
+1. **Flutter:** Wrap the app in `ProviderScope`. Use Consumer, ConsumerWidget, or ConsumerStatefulWidget to get a `ref` and call `ref.watch(provider)` or `ref.read(provider)`.
78
+2. **Dart only:** Create a `ProviderContainer()`, then `container.read(provider)` or `container.listen(...)`. Call `container.dispose()` when done.
79
+3. **Inside another provider:** Use the `Ref` passed to the provider function (or `this.ref` in a Notifier) to `ref.watch` / `ref.read` other providers.
80
+
81
+Example in a widget:
82
+
83
+```dart
84
+return Consumer(
85
+ builder: (context, ref, _) {
86
+ final helloWorld = ref.watch(helloWorldProvider);
87
+ return Text(helloWorld);
88
+ },
89
+);
90
+```
91
+
92
+You can declare multiple providers that return the same type; they are independent. For more detail on Ref, containers, modifiers, and notifier rules, see [reference.md](reference.md).
.claude/skills/riverpod-providers/reference.md
new
+34
@@ -0,0 +1,34 @@
1
+# Riverpod Providers — Reference
2
+
3
+## Provider type summary
4
+
5
+- **Provider** — sync, unmodifiable. Return type `T`.
6
+- **FutureProvider** — async, unmodifiable. Return type `Future<T>`.
7
+- **StreamProvider** — stream, unmodifiable. Return type `Stream<T>`.
8
+- **NotifierProvider** — sync, modifiable. Notifier extends `Notifier<T>`, `build()` returns `T`.
9
+- **AsyncNotifierProvider** — async, modifiable. Notifier extends `AsyncNotifier<T>`, `build()` returns `Future<T>`.
10
+- **StreamNotifierProvider** — stream, modifiable. Notifier extends `StreamNotifier<T>`, `build()` returns `Stream<T>`.
11
+
12
+## Modifiers
13
+
14
+- **autoDispose** — Cache is cleared when the provider has no listeners. Use with family to avoid memory leaks. Disable with `@Riverpod(keepAlive: true)` in codegen.
15
+- **family** — Pass parameters to the provider (e.g. `provider(id)`). See riverpod-family.
16
+
17
+## Notifier rules
18
+
19
+- Do not put logic in the Notifier constructor; `ref` and `state` are not ready. Put initialization in `build()`.
20
+- `build()` is called by the framework; do not call it directly.
21
+- Consumers modify state by calling methods on the notifier: `ref.read(myProvider.notifier).myMethod()`.
22
+
23
+## Multiple providers, same type
24
+
25
+Riverpod allows multiple providers that expose the same type (e.g. two different `Provider<String>`). They are independent; no conflict.
26
+
27
+## Links to other concepts
28
+
29
+- Ref (read, watch, listen, invalidate): riverpod-refs
30
+- Containers / ProviderScope: riverpod-containers
31
+- Auto-dispose: riverpod-auto-dispose
32
+- Family: riverpod-family
33
+- Overrides (testing): riverpod-overrides
34
+- Consumers (widgets): riverpod-consumers
.claude/skills/riverpod-pull-to-refresh/SKILL.md
new
+51
@@ -0,0 +1,51 @@
1
+---
2
+name: riverpod-pull-to-refresh
3
+description: Implement pull-to-refresh with Riverpod using RefreshIndicator and ref.refresh; show spinner on initial load, show previous data during refresh, AsyncValue pattern matching. Use when the user asks about pull-to-refresh, RefreshIndicator with Riverpod, or refreshing async providers.
4
+---
5
+
6
+# Riverpod — Pull-to-refresh
7
+
8
+## Instructions
9
+
10
+Riverpod fits pull-to-refresh well: refresh by invalidating the provider and let AsyncValue handle loading vs data vs error.
11
+
12
+### 1. Provider and UI
13
+
14
+Define an async provider (e.g. FutureProvider or AsyncNotifierProvider) that fetches data. Display it with **ref.watch(provider)** so you get an **AsyncValue**.
15
+
16
+### 2. RefreshIndicator
17
+
18
+Wrap your scrollable (ListView, SingleChildScrollView, etc.) in **RefreshIndicator**. In **onRefresh**, call **ref.refresh(provider.future)** so the indicator stays until the new data is loaded:
19
+
20
+```dart
21
+RefreshIndicator(
22
+ onRefresh: () => ref.refresh(activityProvider.future),
23
+ child: ListView(
24
+ children: [
25
+ // content
26
+ ],
27
+ ),
28
+)
29
+```
30
+
31
+### 3. Loading and error states
32
+
33
+Use **AsyncValue** pattern matching:
34
+
35
+- **Initial load:** No data and no error → show full-screen spinner.
36
+- **Refresh:** Show the refresh indicator and keep showing previous data (or previous error) until the new result arrives.
37
+- **Data:** `AsyncValue(:final value?)` or similar to show the value.
38
+- **Error:** `AsyncValue(:final error?)` to show the error.
39
+
40
+Example (syntax may vary by Riverpod version):
41
+
42
+```dart
43
+final activity = ref.watch(activityProvider);
44
+switch (activity) {
45
+ AsyncValue<Activity>(:final value?) => Text(value.activity),
46
+ AsyncValue(:final error?) => Text('Error: $error'),
47
+ _ => const CircularProgressIndicator(),
48
+}
49
+```
50
+
51
+Use **valueOrNull** (or equivalent) if your API exposes it for nullable data. See the docs for the exact AsyncValue API in your version.
.claude/skills/riverpod-refs/SKILL.md
new
+70
@@ -0,0 +1,70 @@
1
+---
2
+name: riverpod-refs
3
+description: Use Ref and WidgetRef to read, watch, listen, invalidate, and refresh providers; onDispose and onCancel lifecycle; ref.read vs ref.watch vs ref.listen, ref.invalidate and ref.refresh. Use when interacting with Riverpod providers from widgets or other providers, when to use watch vs read, or when resetting provider state. Use this skill whenever the user asks about ref.watch, ref.read, ref.listen, ref.invalidate, or Riverpod lifecycle.
4
+---
5
+
6
+# Riverpod — Refs
7
+
8
+## Instructions
9
+
10
+**Ref** (and **WidgetRef** in widgets) is how you interact with providers: read state, listen to changes, reset state, and register lifecycle callbacks. Providers get a `Ref` as the first parameter of their function or as `this.ref` in a Notifier. Widgets get a **WidgetRef** via Consumer / ConsumerWidget / ConsumerState.
11
+
12
+### Obtaining a Ref
13
+
14
+- **Inside a provider:** First parameter of the provider function, or `ref` property in a Notifier.
15
+- **Inside a widget:** Use a Consumer (builder gives `ref`), ConsumerWidget (`build(context, ref)`), or ConsumerStatefulWidget (state has `ref`). Pass `ref` to other functions if needed.
16
+
17
+### Listening to state
18
+
19
+- **ref.watch(provider)** — Declarative. Your widget/provider rebuilds when the provider changes. Use this by default in build methods.
20
+- **ref.listen(provider, (prev, next) { ... })** — Imperative. Run side effects when the provider changes (e.g. show a dialog, navigate). Safe to use in build; for initState use `ref.listenManual` and manage the subscription.
21
+
22
+```dart
23
+// In a widget
24
+final tick = ref.watch(tickProvider);
25
+return Text('Tick: $tick');
26
+
27
+// Side effect when provider changes
28
+ref.listen(tickProvider, (previous, next) {
29
+ print('Tick changed from $previous to $next');
30
+});
31
+```
32
+
33
+### Reading without listening
34
+
35
+- **ref.read(provider)** — Get current value without subscribing. Use in event handlers (e.g. onPressed), not to "optimize" by avoiding watch. For selective rebuilds use `ref.watch(provider.select((value) => ...))`.
36
+
37
+```dart
38
+onPressed: () {
39
+ final tick = ref.read(tickProvider);
40
+ print('Current tick: $tick');
41
+}
42
+```
43
+
44
+### Resetting state
45
+
46
+- **ref.invalidate(provider)** — Discard current state; provider will recompute on next read. If the provider is listened to, a new state is created.
47
+- **ref.refresh(provider)** — Same as invalidate + read: invalidates and returns the new value. Use when you need the new value immediately.
48
+
49
+```dart
50
+ref.invalidate(tickProvider);
51
+// or
52
+final newTick = ref.refresh(tickProvider);
53
+```
54
+
55
+### Lifecycle: onDispose, onCancel
56
+
57
+Inside a provider you can register callbacks:
58
+
59
+- **ref.onDispose(callback)** — Called when the provider state is destroyed (e.g. auto-dispose or recomputation). Use to close StreamControllers, cancel timers, etc. Do not trigger side effects that modify other providers inside onDispose.
60
+- **ref.onCancel(callback)** — Called when the last listener is removed (before dispose). **ref.onResume(callback)** — Called when a listener is added again after onCancel.
61
+
62
+```dart
63
+final provider = StreamProvider<int>((ref) {
64
+ final controller = StreamController<int>();
65
+ ref.onDispose(controller.close);
66
+ return controller.stream;
67
+});
68
+```
69
+
70
+You can call onDispose multiple times (e.g. one per disposable resource). Return value of onDispose/onCancel can be called to unregister. For more detail and select/listenManual, see [reference.md](reference.md).
.claude/skills/riverpod-refs/reference.md
new
+30
@@ -0,0 +1,30 @@
1
+# Riverpod Refs — Reference
2
+
3
+## ref.watch
4
+
5
+Subscribes to the provider; the current widget/provider rebuilds when the value changes. Use in build. Providers can watch other providers to compose state.
6
+
7
+## ref.read
8
+
9
+Returns current value without subscribing. Use in event handlers (onPressed, callbacks). Do not use read instead of watch to "optimize" — use watch or use select to limit rebuilds.
10
+
11
+## ref.listen
12
+
13
+Registers a one-off listener for side effects (navigation, dialogs, logging). Signature: `(previous, next) => ...`. Safe in build. For listening outside build (e.g. initState), use **ref.listenManual** and store the subscription to cancel later.
14
+
15
+## ref.invalidate / ref.refresh
16
+
17
+- **invalidate(provider)** — Discard state; next read will recompute.
18
+- **refresh(provider)** — invalidate + read in one call; returns the new value.
19
+
20
+## ref.select
21
+
22
+Reduce rebuilds by watching only a part of the state: `ref.watch(provider.select((value) => value.someField))`. Rebuilds only when the selected value changes (by ==).
23
+
24
+## WidgetRef.listenManual
25
+
26
+When you need to listen from initState or outside build, use listenManual and call the returned subscription's `close()` in dispose.
27
+
28
+## Ref vs WidgetRef
29
+
30
+Ref is used inside providers; WidgetRef is used inside widgets. There is no shared interface by design — keep logic in providers and call notifier methods from the UI so only Ref is needed in business logic.
.claude/skills/riverpod-retry/SKILL.md
new
+51
@@ -0,0 +1,51 @@
1
+---
2
+name: riverpod-retry
3
+description: Customize Riverpod automatic retry on provider failure; retry function, per-provider and global retry, disabling retry, ProviderException, awaiting retries. Use when a provider can fail transiently and should retry, or when you need to disable or customize retry logic. Use this skill when the user asks about retry, failed providers, or exponential backoff in Riverpod.
4
+---
5
+
6
+# Riverpod — Automatic retry
7
+
8
+## Instructions
9
+
10
+Riverpod **retries** providers when their computation throws. Retry can be customized per provider or globally.
11
+
12
+### Default behavior
13
+
14
+By default a provider is retried up to a limit (e.g. 10 times) with exponential backoff (e.g. 200ms to 6.4s). **Error** and **ProviderException** are not retried: Error indicates a bug; ProviderException means a dependency failed, so the dependent provider is not retried (the underlying one is).
15
+
16
+### Custom retry function
17
+
18
+A retry function has signature `Duration? Function(int retryCount, Object error)`. Return a **Duration** for the delay before the next retry, or **null** to stop.
19
+
20
+```dart
21
+Duration? myRetry(int retryCount, Object error) {
22
+ if (retryCount >= 5) return null;
23
+ if (error is ProviderException) return null;
24
+ return Duration(milliseconds: 200 * (1 << retryCount));
25
+}
26
+```
27
+
28
+### Where to set it
29
+
30
+- **Per provider:** Pass `retry: myRetry` when defining the provider (or `@Riverpod(retry: myRetry)` with codegen).
31
+- **Globally:** Pass `retry: myRetry` to **ProviderScope** or **ProviderContainer**.
32
+
33
+```dart
34
+// Global
35
+ProviderScope(retry: myRetry, child: MyApp())
36
+
37
+// Per provider (manual)
38
+final myProvider = Provider<int>(retry: myRetry, (ref) => 0);
39
+```
40
+
41
+### Disabling retry
42
+
43
+Return **null** always:
44
+
45
+```dart
46
+ProviderScope(retry: (retryCount, error) => null, child: MyApp())
47
+```
48
+
49
+### Awaiting async providers with retry
50
+
51
+`ref.watch(myProvider.future)` (or equivalent) keeps waiting until either all retries are exhausted or the provider succeeds. So `await ref.watch(myProvider.future)` skips intermediate failures and completes when the provider finally succeeds or gives up.
.claude/skills/riverpod-scoping/SKILL.md
new
+55
@@ -0,0 +1,55 @@
1
+---
2
+name: riverpod-scoping
3
+description: Scope Riverpod providers to a subtree using overrides and dependencies; page-specific behavior, ListView optimization, avoiding passing family parameters. Use when changing provider behavior for one page or widget, or when optimizing rebuilds. Use this skill when the user asks about scoping providers or provider scope.
4
+---
5
+
6
+# Riverpod — Scoping providers
7
+
8
+## Instructions
9
+
10
+**Scoping** means changing a provider's behavior for only part of the app by overriding it in a **non-root** ProviderScope. Use cases: page-specific customization (e.g. theme for one page), performance (e.g. rebuild only the item that changed in a ListView), or avoiding passing family parameters through the tree.
11
+
12
+**Note:** Scoping is complex and may be reworked in future Riverpod versions. Use with care.
13
+
14
+### Opt-in: dependencies
15
+
16
+By default you cannot scope a provider. Opt in by setting **dependencies** on the provider. The first scoped provider usually has `dependencies: const []`.
17
+
18
+```dart
19
+final currentItemIdProvider = Provider<String?>(
20
+ dependencies: const [],
21
+ (ref) => null,
22
+);
23
+```
24
+
25
+### Listening to a scoped provider
26
+
27
+Use ref.watch/ref.read as usual. If another **provider** listens to a scoped provider, that listening provider must list the scoped provider in its **dependencies**:
28
+
29
+```dart
30
+final currentItemProvider = FutureProvider<Item?>(
31
+ dependencies: [currentItemIdProvider],
32
+ (ref) async {
33
+ final currentItemId = ref.watch(currentItemIdProvider);
34
+ if (currentItemId == null) return null;
35
+ return fetchItem(id: currentItemId);
36
+ },
37
+);
38
+```
39
+
40
+Only scoped providers need to be listed in dependencies; non-scoped providers do not.
41
+
42
+### Setting the scoped value: overrides
43
+
44
+Use **overrides** on a ProviderScope that wraps the subtree where the scoped value should apply:
45
+
46
+```dart
47
+ProviderScope(
48
+ overrides: [
49
+ currentItemIdProvider.overrideWithValue('123'),
50
+ ],
51
+ child: const DetailPageView(),
52
+)
53
+```
54
+
55
+That way the detail page sees `'123'` as the current item ID without it being passed down manually. See riverpod-overrides for override syntax.
.claude/skills/riverpod-select/SKILL.md
new
+30
@@ -0,0 +1,30 @@
1
+---
2
+name: riverpod-select
3
+description: Reduce rebuilds with ref.watch(provider.select(...)); select a subset of state so widgets rebuild only when that part changes. Use when optimizing performance or when a widget uses only one field of a large object. Use this skill when the user asks about select, reducing rebuilds, or optimizing Riverpod watch.
4
+---
5
+
6
+# Riverpod — Reducing rebuilds with select
7
+
8
+## Instructions
9
+
10
+By default, **ref.watch(provider)** rebuilds whenever the provider's value changes (by reference/equality). If you only use a subset of the state (e.g. one field of a User), the widget still rebuilds when other fields change.
11
+
12
+Use **select** to watch only a part of the state. Rebuilds happen only when the **selected** value changes (by ==).
13
+
14
+### Syntax
15
+
16
+```dart
17
+// Rebuild only when firstName changes
18
+String name = ref.watch(provider.select((it) => it.firstName));
19
+return Text('Hello $name');
20
+```
21
+
22
+You can call select multiple times for different properties. The selected value should be **immutable**; mutating a returned List/object in place will not trigger a rebuild.
23
+
24
+### Async providers: selectAsync
25
+
26
+For async providers you often use **ref.watch(provider.future)**. To select on the resolved data instead of the whole AsyncValue, use **selectAsync** (when available): it applies a select on the data and returns a Future. See the Riverpod docs for the exact API in your version.
27
+
28
+### When to use
29
+
30
+- Only when the "other" properties change often and you have measured that rebuilds are a bottleneck. Select adds a small cost and extra complexity. Prefer normal watch unless you need the optimization.
.claude/skills/riverpod-testing/SKILL.md
new
+93
@@ -0,0 +1,93 @@
1
+---
2
+name: riverpod-testing
3
+description: Test Riverpod providers and widgets; ProviderContainer.test, unit tests, widget tests with ProviderScope, tester.container(), mocking with overrides, container.listen for auto-dispose, awaiting .future. Use when writing unit or widget tests for Riverpod code, mocking providers, or testing with overrides. Use this skill when the user asks about testing Riverpod, mocking providers, or ProviderContainer in tests.
4
+---
5
+
6
+# Riverpod — Testing
7
+
8
+## Instructions
9
+
10
+Riverpod is designed for testability: isolate state per test, mock via overrides, and keep the test environment close to production.
11
+
12
+### Unit tests (no Flutter)
13
+
14
+Use **ProviderContainer.test()** to create a container for the test. Do not share containers between tests.
15
+
16
+```dart
17
+void main() {
18
+ test('Some description', () {
19
+ final container = ProviderContainer.test();
20
+ expect(container.read(provider), equals('some value'));
21
+ });
22
+}
23
+```
24
+
25
+- **container.read(provider)** — Read current value.
26
+- **container.listen(provider, (prev, next) {})** — Listen and get a subscription; use **subscription.read()** to read. Prefer listen when the provider is auto-dispose so it is not disposed mid-test.
27
+
28
+```dart
29
+final subscription = container.listen<String>(provider, (_, _) {});
30
+expect(subscription.read(), 'Some value');
31
+```
32
+
33
+### Widget tests
34
+
35
+Wrap the widget under test in **ProviderScope**:
36
+
37
+```dart
38
+testWidgets('Some description', (tester) async {
39
+ await tester.pumpWidget(
40
+ const ProviderScope(child: YourWidgetYouWantToTest()),
41
+ );
42
+});
43
+```
44
+
45
+To interact with providers in the test, get the container with **tester.container()**:
46
+
47
+```dart
48
+final container = tester.container();
49
+expect(container.read(provider), 'some value');
50
+```
51
+
52
+### Mocking providers
53
+
54
+Use **overrides** on ProviderContainer or ProviderScope. All providers can be overridden without extra setup.
55
+
56
+```dart
57
+final container = ProviderContainer.test(
58
+ overrides: [
59
+ exampleProvider.overrideWith((ref) => 'Hello from tests'),
60
+ ],
61
+);
62
+
63
+// Or in widget tests:
64
+await tester.pumpWidget(
65
+ ProviderScope(
66
+ overrides: [exampleProvider.overrideWith((ref) => 'Hello from tests')],
67
+ child: const YourWidgetYouWantToTest(),
68
+ ),
69
+);
70
+```
71
+
72
+See riverpod-overrides for family overrides and other override methods.
73
+
74
+### Awaiting async providers
75
+
76
+Read **provider.future** to get a Future that completes with the provider value; use with expectLater:
77
+
78
+```dart
79
+await expectLater(
80
+ container.read(provider.future),
81
+ completion('some value'),
82
+);
83
+```
84
+
85
+### Listening / spying
86
+
87
+Use **container.listen(provider, callback)** and assert on the callback arguments or collect values in a list for assertions. Works with mockito/mocktail verify patterns.
88
+
89
+### Mocking Notifiers
90
+
91
+Prefer mocking a dependency (e.g. repository) the Notifier uses rather than mocking the Notifier. If you must mock a Notifier, **subclass** it (do not implement), so the mock extends the original base class. With code generation, the mock usually needs to live in the same file as the Notifier to access the generated base class.
92
+
93
+See riverpod-overrides and the official docs for more examples.
.claude/skills/shadcn_ui-accordion/SKILL.md
new
+46
@@ -0,0 +1,46 @@
1
+---
2
+name: shadcn_ui-accordion
3
+description: Build collapsible accordions with ShadAccordion and ShadAccordionItem; single or multiple open items. Use when adding expandable sections, FAQs, or vertically stacked reveal content in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Accordion
7
+
8
+## Instructions
9
+
10
+Use `ShadAccordion` for a vertically stacked set of headings that each reveal content. Each item is a `ShadAccordionItem` with `value`, `title`, and `child`.
11
+
12
+### Single (one open at a time)
13
+
14
+```dart
15
+final details = [
16
+ (title: 'Is it acceptable?', content: 'Yes. It adheres to the WAI-ARIA design pattern.'),
17
+ (title: 'Is it styled?', content: "Yes. It comes with default styles that matches the other components' aesthetic."),
18
+ (title: 'Is it animated?', content: "Yes. It's animated by default, but you can disable it if you prefer."),
19
+];
20
+
21
+ShadAccordion<({String content, String title})>(
22
+ children: details.map(
23
+ (detail) => ShadAccordionItem(
24
+ value: detail,
25
+ title: Text(detail.title),
26
+ child: Text(detail.content),
27
+ ),
28
+ ),
29
+)
30
+```
31
+
32
+### Multiple (several open at once)
33
+
34
+Use `ShadAccordion.multiple`:
35
+
36
+```dart
37
+ShadAccordion<({String content, String title})>.multiple(
38
+ children: details.map(
39
+ (detail) => ShadAccordionItem(
40
+ value: detail,
41
+ title: Text(detail.title),
42
+ child: Text(detail.content),
43
+ ),
44
+ ),
45
+)
46
+```
.claude/skills/shadcn_ui-alert/SKILL.md
new
+30
@@ -0,0 +1,30 @@
1
+---
2
+name: shadcn_ui-alert
3
+description: Show callout alerts with ShadAlert and ShadAlert.destructive; icon, title, description. Use when displaying warnings, errors, or important messages in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Alert
7
+
8
+## Instructions
9
+
10
+`ShadAlert` displays a callout for user attention. Use default constructor for standard alerts, `ShadAlert.destructive` for errors or destructive actions.
11
+
12
+### Primary / default
13
+
14
+```dart
15
+ShadAlert(
16
+ icon: Icon(LucideIcons.terminal),
17
+ title: Text('Heads up!'),
18
+ description: Text('You can add components to your app using the cli.'),
19
+)
20
+```
21
+
22
+### Destructive
23
+
24
+```dart
25
+ShadAlert.destructive(
26
+ icon: Icon(LucideIcons.circleAlert),
27
+ title: Text('Error'),
28
+ description: Text('Your session has expired. Please log in again.'),
29
+)
30
+```
.claude/skills/shadcn_ui-avatar/SKILL.md
new
+21
@@ -0,0 +1,21 @@
1
+---
2
+name: shadcn_ui-avatar
3
+description: Display user avatars with ShadAvatar; image URL with optional placeholder. Use when showing user profile images or fallback initials in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Avatar
7
+
8
+## Instructions
9
+
10
+`ShadAvatar` is an image with a placeholder for representing the user. Uses the `universal_image` package for multiple image formats.
11
+
12
+### Basic usage
13
+
14
+```dart
15
+ShadAvatar(
16
+ 'https://example.com/avatar.png',
17
+ placeholder: Text('CN'),
18
+)
19
+```
20
+
21
+Pass the image URL as the first argument; `placeholder` is shown while loading or if the image fails (e.g. initials).
.claude/skills/shadcn_ui-badge/SKILL.md
new
+24
@@ -0,0 +1,24 @@
1
+---
2
+name: shadcn_ui-badge
3
+description: Display badges with ShadBadge; primary, secondary, destructive, outline variants. Use when showing tags, status labels, or count indicators in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Badge
7
+
8
+## Instructions
9
+
10
+`ShadBadge` displays a badge or badge-like component. Variants: default (primary), `ShadBadge.secondary`, `ShadBadge.destructive`, `ShadBadge.outline`.
11
+
12
+### Variants
13
+
14
+```dart
15
+ShadBadge(child: const Text('Primary'))
16
+
17
+ShadBadge.secondary(child: const Text('Secondary'))
18
+
19
+ShadBadge.destructive(child: const Text('Destructive'))
20
+
21
+ShadBadge.outline(child: const Text('Outline'))
22
+```
23
+
24
+Wrap any `child` (typically `Text`) with the desired constructor.
.claude/skills/shadcn_ui-breadcrumb/SKILL.md
new
+58
@@ -0,0 +1,58 @@
1
+---
2
+name: shadcn_ui-breadcrumb
3
+description: Build breadcrumb navigation with ShadBreadcrumb, ShadBreadcrumbLink, ShadBreadcrumbDropdown, custom separator. Use when showing navigation path, hierarchy of links, or dropdown breadcrumb in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Breadcrumb
7
+
8
+## Instructions
9
+
10
+`ShadBreadcrumb` displays the path to the current resource using a hierarchy of links. Children can be `ShadBreadcrumbLink`, `ShadBreadcrumbDropdown`, or plain widgets (e.g. `Text` for current page).
11
+
12
+### Basic breadcrumb
13
+
14
+```dart
15
+ShadBreadcrumb(
16
+ children: [
17
+ ShadBreadcrumbLink(
18
+ onPressed: () => print('Navigating to Home'),
19
+ child: const Text('Home'),
20
+ ),
21
+ ShadBreadcrumbDropdown(
22
+ items: [
23
+ ShadBreadcrumbDropMenuItem(
24
+ onPressed: () => print('Navigating to Documentation'),
25
+ child: const Text('Documentation'),
26
+ ),
27
+ ShadBreadcrumbDropMenuItem(
28
+ onPressed: () => print('Navigating to Themes'),
29
+ child: const Text('Themes'),
30
+ ),
31
+ ],
32
+ showDropdownArrow: false,
33
+ child: ShadBreadcrumbEllipsis(),
34
+ ),
35
+ Text('Components'),
36
+ Text('Breadcrumb'),
37
+ ],
38
+)
39
+```
40
+
41
+### Custom separator
42
+
43
+Default separator is `>`. Override with `separator`:
44
+
45
+```dart
46
+ShadBreadcrumb(
47
+ separator: const Icon(LucideIcons.slash),
48
+ children: [
49
+ ShadBreadcrumbLink(onPressed: () {}, child: const Text('Home')),
50
+ ShadBreadcrumbLink(onPressed: () {}, child: const Text('Components')),
51
+ Text('Breadcrumb'),
52
+ ],
53
+)
54
+```
55
+
56
+### Dropdown in breadcrumb
57
+
58
+Use `ShadBreadcrumbDropdown` with `items` (list of `ShadBreadcrumbDropMenuItem`) and `child` (trigger widget). Use `ShadBreadcrumbEllipsis()` for an ellipsis trigger.
.claude/skills/shadcn_ui-button/SKILL.md
new
+72
@@ -0,0 +1,72 @@
1
+---
2
+name: shadcn_ui-button
3
+description: Use Shadcn UI buttons (ShadButton variants: primary, secondary, destructive, outline, ghost, link); leading icon, loading state, gradient and shadow. Use when adding buttons, CTAs, or button-style widgets in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Button
7
+
8
+## Instructions
9
+
10
+`ShadButton` displays a button or component that looks like a button. Variants: default (primary), `ShadButton.secondary`, `ShadButton.destructive`, `ShadButton.outline`, `ShadButton.ghost`, `ShadButton.link`. Use `leading` (and optionally `trailing`) for icons; support gradient and shadows for custom styling.
11
+
12
+### Variants
13
+
14
+```dart
15
+ShadButton(child: const Text('Primary'), onPressed: () {})
16
+
17
+ShadButton.secondary(child: const Text('Secondary'), onPressed: () {})
18
+
19
+ShadButton.destructive(child: const Text('Destructive'), onPressed: () {})
20
+
21
+ShadButton.outline(child: const Text('Outline'), onPressed: () {})
22
+
23
+ShadButton.ghost(child: const Text('Ghost'), onPressed: () {})
24
+
25
+ShadButton.link(child: const Text('Link'), onPressed: () {})
26
+```
27
+
28
+### Text and icon
29
+
30
+```dart
31
+ShadButton(
32
+ onPressed: () {},
33
+ leading: const Icon(LucideIcons.mail),
34
+ child: const Text('Login with Email'),
35
+)
36
+```
37
+
38
+### Loading state
39
+
40
+Use `leading` with a small `CircularProgressIndicator`:
41
+
42
+```dart
43
+ShadButton(
44
+ onPressed: () {},
45
+ leading: SizedBox.square(
46
+ dimension: 16,
47
+ child: CircularProgressIndicator(
48
+ strokeWidth: 2,
49
+ color: ShadTheme.of(context).colorScheme.primaryForeground,
50
+ ),
51
+ ),
52
+ child: const Text('Please wait'),
53
+)
54
+```
55
+
56
+### Gradient and shadow
57
+
58
+```dart
59
+ShadButton(
60
+ onPressed: () {},
61
+ gradient: const LinearGradient(colors: [Colors.cyan, Colors.indigo]),
62
+ shadows: [
63
+ BoxShadow(
64
+ color: Colors.blue.withValues(alpha:.4),
65
+ spreadRadius: 4,
66
+ blurRadius: 10,
67
+ offset: const Offset(0, 2),
68
+ ),
69
+ ],
70
+ child: const Text('Gradient with Shadow'),
71
+)
72
+```
.claude/skills/shadcn_ui-calendar/SKILL.md
new
+63
@@ -0,0 +1,63 @@
1
+---
2
+name: shadcn_ui-calendar
3
+description: Use ShadCalendar for single, multiple, or range date selection; caption layout, hide navigation, week numbers, outside days, fixed weeks. Use when building date pickers or calendar widgets in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Calendar
7
+
8
+## Instructions
9
+
10
+`ShadCalendar` is a date field that lets users enter and edit dates. Modes: single date, multiple dates (`ShadCalendar.multiple`), or range (`ShadCalendar.range`). Use `fromMonth`/`toMonth` to limit range; `captionLayout` for dropdown months/years.
11
+
12
+### Single
13
+
14
+```dart
15
+final today = DateTime.now();
16
+
17
+ShadCalendar(
18
+ selected: today,
19
+ fromMonth: DateTime(today.year - 1),
20
+ toMonth: DateTime(today.year, 12),
21
+)
22
+```
23
+
24
+### Multiple
25
+
26
+```dart
27
+ShadCalendar.multiple(
28
+ numberOfMonths: 2,
29
+ fromMonth: DateTime(today.year),
30
+ toMonth: DateTime(today.year + 1, 12),
31
+ min: 5,
32
+ max: 10,
33
+)
34
+```
35
+
36
+### Range
37
+
38
+```dart
39
+const ShadCalendar.range(
40
+ min: 2,
41
+ max: 5,
42
+)
43
+```
44
+
45
+### Caption layout
46
+
47
+```dart
48
+ShadCalendar(
49
+ captionLayout: ShadCalendarCaptionLayout.dropdownMonths,
50
+);
51
+
52
+ShadCalendar(
53
+ captionLayout: ShadCalendarCaptionLayout.dropdownYears,
54
+);
55
+```
56
+
57
+### Options
58
+
59
+- `hideNavigation: true` — Hide month navigation.
60
+- `showWeekNumbers: true` — Show week numbers.
61
+- `showOutsideDays: false` — Hide days from adjacent months.
62
+- `fixedWeeks: true` — Fixed number of rows.
63
+- `hideWeekdayNames: true` — Hide weekday headers.
.claude/skills/shadcn_ui-card/SKILL.md
new
+53
@@ -0,0 +1,53 @@
1
+---
2
+name: shadcn_ui-card
3
+description: Build cards with ShadCard; title, description, footer, and child content. Use when creating content cards, project cards, or notification panels in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Card
7
+
8
+## Instructions
9
+
10
+`ShadCard` displays a card with optional header (title, description), body (`child`), and footer. Use `width` to constrain width.
11
+
12
+### Basic card
13
+
14
+```dart
15
+final theme = ShadTheme.of(context);
16
+ShadCard(
17
+ width: 350,
18
+ title: Text('Create project', style: theme.textTheme.h4),
19
+ description: const Text('Deploy your new project in one-click.'),
20
+ footer: Row(
21
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
22
+ children: [
23
+ ShadButton.outline(child: const Text('Cancel'), onPressed: () {}),
24
+ ShadButton(child: const Text('Deploy'), onPressed: () {}),
25
+ ],
26
+ ),
27
+ child: Padding(
28
+ padding: const EdgeInsets.symmetric(vertical: 16),
29
+ child: Column(
30
+ mainAxisSize: MainAxisSize.min,
31
+ crossAxisAlignment: CrossAxisAlignment.stretch,
32
+ children: [
33
+ const Text('Name'),
34
+ const SizedBox(height: 6),
35
+ const ShadInput(placeholder: Text('Name of your project')),
36
+ const SizedBox(height: 16),
37
+ const Text('Framework'),
38
+ const SizedBox(height: 6),
39
+ ShadSelect<String>(
40
+ placeholder: const Text('Select'),
41
+ options: frameworks.entries
42
+ .map((e) => ShadOption(value: e.key, child: Text(e.value)))
43
+ .toList(),
44
+ selectedOptionBuilder: (context, value) => Text(frameworks[value]!),
45
+ onChanged: (value) {},
46
+ ),
47
+ ],
48
+ ),
49
+ ),
50
+)
51
+```
52
+
53
+Title and description are optional. Footer can be any widget (e.g. buttons). Put main content in `child`.
.claude/skills/shadcn_ui-checkbox/SKILL.md
new
+39
@@ -0,0 +1,39 @@
1
+---
2
+name: shadcn_ui-checkbox
3
+description: Add checkboxes with ShadCheckbox and ShadCheckboxFormField; label, sublabel, form validation. Use when adding boolean toggles or terms acceptance in a Flutter shadcn_ui app or ShadForm.
4
+---
5
+
6
+# Shadcn UI — Checkbox
7
+
8
+## Instructions
9
+
10
+`ShadCheckbox` is a control that toggles between checked and unchecked. Use `value` and `onChanged`; optional `label` and `sublabel`. For forms use `ShadCheckboxFormField` with `id`, `validator`, and form state.
11
+
12
+### Standalone
13
+
14
+```dart
15
+ShadCheckbox(
16
+ value: value,
17
+ onChanged: (v) => setState(() => value = v),
18
+ label: const Text('Accept terms and conditions'),
19
+ sublabel: const Text(
20
+ 'You agree to our Terms of Service and Privacy Policy.',
21
+ ),
22
+)
23
+```
24
+
25
+### Form field
26
+
27
+```dart
28
+ShadCheckboxFormField(
29
+ id: 'terms',
30
+ initialValue: false,
31
+ inputLabel: const Text('I accept the terms and conditions'),
32
+ onChanged: (v) {},
33
+ inputSublabel: const Text('You agree to our Terms and Conditions'),
34
+ validator: (v) {
35
+ if (!v) return 'You must accept the terms and conditions';
36
+ return null;
37
+ },
38
+)
39
+```
.claude/skills/shadcn_ui-context-menu/SKILL.md
new
+65
@@ -0,0 +1,65 @@
1
+---
2
+name: shadcn_ui-context-menu
3
+description: Show context menus on right-click with ShadContextMenuRegion, ShadContextMenuItem, submenus, dividers. Use when adding right-click menus, actions, or nested menu items in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Context Menu
7
+
8
+## Instructions
9
+
10
+`ShadContextMenuRegion` wraps a `child` and shows a menu on right-click. Set `items` to a list of `ShadContextMenuItem` or `ShadContextMenuItem.inset`. Use `ShadContextMenuItem` with `items` for submenus; use `Divider(height: 8)` and `Padding` for sections and labels.
11
+
12
+### Basic usage
13
+
14
+```dart
15
+ShadContextMenuRegion(
16
+ constraints: const BoxConstraints(minWidth: 300),
17
+ items: [
18
+ const ShadContextMenuItem.inset(child: Text('Back')),
19
+ const ShadContextMenuItem.inset(
20
+ enabled: false,
21
+ child: Text('Forward'),
22
+ ),
23
+ const ShadContextMenuItem.inset(child: Text('Reload')),
24
+ const ShadContextMenuItem.inset(
25
+ trailing: Icon(LucideIcons.chevronRight),
26
+ items: [
27
+ ShadContextMenuItem(child: Text('Save Page As...')),
28
+ ShadContextMenuItem(child: Text('Create Shortcut...')),
29
+ ShadContextMenuItem(child: Text('Developer Tools')),
30
+ Divider(height: 8),
31
+ ShadContextMenuItem(child: Text('Developer Tools')),
32
+ ],
33
+ child: Text('More Tools'),
34
+ ),
35
+ const Divider(height: 8),
36
+ const ShadContextMenuItem(
37
+ leading: Icon(LucideIcons.check),
38
+ child: Text('Show Bookmarks Bar'),
39
+ ),
40
+ const ShadContextMenuItem.inset(child: Text('Show Full URLs')),
41
+ const Divider(height: 8),
42
+ Padding(
43
+ padding: const EdgeInsets.fromLTRB(36, 8, 8, 8),
44
+ child: Text('People', style: theme.textTheme.small),
45
+ ),
46
+ ShadContextMenuItem(
47
+ leading: /* avatar or icon */,
48
+ child: const Text('Pedro Duarte'),
49
+ ),
50
+ const ShadContextMenuItem.inset(child: Text('Colm Tuite')),
51
+ ],
52
+ child: Container(
53
+ width: 300,
54
+ height: 200,
55
+ alignment: Alignment.center,
56
+ decoration: BoxDecoration(
57
+ border: Border.all(color: theme.colorScheme.border),
58
+ borderRadius: BorderRadius.circular(8),
59
+ ),
60
+ child: const Text('Right click here'),
61
+ ),
62
+)
63
+```
64
+
65
+Use `ShadContextMenuItem` for items with leading/trailing; `ShadContextMenuItem.inset` for indented items. Nest `items` for submenus.
.claude/skills/shadcn_ui-date-picker/SKILL.md
new
+85
@@ -0,0 +1,85 @@
1
+---
2
+name: shadcn_ui-date-picker
3
+description: Use ShadDatePicker and ShadDatePicker.range for single or range date selection; presets, ShadDatePickerFormField, ShadDateRangePickerFormField. Use when adding date pickers or date range fields in a Flutter shadcn_ui app or ShadForm.
4
+---
5
+
6
+# Shadcn UI — Date Picker
7
+
8
+## Instructions
9
+
10
+`ShadDatePicker` is a date picker with optional range and presets. Use `ShadDatePicker.range()` for range selection. For forms use `ShadDatePickerFormField` and `ShadDateRangePickerFormField` with `id`, `label`, `validator`, and optional `fromValueTransformer`/`toValueTransformer`.
11
+
12
+### Single
13
+
14
+```dart
15
+ConstrainedBox(
16
+ constraints: const BoxConstraints(maxWidth: 600),
17
+ child: const ShadDatePicker(),
18
+)
19
+```
20
+
21
+### Range
22
+
23
+```dart
24
+ConstrainedBox(
25
+ constraints: const BoxConstraints(maxWidth: 600),
26
+ child: const ShadDatePicker.range(),
27
+)
28
+```
29
+
30
+### With presets
31
+
32
+Use a `header` widget (e.g. `ShadSelect`) and shared `groupId` so the date picker popover stays open when the select popover closes. Set `selected` from preset choice (e.g. today + days offset).
33
+
34
+```dart
35
+ShadDatePicker(
36
+ groupId: groupId,
37
+ header: Padding(
38
+ padding: const EdgeInsets.only(bottom: 4),
39
+ child: ShadSelect<int>(
40
+ groupId: groupId,
41
+ minWidth: 276,
42
+ placeholder: const Text('Select'),
43
+ options: presets.entries
44
+ .map((e) => ShadOption(value: e.key, child: Text(e.value)))
45
+ .toList(),
46
+ selectedOptionBuilder: (context, value) => Text(presets[value]!),
47
+ onChanged: (value) {
48
+ if (value == null) return;
49
+ setState(() => selected = today.add(Duration(days: value)));
50
+ },
51
+ ),
52
+ ),
53
+ selected: selected,
54
+ calendarDecoration: theme.calendarTheme.decoration,
55
+ popoverPadding: const EdgeInsets.all(4),
56
+)
57
+```
58
+
59
+### Form fields
60
+
61
+```dart
62
+ShadDatePickerFormField(
63
+ label: const Text('Date of birth'),
64
+ onChanged: print,
65
+ description: const Text('Your date of birth is used to calculate your age.'),
66
+ validator: (v) {
67
+ if (v == null) return 'A date of birth is required.';
68
+ return null;
69
+ },
70
+)
71
+
72
+ShadDateRangePickerFormField(
73
+ label: const Text('Range of dates'),
74
+ onChanged: print,
75
+ description: const Text('Select the range of dates you want to search between.'),
76
+ validator: (v) {
77
+ if (v == null) return 'A range of dates is required.';
78
+ if (v.start == null) return 'The start date is required.';
79
+ if (v.end == null) return 'The end date is required.';
80
+ return null;
81
+ },
82
+)
83
+```
84
+
85
+For form initial value as string (e.g. `'date': '2024-02-01'`), use `fromValueTransformer` and `toValueTransformer` on `ShadDatePickerFormField` to convert to/from `DateTime`. See [reference.md](reference.md) or the Form skill for value transformers.
.claude/skills/shadcn_ui-decorator/SKILL.md
new
+57
@@ -0,0 +1,57 @@
1
+---
2
+name: shadcn_ui-decorator
3
+description: Configure ShadDecoration and ShadDecorator for shadcn_ui components; customize secondary border and disableSecondaryBorder. Use when styling form controls, focus rings, or component borders in Flutter shadcn_ui.
4
+---
5
+
6
+# Shadcn UI — Decorator
7
+
8
+## Instructions
9
+
10
+Most shadcn_ui components use a `ShadDecoration` handled by `ShadDecorator` for borders, focus state, labels, and error/description styles.
11
+
12
+### Default decoration
13
+
14
+Default includes secondary border, focused ring, label/error/description styles and padding:
15
+
16
+```dart
17
+ShadDecoration(
18
+ secondaryBorder: ShadBorder.all(
19
+ padding: const EdgeInsets.all(4),
20
+ width: 0,
21
+ ),
22
+ secondaryFocusedBorder: ShadBorder.all(
23
+ width: 2,
24
+ color: colorScheme.ring,
25
+ radius: radius.add(radius / 2),
26
+ padding: const EdgeInsets.all(2),
27
+ ),
28
+ labelStyle: textTheme.muted.copyWith(
29
+ fontWeight: FontWeight.w500,
30
+ color: colorScheme.foreground,
31
+ ),
32
+ errorStyle: textTheme.muted.copyWith(
33
+ fontWeight: FontWeight.w500,
34
+ color: colorScheme.destructive,
35
+ ),
36
+ labelPadding: const EdgeInsets.only(bottom: 8),
37
+ descriptionStyle: textTheme.muted,
38
+ descriptionPadding: const EdgeInsets.only(top: 8),
39
+ errorPadding: const EdgeInsets.only(top: 8),
40
+ errorLabelStyle: textTheme.muted.copyWith(
41
+ fontWeight: FontWeight.w500,
42
+ color: colorScheme.destructive,
43
+ ),
44
+);
45
+```
46
+
47
+### Disable secondary border
48
+
49
+By default a secondary border is drawn around focusable components. To disable it and rely on a bolder primary border:
50
+
51
+```dart
52
+ShadThemeData(
53
+ disableSecondaryBorder: true,
54
+),
55
+```
56
+
57
+**Note:** Disabling the secondary border is not recommended; it can hurt accessibility because the focus ring helps users see which component is focused.
.claude/skills/shadcn_ui-dialog/SKILL.md
new
+83
@@ -0,0 +1,83 @@
1
+---
2
+name: shadcn_ui-dialog
3
+description: Show modal dialogs with showShadDialog, ShadDialog and ShadDialog.alert; title, description, actions, child content. Use when adding modals, confirmations, or alert dialogs in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Dialog
7
+
8
+## Instructions
9
+
10
+A modal dialog that interrupts the user. Use `showShadDialog(context: context, builder: ...)` and return a `ShadDialog` or `ShadDialog.alert`. Provide `title`, optional `description`, optional `actions` (list of widgets, e.g. buttons), and optional `child` for body content. Dismiss with `Navigator.of(context).pop(...)`.
11
+
12
+### Standard dialog
13
+
14
+```dart
15
+showShadDialog(
16
+ context: context,
17
+ builder: (context) => ShadDialog(
18
+ title: const Text('Edit Profile'),
19
+ description: const Text(
20
+ "Make changes to your profile here. Click save when you're done",
21
+ ),
22
+ actions: const [ShadButton(child: Text('Save changes'))],
23
+ child: Container(
24
+ width: 375,
25
+ padding: const EdgeInsets.symmetric(vertical: 20),
26
+ child: Column(
27
+ mainAxisSize: MainAxisSize.min,
28
+ crossAxisAlignment: CrossAxisAlignment.end,
29
+ spacing: 16,
30
+ children: profile
31
+ .map(
32
+ (p) => Row(
33
+ children: [
34
+ Expanded(
35
+ child: Text(
36
+ p.title,
37
+ textAlign: TextAlign.end,
38
+ style: theme.textTheme.small,
39
+ ),
40
+ ),
41
+ const SizedBox(width: 16),
42
+ Expanded(
43
+ flex: 3,
44
+ child: ShadInput(initialValue: p.value),
45
+ ),
46
+ ],
47
+ ),
48
+ )
49
+ .toList(),
50
+ ),
51
+ ),
52
+ ),
53
+);
54
+```
55
+
56
+### Alert dialog
57
+
58
+Use `ShadDialog.alert` for confirmations; pass `actions` with Cancel and Continue (or similar) that call `Navigator.of(context).pop(false)` and `Navigator.of(context).pop(true)`.
59
+
60
+```dart
61
+showShadDialog(
62
+ context: context,
63
+ builder: (context) => ShadDialog.alert(
64
+ title: const Text('Are you absolutely sure?'),
65
+ description: const Padding(
66
+ padding: EdgeInsets.only(bottom: 8),
67
+ child: Text(
68
+ 'This action cannot be undone. This will permanently delete your account and remove your data from our servers.',
69
+ ),
70
+ ),
71
+ actions: [
72
+ ShadButton.outline(
73
+ child: const Text('Cancel'),
74
+ onPressed: () => Navigator.of(context).pop(false),
75
+ ),
76
+ ShadButton(
77
+ child: const Text('Continue'),
78
+ onPressed: () => Navigator.of(context).pop(true),
79
+ ),
80
+ ],
81
+ ),
82
+);
83
+```
.claude/skills/shadcn_ui-form/SKILL.md
new
+100
@@ -0,0 +1,100 @@
1
+---
2
+name: shadcn_ui-form
3
+description: Build forms with ShadForm, validation, and form field values; use ShadInputFormField, ShadCheckboxFormField, ShadSelectFormField, ShadDatePickerFormField, etc. Use when building validated forms, getting form values, or using dot notation for nested fields in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Form
7
+
8
+## Instructions
9
+
10
+`ShadForm` provides centralized form state, a single `Map<String, dynamic>` for all field values, and no need to manage individual controllers. Use a `GlobalKey<ShadFormState>` to call `saveAndValidate()` and read `value`. Give each form field an `id`; use `Shad*FormField` widgets (e.g. `ShadInputFormField`, `ShadCheckboxFormField`, `ShadSelectFormField`, `ShadDatePickerFormField`, `ShadTimePickerFormField`, `ShadRadioGroupFormField`) inside `ShadForm`.
11
+
12
+### Basic form
13
+
14
+```dart
15
+final formKey = GlobalKey<ShadFormState>();
16
+
17
+ShadForm(
18
+ key: formKey,
19
+ child: Column(
20
+ children: [
21
+ ShadInputFormField(
22
+ id: 'username',
23
+ label: const Text('Username'),
24
+ placeholder: const Text('Enter your username'),
25
+ description: const Text('This is your public display name.'),
26
+ validator: (v) {
27
+ if (v.length < 2) return 'Username must be at least 2 characters.';
28
+ return null;
29
+ },
30
+ ),
31
+ const SizedBox(height: 16),
32
+ ShadButton(
33
+ child: const Text('Submit'),
34
+ onPressed: () {
35
+ if (formKey.currentState!.saveAndValidate()) {
36
+ print('value: ${formKey.currentState!.value}');
37
+ } else {
38
+ print('validation failed');
39
+ }
40
+ },
41
+ ),
42
+ ],
43
+ ),
44
+)
45
+```
46
+
47
+### Submit and get value
48
+
49
+```dart
50
+ShadButton(
51
+ child: const Text('Submit'),
52
+ onPressed: () {
53
+ final formState = formKey.currentState!;
54
+ if (!formState.saveAndValidate()) return;
55
+ print('Form value: ${formState.value}');
56
+ },
57
+),
58
+```
59
+
60
+### Initial value
61
+
62
+Set default values with `initialValue` on `ShadForm`:
63
+
64
+```dart
65
+ShadForm(
66
+ initialValue: {
67
+ 'username': 'john_doe',
68
+ 'email': 'john_doe@example.com',
69
+ },
70
+ child: // form fields with matching ids
71
+)
72
+```
73
+
74
+### Set field or full value
75
+
76
+- Single field: `formKey.currentState!.setFieldValue('username', 'new_username');` (use `notifyField: false` to skip updating the field UI).
77
+- Entire form: `formKey.currentState!.setValue({...});` (use `notifyFields: false` to skip updating fields).
78
+
79
+### Value transformers
80
+
81
+Use `fromValueTransformer` and `toValueTransformer` on form fields when the form value type differs from the field type (e.g. form stores string, field uses `DateTime`):
82
+
83
+```dart
84
+ShadDatePickerFormField(
85
+ id: 'date',
86
+ fromValueTransformer: (value) => DateTime.tryParse(value ?? ''),
87
+ toValueTransformer: (date) =>
88
+ date == null ? null : DateFormat('yyyy-MM-dd').format(date),
89
+ ...
90
+)
91
+```
92
+
93
+### Dot notation for nested values
94
+
95
+Field IDs like `user.name` or `profile.settings.theme` produce nested maps in `formKey.currentState!.value`, e.g. `{'user': {'name': '...', 'email': '...'}}`. Provide `initialValue` as a nested map (not dot-notation keys). Customize separator with `fieldIdSeparator` (e.g. `'/'`); set `fieldIdSeparator: null` to disable nesting and keep flat keys.
96
+
97
+## Additional resources
98
+
99
+- Full details on initial value, setFieldValue/setValue, value transformers, and dot notation: [reference.md](reference.md)
100
+- Form field examples: Checkbox, Switch, Input, Select, RadioGroup, DatePicker, TimePicker component docs/skills.
.claude/skills/shadcn_ui-form/reference.md
new
+47
@@ -0,0 +1,47 @@
1
+# Shadcn UI Form — Reference
2
+
3
+## Initial form value
4
+
5
+All form fields with matching `id`s are initialized from `ShadForm`'s `initialValue` map. Fields with their own `initialValue` override the form's.
6
+
7
+## Get the form value
8
+
9
+`formKey.currentState!.value` returns `Map<String, dynamic>`. Use after a successful `saveAndValidate()`.
10
+
11
+## setFieldValue
12
+
13
+`formKey.currentState!.setFieldValue('username', 'new_username');`
14
+
15
+- Pass `notifyField: false` to update only the form value, not the field UI.
16
+
17
+## setValue
18
+
19
+`formKey.currentState!.setValue({ 'username': '...', 'email': '...' });`
20
+
21
+- Pass `notifyFields: false` to update only the form value, not the fields UI.
22
+
23
+## Value transformers
24
+
25
+- **fromValueTransformer**: Converts form value (e.g. string) to field value (e.g. DateTime).
26
+- **toValueTransformer**: Converts field value back to form value for `value` and validation.
27
+
28
+Example: form has `{'date': '2024-02-01'}`; `ShadDatePickerFormField` needs `DateTime`. Use `fromValueTransformer: (value) => DateTime.tryParse(value ?? '')` and `toValueTransformer: (date) => date == null ? null : DateFormat('yyyy-MM-dd').format(date)`.
29
+
30
+## Dot notation
31
+
32
+- IDs like `user.email` or `profile.settings.theme` create nested maps in `value`.
33
+- `initialValue` must be nested: `{'user': {'name': 'John', 'email': '...'}}`.
34
+- Custom separator: `ShadForm(fieldIdSeparator: '/', ...)` then use `user/name`.
35
+- Disable: `ShadForm(fieldIdSeparator: null, ...)` so `user.email` stays a flat key.
36
+
37
+## Form field components
38
+
39
+Use inside `ShadForm` with unique `id` and optional `validator`:
40
+
41
+- ShadInputFormField
42
+- ShadCheckboxFormField
43
+- ShadSwitchFormField
44
+- ShadSelectFormField
45
+- ShadRadioGroupFormField
46
+- ShadDatePickerFormField / ShadDateRangePickerFormField
47
+- ShadTimePickerFormField
.claude/skills/shadcn_ui-getting-started/SKILL.md
new
+122
@@ -0,0 +1,122 @@
1
+---
2
+name: shadcn_ui-getting-started
3
+description: Install and set up Flutter Shadcn UI (shadcn_ui), configure ShadApp with Material or Cupertino, and use bundled packages. Use when starting a Flutter project with shadcn_ui, adding shadcn_ui dependency, or integrating Shadcn with MaterialApp/CupertinoApp.
4
+---
5
+
6
+# Shadcn UI for Flutter — Getting Started
7
+
8
+## Instructions
9
+
10
+### Installation
11
+
12
+From the project root:
13
+
14
+```bash
15
+flutter pub add shadcn_ui
16
+```
17
+
18
+Or add to `pubspec.yaml`:
19
+
20
+```yaml
21
+dependencies:
22
+ shadcn_ui: ^0.2.4 # use latest version
23
+```
24
+
25
+### Shadcn only (pure)
26
+
27
+Use `ShadApp` for apps that use only Shadcn UI components (no Material/Cupertino):
28
+
29
+```dart
30
+import 'package:shadcn_ui/shadcn_ui.dart';
31
+
32
+void main() {
33
+ runApp(const MyApp());
34
+}
35
+
36
+class MyApp extends StatelessWidget {
37
+ const MyApp({super.key});
38
+
39
+ @override
40
+ Widget build(BuildContext context) {
41
+ return ShadApp();
42
+ }
43
+}
44
+```
45
+
46
+For declarative routing use `ShadApp.router`.
47
+
48
+### Shadcn + Material
49
+
50
+Use `ShadApp.custom` with `appBuilder` to wrap `MaterialApp` and use shadcn and Material together. Wrap the app with `ShadAppBuilder` in the builder:
51
+
52
+```dart
53
+import 'package:shadcn_ui/shadcn_ui.dart';
54
+import 'package:flutter/material.dart';
55
+
56
+return ShadApp.custom(
57
+ themeMode: ThemeMode.dark,
58
+ darkTheme: ShadThemeData(
59
+ brightness: Brightness.dark,
60
+ colorScheme: const ShadSlateColorScheme.dark(),
61
+ ),
62
+ appBuilder: (context) {
63
+ return MaterialApp(
64
+ theme: Theme.of(context),
65
+ builder: (context, child) {
66
+ return ShadAppBuilder(child: child!);
67
+ },
68
+ );
69
+ },
70
+);
71
+```
72
+
73
+Use `MaterialApp.router` when using the Router API.
74
+
75
+ShadApp builds a default Material `ThemeData` from `ShadThemeData` (fontFamily, extensions, colorScheme mapping, dividerTheme, textSelectionTheme, iconTheme, scrollbarTheme). Override with `Theme.of(context).copyWith(...)` without losing shadcn defaults.
76
+
77
+### Shadcn + Cupertino
78
+
79
+Use `CupertinoApp` inside `appBuilder` for Cupertino + shadcn:
80
+
81
+```dart
82
+import 'package:shadcn_ui/shadcn_ui.dart';
83
+import 'package:flutter/cupertino.dart';
84
+import 'package:flutter_localizations/flutter_localizations.dart';
85
+
86
+return ShadApp.custom(
87
+ themeMode: ThemeMode.dark,
88
+ darkTheme: ShadThemeData(
89
+ brightness: Brightness.dark,
90
+ colorScheme: const ShadSlateColorScheme.dark(),
91
+ ),
92
+ appBuilder: (context) {
93
+ return CupertinoApp(
94
+ theme: CupertinoTheme.of(context),
95
+ localizationsDelegates: const [
96
+ DefaultMaterialLocalizations.delegate,
97
+ DefaultCupertinoLocalizations.delegate,
98
+ DefaultWidgetsLocalizations.delegate,
99
+ ],
100
+ builder: (context, child) {
101
+ return ShadAppBuilder(child: child!);
102
+ },
103
+ );
104
+ },
105
+);
106
+```
107
+
108
+Use `CupertinoApp.router` for Router-based navigation. Override Cupertino theme with `CupertinoTheme.of(context).copyWith(...)`.
109
+
110
+### Bundled packages
111
+
112
+Shadcn UI re-exports these packages; no extra imports needed:
113
+
114
+- **flutter_animate** — Animations; components accept `List<Effect>` for customization.
115
+- **lucide_icons_flutter** — Icons via `LucideIcons` (e.g. `LucideIcons.activity`). Browse at https://lucide.dev/icons/
116
+- **two_dimensional_scrollables** — Used by `ShadTable`.
117
+- **intl** — i18n and message translation.
118
+- **universal_image** — Multiple image formats; used by `ShadAvatar`.
119
+
120
+### Submit your app
121
+
122
+To add your app to the showcase: [open the GitHub template](https://github.com/nank1ro/flutter-shadcn-ui/issues/new?template=docs-add-app.yaml) and fill the form.
.claude/skills/shadcn_ui-icon-button/SKILL.md
new
+60
@@ -0,0 +1,60 @@
1
+---
2
+name: shadcn_ui-icon-button
3
+description: Use ShadIconButton and variants (primary, secondary, destructive, outline, ghost); loading state, gradient and shadow. Use when adding icon-only buttons or icon CTAs in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Icon Button
7
+
8
+## Instructions
9
+
10
+`ShadIconButton` displays an icon button. Variants: default (primary), `ShadIconButton.secondary`, `ShadIconButton.destructive`, `ShadIconButton.outline`, `ShadIconButton.ghost`. Use `icon` and `onPressed`; optional `iconSize`, `padding`; support gradient and shadows.
11
+
12
+### Variants
13
+
14
+```dart
15
+ShadIconButton(
16
+ onPressed: () => print('Primary'),
17
+ icon: const Icon(LucideIcons.rocket),
18
+)
19
+
20
+ShadIconButton.secondary(icon: const Icon(LucideIcons.rocket), onPressed: () {})
21
+
22
+ShadIconButton.destructive(icon: const Icon(LucideIcons.rocket), onPressed: () {})
23
+
24
+ShadIconButton.outline(icon: const Icon(LucideIcons.rocket), onPressed: () {})
25
+
26
+ShadIconButton.ghost(icon: const Icon(LucideIcons.rocket), onPressed: () {})
27
+```
28
+
29
+### Loading
30
+
31
+Use a small `CircularProgressIndicator` as `icon`:
32
+
33
+```dart
34
+ShadIconButton(
35
+ icon: SizedBox.square(
36
+ dimension: 16,
37
+ child: CircularProgressIndicator(
38
+ strokeWidth: 2,
39
+ color: ShadTheme.of(context).colorScheme.primaryForeground,
40
+ ),
41
+ ),
42
+)
43
+```
44
+
45
+### Gradient and shadow
46
+
47
+```dart
48
+ShadIconButton(
49
+ gradient: const LinearGradient(colors: [Colors.cyan, Colors.indigo]),
50
+ shadows: [
51
+ BoxShadow(
52
+ color: Colors.blue.withValues(alpha: .4),
53
+ spreadRadius: 4,
54
+ blurRadius: 10,
55
+ offset: const Offset(0, 2),
56
+ ),
57
+ ],
58
+ icon: const Icon(LucideIcons.rocket),
59
+)
60
+```
.claude/skills/shadcn_ui-input-otp/SKILL.md
new
+93
@@ -0,0 +1,93 @@
1
+---
2
+name: shadcn_ui-input-otp
3
+description: Build OTP input with ShadInputOTP, ShadInputOTPGroup, ShadInputOTPSlot; maxLength, inputFormatters, ShadInputOTPFormField. Use when adding one-time password or verification code inputs in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Input OTP
7
+
8
+## Instructions
9
+
10
+`ShadInputOTP` is an accessible one-time password component with copy-paste support. Use `children` to build the layout: `ShadInputOTPGroup` containing `ShadInputOTPSlot`s, with optional separators (e.g. `Icon(LucideIcons.dot)`). Set `maxLength`; use `onChanged` for the full OTP string. Restrict input with `keyboardType` and `inputFormatters` (e.g. `FilteringTextInputFormatter.digitsOnly`). For forms use `ShadInputOTPFormField` with `id`, `label`, `validator`.
11
+
12
+### Basic
13
+
14
+```dart
15
+ShadInputOTP(
16
+ onChanged: (v) => print('OTP: $v'),
17
+ maxLength: 6,
18
+ children: const [
19
+ ShadInputOTPGroup(
20
+ children: [
21
+ ShadInputOTPSlot(),
22
+ ShadInputOTPSlot(),
23
+ ShadInputOTPSlot(),
24
+ ],
25
+ ),
26
+ Icon(size: 24, LucideIcons.dot),
27
+ ShadInputOTPGroup(
28
+ children: [
29
+ ShadInputOTPSlot(),
30
+ ShadInputOTPSlot(),
31
+ ShadInputOTPSlot(),
32
+ ],
33
+ ),
34
+ ],
35
+)
36
+```
37
+
38
+### With input formatters (digits only)
39
+
40
+```dart
41
+ShadInputOTP(
42
+ onChanged: (v) => print('OTP: $v'),
43
+ maxLength: 4,
44
+ keyboardType: TextInputType.number,
45
+ inputFormatters: [
46
+ FilteringTextInputFormatter.digitsOnly,
47
+ ],
48
+ children: const [
49
+ ShadInputOTPGroup(
50
+ children: [
51
+ ShadInputOTPSlot(),
52
+ ShadInputOTPSlot(),
53
+ ShadInputOTPSlot(),
54
+ ShadInputOTPSlot(),
55
+ ],
56
+ ),
57
+ ],
58
+)
59
+```
60
+
61
+The package also provides `UpperCaseTextInputFormatter` and `LowerCaseTextInputFormatter`.
62
+
63
+### Form field
64
+
65
+```dart
66
+ShadInputOTPFormField(
67
+ id: 'otp',
68
+ maxLength: 6,
69
+ label: const Text('OTP'),
70
+ description: const Text('Enter your OTP.'),
71
+ validator: (v) {
72
+ if (v.contains(' ')) return 'Fill the whole OTP code';
73
+ return null;
74
+ },
75
+ children: const [
76
+ ShadInputOTPGroup(
77
+ children: [
78
+ ShadInputOTPSlot(),
79
+ ShadInputOTPSlot(),
80
+ ShadInputOTPSlot(),
81
+ ],
82
+ ),
83
+ Icon(size: 24, LucideIcons.dot),
84
+ ShadInputOTPGroup(
85
+ children: [
86
+ ShadInputOTPSlot(),
87
+ ShadInputOTPSlot(),
88
+ ShadInputOTPSlot(),
89
+ ],
90
+ ),
91
+ ],
92
+)
93
+```
.claude/skills/shadcn_ui-input/SKILL.md
new
+59
@@ -0,0 +1,59 @@
1
+---
2
+name: shadcn_ui-input
3
+description: Use ShadInput for text fields; placeholder, leading/trailing widgets, obscureText; ShadInputFormField for forms with validation. Use when adding text inputs, email/password fields, or form input fields in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Input
7
+
8
+## Instructions
9
+
10
+`ShadInput` displays a form input or input-like component. Use `placeholder`, `initialValue`, `keyboardType`, `obscureText`; optional `leading` and `trailing` widgets. For forms use `ShadInputFormField` with `id`, `label`, `description`, `validator`.
11
+
12
+### Basic
13
+
14
+```dart
15
+const ShadInput(
16
+ placeholder: Text('Email'),
17
+ keyboardType: TextInputType.emailAddress,
18
+)
19
+```
20
+
21
+### With leading and trailing
22
+
23
+Example: password with toggle visibility:
24
+
25
+```dart
26
+ShadInput(
27
+ placeholder: const Text('Password'),
28
+ obscureText: obscure,
29
+ leading: Icon(LucideIcons.lock),
30
+ trailing: SizedBox.square(
31
+ dimension: 24,
32
+ child: OverflowBox(
33
+ maxWidth: 28,
34
+ maxHeight: 28,
35
+ child: ShadIconButton(
36
+ iconSize: 20,
37
+ padding: EdgeInsets.all(2),
38
+ icon: Icon(obscure ? LucideIcons.eyeOff : LucideIcons.eye),
39
+ onPressed: () => setState(() => obscure = !obscure),
40
+ ),
41
+ ),
42
+ ),
43
+)
44
+```
45
+
46
+### Form field
47
+
48
+```dart
49
+ShadInputFormField(
50
+ id: 'username',
51
+ label: const Text('Username'),
52
+ placeholder: const Text('Enter your username'),
53
+ description: const Text('This is your public display name.'),
54
+ validator: (v) {
55
+ if (v.length < 2) return 'Username must be at least 2 characters.';
56
+ return null;
57
+ },
58
+)
59
+```
.claude/skills/shadcn_ui-menubar/SKILL.md
new
+70
@@ -0,0 +1,70 @@
1
+---
2
+name: shadcn_ui-menubar
3
+description: Build desktop-style menubars with ShadMenubar and ShadMenubarItem; nested items, ShadContextMenuItem, dividers. Use when adding a persistent top menu (File, Edit, View, etc.) in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Menubar
7
+
8
+## Instructions
9
+
10
+`ShadMenubar` is a visually persistent menu (e.g. File, Edit, View). Each top-level entry is a `ShadMenubarItem` with `child` (label) and `items` (list of menu entries). Use `ShadContextMenuItem` and `ShadContextMenuItem.inset` for entries; use `ShadSeparator.horizontal` or `Divider(height: 8)` for dividers. Nest `ShadContextMenuItem` with `items` for submenus; use `trailing: Icon(LucideIcons.chevronRight)` on parent. Set `enabled: false` to disable an item.
11
+
12
+### Structure
13
+
14
+```dart
15
+ShadMenubar(
16
+ items: [
17
+ ShadMenubarItem(
18
+ items: [
19
+ const ShadContextMenuItem(child: Text('New Tab')),
20
+ const ShadContextMenuItem(child: Text('New Window')),
21
+ const ShadContextMenuItem(
22
+ enabled: false,
23
+ child: Text('New Incognito Window'),
24
+ ),
25
+ divider,
26
+ const ShadContextMenuItem(
27
+ trailing: Icon(LucideIcons.chevronRight),
28
+ items: [
29
+ ShadContextMenuItem(child: Text('Email Link')),
30
+ ShadContextMenuItem(child: Text('Messages')),
31
+ ],
32
+ child: Text('Share'),
33
+ ),
34
+ divider,
35
+ const ShadContextMenuItem(child: Text('Print...')),
36
+ ],
37
+ child: const Text('File'),
38
+ ),
39
+ ShadMenubarItem(
40
+ items: [
41
+ const ShadContextMenuItem(child: Text('Undo')),
42
+ const ShadContextMenuItem(child: Text('Redo')),
43
+ divider,
44
+ ShadContextMenuItem(
45
+ trailing: const Icon(LucideIcons.chevronRight),
46
+ items: [
47
+ const ShadContextMenuItem(child: Text('Find...')),
48
+ const ShadContextMenuItem(child: Text('Find Next')),
49
+ ],
50
+ child: const Text('Find'),
51
+ ),
52
+ divider,
53
+ const ShadContextMenuItem(child: Text('Cut')),
54
+ const ShadContextMenuItem(child: Text('Copy')),
55
+ const ShadContextMenuItem(child: Text('Paste')),
56
+ ],
57
+ child: const Text('Edit'),
58
+ ),
59
+ ShadMenubarItem(
60
+ items: [
61
+ const ShadContextMenuItem.inset(child: Text('Reload')),
62
+ const ShadContextMenuItem.inset(child: Text('Toggle Full Screen')),
63
+ ],
64
+ child: const Text('View'),
65
+ ),
66
+ ],
67
+)
68
+```
69
+
70
+Create a reusable `divider` with `ShadSeparator.horizontal(margin: const EdgeInsets.symmetric(vertical: 4), color: theme.colorScheme.muted)`.
.claude/skills/shadcn_ui-popover/SKILL.md
new
+41
@@ -0,0 +1,41 @@
1
+---
2
+name: shadcn_ui-popover
3
+description: Show rich content in a popover with ShadPopover, ShadPopoverController; trigger button, toggle. Use when adding popovers, floating panels, or button-triggered overlay content in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Popover
7
+
8
+## Instructions
9
+
10
+`ShadPopover` displays rich content in a portal, triggered by a button (or other trigger). Use a `ShadPopoverController()` to control open/close; call `popoverController.toggle` from the trigger. Pass `popover: (context) => ...` returning the overlay widget (e.g. a `SizedBox` with form fields); `child` is the trigger widget.
11
+
12
+### Basic usage
13
+
14
+```dart
15
+final popoverController = ShadPopoverController();
16
+
17
+// dispose: popoverController.dispose();
18
+
19
+ShadPopover(
20
+ controller: popoverController,
21
+ popover: (context) => SizedBox(
22
+ width: 288,
23
+ child: Column(
24
+ crossAxisAlignment: CrossAxisAlignment.start,
25
+ mainAxisSize: MainAxisSize.min,
26
+ children: [
27
+ Text('Dimensions', style: textTheme.h4),
28
+ Text('Set the dimensions for the layer.', style: textTheme.p),
29
+ const SizedBox(height: 4),
30
+ // ... form rows with ShadInput, etc.
31
+ ],
32
+ ),
33
+ ),
34
+ child: ShadButton.outline(
35
+ onPressed: popoverController.toggle,
36
+ child: const Text('Open popover'),
37
+ ),
38
+)
39
+```
40
+
41
+Dispose the controller in `State.dispose`.
.claude/skills/shadcn_ui-progress/SKILL.md
new
+29
@@ -0,0 +1,29 @@
1
+---
2
+name: shadcn_ui-progress
3
+description: Show progress with ShadProgress; determinate (value 0–1) or indeterminate. Use when displaying task completion or loading progress in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Progress
7
+
8
+## Instructions
9
+
10
+`ShadProgress` displays completion progress, typically as a progress bar. Use determinate mode with a `value` (0.0–1.0); omit `value` for indeterminate (animated) progress.
11
+
12
+### Determinate
13
+
14
+```dart
15
+ConstrainedBox(
16
+ constraints: BoxConstraints(
17
+ maxWidth: MediaQuery.sizeOf(context).width * 0.6,
18
+ ),
19
+ child: const ShadProgress(value: 0.5),
20
+)
21
+```
22
+
23
+### Indeterminate
24
+
25
+```dart
26
+const ShadProgress()
27
+```
28
+
29
+Constrain width with `ConstrainedBox` or parent layout as needed.
.claude/skills/shadcn_ui-radio-group/SKILL.md
new
+42
@@ -0,0 +1,42 @@
1
+---
2
+name: shadcn_ui-radio-group
3
+description: Use ShadRadioGroup and ShadRadio for single-choice options; ShadRadioGroupFormField for forms. Use when adding radio buttons or single-select options in a Flutter shadcn_ui app or ShadForm.
4
+---
5
+
6
+# Shadcn UI — Radio Group
7
+
8
+## Instructions
9
+
10
+`ShadRadioGroup<T>` is a set of radio buttons where at most one can be selected. Provide `items` as a list of `ShadRadio<T>` with `value` and `label`. Use `ShadRadioGroupFormField<T>` inside `ShadForm` with `id`, `label`, `items`, and `validator`.
11
+
12
+### Standalone
13
+
14
+```dart
15
+ShadRadioGroup<String>(
16
+ items: [
17
+ ShadRadio(label: Text('Default'), value: 'default'),
18
+ ShadRadio(label: Text('Comfortable'), value: 'comfortable'),
19
+ ShadRadio(label: Text('Nothing'), value: 'nothing'),
20
+ ],
21
+)
22
+```
23
+
24
+### Form field
25
+
26
+```dart
27
+enum NotifyAbout { all, mentions, nothing; }
28
+
29
+ShadRadioGroupFormField<NotifyAbout>(
30
+ label: const Text('Notify me about'),
31
+ items: NotifyAbout.values.map(
32
+ (e) => ShadRadio(
33
+ value: e,
34
+ label: Text(e.message),
35
+ ),
36
+ ),
37
+ validator: (v) {
38
+ if (v == null) return 'You need to select a notification type.';
39
+ return null;
40
+ },
41
+)
42
+```
.claude/skills/shadcn_ui-resizable/SKILL.md
new
+81
@@ -0,0 +1,81 @@
1
+---
2
+name: shadcn_ui-resizable
3
+description: Build resizable panel layouts with ShadResizablePanelGroup and ShadResizablePanel; horizontal/vertical axis, defaultSize, minSize, maxSize, showHandle. Use when adding resizable split panes or panel groups in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Resizable
7
+
8
+## Instructions
9
+
10
+`ShadResizablePanelGroup` contains `ShadResizablePanel` children. Each panel has `id`, `defaultSize` (fraction 0–1), optional `minSize` and `maxSize`, and `child`. Use `axis: Axis.vertical` for vertical splits. Nest `ShadResizablePanelGroup` inside a panel for nested layouts. Set `showHandle: true` to show the resize handle; optional `handleIcon` or `handleIconSrc` to customize. Double-click the handle to reset to default size.
11
+
12
+### Basic horizontal
13
+
14
+```dart
15
+ShadResizablePanelGroup(
16
+ children: [
17
+ ShadResizablePanel(
18
+ id: 0,
19
+ defaultSize: .5,
20
+ minSize: .2,
21
+ maxSize: .8,
22
+ child: Center(child: Text('One', style: theme.textTheme.large)),
23
+ ),
24
+ ShadResizablePanel(
25
+ id: 1,
26
+ defaultSize: .5,
27
+ child: ShadResizablePanelGroup(
28
+ axis: Axis.vertical,
29
+ children: [
30
+ ShadResizablePanel(
31
+ id: 0,
32
+ defaultSize: .3,
33
+ child: Center(child: Text('Two', style: theme.textTheme.large)),
34
+ ),
35
+ ShadResizablePanel(
36
+ id: 1,
37
+ defaultSize: .7,
38
+ child: Align(child: Text('Three', style: theme.textTheme.large)),
39
+ ),
40
+ ],
41
+ ),
42
+ ),
43
+ ],
44
+)
45
+```
46
+
47
+### Vertical
48
+
49
+```dart
50
+ShadResizablePanelGroup(
51
+ axis: Axis.vertical,
52
+ children: [
53
+ ShadResizablePanel(
54
+ id: 0,
55
+ defaultSize: 0.3,
56
+ minSize: 0.1,
57
+ child: Center(child: Text('Header', style: theme.textTheme.large)),
58
+ ),
59
+ ShadResizablePanel(
60
+ id: 1,
61
+ defaultSize: 0.7,
62
+ minSize: 0.1,
63
+ child: Center(child: Text('Footer', style: theme.textTheme.large)),
64
+ ),
65
+ ],
66
+)
67
+```
68
+
69
+### With visible handle
70
+
71
+```dart
72
+ShadResizablePanelGroup(
73
+ showHandle: true,
74
+ children: [
75
+ ShadResizablePanel(id: 0, defaultSize: .5, minSize: .2, child: ...),
76
+ ShadResizablePanel(id: 1, defaultSize: .5, minSize: .2, child: ...),
77
+ ],
78
+)
79
+```
80
+
81
+Wrap the group in a bordered container (e.g. `DecoratedBox` + `ClipRRect` with `theme.radius`) for a clean look.
.claude/skills/shadcn_ui-responsive/SKILL.md
new
+65
@@ -0,0 +1,65 @@
1
+---
2
+name: shadcn_ui-responsive
3
+description: Use Shadcn UI breakpoints and responsive layout with ShadBreakpoints, ShadResponsiveBuilder, and context.breakpoint. Use when building responsive Flutter shadcn_ui layouts, adapting UI to screen size, or checking current breakpoint (tn, sm, md, lg, xl, xxl).
4
+---
5
+
6
+# Shadcn UI — Responsive
7
+
8
+## Instructions
9
+
10
+Responsiveness is built into shadcn_ui via `ShadThemeData.breakpoints`. Use `ShadResponsiveBuilder` or `context.breakpoint` to branch on the current breakpoint.
11
+
12
+### Default breakpoints
13
+
14
+```dart
15
+ShadThemeData(
16
+ breakpoints: ShadBreakpoints(
17
+ tn: 0, // tiny
18
+ sm: 640, // small
19
+ md: 768, // medium
20
+ lg: 1024, // large
21
+ xl: 1280, // extra large
22
+ xxl: 1536, // extra extra large
23
+ ),
24
+);
25
+```
26
+
27
+### Current breakpoint
28
+
29
+**With ShadResponsiveBuilder:**
30
+
31
+```dart
32
+ShadResponsiveBuilder(
33
+ builder: (context, breakpoint) {
34
+ final sm = breakpoint >= ShadTheme.of(context).breakpoints.sm;
35
+ // ...
36
+ },
37
+),
38
+```
39
+
40
+**With context (equivalent):**
41
+
42
+```dart
43
+final sm = context.breakpoint >= ShadTheme.of(context).breakpoints.sm;
44
+```
45
+
46
+Use `>=` so that e.g. "sm" applies to small and larger viewports (Tailwind-style). Use `==` only when you need to match a single breakpoint.
47
+
48
+### Sealed class switch
49
+
50
+The breakpoint is a sealed type; you can switch on it:
51
+
52
+```dart
53
+ShadResponsiveBuilder(
54
+ builder: (context, breakpoint) {
55
+ return switch (breakpoint) {
56
+ ShadBreakpointTN() => const Text('Tiny'),
57
+ ShadBreakpointSM() => const Text('Small'),
58
+ ShadBreakpointMD() => const Text('Medium'),
59
+ ShadBreakpointLG() => const Text('Large'),
60
+ ShadBreakpointXL() => const Text('Extra Large'),
61
+ ShadBreakpointXXL() => const Text('Extra Extra Large'),
62
+ };
63
+ },
64
+),
65
+```
.claude/skills/shadcn_ui-select/SKILL.md
new
+68
@@ -0,0 +1,68 @@
1
+---
2
+name: shadcn_ui-select
3
+description: Use ShadSelect for dropdown option lists; ShadOption, placeholder, selectedOptionBuilder, scrollable groups, ShadSelect.withSearch, ShadSelect.multiple, ShadSelectFormField. Use when adding dropdowns, single/multi select, or searchable select in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Select
7
+
8
+## Instructions
9
+
10
+`ShadSelect<T>` displays a list of options triggered by a button. Provide `options` (list of `ShadOption<T>` or section headers like `Padding` with text), `placeholder`, `selectedOptionBuilder: (context, value) => ...`, and `onChanged`. Use `initialValue` or controlled state for selected value. Set `minWidth`/`maxWidth`/`maxHeight` as needed. For forms use `ShadSelectFormField<T>` with `id`, `validator`. Use `allowDeselection: true` to allow clearing the selection.
11
+
12
+### Basic
13
+
14
+```dart
15
+final fruits = {'apple': 'Apple', 'banana': 'Banana', ...};
16
+
17
+ShadSelect<String>(
18
+ placeholder: const Text('Select a fruit'),
19
+ options: [
20
+ Padding(
21
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
22
+ child: Text(
23
+ 'Fruits',
24
+ style: theme.textTheme.muted.copyWith(
25
+ fontWeight: FontWeight.w600,
26
+ color: theme.colorScheme.popoverForeground,
27
+ ),
28
+ textAlign: TextAlign.start,
29
+ ),
30
+ ),
31
+ ...fruits.entries.map((e) => ShadOption(value: e.key, child: Text(e.value))),
32
+ ],
33
+ selectedOptionBuilder: (context, value) => Text(fruits[value]!),
34
+ onChanged: print,
35
+)
36
+```
37
+
38
+### Form field
39
+
40
+```dart
41
+ShadSelectFormField<String>(
42
+ id: 'email',
43
+ minWidth: 350,
44
+ initialValue: null,
45
+ options: verifiedEmails
46
+ .map((email) => ShadOption(value: email, child: Text(email)))
47
+ .toList(),
48
+ selectedOptionBuilder: (context, value) =>
49
+ value == 'none' ? const Text('Select a verified email to display') : Text(value),
50
+ placeholder: const Text('Select a verified email to display'),
51
+ validator: (v) {
52
+ if (v == null) return 'Please select an email to display';
53
+ return null;
54
+ },
55
+)
56
+```
57
+
58
+### With search
59
+
60
+Use `ShadSelect.withSearch`. Pass `onSearchChanged`, `searchPlaceholder`, and filter options in state (e.g. wrap options in `Offstage(offstage: !filtered.containsKey(key), child: ShadOption(...))` to avoid focus loss when results change).
61
+
62
+### Multiple
63
+
64
+Use `ShadSelect.multiple`. Pass `selectedOptionsBuilder: (context, values) => ...`, `onChanged`, and optionally `allowDeselection: true`, `closeOnSelect: false` to keep popover open after selection.
65
+
66
+## Additional resources
67
+
68
+Scrollable option lists (grouped sections) and full search/multiple examples: [reference.md](reference.md).
.claude/skills/shadcn_ui-select/reference.md
new
+48
@@ -0,0 +1,48 @@
1
+# Shadcn UI Select — Reference
2
+
3
+## Scrollable / grouped options
4
+
5
+Build `options` as a list of section headers (`Padding` with styled `Text`) followed by `ShadOption` widgets for that section. For large lists the select popover scrolls. Example for timezones by region:
6
+
7
+```dart
8
+List<Widget> getTimezonesWidgets(ShadThemeData theme) {
9
+ final widgets = <Widget>[];
10
+ for (final zone in timezones.entries) {
11
+ widgets.add(
12
+ Padding(
13
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
14
+ child: Text(
15
+ zone.key,
16
+ style: theme.textTheme.muted.copyWith(
17
+ fontWeight: FontWeight.w600,
18
+ color: theme.colorScheme.popoverForeground,
19
+ ),
20
+ textAlign: TextAlign.start,
21
+ ),
22
+ ),
23
+ );
24
+ widgets.addAll(zone.value.entries
25
+ .map((e) => ShadOption(value: e.key, child: Text(e.value))));
26
+ }
27
+ return widgets;
28
+}
29
+
30
+ShadSelect<String>(
31
+ placeholder: const Text('Select a timezone'),
32
+ options: getTimezonesWidgets(theme),
33
+ selectedOptionBuilder: (context, value) {
34
+ final timezone = timezones.entries
35
+ .firstWhere((element) => element.value.containsKey(value))
36
+ .value[value];
37
+ return Text(timezone!);
38
+ },
39
+)
40
+```
41
+
42
+## With search
43
+
44
+Use `ShadSelect<String>.withSearch`, maintain `searchValue` in state, compute `filteredFrameworks` (or similar), and pass `onSearchChanged: (value) => setState(() => searchValue = value)`. In `options`, show "No results" when filtered is empty; otherwise map entries and wrap each `ShadOption` in `Offstage(offstage: !filtered.containsKey(key), child: ShadOption(...))` so the widget stays in the tree and focus is preserved.
45
+
46
+## Multiple selection
47
+
48
+Use `ShadSelect<T>.multiple`. Provide `selectedOptionsBuilder: (context, values) => Text(values.join(', '))` (or similar), `onChanged`, and optionally `allowDeselection: true`, `closeOnSelect: false`. Tap outside the popover to close it.
.claude/skills/shadcn_ui-separator/SKILL.md
new
+30
@@ -0,0 +1,30 @@
1
+---
2
+name: shadcn_ui-separator
3
+description: Visually separate content with ShadSeparator.horizontal and ShadSeparator.vertical; thickness, margin, radius. Use when adding dividers or visual separation in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Separator
7
+
8
+## Instructions
9
+
10
+`ShadSeparator` visually or semantically separates content. Use `ShadSeparator.horizontal` for a horizontal line (e.g. between rows); use `ShadSeparator.vertical` for a vertical line (e.g. between columns). Optional: `thickness`, `margin`, `radius`, `color`.
11
+
12
+### Horizontal
13
+
14
+```dart
15
+const ShadSeparator.horizontal(
16
+ thickness: 4,
17
+ margin: EdgeInsets.symmetric(horizontal: 20),
18
+ radius: BorderRadius.all(Radius.circular(4)),
19
+)
20
+```
21
+
22
+### Vertical
23
+
24
+```dart
25
+const ShadSeparator.vertical(
26
+ thickness: 4,
27
+ margin: EdgeInsets.symmetric(vertical: 20),
28
+ radius: BorderRadius.all(Radius.circular(4)),
29
+)
30
+```
.claude/skills/shadcn_ui-sheet/SKILL.md
new
+61
@@ -0,0 +1,61 @@
1
+---
2
+name: shadcn_ui-sheet
3
+description: Show slide-out panels with showShadSheet and ShadSheet; side (top, right, bottom, left), title, description, actions, child. Use when adding side panels, drawers, or slide-over content in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Sheet
7
+
8
+## Instructions
9
+
10
+Sheets extend the dialog pattern to display content from an edge of the screen. Use `showShadSheet(context: context, side: ShadSheetSide.*, builder: (context) => ShadSheet(...))`. `side` can be `ShadSheetSide.top`, `ShadSheetSide.right`, `ShadSheetSide.bottom`, or `ShadSheetSide.left`. Return a `ShadSheet` with optional `title`, `description`, `child` (body), and `actions`. Use `constraints` (e.g. `BoxConstraints(maxWidth: 512)` for left/right) to limit width or height.
11
+
12
+### Basic (right side)
13
+
14
+```dart
15
+showShadSheet(
16
+ side: ShadSheetSide.right,
17
+ context: context,
18
+ builder: (context) => ShadSheet(
19
+ constraints: const BoxConstraints(maxWidth: 512),
20
+ title: const Text('Edit Profile'),
21
+ description: const Text(
22
+ "Make changes to your profile here. Click save when you're done",
23
+ ),
24
+ child: Padding(
25
+ padding: const EdgeInsets.symmetric(vertical: 20),
26
+ child: Column(
27
+ mainAxisSize: MainAxisSize.min,
28
+ crossAxisAlignment: CrossAxisAlignment.stretch,
29
+ spacing: 16,
30
+ children: profile
31
+ .map(
32
+ (p) => Row(
33
+ children: [
34
+ Expanded(
35
+ child: Text(
36
+ p.title,
37
+ textAlign: TextAlign.end,
38
+ style: theme.textTheme.small,
39
+ ),
40
+ ),
41
+ const SizedBox(width: 16),
42
+ Expanded(
43
+ flex: 5,
44
+ child: ShadInput(initialValue: p.value),
45
+ ),
46
+ ],
47
+ ),
48
+ )
49
+ .toList(),
50
+ ),
51
+ ),
52
+ actions: const [
53
+ ShadButton(child: Text('Save changes')),
54
+ ],
55
+ ),
56
+);
57
+```
58
+
59
+### All sides
60
+
61
+Use `side: ShadSheetSide.top`, `ShadSheetSide.bottom`, `ShadSheetSide.left`, or `ShadSheetSide.right` in `showShadSheet`. For left/right sheets pass `constraints: const BoxConstraints(maxWidth: 512)` to the `ShadSheet` so content doesn’t stretch full width.
.claude/skills/shadcn_ui-slider/SKILL.md
new
+21
@@ -0,0 +1,21 @@
1
+---
2
+name: shadcn_ui-slider
3
+description: Use ShadSlider for numeric input within a range; initialValue, max (and min). Use when adding range sliders or numeric selection in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Slider
7
+
8
+## Instructions
9
+
10
+`ShadSlider` lets the user select a value from a range. Set `initialValue` and `max` (and optionally `min`). Use with state or a controller if you need to read the value on change.
11
+
12
+### Basic
13
+
14
+```dart
15
+ShadSlider(
16
+ initialValue: 33,
17
+ max: 100,
18
+)
19
+```
20
+
21
+Use callbacks or state management to react to value changes when needed.
.claude/skills/shadcn_ui-sonner/SKILL.md
new
+36
@@ -0,0 +1,36 @@
1
+---
2
+name: shadcn_ui-sonner
3
+description: Show toasts with ShadSonner and ShadToast; ShadSonner.of(context), show/hide, title, description, action. Use when showing toast notifications or ephemeral feedback in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Sonner
7
+
8
+## Instructions
9
+
10
+Sonner is an opinionated toast component. Ensure `ShadSonner` is in the widget tree (e.g. in app builder). Get the controller with `ShadSonner.of(context)`. Call `sonner.show(ShadToast(...))` to show a toast; use `sonner.hide(id)` to dismiss. Each toast can have `id`, `title`, `description`, and optional `action` (e.g. an Undo button).
11
+
12
+### Show toast
13
+
14
+```dart
15
+ShadButton.outline(
16
+ child: const Text('Show Toast'),
17
+ onPressed: () {
18
+ final sonner = ShadSonner.of(context);
19
+ final id = Random().nextInt(1000);
20
+ final now = DateTime.now();
21
+ sonner.show(
22
+ ShadToast(
23
+ id: id,
24
+ title: const Text('Event has been created'),
25
+ description: Text(DateFormat.yMd().add_jms().format(now)),
26
+ action: ShadButton(
27
+ child: const Text('Undo'),
28
+ onPressed: () => sonner.hide(id),
29
+ ),
30
+ ),
31
+ );
32
+ },
33
+)
34
+```
35
+
36
+Use a unique `id` per toast so you can hide it from the action button or elsewhere.
.claude/skills/shadcn_ui-switch/SKILL.md
new
+36
@@ -0,0 +1,36 @@
1
+---
2
+name: shadcn_ui-switch
3
+description: Add toggle switches with ShadSwitch and ShadSwitchFormField; label, form validation. Use when adding on/off toggles or switch form fields in a Flutter shadcn_ui app or ShadForm.
4
+---
5
+
6
+# Shadcn UI — Switch
7
+
8
+## Instructions
9
+
10
+`ShadSwitch` is a control that toggles between checked and unchecked. Use `value` and `onChanged`; optional `label`. For forms use `ShadSwitchFormField` with `id`, `initialValue`, `inputLabel`, `inputSublabel`, `validator`.
11
+
12
+### Standalone
13
+
14
+```dart
15
+ShadSwitch(
16
+ value: value,
17
+ onChanged: (v) => setState(() => value = v),
18
+ label: const Text('Airplane Mode'),
19
+)
20
+```
21
+
22
+### Form field
23
+
24
+```dart
25
+ShadSwitchFormField(
26
+ id: 'terms',
27
+ initialValue: false,
28
+ inputLabel: const Text('I accept the terms and conditions'),
29
+ onChanged: (v) {},
30
+ inputSublabel: const Text('You agree to our Terms and Conditions'),
31
+ validator: (v) {
32
+ if (!v) return 'You must accept the terms and conditions';
33
+ return null;
34
+ },
35
+)
36
+```
.claude/skills/shadcn_ui-table/SKILL.md
new
+69
@@ -0,0 +1,69 @@
1
+---
2
+name: shadcn_ui-table
3
+description: Build tables with ShadTable.list or ShadTable builder; ShadTableCell.header, ShadTableCell.footer, columnSpanExtent (FixedTableSpanExtent, RemainingTableSpanExtent). Use when displaying tabular data or responsive tables in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Table
7
+
8
+## Instructions
9
+
10
+`ShadTable` is a responsive table. Use `ShadTable.list` for small tables (all rows built at once): pass `header`, `footer` (lists of `ShadTableCell.header` / `ShadTableCell.footer`), and `children` (list of row lists of `ShadTableCell`). Use `ShadTable` with `columnCount`, `rowCount`, `header`, `builder`, `footer` for large tables (on-demand building). Use `columnSpanExtent` to set column widths: `FixedTableSpanExtent(130)` or `MaxTableSpanExtent(FixedTableSpanExtent(120), RemainingTableSpanExtent())`.
11
+
12
+### List (small tables)
13
+
14
+```dart
15
+ShadTable.list(
16
+ header: const [
17
+ ShadTableCell.header(child: Text('Invoice')),
18
+ ShadTableCell.header(child: Text('Status')),
19
+ ShadTableCell.header(child: Text('Method')),
20
+ ShadTableCell.header(
21
+ alignment: Alignment.centerRight,
22
+ child: Text('Amount'),
23
+ ),
24
+ ],
25
+ footer: const [
26
+ ShadTableCell.footer(child: Text('Total')),
27
+ ShadTableCell.footer(child: Text('')),
28
+ ShadTableCell.footer(child: Text('')),
29
+ ShadTableCell.footer(
30
+ alignment: Alignment.centerRight,
31
+ child: Text(r'$2500.00'),
32
+ ),
33
+ ],
34
+ columnSpanExtent: (index) {
35
+ if (index == 2) return const FixedTableSpanExtent(130);
36
+ if (index == 3) {
37
+ return const MaxTableSpanExtent(
38
+ FixedTableSpanExtent(120),
39
+ RemainingTableSpanExtent(),
40
+ );
41
+ }
42
+ return null;
43
+ },
44
+ children: invoices.map(
45
+ (invoice) => [
46
+ ShadTableCell(
47
+ child: Text(
48
+ invoice.invoice,
49
+ style: const TextStyle(fontWeight: FontWeight.w500),
50
+ ),
51
+ ),
52
+ ShadTableCell(child: Text(invoice.paymentStatus)),
53
+ ShadTableCell(child: Text(invoice.paymentMethod)),
54
+ ShadTableCell(
55
+ alignment: Alignment.centerRight,
56
+ child: Text(invoice.totalAmount),
57
+ ),
58
+ ],
59
+ ),
60
+)
61
+```
62
+
63
+### Builder (large tables)
64
+
65
+Use `ShadTable(columnCount:, rowCount:, header: (context, column) => ..., columnSpanExtent: (index) => ..., builder: (context, index) => ShadTableCell(...), footer: (context, column) => ...)`. The builder receives a table index with `row` and `column`. Prefer builder for large data so only visible rows are built.
66
+
67
+## Additional resources
68
+
69
+Detailed column span and builder patterns: [reference.md](reference.md).
.claude/skills/shadcn_ui-table/reference.md
new
+19
@@ -0,0 +1,19 @@
1
+# Shadcn UI Table — Reference
2
+
3
+## columnSpanExtent
4
+
5
+Return `TableSpanExtent?` per column index:
6
+- `FixedTableSpanExtent(130)` — fixed width.
7
+- `MaxTableSpanExtent(FixedTableSpanExtent(120), RemainingTableSpanExtent())` — min 120, then remaining space.
8
+- `null` — default extent.
9
+
10
+## Builder API
11
+
12
+`ShadTable` with:
13
+- `columnCount`, `rowCount`
14
+- `header: (context, column) => ShadTableCell.header(...)`
15
+- `columnSpanExtent: (index) => ...`
16
+- `builder: (context, index) => ShadTableCell(...)` where `index` has `index.row`, `index.column`
17
+- `footer: (context, column) => ShadTableCell.footer(...)` or empty cell
18
+
19
+Use for large tables so widgets are created on demand.
.claude/skills/shadcn_ui-tabs/SKILL.md
new
+63
@@ -0,0 +1,63 @@
1
+---
2
+name: shadcn_ui-tabs
3
+description: Build tabbed UIs with ShadTabs and ShadTab; value, tabs (content + child label), tabBarConstraints, contentConstraints. Use when adding tab panels or layered sections in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Tabs
7
+
8
+## Instructions
9
+
10
+`ShadTabs<T>` displays layered sections (tab panels) one at a time. Provide `value` (currently selected tab value), `tabs` (list of `ShadTab<T>`), and optional `tabBarConstraints` and `contentConstraints`. Each `ShadTab` has `value`, `child` (tab label), and `content` (panel widget).
11
+
12
+### Basic
13
+
14
+```dart
15
+ShadTabs<String>(
16
+ value: 'account',
17
+ tabBarConstraints: const BoxConstraints(maxWidth: 400),
18
+ contentConstraints: const BoxConstraints(maxWidth: 400),
19
+ tabs: [
20
+ ShadTab(
21
+ value: 'account',
22
+ content: ShadCard(
23
+ title: const Text('Account'),
24
+ description: const Text(
25
+ "Make changes to your account here. Click save when you're done."),
26
+ footer: const ShadButton(child: Text('Save changes')),
27
+ child: Column(
28
+ mainAxisSize: MainAxisSize.min,
29
+ children: [
30
+ const SizedBox(height: 16),
31
+ ShadInputFormField(label: const Text('Name'), initialValue: 'Ale'),
32
+ const SizedBox(height: 8),
33
+ ShadInputFormField(label: const Text('Username'), initialValue: 'nank1ro'),
34
+ const SizedBox(height: 16),
35
+ ],
36
+ ),
37
+ ),
38
+ child: const Text('Account'),
39
+ ),
40
+ ShadTab(
41
+ value: 'password',
42
+ content: ShadCard(
43
+ title: const Text('Password'),
44
+ description: const Text(
45
+ "Change your password here. After saving, you'll be logged out."),
46
+ footer: const ShadButton(child: Text('Save password')),
47
+ child: Column(
48
+ children: [
49
+ const SizedBox(height: 16),
50
+ ShadInputFormField(label: const Text('Current password'), obscureText: true),
51
+ const SizedBox(height: 8),
52
+ ShadInputFormField(label: const Text('New password'), obscureText: true),
53
+ const SizedBox(height: 16),
54
+ ],
55
+ ),
56
+ ),
57
+ child: const Text('Password'),
58
+ ),
59
+ ],
60
+)
61
+```
62
+
63
+Use state or a controller to change `value` when the user switches tabs if needed.
.claude/skills/shadcn_ui-textarea/SKILL.md
new
+34
@@ -0,0 +1,34 @@
1
+---
2
+name: shadcn_ui-textarea
3
+description: Use ShadTextarea for multi-line text input; placeholder. ShadTextareaFormField for forms with validation. Use when adding multi-line text fields or bio/description inputs in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Textarea
7
+
8
+## Instructions
9
+
10
+`ShadTextarea` displays a multi-line form field or textarea-like component. Use `placeholder` and optional `initialValue`. For forms use `ShadTextareaFormField` with `id`, `label`, `placeholder`, `description`, and `validator`.
11
+
12
+### Standalone
13
+
14
+```dart
15
+const ShadTextarea(
16
+ placeholder: Text('Type your message here'),
17
+)
18
+```
19
+
20
+### Form field
21
+
22
+```dart
23
+ShadTextareaFormField(
24
+ id: 'bio',
25
+ label: const Text('Bio'),
26
+ placeholder: const Text('Tell us a little bit about yourself'),
27
+ description: const Text(
28
+ 'You can @mention other users and organizations.'),
29
+ validator: (v) {
30
+ if (v.length < 10) return 'Bio must be at least 10 characters.';
31
+ return null;
32
+ },
33
+)
34
+```
.claude/skills/shadcn_ui-theme/SKILL.md
new
+89
@@ -0,0 +1,89 @@
1
+---
2
+name: shadcn_ui-theme
3
+description: Configure Shadcn UI theme and color schemes (ShadThemeData, ShadColorScheme), override colors, use ShadColorScheme.fromName for theme switchers, and add custom colors. Use when theming a Flutter shadcn_ui app, changing color scheme, or adding custom theme colors.
4
+---
5
+
6
+# Shadcn UI — Theme Data
7
+
8
+## Instructions
9
+
10
+Theme and color scheme are defined by `ShadThemeData` and `ShadColorScheme`. Supported scheme names: blue, gray, green, neutral, orange, red, rose, slate, stone, violet, yellow, zinc.
11
+
12
+### Basic usage
13
+
14
+```dart
15
+import 'package:shadcn_ui/shadcn_ui.dart';
16
+
17
+return ShadApp(
18
+ darkTheme: ShadThemeData(
19
+ brightness: Brightness.dark,
20
+ colorScheme: const ShadSlateColorScheme.dark(),
21
+ ),
22
+ child: ...,
23
+);
24
+```
25
+
26
+### Override theme properties
27
+
28
+Override specific properties of the color scheme or component themes:
29
+
30
+```dart
31
+ShadApp(
32
+ darkTheme: ShadThemeData(
33
+ brightness: Brightness.dark,
34
+ colorScheme: const ShadSlateColorScheme.dark(
35
+ background: Colors.blue,
36
+ ),
37
+ primaryButtonTheme: const ShadButtonTheme(
38
+ backgroundColor: Colors.cyan,
39
+ ),
40
+ ),
41
+ ...
42
+);
43
+```
44
+
45
+For fully custom schemes, extend `ShadColorScheme` and pass all required properties.
46
+
47
+### Theme switcher with ShadColorScheme.fromName
48
+
49
+Use `ShadColorScheme.fromName` to let users change the color scheme:
50
+
51
+```dart
52
+final shadThemeColors = [
53
+ 'blue', 'gray', 'green', 'neutral', 'orange', 'red', 'rose',
54
+ 'slate', 'stone', 'violet', 'yellow', 'zinc',
55
+];
56
+
57
+final lightColorScheme = ShadColorScheme.fromName('blue');
58
+final darkColorScheme = ShadColorScheme.fromName('slate', brightness: Brightness.dark);
59
+```
60
+
61
+Use a `ShadSelect<String>` (or similar) with these names and rebuild the app with the chosen scheme via your state management.
62
+
63
+### Custom colors
64
+
65
+Add custom colors via the `custom` parameter on the color scheme:
66
+
67
+```dart
68
+return ShadApp(
69
+ theme: ShadThemeData(
70
+ colorScheme: const ShadZincColorScheme.light(
71
+ custom: {
72
+ 'myCustomColor': Color.fromARGB(255, 177, 4, 196),
73
+ },
74
+ ),
75
+ ),
76
+);
77
+```
78
+
79
+Access: `ShadTheme.of(context).colorScheme.custom['myCustomColor']!`
80
+
81
+Or add an extension:
82
+
83
+```dart
84
+extension CustomColorExtension on ShadColorScheme {
85
+ Color get myCustomColor => custom['myCustomColor']!;
86
+}
87
+```
88
+
89
+Then use: `ShadTheme.of(context).colorScheme.myCustomColor`
.claude/skills/shadcn_ui-time-picker/SKILL.md
new
+43
@@ -0,0 +1,43 @@
1
+---
2
+name: shadcn_ui-time-picker
3
+description: Use ShadTimePicker and ShadTimePickerFormField for time selection; trailing widget, period variant. Use when adding time pickers or time form fields in a Flutter shadcn_ui app or ShadForm.
4
+---
5
+
6
+# Shadcn UI — Time Picker
7
+
8
+## Instructions
9
+
10
+`ShadTimePicker` is a time picker component. Optional `trailing` widget (e.g. icon). For forms use `ShadTimePickerFormField` with `label`, `description`, `onChanged`, and `validator`. Use `ShadTimePickerFormField.period` for AM/PM period selection.
11
+
12
+### Standalone
13
+
14
+```dart
15
+const ShadTimePicker(
16
+ trailing: Padding(
17
+ padding: EdgeInsets.only(left: 8, top: 14),
18
+ child: Icon(LucideIcons.clock4),
19
+ ),
20
+)
21
+```
22
+
23
+### Form field
24
+
25
+```dart
26
+ShadTimePickerFormField(
27
+ label: const Text('Pick a time'),
28
+ onChanged: print,
29
+ description: const Text('The time of the day you want to pick'),
30
+ validator: (v) => v == null ? 'A time is required' : null,
31
+)
32
+```
33
+
34
+### Period (AM/PM)
35
+
36
+```dart
37
+ShadTimePickerFormField.period(
38
+ label: const Text('Pick a time'),
39
+ onChanged: print,
40
+ description: const Text('The time of the day you want to pick'),
41
+ validator: (v) => v == null ? 'A time is required' : null,
42
+)
43
+```
.claude/skills/shadcn_ui-toast/SKILL.md
new
+68
@@ -0,0 +1,68 @@
1
+---
2
+name: shadcn_ui-toast
3
+description: Show toasts with ShadToaster.of(context).show(ShadToast); title, description, action, ShadToast.destructive. Use when displaying temporary messages or feedback in a Flutter shadcn_ui app.
4
+---
5
+
6
+# Shadcn UI — Toast
7
+
8
+## Instructions
9
+
10
+Toasts are succinct temporary messages. Ensure a toaster is in the tree (e.g. `ShadToaster`). Get it with `ShadToaster.of(context)`. Call `toaster.show(ShadToast(...))` to show a toast; use `toaster.hide()` to dismiss (e.g. from an action button). Use `ShadToast` with optional `title`, `description`, and `action`. Use `ShadToast.destructive` for error-style toasts with a destructive action button (use `ShadDecoration` to style the action border).
11
+
12
+### Simple
13
+
14
+```dart
15
+ShadToaster.of(context).show(
16
+ const ShadToast(
17
+ description: Text('Your message has been sent.'),
18
+ ),
19
+);
20
+```
21
+
22
+### With title
23
+
24
+```dart
25
+ShadToaster.of(context).show(
26
+ const ShadToast(
27
+ title: Text('Uh oh! Something went wrong'),
28
+ description: Text('There was a problem with your request'),
29
+ ),
30
+);
31
+```
32
+
33
+### With action
34
+
35
+```dart
36
+ShadToaster.of(context).show(
37
+ ShadToast(
38
+ title: const Text('Uh oh! Something went wrong'),
39
+ description: const Text('There was a problem with your request'),
40
+ action: ShadButton.outline(
41
+ child: const Text('Try again'),
42
+ onPressed: () => ShadToaster.of(context).hide(),
43
+ ),
44
+ ),
45
+);
46
+```
47
+
48
+### Destructive
49
+
50
+```dart
51
+final theme = ShadTheme.of(context);
52
+ShadToaster.of(context).show(
53
+ ShadToast.destructive(
54
+ title: const Text('Uh oh! Something went wrong'),
55
+ description: const Text('There was a problem with your request'),
56
+ action: ShadButton.destructive(
57
+ child: const Text('Try again'),
58
+ decoration: ShadDecoration(
59
+ border: ShadBorder.all(
60
+ color: theme.colorScheme.destructiveForeground,
61
+ width: 1,
62
+ ),
63
+ ),
64
+ onPressed: () => ShadToaster.of(context).hide(),
65
+ ),
66
+ ),
67
+);
68
+```
.claude/skills/shadcn_ui-tooltip/SKILL.md
new
+33
@@ -0,0 +1,33 @@
1
+---
2
+name: shadcn_ui-tooltip
3
+description: Show tooltips on hover or focus with ShadTooltip; builder returns tooltip content, child is trigger. Use when adding hover/focus tooltips in a Flutter shadcn_ui app. Child must use ShadGestureDetector for hover (e.g. ShadButton).
4
+---
5
+
6
+# Shadcn UI — Tooltip
7
+
8
+## Instructions
9
+
10
+`ShadTooltip` shows a popup when the element receives keyboard focus or the mouse hovers over it. Use `builder: (context) => ...` to build the tooltip content (e.g. `Text('Add to library')`) and `child` for the trigger widget. Hover works only if the child uses a `ShadGestureDetector`; `ShadButton` and similar components implement it. For a plain widget (e.g. image), wrap the child with `ShadGestureDetector` so the tooltip shows on hover.
11
+
12
+### Basic
13
+
14
+```dart
15
+ShadTooltip(
16
+ builder: (context) => const Text('Add to library'),
17
+ child: ShadButton.outline(
18
+ child: const Text('Hover/Focus'),
19
+ onPressed: () {},
20
+ ),
21
+)
22
+```
23
+
24
+For a non-interactive child (e.g. `Image`), wrap with `ShadGestureDetector`:
25
+
26
+```dart
27
+ShadTooltip(
28
+ builder: (context) => const Text('Description'),
29
+ child: ShadGestureDetector(
30
+ child: Image.network('...'),
31
+ ),
32
+)
33
+```
.claude/skills/shadcn_ui-typography/SKILL.md
new
+110
@@ -0,0 +1,110 @@
1
+---
2
+name: shadcn_ui-typography
3
+description: Apply Shadcn UI text styles (h1, h2, p, lead, muted, etc.), set custom font family or Google Fonts, and extend ShadTextTheme with custom styles. Use when styling text, headings, or paragraphs in a Flutter shadcn_ui app or when changing the default font.
4
+---
5
+
6
+# Shadcn UI — Typography
7
+
8
+## Instructions
9
+
10
+Use `ShadTheme.of(context).textTheme` for consistent text styles. Default font family is Geist.
11
+
12
+### Text style names
13
+
14
+- **h1Large**, **h1**, **h2**, **h3**, **h4** — Headings
15
+- **p** — Body paragraph
16
+- **blockquote** — Quote style
17
+- **table** — Table header/cell text
18
+- **list** — List item text
19
+- **lead** — Lead/subtitle
20
+- **large** — Large body
21
+- **small** — Small/caption
22
+- **muted** — Muted/secondary text
23
+
24
+### Examples
25
+
26
+```dart
27
+Text(
28
+ 'Taxing Laughter: The Joke Tax Chronicles',
29
+ style: ShadTheme.of(context).textTheme.h1,
30
+)
31
+
32
+Text(
33
+ 'The king, seeing how much happier his subjects were...',
34
+ style: ShadTheme.of(context).textTheme.p,
35
+)
36
+
37
+Text(
38
+ 'Email address',
39
+ style: ShadTheme.of(context).textTheme.small,
40
+)
41
+
42
+Text(
43
+ 'Enter your email address.',
44
+ style: ShadTheme.of(context).textTheme.muted,
45
+)
46
+```
47
+
48
+### Custom font family
49
+
50
+1. Add font assets (e.g. under `/fonts`) and register in `pubspec.yaml`:
51
+
52
+```yaml
53
+flutter:
54
+ fonts:
55
+ - family: UbuntuMono
56
+ fonts:
57
+ - asset: fonts/UbuntuMono-Regular.ttf
58
+ - asset: fonts/UbuntuMono-Italic.ttf
59
+ style: italic
60
+ - asset: fonts/UbuntuMono-Bold.ttf
61
+ weight: 700
62
+```
63
+
64
+2. Set `ShadTextTheme` in `ShadThemeData`:
65
+
66
+```dart
67
+return ShadApp(
68
+ theme: ShadThemeData(
69
+ brightness: Brightness.light,
70
+ colorScheme: const ShadZincColorScheme.light(),
71
+ textTheme: ShadTextTheme(
72
+ colorScheme: const ShadZincColorScheme.light(),
73
+ family: 'UbuntuMono',
74
+ ),
75
+ ),
76
+ ...
77
+);
78
+```
79
+
80
+### Google Fonts
81
+
82
+Install `google_fonts`, then:
83
+
84
+```dart
85
+theme: ShadThemeData(
86
+ brightness: Brightness.light,
87
+ colorScheme: const ShadZincColorScheme.light(),
88
+ textTheme: ShadTextTheme.fromGoogleFont(GoogleFonts.poppins),
89
+),
90
+```
91
+
92
+### Custom text styles
93
+
94
+Extend `ShadTextTheme` with the `custom` parameter:
95
+
96
+```dart
97
+textTheme: ShadTextTheme(
98
+ custom: {
99
+ 'myCustomStyle': const TextStyle(
100
+ fontSize: 16,
101
+ fontWeight: FontWeight.w400,
102
+ color: Colors.blue,
103
+ ),
104
+ },
105
+),
106
+```
107
+
108
+Access: `ShadTheme.of(context).textTheme.custom['myCustomStyle']!`
109
+
110
+Or define an extension on `ShadTextTheme` and use `ShadTheme.of(context).textTheme.myCustomStyle`.
.dart_skills/repos/flutter/skills
new
+1
@@ -0,0 +1 @@
1
+Subproject commit 4ad7731305db281c946183d6a295647492e4fac5
.dart_skills/repos/serverpod/skills-registry
new
+1
@@ -0,0 +1 @@
1
+Subproject commit 8e1512102719e84bdec752dfe3de4770792f3022
.dart_skills/skills_config.json
new
+697
@@ -0,0 +1,697 @@
1
+{
2
+ "version": 1,
3
+ "installations": {
4
+ "claude": {
5
+ "flutter": {
6
+ "skills": [
7
+ {
8
+ "name": "flutter-building-forms",
9
+ "installedAt": "2026-03-29T05:28:58.701617Z"
10
+ },
11
+ {
12
+ "name": "flutter-handling-http-and-json",
13
+ "installedAt": "2026-03-29T05:28:58.702231Z"
14
+ },
15
+ {
16
+ "name": "flutter-building-plugins",
17
+ "installedAt": "2026-03-29T05:28:58.702678Z"
18
+ },
19
+ {
20
+ "name": "flutter-caching-data",
21
+ "installedAt": "2026-03-29T05:28:58.703264Z"
22
+ },
23
+ {
24
+ "name": "flutter-embedding-native-views",
25
+ "installedAt": "2026-03-29T05:28:58.703841Z"
26
+ },
27
+ {
28
+ "name": "flutter-setting-up-on-macos",
29
+ "installedAt": "2026-03-29T05:28:58.704213Z"
30
+ },
31
+ {
32
+ "name": "flutter-adding-home-screen-widgets",
33
+ "installedAt": "2026-03-29T05:28:58.704584Z"
34
+ },
35
+ {
36
+ "name": "flutter-architecting-apps",
37
+ "installedAt": "2026-03-29T05:28:58.704999Z"
38
+ },
39
+ {
40
+ "name": "flutter-implementing-navigation-and-routing",
41
+ "installedAt": "2026-03-29T05:28:58.706469Z"
42
+ },
43
+ {
44
+ "name": "flutter-setting-up-on-windows",
45
+ "installedAt": "2026-03-29T05:28:58.707721Z"
46
+ },
47
+ {
48
+ "name": "flutter-improving-accessibility",
49
+ "installedAt": "2026-03-29T05:28:58.708659Z"
50
+ },
51
+ {
52
+ "name": "flutter-animating-apps",
53
+ "installedAt": "2026-03-29T05:28:58.709903Z"
54
+ },
55
+ {
56
+ "name": "flutter-working-with-databases",
57
+ "installedAt": "2026-03-29T05:28:58.711178Z"
58
+ },
59
+ {
60
+ "name": "flutter-setting-up-on-linux",
61
+ "installedAt": "2026-03-29T05:28:58.712297Z"
62
+ },
63
+ {
64
+ "name": "flutter-theming-apps",
65
+ "installedAt": "2026-03-29T05:28:58.713440Z"
66
+ },
67
+ {
68
+ "name": "flutter-building-layouts",
69
+ "installedAt": "2026-03-29T05:28:58.714553Z"
70
+ },
71
+ {
72
+ "name": "flutter-interoperating-with-native-apis",
73
+ "installedAt": "2026-03-29T05:28:58.715438Z"
74
+ },
75
+ {
76
+ "name": "flutter-managing-state",
77
+ "installedAt": "2026-03-29T05:28:58.715888Z"
78
+ },
79
+ {
80
+ "name": "flutter-testing-apps",
81
+ "installedAt": "2026-03-29T05:28:58.716401Z"
82
+ },
83
+ {
84
+ "name": "flutter-localizing-apps",
85
+ "installedAt": "2026-03-29T05:28:58.716758Z"
86
+ },
87
+ {
88
+ "name": "flutter-reducing-app-size",
89
+ "installedAt": "2026-03-29T05:28:58.717169Z"
90
+ },
91
+ {
92
+ "name": "flutter-handling-concurrency",
93
+ "installedAt": "2026-03-29T05:28:58.717653Z"
94
+ }
95
+ ]
96
+ },
97
+ "riverpod": {
98
+ "skills": [
99
+ {
100
+ "name": "riverpod-family",
101
+ "installedAt": "2026-03-29T05:28:58.718652Z"
102
+ },
103
+ {
104
+ "name": "riverpod-consumers",
105
+ "installedAt": "2026-03-29T05:28:58.719010Z"
106
+ },
107
+ {
108
+ "name": "riverpod-retry",
109
+ "installedAt": "2026-03-29T05:28:58.719373Z"
110
+ },
111
+ {
112
+ "name": "riverpod-eager-initialization",
113
+ "installedAt": "2026-03-29T05:28:58.719722Z"
114
+ },
115
+ {
116
+ "name": "riverpod-faq-and-practices",
117
+ "installedAt": "2026-03-29T05:28:58.720083Z"
118
+ },
119
+ {
120
+ "name": "riverpod-auto-dispose",
121
+ "installedAt": "2026-03-29T05:28:58.720427Z"
122
+ },
123
+ {
124
+ "name": "riverpod-cancel",
125
+ "installedAt": "2026-03-29T05:28:58.720760Z"
126
+ },
127
+ {
128
+ "name": "riverpod-codegen-and-hooks",
129
+ "installedAt": "2026-03-29T05:28:58.721120Z"
130
+ },
131
+ {
132
+ "name": "riverpod-containers",
133
+ "installedAt": "2026-03-29T05:28:58.721509Z"
134
+ },
135
+ {
136
+ "name": "riverpod-scoping",
137
+ "installedAt": "2026-03-29T05:28:58.721885Z"
138
+ },
139
+ {
140
+ "name": "riverpod-from-provider",
141
+ "installedAt": "2026-03-29T05:28:58.722280Z"
142
+ },
143
+ {
144
+ "name": "riverpod-pull-to-refresh",
145
+ "installedAt": "2026-03-29T05:28:58.722678Z"
146
+ },
147
+ {
148
+ "name": "riverpod-observers",
149
+ "installedAt": "2026-03-29T05:28:58.723018Z"
150
+ },
151
+ {
152
+ "name": "riverpod-migration",
153
+ "installedAt": "2026-03-29T05:28:58.724396Z"
154
+ },
155
+ {
156
+ "name": "riverpod-testing",
157
+ "installedAt": "2026-03-29T05:28:58.724751Z"
158
+ },
159
+ {
160
+ "name": "riverpod-getting-started",
161
+ "installedAt": "2026-03-29T05:28:58.725094Z"
162
+ },
163
+ {
164
+ "name": "riverpod-overrides",
165
+ "installedAt": "2026-03-29T05:28:58.725413Z"
166
+ },
167
+ {
168
+ "name": "riverpod-select",
169
+ "installedAt": "2026-03-29T05:28:58.725736Z"
170
+ },
171
+ {
172
+ "name": "riverpod-refs",
173
+ "installedAt": "2026-03-29T05:28:58.726283Z"
174
+ },
175
+ {
176
+ "name": "riverpod-3-0-migration",
177
+ "installedAt": "2026-03-29T05:28:58.726669Z"
178
+ },
179
+ {
180
+ "name": "riverpod-mutations",
181
+ "installedAt": "2026-03-29T05:28:58.727075Z"
182
+ },
183
+ {
184
+ "name": "riverpod-providers",
185
+ "installedAt": "2026-03-29T05:28:58.727721Z"
186
+ },
187
+ {
188
+ "name": "riverpod-offline",
189
+ "installedAt": "2026-03-29T05:28:58.728038Z"
190
+ }
191
+ ]
192
+ },
193
+ "shadcn_ui": {
194
+ "skills": [
195
+ {
196
+ "name": "shadcn_ui-time-picker",
197
+ "installedAt": "2026-03-29T05:28:58.728692Z"
198
+ },
199
+ {
200
+ "name": "shadcn_ui-avatar",
201
+ "installedAt": "2026-03-29T05:28:58.729031Z"
202
+ },
203
+ {
204
+ "name": "shadcn_ui-card",
205
+ "installedAt": "2026-03-29T05:28:58.729357Z"
206
+ },
207
+ {
208
+ "name": "shadcn_ui-sheet",
209
+ "installedAt": "2026-03-29T05:28:58.729691Z"
210
+ },
211
+ {
212
+ "name": "shadcn_ui-switch",
213
+ "installedAt": "2026-03-29T05:28:58.730036Z"
214
+ },
215
+ {
216
+ "name": "shadcn_ui-responsive",
217
+ "installedAt": "2026-03-29T05:28:58.730387Z"
218
+ },
219
+ {
220
+ "name": "shadcn_ui-sonner",
221
+ "installedAt": "2026-03-29T05:28:58.730790Z"
222
+ },
223
+ {
224
+ "name": "shadcn_ui-form",
225
+ "installedAt": "2026-03-29T05:28:58.731321Z"
226
+ },
227
+ {
228
+ "name": "shadcn_ui-dialog",
229
+ "installedAt": "2026-03-29T05:28:58.731687Z"
230
+ },
231
+ {
232
+ "name": "shadcn_ui-button",
233
+ "installedAt": "2026-03-29T05:28:58.732024Z"
234
+ },
235
+ {
236
+ "name": "shadcn_ui-separator",
237
+ "installedAt": "2026-03-29T05:28:58.732377Z"
238
+ },
239
+ {
240
+ "name": "shadcn_ui-accordion",
241
+ "installedAt": "2026-03-29T05:28:58.732723Z"
242
+ },
243
+ {
244
+ "name": "shadcn_ui-tabs",
245
+ "installedAt": "2026-03-29T05:28:58.733073Z"
246
+ },
247
+ {
248
+ "name": "shadcn_ui-badge",
249
+ "installedAt": "2026-03-29T05:28:58.733417Z"
250
+ },
251
+ {
252
+ "name": "shadcn_ui-tooltip",
253
+ "installedAt": "2026-03-29T05:28:58.733774Z"
254
+ },
255
+ {
256
+ "name": "shadcn_ui-decorator",
257
+ "installedAt": "2026-03-29T05:28:58.734124Z"
258
+ },
259
+ {
260
+ "name": "shadcn_ui-getting-started",
261
+ "installedAt": "2026-03-29T05:28:58.734489Z"
262
+ },
263
+ {
264
+ "name": "shadcn_ui-theme",
265
+ "installedAt": "2026-03-29T05:28:58.734939Z"
266
+ },
267
+ {
268
+ "name": "shadcn_ui-table",
269
+ "installedAt": "2026-03-29T05:28:58.735508Z"
270
+ },
271
+ {
272
+ "name": "shadcn_ui-input-otp",
273
+ "installedAt": "2026-03-29T05:28:58.735866Z"
274
+ },
275
+ {
276
+ "name": "shadcn_ui-select",
277
+ "installedAt": "2026-03-29T05:28:58.736379Z"
278
+ },
279
+ {
280
+ "name": "shadcn_ui-menubar",
281
+ "installedAt": "2026-03-29T05:28:58.736783Z"
282
+ },
283
+ {
284
+ "name": "shadcn_ui-textarea",
285
+ "installedAt": "2026-03-29T05:28:58.737145Z"
286
+ },
287
+ {
288
+ "name": "shadcn_ui-input",
289
+ "installedAt": "2026-03-29T05:28:58.737508Z"
290
+ },
291
+ {
292
+ "name": "shadcn_ui-alert",
293
+ "installedAt": "2026-03-29T05:28:58.737922Z"
294
+ },
295
+ {
296
+ "name": "shadcn_ui-toast",
297
+ "installedAt": "2026-03-29T05:28:58.738268Z"
298
+ },
299
+ {
300
+ "name": "shadcn_ui-checkbox",
301
+ "installedAt": "2026-03-29T05:28:58.738628Z"
302
+ },
303
+ {
304
+ "name": "shadcn_ui-typography",
305
+ "installedAt": "2026-03-29T05:28:58.738989Z"
306
+ },
307
+ {
308
+ "name": "shadcn_ui-date-picker",
309
+ "installedAt": "2026-03-29T05:28:58.739350Z"
310
+ },
311
+ {
312
+ "name": "shadcn_ui-context-menu",
313
+ "installedAt": "2026-03-29T05:28:58.739734Z"
314
+ },
315
+ {
316
+ "name": "shadcn_ui-resizable",
317
+ "installedAt": "2026-03-29T05:28:58.740091Z"
318
+ },
319
+ {
320
+ "name": "shadcn_ui-slider",
321
+ "installedAt": "2026-03-29T05:28:58.740478Z"
322
+ },
323
+ {
324
+ "name": "shadcn_ui-progress",
325
+ "installedAt": "2026-03-29T05:28:58.741149Z"
326
+ },
327
+ {
328
+ "name": "shadcn_ui-calendar",
329
+ "installedAt": "2026-03-29T05:28:58.741576Z"
330
+ },
331
+ {
332
+ "name": "shadcn_ui-popover",
333
+ "installedAt": "2026-03-29T05:28:58.742136Z"
334
+ },
335
+ {
336
+ "name": "shadcn_ui-icon-button",
337
+ "installedAt": "2026-03-29T05:28:58.742520Z"
338
+ },
339
+ {
340
+ "name": "shadcn_ui-radio-group",
341
+ "installedAt": "2026-03-29T05:28:58.742942Z"
342
+ },
343
+ {
344
+ "name": "shadcn_ui-breadcrumb",
345
+ "installedAt": "2026-03-29T05:28:58.743362Z"
346
+ }
347
+ ]
348
+ }
349
+ },
350
+ "generic": {
351
+ "flutter": {
352
+ "skills": [
353
+ {
354
+ "name": "flutter-building-forms",
355
+ "installedAt": "2026-03-29T06:04:47.235901Z"
356
+ },
357
+ {
358
+ "name": "flutter-handling-http-and-json",
359
+ "installedAt": "2026-03-29T06:04:47.236554Z"
360
+ },
361
+ {
362
+ "name": "flutter-building-plugins",
363
+ "installedAt": "2026-03-29T06:04:47.237052Z"
364
+ },
365
+ {
366
+ "name": "flutter-caching-data",
367
+ "installedAt": "2026-03-29T06:04:47.237472Z"
368
+ },
369
+ {
370
+ "name": "flutter-embedding-native-views",
371
+ "installedAt": "2026-03-29T06:04:47.237867Z"
372
+ },
373
+ {
374
+ "name": "flutter-setting-up-on-macos",
375
+ "installedAt": "2026-03-29T06:04:47.238342Z"
376
+ },
377
+ {
378
+ "name": "flutter-adding-home-screen-widgets",
379
+ "installedAt": "2026-03-29T06:04:47.238714Z"
380
+ },
381
+ {
382
+ "name": "flutter-architecting-apps",
383
+ "installedAt": "2026-03-29T06:04:47.239337Z"
384
+ },
385
+ {
386
+ "name": "flutter-implementing-navigation-and-routing",
387
+ "installedAt": "2026-03-29T06:04:47.239726Z"
388
+ },
389
+ {
390
+ "name": "flutter-setting-up-on-windows",
391
+ "installedAt": "2026-03-29T06:04:47.240144Z"
392
+ },
393
+ {
394
+ "name": "flutter-improving-accessibility",
395
+ "installedAt": "2026-03-29T06:04:47.240488Z"
396
+ },
397
+ {
398
+ "name": "flutter-animating-apps",
399
+ "installedAt": "2026-03-29T06:04:47.240831Z"
400
+ },
401
+ {
402
+ "name": "flutter-working-with-databases",
403
+ "installedAt": "2026-03-29T06:04:47.241200Z"
404
+ },
405
+ {
406
+ "name": "flutter-setting-up-on-linux",
407
+ "installedAt": "2026-03-29T06:04:47.241609Z"
408
+ },
409
+ {
410
+ "name": "flutter-theming-apps",
411
+ "installedAt": "2026-03-29T06:04:47.241946Z"
412
+ },
413
+ {
414
+ "name": "flutter-building-layouts",
415
+ "installedAt": "2026-03-29T06:04:47.242367Z"
416
+ },
417
+ {
418
+ "name": "flutter-interoperating-with-native-apis",
419
+ "installedAt": "2026-03-29T06:04:47.242710Z"
420
+ },
421
+ {
422
+ "name": "flutter-managing-state",
423
+ "installedAt": "2026-03-29T06:04:47.243044Z"
424
+ },
425
+ {
426
+ "name": "flutter-testing-apps",
427
+ "installedAt": "2026-03-29T06:04:47.243485Z"
428
+ },
429
+ {
430
+ "name": "flutter-localizing-apps",
431
+ "installedAt": "2026-03-29T06:04:47.243829Z"
432
+ },
433
+ {
434
+ "name": "flutter-reducing-app-size",
435
+ "installedAt": "2026-03-29T06:04:47.244160Z"
436
+ },
437
+ {
438
+ "name": "flutter-handling-concurrency",
439
+ "installedAt": "2026-03-29T06:04:47.244573Z"
440
+ }
441
+ ]
442
+ },
443
+ "riverpod": {
444
+ "skills": [
445
+ {
446
+ "name": "riverpod-family",
447
+ "installedAt": "2026-03-29T06:04:47.245172Z"
448
+ },
449
+ {
450
+ "name": "riverpod-consumers",
451
+ "installedAt": "2026-03-29T06:04:47.245541Z"
452
+ },
453
+ {
454
+ "name": "riverpod-retry",
455
+ "installedAt": "2026-03-29T06:04:47.245868Z"
456
+ },
457
+ {
458
+ "name": "riverpod-eager-initialization",
459
+ "installedAt": "2026-03-29T06:04:47.246199Z"
460
+ },
461
+ {
462
+ "name": "riverpod-faq-and-practices",
463
+ "installedAt": "2026-03-29T06:04:47.246568Z"
464
+ },
465
+ {
466
+ "name": "riverpod-auto-dispose",
467
+ "installedAt": "2026-03-29T06:04:47.246945Z"
468
+ },
469
+ {
470
+ "name": "riverpod-cancel",
471
+ "installedAt": "2026-03-29T06:04:47.247275Z"
472
+ },
473
+ {
474
+ "name": "riverpod-codegen-and-hooks",
475
+ "installedAt": "2026-03-29T06:04:47.247637Z"
476
+ },
477
+ {
478
+ "name": "riverpod-containers",
479
+ "installedAt": "2026-03-29T06:04:47.248017Z"
480
+ },
481
+ {
482
+ "name": "riverpod-scoping",
483
+ "installedAt": "2026-03-29T06:04:47.248350Z"
484
+ },
485
+ {
486
+ "name": "riverpod-from-provider",
487
+ "installedAt": "2026-03-29T06:04:47.248713Z"
488
+ },
489
+ {
490
+ "name": "riverpod-pull-to-refresh",
491
+ "installedAt": "2026-03-29T06:04:47.249063Z"
492
+ },
493
+ {
494
+ "name": "riverpod-observers",
495
+ "installedAt": "2026-03-29T06:04:47.249452Z"
496
+ },
497
+ {
498
+ "name": "riverpod-migration",
499
+ "installedAt": "2026-03-29T06:04:47.251226Z"
500
+ },
501
+ {
502
+ "name": "riverpod-testing",
503
+ "installedAt": "2026-03-29T06:04:47.252488Z"
504
+ },
505
+ {
506
+ "name": "riverpod-getting-started",
507
+ "installedAt": "2026-03-29T06:04:47.253108Z"
508
+ },
509
+ {
510
+ "name": "riverpod-overrides",
511
+ "installedAt": "2026-03-29T06:04:47.253586Z"
512
+ },
513
+ {
514
+ "name": "riverpod-select",
515
+ "installedAt": "2026-03-29T06:04:47.253954Z"
516
+ },
517
+ {
518
+ "name": "riverpod-refs",
519
+ "installedAt": "2026-03-29T06:04:47.254527Z"
520
+ },
521
+ {
522
+ "name": "riverpod-3-0-migration",
523
+ "installedAt": "2026-03-29T06:04:47.254877Z"
524
+ },
525
+ {
526
+ "name": "riverpod-mutations",
527
+ "installedAt": "2026-03-29T06:04:47.255199Z"
528
+ },
529
+ {
530
+ "name": "riverpod-providers",
531
+ "installedAt": "2026-03-29T06:04:47.255780Z"
532
+ },
533
+ {
534
+ "name": "riverpod-offline",
535
+ "installedAt": "2026-03-29T06:04:47.256111Z"
536
+ }
537
+ ]
538
+ },
539
+ "shadcn_ui": {
540
+ "skills": [
541
+ {
542
+ "name": "shadcn_ui-time-picker",
543
+ "installedAt": "2026-03-29T06:04:47.256499Z"
544
+ },
545
+ {
546
+ "name": "shadcn_ui-avatar",
547
+ "installedAt": "2026-03-29T06:04:47.256838Z"
548
+ },
549
+ {
550
+ "name": "shadcn_ui-card",
551
+ "installedAt": "2026-03-29T06:04:47.257177Z"
552
+ },
553
+ {
554
+ "name": "shadcn_ui-sheet",
555
+ "installedAt": "2026-03-29T06:04:47.257530Z"
556
+ },
557
+ {
558
+ "name": "shadcn_ui-switch",
559
+ "installedAt": "2026-03-29T06:04:47.257888Z"
560
+ },
561
+ {
562
+ "name": "shadcn_ui-responsive",
563
+ "installedAt": "2026-03-29T06:04:47.258220Z"
564
+ },
565
+ {
566
+ "name": "shadcn_ui-sonner",
567
+ "installedAt": "2026-03-29T06:04:47.258586Z"
568
+ },
569
+ {
570
+ "name": "shadcn_ui-form",
571
+ "installedAt": "2026-03-29T06:04:47.259148Z"
572
+ },
573
+ {
574
+ "name": "shadcn_ui-dialog",
575
+ "installedAt": "2026-03-29T06:04:47.259489Z"
576
+ },
577
+ {
578
+ "name": "shadcn_ui-button",
579
+ "installedAt": "2026-03-29T06:04:47.259871Z"
580
+ },
581
+ {
582
+ "name": "shadcn_ui-separator",
583
+ "installedAt": "2026-03-29T06:04:47.260191Z"
584
+ },
585
+ {
586
+ "name": "shadcn_ui-accordion",
587
+ "installedAt": "2026-03-29T06:04:47.260538Z"
588
+ },
589
+ {
590
+ "name": "shadcn_ui-tabs",
591
+ "installedAt": "2026-03-29T06:04:47.260894Z"
592
+ },
593
+ {
594
+ "name": "shadcn_ui-badge",
595
+ "installedAt": "2026-03-29T06:04:47.261216Z"
596
+ },
597
+ {
598
+ "name": "shadcn_ui-tooltip",
599
+ "installedAt": "2026-03-29T06:04:47.261548Z"
600
+ },
601
+ {
602
+ "name": "shadcn_ui-decorator",
603
+ "installedAt": "2026-03-29T06:04:47.261886Z"
604
+ },
605
+ {
606
+ "name": "shadcn_ui-getting-started",
607
+ "installedAt": "2026-03-29T06:04:47.262220Z"
608
+ },
609
+ {
610
+ "name": "shadcn_ui-theme",
611
+ "installedAt": "2026-03-29T06:04:47.262565Z"
612
+ },
613
+ {
614
+ "name": "shadcn_ui-table",
615
+ "installedAt": "2026-03-29T06:04:47.263455Z"
616
+ },
617
+ {
618
+ "name": "shadcn_ui-input-otp",
619
+ "installedAt": "2026-03-29T06:04:47.263818Z"
620
+ },
621
+ {
622
+ "name": "shadcn_ui-select",
623
+ "installedAt": "2026-03-29T06:04:47.264348Z"
624
+ },
625
+ {
626
+ "name": "shadcn_ui-menubar",
627
+ "installedAt": "2026-03-29T06:04:47.264690Z"
628
+ },
629
+ {
630
+ "name": "shadcn_ui-textarea",
631
+ "installedAt": "2026-03-29T06:04:47.265062Z"
632
+ },
633
+ {
634
+ "name": "shadcn_ui-input",
635
+ "installedAt": "2026-03-29T06:04:47.265396Z"
636
+ },
637
+ {
638
+ "name": "shadcn_ui-alert",
639
+ "installedAt": "2026-03-29T06:04:47.265721Z"
640
+ },
641
+ {
642
+ "name": "shadcn_ui-toast",
643
+ "installedAt": "2026-03-29T06:04:47.266086Z"
644
+ },
645
+ {
646
+ "name": "shadcn_ui-checkbox",
647
+ "installedAt": "2026-03-29T06:04:47.266408Z"
648
+ },
649
+ {
650
+ "name": "shadcn_ui-typography",
651
+ "installedAt": "2026-03-29T06:04:47.266751Z"
652
+ },
653
+ {
654
+ "name": "shadcn_ui-date-picker",
655
+ "installedAt": "2026-03-29T06:04:47.267115Z"
656
+ },
657
+ {
658
+ "name": "shadcn_ui-context-menu",
659
+ "installedAt": "2026-03-29T06:04:47.267520Z"
660
+ },
661
+ {
662
+ "name": "shadcn_ui-resizable",
663
+ "installedAt": "2026-03-29T06:04:47.267853Z"
664
+ },
665
+ {
666
+ "name": "shadcn_ui-slider",
667
+ "installedAt": "2026-03-29T06:04:47.268231Z"
668
+ },
669
+ {
670
+ "name": "shadcn_ui-progress",
671
+ "installedAt": "2026-03-29T06:04:47.268608Z"
672
+ },
673
+ {
674
+ "name": "shadcn_ui-calendar",
675
+ "installedAt": "2026-03-29T06:04:47.268954Z"
676
+ },
677
+ {
678
+ "name": "shadcn_ui-popover",
679
+ "installedAt": "2026-03-29T06:04:47.269296Z"
680
+ },
681
+ {
682
+ "name": "shadcn_ui-icon-button",
683
+ "installedAt": "2026-03-29T06:04:47.269636Z"
684
+ },
685
+ {
686
+ "name": "shadcn_ui-radio-group",
687
+ "installedAt": "2026-03-29T06:04:47.269968Z"
688
+ },
689
+ {
690
+ "name": "shadcn_ui-breadcrumb",
691
+ "installedAt": "2026-03-29T06:04:47.270322Z"
692
+ }
693
+ ]
694
+ }
695
+ }
696
+ }
697
+}
.fvmrc
new
+3
@@ -0,0 +1,3 @@
1
+{
2
+ "flutter": "3.41.1"
3
+}
\ No newline at end of file
.gitignore
new
+48
@@ -0,0 +1,48 @@
1
+# Miscellaneous
2
+*.class
3
+*.log
4
+*.pyc
5
+*.swp
6
+.DS_Store
7
+.atom/
8
+.build/
9
+.buildlog/
10
+.history
11
+.svn/
12
+.swiftpm/
13
+migrate_working_dir/
14
+
15
+# IntelliJ related
16
+*.iml
17
+*.ipr
18
+*.iws
19
+.idea/
20
+
21
+# The .vscode folder contains launch configuration and tasks you configure in
22
+# VS Code which you may wish to be included in version control, so this line
23
+# is commented out by default.
24
+#.vscode/
25
+
26
+# Flutter/Dart/Pub related
27
+**/doc/api/
28
+**/ios/Flutter/.last_build_id
29
+.dart_tool/
30
+.flutter-plugins-dependencies
31
+.pub-cache/
32
+.pub/
33
+/build/
34
+/coverage/
35
+
36
+# Symbolication related
37
+app.*.symbols
38
+
39
+# Obfuscation related
40
+app.*.map.json
41
+
42
+# Android Studio will place build artifacts here
43
+/android/app/debug
44
+/android/app/profile
45
+/android/app/release
46
+
47
+# FVM Version Cache
48
+.fvm/
\ No newline at end of file
.metadata
new
+45
@@ -0,0 +1,45 @@
1
+# This file tracks properties of this Flutter project.
2
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
3
+#
4
+# This file should be version controlled and should not be manually edited.
5
+
6
+version:
7
+ revision: "582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536"
8
+ channel: "stable"
9
+
10
+project_type: app
11
+
12
+# Tracks metadata for the flutter migrate command
13
+migration:
14
+ platforms:
15
+ - platform: root
16
+ create_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536
17
+ base_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536
18
+ - platform: android
19
+ create_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536
20
+ base_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536
21
+ - platform: ios
22
+ create_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536
23
+ base_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536
24
+ - platform: linux
25
+ create_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536
26
+ base_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536
27
+ - platform: macos
28
+ create_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536
29
+ base_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536
30
+ - platform: web
31
+ create_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536
32
+ base_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536
33
+ - platform: windows
34
+ create_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536
35
+ base_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536
36
+
37
+ # User provided section
38
+
39
+ # List of Local paths (relative to this file) that should be
40
+ # ignored by the migrate tool.
41
+ #
42
+ # Files that are not part of the templates will be ignored by default.
43
+ unmanaged_files:
44
+ - 'lib/main.dart'
45
+ - 'ios/Runner.xcodeproj/project.pbxproj'
.vscode/settings.json
new
+3
@@ -0,0 +1,3 @@
1
+{
2
+ "dart.flutterSdkPath": ".fvm/versions/3.41.1"
3
+}
\ No newline at end of file
README.md
new
+72
@@ -0,0 +1,72 @@
1
+# LocalMind
2
+
3
+**Your AI. Your Device. Your Rules.**
4
+
5
+LocalMind is a premium Flutter mobile application designed to provide a beautiful, fast, and privacy-respecting interface for local LLM servers and cloud providers.
6
+
7
+
8
+
9
+## 🚀 Vision
10
+
11
+Built for those who value privacy and performance, LocalMind connects directly to your local inference servers like **LM Studio** or **Ollama**, as well as cloud providers like **OpenRouter**. No middleware, no tracking, and no hidden subscriptions. Just you and your models.
12
+
13
+## ✨ Key Features
14
+
15
+- 🖥️ **Multi-Server Connection**: Seamlessly connect to LM Studio, Ollama, or OpenRouter with real-time health monitoring.
16
+- 💬 **Premium Chat Experience**: A polished, ChatGPT/Claude-inspired interface with:
17
+ - **Streaming Responses**: Real-time SSE support for instant feedback.
18
+ - **Markdown Rendering**: Beautiful rendering of text and code with syntax highlighting.
19
+ - **Message Actions**: Copy, retry, or delete individual messages with a long-press menu.
20
+- 🧠 **Persona Management**: Switch between specialized AI personas (Code Assistant, Math Tutor, etc.) or create your own with custom system prompts.
21
+- 🔄 **Live Model Swapping**: Change the active model mid-conversation without losing context.
22
+- 📦 **Conversation History**: Full-text search through your past chats, pinning important conversations, and local-only storage.
23
+- 🎨 **Deep Customization**:
24
+ - **Theming**: Premium dark-first design (with light mode support).
25
+ - **Chat Parameters**: Fine-tune temperature, top_p, max tokens, and context length.
26
+
27
+## 🛠️ Tech Stack
28
+
29
+LocalMind is built with modern, scalable technologies:
30
+
31
+- **Framework**: [Flutter](https://flutter.dev/) (Latest Stable)
32
+- **UI Components**: Shadcn-inspired design system (`shadcn_ui`)
33
+- **State Management**: [Riverpod](https://riverpod.dev/) for robust, testable state.
34
+- **Local Storage**: [Hive CE](https://pub.dev/packages/hive_ce) for high-performance on-device persistence.
35
+- **Networking**: [Dio](https://pub.dev/packages/dio) for optimized REST API handling.
36
+- **Rendering**: `flutter_markdown` + `flutter_highlight` for rich content.
37
+
38
+## 🛡️ Privacy First
39
+
40
+- **Zero Analytics**: We don't track how you use the app.
41
+- **Local Storage**: Your conversations are stored only on your device, encrypted via Hive.
42
+- **Direct Traffic**: Network requests go directly to _your_ servers. LocalMind never sees your data.
43
+
44
+## 🗺️ Roadmap (Future Plans)
45
+
46
+LocalMind is rapidly evolving. Here is what's coming next:
47
+
48
+- [ ] **AI Voice (Text-to-Speech)**: Listen to your AI's responses with high-quality TTS.
49
+- [ ] **Context Smart Replies**: Integrated suggested follow-ups to keep the conversation flowing.
50
+- [ ] **Multimodal Support**: Attach images and documents for vision-capable models.
51
+- [ ] **Tablet/Desktop Optimization**: A split-view layout for large-screen productivity.
52
+- [ ] **Export Options**: Export your chats to Markdown or PDF to share or archive.
53
+- [ ] **Quick Shortcuts**: OS-level widgets and shortcuts for launching new chats instantly.
54
+
55
+## 🛠️ Getting Started
56
+
57
+### Prerequisites
58
+
59
+- Flutter SDK (latest stable)
60
+- A running local LLM server (e.g., [Ollama](https://ollama.com/) or [LM Studio](https://lmstudio.ai/))
61
+
62
+### Installation
63
+
64
+1. Clone the repository: `git clone https://github.com/abdulmominsakib/localmind.git`
65
+2. Install dependencies: `flutter pub get`
66
+3. Run the app: `flutter run`
67
+
68
+---
69
+
70
+## 📄 License
71
+
72
+LocalMind is open source and available under the MIT License. Contributions are welcome!
analysis_options.yaml
new
+28
@@ -0,0 +1,28 @@
1
+# This file configures the analyzer, which statically analyzes Dart code to
2
+# check for errors, warnings, and lints.
3
+#
4
+# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
5
+# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
6
+# invoked from the command line by running `flutter analyze`.
7
+
8
+# The following line activates a set of recommended lints for Flutter apps,
9
+# packages, and plugins designed to encourage good coding practices.
10
+include: package:flutter_lints/flutter.yaml
11
+
12
+linter:
13
+ # The lint rules applied to this project can be customized in the
14
+ # section below to disable rules from the `package:flutter_lints/flutter.yaml`
15
+ # included above or to enable additional rules. A list of all available lints
16
+ # and their documentation is published at https://dart.dev/lints.
17
+ #
18
+ # Instead of disabling a lint rule for the entire project in the
19
+ # section below, it can also be suppressed for a single line of code
20
+ # or a specific dart file by using the `// ignore: name_of_lint` and
21
+ # `// ignore_for_file: name_of_lint` syntax on the line or in the file
22
+ # producing the lint.
23
+ rules:
24
+ # avoid_print: false # Uncomment to disable the `avoid_print` rule
25
+ # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
26
+
27
+# Additional information about this file can be found at
28
+# https://dart.dev/guides/language/analysis-options
android/.gitignore
new
+14
@@ -0,0 +1,14 @@
1
+gradle-wrapper.jar
2
+/.gradle
3
+/captures/
4
+/gradlew
5
+/gradlew.bat
6
+/local.properties
7
+GeneratedPluginRegistrant.java
8
+.cxx/
9
+
10
+# Remember to never publicly share your keystore.
11
+# See https://flutter.dev/to/reference-keystore
12
+key.properties
13
+**/*.keystore
14
+**/*.jks
android/app/build.gradle.kts
new
+44
@@ -0,0 +1,44 @@
1
+plugins {
2
+ id("com.android.application")
3
+ id("kotlin-android")
4
+ // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
5
+ id("dev.flutter.flutter-gradle-plugin")
6
+}
7
+
8
+android {
9
+ namespace = "com.example.localmind"
10
+ compileSdk = flutter.compileSdkVersion
11
+ ndkVersion = flutter.ndkVersion
12
+
13
+ compileOptions {
14
+ sourceCompatibility = JavaVersion.VERSION_17
15
+ targetCompatibility = JavaVersion.VERSION_17
16
+ }
17
+
18
+ kotlinOptions {
19
+ jvmTarget = JavaVersion.VERSION_17.toString()
20
+ }
21
+
22
+ defaultConfig {
23
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
24
+ applicationId = "com.example.localmind"
25
+ // You can update the following values to match your application needs.
26
+ // For more information, see: https://flutter.dev/to/review-gradle-config.
27
+ minSdk = flutter.minSdkVersion
28
+ targetSdk = flutter.targetSdkVersion
29
+ versionCode = flutter.versionCode
30
+ versionName = flutter.versionName
31
+ }
32
+
33
+ buildTypes {
34
+ release {
35
+ // TODO: Add your own signing config for the release build.
36
+ // Signing with the debug keys for now, so `flutter run --release` works.
37
+ signingConfig = signingConfigs.getByName("debug")
38
+ }
39
+ }
40
+}
41
+
42
+flutter {
43
+ source = "../.."
44
+}
android/app/src/debug/AndroidManifest.xml
new
+7
@@ -0,0 +1,7 @@
1
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <!-- The INTERNET permission is required for development. Specifically,
3
+ the Flutter tool needs it to communicate with the running application
4
+ to allow setting breakpoints, to provide hot reload, etc.
5
+ -->
6
+ <uses-permission android:name="android.permission.INTERNET"/>
7
+</manifest>
android/app/src/main/AndroidManifest.xml
new
+45
@@ -0,0 +1,45 @@
1
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <application
3
+ android:label="localmind"
4
+ android:name="${applicationName}"
5
+ android:icon="@mipmap/ic_launcher">
6
+ <activity
7
+ android:name=".MainActivity"
8
+ android:exported="true"
9
+ android:launchMode="singleTop"
10
+ android:taskAffinity=""
11
+ android:theme="@style/LaunchTheme"
12
+ android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
13
+ android:hardwareAccelerated="true"
14
+ android:windowSoftInputMode="adjustResize">
15
+ <!-- Specifies an Android theme to apply to this Activity as soon as
16
+ the Android process has started. This theme is visible to the user
17
+ while the Flutter UI initializes. After that, this theme continues
18
+ to determine the Window background behind the Flutter UI. -->
19
+ <meta-data
20
+ android:name="io.flutter.embedding.android.NormalTheme"
21
+ android:resource="@style/NormalTheme"
22
+ />
23
+ <intent-filter>
24
+ <action android:name="android.intent.action.MAIN"/>
25
+ <category android:name="android.intent.category.LAUNCHER"/>
26
+ </intent-filter>
27
+ </activity>
28
+ <!-- Don't delete the meta-data below.
29
+ This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
30
+ <meta-data
31
+ android:name="flutterEmbedding"
32
+ android:value="2" />
33
+ </application>
34
+ <!-- Required to query activities that can process text, see:
35
+ https://developer.android.com/training/package-visibility and
36
+ https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
37
+
38
+ In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
39
+ <queries>
40
+ <intent>
41
+ <action android:name="android.intent.action.PROCESS_TEXT"/>
42
+ <data android:mimeType="text/plain"/>
43
+ </intent>
44
+ </queries>
45
+</manifest>
android/app/src/main/kotlin/com/example/localmind/MainActivity.kt
new
+5
@@ -0,0 +1,5 @@
1
+package com.example.localmind
2
+
3
+import io.flutter.embedding.android.FlutterActivity
4
+
5
+class MainActivity : FlutterActivity()
android/app/src/main/res/drawable-v21/launch_background.xml
new
+12
@@ -0,0 +1,12 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<!-- Modify this file to customize your launch splash screen -->
3
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
4
+ <item android:drawable="?android:colorBackground" />
5
+
6
+ <!-- You can insert your own image assets here -->
7
+ <!-- <item>
8
+ <bitmap
9
+ android:gravity="center"
10
+ android:src="@mipmap/launch_image" />
11
+ </item> -->
12
+</layer-list>
android/app/src/main/res/drawable/launch_background.xml
new
+12
@@ -0,0 +1,12 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<!-- Modify this file to customize your launch splash screen -->
3
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
4
+ <item android:drawable="@android:color/white" />
5
+
6
+ <!-- You can insert your own image assets here -->
7
+ <!-- <item>
8
+ <bitmap
9
+ android:gravity="center"
10
+ android:src="@mipmap/launch_image" />
11
+ </item> -->
12
+</layer-list>
android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new
+6
@@ -0,0 +1,6 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
3
+ <background android:drawable="@mipmap/ic_launcher_background"/>
4
+ <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
5
+ <monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
6
+</adaptive-icon>
\ No newline at end of file
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png differ
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png differ
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png differ
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png differ
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png differ
android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png differ
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png differ
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png differ
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png differ
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png differ
android/app/src/main/res/values-night/styles.xml
new
+18
@@ -0,0 +1,18 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<resources>
3
+ <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
4
+ <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
5
+ <!-- Show a splash screen on the activity. Automatically removed when
6
+ the Flutter engine draws its first frame -->
7
+ <item name="android:windowBackground">@drawable/launch_background</item>
8
+ </style>
9
+ <!-- Theme applied to the Android Window as soon as the process has started.
10
+ This theme determines the color of the Android Window while your
11
+ Flutter UI initializes, as well as behind your Flutter UI while its
12
+ running.
13
+
14
+ This Theme is only used starting with V2 of Flutter's Android embedding. -->
15
+ <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
16
+ <item name="android:windowBackground">?android:colorBackground</item>
17
+ </style>
18
+</resources>
android/app/src/main/res/values/styles.xml
new
+18
@@ -0,0 +1,18 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<resources>
3
+ <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
4
+ <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
5
+ <!-- Show a splash screen on the activity. Automatically removed when
6
+ the Flutter engine draws its first frame -->
7
+ <item name="android:windowBackground">@drawable/launch_background</item>
8
+ </style>
9
+ <!-- Theme applied to the Android Window as soon as the process has started.
10
+ This theme determines the color of the Android Window while your
11
+ Flutter UI initializes, as well as behind your Flutter UI while its
12
+ running.
13
+
14
+ This Theme is only used starting with V2 of Flutter's Android embedding. -->
15
+ <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
16
+ <item name="android:windowBackground">?android:colorBackground</item>
17
+ </style>
18
+</resources>
android/app/src/profile/AndroidManifest.xml
new
+7
@@ -0,0 +1,7 @@
1
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <!-- The INTERNET permission is required for development. Specifically,
3
+ the Flutter tool needs it to communicate with the running application
4
+ to allow setting breakpoints, to provide hot reload, etc.
5
+ -->
6
+ <uses-permission android:name="android.permission.INTERNET"/>
7
+</manifest>
android/build.gradle.kts
new
+24
@@ -0,0 +1,24 @@
1
+allprojects {
2
+ repositories {
3
+ google()
4
+ mavenCentral()
5
+ }
6
+}
7
+
8
+val newBuildDir: Directory =
9
+ rootProject.layout.buildDirectory
10
+ .dir("../../build")
11
+ .get()
12
+rootProject.layout.buildDirectory.value(newBuildDir)
13
+
14
+subprojects {
15
+ val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
16
+ project.layout.buildDirectory.value(newSubprojectBuildDir)
17
+}
18
+subprojects {
19
+ project.evaluationDependsOn(":app")
20
+}
21
+
22
+tasks.register<Delete>("clean") {
23
+ delete(rootProject.layout.buildDirectory)
24
+}
android/gradle.properties
new
+2
@@ -0,0 +1,2 @@
1
+org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
2
+android.useAndroidX=true
android/gradle/wrapper/gradle-wrapper.properties
new
+5
@@ -0,0 +1,5 @@
1
+distributionBase=GRADLE_USER_HOME
2
+distributionPath=wrapper/dists
3
+zipStoreBase=GRADLE_USER_HOME
4
+zipStorePath=wrapper/dists
5
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
android/settings.gradle.kts
new
+26
@@ -0,0 +1,26 @@
1
+pluginManagement {
2
+ val flutterSdkPath =
3
+ run {
4
+ val properties = java.util.Properties()
5
+ file("local.properties").inputStream().use { properties.load(it) }
6
+ val flutterSdkPath = properties.getProperty("flutter.sdk")
7
+ require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
8
+ flutterSdkPath
9
+ }
10
+
11
+ includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
12
+
13
+ repositories {
14
+ google()
15
+ mavenCentral()
16
+ gradlePluginPortal()
17
+ }
18
+}
19
+
20
+plugins {
21
+ id("dev.flutter.flutter-plugin-loader") version "1.0.0"
22
+ id("com.android.application") version "8.11.1" apply false
23
+ id("org.jetbrains.kotlin.android") version "2.2.20" apply false
24
+}
25
+
26
+include(":app")
designs/active_chat/screen.png
Binary files /dev/null and b/designs/active_chat/screen.png differ
designs/chat_empty/screen.png
Binary files /dev/null and b/designs/chat_empty/screen.png differ
designs/monolith_precision/DESIGN.md
new
+100
@@ -0,0 +1,100 @@
1
+# Design System Documentation: Obsidian Editorial
2
+
3
+## 1. Overview & Creative North Star
4
+**Creative North Star: The Obsidian Architect**
5
+This design system moves beyond the "generic dark mode" template. It is built on the principles of **Obsidian Editorial**: a philosophy that treats digital interfaces as high-end print medium. We are not just building an AI app; we are building a laboratory for the mind.
6
+
7
+The goal is to move away from "contained" UI (boxes inside boxes) and toward "structural" UI. We achieve this through intentional asymmetry, extreme typographic contrast, and a sophisticated layering of dark-neutral tones that mimic the depth of volcanic glass. The experience should feel whispered, not shouted—authoritative, precise, and breathing with immense negative space.
8
+
9
+---
10
+
11
+### 2. Colors & Tonal Depth
12
+The palette is rooted in a monochromatic spectrum, designed to minimize cognitive load while maximizing perceived depth.
13
+
14
+| Token | Hex | Role |
15
+| :--- | :--- | :--- |
16
+| `background` | `#131313` | The base canvas. Dark, but not true black. |
17
+| `primary` | `#FFFFFF` | Actionable items and high-impact text. |
18
+| `on_primary` | `#1A1C1C` | Inverted text for primary buttons. |
19
+| `surface_container_lowest` | `#0E0E0E` | Inset elements, deep wells, or inactive inputs. |
20
+| `surface_container_high` | `#2A2A2A` | Elevated cards or focused modal layers. |
21
+| `outline_variant` | `#474747` | Subtle structural definition (use sparingly). |
22
+
23
+#### The "No-Line" Rule
24
+Standard 1px solid borders are strictly prohibited for sectioning. To define a new area, use a background color shift. For example, a `surface_container_low` side-panel sitting against a `surface` background. The eye should perceive the edge through the change in tone, not a drawn line.
25
+
26
+#### Surface Hierarchy & Nesting
27
+Treat the UI as a series of physical layers.
28
+- **Base Level:** `background`
29
+- **Sectioning:** `surface_container_low`
30
+- **Interactive Elements:** `surface_container_high`
31
+- **Floating Modals:** `surface_bright` with a 24px backdrop blur.
32
+
33
+#### Signature Textures (The Director’s Polish)
34
+While the base is minimalist, use a subtle 1% noise texture or a microscopic tonal gradient (e.g., `primary` to `primary_container` across 120 degrees) on primary CTAs. This prevents the "flat digital" look and adds a "physical" soul to the white elements.
35
+
36
+---
37
+
38
+### 3. Typography
39
+We utilize **Geist Sans** for its Swiss-inspired neutrality and **Geist Mono** for technical/AI data.
40
+
41
+* **Display (Editorial Voice):** Use `display-lg` (3.5rem) with `-0.04em` letter spacing for hero headers. This creates a tight, "locked-in" editorial feel.
42
+* **Body (The Workhorse):** `body-md` (0.875rem) is the default for AI-generated responses. It provides high legibility without overcrowding the screen.
43
+* **Labels (The Technical Layer):** Use `label-sm` (0.6875rem) in **Geist Mono** for metadata (tokens, server latency, model IDs). All labels should be Uppercase with `0.05em` tracking to distinguish them from content.
44
+
45
+---
46
+
47
+### 4. Elevation & Depth
48
+In this design system, height is an illusion created by light, not lines.
49
+
50
+* **The Layering Principle:** Depth is achieved by "stacking." A `surface_container_lowest` card placed inside a `surface_container_high` section creates a natural "carved out" effect.
51
+* **Ambient Shadows:** For floating elements (menus/modals), use a "Whisper Shadow."
52
+ * *Shadow:* `0px 10px 40px rgba(0, 0, 0, 0.4)`
53
+ * The shadow color must be a tinted version of the background, never pure black.
54
+* **The "Ghost Border" Fallback:** If a border is required for accessibility, use the `outline_variant` token at **15% opacity**. It should be felt, not seen.
55
+* **Glassmorphism:** Use `surface_bright` at 60% opacity with a `20px` backdrop blur for floating navigation bars to allow the underlying AI "stream" of text to bleed through.
56
+
57
+---
58
+
59
+### 5. Components
60
+
61
+#### Buttons
62
+- **Primary:** `primary` background, `on_primary` text. No border. Corners: `6px`.
63
+- **Secondary:** `surface_container_highest` background. Corners: `6px`.
64
+- **Ghost:** No background. `primary` text. Hover state shifts background to `surface_container_low`.
65
+
66
+#### Input Fields
67
+Forbid the "boxed" input. Use a "Platform" style: A `surface_container_lowest` background with a subtle 1px bottom-stroke using `outline_variant`. On focus, the bottom stroke becomes `primary`.
68
+
69
+#### Cards & Lists
70
+Cards must never have dividers. Separate list items using `spacing-4` (1rem) of vertical white space. To group items, wrap them in a `surface_container_low` container with a `md` (0.75rem) corner radius.
71
+
72
+#### AI Command Bar (The "Core" Component)
73
+This is a floating bar using `surface_container_highest`.
74
+- **Radius:** `full` (9999px).
75
+- **Style:** Glassmorphism (blur: 16px).
76
+- **Leading Icon:** Lucide `Sparkles` in `primary`.
77
+
78
+---
79
+
80
+### 6. Do’s and Don’ts
81
+
82
+#### Do
83
+- **Embrace Asymmetry:** Align headings to the far left and metadata to the far right to create a wide, expansive feel.
84
+- **Use Mono for Values:** Any number or technical ID must be in **Geist Mono**.
85
+- **Generous Whitespace:** If you think there is enough space, add 20% more. This system thrives on "the void."
86
+
87
+#### Don’t
88
+- **Don't use #000000:** It kills the "Obsidian" depth. Stick to the `#0E0E0E` to `#131313` range.
89
+- **Don't use Dividers:** If you need a divider, use a `1px` height `surface_container_low` block that only spans 80% of the container width.
90
+- **Don't use Rounded-None:** Always keep corners between `6dp` and `12dp` (except for the Command Bar) to soften the brutalist typography.
91
+- **Don't use colorful Icons:** Keep all Lucide icons in `on_surface_variant` (#C6C6C6) or `primary`.
92
+
93
+---
94
+
95
+### 7. Spacing Scale Reference
96
+Use these tokens to maintain the editorial rhythm:
97
+- **Tight (2):** 0.5rem (Icon/Text spacing)
98
+- **Standard (4):** 1rem (Internal padding)
99
+- **Section (8):** 2rem (Between content blocks)
100
+- **Breathing (12):** 3rem (Margin from screen edges)
\ No newline at end of file
designs/onboarding_step_1/screen.png
Binary files /dev/null and b/designs/onboarding_step_1/screen.png differ
designs/persona_creation/screen.png
Binary files /dev/null and b/designs/persona_creation/screen.png differ
designs/server_management/screen.png
Binary files /dev/null and b/designs/server_management/screen.png differ
designs/settings/screen.png
Binary files /dev/null and b/designs/settings/screen.png differ
docs/cover.webp
Binary files /dev/null and b/docs/cover.webp differ
docs/logo.webp
Binary files /dev/null and b/docs/logo.webp differ
ios/.gitignore
new
+34
@@ -0,0 +1,34 @@
1
+**/dgph
2
+*.mode1v3
3
+*.mode2v3
4
+*.moved-aside
5
+*.pbxuser
6
+*.perspectivev3
7
+**/*sync/
8
+.sconsign.dblite
9
+.tags*
10
+**/.vagrant/
11
+**/DerivedData/
12
+Icon?
13
+**/Pods/
14
+**/.symlinks/
15
+profile
16
+xcuserdata
17
+**/.generated/
18
+Flutter/App.framework
19
+Flutter/Flutter.framework
20
+Flutter/Flutter.podspec
21
+Flutter/Generated.xcconfig
22
+Flutter/ephemeral/
23
+Flutter/app.flx
24
+Flutter/app.zip
25
+Flutter/flutter_assets/
26
+Flutter/flutter_export_environment.sh
27
+ServiceDefinitions.json
28
+Runner/GeneratedPluginRegistrant.*
29
+
30
+# Exceptions to above rules.
31
+!default.mode1v3
32
+!default.mode2v3
33
+!default.pbxuser
34
+!default.perspectivev3
ios/Flutter/AppFrameworkInfo.plist
new
+24
@@ -0,0 +1,24 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+<plist version="1.0">
4
+<dict>
5
+ <key>CFBundleDevelopmentRegion</key>
6
+ <string>en</string>
7
+ <key>CFBundleExecutable</key>
8
+ <string>App</string>
9
+ <key>CFBundleIdentifier</key>
10
+ <string>io.flutter.flutter.app</string>
11
+ <key>CFBundleInfoDictionaryVersion</key>
12
+ <string>6.0</string>
13
+ <key>CFBundleName</key>
14
+ <string>App</string>
15
+ <key>CFBundlePackageType</key>
16
+ <string>FMWK</string>
17
+ <key>CFBundleShortVersionString</key>
18
+ <string>1.0</string>
19
+ <key>CFBundleSignature</key>
20
+ <string>????</string>
21
+ <key>CFBundleVersion</key>
22
+ <string>1.0</string>
23
+</dict>
24
+</plist>
ios/Flutter/Debug.xcconfig
new
+2
@@ -0,0 +1,2 @@
1
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2
+#include "Generated.xcconfig"
ios/Flutter/Release.xcconfig
new
+2
@@ -0,0 +1,2 @@
1
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2
+#include "Generated.xcconfig"
ios/Podfile
new
+43
@@ -0,0 +1,43 @@
1
+# Uncomment this line to define a global platform for your project
2
+# platform :ios, '13.0'
3
+
4
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
6
+
7
+project 'Runner', {
8
+ 'Debug' => :debug,
9
+ 'Profile' => :release,
10
+ 'Release' => :release,
11
+}
12
+
13
+def flutter_root
14
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
15
+ unless File.exist?(generated_xcode_build_settings_path)
16
+ raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
17
+ end
18
+
19
+ File.foreach(generated_xcode_build_settings_path) do |line|
20
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
21
+ return matches[1].strip if matches
22
+ end
23
+ raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
24
+end
25
+
26
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
27
+
28
+flutter_ios_podfile_setup
29
+
30
+target 'Runner' do
31
+ use_frameworks!
32
+
33
+ flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
34
+ target 'RunnerTests' do
35
+ inherit! :search_paths
36
+ end
37
+end
38
+
39
+post_install do |installer|
40
+ installer.pods_project.targets.each do |target|
41
+ flutter_additional_ios_build_settings(target)
42
+ end
43
+end
ios/Podfile.lock
new
+99
@@ -0,0 +1,99 @@
1
+PODS:
2
+ - device_info_plus (0.0.1):
3
+ - Flutter
4
+ - DKImagePickerController/Core (4.3.9):
5
+ - DKImagePickerController/ImageDataManager
6
+ - DKImagePickerController/Resource
7
+ - DKImagePickerController/ImageDataManager (4.3.9)
8
+ - DKImagePickerController/PhotoGallery (4.3.9):
9
+ - DKImagePickerController/Core
10
+ - DKPhotoGallery
11
+ - DKImagePickerController/Resource (4.3.9)
12
+ - DKPhotoGallery (0.0.19):
13
+ - DKPhotoGallery/Core (= 0.0.19)
14
+ - DKPhotoGallery/Model (= 0.0.19)
15
+ - DKPhotoGallery/Preview (= 0.0.19)
16
+ - DKPhotoGallery/Resource (= 0.0.19)
17
+ - SDWebImage
18
+ - SwiftyGif
19
+ - DKPhotoGallery/Core (0.0.19):
20
+ - DKPhotoGallery/Model
21
+ - DKPhotoGallery/Preview
22
+ - SDWebImage
23
+ - SwiftyGif
24
+ - DKPhotoGallery/Model (0.0.19):
25
+ - SDWebImage
26
+ - SwiftyGif
27
+ - DKPhotoGallery/Preview (0.0.19):
28
+ - DKPhotoGallery/Model
29
+ - DKPhotoGallery/Resource
30
+ - SDWebImage
31
+ - SwiftyGif
32
+ - DKPhotoGallery/Resource (0.0.19):
33
+ - SDWebImage
34
+ - SwiftyGif
35
+ - file_picker (0.0.1):
36
+ - DKImagePickerController/PhotoGallery
37
+ - Flutter
38
+ - Flutter (1.0.0)
39
+ - flutter_tts (0.0.1):
40
+ - Flutter
41
+ - haptic_feedback (0.5.1):
42
+ - Flutter
43
+ - irondash_engine_context (0.0.1):
44
+ - Flutter
45
+ - SDWebImage (5.21.7):
46
+ - SDWebImage/Core (= 5.21.7)
47
+ - SDWebImage/Core (5.21.7)
48
+ - super_native_extensions (0.0.1):
49
+ - Flutter
50
+ - SwiftyGif (5.4.5)
51
+
52
+DEPENDENCIES:
53
+ - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
54
+ - file_picker (from `.symlinks/plugins/file_picker/ios`)
55
+ - Flutter (from `Flutter`)
56
+ - flutter_tts (from `.symlinks/plugins/flutter_tts/ios`)
57
+ - haptic_feedback (from `.symlinks/plugins/haptic_feedback/ios`)
58
+ - irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
59
+ - super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
60
+
61
+SPEC REPOS:
62
+ trunk:
63
+ - DKImagePickerController
64
+ - DKPhotoGallery
65
+ - SDWebImage
66
+ - SwiftyGif
67
+
68
+EXTERNAL SOURCES:
69
+ device_info_plus:
70
+ :path: ".symlinks/plugins/device_info_plus/ios"
71
+ file_picker:
72
+ :path: ".symlinks/plugins/file_picker/ios"
73
+ Flutter:
74
+ :path: Flutter
75
+ flutter_tts:
76
+ :path: ".symlinks/plugins/flutter_tts/ios"
77
+ haptic_feedback:
78
+ :path: ".symlinks/plugins/haptic_feedback/ios"
79
+ irondash_engine_context:
80
+ :path: ".symlinks/plugins/irondash_engine_context/ios"
81
+ super_native_extensions:
82
+ :path: ".symlinks/plugins/super_native_extensions/ios"
83
+
84
+SPEC CHECKSUMS:
85
+ device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
86
+ DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
87
+ DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
88
+ file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
89
+ Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
90
+ flutter_tts: 91ad1884033cd95ef6e9338754adad54f01f090f
91
+ haptic_feedback: a366a27ec760f0e865651605b95de8a9bb04ef01
92
+ irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
93
+ SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
94
+ super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7
95
+ SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
96
+
97
+PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
98
+
99
+COCOAPODS: 1.16.2
ios/Runner.xcodeproj/project.pbxproj
new
+735
@@ -0,0 +1,735 @@
1
+// !$*UTF8*$!
2
+{
3
+ archiveVersion = 1;
4
+ classes = {
5
+ };
6
+ objectVersion = 54;
7
+ objects = {
8
+
9
+/* Begin PBXBuildFile section */
10
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
11
+ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
12
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
13
+ 708E3CFC8892B5DE565829CF /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A429165A8A305E5EA5D3366F /* Pods_RunnerTests.framework */; };
14
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
15
+ 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
16
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
17
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
18
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
19
+ D6C3B421FD01BF16D2875FF4 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 68B72B458011EED590BB10A7 /* Pods_Runner.framework */; };
20
+/* End PBXBuildFile section */
21
+
22
+/* Begin PBXContainerItemProxy section */
23
+ 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
24
+ isa = PBXContainerItemProxy;
25
+ containerPortal = 97C146E61CF9000F007C117D /* Project object */;
26
+ proxyType = 1;
27
+ remoteGlobalIDString = 97C146ED1CF9000F007C117D;
28
+ remoteInfo = Runner;
29
+ };
30
+/* End PBXContainerItemProxy section */
31
+
32
+/* Begin PBXCopyFilesBuildPhase section */
33
+ 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
34
+ isa = PBXCopyFilesBuildPhase;
35
+ buildActionMask = 2147483647;
36
+ dstPath = "";
37
+ dstSubfolderSpec = 10;
38
+ files = (
39
+ );
40
+ name = "Embed Frameworks";
41
+ runOnlyForDeploymentPostprocessing = 0;
42
+ };
43
+/* End PBXCopyFilesBuildPhase section */
44
+
45
+/* Begin PBXFileReference section */
46
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
47
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
48
+ 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
49
+ 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
50
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
51
+ 4453DDD0EC8DAE1A94449906 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
52
+ 49416490449ABCCBD5CB71F5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
53
+ 546EC27D20F12194C4550A61 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
54
+ 68B72B458011EED590BB10A7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
55
+ 73343CA27827EBEDA45E3AE4 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
56
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
57
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
58
+ 764D6FCF354D4C047C088B4E /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
59
+ 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
60
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
61
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
62
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
63
+ 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
64
+ 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
65
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
66
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
67
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
68
+ A429165A8A305E5EA5D3366F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
69
+ E4C8076B210B690E52E1A9BD /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
70
+/* End PBXFileReference section */
71
+
72
+/* Begin PBXFrameworksBuildPhase section */
73
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
74
+ isa = PBXFrameworksBuildPhase;
75
+ buildActionMask = 2147483647;
76
+ files = (
77
+ D6C3B421FD01BF16D2875FF4 /* Pods_Runner.framework in Frameworks */,
78
+ );
79
+ runOnlyForDeploymentPostprocessing = 0;
80
+ };
81
+ 9AA24F4969F6FB2D761614D7 /* Frameworks */ = {
82
+ isa = PBXFrameworksBuildPhase;
83
+ buildActionMask = 2147483647;
84
+ files = (
85
+ 708E3CFC8892B5DE565829CF /* Pods_RunnerTests.framework in Frameworks */,
86
+ );
87
+ runOnlyForDeploymentPostprocessing = 0;
88
+ };
89
+/* End PBXFrameworksBuildPhase section */
90
+
91
+/* Begin PBXGroup section */
92
+ 331C8082294A63A400263BE5 /* RunnerTests */ = {
93
+ isa = PBXGroup;
94
+ children = (
95
+ 331C807B294A618700263BE5 /* RunnerTests.swift */,
96
+ );
97
+ path = RunnerTests;
98
+ sourceTree = "<group>";
99
+ };
100
+ 3DD528BE2C5F86FFDE137E7C /* Pods */ = {
101
+ isa = PBXGroup;
102
+ children = (
103
+ 49416490449ABCCBD5CB71F5 /* Pods-Runner.debug.xcconfig */,
104
+ 73343CA27827EBEDA45E3AE4 /* Pods-Runner.release.xcconfig */,
105
+ 4453DDD0EC8DAE1A94449906 /* Pods-Runner.profile.xcconfig */,
106
+ 764D6FCF354D4C047C088B4E /* Pods-RunnerTests.debug.xcconfig */,
107
+ 546EC27D20F12194C4550A61 /* Pods-RunnerTests.release.xcconfig */,
108
+ E4C8076B210B690E52E1A9BD /* Pods-RunnerTests.profile.xcconfig */,
109
+ );
110
+ name = Pods;
111
+ path = Pods;
112
+ sourceTree = "<group>";
113
+ };
114
+ 9740EEB11CF90186004384FC /* Flutter */ = {
115
+ isa = PBXGroup;
116
+ children = (
117
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
118
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
119
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
120
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
121
+ );
122
+ name = Flutter;
123
+ sourceTree = "<group>";
124
+ };
125
+ 97C146E51CF9000F007C117D = {
126
+ isa = PBXGroup;
127
+ children = (
128
+ 9740EEB11CF90186004384FC /* Flutter */,
129
+ 97C146F01CF9000F007C117D /* Runner */,
130
+ 97C146EF1CF9000F007C117D /* Products */,
131
+ 331C8082294A63A400263BE5 /* RunnerTests */,
132
+ 3DD528BE2C5F86FFDE137E7C /* Pods */,
133
+ C5595798F4C168CEA6EC3592 /* Frameworks */,
134
+ );
135
+ sourceTree = "<group>";
136
+ };
137
+ 97C146EF1CF9000F007C117D /* Products */ = {
138
+ isa = PBXGroup;
139
+ children = (
140
+ 97C146EE1CF9000F007C117D /* Runner.app */,
141
+ 331C8081294A63A400263BE5 /* RunnerTests.xctest */,
142
+ );
143
+ name = Products;
144
+ sourceTree = "<group>";
145
+ };
146
+ 97C146F01CF9000F007C117D /* Runner */ = {
147
+ isa = PBXGroup;
148
+ children = (
149
+ 97C146FA1CF9000F007C117D /* Main.storyboard */,
150
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */,
151
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
152
+ 97C147021CF9000F007C117D /* Info.plist */,
153
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
154
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
155
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
156
+ 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
157
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
158
+ );
159
+ path = Runner;
160
+ sourceTree = "<group>";
161
+ };
162
+ C5595798F4C168CEA6EC3592 /* Frameworks */ = {
163
+ isa = PBXGroup;
164
+ children = (
165
+ 68B72B458011EED590BB10A7 /* Pods_Runner.framework */,
166
+ A429165A8A305E5EA5D3366F /* Pods_RunnerTests.framework */,
167
+ );
168
+ name = Frameworks;
169
+ sourceTree = "<group>";
170
+ };
171
+/* End PBXGroup section */
172
+
173
+/* Begin PBXNativeTarget section */
174
+ 331C8080294A63A400263BE5 /* RunnerTests */ = {
175
+ isa = PBXNativeTarget;
176
+ buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
177
+ buildPhases = (
178
+ 6CE0A9A100FDD12D7D51A4C9 /* [CP] Check Pods Manifest.lock */,
179
+ 331C807D294A63A400263BE5 /* Sources */,
180
+ 331C807F294A63A400263BE5 /* Resources */,
181
+ 9AA24F4969F6FB2D761614D7 /* Frameworks */,
182
+ );
183
+ buildRules = (
184
+ );
185
+ dependencies = (
186
+ 331C8086294A63A400263BE5 /* PBXTargetDependency */,
187
+ );
188
+ name = RunnerTests;
189
+ productName = RunnerTests;
190
+ productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
191
+ productType = "com.apple.product-type.bundle.unit-test";
192
+ };
193
+ 97C146ED1CF9000F007C117D /* Runner */ = {
194
+ isa = PBXNativeTarget;
195
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
196
+ buildPhases = (
197
+ 9D758D28133E372A39BC3817 /* [CP] Check Pods Manifest.lock */,
198
+ 9740EEB61CF901F6004384FC /* Run Script */,
199
+ 97C146EA1CF9000F007C117D /* Sources */,
200
+ 97C146EB1CF9000F007C117D /* Frameworks */,
201
+ 97C146EC1CF9000F007C117D /* Resources */,
202
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
203
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
204
+ 9659998E99141CEDEBCD85F9 /* [CP] Embed Pods Frameworks */,
205
+ );
206
+ buildRules = (
207
+ );
208
+ dependencies = (
209
+ );
210
+ name = Runner;
211
+ productName = Runner;
212
+ productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
213
+ productType = "com.apple.product-type.application";
214
+ };
215
+/* End PBXNativeTarget section */
216
+
217
+/* Begin PBXProject section */
218
+ 97C146E61CF9000F007C117D /* Project object */ = {
219
+ isa = PBXProject;
220
+ attributes = {
221
+ BuildIndependentTargetsInParallel = YES;
222
+ LastUpgradeCheck = 1510;
223
+ ORGANIZATIONNAME = "";
224
+ TargetAttributes = {
225
+ 331C8080294A63A400263BE5 = {
226
+ CreatedOnToolsVersion = 14.0;
227
+ TestTargetID = 97C146ED1CF9000F007C117D;
228
+ };
229
+ 97C146ED1CF9000F007C117D = {
230
+ CreatedOnToolsVersion = 7.3.1;
231
+ LastSwiftMigration = 1100;
232
+ };
233
+ };
234
+ };
235
+ buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
236
+ compatibilityVersion = "Xcode 9.3";
237
+ developmentRegion = en;
238
+ hasScannedForEncodings = 0;
239
+ knownRegions = (
240
+ en,
241
+ Base,
242
+ );
243
+ mainGroup = 97C146E51CF9000F007C117D;
244
+ productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
245
+ projectDirPath = "";
246
+ projectRoot = "";
247
+ targets = (
248
+ 97C146ED1CF9000F007C117D /* Runner */,
249
+ 331C8080294A63A400263BE5 /* RunnerTests */,
250
+ );
251
+ };
252
+/* End PBXProject section */
253
+
254
+/* Begin PBXResourcesBuildPhase section */
255
+ 331C807F294A63A400263BE5 /* Resources */ = {
256
+ isa = PBXResourcesBuildPhase;
257
+ buildActionMask = 2147483647;
258
+ files = (
259
+ );
260
+ runOnlyForDeploymentPostprocessing = 0;
261
+ };
262
+ 97C146EC1CF9000F007C117D /* Resources */ = {
263
+ isa = PBXResourcesBuildPhase;
264
+ buildActionMask = 2147483647;
265
+ files = (
266
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
267
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
268
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
269
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
270
+ );
271
+ runOnlyForDeploymentPostprocessing = 0;
272
+ };
273
+/* End PBXResourcesBuildPhase section */
274
+
275
+/* Begin PBXShellScriptBuildPhase section */
276
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
277
+ isa = PBXShellScriptBuildPhase;
278
+ alwaysOutOfDate = 1;
279
+ buildActionMask = 2147483647;
280
+ files = (
281
+ );
282
+ inputPaths = (
283
+ "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
284
+ );
285
+ name = "Thin Binary";
286
+ outputPaths = (
287
+ );
288
+ runOnlyForDeploymentPostprocessing = 0;
289
+ shellPath = /bin/sh;
290
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
291
+ };
292
+ 6CE0A9A100FDD12D7D51A4C9 /* [CP] Check Pods Manifest.lock */ = {
293
+ isa = PBXShellScriptBuildPhase;
294
+ buildActionMask = 2147483647;
295
+ files = (
296
+ );
297
+ inputFileListPaths = (
298
+ );
299
+ inputPaths = (
300
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
301
+ "${PODS_ROOT}/Manifest.lock",
302
+ );
303
+ name = "[CP] Check Pods Manifest.lock";
304
+ outputFileListPaths = (
305
+ );
306
+ outputPaths = (
307
+ "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
308
+ );
309
+ runOnlyForDeploymentPostprocessing = 0;
310
+ shellPath = /bin/sh;
311
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
312
+ showEnvVarsInLog = 0;
313
+ };
314
+ 9659998E99141CEDEBCD85F9 /* [CP] Embed Pods Frameworks */ = {
315
+ isa = PBXShellScriptBuildPhase;
316
+ buildActionMask = 2147483647;
317
+ files = (
318
+ );
319
+ inputFileListPaths = (
320
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
321
+ );
322
+ name = "[CP] Embed Pods Frameworks";
323
+ outputFileListPaths = (
324
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
325
+ );
326
+ runOnlyForDeploymentPostprocessing = 0;
327
+ shellPath = /bin/sh;
328
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
329
+ showEnvVarsInLog = 0;
330
+ };
331
+ 9740EEB61CF901F6004384FC /* Run Script */ = {
332
+ isa = PBXShellScriptBuildPhase;
333
+ alwaysOutOfDate = 1;
334
+ buildActionMask = 2147483647;
335
+ files = (
336
+ );
337
+ inputPaths = (
338
+ );
339
+ name = "Run Script";
340
+ outputPaths = (
341
+ );
342
+ runOnlyForDeploymentPostprocessing = 0;
343
+ shellPath = /bin/sh;
344
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
345
+ };
346
+ 9D758D28133E372A39BC3817 /* [CP] Check Pods Manifest.lock */ = {
347
+ isa = PBXShellScriptBuildPhase;
348
+ buildActionMask = 2147483647;
349
+ files = (
350
+ );
351
+ inputFileListPaths = (
352
+ );
353
+ inputPaths = (
354
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
355
+ "${PODS_ROOT}/Manifest.lock",
356
+ );
357
+ name = "[CP] Check Pods Manifest.lock";
358
+ outputFileListPaths = (
359
+ );
360
+ outputPaths = (
361
+ "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
362
+ );
363
+ runOnlyForDeploymentPostprocessing = 0;
364
+ shellPath = /bin/sh;
365
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
366
+ showEnvVarsInLog = 0;
367
+ };
368
+/* End PBXShellScriptBuildPhase section */
369
+
370
+/* Begin PBXSourcesBuildPhase section */
371
+ 331C807D294A63A400263BE5 /* Sources */ = {
372
+ isa = PBXSourcesBuildPhase;
373
+ buildActionMask = 2147483647;
374
+ files = (
375
+ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
376
+ );
377
+ runOnlyForDeploymentPostprocessing = 0;
378
+ };
379
+ 97C146EA1CF9000F007C117D /* Sources */ = {
380
+ isa = PBXSourcesBuildPhase;
381
+ buildActionMask = 2147483647;
382
+ files = (
383
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
384
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
385
+ 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
386
+ );
387
+ runOnlyForDeploymentPostprocessing = 0;
388
+ };
389
+/* End PBXSourcesBuildPhase section */
390
+
391
+/* Begin PBXTargetDependency section */
392
+ 331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
393
+ isa = PBXTargetDependency;
394
+ target = 97C146ED1CF9000F007C117D /* Runner */;
395
+ targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
396
+ };
397
+/* End PBXTargetDependency section */
398
+
399
+/* Begin PBXVariantGroup section */
400
+ 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
401
+ isa = PBXVariantGroup;
402
+ children = (
403
+ 97C146FB1CF9000F007C117D /* Base */,
404
+ );
405
+ name = Main.storyboard;
406
+ sourceTree = "<group>";
407
+ };
408
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
409
+ isa = PBXVariantGroup;
410
+ children = (
411
+ 97C147001CF9000F007C117D /* Base */,
412
+ );
413
+ name = LaunchScreen.storyboard;
414
+ sourceTree = "<group>";
415
+ };
416
+/* End PBXVariantGroup section */
417
+
418
+/* Begin XCBuildConfiguration section */
419
+ 249021D3217E4FDB00AE95B9 /* Profile */ = {
420
+ isa = XCBuildConfiguration;
421
+ buildSettings = {
422
+ ALWAYS_SEARCH_USER_PATHS = NO;
423
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
424
+ CLANG_ANALYZER_NONNULL = YES;
425
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
426
+ CLANG_CXX_LIBRARY = "libc++";
427
+ CLANG_ENABLE_MODULES = YES;
428
+ CLANG_ENABLE_OBJC_ARC = YES;
429
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
430
+ CLANG_WARN_BOOL_CONVERSION = YES;
431
+ CLANG_WARN_COMMA = YES;
432
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
433
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
434
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
435
+ CLANG_WARN_EMPTY_BODY = YES;
436
+ CLANG_WARN_ENUM_CONVERSION = YES;
437
+ CLANG_WARN_INFINITE_RECURSION = YES;
438
+ CLANG_WARN_INT_CONVERSION = YES;
439
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
440
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
441
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
442
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
443
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
444
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
445
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
446
+ CLANG_WARN_UNREACHABLE_CODE = YES;
447
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
448
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
449
+ COPY_PHASE_STRIP = NO;
450
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
451
+ ENABLE_NS_ASSERTIONS = NO;
452
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
453
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
454
+ GCC_C_LANGUAGE_STANDARD = gnu99;
455
+ GCC_NO_COMMON_BLOCKS = YES;
456
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
457
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
458
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
459
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
460
+ GCC_WARN_UNUSED_FUNCTION = YES;
461
+ GCC_WARN_UNUSED_VARIABLE = YES;
462
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
463
+ MTL_ENABLE_DEBUG_INFO = NO;
464
+ SDKROOT = iphoneos;
465
+ SUPPORTED_PLATFORMS = iphoneos;
466
+ TARGETED_DEVICE_FAMILY = "1,2";
467
+ VALIDATE_PRODUCT = YES;
468
+ };
469
+ name = Profile;
470
+ };
471
+ 249021D4217E4FDB00AE95B9 /* Profile */ = {
472
+ isa = XCBuildConfiguration;
473
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
474
+ buildSettings = {
475
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
476
+ CLANG_ENABLE_MODULES = YES;
477
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
478
+ DEVELOPMENT_TEAM = TZX5LG7ZW5;
479
+ ENABLE_BITCODE = NO;
480
+ INFOPLIST_FILE = Runner/Info.plist;
481
+ LD_RUNPATH_SEARCH_PATHS = (
482
+ "$(inherited)",
483
+ "@executable_path/Frameworks",
484
+ );
485
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.localmind;
486
+ PRODUCT_NAME = "$(TARGET_NAME)";
487
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
488
+ SWIFT_VERSION = 5.0;
489
+ VERSIONING_SYSTEM = "apple-generic";
490
+ };
491
+ name = Profile;
492
+ };
493
+ 331C8088294A63A400263BE5 /* Debug */ = {
494
+ isa = XCBuildConfiguration;
495
+ baseConfigurationReference = 764D6FCF354D4C047C088B4E /* Pods-RunnerTests.debug.xcconfig */;
496
+ buildSettings = {
497
+ BUNDLE_LOADER = "$(TEST_HOST)";
498
+ CODE_SIGN_STYLE = Automatic;
499
+ CURRENT_PROJECT_VERSION = 1;
500
+ GENERATE_INFOPLIST_FILE = YES;
501
+ MARKETING_VERSION = 1.0;
502
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.localmind.RunnerTests;
503
+ PRODUCT_NAME = "$(TARGET_NAME)";
504
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
505
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
506
+ SWIFT_VERSION = 5.0;
507
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
508
+ };
509
+ name = Debug;
510
+ };
511
+ 331C8089294A63A400263BE5 /* Release */ = {
512
+ isa = XCBuildConfiguration;
513
+ baseConfigurationReference = 546EC27D20F12194C4550A61 /* Pods-RunnerTests.release.xcconfig */;
514
+ buildSettings = {
515
+ BUNDLE_LOADER = "$(TEST_HOST)";
516
+ CODE_SIGN_STYLE = Automatic;
517
+ CURRENT_PROJECT_VERSION = 1;
518
+ GENERATE_INFOPLIST_FILE = YES;
519
+ MARKETING_VERSION = 1.0;
520
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.localmind.RunnerTests;
521
+ PRODUCT_NAME = "$(TARGET_NAME)";
522
+ SWIFT_VERSION = 5.0;
523
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
524
+ };
525
+ name = Release;
526
+ };
527
+ 331C808A294A63A400263BE5 /* Profile */ = {
528
+ isa = XCBuildConfiguration;
529
+ baseConfigurationReference = E4C8076B210B690E52E1A9BD /* Pods-RunnerTests.profile.xcconfig */;
530
+ buildSettings = {
531
+ BUNDLE_LOADER = "$(TEST_HOST)";
532
+ CODE_SIGN_STYLE = Automatic;
533
+ CURRENT_PROJECT_VERSION = 1;
534
+ GENERATE_INFOPLIST_FILE = YES;
535
+ MARKETING_VERSION = 1.0;
536
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.localmind.RunnerTests;
537
+ PRODUCT_NAME = "$(TARGET_NAME)";
538
+ SWIFT_VERSION = 5.0;
539
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
540
+ };
541
+ name = Profile;
542
+ };
543
+ 97C147031CF9000F007C117D /* Debug */ = {
544
+ isa = XCBuildConfiguration;
545
+ buildSettings = {
546
+ ALWAYS_SEARCH_USER_PATHS = NO;
547
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
548
+ CLANG_ANALYZER_NONNULL = YES;
549
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
550
+ CLANG_CXX_LIBRARY = "libc++";
551
+ CLANG_ENABLE_MODULES = YES;
552
+ CLANG_ENABLE_OBJC_ARC = YES;
553
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
554
+ CLANG_WARN_BOOL_CONVERSION = YES;
555
+ CLANG_WARN_COMMA = YES;
556
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
557
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
558
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
559
+ CLANG_WARN_EMPTY_BODY = YES;
560
+ CLANG_WARN_ENUM_CONVERSION = YES;
561
+ CLANG_WARN_INFINITE_RECURSION = YES;
562
+ CLANG_WARN_INT_CONVERSION = YES;
563
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
564
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
565
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
566
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
567
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
568
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
569
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
570
+ CLANG_WARN_UNREACHABLE_CODE = YES;
571
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
572
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
573
+ COPY_PHASE_STRIP = NO;
574
+ DEBUG_INFORMATION_FORMAT = dwarf;
575
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
576
+ ENABLE_TESTABILITY = YES;
577
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
578
+ GCC_C_LANGUAGE_STANDARD = gnu99;
579
+ GCC_DYNAMIC_NO_PIC = NO;
580
+ GCC_NO_COMMON_BLOCKS = YES;
581
+ GCC_OPTIMIZATION_LEVEL = 0;
582
+ GCC_PREPROCESSOR_DEFINITIONS = (
583
+ "DEBUG=1",
584
+ "$(inherited)",
585
+ );
586
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
587
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
588
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
589
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
590
+ GCC_WARN_UNUSED_FUNCTION = YES;
591
+ GCC_WARN_UNUSED_VARIABLE = YES;
592
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
593
+ MTL_ENABLE_DEBUG_INFO = YES;
594
+ ONLY_ACTIVE_ARCH = YES;
595
+ SDKROOT = iphoneos;
596
+ TARGETED_DEVICE_FAMILY = "1,2";
597
+ };
598
+ name = Debug;
599
+ };
600
+ 97C147041CF9000F007C117D /* Release */ = {
601
+ isa = XCBuildConfiguration;
602
+ buildSettings = {
603
+ ALWAYS_SEARCH_USER_PATHS = NO;
604
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
605
+ CLANG_ANALYZER_NONNULL = YES;
606
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
607
+ CLANG_CXX_LIBRARY = "libc++";
608
+ CLANG_ENABLE_MODULES = YES;
609
+ CLANG_ENABLE_OBJC_ARC = YES;
610
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
611
+ CLANG_WARN_BOOL_CONVERSION = YES;
612
+ CLANG_WARN_COMMA = YES;
613
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
614
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
615
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
616
+ CLANG_WARN_EMPTY_BODY = YES;
617
+ CLANG_WARN_ENUM_CONVERSION = YES;
618
+ CLANG_WARN_INFINITE_RECURSION = YES;
619
+ CLANG_WARN_INT_CONVERSION = YES;
620
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
621
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
622
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
623
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
624
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
625
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
626
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
627
+ CLANG_WARN_UNREACHABLE_CODE = YES;
628
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
629
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
630
+ COPY_PHASE_STRIP = NO;
631
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
632
+ ENABLE_NS_ASSERTIONS = NO;
633
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
634
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
635
+ GCC_C_LANGUAGE_STANDARD = gnu99;
636
+ GCC_NO_COMMON_BLOCKS = YES;
637
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
638
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
639
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
640
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
641
+ GCC_WARN_UNUSED_FUNCTION = YES;
642
+ GCC_WARN_UNUSED_VARIABLE = YES;
643
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
644
+ MTL_ENABLE_DEBUG_INFO = NO;
645
+ SDKROOT = iphoneos;
646
+ SUPPORTED_PLATFORMS = iphoneos;
647
+ SWIFT_COMPILATION_MODE = wholemodule;
648
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
649
+ TARGETED_DEVICE_FAMILY = "1,2";
650
+ VALIDATE_PRODUCT = YES;
651
+ };
652
+ name = Release;
653
+ };
654
+ 97C147061CF9000F007C117D /* Debug */ = {
655
+ isa = XCBuildConfiguration;
656
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
657
+ buildSettings = {
658
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
659
+ CLANG_ENABLE_MODULES = YES;
660
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
661
+ DEVELOPMENT_TEAM = TZX5LG7ZW5;
662
+ ENABLE_BITCODE = NO;
663
+ INFOPLIST_FILE = Runner/Info.plist;
664
+ LD_RUNPATH_SEARCH_PATHS = (
665
+ "$(inherited)",
666
+ "@executable_path/Frameworks",
667
+ );
668
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.localmind;
669
+ PRODUCT_NAME = "$(TARGET_NAME)";
670
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
671
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
672
+ SWIFT_VERSION = 5.0;
673
+ VERSIONING_SYSTEM = "apple-generic";
674
+ };
675
+ name = Debug;
676
+ };
677
+ 97C147071CF9000F007C117D /* Release */ = {
678
+ isa = XCBuildConfiguration;
679
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
680
+ buildSettings = {
681
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
682
+ CLANG_ENABLE_MODULES = YES;
683
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
684
+ DEVELOPMENT_TEAM = TZX5LG7ZW5;
685
+ ENABLE_BITCODE = NO;
686
+ INFOPLIST_FILE = Runner/Info.plist;
687
+ LD_RUNPATH_SEARCH_PATHS = (
688
+ "$(inherited)",
689
+ "@executable_path/Frameworks",
690
+ );
691
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.localmind;
692
+ PRODUCT_NAME = "$(TARGET_NAME)";
693
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
694
+ SWIFT_VERSION = 5.0;
695
+ VERSIONING_SYSTEM = "apple-generic";
696
+ };
697
+ name = Release;
698
+ };
699
+/* End XCBuildConfiguration section */
700
+
701
+/* Begin XCConfigurationList section */
702
+ 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
703
+ isa = XCConfigurationList;
704
+ buildConfigurations = (
705
+ 331C8088294A63A400263BE5 /* Debug */,
706
+ 331C8089294A63A400263BE5 /* Release */,
707
+ 331C808A294A63A400263BE5 /* Profile */,
708
+ );
709
+ defaultConfigurationIsVisible = 0;
710
+ defaultConfigurationName = Release;
711
+ };
712
+ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
713
+ isa = XCConfigurationList;
714
+ buildConfigurations = (
715
+ 97C147031CF9000F007C117D /* Debug */,
716
+ 97C147041CF9000F007C117D /* Release */,
717
+ 249021D3217E4FDB00AE95B9 /* Profile */,
718
+ );
719
+ defaultConfigurationIsVisible = 0;
720
+ defaultConfigurationName = Release;
721
+ };
722
+ 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
723
+ isa = XCConfigurationList;
724
+ buildConfigurations = (
725
+ 97C147061CF9000F007C117D /* Debug */,
726
+ 97C147071CF9000F007C117D /* Release */,
727
+ 249021D4217E4FDB00AE95B9 /* Profile */,
728
+ );
729
+ defaultConfigurationIsVisible = 0;
730
+ defaultConfigurationName = Release;
731
+ };
732
+/* End XCConfigurationList section */
733
+ };
734
+ rootObject = 97C146E61CF9000F007C117D /* Project object */;
735
+}
ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new
+7
@@ -0,0 +1,7 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<Workspace
3
+ version = "1.0">
4
+ <FileRef
5
+ location = "self:">
6
+ </FileRef>
7
+</Workspace>
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new
+8
@@ -0,0 +1,8 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+<plist version="1.0">
4
+<dict>
5
+ <key>IDEDidComputeMac32BitWarning</key>
6
+ <true/>
7
+</dict>
8
+</plist>
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new
+8
@@ -0,0 +1,8 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+<plist version="1.0">
4
+<dict>
5
+ <key>PreviewsEnabled</key>
6
+ <false/>
7
+</dict>
8
+</plist>
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new
+101
@@ -0,0 +1,101 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<Scheme
3
+ LastUpgradeVersion = "1510"
4
+ version = "1.3">
5
+ <BuildAction
6
+ parallelizeBuildables = "YES"
7
+ buildImplicitDependencies = "YES">
8
+ <BuildActionEntries>
9
+ <BuildActionEntry
10
+ buildForTesting = "YES"
11
+ buildForRunning = "YES"
12
+ buildForProfiling = "YES"
13
+ buildForArchiving = "YES"
14
+ buildForAnalyzing = "YES">
15
+ <BuildableReference
16
+ BuildableIdentifier = "primary"
17
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
18
+ BuildableName = "Runner.app"
19
+ BlueprintName = "Runner"
20
+ ReferencedContainer = "container:Runner.xcodeproj">
21
+ </BuildableReference>
22
+ </BuildActionEntry>
23
+ </BuildActionEntries>
24
+ </BuildAction>
25
+ <TestAction
26
+ buildConfiguration = "Debug"
27
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
28
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
29
+ customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
30
+ shouldUseLaunchSchemeArgsEnv = "YES">
31
+ <MacroExpansion>
32
+ <BuildableReference
33
+ BuildableIdentifier = "primary"
34
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
35
+ BuildableName = "Runner.app"
36
+ BlueprintName = "Runner"
37
+ ReferencedContainer = "container:Runner.xcodeproj">
38
+ </BuildableReference>
39
+ </MacroExpansion>
40
+ <Testables>
41
+ <TestableReference
42
+ skipped = "NO"
43
+ parallelizable = "YES">
44
+ <BuildableReference
45
+ BuildableIdentifier = "primary"
46
+ BlueprintIdentifier = "331C8080294A63A400263BE5"
47
+ BuildableName = "RunnerTests.xctest"
48
+ BlueprintName = "RunnerTests"
49
+ ReferencedContainer = "container:Runner.xcodeproj">
50
+ </BuildableReference>
51
+ </TestableReference>
52
+ </Testables>
53
+ </TestAction>
54
+ <LaunchAction
55
+ buildConfiguration = "Debug"
56
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
57
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
58
+ customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
59
+ launchStyle = "0"
60
+ useCustomWorkingDirectory = "NO"
61
+ ignoresPersistentStateOnLaunch = "NO"
62
+ debugDocumentVersioning = "YES"
63
+ debugServiceExtension = "internal"
64
+ enableGPUValidationMode = "1"
65
+ allowLocationSimulation = "YES">
66
+ <BuildableProductRunnable
67
+ runnableDebuggingMode = "0">
68
+ <BuildableReference
69
+ BuildableIdentifier = "primary"
70
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
71
+ BuildableName = "Runner.app"
72
+ BlueprintName = "Runner"
73
+ ReferencedContainer = "container:Runner.xcodeproj">
74
+ </BuildableReference>
75
+ </BuildableProductRunnable>
76
+ </LaunchAction>
77
+ <ProfileAction
78
+ buildConfiguration = "Profile"
79
+ shouldUseLaunchSchemeArgsEnv = "YES"
80
+ savedToolIdentifier = ""
81
+ useCustomWorkingDirectory = "NO"
82
+ debugDocumentVersioning = "YES">
83
+ <BuildableProductRunnable
84
+ runnableDebuggingMode = "0">
85
+ <BuildableReference
86
+ BuildableIdentifier = "primary"
87
+ BlueprintIdentifier = "97C146ED1CF9000F007C117D"
88
+ BuildableName = "Runner.app"
89
+ BlueprintName = "Runner"
90
+ ReferencedContainer = "container:Runner.xcodeproj">
91
+ </BuildableReference>
92
+ </BuildableProductRunnable>
93
+ </ProfileAction>
94
+ <AnalyzeAction
95
+ buildConfiguration = "Debug">
96
+ </AnalyzeAction>
97
+ <ArchiveAction
98
+ buildConfiguration = "Release"
99
+ revealArchiveInOrganizer = "YES">
100
+ </ArchiveAction>
101
+</Scheme>
ios/Runner.xcworkspace/contents.xcworkspacedata
new
+10
@@ -0,0 +1,10 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<Workspace
3
+ version = "1.0">
4
+ <FileRef
5
+ location = "group:Runner.xcodeproj">
6
+ </FileRef>
7
+ <FileRef
8
+ location = "group:Pods/Pods.xcodeproj">
9
+ </FileRef>
10
+</Workspace>
ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new
+8
@@ -0,0 +1,8 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+<plist version="1.0">
4
+<dict>
5
+ <key>IDEDidComputeMac32BitWarning</key>
6
+ <true/>
7
+</dict>
8
+</plist>
ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new
+8
@@ -0,0 +1,8 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+<plist version="1.0">
4
+<dict>
5
+ <key>PreviewsEnabled</key>
6
+ <false/>
7
+</dict>
8
+</plist>
ios/Runner/AppDelegate.swift
new
+16
@@ -0,0 +1,16 @@
1
+import Flutter
2
+import UIKit
3
+
4
+@main
5
+@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
6
+ override func application(
7
+ _ application: UIApplication,
8
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
9
+ ) -> Bool {
10
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
11
+ }
12
+
13
+ func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
14
+ GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
15
+ }
16
+}
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png differ
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png differ
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png differ
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png differ
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png differ
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png differ
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png differ
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png differ
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png differ
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png differ
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png differ
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png differ
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png differ
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png differ
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png differ
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png differ
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png differ
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png differ
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png differ
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png differ
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png differ
ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new
+134
@@ -0,0 +1,134 @@
1
+{
2
+ "images": [
3
+ {
4
+ "filename": "AppIcon@2x.png",
5
+ "idiom": "iphone",
6
+ "scale": "2x",
7
+ "size": "60x60"
8
+ },
9
+ {
10
+ "filename": "AppIcon@3x.png",
11
+ "idiom": "iphone",
12
+ "scale": "3x",
13
+ "size": "60x60"
14
+ },
15
+ {
16
+ "filename": "AppIcon~ipad.png",
17
+ "idiom": "ipad",
18
+ "scale": "1x",
19
+ "size": "76x76"
20
+ },
21
+ {
22
+ "filename": "AppIcon@2x~ipad.png",
23
+ "idiom": "ipad",
24
+ "scale": "2x",
25
+ "size": "76x76"
26
+ },
27
+ {
28
+ "filename": "AppIcon-83.5@2x~ipad.png",
29
+ "idiom": "ipad",
30
+ "scale": "2x",
31
+ "size": "83.5x83.5"
32
+ },
33
+ {
34
+ "filename": "AppIcon-40@2x.png",
35
+ "idiom": "iphone",
36
+ "scale": "2x",
37
+ "size": "40x40"
38
+ },
39
+ {
40
+ "filename": "AppIcon-40@3x.png",
41
+ "idiom": "iphone",
42
+ "scale": "3x",
43
+ "size": "40x40"
44
+ },
45
+ {
46
+ "filename": "AppIcon-40~ipad.png",
47
+ "idiom": "ipad",
48
+ "scale": "1x",
49
+ "size": "40x40"
50
+ },
51
+ {
52
+ "filename": "AppIcon-40@2x~ipad.png",
53
+ "idiom": "ipad",
54
+ "scale": "2x",
55
+ "size": "40x40"
56
+ },
57
+ {
58
+ "filename": "AppIcon-20@2x.png",
59
+ "idiom": "iphone",
60
+ "scale": "2x",
61
+ "size": "20x20"
62
+ },
63
+ {
64
+ "filename": "AppIcon-20@3x.png",
65
+ "idiom": "iphone",
66
+ "scale": "3x",
67
+ "size": "20x20"
68
+ },
69
+ {
70
+ "filename": "AppIcon-20~ipad.png",
71
+ "idiom": "ipad",
72
+ "scale": "1x",
73
+ "size": "20x20"
74
+ },
75
+ {
76
+ "filename": "AppIcon-20@2x~ipad.png",
77
+ "idiom": "ipad",
78
+ "scale": "2x",
79
+ "size": "20x20"
80
+ },
81
+ {
82
+ "filename": "AppIcon-29.png",
83
+ "idiom": "iphone",
84
+ "scale": "1x",
85
+ "size": "29x29"
86
+ },
87
+ {
88
+ "filename": "AppIcon-29@2x.png",
89
+ "idiom": "iphone",
90
+ "scale": "2x",
91
+ "size": "29x29"
92
+ },
93
+ {
94
+ "filename": "AppIcon-29@3x.png",
95
+ "idiom": "iphone",
96
+ "scale": "3x",
97
+ "size": "29x29"
98
+ },
99
+ {
100
+ "filename": "AppIcon-29~ipad.png",
101
+ "idiom": "ipad",
102
+ "scale": "1x",
103
+ "size": "29x29"
104
+ },
105
+ {
106
+ "filename": "AppIcon-29@2x~ipad.png",
107
+ "idiom": "ipad",
108
+ "scale": "2x",
109
+ "size": "29x29"
110
+ },
111
+ {
112
+ "filename": "AppIcon-60@2x~car.png",
113
+ "idiom": "car",
114
+ "scale": "2x",
115
+ "size": "60x60"
116
+ },
117
+ {
118
+ "filename": "AppIcon-60@3x~car.png",
119
+ "idiom": "car",
120
+ "scale": "3x",
121
+ "size": "60x60"
122
+ },
123
+ {
124
+ "filename": "AppIcon~ios-marketing.png",
125
+ "idiom": "ios-marketing",
126
+ "scale": "1x",
127
+ "size": "1024x1024"
128
+ }
129
+ ],
130
+ "info": {
131
+ "author": "iconkitchen",
132
+ "version": 1
133
+ }
134
+}
\ No newline at end of file
ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new
+23
@@ -0,0 +1,23 @@
1
+{
2
+ "images" : [
3
+ {
4
+ "idiom" : "universal",
5
+ "filename" : "LaunchImage.png",
6
+ "scale" : "1x"
7
+ },
8
+ {
9
+ "idiom" : "universal",
10
+ "filename" : "LaunchImage@2x.png",
11
+ "scale" : "2x"
12
+ },
13
+ {
14
+ "idiom" : "universal",
15
+ "filename" : "LaunchImage@3x.png",
16
+ "scale" : "3x"
17
+ }
18
+ ],
19
+ "info" : {
20
+ "version" : 1,
21
+ "author" : "xcode"
22
+ }
23
+}
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ
ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new
+5
@@ -0,0 +1,5 @@
1
+# Launch Screen Assets
2
+
3
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
4
+
5
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
ios/Runner/Base.lproj/LaunchScreen.storyboard
new
+37
@@ -0,0 +1,37 @@
1
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
3
+ <dependencies>
4
+ <deployment identifier="iOS"/>
5
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
6
+ </dependencies>
7
+ <scenes>
8
+ <!--View Controller-->
9
+ <scene sceneID="EHf-IW-A2E">
10
+ <objects>
11
+ <viewController id="01J-lp-oVM" sceneMemberID="viewController">
12
+ <layoutGuides>
13
+ <viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
14
+ <viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
15
+ </layoutGuides>
16
+ <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
17
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
18
+ <subviews>
19
+ <imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
20
+ </imageView>
21
+ </subviews>
22
+ <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
23
+ <constraints>
24
+ <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
25
+ <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
26
+ </constraints>
27
+ </view>
28
+ </viewController>
29
+ <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
30
+ </objects>
31
+ <point key="canvasLocation" x="53" y="375"/>
32
+ </scene>
33
+ </scenes>
34
+ <resources>
35
+ <image name="LaunchImage" width="168" height="185"/>
36
+ </resources>
37
+</document>
ios/Runner/Base.lproj/Main.storyboard
new
+26
@@ -0,0 +1,26 @@
1
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
3
+ <dependencies>
4
+ <deployment identifier="iOS"/>
5
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
6
+ </dependencies>
7
+ <scenes>
8
+ <!--Flutter View Controller-->
9
+ <scene sceneID="tne-QT-ifu">
10
+ <objects>
11
+ <viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
12
+ <layoutGuides>
13
+ <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
14
+ <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
15
+ </layoutGuides>
16
+ <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
17
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
18
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
19
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
20
+ </view>
21
+ </viewController>
22
+ <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
23
+ </objects>
24
+ </scene>
25
+ </scenes>
26
+</document>
ios/Runner/Info.plist
new
+70
@@ -0,0 +1,70 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+<plist version="1.0">
4
+<dict>
5
+ <key>CADisableMinimumFrameDurationOnPhone</key>
6
+ <true/>
7
+ <key>CFBundleDevelopmentRegion</key>
8
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
9
+ <key>CFBundleDisplayName</key>
10
+ <string>Localmind</string>
11
+ <key>CFBundleExecutable</key>
12
+ <string>$(EXECUTABLE_NAME)</string>
13
+ <key>CFBundleIdentifier</key>
14
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
15
+ <key>CFBundleInfoDictionaryVersion</key>
16
+ <string>6.0</string>
17
+ <key>CFBundleName</key>
18
+ <string>localmind</string>
19
+ <key>CFBundlePackageType</key>
20
+ <string>APPL</string>
21
+ <key>CFBundleShortVersionString</key>
22
+ <string>$(FLUTTER_BUILD_NAME)</string>
23
+ <key>CFBundleSignature</key>
24
+ <string>????</string>
25
+ <key>CFBundleVersion</key>
26
+ <string>$(FLUTTER_BUILD_NUMBER)</string>
27
+ <key>LSRequiresIPhoneOS</key>
28
+ <true/>
29
+ <key>UIApplicationSceneManifest</key>
30
+ <dict>
31
+ <key>UIApplicationSupportsMultipleScenes</key>
32
+ <false/>
33
+ <key>UISceneConfigurations</key>
34
+ <dict>
35
+ <key>UIWindowSceneSessionRoleApplication</key>
36
+ <array>
37
+ <dict>
38
+ <key>UISceneClassName</key>
39
+ <string>UIWindowScene</string>
40
+ <key>UISceneConfigurationName</key>
41
+ <string>flutter</string>
42
+ <key>UISceneDelegateClassName</key>
43
+ <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
44
+ <key>UISceneStoryboardFile</key>
45
+ <string>Main</string>
46
+ </dict>
47
+ </array>
48
+ </dict>
49
+ </dict>
50
+ <key>UIApplicationSupportsIndirectInputEvents</key>
51
+ <true/>
52
+ <key>UILaunchStoryboardName</key>
53
+ <string>LaunchScreen</string>
54
+ <key>UIMainStoryboardFile</key>
55
+ <string>Main</string>
56
+ <key>UISupportedInterfaceOrientations</key>
57
+ <array>
58
+ <string>UIInterfaceOrientationPortrait</string>
59
+ <string>UIInterfaceOrientationLandscapeLeft</string>
60
+ <string>UIInterfaceOrientationLandscapeRight</string>
61
+ </array>
62
+ <key>UISupportedInterfaceOrientations~ipad</key>
63
+ <array>
64
+ <string>UIInterfaceOrientationPortrait</string>
65
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
66
+ <string>UIInterfaceOrientationLandscapeLeft</string>
67
+ <string>UIInterfaceOrientationLandscapeRight</string>
68
+ </array>
69
+</dict>
70
+</plist>
ios/Runner/Runner-Bridging-Header.h
new
+1
@@ -0,0 +1 @@
1
+#import "GeneratedPluginRegistrant.h"
ios/Runner/SceneDelegate.swift
new
+6
@@ -0,0 +1,6 @@
1
+import Flutter
2
+import UIKit
3
+
4
+class SceneDelegate: FlutterSceneDelegate {
5
+
6
+}
ios/RunnerTests/RunnerTests.swift
new
+12
@@ -0,0 +1,12 @@
1
+import Flutter
2
+import UIKit
3
+import XCTest
4
+
5
+class RunnerTests: XCTestCase {
6
+
7
+ func testExample() {
8
+ // If you add code to the Runner application, consider adding tests here.
9
+ // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
10
+ }
11
+
12
+}
lib/app.dart
new
+375
@@ -0,0 +1,375 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+import 'package:go_router/go_router.dart';
4
+import 'package:hugeicons/hugeicons.dart';
5
+import 'package:haptic_feedback/haptic_feedback.dart';
6
+import 'package:shadcn_ui/shadcn_ui.dart';
7
+import 'core/theme/app_theme.dart';
8
+import 'core/providers/app_providers.dart';
9
+import 'package:localmind/features/servers/data/models/server.dart';
10
+import 'features/chat/screens/chat_screen.dart';
11
+import 'features/conversations/screens/conversation_drawer.dart';
12
+import 'features/servers/screens/server_list_screen.dart';
13
+import 'features/servers/screens/add_server_screen.dart';
14
+import 'features/personas/screens/persona_list_screen.dart';
15
+import 'features/personas/screens/create_persona_screen.dart';
16
+import 'features/settings/screens/settings_screen.dart';
17
+import 'features/onboarding/screens/onboarding_server_type_screen.dart';
18
+import 'features/onboarding/screens/onboarding_server_setup_screen.dart';
19
+import 'features/onboarding/screens/onboarding_theme_screen.dart';
20
+import 'core/routes/app_routes.dart';
21
+import 'core/models/enums.dart';
22
+
23
+final _rootNavigatorKey = GlobalKey<NavigatorState>();
24
+final _shellNavigatorKey = GlobalKey<NavigatorState>();
25
+
26
+final routerProvider = Provider<GoRouter>((ref) {
27
+ return GoRouter(
28
+ navigatorKey: _rootNavigatorKey,
29
+ initialLocation: '/',
30
+ redirect: (context, state) {
31
+ final hasCompletedOnboarding = ref
32
+ .read(settingsProvider)
33
+ .hasCompletedOnboarding;
34
+ final isGoingToOnboarding = state.uri.toString().startsWith(
35
+ '/onboarding',
36
+ );
37
+
38
+ if (!hasCompletedOnboarding && !isGoingToOnboarding) {
39
+ return AppRoutes.onboarding;
40
+ }
41
+
42
+ if (hasCompletedOnboarding && isGoingToOnboarding) {
43
+ return AppRoutes.home;
44
+ }
45
+
46
+ return null;
47
+ },
48
+ routes: [
49
+ GoRoute(
50
+ path: AppRoutes.onboarding,
51
+ pageBuilder: (context, state) =>
52
+ const NoTransitionPage(child: OnboardingServerTypeScreen()),
53
+ ),
54
+ GoRoute(
55
+ path: AppRoutes.onboardingSetup,
56
+ pageBuilder: (context, state) {
57
+ final serverType = state.extra as ServerType?;
58
+ return MaterialPage(
59
+ child: OnboardingServerSetupScreen(
60
+ selectedType: serverType ?? ServerType.lmStudio,
61
+ ),
62
+ );
63
+ },
64
+ ),
65
+ GoRoute(
66
+ path: AppRoutes.onboardingTheme,
67
+ pageBuilder: (context, state) =>
68
+ const MaterialPage(child: OnboardingThemeScreen()),
69
+ ),
70
+ ShellRoute(
71
+ navigatorKey: _shellNavigatorKey,
72
+ builder: (context, state, child) {
73
+ return AppShell(child: child);
74
+ },
75
+ routes: [
76
+ GoRoute(
77
+ path: AppRoutes.home,
78
+ pageBuilder: (context, state) =>
79
+ const NoTransitionPage(child: ChatScreen()),
80
+ ),
81
+ GoRoute(
82
+ path: AppRoutes.conversations,
83
+ pageBuilder: (context, state) =>
84
+ const NoTransitionPage(child: ConversationDrawer()),
85
+ ),
86
+ GoRoute(
87
+ path: AppRoutes.servers,
88
+ pageBuilder: (context, state) =>
89
+ const NoTransitionPage(child: ServerListScreen()),
90
+ ),
91
+ GoRoute(
92
+ path: AppRoutes.addServer,
93
+ pageBuilder: (context, state) {
94
+ final server = state.extra as Server?;
95
+ return MaterialPage(child: AddServerScreen(editServer: server));
96
+ },
97
+ ),
98
+ GoRoute(
99
+ path: AppRoutes.personas,
100
+ pageBuilder: (context, state) =>
101
+ const NoTransitionPage(child: PersonaListScreen()),
102
+ ),
103
+ GoRoute(
104
+ path: AppRoutes.createPersona,
105
+ pageBuilder: (context, state) =>
106
+ const MaterialPage(child: CreatePersonaScreen()),
107
+ ),
108
+ GoRoute(
109
+ path: AppRoutes.settings,
110
+ pageBuilder: (context, state) =>
111
+ const NoTransitionPage(child: SettingsScreen()),
112
+ ),
113
+ ],
114
+ ),
115
+ ],
116
+ );
117
+});
118
+
119
+class App extends ConsumerWidget {
120
+ const App({super.key});
121
+
122
+ @override
123
+ Widget build(BuildContext context, WidgetRef ref) {
124
+ final router = ref.watch(routerProvider);
125
+ final appThemeType = ref.watch(themeModeProvider);
126
+
127
+ ThemeData theme = AppTheme.lightTheme;
128
+ ThemeData darkTheme = AppTheme.darkTheme;
129
+ ThemeMode themeMode = ThemeMode.system;
130
+
131
+ var shadTheme = AppTheme.lightShadTheme;
132
+ var shadDarkTheme = AppTheme.darkShadTheme;
133
+
134
+ switch (appThemeType) {
135
+ case AppThemeType.light:
136
+ themeMode = ThemeMode.light;
137
+ shadTheme = AppTheme.lightShadTheme;
138
+ break;
139
+ case AppThemeType.dark:
140
+ themeMode = ThemeMode.dark;
141
+ shadTheme = AppTheme.darkShadTheme;
142
+ // Also ensure shadDarkTheme matches so there's no mismatch
143
+ shadDarkTheme = AppTheme.darkShadTheme;
144
+ break;
145
+ case AppThemeType.claude:
146
+ themeMode = ThemeMode.light; // Force light mode for claude basically
147
+ theme = AppTheme.claudeTheme;
148
+ shadTheme = AppTheme.claudeShadTheme;
149
+ break;
150
+ case AppThemeType.system:
151
+ themeMode = ThemeMode.system;
152
+ shadTheme = AppTheme.lightShadTheme;
153
+ shadDarkTheme = AppTheme.darkShadTheme;
154
+ break;
155
+ }
156
+
157
+ return ShadApp.custom(
158
+ themeMode: themeMode,
159
+ theme: shadTheme,
160
+ darkTheme: shadDarkTheme,
161
+ appBuilder: (context) {
162
+ return MaterialApp.router(
163
+ title: 'LocalMind',
164
+ debugShowCheckedModeBanner: false,
165
+ theme: theme,
166
+ darkTheme: darkTheme,
167
+ themeMode: themeMode,
168
+ routerConfig: router,
169
+ builder: (context, child) {
170
+ return ShadAppBuilder(child: child!);
171
+ },
172
+ );
173
+ },
174
+ );
175
+ }
176
+}
177
+
178
+class AppShell extends StatelessWidget {
179
+ final Widget child;
180
+
181
+ const AppShell({super.key, required this.child});
182
+
183
+ int _getCurrentIndex(BuildContext context) {
184
+ final location = GoRouterState.of(context).uri.toString();
185
+ if (location == '/' || location.isEmpty) return 0;
186
+ if (location.startsWith('/conversations')) return 1;
187
+ if (location.startsWith('/servers')) return 2;
188
+ if (location.startsWith('/personas')) return 3;
189
+ if (location.startsWith('/settings')) return 4;
190
+ return 0;
191
+ }
192
+
193
+ void _onDestinationSelected(int index, BuildContext context) {
194
+ Haptics.vibrate(HapticsType.light);
195
+ switch (index) {
196
+ case 0:
197
+ context.go(AppRoutes.home);
198
+ break;
199
+ case 1:
200
+ context.go(AppRoutes.conversations);
201
+ break;
202
+ case 2:
203
+ context.go(AppRoutes.servers);
204
+ break;
205
+ case 3:
206
+ context.go(AppRoutes.personas);
207
+ break;
208
+ case 4:
209
+ context.go(AppRoutes.settings);
210
+ break;
211
+ }
212
+ }
213
+
214
+ static void openDrawer(BuildContext context) {
215
+ Scaffold.of(context).openDrawer();
216
+ }
217
+
218
+ @override
219
+ Widget build(BuildContext context) {
220
+ final currentIndex = _getCurrentIndex(context);
221
+
222
+ return Scaffold(
223
+ body: child,
224
+ drawer: _NavigationDrawer(
225
+ currentIndex: currentIndex,
226
+ onDestinationSelected: (index) {
227
+ Navigator.of(context).pop();
228
+ _onDestinationSelected(index, context);
229
+ },
230
+ ),
231
+ );
232
+ }
233
+}
234
+
235
+class _NavigationDrawer extends StatelessWidget {
236
+ const _NavigationDrawer({
237
+ required this.currentIndex,
238
+ required this.onDestinationSelected,
239
+ });
240
+
241
+ final int currentIndex;
242
+ final void Function(int) onDestinationSelected;
243
+
244
+ @override
245
+ Widget build(BuildContext context) {
246
+ final theme = Theme.of(context);
247
+ final isDark = theme.brightness == Brightness.dark;
248
+
249
+ return Drawer(
250
+ backgroundColor: isDark
251
+ ? const Color(0xFF0A0A0A)
252
+ : const Color(0xFFFAFAFA),
253
+ child: SafeArea(
254
+ child: Column(
255
+ children: [
256
+ Container(
257
+ padding: const EdgeInsets.all(20),
258
+ child: Row(
259
+ children: [
260
+ Container(
261
+ padding: const EdgeInsets.all(8),
262
+ decoration: BoxDecoration(
263
+ color: isDark
264
+ ? const Color(0xFF3B82F6)
265
+ : const Color(0xFF2563EB),
266
+ borderRadius: BorderRadius.circular(8),
267
+ ),
268
+ child: const Icon(
269
+ Icons.psychology,
270
+ color: Colors.white,
271
+ size: 24,
272
+ ),
273
+ ),
274
+ const SizedBox(width: 12),
275
+ Text(
276
+ 'LocalMind',
277
+ style: TextStyle(
278
+ fontSize: 20,
279
+ fontWeight: FontWeight.bold,
280
+ color: isDark ? Colors.white : Colors.black,
281
+ ),
282
+ ),
283
+ ],
284
+ ),
285
+ ),
286
+ const Divider(height: 1),
287
+ const SizedBox(height: 8),
288
+ _DrawerItem(
289
+ iconData: HugeIcons.strokeRoundedMessageMultiple01,
290
+ label: 'Chat',
291
+ isSelected: currentIndex == 0,
292
+ onTap: () => onDestinationSelected(0),
293
+ ),
294
+ _DrawerItem(
295
+ iconData: HugeIcons.strokeRoundedTimer02,
296
+ label: 'History',
297
+ isSelected: currentIndex == 1,
298
+ onTap: () => onDestinationSelected(1),
299
+ ),
300
+ _DrawerItem(
301
+ iconData: HugeIcons.strokeRoundedServerStack01,
302
+ label: 'Servers',
303
+ isSelected: currentIndex == 2,
304
+ onTap: () => onDestinationSelected(2),
305
+ ),
306
+ _DrawerItem(
307
+ iconData: HugeIcons.strokeRoundedCompass01,
308
+ label: 'Personas',
309
+ isSelected: currentIndex == 3,
310
+ onTap: () => onDestinationSelected(3),
311
+ ),
312
+ const Spacer(),
313
+ const Divider(height: 1),
314
+ _DrawerItem(
315
+ iconData: HugeIcons.strokeRoundedSettings01,
316
+ label: 'Settings',
317
+ isSelected: currentIndex == 4,
318
+ onTap: () => onDestinationSelected(4),
319
+ ),
320
+ const SizedBox(height: 8),
321
+ ],
322
+ ),
323
+ ),
324
+ );
325
+ }
326
+}
327
+
328
+class _DrawerItem extends StatelessWidget {
329
+ const _DrawerItem({
330
+ required this.iconData,
331
+ required this.label,
332
+ required this.isSelected,
333
+ required this.onTap,
334
+ });
335
+
336
+ final List<List<dynamic>> iconData;
337
+ final String label;
338
+ final bool isSelected;
339
+ final VoidCallback onTap;
340
+
341
+ @override
342
+ Widget build(BuildContext context) {
343
+ final theme = Theme.of(context);
344
+ final isDark = theme.brightness == Brightness.dark;
345
+
346
+ return Padding(
347
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
348
+ child: ListTile(
349
+ leading: HugeIcon(
350
+ icon: iconData,
351
+ size: 22,
352
+ color: isSelected
353
+ ? (isDark ? const Color(0xFF3B82F6) : const Color(0xFF2563EB))
354
+ : (isDark ? const Color(0xFF888888) : const Color(0xFF666666)),
355
+ ),
356
+ title: Text(
357
+ label,
358
+ style: TextStyle(
359
+ fontSize: 15,
360
+ fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
361
+ color: isSelected
362
+ ? (isDark ? Colors.white : Colors.black)
363
+ : (isDark ? const Color(0xFFA0A0A0) : const Color(0xFF666666)),
364
+ ),
365
+ ),
366
+ selected: isSelected,
367
+ selectedTileColor: isDark
368
+ ? const Color(0xFF3B82F6).withAlpha(25)
369
+ : const Color(0xFF2563EB).withAlpha(25),
370
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
371
+ onTap: onTap,
372
+ ),
373
+ );
374
+ }
375
+}
lib/core/constants/app_constants.dart
new
+24
@@ -0,0 +1,24 @@
1
+class AppConstants {
2
+ static const String appName = 'LocalMind';
3
+ static const String appTagline = 'Your AI. Your Device. Your Rules.';
4
+ static const String version = '1.0.0';
5
+
6
+ static const double defaultTemperature = 0.7;
7
+ static const double defaultTopP = 0.9;
8
+ static const int defaultMaxTokens = 2048;
9
+ static const int defaultContextLength = 4096;
10
+
11
+ static const int connectionTimeoutMs = 5000;
12
+ static const int receiveTimeoutMs = 60000;
13
+
14
+ static const String serversBox = 'servers';
15
+ static const String messagesBox = 'messages';
16
+ static const String conversationsBox = 'conversations';
17
+ static const String personasBox = 'personas';
18
+ static const String settingsBox = 'settings';
19
+
20
+ static const int lmStudioDefaultPort = 1234;
21
+ static const int ollamaDefaultPort = 11434;
22
+
23
+ static const String openRouterBaseUrl = 'https://openrouter.ai/api/v1';
24
+}
lib/core/models/enums.dart
new
+85
@@ -0,0 +1,85 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:hive_ce/hive.dart';
3
+
4
+enum ServerType { lmStudio, ollama, openRouter }
5
+
6
+enum ConnectionStatus { connected, disconnected, checking, error }
7
+
8
+enum MessageRole { user, assistant, system }
9
+
10
+enum MessageStatus { sending, streaming, complete, error }
11
+
12
+class ServerTypeAdapter extends TypeAdapter<ServerType> {
13
+ @override
14
+ final int typeId = 10;
15
+
16
+ @override
17
+ ServerType read(BinaryReader reader) {
18
+ return ServerType.values[reader.readInt()];
19
+ }
20
+
21
+ @override
22
+ void write(BinaryWriter writer, ServerType obj) {
23
+ writer.writeInt(obj.index);
24
+ }
25
+}
26
+
27
+class ConnectionStatusAdapter extends TypeAdapter<ConnectionStatus> {
28
+ @override
29
+ final int typeId = 11;
30
+
31
+ @override
32
+ ConnectionStatus read(BinaryReader reader) {
33
+ return ConnectionStatus.values[reader.readInt()];
34
+ }
35
+
36
+ @override
37
+ void write(BinaryWriter writer, ConnectionStatus obj) {
38
+ writer.writeInt(obj.index);
39
+ }
40
+}
41
+
42
+class MessageRoleAdapter extends TypeAdapter<MessageRole> {
43
+ @override
44
+ final int typeId = 12;
45
+
46
+ @override
47
+ MessageRole read(BinaryReader reader) {
48
+ return MessageRole.values[reader.readInt()];
49
+ }
50
+
51
+ @override
52
+ void write(BinaryWriter writer, MessageRole obj) {
53
+ writer.writeInt(obj.index);
54
+ }
55
+}
56
+
57
+class MessageStatusAdapter extends TypeAdapter<MessageStatus> {
58
+ @override
59
+ final int typeId = 13;
60
+
61
+ @override
62
+ MessageStatus read(BinaryReader reader) {
63
+ return MessageStatus.values[reader.readInt()];
64
+ }
65
+
66
+ @override
67
+ void write(BinaryWriter writer, MessageStatus obj) {
68
+ writer.writeInt(obj.index);
69
+ }
70
+}
71
+
72
+class ThemeModeAdapter extends TypeAdapter<ThemeMode> {
73
+ @override
74
+ final int typeId = 14;
75
+
76
+ @override
77
+ ThemeMode read(BinaryReader reader) {
78
+ return ThemeMode.values[reader.readInt()];
79
+ }
80
+
81
+ @override
82
+ void write(BinaryWriter writer, ThemeMode obj) {
83
+ writer.writeInt(obj.index);
84
+ }
85
+}
lib/core/providers/app_providers.dart
new
+57
@@ -0,0 +1,57 @@
1
+import 'package:flutter_riverpod/flutter_riverpod.dart';
2
+import 'package:localmind/features/settings/data/models/app_settings.dart';
3
+import 'storage_providers.dart';
4
+
5
+import '../theme/app_theme.dart';
6
+
7
+final themeModeProvider = NotifierProvider<ThemeModeNotifier, AppThemeType>(() {
8
+ return ThemeModeNotifier();
9
+});
10
+
11
+class ThemeModeNotifier extends Notifier<AppThemeType> {
12
+ @override
13
+ AppThemeType build() {
14
+ final boxes = ref.watch(hiveBoxesProvider);
15
+ final savedMode = boxes.settings.get('themeMode');
16
+ if (savedMode is int &&
17
+ savedMode >= 0 &&
18
+ savedMode < AppThemeType.values.length) {
19
+ return AppThemeType.values[savedMode];
20
+ }
21
+ return AppThemeType.system;
22
+ }
23
+
24
+ void setThemeMode(AppThemeType mode) {
25
+ final boxes = ref.read(hiveBoxesProvider);
26
+ state = mode;
27
+ boxes.settings.put('themeMode', mode.index);
28
+ }
29
+}
30
+
31
+final settingsProvider = NotifierProvider<SettingsNotifier, AppSettings>(() {
32
+ return SettingsNotifier();
33
+});
34
+
35
+class SettingsNotifier extends Notifier<AppSettings> {
36
+ @override
37
+ AppSettings build() {
38
+ final boxes = ref.watch(hiveBoxesProvider);
39
+ final saved = boxes.settings.get('appSettings');
40
+ if (saved is AppSettings) {
41
+ return saved;
42
+ }
43
+ return AppSettings();
44
+ }
45
+
46
+ Future<void> updateSettings(AppSettings appSettings) async {
47
+ final boxes = ref.read(hiveBoxesProvider);
48
+ state = appSettings;
49
+ await boxes.settings.put('appSettings', appSettings);
50
+ }
51
+
52
+ Future<void> resetToDefaults() async {
53
+ final boxes = ref.read(hiveBoxesProvider);
54
+ state = AppSettings();
55
+ await boxes.settings.put('appSettings', state);
56
+ }
57
+}
lib/core/providers/service_providers.dart
new
+17
@@ -0,0 +1,17 @@
1
+import 'package:flutter_riverpod/flutter_riverpod.dart';
2
+import 'package:dio/dio.dart';
3
+import '../constants/app_constants.dart';
4
+import 'package:localmind/features/servers/data/server_api_service.dart';
5
+
6
+final dioProvider = Provider<Dio>((ref) {
7
+ return Dio(
8
+ BaseOptions(
9
+ connectTimeout: Duration(milliseconds: AppConstants.connectionTimeoutMs),
10
+ receiveTimeout: Duration(milliseconds: AppConstants.receiveTimeoutMs),
11
+ ),
12
+ );
13
+});
14
+
15
+final serverApiServiceProvider = Provider<ServerApiService>((ref) {
16
+ return ServerApiService(ref.read(dioProvider));
17
+});
lib/core/providers/storage_providers.dart
new
+41
@@ -0,0 +1,41 @@
1
+import 'package:flutter_riverpod/flutter_riverpod.dart';
2
+import 'package:hive_ce/hive.dart';
3
+import 'package:localmind/features/conversations/data/models/conversation.dart';
4
+import 'package:localmind/features/personas/data/models/persona.dart';
5
+import '../storage/hive_initializer.dart';
6
+
7
+final hiveBoxesProvider = Provider<HiveBoxes>((ref) {
8
+ throw UnimplementedError('Must be overridden in main.dart');
9
+});
10
+
11
+final settingsBoxProvider = Provider<dynamic>((ref) {
12
+ final boxes = ref.watch(hiveBoxesProvider);
13
+ return boxes.settings;
14
+});
15
+
16
+final conversationsBoxProvider = Provider<Box<Conversation>>((ref) {
17
+ final boxes = ref.watch(hiveBoxesProvider);
18
+ return boxes.conversations;
19
+});
20
+
21
+final conversationsProvider = Provider<List<Conversation>>((ref) {
22
+ final box = ref.watch(conversationsBoxProvider);
23
+ final conversations = box.values.toList();
24
+ conversations.sort((a, b) => b.updatedAt.compareTo(a.updatedAt));
25
+ return conversations;
26
+});
27
+
28
+final recentConversationsProvider = Provider<List<Conversation>>((ref) {
29
+ final all = ref.watch(conversationsProvider);
30
+ return all.take(5).toList();
31
+});
32
+
33
+final personasBoxProvider = Provider<Box<Persona>>((ref) {
34
+ final boxes = ref.watch(hiveBoxesProvider);
35
+ return boxes.personas;
36
+});
37
+
38
+final personasProvider = Provider<List<Persona>>((ref) {
39
+ final box = ref.watch(personasBoxProvider);
40
+ return box.values.toList();
41
+});
lib/core/providers/subscription_provider.dart
new
+17
@@ -0,0 +1,17 @@
1
+import 'package:flutter_riverpod/flutter_riverpod.dart';
2
+
3
+class SubscriptionNotifier extends Notifier<bool> {
4
+ @override
5
+ bool build() {
6
+ // Mocked state: false = Basic user, true = Premium user
7
+ return false;
8
+ }
9
+
10
+ void togglePremium() {
11
+ state = !state;
12
+ }
13
+}
14
+
15
+final isPremiumUserProvider = NotifierProvider<SubscriptionNotifier, bool>(() {
16
+ return SubscriptionNotifier();
17
+});
lib/core/routes/app_routes.dart
new
+13
@@ -0,0 +1,13 @@
1
+class AppRoutes {
2
+ static const String home = '/';
3
+ static const String onboarding = '/onboarding';
4
+ static const String onboardingSetup = '/onboarding/setup';
5
+ static const String onboardingTheme = '/onboarding/theme';
6
+ static const String conversations = '/conversations';
7
+ static const String servers = '/servers';
8
+ static const String addServer = '/servers/add';
9
+ static const String personas = '/personas';
10
+ static const String createPersona = '/personas/create';
11
+ static const String settings = '/settings';
12
+ static const String models = '/models';
13
+}
lib/core/storage/hive_initializer.dart
new
+59
@@ -0,0 +1,59 @@
1
+import 'package:hive_ce_flutter/hive_flutter.dart';
2
+import 'package:path_provider/path_provider.dart';
3
+import 'package:localmind/features/servers/data/models/server.dart';
4
+import 'package:localmind/features/chat/data/models/message.dart';
5
+import 'package:localmind/features/conversations/data/models/conversation.dart';
6
+import 'package:localmind/features/personas/data/models/persona.dart';
7
+import 'package:localmind/features/settings/data/models/app_settings.dart';
8
+import 'package:localmind/core/models/enums.dart';
9
+import 'hive_keys.dart';
10
+
11
+class HiveBoxes {
12
+ final Box<Server> servers;
13
+ final Box<Message> messages;
14
+ final Box<Conversation> conversations;
15
+ final Box<Persona> personas;
16
+ final Box<dynamic> settings;
17
+
18
+ const HiveBoxes({
19
+ required this.servers,
20
+ required this.messages,
21
+ required this.conversations,
22
+ required this.personas,
23
+ required this.settings,
24
+ });
25
+}
26
+
27
+class HiveInitializer {
28
+ static Future<HiveBoxes> initialize() async {
29
+ final appDir = await getApplicationDocumentsDirectory();
30
+ await Hive.initFlutter(appDir.path);
31
+
32
+ Hive.registerAdapter(ServerTypeAdapter());
33
+ Hive.registerAdapter(ConnectionStatusAdapter());
34
+ Hive.registerAdapter(MessageRoleAdapter());
35
+ Hive.registerAdapter(MessageStatusAdapter());
36
+ Hive.registerAdapter(ServerAdapter());
37
+ Hive.registerAdapter(MessageAdapter());
38
+ Hive.registerAdapter(ConversationAdapter());
39
+ Hive.registerAdapter(PersonaAdapter());
40
+ Hive.registerAdapter(AppSettingsAdapter());
41
+ Hive.registerAdapter(ThemeModeAdapter());
42
+
43
+ final servers = await Hive.openBox<Server>(HiveKeys.servers);
44
+ final messages = await Hive.openBox<Message>(HiveKeys.messages);
45
+ final conversations = await Hive.openBox<Conversation>(
46
+ HiveKeys.conversations,
47
+ );
48
+ final personas = await Hive.openBox<Persona>(HiveKeys.personas);
49
+ final settings = await Hive.openBox<dynamic>(HiveKeys.settings);
50
+
51
+ return HiveBoxes(
52
+ servers: servers,
53
+ messages: messages,
54
+ conversations: conversations,
55
+ personas: personas,
56
+ settings: settings,
57
+ );
58
+ }
59
+}
lib/core/storage/hive_keys.dart
new
+7
@@ -0,0 +1,7 @@
1
+class HiveKeys {
2
+ static const String servers = 'servers';
3
+ static const String messages = 'messages';
4
+ static const String conversations = 'conversations';
5
+ static const String personas = 'personas';
6
+ static const String settings = 'settings';
7
+}
lib/core/theme/app_theme.dart
new
+384
@@ -0,0 +1,384 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:shadcn_ui/shadcn_ui.dart';
3
+import 'colors.dart';
4
+import 'typography.dart';
5
+
6
+enum AppThemeType {
7
+ system,
8
+ light,
9
+ dark,
10
+ claude
11
+}
12
+
13
+class AppTheme {
14
+ static ShadThemeData get darkShadTheme => ShadThemeData(
15
+ brightness: Brightness.dark,
16
+ colorScheme: const ShadSlateColorScheme.dark(
17
+ background: AppColors.darkBackground,
18
+ primary: AppColors.darkAccent,
19
+ primaryForeground: AppColors.darkBackground,
20
+ secondary: AppColors.darkSurface,
21
+ secondaryForeground: AppColors.darkPrimaryText,
22
+ border: AppColors.darkBorder,
23
+ card: AppColors.darkSurface,
24
+ cardForeground: AppColors.darkPrimaryText,
25
+ muted: AppColors.darkSurface,
26
+ mutedForeground: AppColors.darkMutedText,
27
+ ),
28
+ );
29
+
30
+ static ShadThemeData get lightShadTheme => ShadThemeData(
31
+ brightness: Brightness.light,
32
+ colorScheme: const ShadSlateColorScheme.light(
33
+ background: AppColors.lightBackground,
34
+ primary: AppColors.lightAccent,
35
+ primaryForeground: Colors.white,
36
+ secondary: AppColors.lightSurface,
37
+ secondaryForeground: AppColors.lightPrimaryText,
38
+ border: AppColors.lightBorder,
39
+ card: AppColors.lightSurface,
40
+ cardForeground: AppColors.lightPrimaryText,
41
+ muted: AppColors.lightSurface,
42
+ mutedForeground: AppColors.lightMutedText,
43
+ ),
44
+ );
45
+
46
+ static ShadThemeData get claudeShadTheme => ShadThemeData(
47
+ brightness: Brightness.light,
48
+ colorScheme: const ShadSlateColorScheme.light(
49
+ background: AppColors.claudeBackground,
50
+ primary: AppColors.claudeAccent,
51
+ primaryForeground: Colors.white,
52
+ secondary: AppColors.claudeSurface,
53
+ secondaryForeground: AppColors.claudePrimaryText,
54
+ border: AppColors.claudeBorder,
55
+ card: AppColors.claudeSurface,
56
+ cardForeground: AppColors.claudePrimaryText,
57
+ muted: AppColors.claudeSurface,
58
+ mutedForeground: AppColors.claudeMutedText,
59
+ ),
60
+ );
61
+
62
+ static ThemeData get darkTheme => ThemeData(
63
+ useMaterial3: true,
64
+ brightness: Brightness.dark,
65
+ scaffoldBackgroundColor: AppColors.darkBackground,
66
+ colorScheme: const ColorScheme.dark(
67
+ surface: AppColors.darkSurface,
68
+ primary: AppColors.darkAccent,
69
+ onPrimary: AppColors.darkBackground, // White accent, black text
70
+ secondary: AppColors.darkAccent,
71
+ onSecondary: AppColors.darkBackground,
72
+ error: AppColors.error,
73
+ onSurface: AppColors.darkPrimaryText,
74
+ outline: AppColors.darkBorder,
75
+ ),
76
+ textTheme: AppTypography.darkTextTheme,
77
+ appBarTheme: const AppBarTheme(
78
+ backgroundColor: AppColors.darkBackground,
79
+ foregroundColor: AppColors.darkPrimaryText,
80
+ elevation: 0,
81
+ centerTitle: false,
82
+ ),
83
+ cardTheme: CardThemeData(
84
+ color: AppColors.darkSurface,
85
+ elevation: 0,
86
+ shape: RoundedRectangleBorder(
87
+ borderRadius: BorderRadius.circular(8),
88
+ side: const BorderSide(color: AppColors.darkBorder),
89
+ ),
90
+ ),
91
+ dividerTheme: const DividerThemeData(
92
+ color: AppColors.darkBorder,
93
+ thickness: 1,
94
+ ),
95
+ inputDecorationTheme: InputDecorationTheme(
96
+ filled: true,
97
+ fillColor: AppColors.darkSurface,
98
+ border: OutlineInputBorder(
99
+ borderRadius: BorderRadius.circular(8),
100
+ borderSide: const BorderSide(color: AppColors.darkBorder),
101
+ ),
102
+ enabledBorder: OutlineInputBorder(
103
+ borderRadius: BorderRadius.circular(8),
104
+ borderSide: const BorderSide(color: AppColors.darkBorder),
105
+ ),
106
+ focusedBorder: OutlineInputBorder(
107
+ borderRadius: BorderRadius.circular(8),
108
+ borderSide: const BorderSide(color: AppColors.darkAccent, width: 2),
109
+ ),
110
+ contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
111
+ ),
112
+ elevatedButtonTheme: ElevatedButtonThemeData(
113
+ style: ElevatedButton.styleFrom(
114
+ backgroundColor: AppColors.darkAccent,
115
+ foregroundColor: AppColors.darkBackground,
116
+ shape: RoundedRectangleBorder(
117
+ borderRadius: BorderRadius.circular(8),
118
+ ),
119
+ padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
120
+ ),
121
+ ),
122
+ outlinedButtonTheme: OutlinedButtonThemeData(
123
+ style: OutlinedButton.styleFrom(
124
+ foregroundColor: AppColors.darkPrimaryText,
125
+ side: const BorderSide(color: AppColors.darkBorder),
126
+ shape: RoundedRectangleBorder(
127
+ borderRadius: BorderRadius.circular(8),
128
+ ),
129
+ padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
130
+ ),
131
+ ),
132
+ textButtonTheme: TextButtonThemeData(
133
+ style: TextButton.styleFrom(
134
+ foregroundColor: AppColors.darkPrimaryText,
135
+ shape: RoundedRectangleBorder(
136
+ borderRadius: BorderRadius.circular(8),
137
+ ),
138
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
139
+ ),
140
+ ),
141
+ bottomSheetTheme: const BottomSheetThemeData(
142
+ backgroundColor: AppColors.darkSurface,
143
+ shape: RoundedRectangleBorder(
144
+ borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
145
+ ),
146
+ ),
147
+ dialogTheme: DialogThemeData(
148
+ backgroundColor: AppColors.darkSurface,
149
+ shape: RoundedRectangleBorder(
150
+ borderRadius: BorderRadius.circular(12),
151
+ ),
152
+ ),
153
+ snackBarTheme: SnackBarThemeData(
154
+ backgroundColor: AppColors.darkSurface,
155
+ contentTextStyle: AppTypography.darkTextTheme.bodyMedium,
156
+ shape: RoundedRectangleBorder(
157
+ borderRadius: BorderRadius.circular(8),
158
+ ),
159
+ behavior: SnackBarBehavior.floating,
160
+ ),
161
+ listTileTheme: const ListTileThemeData(
162
+ contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
163
+ ),
164
+ navigationBarTheme: NavigationBarThemeData(
165
+ backgroundColor: AppColors.darkBackground,
166
+ indicatorColor: AppColors.darkSurface,
167
+ labelTextStyle: WidgetStateProperty.resolveWith((states) {
168
+ if (states.contains(WidgetState.selected)) {
169
+ return const TextStyle(color: AppColors.darkPrimaryText, fontSize: 12, fontWeight: FontWeight.w600);
170
+ }
171
+ return const TextStyle(color: AppColors.darkMutedText, fontSize: 12, fontWeight: FontWeight.w500);
172
+ }),
173
+ iconTheme: WidgetStateProperty.resolveWith((states) {
174
+ if (states.contains(WidgetState.selected)) {
175
+ return const IconThemeData(color: AppColors.darkPrimaryText);
176
+ }
177
+ return const IconThemeData(color: AppColors.darkMutedText);
178
+ }),
179
+ ),
180
+ );
181
+
182
+ static ThemeData get lightTheme => ThemeData(
183
+ useMaterial3: true,
184
+ brightness: Brightness.light,
185
+ scaffoldBackgroundColor: AppColors.lightBackground,
186
+ colorScheme: const ColorScheme.light(
187
+ surface: AppColors.lightSurface,
188
+ primary: AppColors.lightAccent,
189
+ onPrimary: Colors.white,
190
+ secondary: AppColors.lightAccent,
191
+ onSecondary: Colors.white,
192
+ error: AppColors.error,
193
+ onSurface: AppColors.lightPrimaryText,
194
+ outline: AppColors.lightBorder,
195
+ ),
196
+ textTheme: AppTypography.lightTextTheme,
197
+ appBarTheme: const AppBarTheme(
198
+ backgroundColor: AppColors.lightBackground,
199
+ foregroundColor: AppColors.lightPrimaryText,
200
+ elevation: 0,
201
+ centerTitle: false,
202
+ ),
203
+ cardTheme: CardThemeData(
204
+ color: AppColors.lightSurface,
205
+ elevation: 0,
206
+ shape: RoundedRectangleBorder(
207
+ borderRadius: BorderRadius.circular(8),
208
+ side: const BorderSide(color: AppColors.lightBorder),
209
+ ),
210
+ ),
211
+ dividerTheme: const DividerThemeData(
212
+ color: AppColors.lightBorder,
213
+ thickness: 1,
214
+ ),
215
+ inputDecorationTheme: InputDecorationTheme(
216
+ filled: true,
217
+ fillColor: AppColors.lightSurface,
218
+ border: OutlineInputBorder(
219
+ borderRadius: BorderRadius.circular(8),
220
+ borderSide: const BorderSide(color: AppColors.lightBorder),
221
+ ),
222
+ enabledBorder: OutlineInputBorder(
223
+ borderRadius: BorderRadius.circular(8),
224
+ borderSide: const BorderSide(color: AppColors.lightBorder),
225
+ ),
226
+ focusedBorder: OutlineInputBorder(
227
+ borderRadius: BorderRadius.circular(8),
228
+ borderSide: const BorderSide(color: AppColors.lightAccent, width: 2),
229
+ ),
230
+ contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
231
+ ),
232
+ elevatedButtonTheme: ElevatedButtonThemeData(
233
+ style: ElevatedButton.styleFrom(
234
+ backgroundColor: AppColors.lightAccent,
235
+ foregroundColor: Colors.white,
236
+ shape: RoundedRectangleBorder(
237
+ borderRadius: BorderRadius.circular(8),
238
+ ),
239
+ padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
240
+ ),
241
+ ),
242
+ outlinedButtonTheme: OutlinedButtonThemeData(
243
+ style: OutlinedButton.styleFrom(
244
+ foregroundColor: AppColors.lightPrimaryText,
245
+ side: const BorderSide(color: AppColors.lightBorder),
246
+ shape: RoundedRectangleBorder(
247
+ borderRadius: BorderRadius.circular(8),
248
+ ),
249
+ padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
250
+ ),
251
+ ),
252
+ bottomSheetTheme: const BottomSheetThemeData(
253
+ backgroundColor: AppColors.lightSurface,
254
+ shape: RoundedRectangleBorder(
255
+ borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
256
+ ),
257
+ ),
258
+ dialogTheme: DialogThemeData(
259
+ backgroundColor: AppColors.lightSurface,
260
+ shape: RoundedRectangleBorder(
261
+ borderRadius: BorderRadius.circular(12),
262
+ ),
263
+ ),
264
+ navigationBarTheme: NavigationBarThemeData(
265
+ backgroundColor: AppColors.lightBackground,
266
+ indicatorColor: AppColors.lightSurface,
267
+ labelTextStyle: WidgetStateProperty.resolveWith((states) {
268
+ if (states.contains(WidgetState.selected)) {
269
+ return const TextStyle(color: AppColors.lightPrimaryText, fontSize: 12, fontWeight: FontWeight.w600);
270
+ }
271
+ return const TextStyle(color: AppColors.lightMutedText, fontSize: 12, fontWeight: FontWeight.w500);
272
+ }),
273
+ iconTheme: WidgetStateProperty.resolveWith((states) {
274
+ if (states.contains(WidgetState.selected)) {
275
+ return const IconThemeData(color: AppColors.lightPrimaryText);
276
+ }
277
+ return const IconThemeData(color: AppColors.lightMutedText);
278
+ }),
279
+ ),
280
+ );
281
+
282
+ static ThemeData get claudeTheme => ThemeData(
283
+ useMaterial3: true,
284
+ brightness: Brightness.light,
285
+ scaffoldBackgroundColor: AppColors.claudeBackground,
286
+ colorScheme: const ColorScheme.light(
287
+ surface: AppColors.claudeSurface,
288
+ primary: AppColors.claudeAccent,
289
+ onPrimary: Colors.white,
290
+ secondary: AppColors.claudeAccent,
291
+ onSecondary: Colors.white,
292
+ error: AppColors.error,
293
+ onSurface: AppColors.claudePrimaryText,
294
+ outline: AppColors.claudeBorder,
295
+ ),
296
+ textTheme: AppTypography.lightTextTheme.apply(
297
+ bodyColor: AppColors.claudePrimaryText,
298
+ displayColor: AppColors.claudePrimaryText,
299
+ ),
300
+ appBarTheme: const AppBarTheme(
301
+ backgroundColor: AppColors.claudeBackground,
302
+ foregroundColor: AppColors.claudePrimaryText,
303
+ elevation: 0,
304
+ centerTitle: false,
305
+ ),
306
+ cardTheme: CardThemeData(
307
+ color: AppColors.claudeSurface,
308
+ elevation: 0,
309
+ shape: RoundedRectangleBorder(
310
+ borderRadius: BorderRadius.circular(8),
311
+ side: const BorderSide(color: AppColors.claudeBorder),
312
+ ),
313
+ ),
314
+ dividerTheme: const DividerThemeData(
315
+ color: AppColors.claudeBorder,
316
+ thickness: 1,
317
+ ),
318
+ inputDecorationTheme: InputDecorationTheme(
319
+ filled: true,
320
+ fillColor: AppColors.claudeSurface,
321
+ border: OutlineInputBorder(
322
+ borderRadius: BorderRadius.circular(8),
323
+ borderSide: const BorderSide(color: AppColors.claudeBorder),
324
+ ),
325
+ enabledBorder: OutlineInputBorder(
326
+ borderRadius: BorderRadius.circular(8),
327
+ borderSide: const BorderSide(color: AppColors.claudeBorder),
328
+ ),
329
+ focusedBorder: OutlineInputBorder(
330
+ borderRadius: BorderRadius.circular(8),
331
+ borderSide: const BorderSide(color: AppColors.claudeAccent, width: 2),
332
+ ),
333
+ contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
334
+ ),
335
+ elevatedButtonTheme: ElevatedButtonThemeData(
336
+ style: ElevatedButton.styleFrom(
337
+ backgroundColor: AppColors.claudeAccent,
338
+ foregroundColor: Colors.white,
339
+ shape: RoundedRectangleBorder(
340
+ borderRadius: BorderRadius.circular(8),
341
+ ),
342
+ padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
343
+ ),
344
+ ),
345
+ outlinedButtonTheme: OutlinedButtonThemeData(
346
+ style: OutlinedButton.styleFrom(
347
+ foregroundColor: AppColors.claudePrimaryText,
348
+ side: const BorderSide(color: AppColors.claudeBorder),
349
+ shape: RoundedRectangleBorder(
350
+ borderRadius: BorderRadius.circular(8),
351
+ ),
352
+ padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
353
+ ),
354
+ ),
355
+ bottomSheetTheme: const BottomSheetThemeData(
356
+ backgroundColor: AppColors.claudeSurface,
357
+ shape: RoundedRectangleBorder(
358
+ borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
359
+ ),
360
+ ),
361
+ dialogTheme: DialogThemeData(
362
+ backgroundColor: AppColors.claudeSurface,
363
+ shape: RoundedRectangleBorder(
364
+ borderRadius: BorderRadius.circular(12),
365
+ ),
366
+ ),
367
+ navigationBarTheme: NavigationBarThemeData(
368
+ backgroundColor: AppColors.claudeBackground,
369
+ indicatorColor: AppColors.claudeSurface,
370
+ labelTextStyle: WidgetStateProperty.resolveWith((states) {
371
+ if (states.contains(WidgetState.selected)) {
372
+ return const TextStyle(color: AppColors.claudePrimaryText, fontSize: 12, fontWeight: FontWeight.w600);
373
+ }
374
+ return const TextStyle(color: AppColors.claudeMutedText, fontSize: 12, fontWeight: FontWeight.w500);
375
+ }),
376
+ iconTheme: WidgetStateProperty.resolveWith((states) {
377
+ if (states.contains(WidgetState.selected)) {
378
+ return const IconThemeData(color: AppColors.claudePrimaryText);
379
+ }
380
+ return const IconThemeData(color: AppColors.claudeMutedText);
381
+ }),
382
+ ),
383
+ );
384
+}
lib/core/theme/colors.dart
new
+31
@@ -0,0 +1,31 @@
1
+import 'package:flutter/material.dart';
2
+
3
+class AppColors {
4
+ // Dark Theme (Obsidian)
5
+ static const Color darkBackground = Color(0xFF0A0A0A);
6
+ static const Color darkSurface = Color(0xFF171717);
7
+ static const Color darkBorder = Color(0xFF262626);
8
+ static const Color darkPrimaryText = Color(0xFFFAFAFA);
9
+ static const Color darkMutedText = Color(0xFFA1A1AA);
10
+ static const Color darkAccent = Color(0xFFFAFAFA); // White accent for monochromatic look
11
+
12
+ // Light Theme
13
+ static const Color lightBackground = Color(0xFFFAFAFA);
14
+ static const Color lightSurface = Color(0xFFFFFFFF);
15
+ static const Color lightBorder = Color(0xFFE5E5E5);
16
+ static const Color lightPrimaryText = Color(0xFF0A0A0A);
17
+ static const Color lightMutedText = Color(0xFF71717A);
18
+ static const Color lightAccent = Color(0xFF0A0A0A);
19
+
20
+ // Claude Theme
21
+ static const Color claudeBackground = Color(0xFFF6F4EB);
22
+ static const Color claudeSurface = Color(0xFFFFFFFF);
23
+ static const Color claudeBorder = Color(0xFFE5D5C5);
24
+ static const Color claudePrimaryText = Color(0xFF1F1E1D);
25
+ static const Color claudeMutedText = Color(0xFF8F8A80);
26
+ static const Color claudeAccent = Color(0xFFDA7048);
27
+
28
+ static const Color success = Color(0xFF22C55E);
29
+ static const Color error = Color(0xFFEF4444);
30
+ static const Color warning = Color(0xFFF59E0B);
31
+}
lib/core/theme/typography.dart
new
+193
@@ -0,0 +1,193 @@
1
+import 'package:flutter/material.dart';
2
+import 'colors.dart';
3
+
4
+class AppTypography {
5
+ static const String fontFamily = 'Inter';
6
+ static const String codeFontFamily = 'FiraCode';
7
+
8
+ static TextTheme get darkTextTheme => const TextTheme(
9
+ displayLarge: TextStyle(
10
+ fontFamily: fontFamily,
11
+ fontSize: 57,
12
+ fontWeight: FontWeight.w400,
13
+ color: AppColors.darkPrimaryText,
14
+ ),
15
+ displayMedium: TextStyle(
16
+ fontFamily: fontFamily,
17
+ fontSize: 45,
18
+ fontWeight: FontWeight.w400,
19
+ color: AppColors.darkPrimaryText,
20
+ ),
21
+ displaySmall: TextStyle(
22
+ fontFamily: fontFamily,
23
+ fontSize: 36,
24
+ fontWeight: FontWeight.w400,
25
+ color: AppColors.darkPrimaryText,
26
+ ),
27
+ headlineLarge: TextStyle(
28
+ fontFamily: fontFamily,
29
+ fontSize: 32,
30
+ fontWeight: FontWeight.w600,
31
+ color: AppColors.darkPrimaryText,
32
+ ),
33
+ headlineMedium: TextStyle(
34
+ fontFamily: fontFamily,
35
+ fontSize: 28,
36
+ fontWeight: FontWeight.w600,
37
+ color: AppColors.darkPrimaryText,
38
+ ),
39
+ headlineSmall: TextStyle(
40
+ fontFamily: fontFamily,
41
+ fontSize: 24,
42
+ fontWeight: FontWeight.w600,
43
+ color: AppColors.darkPrimaryText,
44
+ ),
45
+ titleLarge: TextStyle(
46
+ fontFamily: fontFamily,
47
+ fontSize: 22,
48
+ fontWeight: FontWeight.w500,
49
+ color: AppColors.darkPrimaryText,
50
+ ),
51
+ titleMedium: TextStyle(
52
+ fontFamily: fontFamily,
53
+ fontSize: 16,
54
+ fontWeight: FontWeight.w500,
55
+ color: AppColors.darkPrimaryText,
56
+ ),
57
+ titleSmall: TextStyle(
58
+ fontFamily: fontFamily,
59
+ fontSize: 14,
60
+ fontWeight: FontWeight.w500,
61
+ color: AppColors.darkPrimaryText,
62
+ ),
63
+ bodyLarge: TextStyle(
64
+ fontFamily: fontFamily,
65
+ fontSize: 16,
66
+ fontWeight: FontWeight.w400,
67
+ color: AppColors.darkPrimaryText,
68
+ ),
69
+ bodyMedium: TextStyle(
70
+ fontFamily: fontFamily,
71
+ fontSize: 14,
72
+ fontWeight: FontWeight.w400,
73
+ color: AppColors.darkPrimaryText,
74
+ ),
75
+ bodySmall: TextStyle(
76
+ fontFamily: fontFamily,
77
+ fontSize: 12,
78
+ fontWeight: FontWeight.w400,
79
+ color: AppColors.darkMutedText,
80
+ ),
81
+ labelLarge: TextStyle(
82
+ fontFamily: fontFamily,
83
+ fontSize: 14,
84
+ fontWeight: FontWeight.w500,
85
+ color: AppColors.darkPrimaryText,
86
+ ),
87
+ labelMedium: TextStyle(
88
+ fontFamily: fontFamily,
89
+ fontSize: 12,
90
+ fontWeight: FontWeight.w500,
91
+ color: AppColors.darkPrimaryText,
92
+ ),
93
+ labelSmall: TextStyle(
94
+ fontFamily: fontFamily,
95
+ fontSize: 11,
96
+ fontWeight: FontWeight.w500,
97
+ color: AppColors.darkMutedText,
98
+ ),
99
+ );
100
+
101
+ static TextTheme get lightTextTheme => const TextTheme(
102
+ displayLarge: TextStyle(
103
+ fontFamily: fontFamily,
104
+ fontSize: 57,
105
+ fontWeight: FontWeight.w400,
106
+ color: AppColors.lightPrimaryText,
107
+ ),
108
+ displayMedium: TextStyle(
109
+ fontFamily: fontFamily,
110
+ fontSize: 45,
111
+ fontWeight: FontWeight.w400,
112
+ color: AppColors.lightPrimaryText,
113
+ ),
114
+ displaySmall: TextStyle(
115
+ fontFamily: fontFamily,
116
+ fontSize: 36,
117
+ fontWeight: FontWeight.w400,
118
+ color: AppColors.lightPrimaryText,
119
+ ),
120
+ headlineLarge: TextStyle(
121
+ fontFamily: fontFamily,
122
+ fontSize: 32,
123
+ fontWeight: FontWeight.w600,
124
+ color: AppColors.lightPrimaryText,
125
+ ),
126
+ headlineMedium: TextStyle(
127
+ fontFamily: fontFamily,
128
+ fontSize: 28,
129
+ fontWeight: FontWeight.w600,
130
+ color: AppColors.lightPrimaryText,
131
+ ),
132
+ headlineSmall: TextStyle(
133
+ fontFamily: fontFamily,
134
+ fontSize: 24,
135
+ fontWeight: FontWeight.w600,
136
+ color: AppColors.lightPrimaryText,
137
+ ),
138
+ titleLarge: TextStyle(
139
+ fontFamily: fontFamily,
140
+ fontSize: 22,
141
+ fontWeight: FontWeight.w500,
142
+ color: AppColors.lightPrimaryText,
143
+ ),
144
+ titleMedium: TextStyle(
145
+ fontFamily: fontFamily,
146
+ fontSize: 16,
147
+ fontWeight: FontWeight.w500,
148
+ color: AppColors.lightPrimaryText,
149
+ ),
150
+ titleSmall: TextStyle(
151
+ fontFamily: fontFamily,
152
+ fontSize: 14,
153
+ fontWeight: FontWeight.w500,
154
+ color: AppColors.lightPrimaryText,
155
+ ),
156
+ bodyLarge: TextStyle(
157
+ fontFamily: fontFamily,
158
+ fontSize: 16,
159
+ fontWeight: FontWeight.w400,
160
+ color: AppColors.lightPrimaryText,
161
+ ),
162
+ bodyMedium: TextStyle(
163
+ fontFamily: fontFamily,
164
+ fontSize: 14,
165
+ fontWeight: FontWeight.w400,
166
+ color: AppColors.lightPrimaryText,
167
+ ),
168
+ bodySmall: TextStyle(
169
+ fontFamily: fontFamily,
170
+ fontSize: 12,
171
+ fontWeight: FontWeight.w400,
172
+ color: AppColors.lightMutedText,
173
+ ),
174
+ labelLarge: TextStyle(
175
+ fontFamily: fontFamily,
176
+ fontSize: 14,
177
+ fontWeight: FontWeight.w500,
178
+ color: AppColors.lightPrimaryText,
179
+ ),
180
+ labelMedium: TextStyle(
181
+ fontFamily: fontFamily,
182
+ fontSize: 12,
183
+ fontWeight: FontWeight.w500,
184
+ color: AppColors.lightPrimaryText,
185
+ ),
186
+ labelSmall: TextStyle(
187
+ fontFamily: fontFamily,
188
+ fontSize: 11,
189
+ fontWeight: FontWeight.w500,
190
+ color: AppColors.lightMutedText,
191
+ ),
192
+ );
193
+}
lib/features/chat/data/chat_service.dart
new
+256
@@ -0,0 +1,256 @@
1
+import 'dart:async';
2
+import 'dart:convert';
3
+import 'package:dio/dio.dart';
4
+import 'package:localmind/features/servers/data/models/server.dart';
5
+import 'package:localmind/features/chat/data/models/message.dart';
6
+import 'package:localmind/features/chat/data/models/chat_parameters.dart';
7
+import 'package:localmind/core/models/enums.dart';
8
+
9
+abstract class ChatService {
10
+ Stream<String> sendMessage({
11
+ required Server server,
12
+ required String modelId,
13
+ required List<Message> messages,
14
+ required ChatParameters params,
15
+ });
16
+
17
+ void cancelStream();
18
+
19
+ static ChatService forServer(ServerType type, Dio dio) {
20
+ switch (type) {
21
+ case ServerType.lmStudio:
22
+ return LMStudioChatService(dio);
23
+ case ServerType.ollama:
24
+ return OllamaChatService(dio);
25
+ case ServerType.openRouter:
26
+ return OpenRouterChatService(dio);
27
+ }
28
+ }
29
+}
30
+
31
+class LMStudioChatService implements ChatService {
32
+ final Dio _dio;
33
+ CancelToken? _cancelToken;
34
+
35
+ LMStudioChatService(this._dio);
36
+
37
+ @override
38
+ Stream<String> sendMessage({
39
+ required Server server,
40
+ required String modelId,
41
+ required List<Message> messages,
42
+ required ChatParameters params,
43
+ }) async* {
44
+ _cancelToken = CancelToken();
45
+
46
+ final body = {
47
+ 'model': modelId,
48
+ 'messages': messages
49
+ .map((m) => {'role': _roleToString(m.role), 'content': m.content})
50
+ .toList(),
51
+ 'temperature': params.temperature,
52
+ 'top_p': params.topP,
53
+ 'max_tokens': params.maxTokens,
54
+ 'stream': true,
55
+ };
56
+
57
+ final response = await _dio.post<ResponseBody>(
58
+ server.chatEndpoint,
59
+ data: body,
60
+ options: Options(
61
+ responseType: ResponseType.stream,
62
+ headers: {
63
+ 'Content-Type': 'application/json',
64
+ if (server.apiKey != null) 'Authorization': 'Bearer ${server.apiKey}',
65
+ },
66
+ ),
67
+ cancelToken: _cancelToken,
68
+ );
69
+
70
+ final stream = response.data!.stream;
71
+ String buffer = '';
72
+
73
+ await for (final chunk in stream) {
74
+ buffer += String.fromCharCodes(chunk);
75
+ final lines = buffer.split('\n');
76
+ buffer = lines.removeLast();
77
+
78
+ for (final line in lines) {
79
+ if (line.startsWith('data: ')) {
80
+ final data = line.substring(6);
81
+ if (data == '[DONE]') {
82
+ return;
83
+ }
84
+ try {
85
+ final json = jsonDecode(data) as Map<String, dynamic>;
86
+ final content = json['choices']?[0]?['delta']?['content'];
87
+ if (content != null && content is String) {
88
+ yield content;
89
+ }
90
+ } catch (e) {
91
+ // Skip malformed JSON
92
+ }
93
+ }
94
+ }
95
+ }
96
+ }
97
+
98
+ @override
99
+ void cancelStream() {
100
+ _cancelToken?.cancel('User cancelled');
101
+ }
102
+}
103
+
104
+class OllamaChatService implements ChatService {
105
+ final Dio _dio;
106
+ CancelToken? _cancelToken;
107
+
108
+ OllamaChatService(this._dio);
109
+
110
+ @override
111
+ Stream<String> sendMessage({
112
+ required Server server,
113
+ required String modelId,
114
+ required List<Message> messages,
115
+ required ChatParameters params,
116
+ }) async* {
117
+ _cancelToken = CancelToken();
118
+
119
+ final body = {
120
+ 'model': modelId,
121
+ 'messages': messages
122
+ .map((m) => {'role': _roleToString(m.role), 'content': m.content})
123
+ .toList(),
124
+ 'stream': true,
125
+ 'options': {
126
+ 'temperature': params.temperature,
127
+ 'top_p': params.topP,
128
+ 'num_predict': params.maxTokens,
129
+ },
130
+ };
131
+
132
+ final response = await _dio.post<ResponseBody>(
133
+ server.chatEndpoint,
134
+ data: body,
135
+ options: Options(responseType: ResponseType.stream),
136
+ cancelToken: _cancelToken,
137
+ );
138
+
139
+ final stream = response.data!.stream;
140
+ String buffer = '';
141
+
142
+ await for (final chunk in stream) {
143
+ buffer += String.fromCharCodes(chunk);
144
+ final lines = buffer.split('\n');
145
+ buffer = lines.removeLast();
146
+
147
+ for (final line in lines) {
148
+ if (line.isNotEmpty) {
149
+ try {
150
+ final json = jsonDecode(line) as Map<String, dynamic>;
151
+ final content = json['message']?['content'];
152
+ if (content != null && content is String) {
153
+ yield content;
154
+ }
155
+ if (json['done'] == true) {
156
+ return;
157
+ }
158
+ } catch (e) {
159
+ // Skip malformed JSON
160
+ }
161
+ }
162
+ }
163
+ }
164
+ }
165
+
166
+ @override
167
+ void cancelStream() {
168
+ _cancelToken?.cancel('User cancelled');
169
+ }
170
+}
171
+
172
+class OpenRouterChatService implements ChatService {
173
+ final Dio _dio;
174
+ CancelToken? _cancelToken;
175
+
176
+ OpenRouterChatService(this._dio);
177
+
178
+ @override
179
+ Stream<String> sendMessage({
180
+ required Server server,
181
+ required String modelId,
182
+ required List<Message> messages,
183
+ required ChatParameters params,
184
+ }) async* {
185
+ _cancelToken = CancelToken();
186
+
187
+ final body = {
188
+ 'model': modelId,
189
+ 'messages': messages
190
+ .map((m) => {'role': _roleToString(m.role), 'content': m.content})
191
+ .toList(),
192
+ 'temperature': params.temperature,
193
+ 'top_p': params.topP,
194
+ 'max_tokens': params.maxTokens,
195
+ 'stream': true,
196
+ };
197
+
198
+ final response = await _dio.post<ResponseBody>(
199
+ server.chatEndpoint,
200
+ data: body,
201
+ options: Options(
202
+ responseType: ResponseType.stream,
203
+ headers: {
204
+ 'Content-Type': 'application/json',
205
+ 'Authorization': 'Bearer ${server.apiKey}',
206
+ 'HTTP-Referer': 'https://localmind.app',
207
+ 'X-Title': 'LocalMind',
208
+ },
209
+ ),
210
+ cancelToken: _cancelToken,
211
+ );
212
+
213
+ final stream = response.data!.stream;
214
+ String buffer = '';
215
+
216
+ await for (final chunk in stream) {
217
+ buffer += String.fromCharCodes(chunk);
218
+ final lines = buffer.split('\n');
219
+ buffer = lines.removeLast();
220
+
221
+ for (final line in lines) {
222
+ if (line.startsWith('data: ')) {
223
+ final data = line.substring(6);
224
+ if (data == '[DONE]') {
225
+ return;
226
+ }
227
+ try {
228
+ final json = jsonDecode(data) as Map<String, dynamic>;
229
+ final content = json['choices']?[0]?['delta']?['content'];
230
+ if (content != null && content is String) {
231
+ yield content;
232
+ }
233
+ } catch (e) {
234
+ // Skip malformed JSON
235
+ }
236
+ }
237
+ }
238
+ }
239
+ }
240
+
241
+ @override
242
+ void cancelStream() {
243
+ _cancelToken?.cancel('User cancelled');
244
+ }
245
+}
246
+
247
+String _roleToString(MessageRole role) {
248
+ switch (role) {
249
+ case MessageRole.user:
250
+ return 'user';
251
+ case MessageRole.assistant:
252
+ return 'assistant';
253
+ case MessageRole.system:
254
+ return 'system';
255
+ }
256
+}
lib/features/chat/data/models/chat_parameters.dart
new
+36
@@ -0,0 +1,36 @@
1
+import 'package:localmind/core/constants/app_constants.dart';
2
+
3
+class ChatParameters {
4
+ final double temperature;
5
+ final double topP;
6
+ final int maxTokens;
7
+ final int contextLength;
8
+
9
+ const ChatParameters({
10
+ required this.temperature,
11
+ required this.topP,
12
+ required this.maxTokens,
13
+ required this.contextLength,
14
+ });
15
+
16
+ factory ChatParameters.defaults() => const ChatParameters(
17
+ temperature: AppConstants.defaultTemperature,
18
+ topP: AppConstants.defaultTopP,
19
+ maxTokens: AppConstants.defaultMaxTokens,
20
+ contextLength: AppConstants.defaultContextLength,
21
+ );
22
+
23
+ ChatParameters copyWith({
24
+ double? temperature,
25
+ double? topP,
26
+ int? maxTokens,
27
+ int? contextLength,
28
+ }) {
29
+ return ChatParameters(
30
+ temperature: temperature ?? this.temperature,
31
+ topP: topP ?? this.topP,
32
+ maxTokens: maxTokens ?? this.maxTokens,
33
+ contextLength: contextLength ?? this.contextLength,
34
+ );
35
+ }
36
+}
lib/features/chat/data/models/message.dart
new
+82
@@ -0,0 +1,82 @@
1
+import 'package:hive_ce/hive.dart';
2
+import 'package:localmind/core/models/enums.dart';
3
+
4
+part 'message.g.dart';
5
+
6
+@HiveType(typeId: 1)
7
+class Message extends HiveObject {
8
+ @HiveField(0)
9
+ final String id;
10
+
11
+ @HiveField(1)
12
+ final String conversationId;
13
+
14
+ @HiveField(2)
15
+ final MessageRole role;
16
+
17
+ @HiveField(3)
18
+ final String content;
19
+
20
+ @HiveField(4)
21
+ final DateTime createdAt;
22
+
23
+ @HiveField(5)
24
+ final MessageStatus status;
25
+
26
+ @HiveField(6)
27
+ final String? modelId;
28
+
29
+ @HiveField(7)
30
+ final int? tokenCount;
31
+
32
+ @HiveField(8)
33
+ final String? errorMessage;
34
+
35
+ @HiveField(9)
36
+ final List<String>? attachmentPaths;
37
+
38
+ @HiveField(10)
39
+ final int? generationTimeMs;
40
+
41
+ Message({
42
+ required this.id,
43
+ required this.conversationId,
44
+ required this.role,
45
+ required this.content,
46
+ required this.createdAt,
47
+ this.status = MessageStatus.complete,
48
+ this.modelId,
49
+ this.tokenCount,
50
+ this.errorMessage,
51
+ this.attachmentPaths,
52
+ this.generationTimeMs,
53
+ });
54
+
55
+ Message copyWith({
56
+ String? id,
57
+ String? conversationId,
58
+ MessageRole? role,
59
+ String? content,
60
+ DateTime? createdAt,
61
+ MessageStatus? status,
62
+ String? modelId,
63
+ int? tokenCount,
64
+ String? errorMessage,
65
+ List<String>? attachmentPaths,
66
+ int? generationTimeMs,
67
+ }) {
68
+ return Message(
69
+ id: id ?? this.id,
70
+ conversationId: conversationId ?? this.conversationId,
71
+ role: role ?? this.role,
72
+ content: content ?? this.content,
73
+ createdAt: createdAt ?? this.createdAt,
74
+ status: status ?? this.status,
75
+ modelId: modelId ?? this.modelId,
76
+ tokenCount: tokenCount ?? this.tokenCount,
77
+ errorMessage: errorMessage ?? this.errorMessage,
78
+ attachmentPaths: attachmentPaths ?? this.attachmentPaths,
79
+ generationTimeMs: generationTimeMs ?? this.generationTimeMs,
80
+ );
81
+ }
82
+}
lib/features/chat/data/models/message.g.dart
new
+73
@@ -0,0 +1,73 @@
1
+// GENERATED CODE - DO NOT MODIFY BY HAND
2
+
3
+part of 'message.dart';
4
+
5
+// **************************************************************************
6
+// TypeAdapterGenerator
7
+// **************************************************************************
8
+
9
+class MessageAdapter extends TypeAdapter<Message> {
10
+ @override
11
+ final typeId = 1;
12
+
13
+ @override
14
+ Message read(BinaryReader reader) {
15
+ final numOfFields = reader.readByte();
16
+ final fields = <int, dynamic>{
17
+ for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
18
+ };
19
+ return Message(
20
+ id: fields[0] as String,
21
+ conversationId: fields[1] as String,
22
+ role: fields[2] as MessageRole,
23
+ content: fields[3] as String,
24
+ createdAt: fields[4] as DateTime,
25
+ status: fields[5] == null
26
+ ? MessageStatus.complete
27
+ : fields[5] as MessageStatus,
28
+ modelId: fields[6] as String?,
29
+ tokenCount: (fields[7] as num?)?.toInt(),
30
+ errorMessage: fields[8] as String?,
31
+ attachmentPaths: (fields[9] as List?)?.cast<String>(),
32
+ generationTimeMs: (fields[10] as num?)?.toInt(),
33
+ );
34
+ }
35
+
36
+ @override
37
+ void write(BinaryWriter writer, Message obj) {
38
+ writer
39
+ ..writeByte(11)
40
+ ..writeByte(0)
41
+ ..write(obj.id)
42
+ ..writeByte(1)
43
+ ..write(obj.conversationId)
44
+ ..writeByte(2)
45
+ ..write(obj.role)
46
+ ..writeByte(3)
47
+ ..write(obj.content)
48
+ ..writeByte(4)
49
+ ..write(obj.createdAt)
50
+ ..writeByte(5)
51
+ ..write(obj.status)
52
+ ..writeByte(6)
53
+ ..write(obj.modelId)
54
+ ..writeByte(7)
55
+ ..write(obj.tokenCount)
56
+ ..writeByte(8)
57
+ ..write(obj.errorMessage)
58
+ ..writeByte(9)
59
+ ..write(obj.attachmentPaths)
60
+ ..writeByte(10)
61
+ ..write(obj.generationTimeMs);
62
+ }
63
+
64
+ @override
65
+ int get hashCode => typeId.hashCode;
66
+
67
+ @override
68
+ bool operator ==(Object other) =>
69
+ identical(this, other) ||
70
+ other is MessageAdapter &&
71
+ runtimeType == other.runtimeType &&
72
+ typeId == other.typeId;
73
+}
lib/features/chat/providers/chat_providers.dart
new
+395
@@ -0,0 +1,395 @@
1
+import 'dart:async';
2
+import 'dart:math';
3
+import 'package:flutter_riverpod/flutter_riverpod.dart';
4
+import 'package:localmind/core/models/enums.dart';
5
+import 'package:localmind/core/providers/service_providers.dart';
6
+import 'package:localmind/core/providers/storage_providers.dart';
7
+import 'package:localmind/features/chat/data/chat_service.dart';
8
+import 'package:localmind/features/chat/data/models/chat_parameters.dart';
9
+import 'package:localmind/features/chat/data/models/message.dart';
10
+import 'package:localmind/features/conversations/data/models/conversation.dart';
11
+import 'package:localmind/features/conversations/providers/conversation_providers.dart'
12
+ as conv;
13
+import 'package:localmind/features/models/data/models/model_info.dart';
14
+import 'package:localmind/features/servers/providers/server_providers.dart';
15
+import 'package:localmind/core/providers/app_providers.dart';
16
+
17
+final selectedModelProvider =
18
+ NotifierProvider<SelectedModelNotifier, ModelInfo?>(() {
19
+ return SelectedModelNotifier();
20
+ });
21
+
22
+class SelectedModelNotifier extends Notifier<ModelInfo?> {
23
+ @override
24
+ ModelInfo? build() => null;
25
+
26
+ void setModel(ModelInfo? model) {
27
+ state = model;
28
+ }
29
+}
30
+
31
+final isStreamingProvider = NotifierProvider<IsStreamingNotifier, bool>(() {
32
+ return IsStreamingNotifier();
33
+});
34
+
35
+class IsStreamingNotifier extends Notifier<bool> {
36
+ @override
37
+ bool build() => false;
38
+
39
+ void setStreaming(bool streaming) {
40
+ state = streaming;
41
+ }
42
+}
43
+
44
+final chatParamsProvider = Provider<ChatParameters>((ref) {
45
+ final settings = ref.watch(settingsProvider);
46
+ return ChatParameters(
47
+ temperature: settings.temperature,
48
+ topP: settings.topP,
49
+ maxTokens: settings.maxTokens,
50
+ contextLength: settings.contextLength,
51
+ );
52
+});
53
+
54
+final chatServiceProvider = Provider<ChatService>((ref) {
55
+ final server = ref.watch(activeServerProvider);
56
+ if (server == null) {
57
+ throw StateError('No active server');
58
+ }
59
+ return ChatService.forServer(server.type, ref.read(dioProvider));
60
+});
61
+
62
+class ChatState {
63
+ final List<Message> messages;
64
+ final bool isStreaming;
65
+ final String? errorMessage;
66
+ final Message? streamingMessage;
67
+
68
+ const ChatState({
69
+ this.messages = const [],
70
+ this.isStreaming = false,
71
+ this.errorMessage,
72
+ this.streamingMessage,
73
+ });
74
+
75
+ ChatState copyWith({
76
+ List<Message>? messages,
77
+ bool? isStreaming,
78
+ String? errorMessage,
79
+ Message? streamingMessage,
80
+ bool clearError = false,
81
+ bool clearStreaming = false,
82
+ }) {
83
+ return ChatState(
84
+ messages: messages ?? this.messages,
85
+ isStreaming: isStreaming ?? this.isStreaming,
86
+ errorMessage: clearError ? null : (errorMessage ?? this.errorMessage),
87
+ streamingMessage: clearStreaming
88
+ ? null
89
+ : (streamingMessage ?? this.streamingMessage),
90
+ );
91
+ }
92
+}
93
+
94
+final chatProvider = NotifierProvider<ChatNotifier, ChatState>(() {
95
+ return ChatNotifier();
96
+});
97
+
98
+class ChatNotifier extends Notifier<ChatState> {
99
+ StreamSubscription<String>? _streamSubscription;
100
+ String? _currentConversationId;
101
+
102
+ @override
103
+ ChatState build() {
104
+ ref.onDispose(() {
105
+ _streamSubscription?.cancel();
106
+ });
107
+ return const ChatState();
108
+ }
109
+
110
+ Future<void> loadConversation(Conversation conversation) async {
111
+ _currentConversationId = conversation.id;
112
+ final boxes = ref.read(hiveBoxesProvider);
113
+ final messages = boxes.messages.values
114
+ .where((m) => m.conversationId == conversation.id)
115
+ .toList();
116
+ messages.sort((a, b) => a.createdAt.compareTo(b.createdAt));
117
+
118
+ state = ChatState(messages: messages);
119
+ ref
120
+ .read(conv.activeConversationProvider.notifier)
121
+ .setActiveConversation(conversation);
122
+ }
123
+
124
+ Future<void> startNewConversation() async {
125
+ await clearConversation();
126
+ ref
127
+ .read(conv.activeConversationProvider.notifier)
128
+ .setActiveConversation(null);
129
+ }
130
+
131
+ String generateUuid() {
132
+ final random = Random.secure();
133
+ final bytes = List<int>.generate(16, (_) => random.nextInt(256));
134
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
135
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
136
+ return [
137
+ bytes.sublist(0, 4),
138
+ bytes.sublist(4, 6),
139
+ bytes.sublist(6, 8),
140
+ bytes.sublist(8, 10),
141
+ bytes.sublist(10, 16),
142
+ ]
143
+ .map((b) => b.map((e) => e.toRadixString(16).padLeft(2, '0')).join())
144
+ .join('-');
145
+ }
146
+
147
+ Future<void> sendMessage(String content) async {
148
+ final server = ref.read(activeServerProvider);
149
+ final selectedModel = ref.read(selectedModelProvider);
150
+ final chatParams = ref.read(chatParamsProvider);
151
+ final chatService = ref.read(chatServiceProvider);
152
+ final boxes = ref.read(hiveBoxesProvider);
153
+
154
+ if (server == null) {
155
+ state = state.copyWith(errorMessage: 'No server connected');
156
+ return;
157
+ }
158
+
159
+ if (content.trim().isEmpty) return;
160
+
161
+ if (_currentConversationId == null) {
162
+ final server = ref.read(activeServerProvider);
163
+ final selectedModel = ref.read(selectedModelProvider);
164
+ final conversation = await ref
165
+ .read(conv.conversationsProvider.notifier)
166
+ .createConversation(
167
+ title: content.length > 50
168
+ ? '${content.substring(0, 50)}...'
169
+ : content,
170
+ serverId: server?.id,
171
+ modelId: selectedModel?.id,
172
+ );
173
+ _currentConversationId = conversation.id;
174
+ ref
175
+ .read(conv.activeConversationProvider.notifier)
176
+ .setActiveConversation(conversation);
177
+ }
178
+
179
+ final userMessage = Message(
180
+ id: generateUuid(),
181
+ conversationId: _currentConversationId!,
182
+ role: MessageRole.user,
183
+ content: content,
184
+ createdAt: DateTime.now(),
185
+ status: MessageStatus.complete,
186
+ );
187
+
188
+ final assistantMessageId = generateUuid();
189
+ var assistantMessage = Message(
190
+ id: assistantMessageId,
191
+ conversationId: _currentConversationId!,
192
+ role: MessageRole.assistant,
193
+ content: '',
194
+ createdAt: DateTime.now(),
195
+ status: MessageStatus.streaming,
196
+ modelId: selectedModel?.id,
197
+ );
198
+
199
+ state = state.copyWith(
200
+ messages: [...state.messages, userMessage, assistantMessage],
201
+ isStreaming: true,
202
+ streamingMessage: assistantMessage,
203
+ clearError: true,
204
+ );
205
+
206
+ await boxes.messages.put(userMessage.id, userMessage);
207
+ await boxes.messages.put(assistantMessage.id, assistantMessage);
208
+
209
+ final messagesForApi = _buildMessagesForApi(selectedModel);
210
+
211
+ try {
212
+ _streamSubscription?.cancel();
213
+ _streamSubscription = chatService
214
+ .sendMessage(
215
+ server: server,
216
+ modelId: selectedModel?.id ?? 'default',
217
+ messages: messagesForApi,
218
+ params: chatParams,
219
+ )
220
+ .listen(
221
+ (chunk) {
222
+ final currentContent = state.streamingMessage?.content ?? '';
223
+ final updatedMessage =
224
+ (state.streamingMessage ?? assistantMessage).copyWith(
225
+ content: currentContent + chunk,
226
+ );
227
+ state = state.copyWith(streamingMessage: updatedMessage);
228
+ final messageIndex = state.messages.indexWhere(
229
+ (m) => m.id == assistantMessageId,
230
+ );
231
+ if (messageIndex != -1) {
232
+ final updatedMessages = List<Message>.from(state.messages);
233
+ updatedMessages[messageIndex] = updatedMessage;
234
+ state = state.copyWith(messages: updatedMessages);
235
+ }
236
+ },
237
+ onDone: () async {
238
+ final finalMessage = state.streamingMessage?.copyWith(
239
+ status: MessageStatus.complete,
240
+ );
241
+ if (finalMessage != null) {
242
+ await boxes.messages.put(finalMessage.id, finalMessage);
243
+ final messageIndex = state.messages.indexWhere(
244
+ (m) => m.id == assistantMessageId,
245
+ );
246
+ if (messageIndex != -1) {
247
+ final updatedMessages = List<Message>.from(state.messages);
248
+ updatedMessages[messageIndex] = finalMessage;
249
+ state = state.copyWith(
250
+ messages: updatedMessages,
251
+ isStreaming: false,
252
+ clearStreaming: true,
253
+ );
254
+ }
255
+ if (_currentConversationId != null) {
256
+ final preview = finalMessage.content.length > 100
257
+ ? '${finalMessage.content.substring(0, 100)}...'
258
+ : finalMessage.content;
259
+ await ref
260
+ .read(conv.conversationsProvider.notifier)
261
+ .updatePreview(
262
+ _currentConversationId!,
263
+ preview,
264
+ DateTime.now(),
265
+ );
266
+ }
267
+ }
268
+ },
269
+ onError: (error) async {
270
+ final errorMessage = state.streamingMessage?.copyWith(
271
+ status: MessageStatus.error,
272
+ errorMessage: error.toString(),
273
+ );
274
+ if (errorMessage != null) {
275
+ await boxes.messages.put(errorMessage.id, errorMessage);
276
+ final messageIndex = state.messages.indexWhere(
277
+ (m) => m.id == assistantMessageId,
278
+ );
279
+ if (messageIndex != -1) {
280
+ final updatedMessages = List<Message>.from(state.messages);
281
+ updatedMessages[messageIndex] = errorMessage;
282
+ state = state.copyWith(
283
+ messages: updatedMessages,
284
+ isStreaming: false,
285
+ errorMessage: error.toString(),
286
+ clearStreaming: true,
287
+ );
288
+ }
289
+ }
290
+ },
291
+ );
292
+ } catch (e) {
293
+ state = state.copyWith(
294
+ isStreaming: false,
295
+ errorMessage: e.toString(),
296
+ clearStreaming: true,
297
+ );
298
+ }
299
+ }
300
+
301
+ List<Message> _buildMessagesForApi(ModelInfo? selectedModel) {
302
+ final settings = ref.read(settingsProvider);
303
+ final messages = <Message>[];
304
+
305
+ if (settings.showSystemMessages && state.messages.isEmpty) {
306
+ // Add default system prompt if enabled
307
+ }
308
+
309
+ for (final message in state.messages) {
310
+ if (message.role != MessageRole.system || settings.showSystemMessages) {
311
+ messages.add(message);
312
+ }
313
+ }
314
+
315
+ final contextLength = ref.read(chatParamsProvider).contextLength;
316
+ return _truncateToContextWindow(messages, contextLength);
317
+ }
318
+
319
+ List<Message> _truncateToContextWindow(
320
+ List<Message> messages,
321
+ int contextLength,
322
+ ) {
323
+ if (messages.isEmpty) return messages;
324
+
325
+ int estimatedTokens = 0;
326
+ final result = <Message>[];
327
+ const tokensPerChar = 4;
328
+
329
+ for (int i = messages.length - 1; i >= 0; i--) {
330
+ final message = messages[i];
331
+ estimatedTokens += (message.content.length / tokensPerChar).ceil();
332
+
333
+ if (estimatedTokens > contextLength) {
334
+ break;
335
+ }
336
+ result.insert(0, message);
337
+ }
338
+
339
+ return result;
340
+ }
341
+
342
+ void cancelStream() {
343
+ _streamSubscription?.cancel();
344
+ _streamSubscription = null;
345
+ state = state.copyWith(isStreaming: false, clearStreaming: true);
346
+ }
347
+
348
+ Future<void> retryLastMessage() async {
349
+ final messages = state.messages;
350
+ if (messages.isEmpty) return;
351
+
352
+ if (messages.last.role == MessageRole.assistant) {
353
+ final lastAssistantIndex = messages.lastIndexWhere(
354
+ (m) => m.role == MessageRole.assistant,
355
+ );
356
+ if (lastAssistantIndex > 0) {
357
+ final userMessage = messages[lastAssistantIndex - 1];
358
+ final messagesToRemove = messages.sublist(lastAssistantIndex);
359
+ final boxes = ref.read(hiveBoxesProvider);
360
+
361
+ for (final msg in messagesToRemove) {
362
+ await boxes.messages.delete(msg.id);
363
+ }
364
+
365
+ state = state.copyWith(
366
+ messages: messages.sublist(0, lastAssistantIndex),
367
+ clearStreaming: true,
368
+ );
369
+
370
+ await sendMessage(userMessage.content);
371
+ }
372
+ }
373
+ }
374
+
375
+ Future<void> deleteMessage(String messageId) async {
376
+ final boxes = ref.read(hiveBoxesProvider);
377
+ await boxes.messages.delete(messageId);
378
+ state = state.copyWith(
379
+ messages: state.messages.where((m) => m.id != messageId).toList(),
380
+ );
381
+ }
382
+
383
+ Future<void> clearConversation() async {
384
+ final boxes = ref.read(hiveBoxesProvider);
385
+ for (final message in state.messages) {
386
+ await boxes.messages.delete(message.id);
387
+ }
388
+ _currentConversationId = null;
389
+ state = const ChatState();
390
+ }
391
+
392
+ void clearError() {
393
+ state = state.copyWith(clearError: true);
394
+ }
395
+}
lib/features/chat/screens/chat_screen.dart
new
+572
@@ -0,0 +1,572 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+import 'package:go_router/go_router.dart';
4
+import 'package:hugeicons/hugeicons.dart';
5
+import 'package:localmind/core/models/enums.dart';
6
+import 'package:localmind/core/providers/storage_providers.dart';
7
+import 'package:localmind/core/routes/app_routes.dart';
8
+import 'package:localmind/features/chat/data/models/message.dart';
9
+import 'package:localmind/features/chat/providers/chat_providers.dart';
10
+import 'package:localmind/features/chat/views/components/chat_bubble.dart';
11
+import 'package:localmind/features/chat/views/components/chat_input_bar.dart';
12
+import 'package:localmind/features/conversations/data/models/conversation.dart';
13
+import 'package:localmind/features/models/screens/model_picker_sheet.dart';
14
+import 'package:localmind/features/servers/providers/server_providers.dart';
15
+
16
+class ChatScreen extends ConsumerStatefulWidget {
17
+ const ChatScreen({super.key});
18
+
19
+ @override
20
+ ConsumerState<ChatScreen> createState() => _ChatScreenState();
21
+}
22
+
23
+class _ChatScreenState extends ConsumerState<ChatScreen> {
24
+ final ScrollController _scrollController = ScrollController();
25
+ bool _showScrollToBottom = false;
26
+
27
+ static const List<String> _quickPrompts = [
28
+ 'Help me write a function',
29
+ 'Explain this code',
30
+ 'Debug this for me',
31
+ 'How do I use async/await?',
32
+ ];
33
+
34
+ @override
35
+ void initState() {
36
+ super.initState();
37
+ _scrollController.addListener(_onScroll);
38
+ }
39
+
40
+ @override
41
+ void dispose() {
42
+ _scrollController.dispose();
43
+ super.dispose();
44
+ }
45
+
46
+ void _onScroll() {
47
+ final isNearBottom =
48
+ _scrollController.position.pixels >=
49
+ _scrollController.position.maxScrollExtent - 100;
50
+ if (_showScrollToBottom != !isNearBottom) {
51
+ setState(() => _showScrollToBottom = !isNearBottom);
52
+ }
53
+ }
54
+
55
+ void _scrollToBottom() {
56
+ _scrollController.animateTo(
57
+ _scrollController.position.maxScrollExtent,
58
+ duration: const Duration(milliseconds: 300),
59
+ curve: Curves.easeOut,
60
+ );
61
+ }
62
+
63
+ @override
64
+ Widget build(BuildContext context) {
65
+ final chatState = ref.watch(chatProvider);
66
+ final selectedModel = ref.watch(selectedModelProvider);
67
+ final connectionStatus = ref.watch(connectionStatusProvider);
68
+ final theme = Theme.of(context);
69
+ final isDark = theme.brightness == Brightness.dark;
70
+
71
+ ref.listen(chatProvider, (previous, next) {
72
+ if (previous?.messages.length != next.messages.length) {
73
+ WidgetsBinding.instance.addPostFrameCallback((_) {
74
+ if (_scrollController.hasClients) {
75
+ _scrollToBottom();
76
+ }
77
+ });
78
+ }
79
+ });
80
+
81
+ return Column(
82
+ children: [
83
+ AppBar(
84
+ leading: Builder(
85
+ builder: (context) => IconButton(
86
+ icon: const Icon(Icons.menu),
87
+ onPressed: () => Scaffold.of(context).openDrawer(),
88
+ ),
89
+ ),
90
+ title: GestureDetector(
91
+ onTap: () => _showModelPicker(context),
92
+ child: Row(
93
+ mainAxisSize: MainAxisSize.min,
94
+ children: [
95
+ Text(
96
+ selectedModel?.displayName ?? 'Select Model',
97
+ style: const TextStyle(fontSize: 16),
98
+ ),
99
+ const SizedBox(width: 4),
100
+ Icon(
101
+ Icons.expand_more,
102
+ size: 16,
103
+ color: isDark
104
+ ? const Color(0xFF888888)
105
+ : const Color(0xFF666666),
106
+ ),
107
+ ],
108
+ ),
109
+ ),
110
+ actions: [
111
+ IconButton(
112
+ icon: const Icon(Icons.settings_outlined),
113
+ onPressed: () => context.push(AppRoutes.settings),
114
+ tooltip: 'Settings',
115
+ ),
116
+ PopupMenuButton<String>(
117
+ icon: const Icon(Icons.more_vert),
118
+ onSelected: (value) => _handleMenuAction(value, context),
119
+ itemBuilder: (context) => [
120
+ const PopupMenuItem(
121
+ value: 'new_chat',
122
+ child: ListTile(
123
+ leading: Icon(Icons.add),
124
+ title: Text('New Chat'),
125
+ contentPadding: EdgeInsets.zero,
126
+ ),
127
+ ),
128
+ const PopupMenuItem(
129
+ value: 'clear',
130
+ child: ListTile(
131
+ leading: Icon(Icons.delete_outline),
132
+ title: Text('Clear Conversation'),
133
+ contentPadding: EdgeInsets.zero,
134
+ ),
135
+ ),
136
+ ],
137
+ ),
138
+ ],
139
+ ),
140
+ if (connectionStatus == ConnectionStatus.disconnected ||
141
+ connectionStatus == ConnectionStatus.error)
142
+ _ConnectionBanner(status: connectionStatus),
143
+ Expanded(
144
+ child: chatState.messages.isEmpty
145
+ ? _EmptyState(
146
+ onQuickPrompt: (prompt) =>
147
+ ref.read(chatProvider.notifier).sendMessage(prompt),
148
+ quickPrompts: _quickPrompts,
149
+ recentConversations: ref.watch(recentConversationsProvider),
150
+ onSeeAll: () => context.go(AppRoutes.conversations),
151
+ )
152
+ : _MessageList(
153
+ scrollController: _scrollController,
154
+ messages: chatState.messages,
155
+ streamingMessage: chatState.streamingMessage,
156
+ isStreaming: chatState.isStreaming,
157
+ onRetry: (messageId) {
158
+ ref.read(chatProvider.notifier).retryLastMessage();
159
+ },
160
+ onDelete: (messageId) {
161
+ ref.read(chatProvider.notifier).deleteMessage(messageId);
162
+ },
163
+ ),
164
+ ),
165
+ if (_showScrollToBottom && chatState.messages.isNotEmpty)
166
+ Positioned(
167
+ bottom: 100,
168
+ left: 0,
169
+ right: 0,
170
+ child: Center(
171
+ child: GestureDetector(
172
+ onTap: _scrollToBottom,
173
+ child: Container(
174
+ padding: const EdgeInsets.symmetric(
175
+ horizontal: 12,
176
+ vertical: 6,
177
+ ),
178
+ decoration: BoxDecoration(
179
+ color: isDark
180
+ ? const Color(0xFF3A3A3A)
181
+ : const Color(0xFFE5E5E5),
182
+ borderRadius: BorderRadius.circular(16),
183
+ ),
184
+ child: Row(
185
+ mainAxisSize: MainAxisSize.min,
186
+ children: [
187
+ Icon(
188
+ Icons.arrow_downward,
189
+ size: 14,
190
+ color: isDark
191
+ ? const Color(0xFF888888)
192
+ : const Color(0xFF666666),
193
+ ),
194
+ const SizedBox(width: 4),
195
+ Text(
196
+ 'New messages',
197
+ style: TextStyle(
198
+ fontSize: 12,
199
+ color: isDark
200
+ ? const Color(0xFF888888)
201
+ : const Color(0xFF666666),
202
+ ),
203
+ ),
204
+ ],
205
+ ),
206
+ ),
207
+ ),
208
+ ),
209
+ ),
210
+ ChatInputBar(
211
+ onSend: (message) {
212
+ ref.read(chatProvider.notifier).sendMessage(message);
213
+ },
214
+ onStop: () {
215
+ ref.read(chatProvider.notifier).cancelStream();
216
+ },
217
+ ),
218
+ ],
219
+ );
220
+ }
221
+
222
+ void _showModelPicker(BuildContext context) {
223
+ showModalBottomSheet(
224
+ context: context,
225
+ isScrollControlled: true,
226
+ backgroundColor: Colors.transparent,
227
+ builder: (context) => const ModelPickerSheet(),
228
+ );
229
+ }
230
+
231
+ void _handleMenuAction(String action, BuildContext context) {
232
+ switch (action) {
233
+ case 'new_chat':
234
+ ref.read(chatProvider.notifier).startNewConversation();
235
+ break;
236
+ case 'clear':
237
+ showDialog(
238
+ context: context,
239
+ builder: (context) => AlertDialog(
240
+ title: const Text('Clear conversation?'),
241
+ content: const Text(
242
+ 'This will delete all messages in this conversation.',
243
+ ),
244
+ actions: [
245
+ TextButton(
246
+ onPressed: () => Navigator.pop(context),
247
+ child: const Text('Cancel'),
248
+ ),
249
+ TextButton(
250
+ onPressed: () {
251
+ Navigator.pop(context);
252
+ ref.read(chatProvider.notifier).clearConversation();
253
+ },
254
+ child: const Text('Clear'),
255
+ ),
256
+ ],
257
+ ),
258
+ );
259
+ break;
260
+ }
261
+ }
262
+}
263
+
264
+class _ConnectionBanner extends StatelessWidget {
265
+ const _ConnectionBanner({required this.status});
266
+
267
+ final ConnectionStatus status;
268
+
269
+ @override
270
+ Widget build(BuildContext context) {
271
+ final isError = status == ConnectionStatus.error;
272
+
273
+ return Container(
274
+ width: double.infinity,
275
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
276
+ color: isError
277
+ ? Colors.red.withValues(alpha: 0.1)
278
+ : Colors.orange.withValues(alpha: 0.1),
279
+ child: Row(
280
+ children: [
281
+ Icon(
282
+ isError ? Icons.error_outline : Icons.wifi_off,
283
+ size: 16,
284
+ color: isError ? Colors.red : Colors.orange,
285
+ ),
286
+ const SizedBox(width: 8),
287
+ Expanded(
288
+ child: Text(
289
+ isError
290
+ ? 'Connection error. Check your server.'
291
+ : 'Disconnected from server.',
292
+ style: TextStyle(
293
+ fontSize: 13,
294
+ color: isError ? Colors.red : Colors.orange[700],
295
+ ),
296
+ ),
297
+ ),
298
+ TextButton(
299
+ onPressed: () {
300
+ context.push(AppRoutes.servers);
301
+ },
302
+ child: Text(
303
+ 'Configure',
304
+ style: TextStyle(
305
+ fontSize: 13,
306
+ color: isError ? Colors.red : Colors.orange[700],
307
+ ),
308
+ ),
309
+ ),
310
+ ],
311
+ ),
312
+ );
313
+ }
314
+}
315
+
316
+class _EmptyState extends StatelessWidget {
317
+ const _EmptyState({
318
+ required this.onQuickPrompt,
319
+ required this.quickPrompts,
320
+ required this.recentConversations,
321
+ required this.onSeeAll,
322
+ });
323
+
324
+ final void Function(String) onQuickPrompt;
325
+ final List<String> quickPrompts;
326
+ final List<Conversation> recentConversations;
327
+ final VoidCallback onSeeAll;
328
+
329
+ @override
330
+ Widget build(BuildContext context) {
331
+ final theme = Theme.of(context);
332
+ final isDark = theme.brightness == Brightness.dark;
333
+
334
+ return Center(
335
+ child: Padding(
336
+ padding: const EdgeInsets.all(32),
337
+ child: Column(
338
+ mainAxisAlignment: MainAxisAlignment.center,
339
+ children: [
340
+ HugeIcon(
341
+ icon: HugeIcons.strokeRoundedChatBot,
342
+ size: 64,
343
+ color: isDark ? const Color(0xFF3B82F6) : const Color(0xFF2563EB),
344
+ ),
345
+ const SizedBox(height: 24),
346
+ Text(
347
+ 'LocalMind',
348
+ style: TextStyle(
349
+ fontSize: 28,
350
+ fontWeight: FontWeight.bold,
351
+ color: isDark ? Colors.white : Colors.black,
352
+ ),
353
+ ),
354
+ const SizedBox(height: 8),
355
+ Text(
356
+ 'Your AI. Your Device. Your Rules.',
357
+ style: TextStyle(
358
+ fontSize: 14,
359
+ color: isDark
360
+ ? const Color(0xFF888888)
361
+ : const Color(0xFF666666),
362
+ ),
363
+ ),
364
+ const SizedBox(height: 32),
365
+ Text(
366
+ 'Start a conversation',
367
+ style: TextStyle(
368
+ fontSize: 16,
369
+ fontWeight: FontWeight.w500,
370
+ color: isDark ? Colors.white : Colors.black,
371
+ ),
372
+ ),
373
+ const SizedBox(height: 16),
374
+ Wrap(
375
+ spacing: 8,
376
+ runSpacing: 8,
377
+ alignment: WrapAlignment.center,
378
+ children: quickPrompts.map((prompt) {
379
+ return ActionChip(
380
+ label: Text(prompt),
381
+ onPressed: () => onQuickPrompt(prompt),
382
+ backgroundColor: isDark
383
+ ? const Color(0xFF2A2A2A)
384
+ : const Color(0xFFF5F5F5),
385
+ side: BorderSide(
386
+ color: isDark
387
+ ? const Color(0xFF3A3A3A)
388
+ : const Color(0xFFE5E5E5),
389
+ ),
390
+ );
391
+ }).toList(),
392
+ ),
393
+ if (recentConversations.isNotEmpty) ...[
394
+ const SizedBox(height: 32),
395
+ Row(
396
+ mainAxisAlignment: MainAxisAlignment.center,
397
+ children: [
398
+ Text(
399
+ 'Recent chats',
400
+ style: TextStyle(
401
+ fontSize: 14,
402
+ fontWeight: FontWeight.w500,
403
+ color: isDark
404
+ ? const Color(0xFFA0A0A0)
405
+ : const Color(0xFF666666),
406
+ ),
407
+ ),
408
+ const SizedBox(width: 8),
409
+ GestureDetector(
410
+ onTap: onSeeAll,
411
+ child: Text(
412
+ 'See all',
413
+ style: TextStyle(
414
+ fontSize: 14,
415
+ fontWeight: FontWeight.w500,
416
+ color: isDark
417
+ ? const Color(0xFF3B82F6)
418
+ : const Color(0xFF2563EB),
419
+ ),
420
+ ),
421
+ ),
422
+ ],
423
+ ),
424
+ const SizedBox(height: 12),
425
+ ...recentConversations
426
+ .take(5)
427
+ .map(
428
+ (conv) => Padding(
429
+ padding: const EdgeInsets.only(bottom: 8),
430
+ child: _RecentConversationItem(conversation: conv),
431
+ ),
432
+ ),
433
+ ],
434
+ ],
435
+ ),
436
+ ),
437
+ );
438
+ }
439
+}
440
+
441
+class _RecentConversationItem extends ConsumerWidget {
442
+ const _RecentConversationItem({required this.conversation});
443
+
444
+ final Conversation conversation;
445
+
446
+ @override
447
+ Widget build(BuildContext context, WidgetRef ref) {
448
+ final theme = Theme.of(context);
449
+ final isDark = theme.brightness == Brightness.dark;
450
+
451
+ return GestureDetector(
452
+ onTap: () {
453
+ ref.read(chatProvider.notifier).loadConversation(conversation);
454
+ },
455
+ child: Container(
456
+ width: 300,
457
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
458
+ decoration: BoxDecoration(
459
+ color: isDark ? const Color(0xFF1A1A1A) : const Color(0xFFF5F5F5),
460
+ borderRadius: BorderRadius.circular(8),
461
+ border: Border.all(
462
+ color: isDark ? const Color(0xFF2A2A2A) : const Color(0xFFE5E5E5),
463
+ ),
464
+ ),
465
+ child: Row(
466
+ children: [
467
+ Icon(
468
+ Icons.chat_bubble_outline,
469
+ size: 18,
470
+ color: isDark ? const Color(0xFF888888) : const Color(0xFF666666),
471
+ ),
472
+ const SizedBox(width: 12),
473
+ Expanded(
474
+ child: Column(
475
+ crossAxisAlignment: CrossAxisAlignment.start,
476
+ children: [
477
+ Text(
478
+ conversation.title,
479
+ style: TextStyle(
480
+ fontSize: 14,
481
+ fontWeight: FontWeight.w500,
482
+ color: isDark ? Colors.white : Colors.black,
483
+ ),
484
+ maxLines: 1,
485
+ overflow: TextOverflow.ellipsis,
486
+ ),
487
+ if (conversation.lastMessagePreview != null)
488
+ Text(
489
+ conversation.lastMessagePreview!,
490
+ style: TextStyle(
491
+ fontSize: 12,
492
+ color: isDark
493
+ ? const Color(0xFF888888)
494
+ : const Color(0xFF666666),
495
+ ),
496
+ maxLines: 1,
497
+ overflow: TextOverflow.ellipsis,
498
+ ),
499
+ ],
500
+ ),
501
+ ),
502
+ Icon(
503
+ Icons.chevron_right,
504
+ size: 18,
505
+ color: isDark ? const Color(0xFF666666) : const Color(0xFF999999),
506
+ ),
507
+ ],
508
+ ),
509
+ ),
510
+ );
511
+ }
512
+}
513
+
514
+class _MessageList extends StatelessWidget {
515
+ const _MessageList({
516
+ required this.scrollController,
517
+ required this.messages,
518
+ required this.streamingMessage,
519
+ required this.isStreaming,
520
+ required this.onRetry,
521
+ required this.onDelete,
522
+ });
523
+
524
+ final ScrollController scrollController;
525
+ final List<Message> messages;
526
+ final Message? streamingMessage;
527
+ final bool isStreaming;
528
+ final void Function(String) onRetry;
529
+ final void Function(String) onDelete;
530
+
531
+ @override
532
+ Widget build(BuildContext context) {
533
+ final allMessages = <Message>[];
534
+
535
+ for (final message in messages) {
536
+ if (streamingMessage != null &&
537
+ message.id == streamingMessage!.id &&
538
+ message.content.isEmpty &&
539
+ isStreaming) {
540
+ continue;
541
+ }
542
+ allMessages.add(message);
543
+ }
544
+
545
+ return ListView.builder(
546
+ controller: scrollController,
547
+ padding: const EdgeInsets.symmetric(vertical: 16),
548
+ itemCount:
549
+ allMessages.length +
550
+ (streamingMessage != null && isStreaming ? 1 : 0),
551
+ itemBuilder: (context, index) {
552
+ if (streamingMessage != null &&
553
+ isStreaming &&
554
+ index == allMessages.length) {
555
+ return ChatBubble(message: streamingMessage!, isStreaming: true);
556
+ }
557
+
558
+ final message = allMessages[index];
559
+ final isLast = index == allMessages.length - 1;
560
+
561
+ return ChatBubble(
562
+ key: ValueKey(message.id),
563
+ message: message,
564
+ isStreaming:
565
+ isLast && isStreaming && message.id == streamingMessage?.id,
566
+ onRetry: () => onRetry(message.id),
567
+ onDelete: () => onDelete(message.id),
568
+ );
569
+ },
570
+ );
571
+ }
572
+}
lib/features/chat/views/components/chat_bubble.dart
new
+327
@@ -0,0 +1,327 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
3
+import 'package:localmind/core/models/enums.dart';
4
+import 'package:localmind/features/chat/data/models/message.dart';
5
+import 'package:localmind/features/chat/views/components/code_block.dart';
6
+import 'package:localmind/features/chat/views/components/message_action_bar.dart';
7
+import 'package:localmind/features/chat/views/components/typing_indicator.dart';
8
+
9
+class ChatBubble extends StatelessWidget {
10
+ const ChatBubble({
11
+ super.key,
12
+ required this.message,
13
+ this.onCopy,
14
+ this.onRetry,
15
+ this.onDelete,
16
+ this.isStreaming = false,
17
+ });
18
+
19
+ final Message message;
20
+ final VoidCallback? onCopy;
21
+ final VoidCallback? onRetry;
22
+ final VoidCallback? onDelete;
23
+ final bool isStreaming;
24
+
25
+ @override
26
+ Widget build(BuildContext context) {
27
+ switch (message.role) {
28
+ case MessageRole.user:
29
+ return _UserBubble(message: message);
30
+ case MessageRole.assistant:
31
+ return _AssistantBubble(
32
+ message: message,
33
+ onCopy: onCopy,
34
+ onRetry: onRetry,
35
+ onDelete: onDelete,
36
+ isStreaming: isStreaming,
37
+ );
38
+ case MessageRole.system:
39
+ return _SystemBubble(message: message);
40
+ }
41
+ }
42
+}
43
+
44
+class _UserBubble extends StatelessWidget {
45
+ const _UserBubble({required this.message});
46
+
47
+ final Message message;
48
+
49
+ @override
50
+ Widget build(BuildContext context) {
51
+ final theme = Theme.of(context);
52
+ final isDark = theme.brightness == Brightness.dark;
53
+
54
+ return Align(
55
+ alignment: Alignment.centerRight,
56
+ child: Container(
57
+ constraints: BoxConstraints(
58
+ maxWidth: MediaQuery.of(context).size.width * 0.75,
59
+ ),
60
+ margin: const EdgeInsets.only(left: 48, right: 8, top: 4, bottom: 4),
61
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
62
+ decoration: BoxDecoration(
63
+ color: isDark ? const Color(0xFF3B82F6) : const Color(0xFF2563EB),
64
+ borderRadius: BorderRadius.circular(
65
+ 18,
66
+ ).copyWith(bottomRight: const Radius.circular(4)),
67
+ ),
68
+ child: Column(
69
+ crossAxisAlignment: CrossAxisAlignment.end,
70
+ children: [
71
+ MarkdownBody(
72
+ data: message.content,
73
+ styleSheet: MarkdownStyleSheet(
74
+ p: TextStyle(color: Colors.white, fontSize: 15, height: 1.4),
75
+ ),
76
+ shrinkWrap: true,
77
+ ),
78
+ const SizedBox(height: 4),
79
+ Row(
80
+ mainAxisSize: MainAxisSize.min,
81
+ children: [
82
+ Text(
83
+ _formatTime(message.createdAt),
84
+ style: TextStyle(
85
+ fontSize: 11,
86
+ color: Colors.white.withValues(alpha: 0.7),
87
+ ),
88
+ ),
89
+ if (message.status == MessageStatus.error) ...[
90
+ const SizedBox(width: 4),
91
+ Icon(Icons.error_outline, size: 14, color: Colors.red[200]),
92
+ ],
93
+ ],
94
+ ),
95
+ ],
96
+ ),
97
+ ),
98
+ );
99
+ }
100
+}
101
+
102
+class _AssistantBubble extends StatelessWidget {
103
+ const _AssistantBubble({
104
+ required this.message,
105
+ this.onCopy,
106
+ this.onRetry,
107
+ this.onDelete,
108
+ this.isStreaming = false,
109
+ });
110
+
111
+ final Message message;
112
+ final VoidCallback? onCopy;
113
+ final VoidCallback? onRetry;
114
+ final VoidCallback? onDelete;
115
+ final bool isStreaming;
116
+
117
+ @override
118
+ Widget build(BuildContext context) {
119
+ final theme = Theme.of(context);
120
+ final isDark = theme.brightness == Brightness.dark;
121
+
122
+ return Align(
123
+ alignment: Alignment.centerLeft,
124
+ child: Container(
125
+ constraints: BoxConstraints(
126
+ maxWidth: MediaQuery.of(context).size.width * 0.8,
127
+ ),
128
+ margin: const EdgeInsets.only(left: 8, right: 48, top: 4, bottom: 4),
129
+ padding: const EdgeInsets.all(12),
130
+ decoration: BoxDecoration(
131
+ color: isDark ? const Color(0xFF1F1F1F) : const Color(0xFFF5F5F5),
132
+ borderRadius: BorderRadius.circular(
133
+ 18,
134
+ ).copyWith(bottomLeft: const Radius.circular(4)),
135
+ border: Border.all(
136
+ color: isDark ? const Color(0xFF2A2A2A) : const Color(0xFFE5E5E5),
137
+ ),
138
+ ),
139
+ child: Column(
140
+ crossAxisAlignment: CrossAxisAlignment.start,
141
+ children: [
142
+ if (isStreaming && message.content.isEmpty)
143
+ const TypingIndicator()
144
+ else
145
+ _MarkdownContent(content: message.content, isDark: isDark),
146
+ if (isStreaming && message.content.isNotEmpty)
147
+ const StreamingIndicator(),
148
+ const SizedBox(height: 8),
149
+ Row(
150
+ mainAxisSize: MainAxisSize.min,
151
+ children: [
152
+ Text(
153
+ _formatTime(message.createdAt),
154
+ style: TextStyle(
155
+ fontSize: 11,
156
+ color: isDark
157
+ ? const Color(0xFF666666)
158
+ : const Color(0xFF999999),
159
+ ),
160
+ ),
161
+ if (message.status == MessageStatus.error) ...[
162
+ const SizedBox(width: 4),
163
+ Icon(Icons.error_outline, size: 14, color: Colors.red[400]),
164
+ ],
165
+ ],
166
+ ),
167
+ if (!isStreaming && message.status == MessageStatus.complete) ...[
168
+ const SizedBox(height: 4),
169
+ MessageActionBar(
170
+ content: message.content,
171
+ onCopy: onCopy,
172
+ onRetry: onRetry,
173
+ onDelete: onDelete,
174
+ ),
175
+ ],
176
+ ],
177
+ ),
178
+ ),
179
+ );
180
+ }
181
+}
182
+
183
+class _MarkdownContent extends StatelessWidget {
184
+ const _MarkdownContent({required this.content, required this.isDark});
185
+
186
+ final String content;
187
+ final bool isDark;
188
+
189
+ @override
190
+ Widget build(BuildContext context) {
191
+ return MarkdownBody(
192
+ data: content,
193
+ selectable: true,
194
+ styleSheet: MarkdownStyleSheet(
195
+ p: TextStyle(
196
+ color: isDark ? Colors.white : Colors.black,
197
+ fontSize: 15,
198
+ height: 1.5,
199
+ ),
200
+ h1: TextStyle(
201
+ color: isDark ? Colors.white : Colors.black,
202
+ fontSize: 24,
203
+ fontWeight: FontWeight.bold,
204
+ ),
205
+ h2: TextStyle(
206
+ color: isDark ? Colors.white : Colors.black,
207
+ fontSize: 20,
208
+ fontWeight: FontWeight.bold,
209
+ ),
210
+ h3: TextStyle(
211
+ color: isDark ? Colors.white : Colors.black,
212
+ fontSize: 18,
213
+ fontWeight: FontWeight.w600,
214
+ ),
215
+ h4: TextStyle(
216
+ color: isDark ? Colors.white : Colors.black,
217
+ fontSize: 16,
218
+ fontWeight: FontWeight.w600,
219
+ ),
220
+ code: TextStyle(
221
+ backgroundColor: isDark
222
+ ? const Color(0xFF2D2D2D)
223
+ : const Color(0xFFE8E8E8),
224
+ color: isDark ? const Color(0xFF9CDCFE) : const Color(0xFF001080),
225
+ fontFamily: 'monospace',
226
+ fontSize: 13,
227
+ ),
228
+ codeblockDecoration: BoxDecoration(
229
+ color: isDark ? const Color(0xFF1E1E1E) : const Color(0xFFF5F5F5),
230
+ borderRadius: BorderRadius.circular(8),
231
+ ),
232
+ codeblockPadding: const EdgeInsets.all(12),
233
+ blockquote: TextStyle(
234
+ color: isDark ? const Color(0xFF888888) : const Color(0xFF666666),
235
+ fontStyle: FontStyle.italic,
236
+ ),
237
+ blockquoteDecoration: BoxDecoration(
238
+ border: Border(
239
+ left: BorderSide(
240
+ color: isDark ? const Color(0xFF3A3A3A) : const Color(0xFFE0E0E0),
241
+ width: 3,
242
+ ),
243
+ ),
244
+ ),
245
+ blockquotePadding: const EdgeInsets.only(left: 16),
246
+ listBullet: TextStyle(color: isDark ? Colors.white : Colors.black),
247
+ tableHead: TextStyle(
248
+ color: isDark ? Colors.white : Colors.black,
249
+ fontWeight: FontWeight.bold,
250
+ ),
251
+ tableBody: TextStyle(color: isDark ? Colors.white : Colors.black),
252
+ tableBorder: TableBorder.all(
253
+ color: isDark ? const Color(0xFF3A3A3A) : const Color(0xFFE0E0E0),
254
+ ),
255
+ horizontalRuleDecoration: BoxDecoration(
256
+ border: Border(
257
+ top: BorderSide(
258
+ color: isDark ? const Color(0xFF3A3A3A) : const Color(0xFFE0E0E0),
259
+ ),
260
+ ),
261
+ ),
262
+ a: TextStyle(
263
+ color: isDark ? const Color(0xFF60A5FA) : const Color(0xFF2563EB),
264
+ decoration: TextDecoration.underline,
265
+ ),
266
+ ),
267
+ builders: {'code': CodeBlockBuilder(isDark: isDark)},
268
+ );
269
+ }
270
+}
271
+
272
+class CodeBlockBuilder extends MarkdownElementBuilder {
273
+ final bool isDark;
274
+
275
+ CodeBlockBuilder({required this.isDark});
276
+
277
+ @override
278
+ Widget? visitElementAfter(element, TextStyle? preferredStyle) {
279
+ final code = element.textContent;
280
+ String? language;
281
+
282
+ if (element.attributes.containsKey('class')) {
283
+ final classes = element.attributes['class']!;
284
+ final langMatch = RegExp(r'language-(\w+)').firstMatch(classes);
285
+ if (langMatch != null) {
286
+ language = langMatch.group(1);
287
+ }
288
+ }
289
+
290
+ return CodeBlock(code: code, language: language);
291
+ }
292
+}
293
+
294
+class _SystemBubble extends StatelessWidget {
295
+ const _SystemBubble({required this.message});
296
+
297
+ final Message message;
298
+
299
+ @override
300
+ Widget build(BuildContext context) {
301
+ return Center(
302
+ child: Container(
303
+ margin: const EdgeInsets.symmetric(vertical: 8),
304
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
305
+ decoration: BoxDecoration(
306
+ color: Colors.grey.withValues(alpha: 0.1),
307
+ borderRadius: BorderRadius.circular(8),
308
+ ),
309
+ child: Text(
310
+ message.content,
311
+ style: TextStyle(
312
+ fontSize: 13,
313
+ fontStyle: FontStyle.italic,
314
+ color: Colors.grey[600],
315
+ ),
316
+ textAlign: TextAlign.center,
317
+ ),
318
+ ),
319
+ );
320
+ }
321
+}
322
+
323
+String _formatTime(DateTime dateTime) {
324
+ final hour = dateTime.hour.toString().padLeft(2, '0');
325
+ final minute = dateTime.minute.toString().padLeft(2, '0');
326
+ return '$hour:$minute';
327
+}
lib/features/chat/views/components/chat_input_bar.dart
new
+235
@@ -0,0 +1,235 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+import 'package:haptic_feedback/haptic_feedback.dart';
4
+import 'package:localmind/core/models/enums.dart';
5
+import 'package:localmind/features/servers/providers/server_providers.dart';
6
+
7
+class ChatInputBar extends ConsumerStatefulWidget {
8
+ const ChatInputBar({
9
+ super.key,
10
+ required this.onSend,
11
+ required this.onStop,
12
+ this.enabled = true,
13
+ });
14
+
15
+ final void Function(String message) onSend;
16
+ final VoidCallback onStop;
17
+ final bool enabled;
18
+
19
+ @override
20
+ ConsumerState<ChatInputBar> createState() => _ChatInputBarState();
21
+}
22
+
23
+class _ChatInputBarState extends ConsumerState<ChatInputBar> {
24
+ final _controller = TextEditingController();
25
+ final _focusNode = FocusNode();
26
+ bool _isComposing = false;
27
+
28
+ @override
29
+ void dispose() {
30
+ _controller.dispose();
31
+ _focusNode.dispose();
32
+ super.dispose();
33
+ }
34
+
35
+ void _handleSubmit() {
36
+ final text = _controller.text.trim();
37
+ if (text.isEmpty) return;
38
+
39
+ Haptics.vibrate(HapticsType.medium);
40
+ widget.onSend(text);
41
+ _controller.clear();
42
+ setState(() => _isComposing = false);
43
+ }
44
+
45
+ void _handleStop() {
46
+ Haptics.vibrate(HapticsType.light);
47
+ widget.onStop();
48
+ }
49
+
50
+ @override
51
+ Widget build(BuildContext context) {
52
+ final theme = Theme.of(context);
53
+ final isDark = theme.brightness == Brightness.dark;
54
+ final connectionStatus = ref.watch(connectionStatusProvider);
55
+ final isConnected = connectionStatus == ConnectionStatus.connected;
56
+
57
+ final canSend =
58
+ widget.enabled &&
59
+ isConnected &&
60
+ _controller.text.trim().isNotEmpty &&
61
+ !_isComposing;
62
+
63
+ return Container(
64
+ padding: EdgeInsets.only(
65
+ left: 12,
66
+ right: 12,
67
+ top: 8,
68
+ bottom: MediaQuery.of(context).padding.bottom + 8,
69
+ ),
70
+ decoration: BoxDecoration(
71
+ color: isDark ? const Color(0xFF0A0A0A) : const Color(0xFFFAFAFA),
72
+ border: Border(
73
+ top: BorderSide(
74
+ color: isDark ? const Color(0xFF2A2A2A) : const Color(0xFFE5E5E5),
75
+ ),
76
+ ),
77
+ ),
78
+ child: Column(
79
+ mainAxisSize: MainAxisSize.min,
80
+ children: [
81
+ if (!isConnected)
82
+ Container(
83
+ margin: const EdgeInsets.only(bottom: 8),
84
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
85
+ decoration: BoxDecoration(
86
+ color: Colors.orange.withValues(alpha: 0.1),
87
+ borderRadius: BorderRadius.circular(8),
88
+ border: Border.all(color: Colors.orange.withValues(alpha: 0.3)),
89
+ ),
90
+ child: Row(
91
+ children: [
92
+ const Icon(Icons.wifi_off, size: 16, color: Colors.orange),
93
+ const SizedBox(width: 8),
94
+ Text(
95
+ 'Not connected to server',
96
+ style: TextStyle(fontSize: 13, color: Colors.orange[700]),
97
+ ),
98
+ ],
99
+ ),
100
+ ),
101
+ Row(
102
+ crossAxisAlignment: CrossAxisAlignment.end,
103
+ children: [
104
+ Container(
105
+ decoration: BoxDecoration(
106
+ color: isDark
107
+ ? const Color(0xFF1F1F1F)
108
+ : const Color(0xFFFFFFFF),
109
+ borderRadius: BorderRadius.circular(24),
110
+ border: Border.all(
111
+ color: isDark
112
+ ? const Color(0xFF3A3A3A)
113
+ : const Color(0xFFE5E5E5),
114
+ ),
115
+ ),
116
+ child: IconButton(
117
+ icon: Icon(
118
+ Icons.attach_file,
119
+ size: 20,
120
+ color: isDark
121
+ ? const Color(0xFF888888)
122
+ : const Color(0xFF666666),
123
+ ),
124
+ onPressed: null,
125
+ tooltip: 'Attach file (Coming soon)',
126
+ ),
127
+ ),
128
+ const SizedBox(width: 8),
129
+ Expanded(
130
+ child: Container(
131
+ constraints: const BoxConstraints(maxHeight: 150),
132
+ decoration: BoxDecoration(
133
+ color: isDark
134
+ ? const Color(0xFF1F1F1F)
135
+ : const Color(0xFFFFFFFF),
136
+ borderRadius: BorderRadius.circular(24),
137
+ border: Border.all(
138
+ color: isDark
139
+ ? const Color(0xFF3A3A3A)
140
+ : const Color(0xFFE5E5E5),
141
+ ),
142
+ ),
143
+ child: Row(
144
+ crossAxisAlignment: CrossAxisAlignment.end,
145
+ children: [
146
+ Expanded(
147
+ child: TextField(
148
+ controller: _controller,
149
+ focusNode: _focusNode,
150
+ enabled: widget.enabled && isConnected,
151
+ maxLines: null,
152
+ textInputAction: TextInputAction.newline,
153
+ keyboardType: TextInputType.multiline,
154
+ onChanged: (text) {
155
+ setState(
156
+ () => _isComposing = text.trim().isNotEmpty,
157
+ );
158
+ },
159
+ onSubmitted: (_) => _handleSubmit(),
160
+ decoration: InputDecoration(
161
+ hintText: 'Message LocalMind...',
162
+ hintStyle: TextStyle(
163
+ color: isDark
164
+ ? const Color(0xFF666666)
165
+ : const Color(0xFF999999),
166
+ ),
167
+ border: InputBorder.none,
168
+ contentPadding: const EdgeInsets.symmetric(
169
+ horizontal: 20,
170
+ vertical: 12,
171
+ ),
172
+ ),
173
+ ),
174
+ ),
175
+ if (_isComposing)
176
+ Padding(
177
+ padding: const EdgeInsets.only(right: 4, bottom: 4),
178
+ child: Text(
179
+ '${_controller.text.length}',
180
+ style: TextStyle(
181
+ fontSize: 11,
182
+ color: isDark
183
+ ? const Color(0xFF666666)
184
+ : const Color(0xFF999999),
185
+ ),
186
+ ),
187
+ ),
188
+ ],
189
+ ),
190
+ ),
191
+ ),
192
+ const SizedBox(width: 8),
193
+ _isComposing
194
+ ? Container(
195
+ decoration: BoxDecoration(
196
+ color: Colors.red[400],
197
+ shape: BoxShape.circle,
198
+ ),
199
+ child: IconButton(
200
+ icon: const Icon(Icons.stop, color: Colors.white),
201
+ onPressed: _handleStop,
202
+ tooltip: 'Stop generating',
203
+ ),
204
+ )
205
+ : Container(
206
+ decoration: BoxDecoration(
207
+ color: canSend
208
+ ? (isDark
209
+ ? const Color(0xFF3B82F6)
210
+ : const Color(0xFF2563EB))
211
+ : (isDark
212
+ ? const Color(0xFF3A3A3A)
213
+ : const Color(0xFFE5E5E5)),
214
+ shape: BoxShape.circle,
215
+ ),
216
+ child: IconButton(
217
+ icon: Icon(
218
+ Icons.arrow_upward,
219
+ color: canSend
220
+ ? Colors.white
221
+ : (isDark
222
+ ? const Color(0xFF666666)
223
+ : const Color(0xFF999999)),
224
+ ),
225
+ onPressed: canSend ? _handleSubmit : null,
226
+ tooltip: 'Send message',
227
+ ),
228
+ ),
229
+ ],
230
+ ),
231
+ ],
232
+ ),
233
+ );
234
+ }
235
+}
lib/features/chat/views/components/code_block.dart
new
+132
@@ -0,0 +1,132 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:flutter/services.dart';
3
+
4
+class CodeBlock extends StatefulWidget {
5
+ const CodeBlock({super.key, required this.code, this.language});
6
+
7
+ final String code;
8
+ final String? language;
9
+
10
+ @override
11
+ State<CodeBlock> createState() => _CodeBlockState();
12
+}
13
+
14
+class _CodeBlockState extends State<CodeBlock> {
15
+ bool _copied = false;
16
+
17
+ String _normalizeLanguage(String? lang) {
18
+ if (lang == null || lang.isEmpty) return '';
19
+ return lang.toUpperCase();
20
+ }
21
+
22
+ Future<void> _copyToClipboard() async {
23
+ await Clipboard.setData(ClipboardData(text: widget.code));
24
+ setState(() => _copied = true);
25
+ await Future.delayed(const Duration(seconds: 2));
26
+ if (mounted) {
27
+ setState(() => _copied = false);
28
+ }
29
+ }
30
+
31
+ @override
32
+ Widget build(BuildContext context) {
33
+ final theme = Theme.of(context);
34
+ final isDark = theme.brightness == Brightness.dark;
35
+
36
+ final backgroundColor = isDark
37
+ ? const Color(0xFF1E1E1E)
38
+ : const Color(0xFFF5F5F5);
39
+
40
+ final headerBgColor = isDark
41
+ ? const Color(0xFF2D2D2D)
42
+ : const Color(0xFFE8E8E8);
43
+
44
+ final borderColor = isDark
45
+ ? const Color(0xFF3A3A3A)
46
+ : const Color(0xFFE0E0E0);
47
+
48
+ final textColor = isDark
49
+ ? const Color(0xFF9CDCFE)
50
+ : const Color(0xFF001080);
51
+
52
+ return Container(
53
+ decoration: BoxDecoration(
54
+ color: backgroundColor,
55
+ borderRadius: BorderRadius.circular(8),
56
+ border: Border.all(color: borderColor),
57
+ ),
58
+ child: Column(
59
+ crossAxisAlignment: CrossAxisAlignment.stretch,
60
+ children: [
61
+ Container(
62
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
63
+ decoration: BoxDecoration(
64
+ color: headerBgColor,
65
+ borderRadius: const BorderRadius.vertical(
66
+ top: Radius.circular(7),
67
+ ),
68
+ ),
69
+ child: Row(
70
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
71
+ children: [
72
+ Text(
73
+ _normalizeLanguage(widget.language),
74
+ style: TextStyle(
75
+ fontSize: 11,
76
+ fontWeight: FontWeight.w600,
77
+ color: isDark
78
+ ? const Color(0xFF888888)
79
+ : const Color(0xFF666666),
80
+ fontFamily: 'monospace',
81
+ ),
82
+ ),
83
+ GestureDetector(
84
+ onTap: _copyToClipboard,
85
+ child: Row(
86
+ mainAxisSize: MainAxisSize.min,
87
+ children: [
88
+ Icon(
89
+ _copied ? Icons.check : Icons.copy,
90
+ size: 14,
91
+ color: _copied
92
+ ? Colors.green
93
+ : (isDark
94
+ ? const Color(0xFF888888)
95
+ : const Color(0xFF666666)),
96
+ ),
97
+ const SizedBox(width: 4),
98
+ Text(
99
+ _copied ? 'Copied!' : 'Copy',
100
+ style: TextStyle(
101
+ fontSize: 11,
102
+ color: _copied
103
+ ? Colors.green
104
+ : (isDark
105
+ ? const Color(0xFF888888)
106
+ : const Color(0xFF666666)),
107
+ ),
108
+ ),
109
+ ],
110
+ ),
111
+ ),
112
+ ],
113
+ ),
114
+ ),
115
+ SingleChildScrollView(
116
+ scrollDirection: Axis.horizontal,
117
+ padding: const EdgeInsets.all(12),
118
+ child: SelectableText(
119
+ widget.code,
120
+ style: TextStyle(
121
+ fontFamily: 'monospace',
122
+ fontSize: 13,
123
+ height: 1.5,
124
+ color: textColor,
125
+ ),
126
+ ),
127
+ ),
128
+ ],
129
+ ),
130
+ );
131
+ }
132
+}
lib/features/chat/views/components/message_action_bar.dart
new
+172
@@ -0,0 +1,172 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:flutter/services.dart';
3
+import 'package:shadcn_ui/shadcn_ui.dart';
4
+
5
+class MessageActionBar extends StatelessWidget {
6
+ const MessageActionBar({
7
+ super.key,
8
+ required this.content,
9
+ this.onCopy,
10
+ this.onRetry,
11
+ this.onDelete,
12
+ this.onEdit,
13
+ this.onShare,
14
+ });
15
+
16
+ final String content;
17
+ final VoidCallback? onCopy;
18
+ final VoidCallback? onRetry;
19
+ final VoidCallback? onDelete;
20
+ final VoidCallback? onEdit;
21
+ final VoidCallback? onShare;
22
+
23
+ @override
24
+ Widget build(BuildContext context) {
25
+ return Row(
26
+ mainAxisSize: MainAxisSize.min,
27
+ children: [
28
+ _ActionButton(
29
+ icon: Icons.copy,
30
+ label: 'Copy',
31
+ onTap: () async {
32
+ await Clipboard.setData(ClipboardData(text: content));
33
+ onCopy?.call();
34
+ if (context.mounted) {
35
+ ScaffoldMessenger.of(context).showSnackBar(
36
+ const SnackBar(
37
+ content: Text('Copied to clipboard'),
38
+ duration: Duration(seconds: 2),
39
+ ),
40
+ );
41
+ }
42
+ },
43
+ ),
44
+ if (onRetry != null) ...[
45
+ const SizedBox(width: 4),
46
+ _ActionButton(icon: Icons.refresh, label: 'Retry', onTap: onRetry),
47
+ ],
48
+ if (onEdit != null) ...[
49
+ const SizedBox(width: 4),
50
+ _ActionButton(icon: Icons.edit, label: 'Edit', onTap: onEdit),
51
+ ],
52
+ if (onDelete != null) ...[
53
+ const SizedBox(width: 4),
54
+ _ActionButton(
55
+ icon: Icons.delete_outline,
56
+ label: 'Delete',
57
+ onTap: () => _showDeleteConfirmation(context),
58
+ isDestructive: true,
59
+ ),
60
+ ],
61
+ if (onShare != null) ...[
62
+ const SizedBox(width: 4),
63
+ _ActionButton(icon: Icons.ios_share, label: 'Share', onTap: onShare),
64
+ ],
65
+ const SizedBox(width: 4),
66
+ _ActionButton(
67
+ icon: Icons.more_horiz,
68
+ label: 'More',
69
+ onTap: () => _showMoreOptions(context),
70
+ ),
71
+ ],
72
+ );
73
+ }
74
+
75
+ void _showDeleteConfirmation(BuildContext context) {
76
+ showShadDialog(
77
+ context: context,
78
+ builder: (context) => ShadDialog(
79
+ title: const Text('Delete message?'),
80
+ description: const Text('This action cannot be undone.'),
81
+ actions: [
82
+ ShadButton(
83
+ child: const Text('Cancel'),
84
+ onPressed: () => Navigator.of(context).pop(),
85
+ ),
86
+ ShadButton(
87
+ child: const Text('Delete'),
88
+ onPressed: () {
89
+ Navigator.of(context).pop();
90
+ onDelete?.call();
91
+ },
92
+ ),
93
+ ],
94
+ ),
95
+ );
96
+ }
97
+
98
+ void _showMoreOptions(BuildContext context) {
99
+ showShadSheet(
100
+ context: context,
101
+ builder: (context) => ShadSheet(
102
+ title: const Text('Message options'),
103
+ child: Column(
104
+ mainAxisSize: MainAxisSize.min,
105
+ children: [
106
+ ListTile(
107
+ leading: const Icon(Icons.code),
108
+ title: const Text('Copy as Markdown'),
109
+ onTap: () {
110
+ Navigator.of(context).pop();
111
+ Clipboard.setData(ClipboardData(text: content));
112
+ ScaffoldMessenger.of(context).showSnackBar(
113
+ const SnackBar(content: Text('Copied as Markdown')),
114
+ );
115
+ },
116
+ ),
117
+ if (onShare != null)
118
+ ListTile(
119
+ leading: const Icon(Icons.ios_share),
120
+ title: const Text('Share'),
121
+ onTap: () {
122
+ Navigator.of(context).pop();
123
+ onShare?.call();
124
+ },
125
+ ),
126
+ ListTile(
127
+ leading: const Icon(Icons.text_fields),
128
+ title: Text('${content.length} characters'),
129
+ enabled: false,
130
+ ),
131
+ ],
132
+ ),
133
+ ),
134
+ );
135
+ }
136
+}
137
+
138
+class _ActionButton extends StatelessWidget {
139
+ const _ActionButton({
140
+ required this.icon,
141
+ required this.label,
142
+ required this.onTap,
143
+ this.isDestructive = false,
144
+ });
145
+
146
+ final IconData icon;
147
+ final String label;
148
+ final VoidCallback? onTap;
149
+ final bool isDestructive;
150
+
151
+ @override
152
+ Widget build(BuildContext context) {
153
+ final theme = Theme.of(context);
154
+ final isDark = theme.brightness == Brightness.dark;
155
+
156
+ final color = isDestructive
157
+ ? (isDark ? Colors.red[300] : Colors.red[600])
158
+ : (isDark ? const Color(0xFF888888) : const Color(0xFF666666));
159
+
160
+ return Tooltip(
161
+ message: label,
162
+ child: InkWell(
163
+ onTap: onTap,
164
+ borderRadius: BorderRadius.circular(4),
165
+ child: Padding(
166
+ padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
167
+ child: Icon(icon, size: 16, color: color),
168
+ ),
169
+ ),
170
+ );
171
+ }
172
+}
lib/features/chat/views/components/typing_indicator.dart
new
+132
@@ -0,0 +1,132 @@
1
+import 'package:flutter/material.dart';
2
+
3
+class TypingIndicator extends StatefulWidget {
4
+ const TypingIndicator({super.key});
5
+
6
+ @override
7
+ State<TypingIndicator> createState() => _TypingIndicatorState();
8
+}
9
+
10
+class _TypingIndicatorState extends State<TypingIndicator>
11
+ with TickerProviderStateMixin {
12
+ late final AnimationController _controller;
13
+ late final List<Animation<double>> _animations;
14
+
15
+ @override
16
+ void initState() {
17
+ super.initState();
18
+ _controller = AnimationController(
19
+ duration: const Duration(milliseconds: 1200),
20
+ vsync: this,
21
+ )..repeat();
22
+
23
+ _animations = List.generate(3, (index) {
24
+ return Tween<double>(begin: 0, end: 1).animate(
25
+ CurvedAnimation(
26
+ parent: _controller,
27
+ curve: Interval(
28
+ index * 0.2,
29
+ 0.6 + index * 0.2,
30
+ curve: Curves.easeInOut,
31
+ ),
32
+ ),
33
+ );
34
+ });
35
+ }
36
+
37
+ @override
38
+ void dispose() {
39
+ _controller.dispose();
40
+ super.dispose();
41
+ }
42
+
43
+ @override
44
+ Widget build(BuildContext context) {
45
+ final theme = Theme.of(context);
46
+ final isDark = theme.brightness == Brightness.dark;
47
+
48
+ final dotColor = isDark ? const Color(0xFF888888) : const Color(0xFF666666);
49
+
50
+ return Padding(
51
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
52
+ child: Row(
53
+ mainAxisSize: MainAxisSize.min,
54
+ children: List.generate(3, (index) {
55
+ return AnimatedBuilder(
56
+ animation: _animations[index],
57
+ builder: (context, child) {
58
+ return Container(
59
+ margin: const EdgeInsets.symmetric(horizontal: 3),
60
+ child: Transform.translate(
61
+ offset: Offset(0, -4 * _animations[index].value),
62
+ child: Container(
63
+ width: 8,
64
+ height: 8,
65
+ decoration: BoxDecoration(
66
+ color: dotColor.withValues(
67
+ alpha: 0.4 + 0.6 * _animations[index].value,
68
+ ),
69
+ shape: BoxShape.circle,
70
+ ),
71
+ ),
72
+ ),
73
+ );
74
+ },
75
+ );
76
+ }),
77
+ ),
78
+ );
79
+ }
80
+}
81
+
82
+class StreamingIndicator extends StatefulWidget {
83
+ const StreamingIndicator({super.key});
84
+
85
+ @override
86
+ State<StreamingIndicator> createState() => _StreamingIndicatorState();
87
+}
88
+
89
+class _StreamingIndicatorState extends State<StreamingIndicator>
90
+ with SingleTickerProviderStateMixin {
91
+ late final AnimationController _cursorController;
92
+
93
+ @override
94
+ void initState() {
95
+ super.initState();
96
+ _cursorController = AnimationController(
97
+ duration: const Duration(milliseconds: 530),
98
+ vsync: this,
99
+ )..repeat(reverse: true);
100
+ }
101
+
102
+ @override
103
+ void dispose() {
104
+ _cursorController.dispose();
105
+ super.dispose();
106
+ }
107
+
108
+ @override
109
+ Widget build(BuildContext context) {
110
+ final theme = Theme.of(context);
111
+ final isDark = theme.brightness == Brightness.dark;
112
+
113
+ final cursorColor = isDark
114
+ ? const Color(0xFF888888)
115
+ : const Color(0xFF666666);
116
+
117
+ return AnimatedBuilder(
118
+ animation: _cursorController,
119
+ builder: (context, child) {
120
+ return Opacity(
121
+ opacity: _cursorController.value,
122
+ child: Container(
123
+ width: 2,
124
+ height: 18,
125
+ margin: const EdgeInsets.only(left: 2),
126
+ color: cursorColor,
127
+ ),
128
+ );
129
+ },
130
+ );
131
+ }
132
+}
lib/features/conversations/data/models/conversation.dart
new
+81
@@ -0,0 +1,81 @@
1
+import 'package:hive_ce/hive.dart';
2
+
3
+part 'conversation.g.dart';
4
+
5
+@HiveType(typeId: 2)
6
+class Conversation extends HiveObject {
7
+ @HiveField(0)
8
+ final String id;
9
+
10
+ @HiveField(1)
11
+ final String title;
12
+
13
+ @HiveField(2)
14
+ final DateTime createdAt;
15
+
16
+ @HiveField(3)
17
+ final DateTime updatedAt;
18
+
19
+ @HiveField(4)
20
+ final bool isPinned;
21
+
22
+ @HiveField(5)
23
+ final String? personaId;
24
+
25
+ @HiveField(6)
26
+ final String? serverId;
27
+
28
+ @HiveField(7)
29
+ final String? modelId;
30
+
31
+ @HiveField(8)
32
+ final int messageCount;
33
+
34
+ @HiveField(9)
35
+ final String? lastMessagePreview;
36
+
37
+ @HiveField(10)
38
+ final String? systemPrompt;
39
+
40
+ Conversation({
41
+ required this.id,
42
+ required this.title,
43
+ required this.createdAt,
44
+ required this.updatedAt,
45
+ this.isPinned = false,
46
+ this.personaId,
47
+ this.serverId,
48
+ this.modelId,
49
+ this.messageCount = 0,
50
+ this.lastMessagePreview,
51
+ this.systemPrompt,
52
+ });
53
+
54
+ Conversation copyWith({
55
+ String? id,
56
+ String? title,
57
+ DateTime? createdAt,
58
+ DateTime? updatedAt,
59
+ bool? isPinned,
60
+ String? personaId,
61
+ String? serverId,
62
+ String? modelId,
63
+ int? messageCount,
64
+ String? lastMessagePreview,
65
+ String? systemPrompt,
66
+ }) {
67
+ return Conversation(
68
+ id: id ?? this.id,
69
+ title: title ?? this.title,
70
+ createdAt: createdAt ?? this.createdAt,
71
+ updatedAt: updatedAt ?? this.updatedAt,
72
+ isPinned: isPinned ?? this.isPinned,
73
+ personaId: personaId ?? this.personaId,
74
+ serverId: serverId ?? this.serverId,
75
+ modelId: modelId ?? this.modelId,
76
+ messageCount: messageCount ?? this.messageCount,
77
+ lastMessagePreview: lastMessagePreview ?? this.lastMessagePreview,
78
+ systemPrompt: systemPrompt ?? this.systemPrompt,
79
+ );
80
+ }
81
+}
lib/features/conversations/data/models/conversation.g.dart
new
+71
@@ -0,0 +1,71 @@
1
+// GENERATED CODE - DO NOT MODIFY BY HAND
2
+
3
+part of 'conversation.dart';
4
+
5
+// **************************************************************************
6
+// TypeAdapterGenerator
7
+// **************************************************************************
8
+
9
+class ConversationAdapter extends TypeAdapter<Conversation> {
10
+ @override
11
+ final typeId = 2;
12
+
13
+ @override
14
+ Conversation read(BinaryReader reader) {
15
+ final numOfFields = reader.readByte();
16
+ final fields = <int, dynamic>{
17
+ for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
18
+ };
19
+ return Conversation(
20
+ id: fields[0] as String,
21
+ title: fields[1] as String,
22
+ createdAt: fields[2] as DateTime,
23
+ updatedAt: fields[3] as DateTime,
24
+ isPinned: fields[4] == null ? false : fields[4] as bool,
25
+ personaId: fields[5] as String?,
26
+ serverId: fields[6] as String?,
27
+ modelId: fields[7] as String?,
28
+ messageCount: fields[8] == null ? 0 : (fields[8] as num).toInt(),
29
+ lastMessagePreview: fields[9] as String?,
30
+ systemPrompt: fields[10] as String?,
31
+ );
32
+ }
33
+
34
+ @override
35
+ void write(BinaryWriter writer, Conversation obj) {
36
+ writer
37
+ ..writeByte(11)
38
+ ..writeByte(0)
39
+ ..write(obj.id)
40
+ ..writeByte(1)
41
+ ..write(obj.title)
42
+ ..writeByte(2)
43
+ ..write(obj.createdAt)
44
+ ..writeByte(3)
45
+ ..write(obj.updatedAt)
46
+ ..writeByte(4)
47
+ ..write(obj.isPinned)
48
+ ..writeByte(5)
49
+ ..write(obj.personaId)
50
+ ..writeByte(6)
51
+ ..write(obj.serverId)
52
+ ..writeByte(7)
53
+ ..write(obj.modelId)
54
+ ..writeByte(8)
55
+ ..write(obj.messageCount)
56
+ ..writeByte(9)
57
+ ..write(obj.lastMessagePreview)
58
+ ..writeByte(10)
59
+ ..write(obj.systemPrompt);
60
+ }
61
+
62
+ @override
63
+ int get hashCode => typeId.hashCode;
64
+
65
+ @override
66
+ bool operator ==(Object other) =>
67
+ identical(this, other) ||
68
+ other is ConversationAdapter &&
69
+ runtimeType == other.runtimeType &&
70
+ typeId == other.typeId;
71
+}
lib/features/conversations/providers/conversation_providers.dart
new
+227
@@ -0,0 +1,227 @@
1
+import 'package:flutter_riverpod/flutter_riverpod.dart';
2
+import 'package:localmind/core/providers/storage_providers.dart';
3
+import 'package:localmind/features/conversations/data/models/conversation.dart';
4
+
5
+final conversationsProvider =
6
+ NotifierProvider<ConversationsNotifier, List<Conversation>>(() {
7
+ return ConversationsNotifier();
8
+ });
9
+
10
+class ConversationsNotifier extends Notifier<List<Conversation>> {
11
+ @override
12
+ List<Conversation> build() {
13
+ final box = ref.watch(conversationsBoxProvider);
14
+ final conversations = box.values.toList();
15
+ _sortConversations(conversations);
16
+ return conversations;
17
+ }
18
+
19
+ void _sortConversations(List<Conversation> conversations) {
20
+ conversations.sort((a, b) {
21
+ if (a.isPinned != b.isPinned) {
22
+ return a.isPinned ? -1 : 1;
23
+ }
24
+ return b.updatedAt.compareTo(a.updatedAt);
25
+ });
26
+ }
27
+
28
+ Future<Conversation> createConversation({
29
+ String? title,
30
+ String? personaId,
31
+ String? systemPrompt,
32
+ String? serverId,
33
+ String? modelId,
34
+ }) async {
35
+ final box = ref.read(conversationsBoxProvider);
36
+ final now = DateTime.now();
37
+ final id = _generateUuid();
38
+
39
+ final conversation = Conversation(
40
+ id: id,
41
+ title: title ?? 'New Chat',
42
+ createdAt: now,
43
+ updatedAt: now,
44
+ isPinned: false,
45
+ personaId: personaId,
46
+ serverId: serverId,
47
+ modelId: modelId,
48
+ messageCount: 0,
49
+ lastMessagePreview: null,
50
+ systemPrompt: systemPrompt,
51
+ );
52
+
53
+ await box.put(id, conversation);
54
+ state = [conversation, ...state];
55
+ return conversation;
56
+ }
57
+
58
+ Future<void> renameConversation(String id, String newTitle) async {
59
+ final box = ref.read(conversationsBoxProvider);
60
+ final conversation = box.get(id);
61
+ if (conversation != null) {
62
+ final updated = conversation.copyWith(
63
+ title: newTitle,
64
+ updatedAt: DateTime.now(),
65
+ );
66
+ await box.put(id, updated);
67
+ state = [
68
+ for (final c in state)
69
+ if (c.id == id) updated else c,
70
+ ];
71
+ }
72
+ }
73
+
74
+ Future<void> deleteConversation(String id) async {
75
+ final box = ref.read(conversationsBoxProvider);
76
+ await box.delete(id);
77
+ state = state.where((c) => c.id != id).toList();
78
+ }
79
+
80
+ Future<void> togglePin(String id) async {
81
+ final box = ref.read(conversationsBoxProvider);
82
+ final conversation = box.get(id);
83
+ if (conversation != null) {
84
+ final updated = conversation.copyWith(
85
+ isPinned: !conversation.isPinned,
86
+ updatedAt: DateTime.now(),
87
+ );
88
+ await box.put(id, updated);
89
+ _sortConversations(state);
90
+ state = [for (final c in state) c.id == id ? updated : c];
91
+ }
92
+ }
93
+
94
+ Future<void> updatePreview(
95
+ String id,
96
+ String preview,
97
+ DateTime updatedAt, {
98
+ int? messageCount,
99
+ }) async {
100
+ final box = ref.read(conversationsBoxProvider);
101
+ final conversation = box.get(id);
102
+ if (conversation != null) {
103
+ final updated = conversation.copyWith(
104
+ lastMessagePreview: preview,
105
+ updatedAt: updatedAt,
106
+ messageCount: messageCount ?? conversation.messageCount + 1,
107
+ );
108
+ await box.put(id, updated);
109
+ state = [
110
+ for (final c in state)
111
+ if (c.id == id) updated else c,
112
+ ];
113
+ _sortConversations(state);
114
+ state = List.from(state);
115
+ }
116
+ }
117
+
118
+ Future<void> deleteAll() async {
119
+ final box = ref.read(conversationsBoxProvider);
120
+ await box.clear();
121
+ state = [];
122
+ }
123
+
124
+ String _generateUuid() {
125
+ final random = DateTime.now().microsecondsSinceEpoch;
126
+ return '${random.toRadixString(16)}-${DateTime.now().millisecondsSinceEpoch.toRadixString(16)}';
127
+ }
128
+}
129
+
130
+final activeConversationProvider =
131
+ NotifierProvider<ActiveConversationNotifier, Conversation?>(() {
132
+ return ActiveConversationNotifier();
133
+ });
134
+
135
+class ActiveConversationNotifier extends Notifier<Conversation?> {
136
+ @override
137
+ Conversation? build() {
138
+ final conversations = ref.watch(conversationsProvider);
139
+ if (conversations.isEmpty) return null;
140
+ return conversations.first;
141
+ }
142
+
143
+ void setActiveConversation(Conversation? conversation) {
144
+ if (conversation != null) {
145
+ final conversations = ref.read(conversationsProvider);
146
+ final index = conversations.indexWhere((c) => c.id == conversation.id);
147
+ if (index > 0) {
148
+ ref
149
+ .read(conversationsProvider.notifier)
150
+ .updatePreview(
151
+ conversation.id,
152
+ conversation.lastMessagePreview ?? '',
153
+ DateTime.now(),
154
+ );
155
+ }
156
+ }
157
+ state = conversation;
158
+ }
159
+}
160
+
161
+final conversationSearchProvider =
162
+ NotifierProvider<ConversationSearchNotifier, String>(() {
163
+ return ConversationSearchNotifier();
164
+ });
165
+
166
+class ConversationSearchNotifier extends Notifier<String> {
167
+ @override
168
+ String build() => '';
169
+
170
+ void setSearchQuery(String query) {
171
+ state = query;
172
+ }
173
+
174
+ void clearSearch() {
175
+ state = '';
176
+ }
177
+}
178
+
179
+final filteredConversationsProvider = Provider<List<Conversation>>((ref) {
180
+ final conversations = ref.watch(conversationsProvider);
181
+ final query = ref.watch(conversationSearchProvider).toLowerCase();
182
+ if (query.isEmpty) return conversations;
183
+ return conversations.where((c) {
184
+ return c.title.toLowerCase().contains(query) ||
185
+ (c.lastMessagePreview?.toLowerCase().contains(query) ?? false);
186
+ }).toList();
187
+});
188
+
189
+final groupedConversationsProvider = Provider<Map<String, List<Conversation>>>((
190
+ ref,
191
+) {
192
+ final conversations = ref.watch(filteredConversationsProvider);
193
+ final now = DateTime.now();
194
+ final today = DateTime(now.year, now.month, now.day);
195
+ final yesterday = today.subtract(const Duration(days: 1));
196
+ final sevenDaysAgo = today.subtract(const Duration(days: 7));
197
+ final thirtyDaysAgo = today.subtract(const Duration(days: 30));
198
+
199
+ final grouped = <String, List<Conversation>>{};
200
+
201
+ for (final conversation in conversations) {
202
+ final convDate = DateTime(
203
+ conversation.updatedAt.year,
204
+ conversation.updatedAt.month,
205
+ conversation.updatedAt.day,
206
+ );
207
+
208
+ String section;
209
+ if (conversation.isPinned) {
210
+ section = 'PINNED';
211
+ } else if (convDate.isAtSameMomentAs(today)) {
212
+ section = 'TODAY';
213
+ } else if (convDate.isAtSameMomentAs(yesterday)) {
214
+ section = 'YESTERDAY';
215
+ } else if (convDate.isAfter(sevenDaysAgo)) {
216
+ section = 'PREVIOUS 7 DAYS';
217
+ } else if (convDate.isAfter(thirtyDaysAgo)) {
218
+ section = 'PREVIOUS 30 DAYS';
219
+ } else {
220
+ section = 'OLDER';
221
+ }
222
+
223
+ grouped.putIfAbsent(section, () => []).add(conversation);
224
+ }
225
+
226
+ return grouped;
227
+});
lib/features/conversations/screens/conversation_drawer.dart
new
+431
@@ -0,0 +1,431 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+import 'package:go_router/go_router.dart';
4
+import 'package:hugeicons/hugeicons.dart';
5
+import '../../../core/routes/app_routes.dart';
6
+import '../../../features/chat/providers/chat_providers.dart';
7
+import '../data/models/conversation.dart';
8
+import '../providers/conversation_providers.dart';
9
+import '../views/components/conversation_search_bar.dart';
10
+import '../views/components/conversation_tile.dart';
11
+import '../views/components/date_section_header.dart';
12
+
13
+class ConversationDrawer extends ConsumerWidget {
14
+ const ConversationDrawer({super.key});
15
+
16
+ @override
17
+ Widget build(BuildContext context, WidgetRef ref) {
18
+ final theme = Theme.of(context);
19
+ final isDark = theme.brightness == Brightness.dark;
20
+ final groupedConversations = ref.watch(groupedConversationsProvider);
21
+ final activeConversation = ref.watch(activeConversationProvider);
22
+ final searchQuery = ref.watch(conversationSearchProvider);
23
+
24
+ return Drawer(
25
+ backgroundColor: isDark
26
+ ? const Color(0xFF0A0A0A)
27
+ : const Color(0xFFFAFAFA),
28
+ child: SafeArea(
29
+ child: Column(
30
+ children: [
31
+ Container(
32
+ padding: const EdgeInsets.all(16),
33
+ child: Row(
34
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
35
+ children: [
36
+ Row(
37
+ children: [
38
+ Container(
39
+ padding: const EdgeInsets.all(8),
40
+ decoration: BoxDecoration(
41
+ color: isDark
42
+ ? const Color(0xFF3B82F6)
43
+ : const Color(0xFF2563EB),
44
+ borderRadius: BorderRadius.circular(8),
45
+ ),
46
+ child: const Icon(
47
+ Icons.psychology,
48
+ color: Colors.white,
49
+ size: 20,
50
+ ),
51
+ ),
52
+ const SizedBox(width: 12),
53
+ Text(
54
+ 'LocalMind',
55
+ style: TextStyle(
56
+ fontSize: 18,
57
+ fontWeight: FontWeight.bold,
58
+ color: isDark ? Colors.white : Colors.black,
59
+ ),
60
+ ),
61
+ ],
62
+ ),
63
+ IconButton(
64
+ icon: Icon(
65
+ Icons.settings,
66
+ color: isDark
67
+ ? const Color(0xFF888888)
68
+ : const Color(0xFF666666),
69
+ ),
70
+ onPressed: () {
71
+ final navigator = Navigator.of(context);
72
+ navigator.pop();
73
+ Future.delayed(const Duration(milliseconds: 300), () {
74
+ navigator.pushNamed(AppRoutes.settings);
75
+ });
76
+ },
77
+ ),
78
+ ],
79
+ ),
80
+ ),
81
+ Container(
82
+ padding: const EdgeInsets.symmetric(horizontal: 16),
83
+ child: SizedBox(
84
+ width: double.infinity,
85
+ child: ElevatedButton.icon(
86
+ onPressed: () {
87
+ ref.read(chatProvider.notifier).startNewConversation();
88
+ context.go(AppRoutes.home);
89
+ },
90
+ icon: const Icon(Icons.add, size: 18),
91
+ label: const Text('New Chat'),
92
+ style: ElevatedButton.styleFrom(
93
+ backgroundColor: isDark
94
+ ? const Color(0xFF3B82F6)
95
+ : const Color(0xFF2563EB),
96
+ foregroundColor: Colors.white,
97
+ padding: const EdgeInsets.symmetric(vertical: 12),
98
+ shape: RoundedRectangleBorder(
99
+ borderRadius: BorderRadius.circular(8),
100
+ ),
101
+ ),
102
+ ),
103
+ ),
104
+ ),
105
+ const SizedBox(height: 8),
106
+ const ConversationSearchBar(),
107
+ const Divider(height: 1),
108
+ Expanded(
109
+ child: groupedConversations.isEmpty
110
+ ? _buildEmptyState(context, isDark, searchQuery.isNotEmpty)
111
+ : _buildConversationList(
112
+ context,
113
+ ref,
114
+ groupedConversations,
115
+ activeConversation,
116
+ isDark,
117
+ ),
118
+ ),
119
+ const Divider(height: 1),
120
+ Padding(
121
+ padding: const EdgeInsets.all(12),
122
+ child: Row(
123
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
124
+ children: [
125
+ _NavButton(
126
+ icon: HugeIcons.strokeRoundedServerStack01,
127
+ label: 'Servers',
128
+ isDark: isDark,
129
+ onTap: () {
130
+ context.go(AppRoutes.servers);
131
+ },
132
+ ),
133
+ _NavButton(
134
+ icon: HugeIcons.strokeRoundedCompass01,
135
+ label: 'Personas',
136
+ isDark: isDark,
137
+ onTap: () {
138
+ context.go(AppRoutes.personas);
139
+ },
140
+ ),
141
+ _NavButton(
142
+ icon: HugeIcons.strokeRoundedSettings01,
143
+ label: 'Settings',
144
+ isDark: isDark,
145
+ onTap: () {
146
+ context.go(AppRoutes.settings);
147
+ },
148
+ ),
149
+ ],
150
+ ),
151
+ ),
152
+ ],
153
+ ),
154
+ ),
155
+ );
156
+ }
157
+
158
+ Widget _buildEmptyState(BuildContext context, bool isDark, bool isSearching) {
159
+ return Center(
160
+ child: Padding(
161
+ padding: const EdgeInsets.all(32),
162
+ child: Column(
163
+ mainAxisAlignment: MainAxisAlignment.center,
164
+ children: [
165
+ Icon(
166
+ isSearching ? Icons.search_off : Icons.chat_bubble_outline,
167
+ size: 48,
168
+ color: isDark ? const Color(0xFF3A3A3A) : const Color(0xFFE5E5E5),
169
+ ),
170
+ const SizedBox(height: 16),
171
+ Text(
172
+ isSearching ? 'No results found' : 'No conversations yet',
173
+ style: TextStyle(
174
+ fontSize: 16,
175
+ fontWeight: FontWeight.w500,
176
+ color: isDark ? Colors.white : Colors.black,
177
+ ),
178
+ ),
179
+ const SizedBox(height: 8),
180
+ Text(
181
+ isSearching
182
+ ? 'Try a different search term'
183
+ : 'Start a new conversation',
184
+ style: TextStyle(
185
+ fontSize: 14,
186
+ color: isDark
187
+ ? const Color(0xFF888888)
188
+ : const Color(0xFF666666),
189
+ ),
190
+ ),
191
+ ],
192
+ ),
193
+ ),
194
+ );
195
+ }
196
+
197
+ Widget _buildConversationList(
198
+ BuildContext context,
199
+ WidgetRef ref,
200
+ Map<String, List<Conversation>> groupedConversations,
201
+ Conversation? activeConversation,
202
+ bool isDark,
203
+ ) {
204
+ final sectionOrder = [
205
+ 'PINNED',
206
+ 'TODAY',
207
+ 'YESTERDAY',
208
+ 'PREVIOUS 7 DAYS',
209
+ 'PREVIOUS 30 DAYS',
210
+ 'OLDER',
211
+ ];
212
+ final sortedSections = groupedConversations.keys.toList()
213
+ ..sort((a, b) {
214
+ final aIndex = sectionOrder.indexOf(a);
215
+ final bIndex = sectionOrder.indexOf(b);
216
+ return aIndex.compareTo(bIndex);
217
+ });
218
+
219
+ return ListView.builder(
220
+ padding: const EdgeInsets.only(bottom: 8),
221
+ itemCount: sortedSections.length,
222
+ itemBuilder: (context, sectionIndex) {
223
+ final section = sortedSections[sectionIndex];
224
+ final conversations = groupedConversations[section]!;
225
+
226
+ if (section == 'PINNED' && conversations.isEmpty) {
227
+ return const SizedBox.shrink();
228
+ }
229
+
230
+ return Column(
231
+ crossAxisAlignment: CrossAxisAlignment.start,
232
+ children: [
233
+ DateSectionHeader(title: section),
234
+ ...conversations.map((conversation) {
235
+ return ConversationTile(
236
+ conversation: conversation,
237
+ isActive: activeConversation?.id == conversation.id,
238
+ onTap: () {
239
+ ref
240
+ .read(chatProvider.notifier)
241
+ .loadConversation(conversation);
242
+ context.go(AppRoutes.home);
243
+ },
244
+ onLongPress: () {
245
+ _showConversationOptions(context, ref, conversation);
246
+ },
247
+ onDelete: () {
248
+ _showDeleteConfirmation(context, ref, conversation);
249
+ },
250
+ );
251
+ }),
252
+ ],
253
+ );
254
+ },
255
+ );
256
+ }
257
+
258
+ void _showConversationOptions(
259
+ BuildContext context,
260
+ WidgetRef ref,
261
+ Conversation conversation,
262
+ ) {
263
+ showModalBottomSheet(
264
+ context: context,
265
+ builder: (context) {
266
+ return SafeArea(
267
+ child: Column(
268
+ mainAxisSize: MainAxisSize.min,
269
+ children: [
270
+ ListTile(
271
+ leading: Icon(
272
+ conversation.isPinned
273
+ ? Icons.push_pin
274
+ : Icons.push_pin_outlined,
275
+ ),
276
+ title: Text(conversation.isPinned ? 'Unpin' : 'Pin'),
277
+ onTap: () {
278
+ Navigator.pop(context);
279
+ ref
280
+ .read(conversationsProvider.notifier)
281
+ .togglePin(conversation.id);
282
+ },
283
+ ),
284
+ ListTile(
285
+ leading: const Icon(Icons.edit),
286
+ title: const Text('Rename'),
287
+ onTap: () {
288
+ Navigator.pop(context);
289
+ _showRenameDialog(context, ref, conversation);
290
+ },
291
+ ),
292
+ ListTile(
293
+ leading: const Icon(Icons.delete, color: Colors.red),
294
+ title: const Text(
295
+ 'Delete',
296
+ style: TextStyle(color: Colors.red),
297
+ ),
298
+ onTap: () {
299
+ Navigator.pop(context);
300
+ _showDeleteConfirmation(context, ref, conversation);
301
+ },
302
+ ),
303
+ ],
304
+ ),
305
+ );
306
+ },
307
+ );
308
+ }
309
+
310
+ void _showRenameDialog(
311
+ BuildContext context,
312
+ WidgetRef ref,
313
+ Conversation conversation,
314
+ ) {
315
+ final controller = TextEditingController(text: conversation.title);
316
+
317
+ showDialog(
318
+ context: context,
319
+ builder: (context) {
320
+ return AlertDialog(
321
+ title: const Text('Rename conversation'),
322
+ content: TextField(
323
+ controller: controller,
324
+ autofocus: true,
325
+ decoration: const InputDecoration(
326
+ hintText: 'Enter new title',
327
+ border: OutlineInputBorder(),
328
+ ),
329
+ ),
330
+ actions: [
331
+ TextButton(
332
+ onPressed: () => Navigator.pop(context),
333
+ child: const Text('Cancel'),
334
+ ),
335
+ TextButton(
336
+ onPressed: () {
337
+ final newTitle = controller.text.trim();
338
+ if (newTitle.isNotEmpty) {
339
+ ref
340
+ .read(conversationsProvider.notifier)
341
+ .renameConversation(conversation.id, newTitle);
342
+ }
343
+ Navigator.pop(context);
344
+ },
345
+ child: const Text('Rename'),
346
+ ),
347
+ ],
348
+ );
349
+ },
350
+ );
351
+ }
352
+
353
+ void _showDeleteConfirmation(
354
+ BuildContext context,
355
+ WidgetRef ref,
356
+ Conversation conversation,
357
+ ) {
358
+ showDialog(
359
+ context: context,
360
+ builder: (context) {
361
+ return AlertDialog(
362
+ title: const Text('Delete conversation?'),
363
+ content: Text(
364
+ 'Are you sure you want to delete "${conversation.title}"? This cannot be undone.',
365
+ ),
366
+ actions: [
367
+ TextButton(
368
+ onPressed: () => Navigator.pop(context),
369
+ child: const Text('Cancel'),
370
+ ),
371
+ TextButton(
372
+ onPressed: () {
373
+ ref
374
+ .read(conversationsProvider.notifier)
375
+ .deleteConversation(conversation.id);
376
+ Navigator.pop(context);
377
+ },
378
+ style: TextButton.styleFrom(foregroundColor: Colors.red),
379
+ child: const Text('Delete'),
380
+ ),
381
+ ],
382
+ );
383
+ },
384
+ );
385
+ }
386
+}
387
+
388
+class _NavButton extends StatelessWidget {
389
+ const _NavButton({
390
+ required this.icon,
391
+ required this.label,
392
+ required this.isDark,
393
+ required this.onTap,
394
+ });
395
+
396
+ final List<List<dynamic>> icon;
397
+ final String label;
398
+ final bool isDark;
399
+ final VoidCallback onTap;
400
+
401
+ @override
402
+ Widget build(BuildContext context) {
403
+ return InkWell(
404
+ onTap: onTap,
405
+ borderRadius: BorderRadius.circular(8),
406
+ child: Padding(
407
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
408
+ child: Column(
409
+ mainAxisSize: MainAxisSize.min,
410
+ children: [
411
+ HugeIcon(
412
+ icon: icon,
413
+ size: 20,
414
+ color: isDark ? const Color(0xFF888888) : const Color(0xFF666666),
415
+ ),
416
+ const SizedBox(height: 4),
417
+ Text(
418
+ label,
419
+ style: TextStyle(
420
+ fontSize: 12,
421
+ color: isDark
422
+ ? const Color(0xFF888888)
423
+ : const Color(0xFF666666),
424
+ ),
425
+ ),
426
+ ],
427
+ ),
428
+ ),
429
+ );
430
+ }
431
+}
lib/features/conversations/views/components/conversation_search_bar.dart
new
+80
@@ -0,0 +1,80 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+import 'package:localmind/features/conversations/providers/conversation_providers.dart';
4
+
5
+class ConversationSearchBar extends ConsumerStatefulWidget {
6
+ const ConversationSearchBar({super.key});
7
+
8
+ @override
9
+ ConsumerState<ConversationSearchBar> createState() =>
10
+ _ConversationSearchBarState();
11
+}
12
+
13
+class _ConversationSearchBarState extends ConsumerState<ConversationSearchBar> {
14
+ final TextEditingController _controller = TextEditingController();
15
+
16
+ @override
17
+ void dispose() {
18
+ _controller.dispose();
19
+ super.dispose();
20
+ }
21
+
22
+ @override
23
+ Widget build(BuildContext context) {
24
+ final theme = Theme.of(context);
25
+ final isDark = theme.brightness == Brightness.dark;
26
+
27
+ return Container(
28
+ margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
29
+ decoration: BoxDecoration(
30
+ color: isDark ? const Color(0xFF1A1A1A) : const Color(0xFFF5F5F5),
31
+ borderRadius: BorderRadius.circular(8),
32
+ border: Border.all(
33
+ color: isDark ? const Color(0xFF2A2A2A) : const Color(0xFFE5E5E5),
34
+ ),
35
+ ),
36
+ child: TextField(
37
+ controller: _controller,
38
+ onChanged: (value) {
39
+ ref.read(conversationSearchProvider.notifier).setSearchQuery(value);
40
+ },
41
+ style: TextStyle(
42
+ fontSize: 14,
43
+ color: isDark ? Colors.white : Colors.black,
44
+ ),
45
+ decoration: InputDecoration(
46
+ hintText: 'Search conversations...',
47
+ hintStyle: TextStyle(
48
+ fontSize: 14,
49
+ color: isDark ? const Color(0xFF666666) : const Color(0xFF999999),
50
+ ),
51
+ prefixIcon: Icon(
52
+ Icons.search,
53
+ size: 20,
54
+ color: isDark ? const Color(0xFF666666) : const Color(0xFF999999),
55
+ ),
56
+ suffixIcon: _controller.text.isNotEmpty
57
+ ? IconButton(
58
+ icon: Icon(
59
+ Icons.clear,
60
+ size: 18,
61
+ color: isDark
62
+ ? const Color(0xFF666666)
63
+ : const Color(0xFF999999),
64
+ ),
65
+ onPressed: () {
66
+ _controller.clear();
67
+ ref.read(conversationSearchProvider.notifier).clearSearch();
68
+ },
69
+ )
70
+ : null,
71
+ border: InputBorder.none,
72
+ contentPadding: const EdgeInsets.symmetric(
73
+ horizontal: 16,
74
+ vertical: 12,
75
+ ),
76
+ ),
77
+ ),
78
+ );
79
+ }
80
+}
lib/features/conversations/views/components/conversation_tile.dart
new
+134
@@ -0,0 +1,134 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:localmind/features/conversations/data/models/conversation.dart';
3
+
4
+class ConversationTile extends StatelessWidget {
5
+ const ConversationTile({
6
+ super.key,
7
+ required this.conversation,
8
+ required this.isActive,
9
+ required this.onTap,
10
+ required this.onLongPress,
11
+ required this.onDelete,
12
+ });
13
+
14
+ final Conversation conversation;
15
+ final bool isActive;
16
+ final VoidCallback onTap;
17
+ final VoidCallback onLongPress;
18
+ final VoidCallback onDelete;
19
+
20
+ String _formatTimestamp(DateTime dateTime) {
21
+ final now = DateTime.now();
22
+ final diff = now.difference(dateTime);
23
+
24
+ if (diff.inMinutes < 1) {
25
+ return 'Just now';
26
+ } else if (diff.inHours < 1) {
27
+ return '${diff.inMinutes}m ago';
28
+ } else if (diff.inHours < 24) {
29
+ return '${diff.inHours}h ago';
30
+ } else if (diff.inDays == 1) {
31
+ return 'Yesterday';
32
+ } else if (diff.inDays < 7) {
33
+ return '${diff.inDays}d ago';
34
+ } else {
35
+ return '${dateTime.month}/${dateTime.day}/${dateTime.year}';
36
+ }
37
+ }
38
+
39
+ @override
40
+ Widget build(BuildContext context) {
41
+ final theme = Theme.of(context);
42
+ final isDark = theme.brightness == Brightness.dark;
43
+
44
+ return Dismissible(
45
+ key: Key(conversation.id),
46
+ direction: DismissDirection.endToStart,
47
+ background: Container(
48
+ alignment: Alignment.centerRight,
49
+ padding: const EdgeInsets.only(right: 16),
50
+ color: Colors.red,
51
+ child: const Icon(Icons.delete, color: Colors.white),
52
+ ),
53
+ confirmDismiss: (direction) async {
54
+ onDelete();
55
+ return false;
56
+ },
57
+ child: Material(
58
+ color: isActive
59
+ ? (isDark
60
+ ? const Color(0xFF3B82F6).withValues(alpha: 0.2)
61
+ : const Color(0xFF2563EB).withValues(alpha: 0.1))
62
+ : Colors.transparent,
63
+ child: InkWell(
64
+ onTap: onTap,
65
+ onLongPress: onLongPress,
66
+ child: Container(
67
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
68
+ child: Row(
69
+ children: [
70
+ Icon(
71
+ conversation.isPinned
72
+ ? Icons.push_pin
73
+ : Icons.chat_bubble_outline,
74
+ size: 20,
75
+ color: isActive
76
+ ? (isDark
77
+ ? const Color(0xFF3B82F6)
78
+ : const Color(0xFF2563EB))
79
+ : (isDark
80
+ ? const Color(0xFF888888)
81
+ : const Color(0xFF666666)),
82
+ ),
83
+ const SizedBox(width: 12),
84
+ Expanded(
85
+ child: Column(
86
+ crossAxisAlignment: CrossAxisAlignment.start,
87
+ children: [
88
+ Text(
89
+ conversation.title,
90
+ style: TextStyle(
91
+ fontSize: 14,
92
+ fontWeight: isActive
93
+ ? FontWeight.w600
94
+ : FontWeight.w500,
95
+ color: isDark ? Colors.white : Colors.black,
96
+ ),
97
+ maxLines: 1,
98
+ overflow: TextOverflow.ellipsis,
99
+ ),
100
+ if (conversation.lastMessagePreview != null) ...[
101
+ const SizedBox(height: 2),
102
+ Text(
103
+ conversation.lastMessagePreview!,
104
+ style: TextStyle(
105
+ fontSize: 12,
106
+ color: isDark
107
+ ? const Color(0xFF888888)
108
+ : const Color(0xFF666666),
109
+ ),
110
+ maxLines: 1,
111
+ overflow: TextOverflow.ellipsis,
112
+ ),
113
+ ],
114
+ ],
115
+ ),
116
+ ),
117
+ const SizedBox(width: 8),
118
+ Text(
119
+ _formatTimestamp(conversation.updatedAt),
120
+ style: TextStyle(
121
+ fontSize: 12,
122
+ color: isDark
123
+ ? const Color(0xFF666666)
124
+ : const Color(0xFF999999),
125
+ ),
126
+ ),
127
+ ],
128
+ ),
129
+ ),
130
+ ),
131
+ ),
132
+ );
133
+ }
134
+}
lib/features/conversations/views/components/date_section_header.dart
new
+26
@@ -0,0 +1,26 @@
1
+import 'package:flutter/material.dart';
2
+
3
+class DateSectionHeader extends StatelessWidget {
4
+ const DateSectionHeader({super.key, required this.title});
5
+
6
+ final String title;
7
+
8
+ @override
9
+ Widget build(BuildContext context) {
10
+ final theme = Theme.of(context);
11
+ final isDark = theme.brightness == Brightness.dark;
12
+
13
+ return Container(
14
+ padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
15
+ child: Text(
16
+ title,
17
+ style: TextStyle(
18
+ fontSize: 12,
19
+ fontWeight: FontWeight.w600,
20
+ color: isDark ? const Color(0xFF666666) : const Color(0xFF999999),
21
+ letterSpacing: 0.5,
22
+ ),
23
+ ),
24
+ );
25
+ }
26
+}
lib/features/models/data/models/model_info.dart
new
+54
@@ -0,0 +1,54 @@
1
+import 'package:localmind/core/models/enums.dart';
2
+
3
+class ModelInfo {
4
+ final String id;
5
+ final String name;
6
+ final String? description;
7
+ final int? parameterCount;
8
+ final int? contextLength;
9
+ final int? fileSize;
10
+ final String? quantization;
11
+ final String? architecture;
12
+ final ServerType serverType;
13
+ final String serverId;
14
+ final DateTime? modifiedAt;
15
+
16
+ ModelInfo({
17
+ required this.id,
18
+ required this.name,
19
+ this.description,
20
+ this.parameterCount,
21
+ this.contextLength,
22
+ this.fileSize,
23
+ this.quantization,
24
+ this.architecture,
25
+ required this.serverType,
26
+ required this.serverId,
27
+ this.modifiedAt,
28
+ });
29
+
30
+ String get displayName {
31
+ if (name.isNotEmpty) return name;
32
+ return id
33
+ .replaceAll('-', ' ')
34
+ .replaceAll('_', ' ')
35
+ .split(' ')
36
+ .map(
37
+ (w) => w.isNotEmpty ? '${w[0].toUpperCase()}${w.substring(1)}' : '',
38
+ )
39
+ .join(' ');
40
+ }
41
+
42
+ String? get formattedSize {
43
+ if (fileSize == null) return null;
44
+ if (fileSize! < 1024 * 1024 * 1024) {
45
+ return '${(fileSize! / (1024 * 1024)).toStringAsFixed(1)} MB';
46
+ }
47
+ return '${(fileSize! / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
48
+ }
49
+
50
+ String? get parameterCountDisplay {
51
+ if (parameterCount == null) return null;
52
+ return '${parameterCount}B';
53
+ }
54
+}
lib/features/models/screens/model_picker_sheet.dart
new
+43
@@ -0,0 +1,43 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+
4
+class ModelPickerSheet extends ConsumerWidget {
5
+ const ModelPickerSheet({super.key});
6
+
7
+ @override
8
+ Widget build(BuildContext context, WidgetRef ref) {
9
+ return Container(
10
+ height: MediaQuery.of(context).size.height * 0.6,
11
+ padding: const EdgeInsets.all(16),
12
+ child: Column(
13
+ crossAxisAlignment: CrossAxisAlignment.start,
14
+ children: [
15
+ Center(
16
+ child: Container(
17
+ width: 40,
18
+ height: 4,
19
+ decoration: BoxDecoration(
20
+ color: Colors.grey[300],
21
+ borderRadius: BorderRadius.circular(2),
22
+ ),
23
+ ),
24
+ ),
25
+ const SizedBox(height: 16),
26
+ const Text(
27
+ 'Select Model',
28
+ style: TextStyle(
29
+ fontSize: 20,
30
+ fontWeight: FontWeight.bold,
31
+ ),
32
+ ),
33
+ const SizedBox(height: 16),
34
+ const Expanded(
35
+ child: Center(
36
+ child: Text('Model Picker - Placeholder'),
37
+ ),
38
+ ),
39
+ ],
40
+ ),
41
+ );
42
+ }
43
+}
lib/features/onboarding/screens/onboarding_server_setup_screen.dart
new
+314
@@ -0,0 +1,314 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+import 'package:go_router/go_router.dart';
4
+import 'package:shadcn_ui/shadcn_ui.dart';
5
+
6
+import '../../../core/constants/app_constants.dart';
7
+import '../../../core/models/enums.dart';
8
+import 'package:localmind/features/servers/data/models/server.dart';
9
+import '../../../core/providers/service_providers.dart';
10
+import '../../servers/providers/server_providers.dart';
11
+import '../../../core/routes/app_routes.dart';
12
+
13
+class OnboardingServerSetupScreen extends ConsumerStatefulWidget {
14
+ final ServerType selectedType;
15
+
16
+ const OnboardingServerSetupScreen({super.key, required this.selectedType});
17
+
18
+ @override
19
+ ConsumerState<OnboardingServerSetupScreen> createState() =>
20
+ _OnboardingServerSetupScreenState();
21
+}
22
+
23
+class _OnboardingServerSetupScreenState
24
+ extends ConsumerState<OnboardingServerSetupScreen> {
25
+ final _formKey = GlobalKey<FormState>();
26
+ late TextEditingController _nameController;
27
+ late TextEditingController _hostController;
28
+ late TextEditingController _portController;
29
+ late TextEditingController _apiKeyController;
30
+
31
+ bool _isTesting = false;
32
+ bool _isSaving = false;
33
+ String? _testResult;
34
+ bool _testSuccess = false;
35
+
36
+ @override
37
+ void initState() {
38
+ super.initState();
39
+ String defaultName = '';
40
+ String defaultPort = '';
41
+
42
+ switch (widget.selectedType) {
43
+ case ServerType.lmStudio:
44
+ defaultName = 'LM Studio';
45
+ defaultPort = AppConstants.lmStudioDefaultPort.toString();
46
+ break;
47
+ case ServerType.ollama:
48
+ defaultName = 'Ollama';
49
+ defaultPort = AppConstants.ollamaDefaultPort.toString();
50
+ break;
51
+ case ServerType.openRouter:
52
+ defaultName = 'OpenRouter';
53
+ defaultPort = '443';
54
+ break;
55
+ }
56
+
57
+ _nameController = TextEditingController(text: defaultName);
58
+ _hostController = TextEditingController(
59
+ text: widget.selectedType == ServerType.openRouter ? '' : '127.0.0.1',
60
+ );
61
+ _portController = TextEditingController(text: defaultPort);
62
+ _apiKeyController = TextEditingController();
63
+ }
64
+
65
+ @override
66
+ void dispose() {
67
+ _nameController.dispose();
68
+ _hostController.dispose();
69
+ _portController.dispose();
70
+ _apiKeyController.dispose();
71
+ super.dispose();
72
+ }
73
+
74
+ Server _buildServer() {
75
+ return Server(
76
+ id: DateTime.now().millisecondsSinceEpoch.toString(),
77
+ name: _nameController.text.trim(),
78
+ type: widget.selectedType,
79
+ host: _hostController.text.trim(),
80
+ port: int.parse(_portController.text.trim()),
81
+ apiKey: widget.selectedType == ServerType.openRouter
82
+ ? _apiKeyController.text.trim()
83
+ : (_apiKeyController.text.trim().isNotEmpty
84
+ ? _apiKeyController.text.trim()
85
+ : null),
86
+ isDefault: true, // Make it default since it's the first one
87
+ createdAt: DateTime.now(),
88
+ lastConnectedAt: DateTime.now(),
89
+ status: ConnectionStatus.disconnected,
90
+ );
91
+ }
92
+
93
+ Future<void> _testConnection() async {
94
+ if (!_formKey.currentState!.validate()) return;
95
+
96
+ setState(() {
97
+ _isTesting = true;
98
+ _testResult = null;
99
+ _testSuccess = false;
100
+ });
101
+
102
+ final apiService = ref.read(serverApiServiceProvider);
103
+ final testServer = _buildServer();
104
+
105
+ try {
106
+ final isConnected = await apiService.testConnection(testServer);
107
+ setState(() {
108
+ _testSuccess = isConnected;
109
+ _testResult = isConnected
110
+ ? 'Connection successful!'
111
+ : 'Connection failed. Check your settings.';
112
+ });
113
+ } catch (e) {
114
+ setState(() {
115
+ _testSuccess = false;
116
+ _testResult = 'Error: ${e.toString()}';
117
+ });
118
+ } finally {
119
+ setState(() {
120
+ _isTesting = false;
121
+ });
122
+ }
123
+ }
124
+
125
+ Future<void> _saveAndContinue() async {
126
+ if (!_formKey.currentState!.validate()) return;
127
+
128
+ setState(() {
129
+ _isSaving = true;
130
+ });
131
+
132
+ try {
133
+ final server = _buildServer();
134
+ await ref.read(serversProvider.notifier).addServer(server);
135
+ await ref.read(serversProvider.notifier).setDefault(server.id);
136
+ ref.read(activeServerProvider.notifier).setActiveServer(server);
137
+
138
+ if (mounted) {
139
+ context.push(AppRoutes.onboardingTheme);
140
+ }
141
+ } catch (e) {
142
+ if (mounted) {
143
+ ScaffoldMessenger.of(context).showSnackBar(
144
+ SnackBar(
145
+ content: Text('Error: ${e.toString()}'),
146
+ backgroundColor: Colors.red,
147
+ ),
148
+ );
149
+ }
150
+ } finally {
151
+ if (mounted) {
152
+ setState(() {
153
+ _isSaving = false;
154
+ });
155
+ }
156
+ }
157
+ }
158
+
159
+ @override
160
+ Widget build(BuildContext context) {
161
+ final theme = Theme.of(context);
162
+ final isCloud = widget.selectedType == ServerType.openRouter;
163
+
164
+ return Scaffold(
165
+ appBar: AppBar(title: const Text('Setup Connection')),
166
+ body: SafeArea(
167
+ child: Form(
168
+ key: _formKey,
169
+ child: ListView(
170
+ padding: const EdgeInsets.symmetric(
171
+ horizontal: 24.0,
172
+ vertical: 16.0,
173
+ ),
174
+ children: [
175
+ Text(
176
+ 'Configure your ${widget.selectedType.name} server to start chatting.',
177
+ style: theme.textTheme.titleMedium?.copyWith(
178
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
179
+ ),
180
+ ),
181
+ const SizedBox(height: 32),
182
+
183
+ TextFormField(
184
+ controller: _nameController,
185
+ decoration: InputDecoration(
186
+ labelText: 'Server Name',
187
+ filled: true,
188
+ fillColor: theme.colorScheme.surface,
189
+ ),
190
+ validator: (val) =>
191
+ val == null || val.trim().isEmpty ? 'Name required' : null,
192
+ ),
193
+ const SizedBox(height: 16),
194
+
195
+ if (!isCloud) ...[
196
+ TextFormField(
197
+ controller: _hostController,
198
+ decoration: InputDecoration(
199
+ labelText: 'Host / IP Address',
200
+ filled: true,
201
+ fillColor: theme.colorScheme.surface,
202
+ ),
203
+ validator: (val) => val == null || val.trim().isEmpty
204
+ ? 'Host required'
205
+ : null,
206
+ ),
207
+ const SizedBox(height: 16),
208
+
209
+ TextFormField(
210
+ controller: _portController,
211
+ decoration: InputDecoration(
212
+ labelText: 'Port',
213
+ filled: true,
214
+ fillColor: theme.colorScheme.surface,
215
+ ),
216
+ validator: (val) {
217
+ if (val == null || val.trim().isEmpty)
218
+ return 'Port required';
219
+ if (int.tryParse(val) == null) return 'Must be a number';
220
+ return null;
221
+ },
222
+ ),
223
+ const SizedBox(height: 16),
224
+ ],
225
+
226
+ TextFormField(
227
+ controller: _apiKeyController,
228
+ decoration: InputDecoration(
229
+ labelText: isCloud ? 'API Key *' : 'API Key (Optional)',
230
+ filled: true,
231
+ fillColor: theme.colorScheme.surface,
232
+ ),
233
+ validator: (val) {
234
+ if (isCloud && (val == null || val.trim().isEmpty)) {
235
+ return 'API Key required for OpenRouter';
236
+ }
237
+ return null;
238
+ },
239
+ obscureText: true,
240
+ ),
241
+ const SizedBox(height: 24),
242
+
243
+ if (_testResult != null)
244
+ Container(
245
+ padding: const EdgeInsets.all(12),
246
+ margin: const EdgeInsets.only(bottom: 24),
247
+ decoration: BoxDecoration(
248
+ color: _testSuccess
249
+ ? Colors.green.withValues(alpha: 0.1)
250
+ : Colors.red.withValues(alpha: 0.1),
251
+ borderRadius: BorderRadius.circular(8),
252
+ border: Border.all(
253
+ color: _testSuccess ? Colors.green : Colors.red,
254
+ ),
255
+ ),
256
+ child: Row(
257
+ children: [
258
+ Icon(
259
+ _testSuccess ? Icons.check_circle : Icons.error,
260
+ color: _testSuccess ? Colors.green : Colors.red,
261
+ ),
262
+ const SizedBox(width: 8),
263
+ Expanded(
264
+ child: Text(
265
+ _testResult!,
266
+ style: TextStyle(
267
+ color: _testSuccess ? Colors.green : Colors.red,
268
+ ),
269
+ ),
270
+ ),
271
+ ],
272
+ ),
273
+ ),
274
+
275
+ ShadButton.outline(
276
+ width: double.infinity,
277
+ onPressed: _isTesting ? null : _testConnection,
278
+ child: _isTesting
279
+ ? const SizedBox(
280
+ width: 20,
281
+ height: 20,
282
+ child: CircularProgressIndicator(strokeWidth: 2),
283
+ )
284
+ : const Text('Test Connection'),
285
+ ),
286
+ const SizedBox(height: 16),
287
+
288
+ ShadButton(
289
+ width: double.infinity,
290
+ onPressed: _isSaving ? null : _saveAndContinue,
291
+ child: _isSaving
292
+ ? const SizedBox(
293
+ width: 20,
294
+ height: 20,
295
+ child: CircularProgressIndicator(
296
+ strokeWidth: 2,
297
+ color: Colors.white,
298
+ ),
299
+ )
300
+ : const Text(
301
+ 'Save & Continue',
302
+ style: TextStyle(
303
+ fontSize: 16,
304
+ fontWeight: FontWeight.w600,
305
+ ),
306
+ ),
307
+ ),
308
+ ],
309
+ ),
310
+ ),
311
+ ),
312
+ );
313
+ }
314
+}
lib/features/onboarding/screens/onboarding_server_type_screen.dart
new
+242
@@ -0,0 +1,242 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+import 'package:go_router/go_router.dart';
4
+import 'package:hugeicons/hugeicons.dart';
5
+import 'package:shadcn_ui/shadcn_ui.dart';
6
+
7
+import '../../../core/models/enums.dart';
8
+import '../../../core/routes/app_routes.dart';
9
+
10
+class OnboardingServerTypeScreen extends ConsumerStatefulWidget {
11
+ const OnboardingServerTypeScreen({super.key});
12
+
13
+ @override
14
+ ConsumerState<OnboardingServerTypeScreen> createState() =>
15
+ _OnboardingServerTypeScreenState();
16
+}
17
+
18
+class _OnboardingServerTypeScreenState
19
+ extends ConsumerState<OnboardingServerTypeScreen> {
20
+ ServerType? _selectedType;
21
+
22
+ @override
23
+ Widget build(BuildContext context) {
24
+ final theme = Theme.of(context);
25
+
26
+ return Scaffold(
27
+ appBar: AppBar(
28
+ title: Row(
29
+ children: [
30
+ /// TODO: Replace with actual app logo
31
+ Padding(
32
+ padding: const EdgeInsets.all(8),
33
+ child: Placeholder(fallbackHeight: 24, fallbackWidth: 24),
34
+ ),
35
+ const Text(
36
+ 'LOCALMIND',
37
+ style: TextStyle(letterSpacing: 2, fontSize: 14),
38
+ ),
39
+ ],
40
+ ),
41
+ centerTitle: false,
42
+ actions: [
43
+ TextButton.icon(
44
+ onPressed: () {},
45
+ label: const Text('Skip'),
46
+ icon: HugeIcon(icon: HugeIcons.strokeRoundedNext),
47
+ ),
48
+ ],
49
+ ),
50
+ body: Column(
51
+ crossAxisAlignment: CrossAxisAlignment.stretch,
52
+ children: [
53
+ Expanded(
54
+ child: SingleChildScrollView(
55
+ padding: const EdgeInsets.symmetric(horizontal: 24.0),
56
+ child: Column(
57
+ crossAxisAlignment: CrossAxisAlignment.stretch,
58
+ children: [
59
+ const SizedBox(height: 24),
60
+ Text(
61
+ 'Connect Your\nServer',
62
+ textAlign: TextAlign.center,
63
+ style: theme.textTheme.headlineLarge?.copyWith(
64
+ fontWeight: FontWeight.w800,
65
+ height: 1.1,
66
+ ),
67
+ ),
68
+ const SizedBox(height: 16),
69
+ Text(
70
+ 'Connect to LM Studio, Ollama, or\nOpenRouter to start your private AI\nexperience.',
71
+ textAlign: TextAlign.center,
72
+ style: theme.textTheme.bodySmall?.copyWith(
73
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
74
+ height: 1.5,
75
+ ),
76
+ ),
77
+ const SizedBox(height: 24),
78
+ _buildServerCard(
79
+ type: ServerType.lmStudio,
80
+ title: 'LM Studio',
81
+ subtitle: 'LOCAL API',
82
+ iconWidget: Icon(
83
+ Icons.terminal_rounded,
84
+ color: _selectedType == ServerType.lmStudio
85
+ ? theme.colorScheme.primary
86
+ : theme.colorScheme.onSurface,
87
+ ),
88
+ theme: theme,
89
+ ),
90
+ const SizedBox(height: 16),
91
+ _buildServerCard(
92
+ type: ServerType.ollama,
93
+ title: 'Ollama',
94
+ subtitle: 'CLI ENGINE',
95
+ iconWidget: Icon(
96
+ Icons.smart_toy_rounded,
97
+ color: _selectedType == ServerType.ollama
98
+ ? theme.colorScheme.primary
99
+ : theme.colorScheme.onSurface,
100
+ ),
101
+ theme: theme,
102
+ ),
103
+ const SizedBox(height: 16),
104
+ _buildServerCard(
105
+ type: ServerType.openRouter,
106
+ title: 'OpenRouter',
107
+ subtitle: 'UNIFIED CLOUD',
108
+ iconWidget: HugeIcon(
109
+ icon: HugeIcons.strokeRoundedCloudServer,
110
+ color: _selectedType == ServerType.openRouter
111
+ ? theme.colorScheme.primary
112
+ : theme.colorScheme.onSurface,
113
+ ),
114
+ theme: theme,
115
+ ),
116
+ const SizedBox(height: 32),
117
+ ],
118
+ ),
119
+ ),
120
+ ),
121
+ Padding(
122
+ padding: const EdgeInsets.symmetric(horizontal: 24.0),
123
+ child: Column(
124
+ children: [
125
+ // Status Indicator
126
+ Row(
127
+ mainAxisAlignment: MainAxisAlignment.center,
128
+ children: [
129
+ Container(
130
+ width: 8,
131
+ height: 8,
132
+ decoration: BoxDecoration(
133
+ color: _selectedType != null
134
+ ? theme.colorScheme.primary
135
+ : theme.colorScheme.onSurface.withValues(
136
+ alpha: 0.4,
137
+ ),
138
+ shape: BoxShape.circle,
139
+ ),
140
+ ),
141
+ const SizedBox(width: 8),
142
+ Text(
143
+ _selectedType != null
144
+ ? 'READY TO CONTINUE'
145
+ : 'WAITING FOR SELECTION',
146
+ style: theme.textTheme.labelSmall?.copyWith(
147
+ color: theme.colorScheme.onSurface.withValues(
148
+ alpha: 0.5,
149
+ ),
150
+ letterSpacing: 2,
151
+ fontWeight: FontWeight.w600,
152
+ ),
153
+ ),
154
+ ],
155
+ ),
156
+ const SizedBox(height: 16),
157
+ ShadButton(
158
+ width: double.infinity,
159
+ enabled: _selectedType != null,
160
+ onPressed: () {
161
+ if (_selectedType != null) {
162
+ context.push(
163
+ AppRoutes.onboardingSetup,
164
+ extra: _selectedType,
165
+ );
166
+ }
167
+ },
168
+ child: const Text(
169
+ 'Continue',
170
+ style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
171
+ ),
172
+ ),
173
+ SizedBox(
174
+ height: MediaQuery.of(context).padding.bottom,
175
+ ), // Reduced bottom padding slightly for small screens
176
+ ],
177
+ ),
178
+ ),
179
+ ],
180
+ ),
181
+ );
182
+ }
183
+
184
+ Widget _buildServerCard({
185
+ required ServerType type,
186
+ required String title,
187
+ required String subtitle,
188
+ required Widget iconWidget,
189
+ required ThemeData theme,
190
+ }) {
191
+ final isSelected = _selectedType == type;
192
+
193
+ return GestureDetector(
194
+ onTap: () {
195
+ setState(() {
196
+ _selectedType = type;
197
+ });
198
+ },
199
+ child: Container(
200
+ padding: const EdgeInsets.all(20),
201
+ decoration: BoxDecoration(
202
+ color: theme.colorScheme.surface,
203
+ borderRadius: BorderRadius.circular(16),
204
+ border: Border.all(
205
+ color: isSelected
206
+ ? theme.colorScheme.primary
207
+ : theme.colorScheme.outline.withValues(alpha: 0.3),
208
+ width: isSelected ? 2 : 1,
209
+ ),
210
+ ),
211
+ child: Column(
212
+ crossAxisAlignment: CrossAxisAlignment.start,
213
+ children: [
214
+ Container(
215
+ padding: const EdgeInsets.all(12),
216
+ decoration: BoxDecoration(
217
+ color: theme.scaffoldBackgroundColor,
218
+ borderRadius: BorderRadius.circular(12),
219
+ ),
220
+ child: iconWidget,
221
+ ),
222
+ const SizedBox(height: 16),
223
+ Text(
224
+ title,
225
+ style: theme.textTheme.titleMedium?.copyWith(
226
+ fontWeight: FontWeight.bold,
227
+ ),
228
+ ),
229
+ const SizedBox(height: 4),
230
+ Text(
231
+ subtitle,
232
+ style: theme.textTheme.labelSmall?.copyWith(
233
+ letterSpacing: 2,
234
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
235
+ ),
236
+ ),
237
+ ],
238
+ ),
239
+ ),
240
+ );
241
+ }
242
+}
lib/features/onboarding/screens/onboarding_theme_screen.dart
new
+204
@@ -0,0 +1,204 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+import 'package:go_router/go_router.dart';
4
+import 'package:hugeicons/hugeicons.dart';
5
+import 'package:shadcn_ui/shadcn_ui.dart';
6
+
7
+import '../../../core/providers/app_providers.dart';
8
+import '../../../core/routes/app_routes.dart';
9
+import '../../../core/theme/app_theme.dart';
10
+
11
+class OnboardingThemeScreen extends ConsumerStatefulWidget {
12
+ const OnboardingThemeScreen({super.key});
13
+
14
+ @override
15
+ ConsumerState<OnboardingThemeScreen> createState() =>
16
+ _OnboardingThemeScreenState();
17
+}
18
+
19
+class _OnboardingThemeScreenState extends ConsumerState<OnboardingThemeScreen> {
20
+ @override
21
+ Widget build(BuildContext context) {
22
+ final theme = Theme.of(context);
23
+ final currentTheme = ref.watch(themeModeProvider);
24
+
25
+ return Scaffold(
26
+ appBar: AppBar(
27
+ title: const Text('Choose Theme'),
28
+ leading: IconButton(
29
+ icon: const Icon(Icons.arrow_back),
30
+ onPressed: () {
31
+ context.pop();
32
+ },
33
+ ),
34
+ ),
35
+ body: SafeArea(
36
+ child: Padding(
37
+ padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
38
+ child: Column(
39
+ crossAxisAlignment: CrossAxisAlignment.stretch,
40
+ children: [
41
+ Text(
42
+ 'Personalize the app appearance. You can always change this later in settings.',
43
+ style: theme.textTheme.titleMedium?.copyWith(
44
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
45
+ ),
46
+ ),
47
+ const SizedBox(height: 32),
48
+
49
+ Expanded(
50
+ child: ListView(
51
+ children: [
52
+ _buildThemeCard(
53
+ type: AppThemeType.system,
54
+ title: 'System',
55
+ subtitle: 'Matches your device settings',
56
+ iconWidget: HugeIcon(
57
+ icon: HugeIcons.strokeRoundedSettings01,
58
+ color: currentTheme == AppThemeType.system
59
+ ? theme.colorScheme.primary
60
+ : theme.colorScheme.onSurface,
61
+ ),
62
+ currentTheme: currentTheme,
63
+ context: context,
64
+ ),
65
+ const SizedBox(height: 16),
66
+ _buildThemeCard(
67
+ type: AppThemeType.light,
68
+ title: 'Light',
69
+ subtitle: 'Clean and bright',
70
+ iconWidget: HugeIcon(
71
+ icon: HugeIcons.strokeRoundedSun01,
72
+ color: currentTheme == AppThemeType.light
73
+ ? theme.colorScheme.primary
74
+ : theme.colorScheme.onSurface,
75
+ ),
76
+ currentTheme: currentTheme,
77
+ context: context,
78
+ ),
79
+ const SizedBox(height: 16),
80
+ _buildThemeCard(
81
+ type: AppThemeType.dark,
82
+ title: 'Dark',
83
+ subtitle: 'Easy on the eyes',
84
+ iconWidget: HugeIcon(
85
+ icon: HugeIcons.strokeRoundedMoon02,
86
+ color: currentTheme == AppThemeType.dark
87
+ ? theme.colorScheme.primary
88
+ : theme.colorScheme.onSurface,
89
+ ),
90
+ currentTheme: currentTheme,
91
+ context: context,
92
+ ),
93
+ const SizedBox(height: 16),
94
+ _buildThemeCard(
95
+ type: AppThemeType.claude,
96
+ title: 'Claude',
97
+ subtitle: 'A warm, peach-tinted theme',
98
+ iconWidget: HugeIcon(
99
+ icon: HugeIcons.strokeRoundedPaintBrush02,
100
+ color: currentTheme == AppThemeType.claude
101
+ ? theme.colorScheme.primary
102
+ : theme.colorScheme.onSurface,
103
+ ),
104
+ currentTheme: currentTheme,
105
+ context: context,
106
+ ),
107
+ ],
108
+ ),
109
+ ),
110
+
111
+ const SizedBox(height: 24),
112
+ ShadButton(
113
+ width: double.infinity,
114
+ onPressed: () async {
115
+ // Mark onboarding as complete
116
+ final settings = ref.read(settingsProvider);
117
+ await ref
118
+ .read(settingsProvider.notifier)
119
+ .updateSettings(
120
+ settings.copyWith(hasCompletedOnboarding: true),
121
+ );
122
+
123
+ if (context.mounted) {
124
+ context.go(AppRoutes.home);
125
+ }
126
+ },
127
+ child: const Text(
128
+ 'Finish Setup',
129
+ style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
130
+ ),
131
+ ),
132
+ ],
133
+ ),
134
+ ),
135
+ ),
136
+ );
137
+ }
138
+
139
+ Widget _buildThemeCard({
140
+ required AppThemeType type,
141
+ required String title,
142
+ required String subtitle,
143
+ required Widget iconWidget,
144
+ required AppThemeType currentTheme,
145
+ required BuildContext context,
146
+ }) {
147
+ final theme = Theme.of(context);
148
+ final isSelected = currentTheme == type;
149
+
150
+ return GestureDetector(
151
+ onTap: () {
152
+ ref.read(themeModeProvider.notifier).setThemeMode(type);
153
+ },
154
+ child: Container(
155
+ padding: const EdgeInsets.all(20),
156
+ decoration: BoxDecoration(
157
+ color: theme.colorScheme.surface,
158
+ borderRadius: BorderRadius.circular(16),
159
+ border: Border.all(
160
+ color: isSelected
161
+ ? theme.colorScheme.primary
162
+ : theme.colorScheme.outline.withValues(alpha: 0.3),
163
+ width: isSelected ? 2 : 1,
164
+ ),
165
+ ),
166
+ child: Row(
167
+ children: [
168
+ Container(
169
+ padding: const EdgeInsets.all(12),
170
+ decoration: BoxDecoration(
171
+ color: theme.scaffoldBackgroundColor,
172
+ borderRadius: BorderRadius.circular(12),
173
+ ),
174
+ child: iconWidget,
175
+ ),
176
+ const SizedBox(width: 16),
177
+ Expanded(
178
+ child: Column(
179
+ crossAxisAlignment: CrossAxisAlignment.start,
180
+ children: [
181
+ Text(
182
+ title,
183
+ style: theme.textTheme.titleMedium?.copyWith(
184
+ fontWeight: FontWeight.bold,
185
+ ),
186
+ ),
187
+ const SizedBox(height: 4),
188
+ Text(
189
+ subtitle,
190
+ style: theme.textTheme.labelMedium?.copyWith(
191
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
192
+ ),
193
+ ),
194
+ ],
195
+ ),
196
+ ),
197
+ if (isSelected)
198
+ Icon(Icons.check_circle, color: theme.colorScheme.primary),
199
+ ],
200
+ ),
201
+ ),
202
+ );
203
+ }
204
+}
lib/features/personas/data/models/persona.dart
new
+75
@@ -0,0 +1,75 @@
1
+import 'package:hive_ce/hive.dart';
2
+
3
+part 'persona.g.dart';
4
+
5
+@HiveType(typeId: 3)
6
+class Persona extends HiveObject {
7
+ @HiveField(0)
8
+ final String id;
9
+
10
+ @HiveField(1)
11
+ final String name;
12
+
13
+ @HiveField(2)
14
+ final String emoji;
15
+
16
+ @HiveField(3)
17
+ final String systemPrompt;
18
+
19
+ @HiveField(4)
20
+ final String? description;
21
+
22
+ @HiveField(5)
23
+ final bool isBuiltIn;
24
+
25
+ @HiveField(6)
26
+ final DateTime createdAt;
27
+
28
+ @HiveField(7)
29
+ final DateTime updatedAt;
30
+
31
+ @HiveField(8)
32
+ final String? category;
33
+
34
+ @HiveField(9)
35
+ final Map<String, dynamic>? preferredParams;
36
+
37
+ Persona({
38
+ required this.id,
39
+ required this.name,
40
+ required this.emoji,
41
+ required this.systemPrompt,
42
+ this.description,
43
+ this.isBuiltIn = false,
44
+ required this.createdAt,
45
+ required this.updatedAt,
46
+ this.category,
47
+ this.preferredParams,
48
+ });
49
+
50
+ Persona copyWith({
51
+ String? id,
52
+ String? name,
53
+ String? emoji,
54
+ String? systemPrompt,
55
+ String? description,
56
+ bool? isBuiltIn,
57
+ DateTime? createdAt,
58
+ DateTime? updatedAt,
59
+ String? category,
60
+ Map<String, dynamic>? preferredParams,
61
+ }) {
62
+ return Persona(
63
+ id: id ?? this.id,
64
+ name: name ?? this.name,
65
+ emoji: emoji ?? this.emoji,
66
+ systemPrompt: systemPrompt ?? this.systemPrompt,
67
+ description: description ?? this.description,
68
+ isBuiltIn: isBuiltIn ?? this.isBuiltIn,
69
+ createdAt: createdAt ?? this.createdAt,
70
+ updatedAt: updatedAt ?? this.updatedAt,
71
+ category: category ?? this.category,
72
+ preferredParams: preferredParams ?? this.preferredParams,
73
+ );
74
+ }
75
+}
lib/features/personas/data/models/persona.g.dart
new
+68
@@ -0,0 +1,68 @@
1
+// GENERATED CODE - DO NOT MODIFY BY HAND
2
+
3
+part of 'persona.dart';
4
+
5
+// **************************************************************************
6
+// TypeAdapterGenerator
7
+// **************************************************************************
8
+
9
+class PersonaAdapter extends TypeAdapter<Persona> {
10
+ @override
11
+ final typeId = 3;
12
+
13
+ @override
14
+ Persona read(BinaryReader reader) {
15
+ final numOfFields = reader.readByte();
16
+ final fields = <int, dynamic>{
17
+ for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
18
+ };
19
+ return Persona(
20
+ id: fields[0] as String,
21
+ name: fields[1] as String,
22
+ emoji: fields[2] as String,
23
+ systemPrompt: fields[3] as String,
24
+ description: fields[4] as String?,
25
+ isBuiltIn: fields[5] == null ? false : fields[5] as bool,
26
+ createdAt: fields[6] as DateTime,
27
+ updatedAt: fields[7] as DateTime,
28
+ category: fields[8] as String?,
29
+ preferredParams: (fields[9] as Map?)?.cast<String, dynamic>(),
30
+ );
31
+ }
32
+
33
+ @override
34
+ void write(BinaryWriter writer, Persona obj) {
35
+ writer
36
+ ..writeByte(10)
37
+ ..writeByte(0)
38
+ ..write(obj.id)
39
+ ..writeByte(1)
40
+ ..write(obj.name)
41
+ ..writeByte(2)
42
+ ..write(obj.emoji)
43
+ ..writeByte(3)
44
+ ..write(obj.systemPrompt)
45
+ ..writeByte(4)
46
+ ..write(obj.description)
47
+ ..writeByte(5)
48
+ ..write(obj.isBuiltIn)
49
+ ..writeByte(6)
50
+ ..write(obj.createdAt)
51
+ ..writeByte(7)
52
+ ..write(obj.updatedAt)
53
+ ..writeByte(8)
54
+ ..write(obj.category)
55
+ ..writeByte(9)
56
+ ..write(obj.preferredParams);
57
+ }
58
+
59
+ @override
60
+ int get hashCode => typeId.hashCode;
61
+
62
+ @override
63
+ bool operator ==(Object other) =>
64
+ identical(this, other) ||
65
+ other is PersonaAdapter &&
66
+ runtimeType == other.runtimeType &&
67
+ typeId == other.typeId;
68
+}
lib/features/personas/screens/create_persona_screen.dart
new
+57
@@ -0,0 +1,57 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+import 'package:go_router/go_router.dart';
4
+import 'package:shadcn_ui/shadcn_ui.dart';
5
+import '../../../core/providers/storage_providers.dart';
6
+import '../../../core/providers/subscription_provider.dart';
7
+
8
+class CreatePersonaScreen extends ConsumerWidget {
9
+ const CreatePersonaScreen({super.key});
10
+
11
+ @override
12
+ Widget build(BuildContext context, WidgetRef ref) {
13
+ final personas = ref.watch(personasProvider);
14
+ final userCreatedCount = personas.where((p) => !p.isBuiltIn).length;
15
+ final isPremium = ref.watch(isPremiumUserProvider);
16
+
17
+ if (!isPremium && userCreatedCount >= 3) {
18
+ return Scaffold(
19
+ appBar: AppBar(
20
+ title: const Text('Create Persona'),
21
+ leading: IconButton(
22
+ icon: const Icon(Icons.close),
23
+ onPressed: () => context.pop(),
24
+ ),
25
+ ),
26
+ body: Center(
27
+ child: Column(
28
+ mainAxisAlignment: MainAxisAlignment.center,
29
+ children: [
30
+ const Text('You have reached your limit of 3 custom personas.'),
31
+ const SizedBox(height: 16),
32
+ ShadButton(
33
+ child: const Text('Upgrade to Premium'),
34
+ onPressed: () {
35
+ ref.read(isPremiumUserProvider.notifier).togglePremium();
36
+ },
37
+ ),
38
+ ],
39
+ ),
40
+ ),
41
+ );
42
+ }
43
+
44
+ return Scaffold(
45
+ appBar: AppBar(
46
+ title: const Text('Create Persona'),
47
+ leading: IconButton(
48
+ icon: const Icon(Icons.close),
49
+ onPressed: () => context.pop(),
50
+ ),
51
+ ),
52
+ body: const Center(
53
+ child: Text('Create Persona - Placeholder'),
54
+ ),
55
+ );
56
+ }
57
+}
lib/features/personas/screens/persona_list_screen.dart
new
+116
@@ -0,0 +1,116 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+import 'package:go_router/go_router.dart';
4
+import 'package:shadcn_ui/shadcn_ui.dart';
5
+import '../../../core/routes/app_routes.dart';
6
+import '../../../core/providers/storage_providers.dart';
7
+import '../../../core/providers/subscription_provider.dart';
8
+
9
+class PersonaListScreen extends ConsumerWidget {
10
+ const PersonaListScreen({super.key});
11
+
12
+ @override
13
+ Widget build(BuildContext context, WidgetRef ref) {
14
+ final personas = ref.watch(personasProvider);
15
+ final userCreatedCount = personas.where((p) => !p.isBuiltIn).length;
16
+ final isPremium = ref.watch(isPremiumUserProvider);
17
+ final theme = Theme.of(context);
18
+ final isDark = theme.brightness == Brightness.dark;
19
+
20
+ return Stack(
21
+ children: [
22
+ Column(
23
+ children: [
24
+ Container(
25
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
26
+ decoration: BoxDecoration(
27
+ color: isDark
28
+ ? const Color(0xFF0A0A0A)
29
+ : const Color(0xFFFAFAFA),
30
+ border: Border(
31
+ bottom: BorderSide(
32
+ color: isDark
33
+ ? const Color(0xFF2A2A2A)
34
+ : const Color(0xFFE5E5E5),
35
+ ),
36
+ ),
37
+ ),
38
+ child: Row(
39
+ children: [
40
+ Builder(
41
+ builder: (context) => IconButton(
42
+ icon: const Icon(Icons.menu),
43
+ onPressed: () => Scaffold.of(context).openDrawer(),
44
+ ),
45
+ ),
46
+ const SizedBox(width: 8),
47
+ Text(
48
+ 'Personas',
49
+ style: TextStyle(
50
+ fontSize: 20,
51
+ fontWeight: FontWeight.bold,
52
+ color: isDark ? Colors.white : Colors.black,
53
+ ),
54
+ ),
55
+ ],
56
+ ),
57
+ ),
58
+ Expanded(
59
+ child: Center(
60
+ child: Text(
61
+ 'Persona List - Placeholder\nCustom Personas: $userCreatedCount\nIs Premium: $isPremium',
62
+ ),
63
+ ),
64
+ ),
65
+ ],
66
+ ),
67
+ Positioned(
68
+ bottom: 24,
69
+ right: 24,
70
+ child: FloatingActionButton(
71
+ onPressed: () {
72
+ if (!isPremium && userCreatedCount >= 3) {
73
+ showShadDialog(
74
+ context: context,
75
+ builder: (context) => ShadDialog.alert(
76
+ title: const Text('Upgrade to Premium'),
77
+ description: const Padding(
78
+ padding: EdgeInsets.only(bottom: 8),
79
+ child: Text(
80
+ 'You have reached the limit of 3 custom personas for Basic users. Upgrade to Premium to create unlimited personas!',
81
+ ),
82
+ ),
83
+ actions: [
84
+ ShadButton.outline(
85
+ child: const Text('Cancel'),
86
+ onPressed: () => Navigator.of(context).pop(),
87
+ ),
88
+ ShadButton(
89
+ child: const Text('Upgrade'),
90
+ onPressed: () {
91
+ ref
92
+ .read(isPremiumUserProvider.notifier)
93
+ .togglePremium();
94
+ Navigator.of(context).pop();
95
+ ShadToaster.of(context).show(
96
+ const ShadToast(
97
+ title: Text('Upgraded!'),
98
+ description: Text('You are now a premium user.'),
99
+ ),
100
+ );
101
+ },
102
+ ),
103
+ ],
104
+ ),
105
+ );
106
+ } else {
107
+ context.push(AppRoutes.createPersona);
108
+ }
109
+ },
110
+ child: const Icon(Icons.add),
111
+ ),
112
+ ),
113
+ ],
114
+ );
115
+ }
116
+}
lib/features/servers/data/models/server.dart
new
+111
@@ -0,0 +1,111 @@
1
+import 'package:hive_ce/hive.dart';
2
+import 'package:localmind/core/models/enums.dart';
3
+
4
+part 'server.g.dart';
5
+
6
+@HiveType(typeId: 0)
7
+class Server extends HiveObject {
8
+ @HiveField(0)
9
+ final String id;
10
+
11
+ @HiveField(1)
12
+ final String name;
13
+
14
+ @HiveField(2)
15
+ final ServerType type;
16
+
17
+ @HiveField(3)
18
+ final String host;
19
+
20
+ @HiveField(4)
21
+ final int port;
22
+
23
+ @HiveField(5)
24
+ final String? apiKey;
25
+
26
+ @HiveField(6)
27
+ final bool isDefault;
28
+
29
+ @HiveField(7)
30
+ final DateTime createdAt;
31
+
32
+ @HiveField(8)
33
+ final DateTime lastConnectedAt;
34
+
35
+ @HiveField(9)
36
+ final ConnectionStatus status;
37
+
38
+ @HiveField(10)
39
+ final String? iconName;
40
+
41
+ Server({
42
+ required this.id,
43
+ required this.name,
44
+ required this.type,
45
+ required this.host,
46
+ required this.port,
47
+ this.apiKey,
48
+ this.isDefault = false,
49
+ required this.createdAt,
50
+ required this.lastConnectedAt,
51
+ this.status = ConnectionStatus.disconnected,
52
+ this.iconName,
53
+ });
54
+
55
+ String get baseUrl {
56
+ if (type == ServerType.openRouter) {
57
+ return 'https://openrouter.ai/api/v1';
58
+ }
59
+ return 'http://$host:$port';
60
+ }
61
+
62
+ String get chatEndpoint {
63
+ switch (type) {
64
+ case ServerType.lmStudio:
65
+ return '$baseUrl/v1/chat/completions';
66
+ case ServerType.ollama:
67
+ return '$baseUrl/api/chat';
68
+ case ServerType.openRouter:
69
+ return '$baseUrl/chat/completions';
70
+ }
71
+ }
72
+
73
+ String get modelsEndpoint {
74
+ switch (type) {
75
+ case ServerType.lmStudio:
76
+ return '$baseUrl/v1/models';
77
+ case ServerType.ollama:
78
+ return '$baseUrl/api/tags';
79
+ case ServerType.openRouter:
80
+ return '$baseUrl/models';
81
+ }
82
+ }
83
+
84
+ Server copyWith({
85
+ String? id,
86
+ String? name,
87
+ ServerType? type,
88
+ String? host,
89
+ int? port,
90
+ String? apiKey,
91
+ bool? isDefault,
92
+ DateTime? createdAt,
93
+ DateTime? lastConnectedAt,
94
+ ConnectionStatus? status,
95
+ String? iconName,
96
+ }) {
97
+ return Server(
98
+ id: id ?? this.id,
99
+ name: name ?? this.name,
100
+ type: type ?? this.type,
101
+ host: host ?? this.host,
102
+ port: port ?? this.port,
103
+ apiKey: apiKey ?? this.apiKey,
104
+ isDefault: isDefault ?? this.isDefault,
105
+ createdAt: createdAt ?? this.createdAt,
106
+ lastConnectedAt: lastConnectedAt ?? this.lastConnectedAt,
107
+ status: status ?? this.status,
108
+ iconName: iconName ?? this.iconName,
109
+ );
110
+ }
111
+}
lib/features/servers/data/models/server.g.dart
new
+73
@@ -0,0 +1,73 @@
1
+// GENERATED CODE - DO NOT MODIFY BY HAND
2
+
3
+part of 'server.dart';
4
+
5
+// **************************************************************************
6
+// TypeAdapterGenerator
7
+// **************************************************************************
8
+
9
+class ServerAdapter extends TypeAdapter<Server> {
10
+ @override
11
+ final typeId = 0;
12
+
13
+ @override
14
+ Server read(BinaryReader reader) {
15
+ final numOfFields = reader.readByte();
16
+ final fields = <int, dynamic>{
17
+ for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
18
+ };
19
+ return Server(
20
+ id: fields[0] as String,
21
+ name: fields[1] as String,
22
+ type: fields[2] as ServerType,
23
+ host: fields[3] as String,
24
+ port: (fields[4] as num).toInt(),
25
+ apiKey: fields[5] as String?,
26
+ isDefault: fields[6] == null ? false : fields[6] as bool,
27
+ createdAt: fields[7] as DateTime,
28
+ lastConnectedAt: fields[8] as DateTime,
29
+ status: fields[9] == null
30
+ ? ConnectionStatus.disconnected
31
+ : fields[9] as ConnectionStatus,
32
+ iconName: fields[10] as String?,
33
+ );
34
+ }
35
+
36
+ @override
37
+ void write(BinaryWriter writer, Server obj) {
38
+ writer
39
+ ..writeByte(11)
40
+ ..writeByte(0)
41
+ ..write(obj.id)
42
+ ..writeByte(1)
43
+ ..write(obj.name)
44
+ ..writeByte(2)
45
+ ..write(obj.type)
46
+ ..writeByte(3)
47
+ ..write(obj.host)
48
+ ..writeByte(4)
49
+ ..write(obj.port)
50
+ ..writeByte(5)
51
+ ..write(obj.apiKey)
52
+ ..writeByte(6)
53
+ ..write(obj.isDefault)
54
+ ..writeByte(7)
55
+ ..write(obj.createdAt)
56
+ ..writeByte(8)
57
+ ..write(obj.lastConnectedAt)
58
+ ..writeByte(9)
59
+ ..write(obj.status)
60
+ ..writeByte(10)
61
+ ..write(obj.iconName);
62
+ }
63
+
64
+ @override
65
+ int get hashCode => typeId.hashCode;
66
+
67
+ @override
68
+ bool operator ==(Object other) =>
69
+ identical(this, other) ||
70
+ other is ServerAdapter &&
71
+ runtimeType == other.runtimeType &&
72
+ typeId == other.typeId;
73
+}
lib/features/servers/data/server_api_service.dart
new
+159
@@ -0,0 +1,159 @@
1
+import 'package:dio/dio.dart';
2
+import 'package:localmind/features/servers/data/models/server.dart';
3
+import 'package:localmind/features/models/data/models/model_info.dart';
4
+import 'package:localmind/core/models/enums.dart';
5
+
6
+class ServerApiService {
7
+ final Dio _dio;
8
+
9
+ ServerApiService(this._dio);
10
+
11
+ Future<bool> testConnection(Server server) async {
12
+ try {
13
+ final response = await _dio.get(
14
+ server.modelsEndpoint,
15
+ options: Options(
16
+ headers: _getAuthHeaders(server),
17
+ validateStatus: (status) => status != null && status < 500,
18
+ ),
19
+ );
20
+ return response.statusCode == 200;
21
+ } catch (e) {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ Future<int?> pingServer(Server server) async {
27
+ final stopwatch = Stopwatch()..start();
28
+ try {
29
+ await _dio.head(
30
+ server.baseUrl,
31
+ options: Options(headers: _getAuthHeaders(server)),
32
+ );
33
+ stopwatch.stop();
34
+ return stopwatch.elapsedMilliseconds;
35
+ } catch (e) {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ Future<List<ModelInfo>> fetchModels(Server server) async {
41
+ try {
42
+ final response = await _dio.get(
43
+ server.modelsEndpoint,
44
+ options: Options(headers: _getAuthHeaders(server)),
45
+ );
46
+
47
+ switch (server.type) {
48
+ case ServerType.lmStudio:
49
+ return _parseLMStudioModels(response.data, server);
50
+ case ServerType.ollama:
51
+ return _parseOllamaModels(response.data, server);
52
+ case ServerType.openRouter:
53
+ return _parseOpenRouterModels(response.data, server);
54
+ }
55
+ } catch (e) {
56
+ throw Exception('Failed to fetch models: $e');
57
+ }
58
+ }
59
+
60
+ Map<String, String>? _getAuthHeaders(Server server) {
61
+ if (server.apiKey != null && server.apiKey!.isNotEmpty) {
62
+ return {'Authorization': 'Bearer ${server.apiKey}'};
63
+ }
64
+ return null;
65
+ }
66
+
67
+ List<ModelInfo> _parseLMStudioModels(dynamic data, Server server) {
68
+ final List<ModelInfo> models = [];
69
+ if (data['data'] != null) {
70
+ for (final item in data['data']) {
71
+ final id = item['id'] as String? ?? '';
72
+ models.add(
73
+ ModelInfo(
74
+ id: id,
75
+ name: _formatModelName(id),
76
+ serverType: server.type,
77
+ serverId: server.id,
78
+ parameterCount: _extractParameterCount(id),
79
+ ),
80
+ );
81
+ }
82
+ }
83
+ return models;
84
+ }
85
+
86
+ List<ModelInfo> _parseOllamaModels(dynamic data, Server server) {
87
+ final List<ModelInfo> models = [];
88
+ if (data['models'] != null) {
89
+ for (final item in data['models']) {
90
+ final details = item['details'] ?? {};
91
+ final paramSize = details['parameter_size'] as String? ?? '';
92
+ models.add(
93
+ ModelInfo(
94
+ id: item['name'] ?? '',
95
+ name: _formatModelName(item['name'] ?? ''),
96
+ fileSize: item['size'] as int?,
97
+ parameterCount: _parseParameterSize(paramSize),
98
+ quantization: details['quantization_level'] as String?,
99
+ architecture: details['family'] as String?,
100
+ serverType: server.type,
101
+ serverId: server.id,
102
+ modifiedAt: item['modified_at'] != null
103
+ ? DateTime.tryParse(item['modified_at'])
104
+ : null,
105
+ ),
106
+ );
107
+ }
108
+ }
109
+ return models;
110
+ }
111
+
112
+ List<ModelInfo> _parseOpenRouterModels(dynamic data, Server server) {
113
+ final List<ModelInfo> models = [];
114
+ if (data['data'] != null) {
115
+ for (final item in data['data']) {
116
+ if (item['architecture']?['modality'] == 'text->text') {
117
+ models.add(
118
+ ModelInfo(
119
+ id: item['id'] ?? '',
120
+ name: item['name'] ?? '',
121
+ description: item['description'] as String?,
122
+ contextLength: item['context_length'] as int?,
123
+ architecture: item['architecture']?['tokenizer'] as String?,
124
+ serverType: server.type,
125
+ serverId: server.id,
126
+ ),
127
+ );
128
+ }
129
+ }
130
+ }
131
+ return models;
132
+ }
133
+
134
+ String _formatModelName(String id) {
135
+ return id
136
+ .replaceAll('-', ' ')
137
+ .replaceAll('_', ' ')
138
+ .split(' ')
139
+ .map(
140
+ (w) => w.isNotEmpty ? '${w[0].toUpperCase()}${w.substring(1)}' : '',
141
+ )
142
+ .join(' ');
143
+ }
144
+
145
+ int? _extractParameterCount(String id) {
146
+ final regex = RegExp(r'(\d+(?:\.\d+)?)\s*[Bb]');
147
+ final match = regex.firstMatch(id);
148
+ if (match != null) {
149
+ final value = double.tryParse(match.group(1) ?? '');
150
+ return value?.toInt();
151
+ }
152
+ return null;
153
+ }
154
+
155
+ int? _parseParameterSize(String size) {
156
+ final cleaned = size.replaceAll('B', '').replaceAll('b', '').trim();
157
+ return int.tryParse(cleaned);
158
+ }
159
+}
lib/features/servers/providers/server_providers.dart
new
+159
@@ -0,0 +1,159 @@
1
+import 'package:flutter_riverpod/flutter_riverpod.dart';
2
+import 'package:localmind/features/servers/data/models/server.dart';
3
+import 'package:localmind/core/models/enums.dart';
4
+import 'package:localmind/core/providers/storage_providers.dart';
5
+import 'package:localmind/core/providers/service_providers.dart';
6
+
7
+final serversProvider = NotifierProvider<ServersNotifier, List<Server>>(() {
8
+ return ServersNotifier();
9
+});
10
+
11
+final activeServerProvider = NotifierProvider<ActiveServerNotifier, Server?>(
12
+ () {
13
+ return ActiveServerNotifier();
14
+ },
15
+);
16
+
17
+final connectionStatusProvider =
18
+ NotifierProvider<ConnectionStatusNotifier, ConnectionStatus>(() {
19
+ return ConnectionStatusNotifier();
20
+ });
21
+
22
+final availableModelsProvider = FutureProvider.family<List<dynamic>, String>((
23
+ ref,
24
+ serverId,
25
+) async {
26
+ final servers = ref.watch(serversProvider);
27
+ final server = servers.firstWhere(
28
+ (s) => s.id == serverId,
29
+ orElse: () => throw Exception('Server not found'),
30
+ );
31
+ final apiService = ref.watch(serverApiServiceProvider);
32
+ return await apiService.fetchModels(server);
33
+});
34
+
35
+class ServersNotifier extends Notifier<List<Server>> {
36
+ @override
37
+ List<Server> build() {
38
+ final boxes = ref.watch(hiveBoxesProvider);
39
+ return boxes.servers.values.toList().cast<Server>();
40
+ }
41
+
42
+ Future<void> addServer(Server server) async {
43
+ final boxes = ref.read(hiveBoxesProvider);
44
+ await boxes.servers.put(server.id, server);
45
+ state = boxes.servers.values.toList().cast<Server>();
46
+ }
47
+
48
+ Future<void> updateServer(Server server) async {
49
+ final boxes = ref.read(hiveBoxesProvider);
50
+ await boxes.servers.put(server.id, server);
51
+ state = boxes.servers.values.toList().cast<Server>();
52
+ }
53
+
54
+ Future<void> deleteServer(String serverId) async {
55
+ final boxes = ref.read(hiveBoxesProvider);
56
+ await boxes.servers.delete(serverId);
57
+ state = boxes.servers.values.toList().cast<Server>();
58
+ }
59
+
60
+ Future<void> setDefault(String serverId) async {
61
+ final boxes = ref.read(hiveBoxesProvider);
62
+ final updatedServers = state.map((s) {
63
+ return s.copyWith(isDefault: s.id == serverId);
64
+ }).toList();
65
+
66
+ for (final server in updatedServers) {
67
+ await boxes.servers.put(server.id, server);
68
+ }
69
+ state = boxes.servers.values.toList().cast<Server>();
70
+ }
71
+
72
+ Future<ConnectionStatus> testConnection(
73
+ String serverId,
74
+ dynamic apiService,
75
+ ) async {
76
+ final server = state.firstWhere((s) => s.id == serverId);
77
+ final isConnected = await apiService.testConnection(server);
78
+ final status = isConnected
79
+ ? ConnectionStatus.connected
80
+ : ConnectionStatus.error;
81
+
82
+ final updatedServer = server.copyWith(
83
+ status: status,
84
+ lastConnectedAt: DateTime.now(),
85
+ );
86
+ final boxes = ref.read(hiveBoxesProvider);
87
+ await boxes.servers.put(serverId, updatedServer);
88
+ state = boxes.servers.values.toList().cast<Server>();
89
+ return status;
90
+ }
91
+}
92
+
93
+class ActiveServerNotifier extends Notifier<Server?> {
94
+ @override
95
+ Server? build() {
96
+ final servers = ref.watch(serversProvider);
97
+ final boxes = ref.watch(hiveBoxesProvider);
98
+ final defaultServerIdObj = boxes.settings.get('defaultServerId');
99
+
100
+ String? defaultServerId;
101
+ if (defaultServerIdObj is String && defaultServerIdObj.isNotEmpty) {
102
+ defaultServerId = defaultServerIdObj;
103
+ }
104
+
105
+ if (defaultServerId != null) {
106
+ final matchingServer = servers.where((s) => s.id == defaultServerId);
107
+ if (matchingServer.isNotEmpty) {
108
+ return matchingServer.first;
109
+ }
110
+ }
111
+
112
+ if (servers.isNotEmpty) {
113
+ final defaultServer = servers.where((s) => s.isDefault);
114
+ return defaultServer.isNotEmpty ? defaultServer.first : servers.first;
115
+ }
116
+ return null;
117
+ }
118
+
119
+ void setActiveServer(Server? server) {
120
+ final boxes = ref.read(hiveBoxesProvider);
121
+ state = server;
122
+ if (server != null) {
123
+ boxes.settings.put('defaultServerId', server.id);
124
+ }
125
+ }
126
+}
127
+
128
+class ConnectionStatusNotifier extends Notifier<ConnectionStatus> {
129
+ @override
130
+ ConnectionStatus build() {
131
+ final activeServer = ref.watch(activeServerProvider);
132
+ final apiService = ref.watch(serverApiServiceProvider);
133
+
134
+ if (activeServer == null) {
135
+ return ConnectionStatus.disconnected;
136
+ }
137
+
138
+ _checkConnection(activeServer, apiService);
139
+ return ConnectionStatus.checking;
140
+ }
141
+
142
+ Future<void> _checkConnection(Server server, dynamic apiService) async {
143
+ try {
144
+ final isConnected = await apiService.testConnection(server);
145
+ state = isConnected ? ConnectionStatus.connected : ConnectionStatus.error;
146
+ } catch (e) {
147
+ state = ConnectionStatus.error;
148
+ }
149
+ }
150
+
151
+ Future<void> refresh() async {
152
+ final activeServer = ref.read(activeServerProvider);
153
+ final apiService = ref.read(serverApiServiceProvider);
154
+ if (activeServer != null) {
155
+ state = ConnectionStatus.checking;
156
+ await _checkConnection(activeServer, apiService);
157
+ }
158
+ }
159
+}
lib/features/servers/screens/add_server_screen.dart
new
+432
@@ -0,0 +1,432 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+import 'package:go_router/go_router.dart';
4
+import 'package:shadcn_ui/shadcn_ui.dart';
5
+import '../../../core/constants/app_constants.dart';
6
+import 'package:localmind/features/servers/data/models/server.dart';
7
+import '../../../core/models/enums.dart';
8
+import '../../../core/providers/service_providers.dart';
9
+import '../providers/server_providers.dart';
10
+import '../widgets/server_icon_picker.dart';
11
+import '../widgets/server_type_selector.dart';
12
+
13
+class AddServerScreen extends ConsumerStatefulWidget {
14
+ final Server? editServer;
15
+
16
+ const AddServerScreen({super.key, this.editServer});
17
+
18
+ @override
19
+ ConsumerState<AddServerScreen> createState() => _AddServerScreenState();
20
+}
21
+
22
+class _AddServerScreenState extends ConsumerState<AddServerScreen> {
23
+ final _formKey = GlobalKey<FormState>();
24
+ late TextEditingController _nameController;
25
+ late TextEditingController _hostController;
26
+ late TextEditingController _portController;
27
+ late TextEditingController _apiKeyController;
28
+
29
+ late ServerType _selectedType;
30
+ String? _selectedIconName;
31
+ bool _isTesting = false;
32
+ bool _isSaving = false;
33
+ String? _testResult;
34
+
35
+ bool get _isEditing => widget.editServer != null;
36
+
37
+ @override
38
+ void initState() {
39
+ super.initState();
40
+ final server = widget.editServer;
41
+ _selectedType = server?.type ?? ServerType.lmStudio;
42
+ _selectedIconName = server?.iconName;
43
+ _nameController = TextEditingController(text: server?.name ?? '');
44
+ _hostController = TextEditingController(text: server?.host ?? '');
45
+ _portController = TextEditingController(
46
+ text:
47
+ server?.port.toString() ??
48
+ AppConstants.lmStudioDefaultPort.toString(),
49
+ );
50
+ _apiKeyController = TextEditingController(text: server?.apiKey ?? '');
51
+ }
52
+
53
+ @override
54
+ void dispose() {
55
+ _nameController.dispose();
56
+ _hostController.dispose();
57
+ _portController.dispose();
58
+ _apiKeyController.dispose();
59
+ super.dispose();
60
+ }
61
+
62
+ void _onTypeChanged(ServerType type) {
63
+ setState(() {
64
+ _selectedType = type;
65
+ _testResult = null;
66
+
67
+ if (type == ServerType.openRouter) {
68
+ _portController.text = '443';
69
+ } else if (type == ServerType.lmStudio) {
70
+ _portController.text = AppConstants.lmStudioDefaultPort.toString();
71
+ } else if (type == ServerType.ollama) {
72
+ _portController.text = AppConstants.ollamaDefaultPort.toString();
73
+ }
74
+ });
75
+ }
76
+
77
+ Future<void> _testConnection() async {
78
+ if (!_formKey.currentState!.validate()) return;
79
+
80
+ setState(() {
81
+ _isTesting = true;
82
+ _testResult = null;
83
+ });
84
+
85
+ final apiService = ref.read(serverApiServiceProvider);
86
+ final testServer = _buildServer();
87
+
88
+ try {
89
+ final isConnected = await apiService.testConnection(testServer);
90
+ setState(() {
91
+ _testResult = isConnected
92
+ ? 'Connection successful!'
93
+ : 'Connection failed. Check your settings.';
94
+ });
95
+ } catch (e) {
96
+ setState(() {
97
+ _testResult = 'Error: ${e.toString()}';
98
+ });
99
+ } finally {
100
+ setState(() {
101
+ _isTesting = false;
102
+ });
103
+ }
104
+ }
105
+
106
+ Server _buildServer() {
107
+ return Server(
108
+ id:
109
+ widget.editServer?.id ??
110
+ DateTime.now().millisecondsSinceEpoch.toString(),
111
+ name: _nameController.text.trim(),
112
+ type: _selectedType,
113
+ host: _hostController.text.trim(),
114
+ port: int.parse(_portController.text.trim()),
115
+ apiKey: _selectedType == ServerType.openRouter
116
+ ? _apiKeyController.text.trim()
117
+ : (_apiKeyController.text.trim().isNotEmpty
118
+ ? _apiKeyController.text.trim()
119
+ : null),
120
+ isDefault: widget.editServer?.isDefault ?? false,
121
+ createdAt: widget.editServer?.createdAt ?? DateTime.now(),
122
+ lastConnectedAt: widget.editServer?.lastConnectedAt ?? DateTime.now(),
123
+ status: ConnectionStatus.disconnected,
124
+ iconName: _selectedIconName,
125
+ );
126
+ }
127
+
128
+ Future<void> _saveServer() async {
129
+ if (!_formKey.currentState!.validate()) return;
130
+
131
+ setState(() {
132
+ _isSaving = true;
133
+ });
134
+
135
+ try {
136
+ final server = _buildServer();
137
+
138
+ if (_isEditing) {
139
+ await ref.read(serversProvider.notifier).updateServer(server);
140
+ } else {
141
+ await ref.read(serversProvider.notifier).addServer(server);
142
+ }
143
+
144
+ if (mounted) {
145
+ context.pop();
146
+ ScaffoldMessenger.of(context).showSnackBar(
147
+ SnackBar(
148
+ content: Text(_isEditing ? 'Server updated' : 'Server added'),
149
+ duration: const Duration(seconds: 2),
150
+ ),
151
+ );
152
+ }
153
+ } catch (e) {
154
+ if (mounted) {
155
+ ScaffoldMessenger.of(context).showSnackBar(
156
+ SnackBar(
157
+ content: Text('Error: ${e.toString()}'),
158
+ backgroundColor: Colors.red,
159
+ ),
160
+ );
161
+ }
162
+ } finally {
163
+ if (mounted) {
164
+ setState(() {
165
+ _isSaving = false;
166
+ });
167
+ }
168
+ }
169
+ }
170
+
171
+ String? _validateName(String? value) {
172
+ if (value == null || value.trim().isEmpty) {
173
+ return 'Name is required';
174
+ }
175
+ if (value.trim().length > 50) {
176
+ return 'Name must be 50 characters or less';
177
+ }
178
+ return null;
179
+ }
180
+
181
+ String? _validateHost(String? value) {
182
+ if (_selectedType == ServerType.openRouter) return null;
183
+
184
+ if (value == null || value.trim().isEmpty) {
185
+ return 'Host is required';
186
+ }
187
+ final hostPattern = RegExp(r'^[a-zA-Z0-9]([a-zA-Z0-9\-\.]*[a-zA-Z0-9])?$');
188
+ if (!hostPattern.hasMatch(value.trim())) {
189
+ return 'Enter a valid hostname or IP address';
190
+ }
191
+ return null;
192
+ }
193
+
194
+ String? _validatePort(String? value) {
195
+ if (_selectedType == ServerType.openRouter) return null;
196
+
197
+ if (value == null || value.trim().isEmpty) {
198
+ return 'Port is required';
199
+ }
200
+ final port = int.tryParse(value.trim());
201
+ if (port == null || port < 1 || port > 65535) {
202
+ return 'Enter a valid port (1-65535)';
203
+ }
204
+ return null;
205
+ }
206
+
207
+ String? _validateApiKey(String? value) {
208
+ if (_selectedType == ServerType.openRouter) {
209
+ if (value == null || value.trim().isEmpty) {
210
+ return 'API key is required for OpenRouter';
211
+ }
212
+ if (!value.trim().startsWith('sk-')) {
213
+ return 'OpenRouter API keys start with sk-';
214
+ }
215
+ }
216
+ return null;
217
+ }
218
+
219
+ void _showIconPicker() {
220
+ showShadDialog(
221
+ context: context,
222
+ builder: (context) => ServerIconPicker(
223
+ selectedIconName: _selectedIconName,
224
+ onIconSelected: (iconName) {
225
+ setState(() {
226
+ _selectedIconName = iconName;
227
+ });
228
+ },
229
+ ),
230
+ );
231
+ }
232
+
233
+ @override
234
+ Widget build(BuildContext context) {
235
+ final theme = Theme.of(context);
236
+
237
+ return Scaffold(
238
+ appBar: AppBar(
239
+ title: Text(_isEditing ? 'Edit Server' : 'Add Server'),
240
+ leading: IconButton(
241
+ icon: const Icon(Icons.close),
242
+ onPressed: () => context.pop(),
243
+ ),
244
+ ),
245
+ body: Form(
246
+ key: _formKey,
247
+ child: ListView(
248
+ padding: const EdgeInsets.all(16),
249
+ children: [
250
+ Text('Server Type', style: theme.textTheme.titleSmall),
251
+ const SizedBox(height: 8),
252
+ ServerTypeSelector(
253
+ selectedType: _selectedType,
254
+ onChanged: _onTypeChanged,
255
+ ),
256
+ const SizedBox(height: 24),
257
+
258
+ Text('Server Icon', style: theme.textTheme.titleSmall),
259
+ const SizedBox(height: 8),
260
+ GestureDetector(
261
+ onTap: _showIconPicker,
262
+ child: Container(
263
+ padding: const EdgeInsets.all(12),
264
+ decoration: BoxDecoration(
265
+ border: Border.all(color: theme.colorScheme.outline),
266
+ borderRadius: BorderRadius.circular(8),
267
+ ),
268
+ child: Row(
269
+ children: [
270
+ Container(
271
+ width: 40,
272
+ height: 40,
273
+ decoration: BoxDecoration(
274
+ color: theme.colorScheme.primaryContainer,
275
+ borderRadius: BorderRadius.circular(8),
276
+ ),
277
+ child: HugeIcon(
278
+ _selectedIconName != null
279
+ ? (getHugeIconByName(_selectedIconName)?.iconData ??
280
+ getDefaultServerIcon(
281
+ _selectedType.name,
282
+ )!.iconData)
283
+ : getDefaultServerIcon(
284
+ _selectedType.name,
285
+ )!.iconData,
286
+ size: 24,
287
+ color: theme.colorScheme.primary,
288
+ ),
289
+ ),
290
+ const SizedBox(width: 12),
291
+ Expanded(
292
+ child: Text(
293
+ _selectedIconName ?? 'Default icon',
294
+ style: theme.textTheme.bodyMedium,
295
+ ),
296
+ ),
297
+ Icon(
298
+ Icons.edit,
299
+ size: 18,
300
+ color: theme.colorScheme.onSurfaceVariant,
301
+ ),
302
+ ],
303
+ ),
304
+ ),
305
+ ),
306
+ const SizedBox(height: 24),
307
+
308
+ TextFormField(
309
+ controller: _nameController,
310
+ decoration: const InputDecoration(
311
+ labelText: 'Name',
312
+ hintText: 'My Server',
313
+ ),
314
+ validator: _validateName,
315
+ textInputAction: TextInputAction.next,
316
+ ),
317
+ const SizedBox(height: 16),
318
+
319
+ if (_selectedType != ServerType.openRouter) ...[
320
+ TextFormField(
321
+ controller: _hostController,
322
+ decoration: const InputDecoration(
323
+ labelText: 'Host / IP Address',
324
+ hintText: '192.168.1.100',
325
+ ),
326
+ validator: _validateHost,
327
+ keyboardType: TextInputType.text,
328
+ textInputAction: TextInputAction.next,
329
+ ),
330
+ const SizedBox(height: 16),
331
+
332
+ TextFormField(
333
+ controller: _portController,
334
+ decoration: InputDecoration(
335
+ labelText: 'Port',
336
+ hintText: _selectedType == ServerType.lmStudio
337
+ ? AppConstants.lmStudioDefaultPort.toString()
338
+ : AppConstants.ollamaDefaultPort.toString(),
339
+ ),
340
+ validator: _validatePort,
341
+ keyboardType: TextInputType.number,
342
+ textInputAction: TextInputAction.next,
343
+ ),
344
+ const SizedBox(height: 16),
345
+ ],
346
+
347
+ TextFormField(
348
+ controller: _apiKeyController,
349
+ decoration: InputDecoration(
350
+ labelText: _selectedType == ServerType.openRouter
351
+ ? 'API Key *'
352
+ : 'API Key (optional)',
353
+ hintText: _selectedType == ServerType.openRouter
354
+ ? 'sk-...'
355
+ : 'For authenticated servers',
356
+ ),
357
+ validator: _validateApiKey,
358
+ obscureText: true,
359
+ textInputAction: TextInputAction.done,
360
+ ),
361
+ const SizedBox(height: 24),
362
+
363
+ if (_testResult != null) ...[
364
+ Container(
365
+ padding: const EdgeInsets.all(12),
366
+ decoration: BoxDecoration(
367
+ color: _testResult!.contains('successful')
368
+ ? Colors.green.withAlpha(25)
369
+ : Colors.red.withAlpha(25),
370
+ borderRadius: BorderRadius.circular(8),
371
+ border: Border.all(
372
+ color: _testResult!.contains('successful')
373
+ ? Colors.green
374
+ : Colors.red,
375
+ ),
376
+ ),
377
+ child: Row(
378
+ children: [
379
+ Icon(
380
+ _testResult!.contains('successful')
381
+ ? Icons.check_circle
382
+ : Icons.error,
383
+ color: _testResult!.contains('successful')
384
+ ? Colors.green
385
+ : Colors.red,
386
+ ),
387
+ const SizedBox(width: 8),
388
+ Expanded(
389
+ child: Text(
390
+ _testResult!,
391
+ style: TextStyle(
392
+ color: _testResult!.contains('successful')
393
+ ? Colors.green
394
+ : Colors.red,
395
+ ),
396
+ ),
397
+ ),
398
+ ],
399
+ ),
400
+ ),
401
+ const SizedBox(height: 16),
402
+ ],
403
+
404
+ OutlinedButton.icon(
405
+ onPressed: _isTesting ? null : _testConnection,
406
+ icon: _isTesting
407
+ ? const SizedBox(
408
+ width: 16,
409
+ height: 16,
410
+ child: CircularProgressIndicator(strokeWidth: 2),
411
+ )
412
+ : const Icon(Icons.network_check),
413
+ label: Text(_isTesting ? 'Testing...' : 'Test Connection'),
414
+ ),
415
+ const SizedBox(height: 24),
416
+
417
+ ElevatedButton(
418
+ onPressed: _isSaving ? null : _saveServer,
419
+ child: _isSaving
420
+ ? const SizedBox(
421
+ width: 20,
422
+ height: 20,
423
+ child: CircularProgressIndicator(strokeWidth: 2),
424
+ )
425
+ : Text(_isEditing ? 'Update Server' : 'Save Server'),
426
+ ),
427
+ ],
428
+ ),
429
+ ),
430
+ );
431
+ }
432
+}
lib/features/servers/screens/server_list_screen.dart
new
+184
@@ -0,0 +1,184 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+import 'package:go_router/go_router.dart';
4
+import '../../../core/routes/app_routes.dart';
5
+import '../../../core/providers/service_providers.dart';
6
+import '../providers/server_providers.dart';
7
+import '../widgets/server_card.dart';
8
+
9
+class ServerListScreen extends ConsumerWidget {
10
+ const ServerListScreen({super.key});
11
+
12
+ @override
13
+ Widget build(BuildContext context, WidgetRef ref) {
14
+ final servers = ref.watch(serversProvider);
15
+ final activeServer = ref.watch(activeServerProvider);
16
+ final theme = Theme.of(context);
17
+ final isDark = theme.brightness == Brightness.dark;
18
+
19
+ return Stack(
20
+ children: [
21
+ Column(
22
+ children: [
23
+ Container(
24
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
25
+ decoration: BoxDecoration(
26
+ color: isDark
27
+ ? const Color(0xFF0A0A0A)
28
+ : const Color(0xFFFAFAFA),
29
+ border: Border(
30
+ bottom: BorderSide(
31
+ color: isDark
32
+ ? const Color(0xFF2A2A2A)
33
+ : const Color(0xFFE5E5E5),
34
+ ),
35
+ ),
36
+ ),
37
+ child: Row(
38
+ children: [
39
+ Builder(
40
+ builder: (context) => IconButton(
41
+ icon: const Icon(Icons.menu),
42
+ onPressed: () => Scaffold.of(context).openDrawer(),
43
+ ),
44
+ ),
45
+ const SizedBox(width: 8),
46
+ Text(
47
+ 'Servers',
48
+ style: TextStyle(
49
+ fontSize: 20,
50
+ fontWeight: FontWeight.bold,
51
+ color: isDark ? Colors.white : Colors.black,
52
+ ),
53
+ ),
54
+ ],
55
+ ),
56
+ ),
57
+ Expanded(
58
+ child: servers.isEmpty
59
+ ? _buildEmptyState(context, isDark, theme)
60
+ : RefreshIndicator(
61
+ onRefresh: () async {
62
+ for (final server in servers) {
63
+ await ref
64
+ .read(serversProvider.notifier)
65
+ .testConnection(
66
+ server.id,
67
+ ref.read(serverApiServiceProvider),
68
+ );
69
+ }
70
+ },
71
+ child: ListView.builder(
72
+ padding: const EdgeInsets.all(16),
73
+ itemCount: servers.length,
74
+ itemBuilder: (context, index) {
75
+ final server = servers[index];
76
+ return Padding(
77
+ padding: const EdgeInsets.only(bottom: 12),
78
+ child: ServerCard(
79
+ server: server,
80
+ isActive: activeServer?.id == server.id,
81
+ onTap: () {
82
+ ref
83
+ .read(activeServerProvider.notifier)
84
+ .setActiveServer(server);
85
+ ScaffoldMessenger.of(context).showSnackBar(
86
+ SnackBar(
87
+ content: Text('Switched to ${server.name}'),
88
+ duration: const Duration(seconds: 2),
89
+ ),
90
+ );
91
+ },
92
+ onEdit: () =>
93
+ _showEditDialog(context, ref, server),
94
+ onDelete: () =>
95
+ _showDeleteConfirmation(context, ref, server),
96
+ onSetDefault: () {
97
+ ref
98
+ .read(serversProvider.notifier)
99
+ .setDefault(server.id);
100
+ },
101
+ ),
102
+ );
103
+ },
104
+ ),
105
+ ),
106
+ ),
107
+ ],
108
+ ),
109
+ Positioned(
110
+ bottom: 24,
111
+ right: 24,
112
+ child: FloatingActionButton(
113
+ onPressed: () => context.push(AppRoutes.addServer),
114
+ child: const Icon(Icons.add),
115
+ ),
116
+ ),
117
+ ],
118
+ );
119
+ }
120
+
121
+ Widget _buildEmptyState(BuildContext context, bool isDark, ThemeData theme) {
122
+ return Center(
123
+ child: Padding(
124
+ padding: const EdgeInsets.all(32),
125
+ child: Column(
126
+ mainAxisAlignment: MainAxisAlignment.center,
127
+ children: [
128
+ Icon(Icons.computer, size: 80, color: theme.colorScheme.outline),
129
+ const SizedBox(height: 24),
130
+ Text('No Servers Yet', style: theme.textTheme.headlineSmall),
131
+ const SizedBox(height: 8),
132
+ Text(
133
+ 'Add your first server to start chatting with AI models.',
134
+ style: theme.textTheme.bodyMedium?.copyWith(
135
+ color: theme.colorScheme.outline,
136
+ ),
137
+ textAlign: TextAlign.center,
138
+ ),
139
+ const SizedBox(height: 24),
140
+ ElevatedButton.icon(
141
+ onPressed: () => context.push(AppRoutes.addServer),
142
+ icon: const Icon(Icons.add),
143
+ label: const Text('Add Server'),
144
+ ),
145
+ ],
146
+ ),
147
+ ),
148
+ );
149
+ }
150
+
151
+ void _showEditDialog(BuildContext context, WidgetRef ref, dynamic server) {
152
+ context.push(AppRoutes.addServer, extra: server);
153
+ }
154
+
155
+ void _showDeleteConfirmation(
156
+ BuildContext context,
157
+ WidgetRef ref,
158
+ dynamic server,
159
+ ) {
160
+ showDialog(
161
+ context: context,
162
+ builder: (context) => AlertDialog(
163
+ title: const Text('Delete Server'),
164
+ content: Text(
165
+ 'Are you sure you want to delete "${server.name}"? This cannot be undone.',
166
+ ),
167
+ actions: [
168
+ TextButton(
169
+ onPressed: () => Navigator.pop(context),
170
+ child: const Text('Cancel'),
171
+ ),
172
+ TextButton(
173
+ onPressed: () {
174
+ ref.read(serversProvider.notifier).deleteServer(server.id);
175
+ Navigator.pop(context);
176
+ },
177
+ style: TextButton.styleFrom(foregroundColor: Colors.red),
178
+ child: const Text('Delete'),
179
+ ),
180
+ ],
181
+ ),
182
+ );
183
+ }
184
+}
lib/features/servers/widgets/connection_status_indicator.dart
new
+96
@@ -0,0 +1,96 @@
1
+import 'package:flutter/material.dart';
2
+import '../../../core/models/enums.dart';
3
+
4
+class ConnectionStatusIndicator extends StatefulWidget {
5
+ final ConnectionStatus status;
6
+ final double size;
7
+
8
+ const ConnectionStatusIndicator({
9
+ super.key,
10
+ required this.status,
11
+ this.size = 12,
12
+ });
13
+
14
+ @override
15
+ State<ConnectionStatusIndicator> createState() => _ConnectionStatusIndicatorState();
16
+}
17
+
18
+class _ConnectionStatusIndicatorState extends State<ConnectionStatusIndicator>
19
+ with SingleTickerProviderStateMixin {
20
+ late AnimationController _controller;
21
+ late Animation<double> _animation;
22
+
23
+ @override
24
+ void initState() {
25
+ super.initState();
26
+ _controller = AnimationController(
27
+ duration: const Duration(milliseconds: 1000),
28
+ vsync: this,
29
+ );
30
+ _animation = Tween<double>(begin: 0.4, end: 1.0).animate(
31
+ CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
32
+ );
33
+
34
+ if (widget.status == ConnectionStatus.connected) {
35
+ _controller.repeat(reverse: true);
36
+ }
37
+ }
38
+
39
+ @override
40
+ void didUpdateWidget(ConnectionStatusIndicator oldWidget) {
41
+ super.didUpdateWidget(oldWidget);
42
+ if (widget.status == ConnectionStatus.connected) {
43
+ _controller.repeat(reverse: true);
44
+ } else {
45
+ _controller.stop();
46
+ _controller.value = 1.0;
47
+ }
48
+ }
49
+
50
+ @override
51
+ void dispose() {
52
+ _controller.dispose();
53
+ super.dispose();
54
+ }
55
+
56
+ Color get _color {
57
+ switch (widget.status) {
58
+ case ConnectionStatus.connected:
59
+ return const Color(0xFF22C55E);
60
+ case ConnectionStatus.error:
61
+ return const Color(0xFFEF4444);
62
+ case ConnectionStatus.checking:
63
+ return const Color(0xFFF59E0B);
64
+ case ConnectionStatus.disconnected:
65
+ return const Color(0xFF71717A);
66
+ }
67
+ }
68
+
69
+ @override
70
+ Widget build(BuildContext context) {
71
+ if (widget.status == ConnectionStatus.checking) {
72
+ return SizedBox(
73
+ width: widget.size,
74
+ height: widget.size,
75
+ child: CircularProgressIndicator(
76
+ strokeWidth: 2,
77
+ color: _color,
78
+ ),
79
+ );
80
+ }
81
+
82
+ return AnimatedBuilder(
83
+ animation: _animation,
84
+ builder: (context, child) {
85
+ return Container(
86
+ width: widget.size,
87
+ height: widget.size,
88
+ decoration: BoxDecoration(
89
+ shape: BoxShape.circle,
90
+ color: _color.withValues(alpha: widget.status == ConnectionStatus.connected ? _animation.value : 1.0),
91
+ ),
92
+ );
93
+ },
94
+ );
95
+ }
96
+}
lib/features/servers/widgets/server_card.dart
new
+184
@@ -0,0 +1,184 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:localmind/features/servers/data/models/server.dart';
3
+import '../../../core/models/enums.dart';
4
+import 'connection_status_indicator.dart';
5
+import 'server_icon_picker.dart';
6
+
7
+class ServerCard extends StatelessWidget {
8
+ final Server server;
9
+ final bool isActive;
10
+ final VoidCallback? onTap;
11
+ final VoidCallback? onEdit;
12
+ final VoidCallback? onDelete;
13
+ final VoidCallback? onSetDefault;
14
+
15
+ const ServerCard({
16
+ super.key,
17
+ required this.server,
18
+ this.isActive = false,
19
+ this.onTap,
20
+ this.onEdit,
21
+ this.onDelete,
22
+ this.onSetDefault,
23
+ });
24
+
25
+ HugeIconData? get _serverIconData {
26
+ if (server.iconName != null) {
27
+ return getHugeIconByName(server.iconName);
28
+ }
29
+ return getDefaultServerIcon(server.type.name);
30
+ }
31
+
32
+ String get _serverTypeName {
33
+ switch (server.type) {
34
+ case ServerType.lmStudio:
35
+ return 'LM Studio';
36
+ case ServerType.ollama:
37
+ return 'Ollama';
38
+ case ServerType.openRouter:
39
+ return 'OpenRouter';
40
+ }
41
+ }
42
+
43
+ String get _serverAddress {
44
+ if (server.type == ServerType.openRouter) {
45
+ return 'openrouter.ai';
46
+ }
47
+ return '${server.host}:${server.port}';
48
+ }
49
+
50
+ @override
51
+ Widget build(BuildContext context) {
52
+ final theme = Theme.of(context);
53
+
54
+ return Card(
55
+ color: isActive
56
+ ? theme.colorScheme.primaryContainer.withValues(alpha: 0.3)
57
+ : null,
58
+ child: InkWell(
59
+ onTap: onTap,
60
+ borderRadius: BorderRadius.circular(8),
61
+ child: Padding(
62
+ padding: const EdgeInsets.all(16),
63
+ child: Row(
64
+ children: [
65
+ Container(
66
+ width: 48,
67
+ height: 48,
68
+ decoration: BoxDecoration(
69
+ color: theme.colorScheme.primaryContainer,
70
+ borderRadius: BorderRadius.circular(8),
71
+ ),
72
+ child: _serverIconData != null
73
+ ? HugeIcon(
74
+ _serverIconData!.iconData,
75
+ size: 24,
76
+ color: theme.colorScheme.primary,
77
+ )
78
+ : Icon(Icons.dns, color: theme.colorScheme.primary),
79
+ ),
80
+ const SizedBox(width: 16),
81
+ Expanded(
82
+ child: Column(
83
+ crossAxisAlignment: CrossAxisAlignment.start,
84
+ children: [
85
+ Row(
86
+ children: [
87
+ Expanded(
88
+ child: Text(
89
+ server.name,
90
+ style: theme.textTheme.titleMedium?.copyWith(
91
+ fontWeight: FontWeight.w600,
92
+ ),
93
+ overflow: TextOverflow.ellipsis,
94
+ ),
95
+ ),
96
+ if (server.isDefault)
97
+ Container(
98
+ padding: const EdgeInsets.symmetric(
99
+ horizontal: 8,
100
+ vertical: 2,
101
+ ),
102
+ decoration: BoxDecoration(
103
+ color: theme.colorScheme.primary,
104
+ borderRadius: BorderRadius.circular(4),
105
+ ),
106
+ child: Text(
107
+ 'Default',
108
+ style: theme.textTheme.labelSmall?.copyWith(
109
+ color: theme.colorScheme.onPrimary,
110
+ ),
111
+ ),
112
+ ),
113
+ ],
114
+ ),
115
+ const SizedBox(height: 4),
116
+ Row(
117
+ children: [
118
+ Text(_serverTypeName, style: theme.textTheme.bodySmall),
119
+ const SizedBox(width: 8),
120
+ Text('•', style: theme.textTheme.bodySmall),
121
+ const SizedBox(width: 8),
122
+ Text(_serverAddress, style: theme.textTheme.bodySmall),
123
+ ],
124
+ ),
125
+ ],
126
+ ),
127
+ ),
128
+ const SizedBox(width: 12),
129
+ ConnectionStatusIndicator(status: server.status),
130
+ PopupMenuButton<String>(
131
+ onSelected: (value) {
132
+ switch (value) {
133
+ case 'edit':
134
+ onEdit?.call();
135
+ break;
136
+ case 'delete':
137
+ onDelete?.call();
138
+ break;
139
+ case 'setDefault':
140
+ onSetDefault?.call();
141
+ break;
142
+ }
143
+ },
144
+ itemBuilder: (context) => [
145
+ const PopupMenuItem(
146
+ value: 'edit',
147
+ child: Row(
148
+ children: [
149
+ Icon(Icons.edit, size: 18),
150
+ SizedBox(width: 8),
151
+ Text('Edit'),
152
+ ],
153
+ ),
154
+ ),
155
+ if (!server.isDefault)
156
+ const PopupMenuItem(
157
+ value: 'setDefault',
158
+ child: Row(
159
+ children: [
160
+ Icon(Icons.star, size: 18),
161
+ SizedBox(width: 8),
162
+ Text('Set as Default'),
163
+ ],
164
+ ),
165
+ ),
166
+ const PopupMenuItem(
167
+ value: 'delete',
168
+ child: Row(
169
+ children: [
170
+ Icon(Icons.delete, size: 18, color: Colors.red),
171
+ SizedBox(width: 8),
172
+ Text('Delete', style: TextStyle(color: Colors.red)),
173
+ ],
174
+ ),
175
+ ),
176
+ ],
177
+ ),
178
+ ],
179
+ ),
180
+ ),
181
+ ),
182
+ );
183
+ }
184
+}
lib/features/servers/widgets/server_icon_picker.dart
new
+424
@@ -0,0 +1,424 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:hugeicons/hugeicons.dart';
3
+import 'package:hugeicons/styles/stroke_rounded.dart';
4
+import 'package:shadcn_ui/shadcn_ui.dart';
5
+
6
+class HugeIconData {
7
+ final String name;
8
+ final List<List<dynamic>> iconData;
9
+
10
+ const HugeIconData(this.name, this.iconData);
11
+}
12
+
13
+const List<HugeIconData> serverIcons = [
14
+ HugeIconData('Server Stack', HugeIconsStrokeRounded.serverStack01),
15
+ HugeIconData('Server Stack 02', HugeIconsStrokeRounded.serverStack02),
16
+ HugeIconData('Server Stack 03', HugeIconsStrokeRounded.serverStack03),
17
+ HugeIconData('Cloud', HugeIconsStrokeRounded.cloud),
18
+ HugeIconData('Cloud Server', HugeIconsStrokeRounded.cloudServer),
19
+ HugeIconData('MCP Server', HugeIconsStrokeRounded.mcpServer),
20
+ HugeIconData('Database', HugeIconsStrokeRounded.database),
21
+ HugeIconData('Database 01', HugeIconsStrokeRounded.database01),
22
+ HugeIconData('Database 02', HugeIconsStrokeRounded.database02),
23
+ HugeIconData('CPU', HugeIconsStrokeRounded.cpu),
24
+ HugeIconData('Chip', HugeIconsStrokeRounded.chip),
25
+ HugeIconData('Chip 02', HugeIconsStrokeRounded.chip02),
26
+ HugeIconData('Computer', HugeIconsStrokeRounded.computer),
27
+ HugeIconData('Laptop', HugeIconsStrokeRounded.laptop),
28
+ HugeIconData('Computer Terminal', HugeIconsStrokeRounded.computerTerminal01),
29
+ HugeIconData('Code', HugeIconsStrokeRounded.code),
30
+ HugeIconData('AI Brain', HugeIconsStrokeRounded.aiBrain01),
31
+ HugeIconData('AI Brain 02', HugeIconsStrokeRounded.aiBrain02),
32
+ HugeIconData('AI Cloud', HugeIconsStrokeRounded.aiCloud),
33
+ HugeIconData('AI Network', HugeIconsStrokeRounded.aiNetwork),
34
+ HugeIconData('AI Chat', HugeIconsStrokeRounded.aiChat01),
35
+ HugeIconData('Cellular Network', HugeIconsStrokeRounded.cellularNetwork),
36
+ HugeIconData('Plug 01', HugeIconsStrokeRounded.plug01),
37
+ HugeIconData('Plug 02', HugeIconsStrokeRounded.plug02),
38
+ HugeIconData('Bot', HugeIconsStrokeRounded.robot01),
39
+ HugeIconData('Bot 02', HugeIconsStrokeRounded.robot02),
40
+ HugeIconData('Robotic', HugeIconsStrokeRounded.robotic),
41
+ HugeIconData('Rocket', HugeIconsStrokeRounded.rocket),
42
+ HugeIconData('Star', HugeIconsStrokeRounded.star),
43
+ HugeIconData('Settings 01', HugeIconsStrokeRounded.settings01),
44
+ HugeIconData('Settings 02', HugeIconsStrokeRounded.settings02),
45
+ HugeIconData('Home', HugeIconsStrokeRounded.home01),
46
+ HugeIconData('Home 02', HugeIconsStrokeRounded.home02),
47
+ HugeIconData('Folder', HugeIconsStrokeRounded.folder01),
48
+ HugeIconData('Folder 02', HugeIconsStrokeRounded.folder02),
49
+ HugeIconData('File', HugeIconsStrokeRounded.file01),
50
+ HugeIconData('Lock', HugeIconsStrokeRounded.lock),
51
+ HugeIconData('Key', HugeIconsStrokeRounded.key01),
52
+ HugeIconData('Link', HugeIconsStrokeRounded.link01),
53
+ HugeIconData('Globe', HugeIconsStrokeRounded.globe),
54
+ HugeIconData('API', HugeIconsStrokeRounded.api),
55
+ HugeIconData('Arrow Right', HugeIconsStrokeRounded.arrowRight01),
56
+ HugeIconData('Check Circle', HugeIconsStrokeRounded.checkmarkCircle01),
57
+ HugeIconData('Alert Circle', HugeIconsStrokeRounded.alertCircle),
58
+ HugeIconData('Info Circle', HugeIconsStrokeRounded.informationCircle),
59
+ HugeIconData('Zap', HugeIconsStrokeRounded.zap),
60
+ HugeIconData('Cloud Upload', HugeIconsStrokeRounded.cloudUpload),
61
+ HugeIconData('Cloud Download', HugeIconsStrokeRounded.cloudDownload),
62
+ HugeIconData('Refresh', HugeIconsStrokeRounded.refresh),
63
+ HugeIconData('Hard Drive', HugeIconsStrokeRounded.hardDrive),
64
+ HugeIconData('Drive', HugeIconsStrokeRounded.drive),
65
+];
66
+
67
+class HugeIcon extends StatelessWidget {
68
+ final List<List<dynamic>> iconData;
69
+ final double size;
70
+ final Color? color;
71
+
72
+ const HugeIcon(this.iconData, {super.key, this.size = 24, this.color});
73
+
74
+ @override
75
+ Widget build(BuildContext context) {
76
+ return CustomPaint(
77
+ size: Size(size, size),
78
+ painter: _HugeIconPainter(
79
+ iconData: iconData,
80
+ color: color ?? Theme.of(context).colorScheme.onSurface,
81
+ ),
82
+ );
83
+ }
84
+}
85
+
86
+class _HugeIconPainter extends CustomPainter {
87
+ final List<List<dynamic>> iconData;
88
+ final Color color;
89
+
90
+ _HugeIconPainter({required this.iconData, required this.color});
91
+
92
+ @override
93
+ void paint(Canvas canvas, Size size) {
94
+ final paint = Paint()
95
+ ..color = color
96
+ ..strokeWidth = 1.5
97
+ ..style = PaintingStyle.stroke
98
+ ..strokeCap = StrokeCap.round
99
+ ..strokeJoin = StrokeJoin.round;
100
+
101
+ for (final pathData in iconData) {
102
+ if (pathData[0] == 'path') {
103
+ final pathMap = pathData[1] as Map<dynamic, dynamic>;
104
+ final d = pathMap['d'] as String;
105
+ final strokeWidth = pathMap['strokeWidth'];
106
+ final strokeLinecap = pathMap['strokeLinecap'];
107
+ final strokeLinejoin = pathMap['strokeLinejoin'];
108
+
109
+ if (strokeWidth != null) {
110
+ paint.strokeWidth = double.parse(strokeWidth.toString());
111
+ }
112
+ if (strokeLinecap == 'round') {
113
+ paint.strokeCap = StrokeCap.round;
114
+ }
115
+ if (strokeLinejoin == 'round') {
116
+ paint.strokeJoin = StrokeJoin.round;
117
+ }
118
+
119
+ final path = _parseSvgPath(d);
120
+ canvas.drawPath(path, paint);
121
+ }
122
+ }
123
+ }
124
+
125
+ Path _parseSvgPath(String d) {
126
+ final path = Path();
127
+ final commands = RegExp(
128
+ r'([MmLlHhVvCcSsQqTtAaZz])([^MmLlHhVvCcSsQqTtAaZz]*)',
129
+ ).allMatches(d);
130
+
131
+ for (final match in commands) {
132
+ final command = match.group(1)!;
133
+ final argsStr = match.group(2)?.trim() ?? '';
134
+ final args = argsStr.isEmpty
135
+ ? <double>[]
136
+ : argsStr
137
+ .split(RegExp(r'[\s,]+'))
138
+ .where((s) => s.isNotEmpty)
139
+ .map(double.parse)
140
+ .toList();
141
+
142
+ switch (command) {
143
+ case 'M':
144
+ if (args.isNotEmpty) {
145
+ path.moveTo(args[0], args[1]);
146
+ for (int i = 2; i < args.length; i += 2) {
147
+ path.lineTo(args[i], args[i + 1]);
148
+ }
149
+ }
150
+ break;
151
+ case 'm':
152
+ if (args.isNotEmpty) {
153
+ path.moveTo(args[0], args[1]);
154
+ for (int i = 2; i < args.length; i += 2) {
155
+ path.relativeLineTo(args[i], args[i + 1]);
156
+ }
157
+ }
158
+ break;
159
+ case 'L':
160
+ for (int i = 0; i < args.length; i += 2) {
161
+ path.lineTo(args[i], args[i + 1]);
162
+ }
163
+ break;
164
+ case 'l':
165
+ for (int i = 0; i < args.length; i += 2) {
166
+ path.relativeLineTo(args[i], args[i + 1]);
167
+ }
168
+ break;
169
+ case 'H':
170
+ for (final x in args) {
171
+ path.lineTo(x, path.getBounds().bottom);
172
+ }
173
+ break;
174
+ case 'h':
175
+ for (final dx in args) {
176
+ path.relativeLineTo(dx, 0);
177
+ }
178
+ break;
179
+ case 'V':
180
+ for (final y in args) {
181
+ path.lineTo(path.getBounds().right, y);
182
+ }
183
+ break;
184
+ case 'v':
185
+ for (final dy in args) {
186
+ path.relativeLineTo(0, dy);
187
+ }
188
+ break;
189
+ case 'C':
190
+ for (int i = 0; i < args.length; i += 6) {
191
+ path.cubicTo(
192
+ args[i],
193
+ args[i + 1],
194
+ args[i + 2],
195
+ args[i + 3],
196
+ args[i + 4],
197
+ args[i + 5],
198
+ );
199
+ }
200
+ break;
201
+ case 'c':
202
+ for (int i = 0; i < args.length; i += 6) {
203
+ path.relativeCubicTo(
204
+ args[i],
205
+ args[i + 1],
206
+ args[i + 2],
207
+ args[i + 3],
208
+ args[i + 4],
209
+ args[i + 5],
210
+ );
211
+ }
212
+ break;
213
+ case 'Q':
214
+ for (int i = 0; i < args.length; i += 4) {
215
+ path.quadraticBezierTo(
216
+ args[i],
217
+ args[i + 1],
218
+ args[i + 2],
219
+ args[i + 3],
220
+ );
221
+ }
222
+ break;
223
+ case 'q':
224
+ for (int i = 0; i < args.length; i += 4) {
225
+ path.relativeQuadraticBezierTo(
226
+ args[i],
227
+ args[i + 1],
228
+ args[i + 2],
229
+ args[i + 3],
230
+ );
231
+ }
232
+ break;
233
+ case 'Z':
234
+ case 'z':
235
+ path.close();
236
+ break;
237
+ }
238
+ }
239
+ return path;
240
+ }
241
+
242
+ @override
243
+ bool shouldRepaint(covariant _HugeIconPainter oldDelegate) {
244
+ return oldDelegate.iconData != iconData || oldDelegate.color != color;
245
+ }
246
+}
247
+
248
+class ServerIconPicker extends StatefulWidget {
249
+ final String? selectedIconName;
250
+ final ValueChanged<String> onIconSelected;
251
+
252
+ const ServerIconPicker({
253
+ super.key,
254
+ this.selectedIconName,
255
+ required this.onIconSelected,
256
+ });
257
+
258
+ @override
259
+ State<ServerIconPicker> createState() => _ServerIconPickerState();
260
+}
261
+
262
+class _ServerIconPickerState extends State<ServerIconPicker> {
263
+ late String? _selected;
264
+ final TextEditingController _searchController = TextEditingController();
265
+ List<HugeIconData> _filteredIcons = serverIcons;
266
+
267
+ @override
268
+ void initState() {
269
+ super.initState();
270
+ _selected = widget.selectedIconName;
271
+ }
272
+
273
+ @override
274
+ void dispose() {
275
+ _searchController.dispose();
276
+ super.dispose();
277
+ }
278
+
279
+ void _filterIcons(String query) {
280
+ setState(() {
281
+ if (query.isEmpty) {
282
+ _filteredIcons = serverIcons;
283
+ } else {
284
+ _filteredIcons = serverIcons
285
+ .where(
286
+ (icon) => icon.name.toLowerCase().contains(query.toLowerCase()),
287
+ )
288
+ .toList();
289
+ }
290
+ });
291
+ }
292
+
293
+ @override
294
+ Widget build(BuildContext context) {
295
+ final theme = Theme.of(context);
296
+
297
+ return ShadSheet(
298
+ title: const Text('Select Icon'),
299
+ description: const Text('Choose an icon for your server'),
300
+ child: SizedBox(
301
+ width: double.maxFinite,
302
+ height: MediaQuery.of(context).size.height * 0.6,
303
+ child: Column(
304
+ children: [
305
+ Padding(
306
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
307
+ child: ShadInput(
308
+ controller: _searchController,
309
+ placeholder: const Text('Search icons...'),
310
+ leading: const Padding(
311
+ padding: EdgeInsets.only(left: 8),
312
+ child: Icon(Icons.search, size: 18),
313
+ ),
314
+ onChanged: _filterIcons,
315
+ ),
316
+ ),
317
+ Expanded(
318
+ child: GridView.builder(
319
+ padding: const EdgeInsets.all(16),
320
+ gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
321
+ crossAxisCount: 5,
322
+ mainAxisSpacing: 8,
323
+ crossAxisSpacing: 8,
324
+ ),
325
+ itemCount: _filteredIcons.length,
326
+ itemBuilder: (context, index) {
327
+ final icon = _filteredIcons[index];
328
+ final isSelected = _selected == icon.name;
329
+
330
+ return GestureDetector(
331
+ onTap: () {
332
+ setState(() {
333
+ _selected = icon.name;
334
+ });
335
+ widget.onIconSelected(icon.name);
336
+ },
337
+ child: Container(
338
+ decoration: BoxDecoration(
339
+ color: isSelected
340
+ ? theme.colorScheme.primary.withValues(alpha: 0.2)
341
+ : theme.colorScheme.surfaceContainerHighest
342
+ .withValues(alpha: 0.3),
343
+ borderRadius: BorderRadius.circular(8),
344
+ border: Border.all(
345
+ color: isSelected
346
+ ? theme.colorScheme.primary
347
+ : Colors.transparent,
348
+ width: 2,
349
+ ),
350
+ ),
351
+ child: Column(
352
+ mainAxisAlignment: MainAxisAlignment.center,
353
+ children: [
354
+ HugeIcon(
355
+ icon.iconData,
356
+ size: 24,
357
+ color: isSelected
358
+ ? theme.colorScheme.primary
359
+ : theme.colorScheme.onSurface,
360
+ ),
361
+ const SizedBox(height: 4),
362
+ Text(
363
+ icon.name,
364
+ style: theme.textTheme.labelSmall?.copyWith(
365
+ fontSize: 8,
366
+ ),
367
+ textAlign: TextAlign.center,
368
+ maxLines: 1,
369
+ overflow: TextOverflow.ellipsis,
370
+ ),
371
+ ],
372
+ ),
373
+ ),
374
+ );
375
+ },
376
+ ),
377
+ ),
378
+ Padding(
379
+ padding: const EdgeInsets.all(16),
380
+ child: ShadButton(
381
+ onPressed: () => Navigator.of(context).pop(),
382
+ child: const Text('Done'),
383
+ ),
384
+ ),
385
+ ],
386
+ ),
387
+ ),
388
+ );
389
+ }
390
+}
391
+
392
+HugeIconData? getHugeIconByName(String? name) {
393
+ if (name == null) return null;
394
+ try {
395
+ return serverIcons.firstWhere((icon) => icon.name == name);
396
+ } catch (_) {
397
+ return null;
398
+ }
399
+}
400
+
401
+HugeIconData? getDefaultServerIcon(String? serverType) {
402
+ switch (serverType) {
403
+ case 'lmStudio':
404
+ return serverIcons.firstWhere(
405
+ (icon) => icon.name == 'Computer Terminal',
406
+ orElse: () => serverIcons.first,
407
+ );
408
+ case 'ollama':
409
+ return serverIcons.firstWhere(
410
+ (icon) => icon.name == 'Bot',
411
+ orElse: () => serverIcons.first,
412
+ );
413
+ case 'openRouter':
414
+ return serverIcons.firstWhere(
415
+ (icon) => icon.name == 'Cloud',
416
+ orElse: () => serverIcons.first,
417
+ );
418
+ default:
419
+ return serverIcons.firstWhere(
420
+ (icon) => icon.name == 'Server Stack',
421
+ orElse: () => serverIcons.first,
422
+ );
423
+ }
424
+}
lib/features/servers/widgets/server_type_selector.dart
new
+43
@@ -0,0 +1,43 @@
1
+import 'package:flutter/material.dart';
2
+import '../../../core/models/enums.dart';
3
+
4
+class ServerTypeSelector extends StatelessWidget {
5
+ final ServerType selectedType;
6
+ final ValueChanged<ServerType> onChanged;
7
+
8
+ const ServerTypeSelector({
9
+ super.key,
10
+ required this.selectedType,
11
+ required this.onChanged,
12
+ });
13
+
14
+ @override
15
+ Widget build(BuildContext context) {
16
+ return SegmentedButton<ServerType>(
17
+ segments: const [
18
+ ButtonSegment<ServerType>(
19
+ value: ServerType.lmStudio,
20
+ label: Text('LM Studio'),
21
+ icon: Icon(Icons.terminal, size: 18),
22
+ ),
23
+ ButtonSegment<ServerType>(
24
+ value: ServerType.ollama,
25
+ label: Text('Ollama'),
26
+ icon: Icon(Icons.pets, size: 18),
27
+ ),
28
+ ButtonSegment<ServerType>(
29
+ value: ServerType.openRouter,
30
+ label: Text('OpenRouter'),
31
+ icon: Icon(Icons.cloud, size: 18),
32
+ ),
33
+ ],
34
+ selected: {selectedType},
35
+ onSelectionChanged: (Set<ServerType> selected) {
36
+ if (selected.isNotEmpty) {
37
+ onChanged(selected.first);
38
+ }
39
+ },
40
+ showSelectedIcon: false,
41
+ );
42
+ }
43
+}
lib/features/settings/data/models/app_settings.dart
new
+106
@@ -0,0 +1,106 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:hive_ce/hive.dart';
3
+
4
+part 'app_settings.g.dart';
5
+
6
+@HiveType(typeId: 4)
7
+class AppSettings extends HiveObject {
8
+ @HiveField(0)
9
+ final double temperature;
10
+
11
+ @HiveField(1)
12
+ final double topP;
13
+
14
+ @HiveField(2)
15
+ final int maxTokens;
16
+
17
+ @HiveField(3)
18
+ final int contextLength;
19
+
20
+ @HiveField(4)
21
+ final ThemeMode themeMode;
22
+
23
+ @HiveField(5)
24
+ final double fontSize;
25
+
26
+ @HiveField(6)
27
+ final bool showSystemMessages;
28
+
29
+ @HiveField(7)
30
+ final bool hapticFeedbackEnabled;
31
+
32
+ @HiveField(8)
33
+ final bool sendOnEnter;
34
+
35
+ @HiveField(9)
36
+ final String? defaultServerId;
37
+
38
+ @HiveField(10)
39
+ final bool showDataIndicator;
40
+
41
+ @HiveField(11)
42
+ final bool autoGenerateTitle;
43
+
44
+ @HiveField(12)
45
+ final bool streamingEnabled;
46
+
47
+ @HiveField(13)
48
+ final String? defaultPersonaId;
49
+
50
+ @HiveField(14, defaultValue: false)
51
+ final bool hasCompletedOnboarding;
52
+
53
+ AppSettings({
54
+ this.temperature = 0.7,
55
+ this.topP = 0.9,
56
+ this.maxTokens = 2048,
57
+ this.contextLength = 4096,
58
+ this.themeMode = ThemeMode.dark,
59
+ this.fontSize = 16.0,
60
+ this.showSystemMessages = false,
61
+ this.hapticFeedbackEnabled = true,
62
+ this.sendOnEnter = false,
63
+ this.defaultServerId,
64
+ this.showDataIndicator = true,
65
+ this.autoGenerateTitle = true,
66
+ this.streamingEnabled = true,
67
+ this.defaultPersonaId,
68
+ this.hasCompletedOnboarding = false,
69
+ });
70
+
71
+ AppSettings copyWith({
72
+ double? temperature,
73
+ double? topP,
74
+ int? maxTokens,
75
+ int? contextLength,
76
+ ThemeMode? themeMode,
77
+ double? fontSize,
78
+ bool? showSystemMessages,
79
+ bool? hapticFeedbackEnabled,
80
+ bool? sendOnEnter,
81
+ String? defaultServerId,
82
+ bool? showDataIndicator,
83
+ bool? autoGenerateTitle,
84
+ bool? streamingEnabled,
85
+ String? defaultPersonaId,
86
+ bool? hasCompletedOnboarding,
87
+ }) {
88
+ return AppSettings(
89
+ temperature: temperature ?? this.temperature,
90
+ topP: topP ?? this.topP,
91
+ maxTokens: maxTokens ?? this.maxTokens,
92
+ contextLength: contextLength ?? this.contextLength,
93
+ themeMode: themeMode ?? this.themeMode,
94
+ fontSize: fontSize ?? this.fontSize,
95
+ showSystemMessages: showSystemMessages ?? this.showSystemMessages,
96
+ hapticFeedbackEnabled: hapticFeedbackEnabled ?? this.hapticFeedbackEnabled,
97
+ sendOnEnter: sendOnEnter ?? this.sendOnEnter,
98
+ defaultServerId: defaultServerId ?? this.defaultServerId,
99
+ showDataIndicator: showDataIndicator ?? this.showDataIndicator,
100
+ autoGenerateTitle: autoGenerateTitle ?? this.autoGenerateTitle,
101
+ streamingEnabled: streamingEnabled ?? this.streamingEnabled,
102
+ defaultPersonaId: defaultPersonaId ?? this.defaultPersonaId,
103
+ hasCompletedOnboarding: hasCompletedOnboarding ?? this.hasCompletedOnboarding,
104
+ );
105
+ }
106
+}
lib/features/settings/data/models/app_settings.g.dart
new
+83
@@ -0,0 +1,83 @@
1
+// GENERATED CODE - DO NOT MODIFY BY HAND
2
+
3
+part of 'app_settings.dart';
4
+
5
+// **************************************************************************
6
+// TypeAdapterGenerator
7
+// **************************************************************************
8
+
9
+class AppSettingsAdapter extends TypeAdapter<AppSettings> {
10
+ @override
11
+ final typeId = 4;
12
+
13
+ @override
14
+ AppSettings read(BinaryReader reader) {
15
+ final numOfFields = reader.readByte();
16
+ final fields = <int, dynamic>{
17
+ for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
18
+ };
19
+ return AppSettings(
20
+ temperature: fields[0] == null ? 0.7 : (fields[0] as num).toDouble(),
21
+ topP: fields[1] == null ? 0.9 : (fields[1] as num).toDouble(),
22
+ maxTokens: fields[2] == null ? 2048 : (fields[2] as num).toInt(),
23
+ contextLength: fields[3] == null ? 4096 : (fields[3] as num).toInt(),
24
+ themeMode: fields[4] == null ? ThemeMode.dark : fields[4] as ThemeMode,
25
+ fontSize: fields[5] == null ? 16.0 : (fields[5] as num).toDouble(),
26
+ showSystemMessages: fields[6] == null ? false : fields[6] as bool,
27
+ hapticFeedbackEnabled: fields[7] == null ? true : fields[7] as bool,
28
+ sendOnEnter: fields[8] == null ? false : fields[8] as bool,
29
+ defaultServerId: fields[9] as String?,
30
+ showDataIndicator: fields[10] == null ? true : fields[10] as bool,
31
+ autoGenerateTitle: fields[11] == null ? true : fields[11] as bool,
32
+ streamingEnabled: fields[12] == null ? true : fields[12] as bool,
33
+ defaultPersonaId: fields[13] as String?,
34
+ hasCompletedOnboarding: fields[14] == null ? false : fields[14] as bool,
35
+ );
36
+ }
37
+
38
+ @override
39
+ void write(BinaryWriter writer, AppSettings obj) {
40
+ writer
41
+ ..writeByte(15)
42
+ ..writeByte(0)
43
+ ..write(obj.temperature)
44
+ ..writeByte(1)
45
+ ..write(obj.topP)
46
+ ..writeByte(2)
47
+ ..write(obj.maxTokens)
48
+ ..writeByte(3)
49
+ ..write(obj.contextLength)
50
+ ..writeByte(4)
51
+ ..write(obj.themeMode)
52
+ ..writeByte(5)
53
+ ..write(obj.fontSize)
54
+ ..writeByte(6)
55
+ ..write(obj.showSystemMessages)
56
+ ..writeByte(7)
57
+ ..write(obj.hapticFeedbackEnabled)
58
+ ..writeByte(8)
59
+ ..write(obj.sendOnEnter)
60
+ ..writeByte(9)
61
+ ..write(obj.defaultServerId)
62
+ ..writeByte(10)
63
+ ..write(obj.showDataIndicator)
64
+ ..writeByte(11)
65
+ ..write(obj.autoGenerateTitle)
66
+ ..writeByte(12)
67
+ ..write(obj.streamingEnabled)
68
+ ..writeByte(13)
69
+ ..write(obj.defaultPersonaId)
70
+ ..writeByte(14)
71
+ ..write(obj.hasCompletedOnboarding);
72
+ }
73
+
74
+ @override
75
+ int get hashCode => typeId.hashCode;
76
+
77
+ @override
78
+ bool operator ==(Object other) =>
79
+ identical(this, other) ||
80
+ other is AppSettingsAdapter &&
81
+ runtimeType == other.runtimeType &&
82
+ typeId == other.typeId;
83
+}
lib/features/settings/screens/settings_screen.dart
new
+23
@@ -0,0 +1,23 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+import 'package:go_router/go_router.dart';
4
+
5
+class SettingsScreen extends ConsumerWidget {
6
+ const SettingsScreen({super.key});
7
+
8
+ @override
9
+ Widget build(BuildContext context, WidgetRef ref) {
10
+ return Scaffold(
11
+ appBar: AppBar(
12
+ title: const Text('Settings'),
13
+ leading: IconButton(
14
+ icon: const Icon(Icons.arrow_back),
15
+ onPressed: () => context.pop(),
16
+ ),
17
+ ),
18
+ body: const Center(
19
+ child: Text('Settings - Placeholder'),
20
+ ),
21
+ );
22
+ }
23
+}
lib/hive_registrar.g.dart
new
+30
@@ -0,0 +1,30 @@
1
+// Generated by Hive CE
2
+// Do not modify
3
+// Check in to version control
4
+
5
+import 'package:hive_ce/hive_ce.dart';
6
+import 'package:localmind/features/settings/data/models/app_settings.dart';
7
+import 'package:localmind/features/conversations/data/models/conversation.dart';
8
+import 'package:localmind/features/chat/data/models/message.dart';
9
+import 'package:localmind/features/personas/data/models/persona.dart';
10
+import 'package:localmind/features/servers/data/models/server.dart';
11
+
12
+extension HiveRegistrar on HiveInterface {
13
+ void registerAdapters() {
14
+ registerAdapter(AppSettingsAdapter());
15
+ registerAdapter(ConversationAdapter());
16
+ registerAdapter(MessageAdapter());
17
+ registerAdapter(PersonaAdapter());
18
+ registerAdapter(ServerAdapter());
19
+ }
20
+}
21
+
22
+extension IsolatedHiveRegistrar on IsolatedHiveInterface {
23
+ void registerAdapters() {
24
+ registerAdapter(AppSettingsAdapter());
25
+ registerAdapter(ConversationAdapter());
26
+ registerAdapter(MessageAdapter());
27
+ registerAdapter(PersonaAdapter());
28
+ registerAdapter(ServerAdapter());
29
+ }
30
+}
lib/main.dart
new
+20
@@ -0,0 +1,20 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+import 'app.dart';
4
+import 'core/storage/hive_initializer.dart';
5
+import 'core/providers/storage_providers.dart';
6
+
7
+Future<void> main() async {
8
+ WidgetsFlutterBinding.ensureInitialized();
9
+
10
+ final hiveBoxes = await HiveInitializer.initialize();
11
+
12
+ runApp(
13
+ ProviderScope(
14
+ overrides: [
15
+ hiveBoxesProvider.overrideWithValue(hiveBoxes),
16
+ ],
17
+ child: const App(),
18
+ ),
19
+ );
20
+}
logo_files/android/play_store_512.png
Binary files /dev/null and b/logo_files/android/play_store_512.png differ
logo_files/android/res/mipmap-anydpi-v26/ic_launcher.xml
new
+6
@@ -0,0 +1,6 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
3
+ <background android:drawable="@mipmap/ic_launcher_background"/>
4
+ <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
5
+ <monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
6
+</adaptive-icon>
\ No newline at end of file
logo_files/android/res/mipmap-hdpi/ic_launcher.png
Binary files /dev/null and b/logo_files/android/res/mipmap-hdpi/ic_launcher.png differ
logo_files/android/res/mipmap-hdpi/ic_launcher_background.png
Binary files /dev/null and b/logo_files/android/res/mipmap-hdpi/ic_launcher_background.png differ
logo_files/android/res/mipmap-hdpi/ic_launcher_foreground.png
Binary files /dev/null and b/logo_files/android/res/mipmap-hdpi/ic_launcher_foreground.png differ
logo_files/android/res/mipmap-hdpi/ic_launcher_monochrome.png
Binary files /dev/null and b/logo_files/android/res/mipmap-hdpi/ic_launcher_monochrome.png differ
logo_files/android/res/mipmap-mdpi/ic_launcher.png
Binary files /dev/null and b/logo_files/android/res/mipmap-mdpi/ic_launcher.png differ
logo_files/android/res/mipmap-mdpi/ic_launcher_background.png
Binary files /dev/null and b/logo_files/android/res/mipmap-mdpi/ic_launcher_background.png differ
logo_files/android/res/mipmap-mdpi/ic_launcher_foreground.png
Binary files /dev/null and b/logo_files/android/res/mipmap-mdpi/ic_launcher_foreground.png differ
logo_files/android/res/mipmap-mdpi/ic_launcher_monochrome.png
Binary files /dev/null and b/logo_files/android/res/mipmap-mdpi/ic_launcher_monochrome.png differ
logo_files/android/res/mipmap-xhdpi/ic_launcher.png
Binary files /dev/null and b/logo_files/android/res/mipmap-xhdpi/ic_launcher.png differ
logo_files/android/res/mipmap-xhdpi/ic_launcher_background.png
Binary files /dev/null and b/logo_files/android/res/mipmap-xhdpi/ic_launcher_background.png differ
logo_files/android/res/mipmap-xhdpi/ic_launcher_foreground.png
Binary files /dev/null and b/logo_files/android/res/mipmap-xhdpi/ic_launcher_foreground.png differ
logo_files/android/res/mipmap-xhdpi/ic_launcher_monochrome.png
Binary files /dev/null and b/logo_files/android/res/mipmap-xhdpi/ic_launcher_monochrome.png differ
logo_files/android/res/mipmap-xxhdpi/ic_launcher.png
Binary files /dev/null and b/logo_files/android/res/mipmap-xxhdpi/ic_launcher.png differ
logo_files/android/res/mipmap-xxhdpi/ic_launcher_background.png
Binary files /dev/null and b/logo_files/android/res/mipmap-xxhdpi/ic_launcher_background.png differ
logo_files/android/res/mipmap-xxhdpi/ic_launcher_foreground.png
Binary files /dev/null and b/logo_files/android/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
logo_files/android/res/mipmap-xxhdpi/ic_launcher_monochrome.png
Binary files /dev/null and b/logo_files/android/res/mipmap-xxhdpi/ic_launcher_monochrome.png differ
logo_files/android/res/mipmap-xxxhdpi/ic_launcher.png
Binary files /dev/null and b/logo_files/android/res/mipmap-xxxhdpi/ic_launcher.png differ
logo_files/android/res/mipmap-xxxhdpi/ic_launcher_background.png
Binary files /dev/null and b/logo_files/android/res/mipmap-xxxhdpi/ic_launcher_background.png differ
logo_files/android/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Binary files /dev/null and b/logo_files/android/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
logo_files/android/res/mipmap-xxxhdpi/ic_launcher_monochrome.png
Binary files /dev/null and b/logo_files/android/res/mipmap-xxxhdpi/ic_launcher_monochrome.png differ
logo_files/ios/AppIcon-20@2x.png
Binary files /dev/null and b/logo_files/ios/AppIcon-20@2x.png differ
logo_files/ios/AppIcon-20@2x~ipad.png
Binary files /dev/null and b/logo_files/ios/AppIcon-20@2x~ipad.png differ
logo_files/ios/AppIcon-20@3x.png
Binary files /dev/null and b/logo_files/ios/AppIcon-20@3x.png differ
logo_files/ios/AppIcon-20~ipad.png
Binary files /dev/null and b/logo_files/ios/AppIcon-20~ipad.png differ
logo_files/ios/AppIcon-29.png
Binary files /dev/null and b/logo_files/ios/AppIcon-29.png differ
logo_files/ios/AppIcon-29@2x.png
Binary files /dev/null and b/logo_files/ios/AppIcon-29@2x.png differ
logo_files/ios/AppIcon-29@2x~ipad.png
Binary files /dev/null and b/logo_files/ios/AppIcon-29@2x~ipad.png differ
logo_files/ios/AppIcon-29@3x.png
Binary files /dev/null and b/logo_files/ios/AppIcon-29@3x.png differ
logo_files/ios/AppIcon-29~ipad.png
Binary files /dev/null and b/logo_files/ios/AppIcon-29~ipad.png differ
logo_files/ios/AppIcon-40@2x.png
Binary files /dev/null and b/logo_files/ios/AppIcon-40@2x.png differ
logo_files/ios/AppIcon-40@2x~ipad.png
Binary files /dev/null and b/logo_files/ios/AppIcon-40@2x~ipad.png differ
logo_files/ios/AppIcon-40@3x.png
Binary files /dev/null and b/logo_files/ios/AppIcon-40@3x.png differ
logo_files/ios/AppIcon-40~ipad.png
Binary files /dev/null and b/logo_files/ios/AppIcon-40~ipad.png differ
logo_files/ios/AppIcon-60@2x~car.png
Binary files /dev/null and b/logo_files/ios/AppIcon-60@2x~car.png differ
logo_files/ios/AppIcon-60@3x~car.png
Binary files /dev/null and b/logo_files/ios/AppIcon-60@3x~car.png differ
logo_files/ios/AppIcon-83.5@2x~ipad.png
Binary files /dev/null and b/logo_files/ios/AppIcon-83.5@2x~ipad.png differ
logo_files/ios/AppIcon@2x.png
Binary files /dev/null and b/logo_files/ios/AppIcon@2x.png differ
logo_files/ios/AppIcon@2x~ipad.png
Binary files /dev/null and b/logo_files/ios/AppIcon@2x~ipad.png differ
logo_files/ios/AppIcon@3x.png
Binary files /dev/null and b/logo_files/ios/AppIcon@3x.png differ
logo_files/ios/AppIcon~ios-marketing.png
Binary files /dev/null and b/logo_files/ios/AppIcon~ios-marketing.png differ
logo_files/ios/AppIcon~ipad.png
Binary files /dev/null and b/logo_files/ios/AppIcon~ipad.png differ
logo_files/ios/Contents.json
new
+134
@@ -0,0 +1,134 @@
1
+{
2
+ "images": [
3
+ {
4
+ "filename": "AppIcon@2x.png",
5
+ "idiom": "iphone",
6
+ "scale": "2x",
7
+ "size": "60x60"
8
+ },
9
+ {
10
+ "filename": "AppIcon@3x.png",
11
+ "idiom": "iphone",
12
+ "scale": "3x",
13
+ "size": "60x60"
14
+ },
15
+ {
16
+ "filename": "AppIcon~ipad.png",
17
+ "idiom": "ipad",
18
+ "scale": "1x",
19
+ "size": "76x76"
20
+ },
21
+ {
22
+ "filename": "AppIcon@2x~ipad.png",
23
+ "idiom": "ipad",
24
+ "scale": "2x",
25
+ "size": "76x76"
26
+ },
27
+ {
28
+ "filename": "AppIcon-83.5@2x~ipad.png",
29
+ "idiom": "ipad",
30
+ "scale": "2x",
31
+ "size": "83.5x83.5"
32
+ },
33
+ {
34
+ "filename": "AppIcon-40@2x.png",
35
+ "idiom": "iphone",
36
+ "scale": "2x",
37
+ "size": "40x40"
38
+ },
39
+ {
40
+ "filename": "AppIcon-40@3x.png",
41
+ "idiom": "iphone",
42
+ "scale": "3x",
43
+ "size": "40x40"
44
+ },
45
+ {
46
+ "filename": "AppIcon-40~ipad.png",
47
+ "idiom": "ipad",
48
+ "scale": "1x",
49
+ "size": "40x40"
50
+ },
51
+ {
52
+ "filename": "AppIcon-40@2x~ipad.png",
53
+ "idiom": "ipad",
54
+ "scale": "2x",
55
+ "size": "40x40"
56
+ },
57
+ {
58
+ "filename": "AppIcon-20@2x.png",
59
+ "idiom": "iphone",
60
+ "scale": "2x",
61
+ "size": "20x20"
62
+ },
63
+ {
64
+ "filename": "AppIcon-20@3x.png",
65
+ "idiom": "iphone",
66
+ "scale": "3x",
67
+ "size": "20x20"
68
+ },
69
+ {
70
+ "filename": "AppIcon-20~ipad.png",
71
+ "idiom": "ipad",
72
+ "scale": "1x",
73
+ "size": "20x20"
74
+ },
75
+ {
76
+ "filename": "AppIcon-20@2x~ipad.png",
77
+ "idiom": "ipad",
78
+ "scale": "2x",
79
+ "size": "20x20"
80
+ },
81
+ {
82
+ "filename": "AppIcon-29.png",
83
+ "idiom": "iphone",
84
+ "scale": "1x",
85
+ "size": "29x29"
86
+ },
87
+ {
88
+ "filename": "AppIcon-29@2x.png",
89
+ "idiom": "iphone",
90
+ "scale": "2x",
91
+ "size": "29x29"
92
+ },
93
+ {
94
+ "filename": "AppIcon-29@3x.png",
95
+ "idiom": "iphone",
96
+ "scale": "3x",
97
+ "size": "29x29"
98
+ },
99
+ {
100
+ "filename": "AppIcon-29~ipad.png",
101
+ "idiom": "ipad",
102
+ "scale": "1x",
103
+ "size": "29x29"
104
+ },
105
+ {
106
+ "filename": "AppIcon-29@2x~ipad.png",
107
+ "idiom": "ipad",
108
+ "scale": "2x",
109
+ "size": "29x29"
110
+ },
111
+ {
112
+ "filename": "AppIcon-60@2x~car.png",
113
+ "idiom": "car",
114
+ "scale": "2x",
115
+ "size": "60x60"
116
+ },
117
+ {
118
+ "filename": "AppIcon-60@3x~car.png",
119
+ "idiom": "car",
120
+ "scale": "3x",
121
+ "size": "60x60"
122
+ },
123
+ {
124
+ "filename": "AppIcon~ios-marketing.png",
125
+ "idiom": "ios-marketing",
126
+ "scale": "1x",
127
+ "size": "1024x1024"
128
+ }
129
+ ],
130
+ "info": {
131
+ "author": "iconkitchen",
132
+ "version": 1
133
+ }
134
+}
\ No newline at end of file
logo_files/play-store/play_store_feature_graphic.png
Binary files /dev/null and b/logo_files/play-store/play_store_feature_graphic.png differ
plans/00_project_setup.md
new
+417
@@ -0,0 +1,417 @@
1
+# Feature 00 — Project Setup & Scaffolding
2
+
3
+> **Phase:** 0 — Setup
4
+> **Status:** Pending
5
+> **Est. Time:** 1 week
6
+
7
+---
8
+
9
+## Goal
10
+
11
+Transform the default Flutter counter app into a production-ready project shell with:
12
+- Feature-based folder structure
13
+- ShadCN UI theme integration
14
+- Riverpod provider scope
15
+- Hive CE local storage initialization
16
+- App-wide routing (GoRouter)
17
+- Base app shell (navigation scaffold)
18
+
19
+---
20
+
21
+## Dependencies Used
22
+
23
+| Package | Purpose |
24
+| -------------------- | -------------------------------- |
25
+| `flutter_riverpod` | State management (ProviderScope) |
26
+| `hive_ce` | Local encrypted storage |
27
+| `hive_ce_flutter` | Hive Flutter initialization |
28
+| `shadcn_ui` | Component library & theming |
29
+| `path_provider` | App directory paths |
30
+| `go_router` | Declarative routing |
31
+
32
+---
33
+
34
+## Folder Structure
35
+
36
+Create the full directory skeleton under `lib/`:
37
+
38
+```
39
+lib/
40
+├── main.dart # Entry point — init Hive, wrap with ProviderScope
41
+├── app.dart # MaterialApp / ShadCN App root with routing
42
+│
43
+├── core/ # SHARED, CROSS-CUTTING CONCERNS
44
+│ ├── constants/
45
+│ │ └── app_constants.dart
46
+│ ├── models/
47
+│ │ ├── server.dart
48
+│ │ ├── message.dart
49
+│ │ ├── conversation.dart
50
+│ │ ├── persona.dart
51
+│ │ ├── model_info.dart
52
+│ │ ├── chat_parameters.dart
53
+│ │ └── app_settings.dart
54
+│ ├── providers/
55
+│ │ └── app_providers.dart
56
+│ ├── repository/
57
+│ │ └── storage/
58
+│ │ ├── hive_initializer.dart
59
+│ │ ├── hive_keys.dart
60
+│ │ └── hive_boxes.dart
61
+│ ├── routes/
62
+│ │ └── app_router.dart
63
+│ ├── theme/
64
+│ │ ├── app_theme.dart
65
+│ │ ├── colors.dart
66
+│ │ └── typography.dart
67
+│ ├── components/
68
+│ │ ├── app_button.dart
69
+│ │ ├── app_card.dart
70
+│ │ └── app_text_field.dart
71
+│ └── utils/
72
+│ └── uuid.dart
73
+│
74
+├── features/ # FEATURE MODULES (vertical slices)
75
+│ ├── server_connection/
76
+│ │ ├── data/
77
+│ │ │ └── models/
78
+│ │ ├── providers/
79
+│ │ │ └── server_connection_providers.dart
80
+│ │ └── views/
81
+│ │ ├── server_list_page.dart
82
+│ │ ├── add_server_page.dart
83
+│ │ └── components/
84
+│ │ ├── server_card.dart
85
+│ │ ├── connection_status_indicator.dart
86
+│ │ └── server_type_selector.dart
87
+│ │
88
+│ ├── chat/
89
+│ │ ├── data/
90
+│ │ │ └── models/
91
+│ │ ├── providers/
92
+│ │ │ └── chat_providers.dart
93
+│ │ └── views/
94
+│ │ ├── chat_page.dart
95
+│ │ └── components/
96
+│ │ ├── chat_bubble.dart
97
+│ │ ├── chat_input_bar.dart
98
+│ │ ├── typing_indicator.dart
99
+│ │ ├── message_action_bar.dart
100
+│ │ └── code_block.dart
101
+│ │
102
+│ ├── model_management/
103
+│ │ ├── data/
104
+│ │ │ └── models/
105
+│ │ ├── providers/
106
+│ │ │ └── model_management_providers.dart
107
+│ │ └── views/
108
+│ │ ├── model_picker_sheet.dart
109
+│ │ └── components/
110
+│ │ ├── model_tile.dart
111
+│ │ └── model_metadata_chip.dart
112
+│ │
113
+│ ├── conversation/
114
+│ │ ├── data/
115
+│ │ │ └── models/
116
+│ │ ├── providers/
117
+│ │ │ └── conversation_providers.dart
118
+│ │ └── views/
119
+│ │ ├── conversation_drawer.dart
120
+│ │ └── components/
121
+│ │ ├── conversation_tile.dart
122
+│ │ ├── conversation_search_bar.dart
123
+│ │ └── date_section_header.dart
124
+│ │
125
+│ ├── persona/
126
+│ │ ├── data/
127
+│ │ │ └── models/
128
+│ │ ├── providers/
129
+│ │ │ └── persona_providers.dart
130
+│ │ └── views/
131
+│ │ ├── persona_list_page.dart
132
+│ │ ├── create_persona_page.dart
133
+│ │ └── components/
134
+│ │ ├── persona_card.dart
135
+│ │ └── persona_category_chips.dart
136
+│ │
137
+│ └── settings/
138
+│ ├── data/
139
+│ │ └── models/
140
+│ ├── providers/
141
+│ │ └── settings_providers.dart
142
+│ └── views/
143
+│ ├── settings_page.dart
144
+│ └── components/
145
+│ ├── settings_section.dart
146
+│ ├── settings_slider.dart
147
+│ ├── settings_toggle.dart
148
+│ ├── settings_dropdown.dart
149
+│ ├── theme_toggle.dart
150
+│ └── font_size_preview.dart
151
+```
152
+
153
+---
154
+
155
+## Implementation Tasks
156
+
157
+### 1. `main.dart` — Entry Point
158
+
159
+**Responsibilities:**
160
+1. Initialize Hive CE with encryption
161
+2. Open required Hive boxes (servers, conversations, personas, settings, messages)
162
+3. Wrap app with `ProviderScope` (Riverpod)
163
+4. Launch `App` widget from `app.dart`
164
+
165
+**Implementation details:**
166
+
167
+- Call `WidgetsFlutterBinding.ensureInitialized()` before any async work.
168
+- Initialize Hive via `HiveFlutter.init()` using `path_provider`'s app documents directory.
169
+- Generate or retrieve an encryption key stored in device secure storage (for encrypted Hive boxes).
170
+- Register all Hive type adapters for custom data models.
171
+- Open boxes: `serversBox`, `conversationsBox`, `personasBox`, `settingsBox`, `messagesBox`.
172
+- Wrap the `App()` widget with `ProviderScope()`.
173
+
174
+### 2. `app.dart` — App Root
175
+
176
+**Responsibilities:**
177
+1. Configure ShadCN theme (dark-first, with light mode support)
178
+2. Set up GoRouter routing
179
+3. Define the app shell (drawer-based navigation)
180
+
181
+**Routes to define:**
182
+
183
+| Route | Screen |
184
+| ---------------------- | ------------------------- |
185
+| `/` | ChatPage (home) |
186
+| `/conversations` | ConversationDrawer |
187
+| `/servers` | ServerListPage |
188
+| `/servers/add` | AddServerPage |
189
+| `/servers/edit/:id` | AddServerPage (edit mode) |
190
+| `/models` | ModelPickerSheet |
191
+| `/personas` | PersonaListPage |
192
+| `/personas/create` | CreatePersonaPage |
193
+| `/personas/edit/:id` | CreatePersonaPage (edit) |
194
+| `/settings` | SettingsPage |
195
+
196
+**Navigation approach:**
197
+- Use a **drawer / sidebar** for conversation list (like ChatGPT).
198
+- Primary view is always the Chat screen.
199
+- Settings, servers, personas accessible from drawer or app bar actions.
200
+
201
+### 3. ShadCN Theme Setup
202
+
203
+Create `core/theme/colors.dart`:
204
+
205
+```dart
206
+class AppColors {
207
+ // --- Dark Theme (Default) ---
208
+ static const Color darkBackground = Color(0xFF0A0A0A);
209
+ static const Color darkSurface = Color(0xFF171717);
210
+ static const Color darkBorder = Color(0xFF262626);
211
+ static const Color darkPrimaryText = Color(0xFFFAFAFA);
212
+ static const Color darkMutedText = Color(0xFFA1A1AA);
213
+ static const Color darkAccent = Color(0xFF3B82F6);
214
+
215
+ // --- Light Theme ---
216
+ static const Color lightBackground = Color(0xFFFAFAFA);
217
+ static const Color lightSurface = Color(0xFFFFFFFF);
218
+ static const Color lightBorder = Color(0xFFE5E5E5);
219
+ static const Color lightPrimaryText = Color(0xFF0A0A0A);
220
+ static const Color lightMutedText = Color(0xFF71717A);
221
+ static const Color lightAccent = Color(0xFF2563EB);
222
+}
223
+```
224
+
225
+Create `core/theme/app_theme.dart`:
226
+- **Dark theme (primary):** Background `#0A0A0A`, Surface `#171717`, Border `#262626`, Primary `#FAFAFA`, Muted `#A1A1AA`, Accent blue.
227
+- **Light theme (secondary):** Background `#FAFAFA`, Surface `#FFFFFF`, Border `#E5E5E5`, Primary `#0A0A0A`, Muted `#71717A`.
228
+- **Typography:** Use `Inter` font family.
229
+- **Corner radius:** Default `8px` for cards/buttons, `12px` for sheets/dialogs.
230
+- **Transitions:** Default 200ms ease-in-out.
231
+
232
+### 4. Riverpod Provider Structure
233
+
234
+Create `core/providers/app_providers.dart`:
235
+
236
+```dart
237
+// Theme provider
238
+final themeModeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeMode>((ref) {
239
+ return ThemeModeNotifier();
240
+});
241
+
242
+// Current active server
243
+final activeServerProvider = StateNotifierProvider<ActiveServerNotifier, Server?>((ref) {
244
+ return ActiveServerNotifier();
245
+});
246
+
247
+// Current active conversation
248
+final activeConversationProvider = StateNotifierProvider<ActiveConversationNotifier, Conversation?>((ref) {
249
+ return ActiveConversationNotifier();
250
+});
251
+
252
+// Hive boxes (initialized in main.dart, overridden here)
253
+final hiveBoxesProvider = Provider<HiveBoxes>((ref) {
254
+ throw UnimplementedError('Must be overridden with actual HiveBoxes');
255
+});
256
+```
257
+
258
+### 5. Hive Storage Initialization
259
+
260
+Create `core/repository/storage/hive_keys.dart`:
261
+
262
+```dart
263
+class HiveKeys {
264
+ static const String servers = 'servers';
265
+ static const String messages = 'messages';
266
+ static const String conversations = 'conversations';
267
+ static const String personas = 'personas';
268
+ static const String settings = 'settings';
269
+}
270
+```
271
+
272
+Create `core/repository/storage/hive_boxes.dart`:
273
+
274
+```dart
275
+class HiveBoxes {
276
+ final Box<Server> servers;
277
+ final Box<Message> messages;
278
+ final Box<Conversation> conversations;
279
+ final Box<Persona> personas;
280
+ final Box<AppSettings> settings;
281
+
282
+ const HiveBoxes({
283
+ required this.servers,
284
+ required this.messages,
285
+ required this.conversations,
286
+ required this.personas,
287
+ required this.settings,
288
+ });
289
+}
290
+```
291
+
292
+Create `core/repository/storage/hive_initializer.dart`:
293
+
294
+```dart
295
+class HiveInitializer {
296
+ static Future<HiveBoxes> initialize() async {
297
+ final appDir = await getApplicationDocumentsDirectory();
298
+ await Hive.initFlutter(appDir.path);
299
+
300
+ // Register adapters (Server, Message, Conversation, Persona, AppSettings)
301
+ // ...
302
+
303
+ final servers = await Hive.openBox<Server>(HiveKeys.servers);
304
+ final messages = await Hive.openBox<Message>(HiveKeys.messages);
305
+ final conversations = await Hive.openBox<Conversation>(HiveKeys.conversations);
306
+ final personas = await Hive.openBox<Persona>(HiveKeys.personas);
307
+ final settings = await Hive.openBox<AppSettings>(HiveKeys.settings);
308
+
309
+ return HiveBoxes(
310
+ servers: servers,
311
+ messages: messages,
312
+ conversations: conversations,
313
+ personas: personas,
314
+ settings: settings,
315
+ );
316
+ }
317
+}
318
+```
319
+
320
+### 6. App Constants
321
+
322
+Create `core/constants/app_constants.dart`:
323
+
324
+```dart
325
+class AppConstants {
326
+ static const String appName = 'LocalMind';
327
+ static const String appTagline = 'Your AI. Your Device. Your Rules.';
328
+ static const String version = '1.0.0';
329
+
330
+ // Default chat parameters
331
+ static const double defaultTemperature = 0.7;
332
+ static const double defaultTopP = 0.9;
333
+ static const int defaultMaxTokens = 2048;
334
+ static const int defaultContextLength = 4096;
335
+
336
+ // API defaults
337
+ static const int connectionTimeoutMs = 5000;
338
+ static const int receiveTimeoutMs = 60000;
339
+}
340
+```
341
+
342
+### 7. GoRouter Setup
343
+
344
+Create `core/routes/app_router.dart`:
345
+
346
+```dart
347
+final appRouter = GoRouter(
348
+ initialLocation: '/',
349
+ routes: [
350
+ GoRoute(
351
+ path: '/',
352
+ builder: (context, state) => const ChatPage(),
353
+ routes: [
354
+ GoRoute(path: 'servers', builder: (context, state) => const ServerListPage()),
355
+ GoRoute(path: 'servers/add', builder: (context, state) => const AddServerPage()),
356
+ GoRoute(path: 'personas', builder: (context, state) => const PersonaListPage()),
357
+ GoRoute(path: 'personas/create', builder: (context, state) => const CreatePersonaPage()),
358
+ GoRoute(path: 'settings', builder: (context, state) => const SettingsPage()),
359
+ ],
360
+ ),
361
+ ],
362
+);
363
+```
364
+
365
+---
366
+
367
+## Core Models (Stubs)
368
+
369
+Create stub model files in `core/models/` with minimal implementations. Full implementations come in Feature 07.
370
+
371
+**Models to create (stubs):**
372
+- `server.dart` — ServerType enum, ConnectionStatus enum, Server class
373
+- `message.dart` — MessageRole enum, MessageStatus enum, Message class
374
+- `conversation.dart` — Conversation class
375
+- `persona.dart` — Persona class
376
+- `model_info.dart` — ModelInfo class
377
+- `chat_parameters.dart` — ChatParameters class
378
+- `app_settings.dart` — AppSettings class
379
+
380
+Each stub should have:
381
+- All required fields
382
+- `fromMap()` / `toMap()` methods
383
+- `copyWith()` method
384
+- Hive annotations (`@HiveType`, `@HiveField`)
385
+
386
+---
387
+
388
+## Core Components (Stubs)
389
+
390
+Create stub reusable components in `core/components/`:
391
+
392
+- `app_button.dart` — PrimaryButton, OutlineButton variants
393
+- `app_card.dart` — Card with standard styling
394
+- `app_text_field.dart` — Standard text input
395
+
396
+These get fully implemented in Feature 08 (Design System).
397
+
398
+---
399
+
400
+## Acceptance Criteria
401
+
402
+- [ ] `flutter run` launches the app with ShadCN dark theme (no counter app).
403
+- [ ] App shell shows a chat screen placeholder with a working drawer/sidebar.
404
+- [ ] Hive initializes without errors and boxes are accessible.
405
+- [ ] Riverpod `ProviderScope` wraps the entire app.
406
+- [ ] All feature folders exist with placeholder files.
407
+- [ ] Routing works — navigating to `/settings` shows a settings placeholder.
408
+- [ ] Both dark and light themes render correctly when toggled.
409
+
410
+---
411
+
412
+## Notes
413
+
414
+- Do NOT implement any feature logic in this phase. Only scaffolding.
415
+- The ShadCN theme should be the single source of truth for all colors/typography.
416
+- Prefer `ConsumerWidget` / `ConsumerStatefulWidget` for all screens from the start.
417
+- Feature imports are **forbidden** — features must not import from other features.
plans/01_server_connection.md
new
+251
@@ -0,0 +1,251 @@
1
+# Feature 01 — Server Connection & Management
2
+
3
+> **Phase:** 1 — Core
4
+> **Status:** Pending
5
+> **Depends on:** `00_project_setup`, `07_core_architecture`
6
+
7
+---
8
+
9
+## Goal
10
+
11
+Allow users to configure and manage connections to local LLM servers (LM Studio, Ollama) and cloud providers (OpenRouter). This is the foundational feature — no chat works without a connected server.
12
+
13
+---
14
+
15
+## Feature Folder Structure
16
+
17
+```
18
+features/server_connection/
19
+├── data/
20
+│ └── models/ # (Uses core/models/server.dart)
21
+│
22
+├── providers/
23
+│ └── server_connection_providers.dart
24
+│
25
+└── views/
26
+ ├── server_list_page.dart # Main page
27
+ ├── add_server_page.dart # Add/Edit server
28
+ └── components/
29
+ ├── server_card.dart
30
+ ├── connection_status_indicator.dart
31
+ └── server_type_selector.dart
32
+```
33
+
34
+---
35
+
36
+## Dependencies Used
37
+
38
+| Package | Purpose |
39
+| ------- | ---------------------------------------------------- |
40
+| `dio` | HTTP client for REST API calls and connection testing |
41
+| `hive_ce` | Persist server profiles locally |
42
+| `flutter_riverpod` | State management for server list & active server |
43
+
44
+---
45
+
46
+## Data Model
47
+
48
+The `Server` model and enums (`ServerType`, `ConnectionStatus`) are defined in `core/models/server.dart`.
49
+
50
+---
51
+
52
+## API Service
53
+
54
+`ServerRepository` is defined in `core/repository/server/server_repository.dart`.
55
+
56
+```dart
57
+class ServerRepository {
58
+ final Dio _dio;
59
+
60
+ Future<bool> testConnection(Server server);
61
+ Future<List<ModelInfo>> fetchModels(Server server);
62
+ Future<int?> pingServer(Server server);
63
+}
64
+```
65
+
66
+**LM Studio specifics:**
67
+- Models: `GET /v1/models` → OpenAI-compatible model list
68
+- Health: `GET /v1/models` returning 200 = healthy
69
+
70
+**Ollama specifics:**
71
+- Models: `GET /api/tags` → `{ models: [...] }`
72
+- Health: `GET /api/tags` returning 200 = healthy
73
+
74
+**OpenRouter specifics:**
75
+- Models: `GET /models` with `Authorization: Bearer {apiKey}`
76
+- Health: `GET /models` returning 200 = healthy
77
+
78
+---
79
+
80
+## Providers
81
+
82
+Location: `features/server_connection/providers/server_connection_providers.dart`
83
+
84
+### Provider Organization
85
+
86
+```dart
87
+// 1. Repository Provider
88
+final serverRepositoryProvider = Provider<ServerRepository>((ref) {
89
+ return ServerRepository(ref.read(dioProvider));
90
+});
91
+
92
+// 2. All saved servers
93
+final serversProvider = StateNotifierProvider<ServersNotifier, List<Server>>((ref) {
94
+ final repository = ref.watch(serverRepositoryProvider);
95
+ return ServersNotifier(repository, ref.read(hiveBoxesProvider).servers);
96
+});
97
+
98
+// 3. Active server
99
+final activeServerProvider = StateNotifierProvider<ActiveServerNotifier, Server?>((ref) {
100
+ return ActiveServerNotifier(ref.read(hiveBoxesProvider).settings);
101
+});
102
+
103
+// 4. Connection status for a specific server
104
+final connectionStatusProvider = FutureProvider.family<ConnectionStatus, String>((ref, serverId) async {
105
+ final server = ref.read(serversProvider).firstWhere((s) => s.id == serverId);
106
+ final repo = ref.read(serverRepositoryProvider);
107
+ final isConnected = await repo.testConnection(server);
108
+ return isConnected ? ConnectionStatus.connected : ConnectionStatus.disconnected;
109
+});
110
+```
111
+
112
+### `ServersNotifier` StateNotifier
113
+
114
+```dart
115
+class ServersNotifier extends StateNotifier<List<Server>> {
116
+ final ServerRepository _repository;
117
+ final Box<Server> _box;
118
+
119
+ ServersNotifier(this._repository, this._box) : super([]) {
120
+ loadServers();
121
+ }
122
+
123
+ void loadServers() {
124
+ state = _box.values.toList();
125
+ }
126
+
127
+ Future<void> addServer(Server server) async { ... }
128
+ Future<void> updateServer(Server server) async { ... }
129
+ Future<void> deleteServer(String serverId) async { ... }
130
+ Future<void> setDefault(String serverId) async { ... }
131
+ Future<ConnectionStatus> testConnection(String serverId) async { ... }
132
+}
133
+```
134
+
135
+### `ActiveServerNotifier` StateNotifier
136
+
137
+```dart
138
+class ActiveServerNotifier extends StateNotifier<Server?> {
139
+ final Box<AppSettings> _settingsBox;
140
+
141
+ ActiveServerNotifier(this._settingsBox) : super(null) {
142
+ _loadActiveServer();
143
+ }
144
+
145
+ void _loadActiveServer() { ... }
146
+ Future<void> setActiveServer(String? serverId) async { ... }
147
+}
148
+```
149
+
150
+---
151
+
152
+## Views
153
+
154
+### `server_list_page.dart`
155
+
156
+**Layout:**
157
+- App bar with title "Servers" and "+" add button
158
+- List of server cards showing:
159
+ - Server name (bold)
160
+ - Server type icon (LM Studio / Ollama / OpenRouter)
161
+ - Host:Port or "Cloud" for OpenRouter
162
+ - Connection status indicator
163
+ - "Default" badge if marked as default
164
+- Empty state: illustration + "Add your first server" CTA
165
+- Pull-to-refresh to re-test all connections
166
+
167
+**Interactions:**
168
+- Tap card → navigate to edit server
169
+- Long-press → context menu (Edit, Delete, Set Default, Test Connection)
170
+- Swipe left → delete with confirmation
171
+
172
+### `add_server_page.dart`
173
+
174
+**Layout:**
175
+- Server type selector (3 segmented buttons: LM Studio, Ollama, OpenRouter)
176
+- Form fields that change based on type:
177
+
178
+**For LM Studio / Ollama:**
179
+| Field | Type | Validation |
180
+| ----------- | ----------- | ----------------------------- |
181
+| Name | Text | Required, 1–50 chars |
182
+| Host/IP | Text | Required, valid IP or hostname|
183
+| Port | Number | Required, 1–65535 |
184
+| API Key | Text (opt) | Optional |
185
+
186
+**For OpenRouter:**
187
+| Field | Type | Validation |
188
+| ----------- | ----------- | ----------------------------- |
189
+| Name | Text | Required, 1–50 chars |
190
+| API Key | Text | Required, starts with `sk-` |
191
+
192
+- "Test Connection" button → shows result toast
193
+- "Save" button → validates, tests connection, saves to Hive
194
+- Auto-fill defaults: LM Studio port = 1234, Ollama port = 11434
195
+
196
+**Error handling:**
197
+- Connection refused → "Cannot reach server. Is it running?"
198
+- Timeout → "Server took too long to respond. Check the address."
199
+- 401 → "Invalid API key."
200
+- Generic → show raw error message
201
+
202
+---
203
+
204
+## Components
205
+
206
+### `server_card.dart`
207
+
208
+A card showing:
209
+- Leading: Server type icon
210
+- Title: Server name
211
+- Subtitle: `host:port` or `openrouter.ai`
212
+- Trailing: `ConnectionStatusIndicator` + chevron
213
+- Default badge (if applicable)
214
+
215
+### `connection_status_indicator.dart`
216
+
217
+Animated dot:
218
+- Green (pulsing) = Connected
219
+- Red (static) = Error
220
+- Grey = Not checked
221
+- Loading spinner = Checking
222
+
223
+### `server_type_selector.dart`
224
+
225
+Three-segment toggle:
226
+- LM Studio (terminal icon)
227
+- Ollama (llama emoji)
228
+- OpenRouter (cloud icon)
229
+
230
+---
231
+
232
+## Background Connection Monitoring
233
+
234
+- When app is in foreground, periodically ping the active server every 30 seconds.
235
+- Update `connectionStatusProvider` accordingly.
236
+- Show a subtle banner at top of chat if connection is lost.
237
+
238
+---
239
+
240
+## Acceptance Criteria
241
+
242
+- [ ] User can add an LM Studio server with IP + port and test connection.
243
+- [ ] User can add an Ollama server with IP + port and test connection.
244
+- [ ] User can add an OpenRouter server with API key and test connection.
245
+- [ ] Server list displays all saved servers with connection status.
246
+- [ ] User can edit and delete servers.
247
+- [ ] User can set a default server.
248
+- [ ] Connection status auto-updates with visual indicator.
249
+- [ ] Server profiles persist across app restarts (Hive).
250
+- [ ] Connection errors show meaningful error messages.
251
+- [ ] Empty state is handled gracefully.
plans/02_chat_interface.md
new
+362
@@ -0,0 +1,362 @@
1
+# Feature 02 — Chat Interface
2
+
3
+> **Phase:** 1–2 — Core + Chat Polish
4
+> **Status:** Pending
5
+> **Depends on:** `00_project_setup`, `01_server_connection`, `07_core_architecture`
6
+
7
+---
8
+
9
+## Goal
10
+
11
+Build a ChatGPT/Claude-quality chat interface with:
12
+- Beautiful message bubbles with markdown rendering
13
+- Real-time streaming responses (SSE / chunked transfer)
14
+- Rich input bar with actions
15
+- Message interactions (copy, retry, delete, edit)
16
+- Typing indicators and loading states
17
+
18
+---
19
+
20
+## Feature Folder Structure
21
+
22
+```
23
+features/chat/
24
+├── data/
25
+│ └── models/ # (Uses core/models/message.dart)
26
+│
27
+├── providers/
28
+│ └── chat_providers.dart
29
+│
30
+└── views/
31
+ ├── chat_page.dart # Main chat screen
32
+ └── components/
33
+ ├── chat_bubble.dart
34
+ ├── chat_input_bar.dart
35
+ ├── typing_indicator.dart
36
+ ├── message_action_bar.dart
37
+ └── code_block.dart
38
+```
39
+
40
+---
41
+
42
+## Dependencies Used
43
+
44
+| Package | Purpose |
45
+| ------------------------ | ------------------------------- |
46
+| `dio` | HTTP client with streaming |
47
+| `flutter_markdown` | Render markdown in messages |
48
+| `flutter_highlight` | Code block syntax highlighting |
49
+| `haptic_feedback` | Tactile feedback on actions |
50
+
51
+---
52
+
53
+## Data Model
54
+
55
+The `Message` model and enums (`MessageRole`, `MessageStatus`) are defined in `core/models/message.dart`.
56
+
57
+---
58
+
59
+## API Integration — Streaming Chat Completions
60
+
61
+### LM Studio & OpenRouter (OpenAI-compatible)
62
+
63
+```
64
+POST /v1/chat/completions
65
+Headers:
66
+ Content-Type: application/json
67
+ Authorization: Bearer {apiKey} // OpenRouter only
68
+Body:
69
+{
70
+ "model": "model-id",
71
+ "messages": [
72
+ {"role": "system", "content": "..."},
73
+ {"role": "user", "content": "..."},
74
+ {"role": "assistant", "content": "..."}
75
+ ],
76
+ "temperature": 0.7,
77
+ "top_p": 0.9,
78
+ "max_tokens": 2048,
79
+ "stream": true
80
+}
81
+```
82
+
83
+**Streaming response format (SSE):**
84
+```
85
+data: {"choices":[{"delta":{"content":"Hello"}}]}
86
+data: {"choices":[{"delta":{"content":" world"}}]}
87
+data: [DONE]
88
+```
89
+
90
+### Ollama
91
+
92
+```
93
+POST /api/chat
94
+Body:
95
+{
96
+ "model": "model-name",
97
+ "messages": [
98
+ {"role": "system", "content": "..."},
99
+ {"role": "user", "content": "..."}
100
+ ],
101
+ "stream": true,
102
+ "options": {
103
+ "temperature": 0.7,
104
+ "top_p": 0.9,
105
+ "num_predict": 2048
106
+ }
107
+}
108
+```
109
+
110
+**Streaming response format (NDJSON):**
111
+```json
112
+{"message":{"role":"assistant","content":"Hello"},"done":false}
113
+{"message":{"role":"assistant","content":" world"},"done":false}
114
+{"message":{"role":"assistant","content":""},"done":true,"total_duration":...}
115
+```
116
+
117
+---
118
+
119
+## Chat Services
120
+
121
+`ChatService` and implementations are defined in `core/repository/chat/`.
122
+
123
+```dart
124
+abstract class ChatService {
125
+ Stream<String> sendMessage({
126
+ required Server server,
127
+ required String modelId,
128
+ required List<Message> messages,
129
+ required ChatParameters params,
130
+ });
131
+
132
+ void cancelStream();
133
+}
134
+```
135
+
136
+### Streaming Implementation Details
137
+
138
+1. **Context window management:** Truncate message history to fit within `contextLength`. Keep system prompt + last N messages.
139
+2. **Token estimation:** Use 4 chars ≈ 1 token heuristic.
140
+3. **Error recovery:** If stream breaks, save partial and mark as error.
141
+4. **Cancel token:** Pass `CancelToken` to Dio for user cancellation.
142
+
143
+---
144
+
145
+## Providers
146
+
147
+Location: `features/chat/providers/chat_providers.dart`
148
+
149
+### Provider Organization
150
+
151
+```dart
152
+// 1. Chat Service (factory based on active server type)
153
+final chatServiceProvider = Provider<ChatService>((ref) {
154
+ final server = ref.watch(activeServerProvider);
155
+ if (server == null) throw StateError('No active server');
156
+ return ChatService.forServer(server.type, ref.read(dioProvider));
157
+});
158
+
159
+// 2. Chat Parameters (from settings)
160
+final chatParamsProvider = Provider<ChatParameters>((ref) {
161
+ final settings = ref.watch(settingsProvider);
162
+ return ChatParameters(
163
+ temperature: settings.temperature,
164
+ topP: settings.topP,
165
+ maxTokens: settings.maxTokens,
166
+ contextLength: settings.contextLength,
167
+ );
168
+});
169
+
170
+// 3. Selected model for current chat
171
+final selectedModelProvider = StateProvider<ModelInfo?>((ref) => null);
172
+
173
+// 4. Streaming state
174
+final isStreamingProvider = StateProvider<bool>((ref) => false);
175
+
176
+// 5. Messages for current conversation
177
+final messagesProvider = StateNotifierProvider<MessagesNotifier, List<Message>>((ref) {
178
+ final repository = ref.watch(messageRepositoryProvider);
179
+ final conversation = ref.watch(activeConversationProvider);
180
+ return MessagesNotifier(repository, conversation?.id);
181
+});
182
+
183
+// 6. Message list state
184
+class MessagesNotifier extends StateNotifier<List<Message>> {
185
+ final MessageRepository _repository;
186
+ final String? _conversationId;
187
+
188
+ MessagesNotifier(this._repository, this._conversationId) : super([]) {
189
+ if (_conversationId != null) loadMessages();
190
+ }
191
+
192
+ Future<void> loadMessages() async { ... }
193
+ Future<void> sendMessage(String content) async { ... }
194
+ Future<void> retryLastMessage() async { ... }
195
+ Future<void> editMessage(String messageId, String newContent) async { ... }
196
+ Future<void> deleteMessage(String messageId) async { ... }
197
+ void cancelStream() { ... }
198
+ Future<void> clearConversation() async { ... }
199
+}
200
+```
201
+
202
+---
203
+
204
+## Views
205
+
206
+### `chat_page.dart`
207
+
208
+**Layout (top to bottom):**
209
+
210
+```
211
+┌──────────────────────────────────────┐
212
+│ AppBar │
213
+│ [☰ Drawer] [Model: GPT-4] [⋮ Menu]│
214
+├──────────────────────────────────────┤
215
+│ │
216
+│ Message List (scrollable) │
217
+│ │
218
+│ ┌─────────────────────────────┐ │
219
+│ │ 👤 User message │ │
220
+│ └─────────────────────────────┘ │
221
+│ │
222
+│ ┌─────────────────────────────┐ │
223
+│ │ 🤖 Assistant response │ │
224
+│ │ (markdown rendered) │ │
225
+│ │ [Copy] [Retry] [⋮] │ │
226
+│ └─────────────────────────────┘ │
227
+│ │
228
+│ ┌─────────────────────────────┐ │
229
+│ │ ⏳ Streaming indicator... │ │
230
+│ └─────────────────────────────┘ │
231
+│ │
232
+├──────────────────────────────────────┤
233
+│ Input Bar │
234
+│ [📎] [ Type a message... ] [▶] │
235
+│ [Stop Generating] (if streaming) │
236
+└──────────────────────────────────────┘
237
+```
238
+
239
+**App bar details:**
240
+- Left: Hamburger menu to open conversation drawer
241
+- Center: Current model name (tappable → opens model picker)
242
+- Right: overflow menu (New Chat, Clear, Chat Settings)
243
+
244
+**Empty state (new conversation):**
245
+- Show LocalMind logo centered
246
+- "Start a conversation" subtitle
247
+- Quick-start prompt suggestions as tappable chips
248
+
249
+---
250
+
251
+## Components
252
+
253
+### `chat_bubble.dart`
254
+
255
+**User messages:**
256
+- Right-aligned, filled background
257
+- Plain text or light markdown
258
+- Timestamp on bottom-right
259
+
260
+**Assistant messages:**
261
+- Left-aligned, transparent/subtle surface background
262
+- Full markdown rendering (headers, bold, italic, lists, code blocks, links, tables)
263
+- Action buttons: Copy | Retry | ⋮
264
+
265
+**System messages:**
266
+- Centered, muted text, italic
267
+- Toggleable in settings
268
+
269
+**Streaming state:**
270
+- Show text as it arrives
271
+- Blinking cursor at end
272
+- Auto-scroll to bottom
273
+
274
+### `chat_input_bar.dart`
275
+
276
+**Layout:**
277
+- Multi-line text field (max 6 lines, expanding)
278
+- Left: attachment button (📎) — future multimodal
279
+- Right: send button (▶) → stop button (⏹) when streaming
280
+- Placeholder: "Message LocalMind..."
281
+
282
+**Behavior:**
283
+- Send on button tap or keyboard send action
284
+- Disable when input empty or disconnected
285
+- Character/token count near limits
286
+- Haptic feedback on send
287
+
288
+### `typing_indicator.dart`
289
+
290
+- Three bouncing dots animation
291
+- Shown while waiting for first token
292
+
293
+### `message_action_bar.dart`
294
+
295
+- **Copy** — copy full text to clipboard
296
+- **Retry** — regenerate response
297
+- **Edit** (user messages) — open edit mode
298
+- **Delete** — remove with confirmation
299
+- **More (⋮)** — bottom sheet:
300
+ - "Copy as Markdown"
301
+ - "Share"
302
+ - "View raw"
303
+ - "Token count: {n}"
304
+
305
+### `code_block.dart`
306
+
307
+- Language detection from fence info string
308
+- Syntax highlighting via `flutter_highlight`
309
+- Header bar: language name + copy button
310
+- Dark background, horizontal scroll
311
+- Monospace font (Fira Code / JetBrains Mono)
312
+
313
+---
314
+
315
+## Scroll Behavior
316
+
317
+- Auto-scroll to bottom on new message (if user already near bottom)
318
+- If user scrolled up, show "New messages ↓" floating button
319
+- Smooth scroll animation
320
+- Scroll-to-bottom FAB when away from bottom
321
+
322
+---
323
+
324
+## Error Handling
325
+
326
+| Error | User-facing message |
327
+| ---------------------------- | -------------------------------------------------------- |
328
+| Server unreachable | "Can't reach {server name}. Check your connection." |
329
+| Model not found | "Model '{name}' is no longer available." |
330
+| Context length exceeded | "Conversation too long. Some older messages were trimmed."|
331
+| Stream interrupted | "Response was interrupted. Tap retry to try again." |
332
+| Rate limit (OpenRouter) | "Rate limit reached. Please wait a moment." |
333
+| Generic API error | Show raw error + "Tap to retry" |
334
+
335
+---
336
+
337
+## Performance Considerations
338
+
339
+- **ListView.builder** for message list — only render visible
340
+- **Markdown rendering** — lazy, only visible messages
341
+- **Syntax highlighting** — cache highlighted blocks
342
+- **Long conversations** — for 500+ messages, consider pagination
343
+
344
+---
345
+
346
+## Acceptance Criteria
347
+
348
+- [ ] User can type and send a message.
349
+- [ ] Response streams in real-time (LM Studio).
350
+- [ ] Response streams in real-time (Ollama).
351
+- [ ] Response streams in real-time (OpenRouter).
352
+- [ ] Markdown renders correctly.
353
+- [ ] Code blocks have syntax highlighting and copy button.
354
+- [ ] User can copy a full message.
355
+- [ ] User can retry a failed response.
356
+- [ ] User can delete individual messages.
357
+- [ ] Typing indicator shows while waiting.
358
+- [ ] "Stop generating" button cancels stream.
359
+- [ ] Scroll behavior follows spec.
360
+- [ ] Long conversations (100+ messages) scroll smoothly.
361
+- [ ] Errors display user-friendly messages with retry.
362
+- [ ] Haptic feedback on send.
plans/03_model_management.md
new
+273
@@ -0,0 +1,273 @@
1
+# Feature 03 — Model Management
2
+
3
+> **Phase:** 2 — Chat Polish
4
+> **Status:** Pending
5
+> **Depends on:** `01_server_connection`, `07_core_architecture`
6
+
7
+---
8
+
9
+## Goal
10
+
11
+Let users browse, select, and swap LLM models from their connected servers. Display model metadata and allow mid-conversation model switching.
12
+
13
+---
14
+
15
+## Feature Folder Structure
16
+
17
+```
18
+features/model_management/
19
+├── data/
20
+│ └── models/ # (Uses core/models/model_info.dart)
21
+│
22
+├── providers/
23
+│ └── model_management_providers.dart
24
+│
25
+└── views/
26
+ ├── model_picker_sheet.dart # Bottom sheet for model selection
27
+ └── components/
28
+ ├── model_tile.dart
29
+ └── model_metadata_chip.dart
30
+```
31
+
32
+---
33
+
34
+## Dependencies Used
35
+
36
+| Package | Purpose |
37
+| ------------------- | ---------------------------- |
38
+| `dio` | Fetch model lists from APIs |
39
+| `flutter_riverpod` | Model state management |
40
+
41
+---
42
+
43
+## Data Model
44
+
45
+`ModelInfo` is defined in `core/models/model_info.dart`.
46
+
47
+```dart
48
+class ModelInfo {
49
+ final String id;
50
+ final String name;
51
+ final String? description;
52
+ final int? parameterCount;
53
+ final int? contextLength;
54
+ final int? fileSize;
55
+ final String? quantization;
56
+ final String? architecture;
57
+ final ServerType serverType;
58
+ final String serverId;
59
+ final DateTime? modifiedAt;
60
+}
61
+```
62
+
63
+---
64
+
65
+## API Response Parsing
66
+
67
+### LM Studio (`GET /v1/models`)
68
+
69
+```json
70
+{
71
+ "data": [
72
+ {
73
+ "id": "llama-3.2-3b-instruct",
74
+ "object": "model",
75
+ "owned_by": "organization-owner"
76
+ }
77
+ ]
78
+}
79
+```
80
+
81
+- LM Studio provides minimal metadata — only `id`
82
+- Parse model name from ID (replace hyphens/underscores with spaces, capitalize)
83
+- Try to extract parameter count from name (e.g., "3b" → 3 billion)
84
+
85
+### Ollama (`GET /api/tags`)
86
+
87
+```json
88
+{
89
+ "models": [
90
+ {
91
+ "name": "llama3.2:3b",
92
+ "model": "llama3.2:3b",
93
+ "size": 2019393189,
94
+ "digest": "...",
95
+ "details": {
96
+ "parent_model": "",
97
+ "format": "gguf",
98
+ "family": "llama",
99
+ "families": ["llama"],
100
+ "parameter_size": "3.2B",
101
+ "quantization_level": "Q4_K_M"
102
+ },
103
+ "modified_at": "2024-..."
104
+ }
105
+ ]
106
+}
107
+```
108
+
109
+- Ollama provides rich metadata
110
+- Parse `parameter_size` to extract number
111
+- Use `details.family` for architecture
112
+- Use `size` for file size display
113
+
114
+### OpenRouter (`GET /v1/models`)
115
+
116
+```json
117
+{
118
+ "data": [
119
+ {
120
+ "id": "google/gemma-2-9b-it",
121
+ "name": "Google: Gemma 2 9B",
122
+ "description": "...",
123
+ "context_length": 8192,
124
+ "pricing": { "prompt": "0.00003", "completion": "0.00006" },
125
+ "architecture": { "modality": "text->text", "tokenizer": "Gemini" }
126
+ }
127
+ ]
128
+}
129
+```
130
+
131
+- OpenRouter provides most metadata including pricing
132
+- Filter to text-only models
133
+
134
+---
135
+
136
+## Providers
137
+
138
+Location: `features/model_management/providers/model_management_providers.dart`
139
+
140
+### Provider Organization
141
+
142
+```dart
143
+// 1. Available models for active server
144
+final availableModelsProvider = FutureProvider<List<ModelInfo>>((ref) async {
145
+ final server = ref.watch(activeServerProvider);
146
+ if (server == null) return [];
147
+ return ref.read(serverRepositoryProvider).fetchModels(server);
148
+});
149
+
150
+// 2. Currently selected model (stored in conversation or globally)
151
+final selectedModelProvider = StateProvider<ModelInfo?>((ref) => null);
152
+
153
+// 3. Search/filter query
154
+final modelSearchQueryProvider = StateProvider<String>((ref) => '');
155
+
156
+// 4. Filtered models based on search
157
+final filteredModelsProvider = Provider<List<ModelInfo>>((ref) {
158
+ final models = ref.watch(availableModelsProvider).value ?? [];
159
+ final query = ref.watch(modelSearchQueryProvider).toLowerCase();
160
+ if (query.isEmpty) return models;
161
+ return models.where((m) =>
162
+ m.name.toLowerCase().contains(query) ||
163
+ m.id.toLowerCase().contains(query)
164
+ ).toList();
165
+});
166
+
167
+// 5. Model cache (5 minute TTL per server)
168
+final modelCacheProvider = StateNotifierProvider<ModelCacheNotifier, Map<String, CachedModels>>((ref) {
169
+ return ModelCacheNotifier();
170
+});
171
+
172
+class ModelCacheNotifier extends StateNotifier<Map<String, CachedModels>> {
173
+ // Cache management with 5-minute TTL
174
+}
175
+```
176
+
177
+---
178
+
179
+## Views
180
+
181
+### `model_picker_sheet.dart`
182
+
183
+**Trigger:** Tap on model name in chat app bar.
184
+
185
+**Layout:**
186
+
187
+```
188
+┌──────────────────────────────────────┐
189
+│ ═══ (drag handle) │
190
+│ │
191
+│ Select Model │
192
+│ [🔍 Search models... ] │
193
+│ │
194
+│ ── Active Server: My Desktop ── │
195
+│ │
196
+│ ┌──────────────────────────────┐ │
197
+│ │ ✅ Llama 3.2 3B Instruct │ │
198
+│ │ 3.2B params · Q4_K_M │ │
199
+│ │ 1.9 GB · 4096 context │ │
200
+│ └──────────────────────────────┘ │
201
+│ │
202
+│ ┌──────────────────────────────┐ │
203
+│ │ Mistral 7B Instruct │ │
204
+│ │ 7B params · Q4_K_M │ │
205
+│ │ 3.8 GB · 8192 context │ │
206
+│ └──────────────────────────────┘ │
207
+│ │
208
+│ [Refresh Models] │
209
+└──────────────────────────────────────┘
210
+```
211
+
212
+**Features:**
213
+- Search/filter bar at top
214
+- Selected model has checkmark
215
+- Each tile: name, parameter count, quantization, size, context length
216
+- Gracefully omit unavailable metadata
217
+- "Refresh Models" button to re-fetch
218
+- Loading skeleton while fetching
219
+- Error state with retry
220
+
221
+---
222
+
223
+## Components
224
+
225
+### `model_tile.dart`
226
+
227
+- Leading: Model architecture icon or first letter avatar
228
+- Title: Model display name
229
+- Subtitle: Metadata chips
230
+- Trailing: Checkmark if selected
231
+- Tap → select, dismiss sheet
232
+
233
+### `model_metadata_chip.dart`
234
+
235
+Small pill labels:
236
+- `3.2B` (parameter count)
237
+- `Q4_K_M` (quantization)
238
+- `1.9 GB` (file size)
239
+- `4096 ctx` (context length)
240
+
241
+---
242
+
243
+## Mid-Chat Model Swapping
244
+
245
+1. Update `selectedModelProvider` with new model
246
+2. Show system divider: "Switched to {model name}"
247
+3. Subsequent messages use new model
248
+4. `Message.modelId` records which model generated each message
249
+5. No conversation reset — continue with existing history
250
+
251
+---
252
+
253
+## Model Caching
254
+
255
+- Cache model list per server (in-memory)
256
+- Cache duration: 5 minutes
257
+- Force refresh via "Refresh Models" button
258
+- When switching servers, clear and re-fetch
259
+
260
+---
261
+
262
+## Acceptance Criteria
263
+
264
+- [ ] Model list fetched for LM Studio servers.
265
+- [ ] Model list fetched for Ollama servers (full metadata).
266
+- [ ] Model list fetched for OpenRouter servers.
267
+- [ ] User can search/filter models by name.
268
+- [ ] Selected model is visually indicated.
269
+- [ ] Tapping a model selects and closes picker.
270
+- [ ] Model can be swapped mid-conversation.
271
+- [ ] Model metadata displays correctly.
272
+- [ ] Loading and error states handled.
273
+- [ ] Model list is cached and can be force-refreshed.
plans/04_conversation_management.md
new
+271
@@ -0,0 +1,271 @@
1
+# Feature 04 — Conversation Management
2
+
3
+> **Phase:** 3 — Features
4
+> **Status:** Pending
5
+> **Depends on:** `00_project_setup`, `02_chat_interface`, `07_core_architecture`
6
+
7
+---
8
+
9
+## Goal
10
+
11
+Provide a full conversation lifecycle: create, rename, delete, search, and pin conversations. Build a ChatGPT-style sidebar/drawer for browsing conversation history.
12
+
13
+---
14
+
15
+## Feature Folder Structure
16
+
17
+```
18
+features/conversation/
19
+├── data/
20
+│ └── models/ # (Uses core/models/conversation.dart)
21
+│
22
+├── providers/
23
+│ └── conversation_providers.dart
24
+│
25
+└── views/
26
+ ├── conversation_drawer.dart # Sidebar navigation
27
+ └── components/
28
+ ├── conversation_tile.dart
29
+ ├── conversation_search_bar.dart
30
+ └── date_section_header.dart
31
+```
32
+
33
+---
34
+
35
+## Dependencies Used
36
+
37
+| Package | Purpose |
38
+| ------------------- | -------------------------------- |
39
+| `hive_ce` | Persist conversations locally |
40
+| `flutter_riverpod` | Conversation state management |
41
+
42
+---
43
+
44
+## Data Model
45
+
46
+The `Conversation` model is defined in `core/models/conversation.dart`.
47
+
48
+---
49
+
50
+## Providers
51
+
52
+Location: `features/conversation/providers/conversation_providers.dart`
53
+
54
+### Provider Organization
55
+
56
+```dart
57
+// 1. All conversations
58
+final conversationsProvider = StateNotifierProvider<ConversationsNotifier, List<Conversation>>((ref) {
59
+ final box = ref.watch(hiveBoxesProvider).conversations;
60
+ return ConversationsNotifier(box);
61
+});
62
+
63
+// 2. Currently active conversation
64
+final activeConversationProvider = StateNotifierProvider<ActiveConversationNotifier, Conversation?>((ref) {
65
+ final conversationsBox = ref.watch(hiveBoxesProvider).conversations;
66
+ final settingsBox = ref.watch(hiveBoxesProvider).settings;
67
+ return ActiveConversationNotifier(conversationsBox, settingsBox);
68
+});
69
+
70
+// 3. Search query
71
+final conversationSearchProvider = StateProvider<String>((ref) => '');
72
+
73
+// 4. Filtered conversations based on search
74
+final filteredConversationsProvider = Provider<List<Conversation>>((ref) {
75
+ final convos = ref.watch(conversationsProvider);
76
+ final query = ref.watch(conversationSearchProvider).toLowerCase();
77
+ if (query.isEmpty) return convos;
78
+ return convos.where((c) =>
79
+ c.title.toLowerCase().contains(query) ||
80
+ (c.lastMessagePreview?.toLowerCase().contains(query) ?? false)
81
+ ).toList();
82
+});
83
+```
84
+
85
+### `ConversationsNotifier`
86
+
87
+```dart
88
+class ConversationsNotifier extends StateNotifier<List<Conversation>> {
89
+ final Box<Conversation> _box;
90
+
91
+ ConversationsNotifier(this._box) : super([]) {
92
+ loadConversations();
93
+ }
94
+
95
+ Future<void> loadConversations();
96
+ Future<Conversation> createConversation({String? title, String? personaId, String? systemPrompt});
97
+ Future<void> renameConversation(String id, String newTitle);
98
+ Future<void> deleteConversation(String id);
99
+ Future<void> togglePin(String id);
100
+ Future<void> updatePreview(String id, String preview, DateTime updatedAt);
101
+ Future<void> deleteAll();
102
+}
103
+```
104
+
105
+### `ActiveConversationNotifier`
106
+
107
+```dart
108
+class ActiveConversationNotifier extends StateNotifier<Conversation?> {
109
+ final Box<Conversation> _conversationsBox;
110
+ final Box<AppSettings> _settingsBox;
111
+
112
+ ActiveConversationNotifier(this._conversationsBox, this._settingsBox) : super(null) {
113
+ _loadActiveConversation();
114
+ }
115
+
116
+ void _loadActiveConversation() { ... }
117
+ Future<void> setActiveConversation(String? id) async { ... }
118
+}
119
+```
120
+
121
+---
122
+
123
+## Auto-Title Generation
124
+
125
+When a new conversation is started:
126
+
127
+1. First message sent with title "New Chat"
128
+2. After first assistant response, auto-generate title
129
+3. **Method A (preferred):** Send lightweight title generation request:
130
+ ```
131
+ System: Generate a short title (max 6 words) for the following conversation.
132
+ Respond with ONLY the title, no quotes or punctuation.
133
+ User: {first user message}
134
+ Assistant: {first assistant response (truncated to 200 chars)}
135
+ ```
136
+4. **Method B (fallback):** Use first 50 chars of first user message
137
+5. Update conversation title in Hive
138
+
139
+---
140
+
141
+## Views
142
+
143
+### `conversation_drawer.dart`
144
+
145
+Primary navigation element, opened from hamburger menu.
146
+
147
+**Layout:**
148
+
149
+```
150
+┌──────────────────────────────────────┐
151
+│ LocalMind [⚙️] │
152
+│ │
153
+│ [+ New Chat] │
154
+│ │
155
+│ [🔍 Search conversations... ] │
156
+│ │
157
+│ ── PINNED ── │
158
+│ 📌 Project Brainstorm │
159
+│ "Let me help you with..." │
160
+│ 3 hours ago │
161
+│ │
162
+│ ── TODAY ── │
163
+│ 💬 How to cook pasta │
164
+│ "Here's a simple recipe..." │
165
+│ 2 hours ago │
166
+│ │
167
+│ ── YESTERDAY ── │
168
+│ 💬 Explain quantum computing │
169
+│ "At its core, quantum..." │
170
+│ Yesterday │
171
+│ │
172
+│ ───────────────────── │
173
+│ [🖥 Servers] [👤 Personas] │
174
+│ [⚙️ Settings] │
175
+└──────────────────────────────────────┘
176
+```
177
+
178
+**Section grouping:**
179
+- Pinned (always on top)
180
+- Today
181
+- Yesterday
182
+- Previous 7 Days
183
+- Previous 30 Days
184
+- Older
185
+
186
+**Interactions:**
187
+- Tap → load and switch to conversation
188
+- Long-press → context menu: Rename, Pin/Unpin, Delete
189
+- Swipe left → delete with undo snackbar
190
+- Active conversation highlighted
191
+
192
+---
193
+
194
+## Components
195
+
196
+### `conversation_tile.dart`
197
+
198
+- Leading: Pin icon (if pinned) or message icon
199
+- Title: Conversation title (single line, ellipsis)
200
+- Subtitle: Last message preview (single line, muted)
201
+- Trailing: Relative timestamp ("2h ago", "Yesterday")
202
+- Active state: subtle accent background
203
+- Slidable: swipe-to-delete
204
+
205
+### `conversation_search_bar.dart`
206
+
207
+- ShadCN-styled search input
208
+- Clear button when text present
209
+- Debounced search (300ms)
210
+
211
+### `date_section_header.dart`
212
+
213
+- Uppercase, small text, muted color
214
+- "TODAY", "YESTERDAY", "PREVIOUS 7 DAYS", etc.
215
+
216
+---
217
+
218
+## Conversation Lifecycle
219
+
220
+### Create
221
+1. User taps "+ New Chat"
222
+2. Create new `Conversation` with title "New Chat"
223
+3. Save to Hive
224
+4. Set as active conversation
225
+5. Navigate to chat screen
226
+6. Auto-title after first exchange
227
+
228
+### Switch
229
+1. User taps conversation in drawer
230
+2. Set as active conversation
231
+3. Load messages from Hive
232
+4. Restore model selection if stored
233
+5. Close drawer, show chat
234
+
235
+### Delete
236
+1. User swipes or selects delete
237
+2. Show confirmation dialog
238
+3. Delete conversation + all messages
239
+4. If deleting active, switch to most recent or create new
240
+5. Show undo snackbar (5 seconds)
241
+
242
+### Rename
243
+1. Select rename from context menu
244
+2. Show inline edit or dialog
245
+3. Save on confirm
246
+
247
+---
248
+
249
+## Storage
250
+
251
+- Conversations in `Hive.box<Conversation>('conversations')`
252
+- Messages in `Hive.box<Message>('messages')` with `conversationId` field
253
+- Cascade delete messages on conversation delete
254
+- Order by: `isPinned DESC, updatedAt DESC`
255
+
256
+---
257
+
258
+## Acceptance Criteria
259
+
260
+- [ ] User can create a new conversation.
261
+- [ ] User can switch between conversations.
262
+- [ ] User can rename a conversation.
263
+- [ ] User can delete a conversation (with confirmation).
264
+- [ ] User can pin/unpin a conversation.
265
+- [ ] Conversations grouped by date in sidebar.
266
+- [ ] Search filters by title and message preview.
267
+- [ ] Auto-title generates after first exchange.
268
+- [ ] Active conversation highlighted in sidebar.
269
+- [ ] Empty state shows "No conversations yet" CTA.
270
+- [ ] Deleting active conversation switches or creates new.
271
+- [ ] Conversation data persists across restarts.
plans/05_templates_personas.md
new
+279
@@ -0,0 +1,279 @@
1
+# Feature 05 — Templates & Personas
2
+
3
+> **Phase:** 3 — Features
4
+> **Status:** Pending
5
+> **Depends on:** `00_project_setup`, `04_conversation_management`, `07_core_architecture`
6
+
7
+---
8
+
9
+## Goal
10
+
11
+Allow users to define reusable AI personas (system prompts + identity) that shape model behavior. Include built-in presets and let users create custom personas.
12
+
13
+---
14
+
15
+## Feature Folder Structure
16
+
17
+```
18
+features/persona/
19
+├── data/
20
+│ └── models/ # (Uses core/models/persona.dart)
21
+│
22
+├── providers/
23
+│ └── persona_providers.dart
24
+│
25
+└── views/
26
+ ├── persona_list_page.dart # Browse personas
27
+ ├── create_persona_page.dart # Create/edit persona
28
+ └── components/
29
+ ├── persona_card.dart
30
+ └── persona_category_chips.dart
31
+```
32
+
33
+---
34
+
35
+## Dependencies Used
36
+
37
+| Package | Purpose |
38
+| ------------------- | ----------------------------- |
39
+| `hive_ce` | Persist personas locally |
40
+| `flutter_riverpod` | Persona state management |
41
+
42
+---
43
+
44
+## Data Model
45
+
46
+The `Persona` model is defined in `core/models/persona.dart`.
47
+
48
+---
49
+
50
+## Built-in Presets
51
+
52
+These ship with the app and cannot be deleted (but can be cloned):
53
+
54
+### 1. General Assistant 🤖
55
+```
56
+You are a helpful, knowledgeable AI assistant. Provide clear, accurate, and concise responses. When you're not sure about something, say so. Use markdown formatting for structured responses.
57
+```
58
+Category: General
59
+
60
+### 2. Code Assistant 🧑💻
61
+```
62
+You are an expert software engineer. Help with coding questions, debugging, code reviews, and architecture decisions. Always provide code examples with proper syntax highlighting. Explain your reasoning. Follow best practices and mention potential pitfalls. When writing code, include comments for complex logic.
63
+```
64
+Category: Coding
65
+
66
+### 3. Math Tutor 📐
67
+```
68
+You are a patient and thorough math tutor. Explain concepts step by step, starting from fundamentals. Use examples to illustrate abstract concepts. When solving problems, show your work clearly. Use LaTeX notation for mathematical expressions when appropriate. Encourage the student and offer practice problems.
69
+```
70
+Category: Education
71
+
72
+### 4. Story Writer ✍️
73
+```
74
+You are a creative fiction writer. Help craft engaging stories, develop characters, build worlds, and write dialogue. Match the tone and style the user requests. Offer constructive suggestions to improve narratives. Be creative and take risks with your writing while staying true to the user's vision.
75
+```
76
+Category: Creative
77
+
78
+### 5. General Tutor 📚
79
+```
80
+You are an educational tutor skilled in all subjects. Explain complex topics in simple terms. Use analogies, examples, and visual descriptions. Break down topics into digestible pieces. Check understanding by posing questions. Adapt your explanation style to the student's level.
81
+```
82
+Category: Education
83
+
84
+### 6. Writing Editor ✏️
85
+```
86
+You are a professional writing editor. Help improve text clarity, grammar, style, and structure. Provide specific suggestions with explanations. Maintain the author's voice while improving readability. Offer alternatives rather than dictating changes. Format feedback clearly with before/after examples.
87
+```
88
+Category: Creative
89
+
90
+### 7. Summarizer 📋
91
+```
92
+You are a concise summarizer. When given text, provide clear, accurate summaries that capture the key points. Use bullet points for multiple items. Maintain the original meaning without adding interpretation. Adjust summary length based on the input — shorter inputs get shorter summaries.
93
+```
94
+Category: General
95
+
96
+---
97
+
98
+## Providers
99
+
100
+Location: `features/persona/providers/persona_providers.dart`
101
+
102
+### Provider Organization
103
+
104
+```dart
105
+// 1. All personas (built-in + user-created)
106
+final personasProvider = StateNotifierProvider<PersonasNotifier, List<Persona>>((ref) {
107
+ final box = ref.watch(hiveBoxesProvider).personas;
108
+ return PersonasNotifier(box);
109
+});
110
+
111
+// 2. Currently selected persona for new chat
112
+final selectedPersonaProvider = StateProvider<Persona?>((ref) => null);
113
+
114
+// 3. Category filter
115
+final personaCategoryFilterProvider = StateProvider<String?>((ref) => null);
116
+
117
+// 4. Filtered personas based on category
118
+final filteredPersonasProvider = Provider<List<Persona>>((ref) {
119
+ final personas = ref.watch(personasProvider);
120
+ final category = ref.watch(personaCategoryFilterProvider);
121
+ if (category == null) return personas;
122
+ return personas.where((p) => p.category == category).toList();
123
+});
124
+```
125
+
126
+### `PersonasNotifier`
127
+
128
+```dart
129
+class PersonasNotifier extends StateNotifier<List<Persona>> {
130
+ final Box<Persona> _box;
131
+
132
+ PersonasNotifier(this._box) : super([]) {
133
+ loadPersonas();
134
+ }
135
+
136
+ Future<void> loadPersonas();
137
+ Future<void> seedBuiltInPersonas();
138
+ Future<void> createPersona(Persona persona);
139
+ Future<void> updatePersona(Persona persona);
140
+ Future<void> deletePersona(String id);
141
+ Future<Persona> clonePersona(String id);
142
+}
143
+```
144
+
145
+---
146
+
147
+## Views
148
+
149
+### `persona_list_page.dart`
150
+
151
+**Layout:**
152
+
153
+```
154
+┌──────────────────────────────────────┐
155
+│ Personas [+ ] │
156
+│ │
157
+│ [All] [Coding] [Education] [Creat.] │
158
+│ │
159
+│ ── BUILT-IN ── │
160
+│ ┌──────────────────────────────┐ │
161
+│ │ 🤖 General Assistant │ │
162
+│ │ Helpful, knowledgeable... │ │
163
+│ └──────────────────────────────┘ │
164
+│ ┌──────────────────────────────┐ │
165
+│ │ 🧑💻 Code Assistant │ │
166
+│ │ Expert software engineer... │ │
167
+│ └──────────────────────────────┘ │
168
+│ ... │
169
+│ │
170
+│ ── MY PERSONAS ── │
171
+│ ┌──────────────────────────────┐ │
172
+│ │ 🎯 Interview Prep │ │
173
+│ │ Practice tech interviews... │ │
174
+│ └──────────────────────────────┘ │
175
+│ │
176
+│ [Create Custom Persona] │
177
+└──────────────────────────────────────┘
178
+```
179
+
180
+**Features:**
181
+- Category filter chips (All, Coding, Education, Creative, General)
182
+- Sections: "Built-in" and "My Personas"
183
+- Tap card → apply to current/new conversation
184
+- Long-press built-in → "Clone & Edit"
185
+- Long-press custom → Edit, Delete, Clone
186
+
187
+---
188
+
189
+### `create_persona_page.dart`
190
+
191
+**Form fields:**
192
+
193
+| Field | Type | Validation |
194
+| -------------- | --------- | ----------------------------------- |
195
+| Name | Text | Required, 1–50 chars |
196
+| Emoji | Picker | Required, single emoji |
197
+| Category | Dropdown | Optional (Coding, Education, etc.) |
198
+| Description | Text | Optional, 1–200 chars |
199
+| System Prompt | Multiline | Required, 1–4000 chars |
200
+
201
+**System prompt field:**
202
+- Large multiline text area (min 6 lines)
203
+- Character count indicator
204
+- Preview mode toggle
205
+
206
+**Emoji picker:**
207
+- Grid of common emojis
208
+- Categories: People, Animals, Objects, Symbols
209
+
210
+**Optional parameter overrides:**
211
+- Expandable "Advanced Settings"
212
+- Temperature slider (0.0 – 2.0)
213
+- Top P slider (0.0 – 1.0)
214
+
215
+---
216
+
217
+## Components
218
+
219
+### `persona_card.dart`
220
+
221
+- Left: Large emoji avatar (40x40)
222
+- Title: Persona name
223
+- Subtitle: Description or first line of system prompt
224
+- Right: "Built-in" badge
225
+- Bottom: Category chip
226
+- Tap → apply persona
227
+
228
+### `persona_category_chips.dart`
229
+
230
+Horizontal scroll:
231
+- "All" (default)
232
+- "Coding" 🧑💻
233
+- "Education" 📚
234
+- "Creative" ✍️
235
+- "General" 🤖
236
+
237
+---
238
+
239
+## Applying a Persona
240
+
241
+### To a New Conversation
242
+
243
+1. Select persona from list
244
+2. Create conversation with `personaId` set
245
+3. Persona `systemPrompt` prepended as first system message
246
+4. If persona has `preferredParams`, override global chat params
247
+5. Chat app bar shows persona emoji
248
+
249
+### To an Existing Conversation
250
+
251
+1. Chat overflow menu → "Change Persona"
252
+2. Opens persona picker
253
+3. Selecting persona adds divider and uses new system prompt
254
+4. "Remove Persona" option
255
+
256
+---
257
+
258
+## First Launch Seeding
259
+
260
+On first launch (empty personas box):
261
+1. Seed all 7 built-in personas into Hive
262
+2. Mark with `isBuiltIn: true`
263
+3. Set "General Assistant" as default
264
+
265
+---
266
+
267
+## Acceptance Criteria
268
+
269
+- [ ] 7 built-in personas available on first launch.
270
+- [ ] User can browse personas with category filtering.
271
+- [ ] User can create custom persona.
272
+- [ ] User can edit and delete custom personas.
273
+- [ ] User can clone built-in persona for customization.
274
+- [ ] Applying persona to new chat sets system prompt.
275
+- [ ] Persona can be changed mid-conversation.
276
+- [ ] Persona emoji appears in chat app bar when active.
277
+- [ ] Parameter overrides from personas are respected.
278
+- [ ] Built-in personas cannot be deleted.
279
+- [ ] Persona data persists across restarts.
plans/06_settings.md
new
+280
@@ -0,0 +1,280 @@
1
+# Feature 06 — Settings
2
+
3
+> **Phase:** 3 — Features
4
+> **Status:** Pending
5
+> **Depends on:** `00_project_setup`, `07_core_architecture`
6
+
7
+---
8
+
9
+## Goal
10
+
11
+Provide a comprehensive settings screen for configuring chat parameters, UI preferences, default server, and reviewing app/privacy information.
12
+
13
+---
14
+
15
+## Feature Folder Structure
16
+
17
+```
18
+features/settings/
19
+├── data/
20
+│ └── models/ # (Uses core/models/app_settings.dart)
21
+│
22
+├── providers/
23
+│ └── settings_providers.dart
24
+│
25
+└── views/
26
+ ├── settings_page.dart # Main settings screen
27
+ └── components/
28
+ ├── settings_section.dart
29
+ ├── settings_slider.dart
30
+ ├── settings_toggle.dart
31
+ ├── settings_dropdown.dart
32
+ ├── theme_toggle.dart
33
+ └── font_size_preview.dart
34
+```
35
+
36
+---
37
+
38
+## Dependencies Used
39
+
40
+| Package | Purpose |
41
+| ------------------- | --------------------------- |
42
+| `hive_ce` | Persist settings locally |
43
+| `flutter_riverpod` | Settings state management |
44
+
45
+---
46
+
47
+## Data Model
48
+
49
+`AppSettings` is defined in `core/models/app_settings.dart`.
50
+
51
+---
52
+
53
+## Providers
54
+
55
+Location: `features/settings/providers/settings_providers.dart`
56
+
57
+### Provider Organization
58
+
59
+```dart
60
+// 1. Settings state
61
+final settingsProvider = StateNotifierProvider<SettingsNotifier, AppSettings>((ref) {
62
+ return SettingsNotifier(ref.read(hiveBoxesProvider).settings);
63
+});
64
+
65
+// 2. Convenience providers
66
+final themeModeProvider = Provider<ThemeMode>((ref) {
67
+ return ref.watch(settingsProvider).themeMode;
68
+});
69
+
70
+final fontSizeProvider = Provider<double>((ref) {
71
+ return ref.watch(settingsProvider).fontSize;
72
+});
73
+
74
+final chatParamsProvider = Provider<ChatParameters>((ref) {
75
+ final settings = ref.watch(settingsProvider);
76
+ return ChatParameters(
77
+ temperature: settings.temperature,
78
+ topP: settings.topP,
79
+ maxTokens: settings.maxTokens,
80
+ contextLength: settings.contextLength,
81
+ );
82
+});
83
+```
84
+
85
+### `SettingsNotifier`
86
+
87
+```dart
88
+class SettingsNotifier extends StateNotifier<AppSettings> {
89
+ final Box<AppSettings> _box;
90
+
91
+ SettingsNotifier(this._box) : super(AppSettings()) {
92
+ loadSettings();
93
+ }
94
+
95
+ Future<void> loadSettings();
96
+ void setTemperature(double value);
97
+ void setTopP(double value);
98
+ void setMaxTokens(int value);
99
+ void setContextLength(int value);
100
+ void setThemeMode(ThemeMode mode);
101
+ void setFontSize(double size);
102
+ void setHapticFeedback(bool enabled);
103
+ void setSendOnEnter(bool enabled);
104
+ void setShowSystemMessages(bool show);
105
+ void setDefaultServer(String? serverId);
106
+ void setAutoGenerateTitle(bool enabled);
107
+ void setStreamingEnabled(bool enabled);
108
+ void setDefaultPersona(String? personaId);
109
+ void setShowDataIndicator(bool show);
110
+ Future<void> resetToDefaults();
111
+ Future<void> _persist();
112
+}
113
+```
114
+
115
+---
116
+
117
+## Views
118
+
119
+### `settings_page.dart`
120
+
121
+**Layout:**
122
+
123
+```
124
+┌──────────────────────────────────────┐
125
+│ ← Settings │
126
+│ │
127
+│ ── CHAT PARAMETERS ── │
128
+│ │
129
+│ Temperature 0.7 │
130
+│ ├──────────●──────────────────┤ │
131
+│ Controls randomness. Lower = │
132
+│ focused, Higher = creative. │
133
+│ │
134
+│ Top P 0.9 │
135
+│ ├──────────────────●──────────┤ │
136
+│ Nucleus sampling threshold. │
137
+│ │
138
+│ Max Tokens 2048 │
139
+│ [ 2048 ] │
140
+│ Maximum response length. │
141
+│ │
142
+│ Context Length 4096 │
143
+│ [ 4096 ] │
144
+│ Conversation history window. │
145
+│ │
146
+│ ── APPEARANCE ── │
147
+│ │
148
+│ Theme │
149
+│ [System] [Light] [Dark ✓] │
150
+│ │
151
+│ Font Size 16 │
152
+│ A ├────────●──────────────┤ A │
153
+│ Preview: The quick brown fox... │
154
+│ │
155
+│ ── BEHAVIOR ── │
156
+│ │
157
+│ Streaming Responses [✓] │
158
+│ Auto-generate Titles [✓] │
159
+│ Send on Enter [ ] │
160
+│ Show System Messages [ ] │
161
+│ Haptic Feedback [✓] │
162
+│ │
163
+│ ── DEFAULT SERVER ── │
164
+│ │
165
+│ [My Desktop LM Studio ▼] │
166
+│ │
167
+│ ── DEFAULT PERSONA ── │
168
+│ │
169
+│ [🤖 General Assistant ▼] │
170
+│ │
171
+│ ── PRIVACY ── │
172
+│ │
173
+│ Show Data Indicator [✓] │
174
+│ "LocalMind never sees your data" │
175
+│ │
176
+│ ── DATA MANAGEMENT ── │
177
+│ │
178
+│ [Export All Conversations] │
179
+│ [Delete All Conversations] ⚠️ │
180
+│ [Reset Settings to Defaults] │
181
+│ │
182
+│ ── ABOUT ── │
183
+│ │
184
+│ LocalMind v1.0.0 │
185
+│ Your AI. Your Device. Your Rules. │
186
+│ │
187
+│ [Privacy Policy] │
188
+│ [Open Source Licenses] │
189
+│ │
190
+└──────────────────────────────────────┘
191
+```
192
+
193
+---
194
+
195
+## Components
196
+
197
+### `settings_section.dart`
198
+
199
+Groups related settings with uppercase section header.
200
+
201
+### `settings_slider.dart`
202
+
203
+- Label with current value display
204
+- Slider control
205
+- Description text below
206
+- Used for: Temperature, Top P, Font Size
207
+
208
+### `settings_toggle.dart`
209
+
210
+- Label on left
211
+- Switch on right
212
+- Used for: Streaming, Auto-title, Send on Enter, etc.
213
+
214
+### `settings_dropdown.dart`
215
+
216
+- Label on left
217
+- Dropdown selector on right
218
+- Used for: Default Server, Default Persona
219
+
220
+### `theme_toggle.dart`
221
+
222
+Visual toggle between System, Light, Dark with icons (🌐 ☀️ 🌙).
223
+
224
+### `font_size_preview.dart`
225
+
226
+Live preview text that updates as font size slider moves.
227
+
228
+---
229
+
230
+## Chat Parameter Explanations
231
+
232
+| Parameter | Description |
233
+| --------------- | ------------------------------------------------------------ |
234
+| Temperature | Controls randomness. 0 = deterministic, 2 = very random. |
235
+| Top P | Nucleus sampling. 0.1 = conservative, 1.0 = all options. |
236
+| Max Tokens | Maximum number of tokens in the response. |
237
+| Context Length | How many tokens of conversation history to send. |
238
+
239
+---
240
+
241
+## Dangerous Actions
242
+
243
+### Delete All Conversations
244
+
245
+1. Tap "Delete All Conversations"
246
+2. Show confirmation with red accent
247
+3. On confirm: delete all, clear providers, navigate to empty chat
248
+
249
+### Reset Settings
250
+
251
+1. Tap "Reset Settings to Defaults"
252
+2. Show confirmation dialog
253
+3. On confirm: reset to factory defaults
254
+
255
+---
256
+
257
+## Settings Persistence
258
+
259
+- All settings saved immediately (no "Save" button)
260
+- `_persist()` called after every state update
261
+- On launch, `loadSettings()` reads from Hive or creates defaults
262
+
263
+---
264
+
265
+## Acceptance Criteria
266
+
267
+- [ ] Temperature slider works and updates chat behavior.
268
+- [ ] Top P slider works and updates chat behavior.
269
+- [ ] Max Tokens input validates and restricts to bounds.
270
+- [ ] Context Length input validates and restricts to bounds.
271
+- [ ] Theme toggle switches System/Light/Dark and persists.
272
+- [ ] Font size slider adjusts chat text size with live preview.
273
+- [ ] All toggle switches work and persist.
274
+- [ ] Default server dropdown shows all saved servers.
275
+- [ ] Default persona dropdown shows all personas.
276
+- [ ] "Delete All Conversations" works with confirmation.
277
+- [ ] "Reset Settings" restores all defaults.
278
+- [ ] All settings persist across restarts.
279
+- [ ] Settings screen scrolls smoothly.
280
+- [ ] Each parameter has helpful description.
plans/07_core_architecture.md
new
+578
@@ -0,0 +1,578 @@
1
+# Feature 07 — Core Architecture (Models, Services, Storage, Providers)
2
+
3
+> **Phase:** 0–1
4
+> **Status:** Pending
5
+> **Depends on:** None (foundational)
6
+> **Replaces:** Original `core/` structure
7
+
8
+---
9
+
10
+## Goal
11
+
12
+Define the core data models, service interfaces, storage layer, and global providers that all features depend on. This is the application's backbone.
13
+
14
+---
15
+
16
+## Overview
17
+
18
+This document details the `core/` layer — code that is shared across multiple features. Each feature's internal structure is defined in its respective plan file.
19
+
20
+### Core Folder Structure
21
+
22
+```
23
+core/
24
+├── constants/
25
+│ └── app_constants.dart # App-wide constants
26
+├── models/
27
+│ ├── server.dart # Server model + enums
28
+│ ├── message.dart # Message model + enums
29
+│ ├── conversation.dart # Conversation model
30
+│ ├── persona.dart # Persona model
31
+│ ├── model_info.dart # LLM model metadata
32
+│ ├── chat_parameters.dart # Chat parameter config
33
+│ └── app_settings.dart # Settings model
34
+├── providers/
35
+│ └── app_providers.dart # Global Riverpod providers
36
+├── repository/
37
+│ ├── storage/
38
+│ │ ├── hive_initializer.dart
39
+│ │ ├── hive_keys.dart
40
+│ │ └── hive_boxes.dart
41
+│ ├── server/
42
+│ │ └── server_repository.dart
43
+│ └── chat/
44
+│ ├── chat_service.dart
45
+│ ├── lm_studio_chat_service.dart
46
+│ ├── ollama_chat_service.dart
47
+│ └── openrouter_chat_service.dart
48
+├── routes/
49
+│ └── app_router.dart
50
+├── theme/
51
+│ ├── app_theme.dart
52
+│ ├── colors.dart
53
+│ └── typography.dart
54
+├── components/
55
+│ ├── app_button.dart
56
+│ ├── app_card.dart
57
+│ └── app_text_field.dart
58
+└── utils/
59
+ ├── uuid.dart
60
+ └── date_utils.dart
61
+```
62
+
63
+---
64
+
65
+## Hive Type IDs
66
+
67
+| Type ID | Model |
68
+| ------- | --------------- |
69
+| 0 | Server |
70
+| 1 | Message |
71
+| 2 | Conversation |
72
+| 3 | Persona |
73
+| 4 | AppSettings |
74
+| 10 | ServerType |
75
+| 11 | ConnectionStatus|
76
+| 12 | MessageRole |
77
+| 13 | MessageStatus |
78
+
79
+---
80
+
81
+## Models — Full Specifications
82
+
83
+### `core/models/server.dart`
84
+
85
+```dart
86
+@HiveType(typeId: 10)
87
+enum ServerType { lmStudio, ollama, openRouter }
88
+
89
+@HiveType(typeId: 11)
90
+enum ConnectionStatus { connected, disconnected, checking, error }
91
+
92
+@HiveType(typeId: 0)
93
+class Server extends HiveObject {
94
+ @HiveField(0)
95
+ String id; // UUID
96
+
97
+ @HiveField(1)
98
+ String name; // User-defined label
99
+
100
+ @HiveField(2)
101
+ ServerType type;
102
+
103
+ @HiveField(3)
104
+ String host; // IP address or hostname
105
+
106
+ @HiveField(4)
107
+ int port; // Port number
108
+
109
+ @HiveField(5)
110
+ String? apiKey; // Required for OpenRouter
111
+
112
+ @HiveField(6)
113
+ bool isDefault;
114
+
115
+ @HiveField(7)
116
+ DateTime createdAt;
117
+
118
+ @HiveField(8)
119
+ DateTime lastConnectedAt;
120
+
121
+ @HiveField(9)
122
+ ConnectionStatus status;
123
+
124
+ String get baseUrl {
125
+ if (type == ServerType.openRouter) return 'https://openrouter.ai/api/v1';
126
+ return 'http://$host:$port';
127
+ }
128
+
129
+ String get chatEndpoint {
130
+ switch (type) {
131
+ case ServerType.lmStudio: return '$baseUrl/v1/chat/completions';
132
+ case ServerType.ollama: return '$baseUrl/api/chat';
133
+ case ServerType.openRouter: return '$baseUrl/chat/completions';
134
+ }
135
+ }
136
+
137
+ String get modelsEndpoint {
138
+ switch (type) {
139
+ case ServerType.lmStudio: return '$baseUrl/v1/models';
140
+ case ServerType.ollama: return '$baseUrl/api/tags';
141
+ case ServerType.openRouter: return '$baseUrl/models';
142
+ }
143
+ }
144
+
145
+ // ... fromMap(), toMap(), copyWith()
146
+}
147
+```
148
+
149
+### `core/models/message.dart`
150
+
151
+```dart
152
+@HiveType(typeId: 12)
153
+enum MessageRole { user, assistant, system }
154
+
155
+@HiveType(typeId: 13)
156
+enum MessageStatus { sending, streaming, complete, error }
157
+
158
+@HiveType(typeId: 1)
159
+class Message extends HiveObject {
160
+ @HiveField(0)
161
+ String id; // UUID
162
+
163
+ @HiveField(1)
164
+ String conversationId; // FK to Conversation
165
+
166
+ @HiveField(2)
167
+ MessageRole role;
168
+
169
+ @HiveField(3)
170
+ String content; // Message text (markdown)
171
+
172
+ @HiveField(4)
173
+ DateTime createdAt;
174
+
175
+ @HiveField(5)
176
+ MessageStatus status;
177
+
178
+ @HiveField(6)
179
+ String? modelId; // Which model generated this
180
+
181
+ @HiveField(7)
182
+ int? tokenCount;
183
+
184
+ @HiveField(8)
185
+ String? errorMessage;
186
+
187
+ @HiveField(9)
188
+ List<String>? attachmentPaths;
189
+
190
+ @HiveField(10)
191
+ int? generationTimeMs;
192
+
193
+ // ... fromMap(), toMap(), copyWith()
194
+}
195
+```
196
+
197
+### `core/models/conversation.dart`
198
+
199
+```dart
200
+@HiveType(typeId: 2)
201
+class Conversation extends HiveObject {
202
+ @HiveField(0)
203
+ String id;
204
+
205
+ @HiveField(1)
206
+ String title;
207
+
208
+ @HiveField(2)
209
+ DateTime createdAt;
210
+
211
+ @HiveField(3)
212
+ DateTime updatedAt;
213
+
214
+ @HiveField(4)
215
+ bool isPinned;
216
+
217
+ @HiveField(5)
218
+ String? personaId;
219
+
220
+ @HiveField(6)
221
+ String? serverId;
222
+
223
+ @HiveField(7)
224
+ String? modelId;
225
+
226
+ @HiveField(8)
227
+ int messageCount;
228
+
229
+ @HiveField(9)
230
+ String? lastMessagePreview;
231
+
232
+ @HiveField(10)
233
+ String? systemPrompt;
234
+
235
+ // ... fromMap(), toMap(), copyWith()
236
+}
237
+```
238
+
239
+### `core/models/persona.dart`
240
+
241
+```dart
242
+@HiveType(typeId: 3)
243
+class Persona extends HiveObject {
244
+ @HiveField(0)
245
+ String id;
246
+
247
+ @HiveField(1)
248
+ String name;
249
+
250
+ @HiveField(2)
251
+ String emoji;
252
+
253
+ @HiveField(3)
254
+ String systemPrompt;
255
+
256
+ @HiveField(4)
257
+ String? description;
258
+
259
+ @HiveField(5)
260
+ bool isBuiltIn;
261
+
262
+ @HiveField(6)
263
+ DateTime createdAt;
264
+
265
+ @HiveField(7)
266
+ DateTime updatedAt;
267
+
268
+ @HiveField(8)
269
+ String? category;
270
+
271
+ @HiveField(9)
272
+ Map<String, dynamic>? preferredParams;
273
+
274
+ // ... fromMap(), toMap(), copyWith()
275
+}
276
+```
277
+
278
+### `core/models/app_settings.dart`
279
+
280
+```dart
281
+@HiveType(typeId: 4)
282
+class AppSettings extends HiveObject {
283
+ @HiveField(0)
284
+ double temperature;
285
+
286
+ @HiveField(1)
287
+ double topP;
288
+
289
+ @HiveField(2)
290
+ int maxTokens;
291
+
292
+ @HiveField(3)
293
+ int contextLength;
294
+
295
+ @HiveField(4)
296
+ ThemeMode themeMode;
297
+
298
+ @HiveField(5)
299
+ double fontSize;
300
+
301
+ @HiveField(6)
302
+ bool showSystemMessages;
303
+
304
+ @HiveField(7)
305
+ bool hapticFeedbackEnabled;
306
+
307
+ @HiveField(8)
308
+ bool sendOnEnter;
309
+
310
+ @HiveField(9)
311
+ String? defaultServerId;
312
+
313
+ @HiveField(10)
314
+ bool showDataIndicator;
315
+
316
+ @HiveField(11)
317
+ bool autoGenerateTitle;
318
+
319
+ @HiveField(12)
320
+ bool streamingEnabled;
321
+
322
+ @HiveField(13)
323
+ String? defaultPersonaId;
324
+
325
+ // ... fromMap(), toMap(), copyWith()
326
+}
327
+```
328
+
329
+### `core/models/model_info.dart` (Non-Hive)
330
+
331
+```dart
332
+class ModelInfo {
333
+ final String id;
334
+ final String name;
335
+ final String? description;
336
+ final int? parameterCount;
337
+ final int? contextLength;
338
+ final int? fileSize;
339
+ final String? quantization;
340
+ final String? architecture;
341
+ final ServerType serverType;
342
+ final String serverId;
343
+ final DateTime? modifiedAt;
344
+
345
+ // ... fromMap(), toMap(), copyWith()
346
+}
347
+```
348
+
349
+### `core/models/chat_parameters.dart` (Non-Hive)
350
+
351
+```dart
352
+class ChatParameters {
353
+ final double temperature;
354
+ final double topP;
355
+ final int maxTokens;
356
+ final int contextLength;
357
+
358
+ factory ChatParameters.defaults() => ChatParameters(
359
+ temperature: 0.7,
360
+ topP: 0.9,
361
+ maxTokens: 2048,
362
+ contextLength: 4096,
363
+ );
364
+
365
+ // ... copyWith()
366
+}
367
+```
368
+
369
+---
370
+
371
+## Repository Layer
372
+
373
+### `core/repository/storage/hive_keys.dart`
374
+
375
+```dart
376
+class HiveKeys {
377
+ static const String servers = 'servers';
378
+ static const String messages = 'messages';
379
+ static const String conversations = 'conversations';
380
+ static const String personas = 'personas';
381
+ static const String settings = 'settings';
382
+}
383
+```
384
+
385
+### `core/repository/storage/hive_boxes.dart`
386
+
387
+```dart
388
+class HiveBoxes {
389
+ final Box<Server> servers;
390
+ final Box<Message> messages;
391
+ final Box<Conversation> conversations;
392
+ final Box<Persona> personas;
393
+ final Box<AppSettings> settings;
394
+
395
+ const HiveBoxes({
396
+ required this.servers,
397
+ required this.messages,
398
+ required this.conversations,
399
+ required this.personas,
400
+ required this.settings,
401
+ });
402
+}
403
+```
404
+
405
+### `core/repository/storage/hive_initializer.dart`
406
+
407
+```dart
408
+class HiveInitializer {
409
+ static Future<HiveBoxes> initialize() async {
410
+ final appDir = await getApplicationDocumentsDirectory();
411
+ await Hive.initFlutter(appDir.path);
412
+
413
+ // Register ALL adapters
414
+ Hive.registerAdapter(ServerAdapter());
415
+ Hive.registerAdapter(ServerTypeAdapter());
416
+ Hive.registerAdapter(ConnectionStatusAdapter());
417
+ Hive.registerAdapter(MessageAdapter());
418
+ Hive.registerAdapter(MessageRoleAdapter());
419
+ Hive.registerAdapter(MessageStatusAdapter());
420
+ Hive.registerAdapter(ConversationAdapter());
421
+ Hive.registerAdapter(PersonaAdapter());
422
+ Hive.registerAdapter(AppSettingsAdapter());
423
+
424
+ final servers = await Hive.openBox<Server>(HiveKeys.servers);
425
+ final messages = await Hive.openBox<Message>(HiveKeys.messages);
426
+ final conversations = await Hive.openBox<Conversation>(HiveKeys.conversations);
427
+ final personas = await Hive.openBox<Persona>(HiveKeys.personas);
428
+ final settings = await Hive.openBox<AppSettings>(HiveKeys.settings);
429
+
430
+ return HiveBoxes(
431
+ servers: servers,
432
+ messages: messages,
433
+ conversations: conversations,
434
+ personas: personas,
435
+ settings: settings,
436
+ );
437
+ }
438
+}
439
+```
440
+
441
+---
442
+
443
+## Services
444
+
445
+### `core/repository/chat/chat_service.dart`
446
+
447
+```dart
448
+abstract class ChatService {
449
+ Stream<String> sendMessage({
450
+ required Server server,
451
+ required String modelId,
452
+ required List<Message> messages,
453
+ required ChatParameters params,
454
+ });
455
+
456
+ void cancelStream();
457
+
458
+ static ChatService forServer(ServerType type, Dio dio) {
459
+ switch (type) {
460
+ case ServerType.lmStudio: return LMStudioChatService(dio);
461
+ case ServerType.ollama: return OllamaChatService(dio);
462
+ case ServerType.openRouter: return OpenRouterChatService(dio);
463
+ }
464
+ }
465
+}
466
+```
467
+
468
+**Implementations:**
469
+- `lm_studio_chat_service.dart` — OpenAI-compatible SSE streaming
470
+- `ollama_chat_service.dart` — NDJSON streaming
471
+- `openrouter_chat_service.dart` — OpenAI-compatible SSE with API key auth
472
+
473
+### `core/repository/server/server_repository.dart`
474
+
475
+```dart
476
+class ServerRepository {
477
+ final Dio _dio;
478
+
479
+ Future<bool> testConnection(Server server);
480
+ Future<List<ModelInfo>> fetchModels(Server server);
481
+ Future<int?> pingServer(Server server);
482
+}
483
+```
484
+
485
+---
486
+
487
+## Providers
488
+
489
+### `core/providers/app_providers.dart`
490
+
491
+```dart
492
+// Hive boxes (initialized in main.dart)
493
+final hiveBoxesProvider = Provider<HiveBoxes>((ref) {
494
+ throw UnimplementedError('Must be overridden with actual HiveBoxes');
495
+});
496
+
497
+// Dio HTTP client
498
+final dioProvider = Provider<Dio>((ref) {
499
+ return Dio(BaseOptions(
500
+ connectTimeout: Duration(milliseconds: AppConstants.connectionTimeoutMs),
501
+ receiveTimeout: Duration(milliseconds: AppConstants.receiveTimeoutMs),
502
+ ));
503
+});
504
+
505
+// Theme
506
+final themeModeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeMode>((ref) {
507
+ return ThemeModeNotifier();
508
+});
509
+
510
+// Active Server
511
+final activeServerProvider = StateNotifierProvider<ActiveServerNotifier, Server?>((ref) {
512
+ return ActiveServerNotifier();
513
+});
514
+
515
+// Active Conversation
516
+final activeConversationProvider = StateNotifierProvider<ActiveConversationNotifier, Conversation?>((ref) {
517
+ return ActiveConversationNotifier();
518
+});
519
+```
520
+
521
+---
522
+
523
+## Utilities
524
+
525
+### `core/utils/uuid.dart`
526
+
527
+```dart
528
+String generateUuid() {
529
+ final random = Random.secure();
530
+ final bytes = List<int>.generate(16, (_) => random.nextInt(256));
531
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
532
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
533
+ return [
534
+ bytes.sublist(0, 4), bytes.sublist(4, 6),
535
+ bytes.sublist(6, 8), bytes.sublist(8, 10),
536
+ bytes.sublist(10, 16),
537
+ ].map((b) => b.map((e) => e.toRadixString(16).padLeft(2, '0')).join())
538
+ .join('-');
539
+}
540
+```
541
+
542
+---
543
+
544
+## Error Handling Strategy
545
+
546
+```dart
547
+class AppException implements Exception {
548
+ final String message;
549
+ final String? technicalDetail;
550
+ final AppErrorType type;
551
+}
552
+
553
+enum AppErrorType {
554
+ connectionRefused,
555
+ timeout,
556
+ unauthorized,
557
+ serverError,
558
+ modelNotFound,
559
+ contextOverflow,
560
+ streamInterrupted,
561
+ rateLimited,
562
+ unknown,
563
+}
564
+```
565
+
566
+Each `AppErrorType` maps to a user-friendly message (see `02_chat_interface.md` error table).
567
+
568
+---
569
+
570
+## Acceptance Criteria
571
+
572
+- [ ] All Hive models compile and their adapters are generated.
573
+- [ ] `HiveInitializer` successfully opens all boxes on app launch.
574
+- [ ] `ChatService` correctly routes to the right implementation per server type.
575
+- [ ] `ServerRepository` can test connections and fetch models.
576
+- [ ] All providers are defined and accessible.
577
+- [ ] `AppException` provides user-friendly error messages.
578
+- [ ] UUID generation produces valid unique IDs.
plans/08_design_system.md
new
+340
@@ -0,0 +1,340 @@
1
+# Feature 08 — Design System
2
+
3
+> **Phase:** 0–1
4
+> **Status:** Pending
5
+> **Depends on:** `00_project_setup`
6
+
7
+---
8
+
9
+## Goal
10
+
11
+Define the visual language and reusable components for LocalMind using ShadCN-inspired principles. Ensure a premium, dark-first, and highly polished user experience.
12
+
13
+---
14
+
15
+## Location
16
+
17
+Design system components live in the `core/` layer since they are shared across all features:
18
+
19
+```
20
+core/
21
+├── theme/
22
+│ ├── app_theme.dart
23
+│ ├── colors.dart
24
+│ └── typography.dart
25
+└── components/
26
+ ├── app_button.dart
27
+ ├── app_card.dart
28
+ ├── app_text_field.dart
29
+ ├── app_sheet.dart
30
+ ├── app_dialog.dart
31
+ └── app_loading_indicator.dart
32
+```
33
+
34
+---
35
+
36
+## Core Principles
37
+
38
+1. **High Contrast & Legibility**: Text must be readable against backgrounds. Use neutral grays, not pure black.
39
+2. **Density**: Compact enough for information density, spacious enough for touch targets.
40
+3. **Feedback**: Every interaction should have visual (hover, press) and tactile (haptic) feedback.
41
+4. **Consistency**: Use the same spacing, border radius, and typography scales everywhere.
42
+5. **ShadCN Aesthetic**: Clean borders, subtle surfaces, neutral colors with a single vibrant accent.
43
+
44
+---
45
+
46
+## Color Tokens
47
+
48
+Location: `core/theme/colors.dart`
49
+
50
+```dart
51
+class AppColors {
52
+ // --- Dark Theme (Default) ---
53
+ static const Color darkBackground = Color(0xFF0A0A0A);
54
+ static const Color darkSurface = Color(0xFF171717);
55
+ static const Color darkBorder = Color(0xFF262626);
56
+ static const Color darkPrimaryText = Color(0xFFFAFAFA);
57
+ static const Color darkMutedText = Color(0xFFA1A1AA);
58
+ static const Color darkAccent = Color(0xFF3B82F6);
59
+ static const Color darkError = Color(0xFFEF4444);
60
+ static const Color darkSuccess = Color(0xFF22C55E);
61
+
62
+ // --- Light Theme ---
63
+ static const Color lightBackground = Color(0xFFFAFAFA);
64
+ static const Color lightSurface = Color(0xFFFFFFFF);
65
+ static const Color lightBorder = Color(0xFFE5E5E5);
66
+ static const Color lightPrimaryText = Color(0xFF0A0A0A);
67
+ static const Color lightMutedText = Color(0xFF71717A);
68
+ static const Color lightAccent = Color(0xFF2563EB);
69
+ static const Color lightError = Color(0xFFDC2626);
70
+ static const Color lightSuccess = Color(0xFF16A34A);
71
+}
72
+```
73
+
74
+---
75
+
76
+## Typography
77
+
78
+Location: `core/theme/typography.dart`
79
+
80
+Font Family: `Inter`
81
+Code Font: `Fira Code` or `JetBrains Mono`
82
+
83
+```dart
84
+class AppTypography {
85
+ static const String fontFamily = 'Inter';
86
+ static const String codeFontFamily = 'FiraCode';
87
+
88
+ // Text styles using Flutter's TextTheme
89
+ static TextTheme get textTheme => TextTheme(
90
+ displayLarge: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
91
+ displayMedium: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
92
+ displaySmall: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
93
+ headlineMedium: TextStyle(fontSize: 20, fontWeight: FontWeight.w600),
94
+ titleLarge: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
95
+ titleMedium: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
96
+ bodyLarge: TextStyle(fontSize: 16, fontWeight: FontWeight.normal),
97
+ bodyMedium: TextStyle(fontSize: 14, fontWeight: FontWeight.normal),
98
+ bodySmall: TextStyle(fontSize: 12, fontWeight: FontWeight.normal),
99
+ labelLarge: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
100
+ labelSmall: TextStyle(fontSize: 11, fontWeight: FontWeight.w500),
101
+ );
102
+}
103
+```
104
+
105
+---
106
+
107
+## Theme Data
108
+
109
+Location: `core/theme/app_theme.dart`
110
+
111
+```dart
112
+class AppTheme {
113
+ static ThemeData get darkTheme => ThemeData(
114
+ useMaterial3: true,
115
+ brightness: Brightness.dark,
116
+ scaffoldBackgroundColor: AppColors.darkBackground,
117
+ colorScheme: ColorScheme.dark(
118
+ surface: AppColors.darkSurface,
119
+ primary: AppColors.darkAccent,
120
+ error: AppColors.darkError,
121
+ ),
122
+ textTheme: AppTypography.textTheme.apply(
123
+ bodyColor: AppColors.darkPrimaryText,
124
+ displayColor: AppColors.darkPrimaryText,
125
+ ),
126
+ cardTheme: CardTheme(
127
+ color: AppColors.darkSurface,
128
+ shape: RoundedRectangleBorder(
129
+ borderRadius: BorderRadius.circular(8),
130
+ side: BorderSide(color: AppColors.darkBorder),
131
+ ),
132
+ ),
133
+ inputDecorationTheme: InputDecorationTheme(
134
+ border: OutlineInputBorder(
135
+ borderRadius: BorderRadius.circular(8),
136
+ borderSide: BorderSide(color: AppColors.darkBorder),
137
+ ),
138
+ filled: true,
139
+ fillColor: AppColors.darkSurface,
140
+ ),
141
+ dividerTheme: DividerThemeData(
142
+ color: AppColors.darkBorder,
143
+ thickness: 1,
144
+ ),
145
+ );
146
+
147
+ static ThemeData get lightTheme => ThemeData(
148
+ useMaterial3: true,
149
+ brightness: Brightness.light,
150
+ scaffoldBackgroundColor: AppColors.lightBackground,
151
+ colorScheme: ColorScheme.light(
152
+ surface: AppColors.lightSurface,
153
+ primary: AppColors.lightAccent,
154
+ error: AppColors.lightError,
155
+ ),
156
+ textTheme: AppTypography.textTheme.apply(
157
+ bodyColor: AppColors.lightPrimaryText,
158
+ displayColor: AppColors.lightPrimaryText,
159
+ ),
160
+ cardTheme: CardTheme(
161
+ color: AppColors.lightSurface,
162
+ shape: RoundedRectangleBorder(
163
+ borderRadius: BorderRadius.circular(8),
164
+ side: BorderSide(color: AppColors.lightBorder),
165
+ ),
166
+ ),
167
+ inputDecorationTheme: InputDecorationTheme(
168
+ border: OutlineInputBorder(
169
+ borderRadius: BorderRadius.circular(8),
170
+ borderSide: BorderSide(color: AppColors.lightBorder),
171
+ ),
172
+ filled: true,
173
+ fillColor: AppColors.lightSurface,
174
+ ),
175
+ dividerTheme: DividerThemeData(
176
+ color: AppColors.lightBorder,
177
+ thickness: 1,
178
+ ),
179
+ );
180
+}
181
+```
182
+
183
+---
184
+
185
+## Components
186
+
187
+Location: `core/components/`
188
+
189
+### `app_button.dart`
190
+
191
+```dart
192
+enum AppButtonVariant { primary, outline, ghost, destructive }
193
+
194
+class AppButton extends StatelessWidget {
195
+ const AppButton({
196
+ super.key,
197
+ required this.onPressed,
198
+ required this.child,
199
+ this.variant = AppButtonVariant.primary,
200
+ this.isLoading = false,
201
+ this.icon,
202
+ });
203
+
204
+ final VoidCallback? onPressed;
205
+ final Widget child;
206
+ final AppButtonVariant variant;
207
+ final bool isLoading;
208
+ final Widget? icon;
209
+}
210
+```
211
+
212
+**Variants:**
213
+- **Primary**: Filled background (accent color), white text
214
+- **Outline**: Transparent background, border, text color
215
+- **Ghost**: Transparent, text only, hover state shows surface tone
216
+- **Destructive**: Red accent for dangerous actions
217
+
218
+### `app_card.dart`
219
+
220
+```dart
221
+class AppCard extends StatelessWidget {
222
+ const AppCard({
223
+ super.key,
224
+ required this.child,
225
+ this.padding,
226
+ this.onTap,
227
+ });
228
+
229
+ final Widget child;
230
+ final EdgeInsetsGeometry? padding;
231
+ final VoidCallback? onTap;
232
+}
233
+```
234
+
235
+- Background: `darkSurface`
236
+- Border: `darkBorder`
237
+- Border radius: `8px`
238
+- Optional tap handler with ripple
239
+
240
+### `app_text_field.dart`
241
+
242
+```dart
243
+class AppTextField extends StatelessWidget {
244
+ const AppTextField({
245
+ super.key,
246
+ this.controller,
247
+ this.hint,
248
+ this.label,
249
+ this.error,
250
+ this.prefixIcon,
251
+ this.suffixIcon,
252
+ this.onChanged,
253
+ this.onSubmitted,
254
+ this.obscureText = false,
255
+ this.maxLines = 1,
256
+ });
257
+
258
+ final TextEditingController? controller;
259
+ final String? hint;
260
+ final String? label;
261
+ final String? error;
262
+ final Widget? prefixIcon;
263
+ final Widget? suffixIcon;
264
+ final ValueChanged<String>? onChanged;
265
+ final ValueChanged<String>? onSubmitted;
266
+ final bool obscureText;
267
+ final int maxLines;
268
+}
269
+```
270
+
271
+- Border: `darkBorder`
272
+- Background: transparent or `darkSurface` (focused)
273
+- Error text below when provided
274
+
275
+### `app_sheet.dart`
276
+
277
+Bottom sheet wrapper with drag handle, padding, and standard close button.
278
+
279
+### `app_dialog.dart`
280
+
281
+Dialog wrapper with title, description, actions, and standard padding/radius.
282
+
283
+### `app_loading_indicator.dart`
284
+
285
+Minimalist circular progress indicator, accent color.
286
+
287
+---
288
+
289
+## Haptic Feedback
290
+
291
+**Triggers:**
292
+- **Light impact**: Toggling switches, expanding sections
293
+- **Medium impact**: Sending message, deleting, saving
294
+- **Success impact**: Successful server test, saving persona
295
+- **Error impact**: Connection failure, invalid input
296
+
297
+---
298
+
299
+## Animations
300
+
301
+Location: `core/utils/animations.dart` (or inline)
302
+
303
+1. **Page Transitions**: `CupertinoPageRoute` or `FadeUpwardsPageTransitionsBuilder`
304
+2. **Loading Spinners**: Minimalist circular, accent color
305
+3. **Typing Indicator**: 3 bouncing dots
306
+4. **Blinking Cursor**: For streaming text
307
+5. **Hover States**: Scale `1.02x` or opacity change
308
+
309
+---
310
+
311
+## Spacing & Radius Constants
312
+
313
+```dart
314
+class AppSizes {
315
+ static const double paddingXs = 4;
316
+ static const double paddingSm = 8;
317
+ static const double paddingMd = 16;
318
+ static const double paddingLg = 24;
319
+ static const double paddingXl = 32;
320
+
321
+ static const double radiusSm = 4;
322
+ static const double radiusMd = 8;
323
+ static const double radiusLg = 12;
324
+ static const double radiusXl = 16;
325
+
326
+ static const double iconSm = 16;
327
+ static const double iconMd = 24;
328
+ static const double iconLg = 32;
329
+}
330
+```
331
+
332
+---
333
+
334
+## Acceptance Criteria
335
+
336
+- [ ] `AppColors` and typography defined and integrated into `ThemeData`.
337
+- [ ] Core components (Buttons, Inputs, Cards) created and used consistently.
338
+- [ ] Haptic feedback map defined and applied to key interactions.
339
+- [ ] Transitions and loading states feel premium and responsive.
340
+- [ ] Dark and Light mode fully supported and verifiable via system toggle.
plans/09_extended_features.md
new
+187
@@ -0,0 +1,187 @@
1
+# Feature 09 — Extended Features
2
+
3
+> **Phase:** 4 — Extended
4
+> **Status:** Pending
5
+> **Depends on:** `02_chat_interface`
6
+
7
+---
8
+
9
+## Goal
10
+
11
+Add advanced capabilities that elevate LocalMind from a basic chat client to a premium AI assistant tool.
12
+
13
+---
14
+
15
+## Features
16
+
17
+### 1. AI Voice (Text-to-Speech)
18
+
19
+**Package:** `flutter_tts`
20
+
21
+**Implementation:**
22
+- Add "Read Aloud" action to assistant messages (in action bar)
23
+- Highlight text as it is being read (if feasible)
24
+- Global setting to auto-read assistant responses
25
+- TTS stops when navigating away or starting new generation
26
+- Voice selection in settings (OS-dependent)
27
+
28
+### 2. Context Smart Replies
29
+
30
+**Implementation:**
31
+- When assistant message completes, generate 2-3 suggested follow-up prompts
32
+- Show as selectable chips above input bar
33
+- Generation method: lightweight background prompt or rule-based suggestions
34
+- Rule-based fallbacks: "Tell me more", "Explain simply", "Give an example"
35
+
36
+### 3. Multimodal Support (Images/Documents)
37
+
38
+**Packages:** `file_picker`, `image_picker`
39
+
40
+**Implementation:**
41
+- Update `Message` model to support `attachmentPaths`
42
+- Add paperclip `[📎]` icon to `ChatInputBar`
43
+- Allow selecting images (`.png`, `.jpg`)
44
+- If model is multimodal (LLaVA, GPT-4o, Claude 3.5 Sonnet), encode image as Base64
45
+- Display image thumbnails in user chat bubble
46
+
47
+### 4. Tablet & Desktop Optimized Layout
48
+
49
+**Implementation:**
50
+- Detect screen width (`MediaQuery.of(context).size.width`)
51
+- If width > 600px (tablet) or 900px (desktop):
52
+ - Pin conversation drawer to left side permanently
53
+ - Show `ChatPage` in remaining space
54
+- Limit chat bubble width to ~800px, centered on wide screens
55
+
56
+### 5. Export Conversation
57
+
58
+**Implementation:**
59
+- Add "Export Chat" option to conversation context menu
60
+- Formats:
61
+ - **Markdown (.md)**: Plain text export
62
+ - **PDF**: Use `pdf` package to render chat history
63
+- Use `share_plus` to share to other apps
64
+
65
+### 6. Quick-Launch Shortcut / Widget
66
+
67
+**Implementation:**
68
+- Use `quick_actions` package for home screen shortcuts
69
+- Add "New Chat" shortcut from OS launcher icon
70
+
71
+---
72
+
73
+## Feature Folder Structure
74
+
75
+Extended features are organized within existing features or as new sub-features:
76
+
77
+```
78
+features/
79
+├── chat/
80
+│ └── views/components/
81
+│ └── smart_reply_chips.dart # Feature 2
82
+│
83
+├── settings/
84
+│ └── views/
85
+│ └── components/
86
+│ └── voice_settings.dart # Feature 1 (settings portion)
87
+│
88
+├── extended/ # New feature for multimodal/export
89
+│ ├── data/
90
+│ ├── providers/
91
+│ └── views/
92
+│ ├── image_attachment_sheet.dart # Feature 3
93
+│ └── export_dialog.dart # Feature 5
94
+```
95
+
96
+---
97
+
98
+## Implementation Notes
99
+
100
+### TTS Integration
101
+
102
+```dart
103
+class TTSService {
104
+ final FlutterTts _flutterTts = FlutterTts();
105
+
106
+ Future<void> speak(String text);
107
+ Future<void> stop();
108
+ Future<void> setVoice(String voiceId);
109
+
110
+ // Events
111
+ Stream<void> get onStart;
112
+ Stream<void> get onComplete;
113
+}
114
+```
115
+
116
+### Smart Reply Generation
117
+
118
+```dart
119
+class SmartReplyService {
120
+ final ChatService _chatService;
121
+
122
+ Future<List<String>> generateReplies({
123
+ required Server server,
124
+ required String modelId,
125
+ required List<Message> conversation,
126
+ required Persona? persona,
127
+ });
128
+
129
+ // Fallback suggestions
130
+ static const List<String> fallbackSuggestions = [
131
+ "Tell me more",
132
+ "Can you explain this more simply?",
133
+ "Give me an example",
134
+ "What are the pros and cons?",
135
+ "How does this work?",
136
+ ];
137
+}
138
+```
139
+
140
+### Image Attachment
141
+
142
+```dart
143
+class ImageAttachmentService {
144
+ final ImagePicker _picker = ImagePicker();
145
+
146
+ Future<String?> pickImage();
147
+ Future<String> encodeImageToBase64(String path);
148
+ bool isMultimodalModel(String? modelId);
149
+}
150
+```
151
+
152
+### Responsive Layout
153
+
154
+```dart
155
+class ResponsiveLayout extends StatelessWidget {
156
+ const ResponsiveLayout({
157
+ super.key,
158
+ required this.chatPage,
159
+ required this.drawer,
160
+ });
161
+
162
+ final Widget chatPage;
163
+ final Widget drawer;
164
+
165
+ @override
166
+ Widget build(BuildContext context) {
167
+ final width = MediaQuery.of(context).size.width;
168
+ if (width > 900) {
169
+ // Tablet/Desktop: Permanent drawer
170
+ return Row(children: [drawer, Expanded(child: chatPage)]);
171
+ }
172
+ // Mobile: Drawer in scaffold
173
+ return chatPage;
174
+ }
175
+}
176
+```
177
+
178
+---
179
+
180
+## Acceptance Criteria
181
+
182
+- [ ] TTS button reads message text aloud; can be stopped.
183
+- [ ] Smart reply chips appear after completion and populate input when tapped.
184
+- [ ] User can attach image and send to multimodal model.
185
+- [ ] UI scales responsively on larger screens (persistent sidebar).
186
+- [ ] User can export conversation to Markdown and share it.
187
+- [ ] Quick action shortcuts available from OS launcher.
plans/plan.md
new
+419
@@ -0,0 +1,419 @@
1
+# LocalMind — Project Plan
2
+
3
+## App Name: **LocalMind**
4
+
5
+**Tagline:** _Your AI. Your Device. Your Rules._
6
+
7
+The name "LocalMind" communicates two things immediately:
8
+
9
+- **Local** — runs on your network, no cloud, no tracking, no subscriptions
10
+- **Mind** — intelligent, conversational, thoughtful
11
+
12
+It's short, memorable, and instantly signals privacy-first, offline-capable AI.
13
+
14
+---
15
+
16
+## Vision
17
+
18
+LocalMind is a premium Flutter mobile app that acts as a beautiful, fast, and privacy-respecting interface for local LLM servers (LM Studio, Ollama) and optional cloud providers (OpenRouter). Inspired by the polish of ChatGPT and Claude's UI, but built for power users who want full control over their AI stack.
19
+
20
+---
21
+
22
+## Tech Stack
23
+
24
+| Layer | Technology |
25
+| ------------------ | -------------------------------------------------------------------- |
26
+| Framework | Flutter (latest stable) |
27
+| UI Components | shadcn-inspired Flutter package (`flutter_shadcn_ui` or custom port) |
28
+| State Management | Riverpod |
29
+| Local Storage | Hive CE Edition |
30
+| Networking | Dio (HTTP client for LM Studio / Ollama REST APIs) |
31
+| Markdown Rendering | `flutter_markdown` |
32
+| Code Highlighting | `flutter_highlight` |
33
+| TTS | `flutter_tts` |
34
+| File Handling | `file_picker` + `path_provider` |
35
+| Platform | Android first, iOS later |
36
+
37
+---
38
+
39
+## Architecture: Feature-Based with Riverpod
40
+
41
+This project follows the **Feature-Based Architecture** where code is organized by feature (vertical slices) rather than by technical layer. Shared, cross-cutting concerns live in a `core/` directory. State management is handled exclusively with **Riverpod**.
42
+
43
+### Core Principles
44
+
45
+- **Feature Isolation:** Each feature owns its data, providers, and views. Features must **never** import from other features. If two features need the same code, move it to `core/`.
46
+- **Riverpod-First State Management:** All state is managed via Riverpod providers. No `ChangeNotifier`, `Bloc`, or raw `setState` for business logic.
47
+- **Core for Shared Code:** Anything used by 2+ features belongs in `core/`. This includes shared models, repositories, providers, components, constants, utilities, and infrastructure.
48
+- **Unidirectional Data Flow:** Data flows from Repository → Provider/StateNotifier → View. User events flow from View → Provider/StateNotifier → Repository.
49
+- **Single Source of Truth (SSOT):** Each piece of data has exactly one authoritative source — the Repository. Providers expose that data reactively to the UI.
50
+
51
+---
52
+
53
+## Project Structure
54
+
55
+```
56
+lib/
57
+├── main.dart # Entry point
58
+├── app.dart # App root (MaterialApp / ShadCN App)
59
+│
60
+├── core/ # Shared, cross-cutting concerns (GLOBAL)
61
+│ ├── constants/ # App-wide constants (colors, URLs, config)
62
+│ │ └── app_constants.dart
63
+│ ├── models/ # Shared domain models
64
+│ │ ├── server.dart # Server model + enums (ServerType, ConnectionStatus)
65
+│ │ ├── message.dart # Message model + enums (MessageRole, MessageStatus)
66
+│ │ ├── conversation.dart # Conversation model
67
+│ │ ├── persona.dart # Persona model
68
+│ │ ├── model_info.dart # LLM model metadata
69
+│ │ ├── chat_parameters.dart # Chat parameter config
70
+│ │ └── app_settings.dart # App settings model
71
+│ ├── providers/ # Shared Riverpod providers
72
+│ │ └── app_providers.dart # Theme, active server, active conversation
73
+│ ├── repository/ # Shared repositories & services
74
+│ │ ├── storage/ # Hive storage (init, adapters, box references)
75
+│ │ │ ├── hive_initializer.dart
76
+│ │ │ ├── hive_keys.dart
77
+│ │ │ └── hive_boxes.dart
78
+│ │ ├── server/ # Server API service
79
+│ │ │ └── server_repository.dart
80
+│ │ └── chat/ # Chat service + implementations
81
+│ │ ├── chat_service.dart
82
+│ │ ├── lm_studio_chat_service.dart
83
+│ │ ├── ollama_chat_service.dart
84
+│ │ └── openrouter_chat_service.dart
85
+│ ├── routes/ # App routing (GoRouter config)
86
+│ │ └── app_router.dart
87
+│ ├── theme/ # App theme data
88
+│ │ ├── app_theme.dart
89
+│ │ ├── colors.dart
90
+│ │ └── typography.dart
91
+│ ├── components/ # Reusable UI widgets (shared across features)
92
+│ │ ├── app_button.dart
93
+│ │ ├── app_card.dart
94
+│ │ ├── app_text_field.dart
95
+│ │ └── ...
96
+│ └── utils/ # General-purpose utilities
97
+│ ├── uuid.dart
98
+│ └── date_utils.dart
99
+│
100
+├── features/ # Feature modules (vertical slices)
101
+│ ├── server_connection/ # Server setup & management
102
+│ │ ├── data/
103
+│ │ │ └── models/
104
+│ │ ├── providers/
105
+│ │ │ └── server_connection_providers.dart
106
+│ │ └── views/
107
+│ │ ├── server_list_page.dart
108
+│ │ ├── add_server_page.dart
109
+│ │ └── components/
110
+│ │ ├── server_card.dart
111
+│ │ ├── connection_status_indicator.dart
112
+│ │ └── server_type_selector.dart
113
+│ │
114
+│ ├── chat/ # Chat interface
115
+│ │ ├── data/
116
+│ │ │ └── models/
117
+│ │ ├── providers/
118
+│ │ │ └── chat_providers.dart
119
+│ │ └── views/
120
+│ │ ├── chat_page.dart
121
+│ │ └── components/
122
+│ │ ├── chat_bubble.dart
123
+│ │ ├── chat_input_bar.dart
124
+│ │ ├── typing_indicator.dart
125
+│ │ ├── message_action_bar.dart
126
+│ │ └── code_block.dart
127
+│ │
128
+│ ├── model_management/ # Model browsing & selection
129
+│ │ ├── data/
130
+│ │ │ └── models/
131
+│ │ ├── providers/
132
+│ │ │ └── model_management_providers.dart
133
+│ │ └── views/
134
+│ │ ├── model_picker_sheet.dart
135
+│ │ └── components/
136
+│ │ ├── model_tile.dart
137
+│ │ └── model_metadata_chip.dart
138
+│ │
139
+│ ├── conversation/ # Conversation history management
140
+│ │ ├── data/
141
+│ │ │ └── models/
142
+│ │ ├── providers/
143
+│ │ │ └── conversation_providers.dart
144
+│ │ └── views/
145
+│ │ ├── conversation_drawer.dart
146
+│ │ └── components/
147
+│ │ ├── conversation_tile.dart
148
+│ │ ├── conversation_search_bar.dart
149
+│ │ └── date_section_header.dart
150
+│ │
151
+│ ├── persona/ # Templates & personas
152
+│ │ ├── data/
153
+│ │ │ └── models/
154
+│ │ ├── providers/
155
+│ │ │ └── persona_providers.dart
156
+│ │ └── views/
157
+│ │ ├── persona_list_page.dart
158
+│ │ ├── create_persona_page.dart
159
+│ │ └── components/
160
+│ │ ├── persona_card.dart
161
+│ │ └── persona_category_chips.dart
162
+│ │
163
+│ └── settings/ # App settings
164
+│ ├── data/
165
+│ │ └── models/
166
+│ ├── providers/
167
+│ │ └── settings_providers.dart
168
+│ └── views/
169
+│ ├── settings_page.dart
170
+│ └── components/
171
+│ ├── settings_section.dart
172
+│ ├── settings_slider.dart
173
+│ ├── settings_toggle.dart
174
+│ ├── theme_toggle.dart
175
+│ └── font_size_preview.dart
176
+│
177
+└── loc/ # Localization (optional, future)
178
+```
179
+
180
+---
181
+
182
+## Core (Global) Layer
183
+
184
+The `core/` directory holds code that is **shared across multiple features**. The rule is simple: _if it's used by 2+ features, it belongs in `core/`_.
185
+
186
+### `core/constants/`
187
+
188
+App-wide constant values.
189
+
190
+| What to put here | Example |
191
+|-----------------|---------|
192
+| Color constants | `AppColors` with static `Color` fields |
193
+| URL constants | `AppUrls` with static URL strings (API base, privacy policy) |
194
+| Size/spacing constants | `AppSizes` with static doubles for padding, radius |
195
+| Config values | API timeouts, pagination limits |
196
+
197
+### `core/models/`
198
+
199
+Domain models shared across features. All Hive models live here:
200
+- `Server` — server connection profiles
201
+- `Message` — chat messages
202
+- `Conversation` — conversation threads
203
+- `Persona` — AI personas
204
+- `ModelInfo` — LLM model metadata
205
+- `ChatParameters` — chat configuration
206
+- `AppSettings` — app preferences
207
+
208
+### `core/providers/`
209
+
210
+Riverpod providers consumed by multiple features:
211
+- `themeProvider` — current theme mode
212
+- `activeServerProvider` — currently selected server
213
+- `activeConversationProvider` — currently active conversation
214
+- `hiveBoxesProvider` — opened Hive boxes
215
+
216
+### `core/repository/`
217
+
218
+Repositories and services for shared data domains:
219
+
220
+```
221
+core/repository/
222
+├── storage/ # Hive initialization, adapters, box access
223
+├── server/ # ServerApiService (connection testing, model fetching)
224
+└── chat/ # ChatService + implementations (LM Studio, Ollama, OpenRouter)
225
+```
226
+
227
+### `core/routes/`
228
+
229
+App-level routing and navigation configuration (GoRouter setup, route constants).
230
+
231
+### `core/theme/`
232
+
233
+App theme data — `ThemeData`, text styles, component themes. Color definitions reference `core/constants/`.
234
+
235
+### `core/components/`
236
+
237
+Reusable UI widgets consumed by multiple features. These are **presentational** widgets that receive data via constructor params — no business logic.
238
+
239
+### `core/utils/`
240
+
241
+General-purpose utility classes and functions (UUID generation, date formatters, etc.).
242
+
243
+---
244
+
245
+## Feature Layer
246
+
247
+Each feature directory follows a **consistent internal structure**:
248
+
249
+```
250
+features/<feature_name>/
251
+├── data/ # Data layer
252
+│ └── models/ # Feature-specific models (if any, beyond core models)
253
+│
254
+├── providers/ # State management
255
+│ └── <feature>_providers.dart # All Riverpod providers for this feature
256
+│
257
+└── views/ # UI layer
258
+ ├── <feature>_page.dart # Main page widget(s)
259
+ ├── add_<feature>_page.dart # Create/edit page (if applicable)
260
+ └── components/ # Feature-specific UI components
261
+ └── <component>.dart
262
+```
263
+
264
+### Rules
265
+
266
+1. **Never import from another feature.** If feature A needs something from feature B, extract it to `core/`.
267
+2. **Keep views lean.** Views (`ConsumerWidget` / `ConsumerStatefulWidget`) should only handle UI concerns — layout, animations, navigation. All business logic lives in providers.
268
+3. **One provider file per feature** is the default. Split only when the file exceeds ~400 lines.
269
+4. **Models must have serialization.** Every model needs `fromMap()` / `fromJson()` and `toMap()` / `toJson()` along with `copyWith()`.
270
+
271
+---
272
+
273
+## State Management with Riverpod
274
+
275
+### Provider Types Used
276
+
277
+| Provider Type | When to Use | Example |
278
+|---------------|-------------|---------|
279
+| `Provider` | Expose a stateless repository or service instance | `final repoProvider = Provider<Repo>((ref) => Repo());` |
280
+| `StateProvider` | Simple mutable state (filters, toggles, search query) | `final searchProvider = StateProvider<String>((ref) => '');` |
281
+| `StateNotifierProvider` | Complex state with methods (CRUD controllers, paginated lists) | `final listProvider = StateNotifierProvider<Notifier, State>((ref) { ... });` |
282
+| `FutureProvider` | One-shot async data fetching | `final dataProvider = FutureProvider<List<Item>>((ref) async { ... });` |
283
+| `FutureProvider.family` | Parameterized async data fetching | `final itemProvider = FutureProvider.family<Item?, String>((ref, id) async { ... });` |
284
+| `StreamProvider` | Real-time data streams | `final streamProvider = StreamProvider<List<Item>>((ref) { ... });` |
285
+
286
+### Provider File Organization
287
+
288
+Each feature's `providers/` file is organized in this order:
289
+
290
+1. **Repository Providers** — `Provider<XRepository>`
291
+2. **Filter/UI State Providers** — `StateProvider<String>`, `StateProvider<bool>`
292
+3. **State Classes** — Immutable state objects with `copyWith()`
293
+4. **StateNotifier Classes** — Business logic
294
+5. **StateNotifierProvider declarations** — Wiring notifiers to their dependencies
295
+
296
+### Use `autoDispose` by Default
297
+
298
+Prefer `.autoDispose` on providers scoped to a screen or feature:
299
+
300
+```dart
301
+final featureListProvider = StateNotifierProvider.autoDispose<
302
+ FeatureListNotifier, FeatureListState
303
+>((ref) { ... });
304
+```
305
+
306
+---
307
+
308
+## Core Features (MVP)
309
+
310
+### 1. Server Connection (`features/server_connection/`)
311
+
312
+- Add LM Studio server (IP + port)
313
+- Add Ollama server (IP + port)
314
+- Add OpenRouter (API key)
315
+- Connection health indicator (ping status)
316
+- Multiple server profiles
317
+
318
+### 2. Chat Interface (`features/chat/`)
319
+
320
+- ChatGPT/Claude-like chat bubbles
321
+- Markdown rendering with syntax highlighting
322
+- Streaming responses (SSE support)
323
+- Copy, retry, delete individual messages
324
+- Long-press context menu on messages
325
+
326
+### 3. Model Management (`features/model_management/`)
327
+
328
+- Fetch and list available models from connected server
329
+- Swap models mid-chat
330
+- Display model metadata (size, context length, etc.)
331
+
332
+### 4. Conversation Management (`features/conversation/`)
333
+
334
+- Create, rename, delete conversations
335
+- Conversation history with search
336
+- Pin important chats
337
+
338
+### 5. Persona Management (`features/persona/`)
339
+
340
+- Built-in presets: Code Assistant, Math Tutor, Story Writer, General Tutor
341
+- Create custom persona (system prompt + name + avatar emoji)
342
+- Apply persona per conversation
343
+
344
+### 6. Settings (`features/settings/`)
345
+
346
+- Chat parameters: temperature, top_p, max tokens, context length
347
+- UI preferences: theme (dark/light), font size
348
+- Default server selection
349
+
350
+---
351
+
352
+## Extended Features (Post-MVP)
353
+
354
+| Feature | Priority |
355
+| -------------------------------------------- | -------- |
356
+| AI Voice (TTS) — read responses aloud | High |
357
+| Context Smart Replies — suggested follow-ups | High |
358
+| Multimodal — attach images/documents | Medium |
359
+| Tablet-optimized split layout | Medium |
360
+| Export conversation (Markdown, PDF) | Medium |
361
+| Widget / quick-launch shortcut | Low |
362
+| Haptic feedback | Low |
363
+
364
+---
365
+
366
+## Design Philosophy
367
+
368
+- **Shadcn-inspired**: Clean, neutral, component-driven. Cards, popovers, command palettes, sheets.
369
+- **Dark-first**: Primary theme is a deep neutral dark (not pure black). Light mode optional.
370
+- **Density-aware**: Comfortable padding on phone, denser on tablet.
371
+- **No fluff**: Every UI element serves a purpose. No splash screens, no onboarding carousels beyond essentials.
372
+
373
+---
374
+
375
+## Privacy Commitments
376
+
377
+- No analytics by default
378
+- Conversations stored only on-device (Hive encrypted)
379
+- Network requests go only to user-configured servers
380
+- "LocalMind never sees your data" — shown in onboarding and settings
381
+- Optional: show a live indicator of where data is going (local vs. cloud)
382
+
383
+---
384
+
385
+## Milestones
386
+
387
+| Phase | Deliverable | Est. Time |
388
+| --------------- | ---------------------------------------- | --------- |
389
+| 0 — Setup | Flutter project, core folder structure, shadcn theme, routing | 1 week |
390
+| 1 — Core | Server config + basic chat (LM Studio) | 2 weeks |
391
+| 2 — Chat Polish | Streaming, markdown, model swap | 2 weeks |
392
+| 3 — Features | Personas, conversation history, settings | 2 weeks |
393
+| 4 — Extended | TTS, smart replies, multimodal | 3 weeks |
394
+| 5 — Release | Polish, Play Store prep, beta testing | 2 weeks |
395
+
396
+---
397
+
398
+## Monetization (Optional)
399
+
400
+- Free: Core chat, 1 server, basic personas
401
+- One-time unlock (~$3–5): Unlimited servers, TTS, smart replies, multimodal, no ads
402
+- No subscriptions. Ever.
403
+
404
+---
405
+
406
+## Tracking Checklist
407
+
408
+Follow these detailed breakdown documents to implement the project:
409
+
410
+- [ ] [Feature 00 — Project Setup & Scaffolding](00_project_setup.md)
411
+- [ ] [Feature 01 — Server Connection & Management](01_server_connection.md)
412
+- [ ] [Feature 02 — Chat Interface](02_chat_interface.md)
413
+- [ ] [Feature 03 — Model Management](03_model_management.md)
414
+- [ ] [Feature 04 — Conversation Management](04_conversation_management.md)
415
+- [ ] [Feature 05 — Templates & Personas](05_templates_personas.md)
416
+- [ ] [Feature 06 — Settings](06_settings.md)
417
+- [ ] [Feature 07 — Core Architecture](07_core_architecture.md)
418
+- [ ] [Feature 08 — Design System](08_design_system.md)
419
+- [ ] [Feature 09 — Extended Features](09_extended_features.md)
pubspec.lock
new
+1175
@@ -0,0 +1,1175 @@
1
+# Generated by pub
2
+# See https://dart.dev/tools/pub/glossary#lockfile
3
+packages:
4
+ _fe_analyzer_shared:
5
+ dependency: transitive
6
+ description:
7
+ name: _fe_analyzer_shared
8
+ sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d"
9
+ url: "https://pub.dev"
10
+ source: hosted
11
+ version: "93.0.0"
12
+ analyzer:
13
+ dependency: transitive
14
+ description:
15
+ name: analyzer
16
+ sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b
17
+ url: "https://pub.dev"
18
+ source: hosted
19
+ version: "10.0.1"
20
+ args:
21
+ dependency: transitive
22
+ description:
23
+ name: args
24
+ sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
25
+ url: "https://pub.dev"
26
+ source: hosted
27
+ version: "2.7.0"
28
+ async:
29
+ dependency: transitive
30
+ description:
31
+ name: async
32
+ sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
33
+ url: "https://pub.dev"
34
+ source: hosted
35
+ version: "2.13.1"
36
+ boolean_selector:
37
+ dependency: transitive
38
+ description:
39
+ name: boolean_selector
40
+ sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
41
+ url: "https://pub.dev"
42
+ source: hosted
43
+ version: "2.1.2"
44
+ boxy:
45
+ dependency: transitive
46
+ description:
47
+ name: boxy
48
+ sha256: "569373f23560f5a5dbe53c08a7463a698635e7ac72ba355ff4fa52516c0d2e32"
49
+ url: "https://pub.dev"
50
+ source: hosted
51
+ version: "2.2.2"
52
+ build:
53
+ dependency: transitive
54
+ description:
55
+ name: build
56
+ sha256: aadd943f4f8cc946882c954c187e6115a84c98c81ad1d9c6cbf0895a8c85da9c
57
+ url: "https://pub.dev"
58
+ source: hosted
59
+ version: "4.0.5"
60
+ build_config:
61
+ dependency: transitive
62
+ description:
63
+ name: build_config
64
+ sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71"
65
+ url: "https://pub.dev"
66
+ source: hosted
67
+ version: "1.3.0"
68
+ build_daemon:
69
+ dependency: transitive
70
+ description:
71
+ name: build_daemon
72
+ sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957
73
+ url: "https://pub.dev"
74
+ source: hosted
75
+ version: "4.1.1"
76
+ build_runner:
77
+ dependency: "direct dev"
78
+ description:
79
+ name: build_runner
80
+ sha256: "521daf8d189deb79ba474e43a696b41c49fb3987818dbacf3308f1e03673a75e"
81
+ url: "https://pub.dev"
82
+ source: hosted
83
+ version: "2.13.1"
84
+ built_collection:
85
+ dependency: transitive
86
+ description:
87
+ name: built_collection
88
+ sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
89
+ url: "https://pub.dev"
90
+ source: hosted
91
+ version: "5.1.1"
92
+ built_value:
93
+ dependency: transitive
94
+ description:
95
+ name: built_value
96
+ sha256: "0730c18c770d05636a8f945c32a4d7d81cb6e0f0148c8db4ad12e7748f7e49af"
97
+ url: "https://pub.dev"
98
+ source: hosted
99
+ version: "8.12.5"
100
+ characters:
101
+ dependency: transitive
102
+ description:
103
+ name: characters
104
+ sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
105
+ url: "https://pub.dev"
106
+ source: hosted
107
+ version: "1.4.1"
108
+ checked_yaml:
109
+ dependency: transitive
110
+ description:
111
+ name: checked_yaml
112
+ sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
113
+ url: "https://pub.dev"
114
+ source: hosted
115
+ version: "2.0.4"
116
+ cli_config:
117
+ dependency: transitive
118
+ description:
119
+ name: cli_config
120
+ sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
121
+ url: "https://pub.dev"
122
+ source: hosted
123
+ version: "0.2.0"
124
+ clock:
125
+ dependency: transitive
126
+ description:
127
+ name: clock
128
+ sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
129
+ url: "https://pub.dev"
130
+ source: hosted
131
+ version: "1.1.2"
132
+ code_assets:
133
+ dependency: transitive
134
+ description:
135
+ name: code_assets
136
+ sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
137
+ url: "https://pub.dev"
138
+ source: hosted
139
+ version: "1.0.0"
140
+ code_builder:
141
+ dependency: transitive
142
+ description:
143
+ name: code_builder
144
+ sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d"
145
+ url: "https://pub.dev"
146
+ source: hosted
147
+ version: "4.11.1"
148
+ collection:
149
+ dependency: transitive
150
+ description:
151
+ name: collection
152
+ sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
153
+ url: "https://pub.dev"
154
+ source: hosted
155
+ version: "1.19.1"
156
+ convert:
157
+ dependency: transitive
158
+ description:
159
+ name: convert
160
+ sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
161
+ url: "https://pub.dev"
162
+ source: hosted
163
+ version: "3.1.2"
164
+ coverage:
165
+ dependency: transitive
166
+ description:
167
+ name: coverage
168
+ sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d"
169
+ url: "https://pub.dev"
170
+ source: hosted
171
+ version: "1.15.0"
172
+ cross_file:
173
+ dependency: transitive
174
+ description:
175
+ name: cross_file
176
+ sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
177
+ url: "https://pub.dev"
178
+ source: hosted
179
+ version: "0.3.5+2"
180
+ crypto:
181
+ dependency: transitive
182
+ description:
183
+ name: crypto
184
+ sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
185
+ url: "https://pub.dev"
186
+ source: hosted
187
+ version: "3.0.7"
188
+ cupertino_icons:
189
+ dependency: "direct main"
190
+ description:
191
+ name: cupertino_icons
192
+ sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
193
+ url: "https://pub.dev"
194
+ source: hosted
195
+ version: "1.0.9"
196
+ dart_style:
197
+ dependency: transitive
198
+ description:
199
+ name: dart_style
200
+ sha256: "29f7ecc274a86d32920b1d9cfc7502fa87220da41ec60b55f329559d5732e2b2"
201
+ url: "https://pub.dev"
202
+ source: hosted
203
+ version: "3.1.7"
204
+ dbus:
205
+ dependency: transitive
206
+ description:
207
+ name: dbus
208
+ sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270
209
+ url: "https://pub.dev"
210
+ source: hosted
211
+ version: "0.7.12"
212
+ device_info_plus:
213
+ dependency: transitive
214
+ description:
215
+ name: device_info_plus
216
+ sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a"
217
+ url: "https://pub.dev"
218
+ source: hosted
219
+ version: "11.5.0"
220
+ device_info_plus_platform_interface:
221
+ dependency: transitive
222
+ description:
223
+ name: device_info_plus_platform_interface
224
+ sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f
225
+ url: "https://pub.dev"
226
+ source: hosted
227
+ version: "7.0.3"
228
+ dio:
229
+ dependency: "direct main"
230
+ description:
231
+ name: dio
232
+ sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c
233
+ url: "https://pub.dev"
234
+ source: hosted
235
+ version: "5.9.2"
236
+ dio_web_adapter:
237
+ dependency: transitive
238
+ description:
239
+ name: dio_web_adapter
240
+ sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340"
241
+ url: "https://pub.dev"
242
+ source: hosted
243
+ version: "2.1.2"
244
+ extended_image:
245
+ dependency: transitive
246
+ description:
247
+ name: extended_image
248
+ sha256: f6cbb1d798f51262ed1a3d93b4f1f2aa0d76128df39af18ecb77fa740f88b2e0
249
+ url: "https://pub.dev"
250
+ source: hosted
251
+ version: "10.0.1"
252
+ extended_image_library:
253
+ dependency: transitive
254
+ description:
255
+ name: extended_image_library
256
+ sha256: "1f9a24d3a00c2633891c6a7b5cab2807999eb2d5b597e5133b63f49d113811fe"
257
+ url: "https://pub.dev"
258
+ source: hosted
259
+ version: "5.0.1"
260
+ fake_async:
261
+ dependency: transitive
262
+ description:
263
+ name: fake_async
264
+ sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
265
+ url: "https://pub.dev"
266
+ source: hosted
267
+ version: "1.3.3"
268
+ ffi:
269
+ dependency: transitive
270
+ description:
271
+ name: ffi
272
+ sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
273
+ url: "https://pub.dev"
274
+ source: hosted
275
+ version: "2.2.0"
276
+ file:
277
+ dependency: transitive
278
+ description:
279
+ name: file
280
+ sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
281
+ url: "https://pub.dev"
282
+ source: hosted
283
+ version: "7.0.1"
284
+ file_picker:
285
+ dependency: "direct main"
286
+ description:
287
+ name: file_picker
288
+ sha256: "57d9a1dd5063f85fa3107fb42d1faffda52fdc948cefd5fe5ea85267a5fc7343"
289
+ url: "https://pub.dev"
290
+ source: hosted
291
+ version: "10.3.10"
292
+ fixnum:
293
+ dependency: transitive
294
+ description:
295
+ name: fixnum
296
+ sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
297
+ url: "https://pub.dev"
298
+ source: hosted
299
+ version: "1.1.1"
300
+ flutter:
301
+ dependency: "direct main"
302
+ description: flutter
303
+ source: sdk
304
+ version: "0.0.0"
305
+ flutter_animate:
306
+ dependency: transitive
307
+ description:
308
+ name: flutter_animate
309
+ sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5"
310
+ url: "https://pub.dev"
311
+ source: hosted
312
+ version: "4.5.2"
313
+ flutter_lints:
314
+ dependency: "direct dev"
315
+ description:
316
+ name: flutter_lints
317
+ sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
318
+ url: "https://pub.dev"
319
+ source: hosted
320
+ version: "6.0.0"
321
+ flutter_localizations:
322
+ dependency: transitive
323
+ description: flutter
324
+ source: sdk
325
+ version: "0.0.0"
326
+ flutter_markdown_plus:
327
+ dependency: "direct main"
328
+ description:
329
+ name: flutter_markdown_plus
330
+ sha256: "039177906850278e8fb1cd364115ee0a46281135932fa8ecea8455522166d2de"
331
+ url: "https://pub.dev"
332
+ source: hosted
333
+ version: "1.0.7"
334
+ flutter_plugin_android_lifecycle:
335
+ dependency: transitive
336
+ description:
337
+ name: flutter_plugin_android_lifecycle
338
+ sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0"
339
+ url: "https://pub.dev"
340
+ source: hosted
341
+ version: "2.0.34"
342
+ flutter_riverpod:
343
+ dependency: "direct main"
344
+ description:
345
+ name: flutter_riverpod
346
+ sha256: "4e166be88e1dbbaa34a280bdb744aeae73b7ef25fdf8db7a3bb776760a3648e2"
347
+ url: "https://pub.dev"
348
+ source: hosted
349
+ version: "3.3.1"
350
+ flutter_shaders:
351
+ dependency: transitive
352
+ description:
353
+ name: flutter_shaders
354
+ sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2"
355
+ url: "https://pub.dev"
356
+ source: hosted
357
+ version: "0.1.3"
358
+ flutter_svg:
359
+ dependency: transitive
360
+ description:
361
+ name: flutter_svg
362
+ sha256: "1ded017b39c8e15c8948ea855070a5ff8ff8b3d5e83f3446e02d6bb12add7ad9"
363
+ url: "https://pub.dev"
364
+ source: hosted
365
+ version: "2.2.4"
366
+ flutter_test:
367
+ dependency: "direct dev"
368
+ description: flutter
369
+ source: sdk
370
+ version: "0.0.0"
371
+ flutter_tts:
372
+ dependency: "direct main"
373
+ description:
374
+ name: flutter_tts
375
+ sha256: ce5eb209b40e95f2f4a1397116c87ab2fcdff32257d04ed7a764e75894c03775
376
+ url: "https://pub.dev"
377
+ source: hosted
378
+ version: "4.2.5"
379
+ flutter_web_plugins:
380
+ dependency: transitive
381
+ description: flutter
382
+ source: sdk
383
+ version: "0.0.0"
384
+ frontend_server_client:
385
+ dependency: transitive
386
+ description:
387
+ name: frontend_server_client
388
+ sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
389
+ url: "https://pub.dev"
390
+ source: hosted
391
+ version: "4.0.0"
392
+ glob:
393
+ dependency: transitive
394
+ description:
395
+ name: glob
396
+ sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
397
+ url: "https://pub.dev"
398
+ source: hosted
399
+ version: "2.1.3"
400
+ go_router:
401
+ dependency: "direct main"
402
+ description:
403
+ name: go_router
404
+ sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3
405
+ url: "https://pub.dev"
406
+ source: hosted
407
+ version: "14.8.1"
408
+ graphs:
409
+ dependency: transitive
410
+ description:
411
+ name: graphs
412
+ sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
413
+ url: "https://pub.dev"
414
+ source: hosted
415
+ version: "2.3.2"
416
+ haptic_feedback:
417
+ dependency: "direct main"
418
+ description:
419
+ name: haptic_feedback
420
+ sha256: dcc2494994c41428823f8f2082fd17a4e89e372bd142e07681420cbfaf99dcad
421
+ url: "https://pub.dev"
422
+ source: hosted
423
+ version: "0.6.4+3"
424
+ hive_ce:
425
+ dependency: "direct main"
426
+ description:
427
+ name: hive_ce
428
+ sha256: "8e9980e68643afb1e765d3af32b47996552a64e190d03faf622cea07c1294418"
429
+ url: "https://pub.dev"
430
+ source: hosted
431
+ version: "2.19.3"
432
+ hive_ce_flutter:
433
+ dependency: "direct main"
434
+ description:
435
+ name: hive_ce_flutter
436
+ sha256: "2677e95a333ff15af43ccd06af7eb7abbf1a4f154ea071997f3de4346cae913a"
437
+ url: "https://pub.dev"
438
+ source: hosted
439
+ version: "2.3.4"
440
+ hive_ce_generator:
441
+ dependency: "direct dev"
442
+ description:
443
+ name: hive_ce_generator
444
+ sha256: c9a7cda57823ffec35846c051637ddcd105eafa253e7395d9c8c0ed5e87fbb72
445
+ url: "https://pub.dev"
446
+ source: hosted
447
+ version: "1.11.1"
448
+ hooks:
449
+ dependency: transitive
450
+ description:
451
+ name: hooks
452
+ sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388
453
+ url: "https://pub.dev"
454
+ source: hosted
455
+ version: "1.0.2"
456
+ http:
457
+ dependency: transitive
458
+ description:
459
+ name: http
460
+ sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
461
+ url: "https://pub.dev"
462
+ source: hosted
463
+ version: "1.6.0"
464
+ http_client_helper:
465
+ dependency: transitive
466
+ description:
467
+ name: http_client_helper
468
+ sha256: "8a9127650734da86b5c73760de2b404494c968a3fd55602045ffec789dac3cb1"
469
+ url: "https://pub.dev"
470
+ source: hosted
471
+ version: "3.0.0"
472
+ http_multi_server:
473
+ dependency: transitive
474
+ description:
475
+ name: http_multi_server
476
+ sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
477
+ url: "https://pub.dev"
478
+ source: hosted
479
+ version: "3.2.2"
480
+ http_parser:
481
+ dependency: transitive
482
+ description:
483
+ name: http_parser
484
+ sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
485
+ url: "https://pub.dev"
486
+ source: hosted
487
+ version: "4.1.2"
488
+ hugeicons:
489
+ dependency: "direct main"
490
+ description:
491
+ name: hugeicons
492
+ sha256: d19c0e2b57ccf455dd8ef08b84da40ae6dbba898c92960a0a0ada77df7865b8a
493
+ url: "https://pub.dev"
494
+ source: hosted
495
+ version: "1.1.5"
496
+ intl:
497
+ dependency: transitive
498
+ description:
499
+ name: intl
500
+ sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
501
+ url: "https://pub.dev"
502
+ source: hosted
503
+ version: "0.20.2"
504
+ io:
505
+ dependency: transitive
506
+ description:
507
+ name: io
508
+ sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
509
+ url: "https://pub.dev"
510
+ source: hosted
511
+ version: "1.0.5"
512
+ irondash_engine_context:
513
+ dependency: transitive
514
+ description:
515
+ name: irondash_engine_context
516
+ sha256: "2bb0bc13dfda9f5aaef8dde06ecc5feb1379f5bb387d59716d799554f3f305d7"
517
+ url: "https://pub.dev"
518
+ source: hosted
519
+ version: "0.5.5"
520
+ irondash_message_channel:
521
+ dependency: transitive
522
+ description:
523
+ name: irondash_message_channel
524
+ sha256: b4101669776509c76133b8917ab8cfc704d3ad92a8c450b92934dd8884a2f060
525
+ url: "https://pub.dev"
526
+ source: hosted
527
+ version: "0.7.0"
528
+ isolate_channel:
529
+ dependency: transitive
530
+ description:
531
+ name: isolate_channel
532
+ sha256: a9d3d620695bc984244dafae00b95e4319d6974b2d77f4b9e1eb4f2efe099094
533
+ url: "https://pub.dev"
534
+ source: hosted
535
+ version: "0.6.1"
536
+ js:
537
+ dependency: transitive
538
+ description:
539
+ name: js
540
+ sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
541
+ url: "https://pub.dev"
542
+ source: hosted
543
+ version: "0.7.2"
544
+ json_annotation:
545
+ dependency: transitive
546
+ description:
547
+ name: json_annotation
548
+ sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8
549
+ url: "https://pub.dev"
550
+ source: hosted
551
+ version: "4.11.0"
552
+ leak_tracker:
553
+ dependency: transitive
554
+ description:
555
+ name: leak_tracker
556
+ sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
557
+ url: "https://pub.dev"
558
+ source: hosted
559
+ version: "11.0.2"
560
+ leak_tracker_flutter_testing:
561
+ dependency: transitive
562
+ description:
563
+ name: leak_tracker_flutter_testing
564
+ sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
565
+ url: "https://pub.dev"
566
+ source: hosted
567
+ version: "3.0.10"
568
+ leak_tracker_testing:
569
+ dependency: transitive
570
+ description:
571
+ name: leak_tracker_testing
572
+ sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
573
+ url: "https://pub.dev"
574
+ source: hosted
575
+ version: "3.0.2"
576
+ lints:
577
+ dependency: transitive
578
+ description:
579
+ name: lints
580
+ sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
581
+ url: "https://pub.dev"
582
+ source: hosted
583
+ version: "6.1.0"
584
+ logging:
585
+ dependency: transitive
586
+ description:
587
+ name: logging
588
+ sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
589
+ url: "https://pub.dev"
590
+ source: hosted
591
+ version: "1.3.0"
592
+ lucide_icons_flutter:
593
+ dependency: transitive
594
+ description:
595
+ name: lucide_icons_flutter
596
+ sha256: "39472ddc8c5fc586d06a619a3871817ea02dd17defd31e67dcfc9ec77658b2a1"
597
+ url: "https://pub.dev"
598
+ source: hosted
599
+ version: "3.1.11"
600
+ markdown:
601
+ dependency: transitive
602
+ description:
603
+ name: markdown
604
+ sha256: ee85086ad7698b42522c6ad42fe195f1b9898e4d974a1af4576c1a3a176cada9
605
+ url: "https://pub.dev"
606
+ source: hosted
607
+ version: "7.3.1"
608
+ matcher:
609
+ dependency: transitive
610
+ description:
611
+ name: matcher
612
+ sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
613
+ url: "https://pub.dev"
614
+ source: hosted
615
+ version: "0.12.18"
616
+ material_color_utilities:
617
+ dependency: transitive
618
+ description:
619
+ name: material_color_utilities
620
+ sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
621
+ url: "https://pub.dev"
622
+ source: hosted
623
+ version: "0.13.0"
624
+ meta:
625
+ dependency: transitive
626
+ description:
627
+ name: meta
628
+ sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
629
+ url: "https://pub.dev"
630
+ source: hosted
631
+ version: "1.17.0"
632
+ mime:
633
+ dependency: transitive
634
+ description:
635
+ name: mime
636
+ sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
637
+ url: "https://pub.dev"
638
+ source: hosted
639
+ version: "2.0.0"
640
+ native_toolchain_c:
641
+ dependency: transitive
642
+ description:
643
+ name: native_toolchain_c
644
+ sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
645
+ url: "https://pub.dev"
646
+ source: hosted
647
+ version: "0.17.6"
648
+ node_preamble:
649
+ dependency: transitive
650
+ description:
651
+ name: node_preamble
652
+ sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
653
+ url: "https://pub.dev"
654
+ source: hosted
655
+ version: "2.0.2"
656
+ objective_c:
657
+ dependency: transitive
658
+ description:
659
+ name: objective_c
660
+ sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
661
+ url: "https://pub.dev"
662
+ source: hosted
663
+ version: "9.3.0"
664
+ package_config:
665
+ dependency: transitive
666
+ description:
667
+ name: package_config
668
+ sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
669
+ url: "https://pub.dev"
670
+ source: hosted
671
+ version: "2.2.0"
672
+ path:
673
+ dependency: transitive
674
+ description:
675
+ name: path
676
+ sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
677
+ url: "https://pub.dev"
678
+ source: hosted
679
+ version: "1.9.1"
680
+ path_parsing:
681
+ dependency: transitive
682
+ description:
683
+ name: path_parsing
684
+ sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
685
+ url: "https://pub.dev"
686
+ source: hosted
687
+ version: "1.1.0"
688
+ path_provider:
689
+ dependency: "direct main"
690
+ description:
691
+ name: path_provider
692
+ sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
693
+ url: "https://pub.dev"
694
+ source: hosted
695
+ version: "2.1.5"
696
+ path_provider_android:
697
+ dependency: transitive
698
+ description:
699
+ name: path_provider_android
700
+ sha256: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba"
701
+ url: "https://pub.dev"
702
+ source: hosted
703
+ version: "2.2.23"
704
+ path_provider_foundation:
705
+ dependency: transitive
706
+ description:
707
+ name: path_provider_foundation
708
+ sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
709
+ url: "https://pub.dev"
710
+ source: hosted
711
+ version: "2.6.0"
712
+ path_provider_linux:
713
+ dependency: transitive
714
+ description:
715
+ name: path_provider_linux
716
+ sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
717
+ url: "https://pub.dev"
718
+ source: hosted
719
+ version: "2.2.1"
720
+ path_provider_platform_interface:
721
+ dependency: transitive
722
+ description:
723
+ name: path_provider_platform_interface
724
+ sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
725
+ url: "https://pub.dev"
726
+ source: hosted
727
+ version: "2.1.2"
728
+ path_provider_windows:
729
+ dependency: transitive
730
+ description:
731
+ name: path_provider_windows
732
+ sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
733
+ url: "https://pub.dev"
734
+ source: hosted
735
+ version: "2.3.0"
736
+ petitparser:
737
+ dependency: transitive
738
+ description:
739
+ name: petitparser
740
+ sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
741
+ url: "https://pub.dev"
742
+ source: hosted
743
+ version: "7.0.2"
744
+ pixel_snap:
745
+ dependency: transitive
746
+ description:
747
+ name: pixel_snap
748
+ sha256: "677410ea37b07cd37ecb6d5e6c0d8d7615a7cf3bd92ba406fd1ac57e937d1fb0"
749
+ url: "https://pub.dev"
750
+ source: hosted
751
+ version: "0.1.5"
752
+ platform:
753
+ dependency: transitive
754
+ description:
755
+ name: platform
756
+ sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
757
+ url: "https://pub.dev"
758
+ source: hosted
759
+ version: "3.1.6"
760
+ plugin_platform_interface:
761
+ dependency: transitive
762
+ description:
763
+ name: plugin_platform_interface
764
+ sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
765
+ url: "https://pub.dev"
766
+ source: hosted
767
+ version: "2.1.8"
768
+ pool:
769
+ dependency: transitive
770
+ description:
771
+ name: pool
772
+ sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
773
+ url: "https://pub.dev"
774
+ source: hosted
775
+ version: "1.5.2"
776
+ pub_semver:
777
+ dependency: transitive
778
+ description:
779
+ name: pub_semver
780
+ sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
781
+ url: "https://pub.dev"
782
+ source: hosted
783
+ version: "2.2.0"
784
+ pubspec_parse:
785
+ dependency: transitive
786
+ description:
787
+ name: pubspec_parse
788
+ sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
789
+ url: "https://pub.dev"
790
+ source: hosted
791
+ version: "1.5.0"
792
+ riverpod:
793
+ dependency: transitive
794
+ description:
795
+ name: riverpod
796
+ sha256: "8c22216be8ad3ef2b44af3a329693558c98eca7b8bd4ef495c92db0bba279f83"
797
+ url: "https://pub.dev"
798
+ source: hosted
799
+ version: "3.2.1"
800
+ serial_csv:
801
+ dependency: transitive
802
+ description:
803
+ name: serial_csv
804
+ sha256: "2d62bb70cb3ce7251383fc86ea9aae1298ab1e57af6ef4e93b6a9751c5c268dd"
805
+ url: "https://pub.dev"
806
+ source: hosted
807
+ version: "0.5.2"
808
+ shadcn_ui:
809
+ dependency: "direct main"
810
+ description:
811
+ name: shadcn_ui
812
+ sha256: "2b5fe5d30dd535611196e7403429b21b64dfd4bfec9ee694a711c99ed85c5414"
813
+ url: "https://pub.dev"
814
+ source: hosted
815
+ version: "0.53.1"
816
+ shelf:
817
+ dependency: transitive
818
+ description:
819
+ name: shelf
820
+ sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
821
+ url: "https://pub.dev"
822
+ source: hosted
823
+ version: "1.4.2"
824
+ shelf_packages_handler:
825
+ dependency: transitive
826
+ description:
827
+ name: shelf_packages_handler
828
+ sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
829
+ url: "https://pub.dev"
830
+ source: hosted
831
+ version: "3.0.2"
832
+ shelf_static:
833
+ dependency: transitive
834
+ description:
835
+ name: shelf_static
836
+ sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
837
+ url: "https://pub.dev"
838
+ source: hosted
839
+ version: "1.1.3"
840
+ shelf_web_socket:
841
+ dependency: transitive
842
+ description:
843
+ name: shelf_web_socket
844
+ sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
845
+ url: "https://pub.dev"
846
+ source: hosted
847
+ version: "3.0.0"
848
+ sky_engine:
849
+ dependency: transitive
850
+ description: flutter
851
+ source: sdk
852
+ version: "0.0.0"
853
+ slang:
854
+ dependency: transitive
855
+ description:
856
+ name: slang
857
+ sha256: ea6702ed6b1c82065fb2de906fe34ac9298117342e3c2ea2567132efdc81bd17
858
+ url: "https://pub.dev"
859
+ source: hosted
860
+ version: "4.14.0"
861
+ slang_flutter:
862
+ dependency: transitive
863
+ description:
864
+ name: slang_flutter
865
+ sha256: dcc4e77527c91b12348fc8bdd43d3eb92d8cea37c12a23a1f9719cdc12c804c6
866
+ url: "https://pub.dev"
867
+ source: hosted
868
+ version: "4.14.0"
869
+ source_gen:
870
+ dependency: transitive
871
+ description:
872
+ name: source_gen
873
+ sha256: "732792cfd197d2161a65bb029606a46e0a18ff30ef9e141a7a82172b05ea8ecd"
874
+ url: "https://pub.dev"
875
+ source: hosted
876
+ version: "4.2.2"
877
+ source_helper:
878
+ dependency: transitive
879
+ description:
880
+ name: source_helper
881
+ sha256: "1d3b229b2934034fb2e691fbb3d53e0f75a4af7b1407f88425ed8f209bcb1b8f"
882
+ url: "https://pub.dev"
883
+ source: hosted
884
+ version: "1.3.11"
885
+ source_map_stack_trace:
886
+ dependency: transitive
887
+ description:
888
+ name: source_map_stack_trace
889
+ sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
890
+ url: "https://pub.dev"
891
+ source: hosted
892
+ version: "2.1.2"
893
+ source_maps:
894
+ dependency: transitive
895
+ description:
896
+ name: source_maps
897
+ sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
898
+ url: "https://pub.dev"
899
+ source: hosted
900
+ version: "0.10.13"
901
+ source_span:
902
+ dependency: transitive
903
+ description:
904
+ name: source_span
905
+ sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
906
+ url: "https://pub.dev"
907
+ source: hosted
908
+ version: "1.10.2"
909
+ stack_trace:
910
+ dependency: transitive
911
+ description:
912
+ name: stack_trace
913
+ sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
914
+ url: "https://pub.dev"
915
+ source: hosted
916
+ version: "1.12.1"
917
+ state_notifier:
918
+ dependency: transitive
919
+ description:
920
+ name: state_notifier
921
+ sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb
922
+ url: "https://pub.dev"
923
+ source: hosted
924
+ version: "1.0.0"
925
+ stream_channel:
926
+ dependency: transitive
927
+ description:
928
+ name: stream_channel
929
+ sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
930
+ url: "https://pub.dev"
931
+ source: hosted
932
+ version: "2.1.4"
933
+ stream_transform:
934
+ dependency: transitive
935
+ description:
936
+ name: stream_transform
937
+ sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
938
+ url: "https://pub.dev"
939
+ source: hosted
940
+ version: "2.1.1"
941
+ string_scanner:
942
+ dependency: transitive
943
+ description:
944
+ name: string_scanner
945
+ sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
946
+ url: "https://pub.dev"
947
+ source: hosted
948
+ version: "1.4.1"
949
+ super_clipboard:
950
+ dependency: transitive
951
+ description:
952
+ name: super_clipboard
953
+ sha256: e73f3bb7e66cc9260efa1dc507f979138e7e106c3521e2dda2d0311f6d728a16
954
+ url: "https://pub.dev"
955
+ source: hosted
956
+ version: "0.9.1"
957
+ super_native_extensions:
958
+ dependency: transitive
959
+ description:
960
+ name: super_native_extensions
961
+ sha256: b9611dcb68f1047d6f3ef11af25e4e68a21b1a705bbcc3eb8cb4e9f5c3148569
962
+ url: "https://pub.dev"
963
+ source: hosted
964
+ version: "0.9.1"
965
+ syntax_highlight:
966
+ dependency: "direct main"
967
+ description:
968
+ name: syntax_highlight
969
+ sha256: "4d3ba40658cadba6ba55d697f29f00b43538ebb6eb4a0ca0e895c568eaced138"
970
+ url: "https://pub.dev"
971
+ source: hosted
972
+ version: "0.5.0"
973
+ term_glyph:
974
+ dependency: transitive
975
+ description:
976
+ name: term_glyph
977
+ sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
978
+ url: "https://pub.dev"
979
+ source: hosted
980
+ version: "1.2.2"
981
+ test:
982
+ dependency: transitive
983
+ description:
984
+ name: test
985
+ sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a"
986
+ url: "https://pub.dev"
987
+ source: hosted
988
+ version: "1.29.0"
989
+ test_api:
990
+ dependency: transitive
991
+ description:
992
+ name: test_api
993
+ sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
994
+ url: "https://pub.dev"
995
+ source: hosted
996
+ version: "0.7.9"
997
+ test_core:
998
+ dependency: transitive
999
+ description:
1000
+ name: test_core
1001
+ sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943"
1002
+ url: "https://pub.dev"
1003
+ source: hosted
1004
+ version: "0.6.15"
1005
+ theme_extensions_builder_annotation:
1006
+ dependency: transitive
1007
+ description:
1008
+ name: theme_extensions_builder_annotation
1009
+ sha256: "75f28ac85d396d143d111a47c1395b01f3be41b7135f37bd51512921944e4206"
1010
+ url: "https://pub.dev"
1011
+ source: hosted
1012
+ version: "7.3.0"
1013
+ two_dimensional_scrollables:
1014
+ dependency: transitive
1015
+ description:
1016
+ name: two_dimensional_scrollables
1017
+ sha256: "45f77f28f3f084ac3c79f74b075c4d719937a3972e71191efc533d6f9487c045"
1018
+ url: "https://pub.dev"
1019
+ source: hosted
1020
+ version: "0.4.1"
1021
+ typed_data:
1022
+ dependency: transitive
1023
+ description:
1024
+ name: typed_data
1025
+ sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
1026
+ url: "https://pub.dev"
1027
+ source: hosted
1028
+ version: "1.4.0"
1029
+ universal_image:
1030
+ dependency: transitive
1031
+ description:
1032
+ name: universal_image
1033
+ sha256: ef47a4a002158cf0b36ed3b7605af132d2476cc42703e41b8067d3603705c40d
1034
+ url: "https://pub.dev"
1035
+ source: hosted
1036
+ version: "1.0.11"
1037
+ uuid:
1038
+ dependency: transitive
1039
+ description:
1040
+ name: uuid
1041
+ sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
1042
+ url: "https://pub.dev"
1043
+ source: hosted
1044
+ version: "4.5.3"
1045
+ vector_graphics:
1046
+ dependency: transitive
1047
+ description:
1048
+ name: vector_graphics
1049
+ sha256: "7076216a10d5c390315fbe536a30f1254c341e7543e6c4c8a815e591307772b1"
1050
+ url: "https://pub.dev"
1051
+ source: hosted
1052
+ version: "1.1.20"
1053
+ vector_graphics_codec:
1054
+ dependency: transitive
1055
+ description:
1056
+ name: vector_graphics_codec
1057
+ sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
1058
+ url: "https://pub.dev"
1059
+ source: hosted
1060
+ version: "1.1.13"
1061
+ vector_graphics_compiler:
1062
+ dependency: transitive
1063
+ description:
1064
+ name: vector_graphics_compiler
1065
+ sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74"
1066
+ url: "https://pub.dev"
1067
+ source: hosted
1068
+ version: "1.2.0"
1069
+ vector_math:
1070
+ dependency: transitive
1071
+ description:
1072
+ name: vector_math
1073
+ sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
1074
+ url: "https://pub.dev"
1075
+ source: hosted
1076
+ version: "2.2.0"
1077
+ vm_service:
1078
+ dependency: transitive
1079
+ description:
1080
+ name: vm_service
1081
+ sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
1082
+ url: "https://pub.dev"
1083
+ source: hosted
1084
+ version: "15.0.2"
1085
+ watcher:
1086
+ dependency: transitive
1087
+ description:
1088
+ name: watcher
1089
+ sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
1090
+ url: "https://pub.dev"
1091
+ source: hosted
1092
+ version: "1.2.1"
1093
+ web:
1094
+ dependency: transitive
1095
+ description:
1096
+ name: web
1097
+ sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
1098
+ url: "https://pub.dev"
1099
+ source: hosted
1100
+ version: "1.1.1"
1101
+ web_socket:
1102
+ dependency: transitive
1103
+ description:
1104
+ name: web_socket
1105
+ sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
1106
+ url: "https://pub.dev"
1107
+ source: hosted
1108
+ version: "1.0.1"
1109
+ web_socket_channel:
1110
+ dependency: transitive
1111
+ description:
1112
+ name: web_socket_channel
1113
+ sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
1114
+ url: "https://pub.dev"
1115
+ source: hosted
1116
+ version: "3.0.3"
1117
+ webkit_inspection_protocol:
1118
+ dependency: transitive
1119
+ description:
1120
+ name: webkit_inspection_protocol
1121
+ sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
1122
+ url: "https://pub.dev"
1123
+ source: hosted
1124
+ version: "1.2.1"
1125
+ win32:
1126
+ dependency: transitive
1127
+ description:
1128
+ name: win32
1129
+ sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
1130
+ url: "https://pub.dev"
1131
+ source: hosted
1132
+ version: "5.15.0"
1133
+ win32_registry:
1134
+ dependency: transitive
1135
+ description:
1136
+ name: win32_registry
1137
+ sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae"
1138
+ url: "https://pub.dev"
1139
+ source: hosted
1140
+ version: "2.1.0"
1141
+ xdg_directories:
1142
+ dependency: transitive
1143
+ description:
1144
+ name: xdg_directories
1145
+ sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
1146
+ url: "https://pub.dev"
1147
+ source: hosted
1148
+ version: "1.1.0"
1149
+ xml:
1150
+ dependency: transitive
1151
+ description:
1152
+ name: xml
1153
+ sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
1154
+ url: "https://pub.dev"
1155
+ source: hosted
1156
+ version: "6.6.1"
1157
+ yaml:
1158
+ dependency: transitive
1159
+ description:
1160
+ name: yaml
1161
+ sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
1162
+ url: "https://pub.dev"
1163
+ source: hosted
1164
+ version: "3.1.3"
1165
+ yaml_writer:
1166
+ dependency: transitive
1167
+ description:
1168
+ name: yaml_writer
1169
+ sha256: "69651cd7238411179ac32079937d4aa9a2970150d6b2ae2c6fe6de09402a5dc5"
1170
+ url: "https://pub.dev"
1171
+ source: hosted
1172
+ version: "2.1.0"
1173
+sdks:
1174
+ dart: ">=3.11.0 <4.0.0"
1175
+ flutter: ">=3.38.4"
pubspec.yaml
new
+104
@@ -0,0 +1,104 @@
1
+name: localmind
2
+description: "A new Flutter project."
3
+# The following line prevents the package from being accidentally published to
4
+# pub.dev using `flutter pub publish`. This is preferred for private packages.
5
+publish_to: 'none' # Remove this line if you wish to publish to pub.dev
6
+
7
+# The following defines the version and build number for your application.
8
+# A version number is three numbers separated by dots, like 1.2.43
9
+# followed by an optional build number separated by a +.
10
+# Both the version and the builder number may be overridden in flutter
11
+# build by specifying --build-name and --build-number, respectively.
12
+# In Android, build-name is used as versionName while build-number used as versionCode.
13
+# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
14
+# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
15
+# Read more about iOS versioning at
16
+# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
17
+# In Windows, build-name is used as the major, minor, and patch parts
18
+# of the product and file versions while build-number is used as the build suffix.
19
+version: 1.0.0+1
20
+
21
+environment:
22
+ sdk: ^3.11.0
23
+
24
+# Dependencies specify other packages that your package needs in order to work.
25
+# To automatically upgrade your package dependencies to the latest versions
26
+# consider running `flutter pub upgrade --major-versions`. Alternatively,
27
+# dependencies can be manually updated by changing the version numbers below to
28
+# the latest version available on pub.dev. To see which dependencies have newer
29
+# versions available, run `flutter pub outdated`.
30
+dependencies:
31
+ flutter:
32
+ sdk: flutter
33
+
34
+ # The following adds the Cupertino Icons font to your application.
35
+ # Use with the CupertinoIcons class for iOS style icons.
36
+ cupertino_icons: ^1.0.8
37
+ flutter_riverpod: ^3.3.1
38
+ go_router: ^14.8.1
39
+ hive_ce: ^2.19.3
40
+ hive_ce_flutter: ^2.3.4
41
+ shadcn_ui: ^0.53.1
42
+ dio: ^5.9.2
43
+ flutter_markdown_plus: ^1.0.7
44
+ syntax_highlight: ^0.5.0
45
+ flutter_tts: ^4.2.5
46
+ file_picker: ^10.3.10
47
+ path_provider: ^2.1.5
48
+ haptic_feedback: ^0.6.4+3
49
+ hugeicons: ^1.1.5
50
+
51
+dev_dependencies:
52
+ flutter_test:
53
+ sdk: flutter
54
+
55
+ # The "flutter_lints" package below contains a set of recommended lints to
56
+ # encourage good coding practices. The lint set provided by the package is
57
+ # activated in the `analysis_options.yaml` file located at the root of your
58
+ # package. See that file for information about deactivating specific lint
59
+ # rules and activating additional ones.
60
+ flutter_lints: ^6.0.0
61
+ build_runner: ^2.13.1
62
+ hive_ce_generator: ^1.11.1
63
+
64
+# For information on the generic Dart part of this file, see the
65
+# following page: https://dart.dev/tools/pub/pubspec
66
+
67
+# The following section is specific to Flutter packages.
68
+flutter:
69
+
70
+ # The following line ensures that the Material Icons font is
71
+ # included with your application, so that you can use the icons in
72
+ # the material Icons class.
73
+ uses-material-design: true
74
+
75
+ # To add assets to your application, add an assets section, like this:
76
+ # assets:
77
+ # - images/a_dot_burr.jpeg
78
+ # - images/a_dot_ham.jpeg
79
+
80
+ # An image asset can refer to one or more resolution-specific "variants", see
81
+ # https://flutter.dev/to/resolution-aware-images
82
+
83
+ # For details regarding adding assets from package dependencies, see
84
+ # https://flutter.dev/to/asset-from-package
85
+
86
+ # To add custom fonts to your application, add a fonts section here,
87
+ # in this "flutter" section. Each entry in this list should have a
88
+ # "family" key with the font family name, and a "fonts" key with a
89
+ # list giving the asset and other descriptors for the font. For
90
+ # example:
91
+ # fonts:
92
+ # - family: Schyler
93
+ # fonts:
94
+ # - asset: fonts/Schyler-Regular.ttf
95
+ # - asset: fonts/Schyler-Italic.ttf
96
+ # style: italic
97
+ # - family: Trajan Pro
98
+ # fonts:
99
+ # - asset: fonts/TrajanPro.ttf
100
+ # - asset: fonts/TrajanPro_Bold.ttf
101
+ # weight: 700
102
+ #
103
+ # For details regarding fonts from package dependencies,
104
+ # see https://flutter.dev/to/font-from-package