@rashidee/co2 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/dist/index.js +25449 -0
  2. package/drizzle/0000_remarkable_charles_xavier.sql +210 -0
  3. package/drizzle/0001_wise_wolf_cub.sql +13 -0
  4. package/drizzle/0002_secret_spyke.sql +16 -0
  5. package/drizzle/0003_workspace_config_singleton.sql +16 -0
  6. package/drizzle/0004_terminal_claim_v120.sql +11 -0
  7. package/drizzle/0005_brainstorm_v124.sql +10 -0
  8. package/drizzle/0006_project_v140.sql +6 -0
  9. package/drizzle/0007_prd_wizard_v124.sql +14 -0
  10. package/drizzle/0008_workflow_artifact_v126.sql +24 -0
  11. package/drizzle/0009_project_import_v150.sql +7 -0
  12. package/drizzle/0010_quality_v151.sql +3 -0
  13. package/drizzle/meta/0000_snapshot.json +1335 -0
  14. package/drizzle/meta/0001_snapshot.json +1416 -0
  15. package/drizzle/meta/0002_snapshot.json +1510 -0
  16. package/drizzle/meta/_journal.json +83 -0
  17. package/package.json +41 -0
  18. package/plugin/.claude-plugin/marketplace.json +36 -0
  19. package/plugin/.claude-plugin/plugin.json +22 -0
  20. package/plugin/README.md +625 -0
  21. package/plugin/SKILLS.md +350 -0
  22. package/plugin/co2.svg +5 -0
  23. package/plugin/commands/brainstorm-loop.md +129 -0
  24. package/plugin/skills/conductor-defect/SKILL.md +898 -0
  25. package/plugin/skills/conductor-feature-develop/SKILL.md +1383 -0
  26. package/plugin/skills/conductor-feature-develop/references/implementation-domain-template.md +131 -0
  27. package/plugin/skills/conductor-feature-develop/references/implementation-master-template.md +89 -0
  28. package/plugin/skills/conductor-feature-develop/references/playwright-setup.md +224 -0
  29. package/plugin/skills/conductor-feature-prepare/SKILL.md +604 -0
  30. package/plugin/skills/depgen-k8s/SKILL.md +468 -0
  31. package/plugin/skills/depgen-k8s/references/dockerfile-laravel.md +197 -0
  32. package/plugin/skills/depgen-k8s/references/dockerfile-nodejs.md +163 -0
  33. package/plugin/skills/depgen-k8s/references/dockerfile-spring-boot.md +174 -0
  34. package/plugin/skills/depgen-k8s/references/k8s-patterns.md +869 -0
  35. package/plugin/skills/mockgen-shadcn/SKILL.md +1067 -0
  36. package/plugin/skills/mockgen-shadcn/references/admin-layout-template.md +1467 -0
  37. package/plugin/skills/mockgen-shadcn/references/mockup-hub-template.md +498 -0
  38. package/plugin/skills/mockgen-shadcn/references/mockup-index-template.md +216 -0
  39. package/plugin/skills/mockgen-tailwind/SKILL.md +904 -0
  40. package/plugin/skills/mockgen-tailwind/references/admin-layout-template.md +720 -0
  41. package/plugin/skills/mockgen-tailwind/references/mockup-hub-template.md +498 -0
  42. package/plugin/skills/mockgen-tailwind/references/mockup-index-template.md +190 -0
  43. package/plugin/skills/modelgen-nosql/SKILL.md +617 -0
  44. package/plugin/skills/modelgen-nosql/references/model-extraction-methodology.md +497 -0
  45. package/plugin/skills/modelgen-nosql/references/nosql-design-guide.md +258 -0
  46. package/plugin/skills/modelgen-relational/SKILL.md +707 -0
  47. package/plugin/skills/modelgen-relational/references/model-extraction-methodology.md +499 -0
  48. package/plugin/skills/specgen-flutter-riverpod/SKILL.md +898 -0
  49. package/plugin/skills/specgen-flutter-riverpod/references/component-patterns.md +971 -0
  50. package/plugin/skills/specgen-flutter-riverpod/references/notification-patterns.md +457 -0
  51. package/plugin/skills/specgen-flutter-riverpod/references/routing-patterns.md +362 -0
  52. package/plugin/skills/specgen-flutter-riverpod/references/security-patterns.md +709 -0
  53. package/plugin/skills/specgen-flutter-riverpod/references/spec-template.md +1566 -0
  54. package/plugin/skills/specgen-flutter-riverpod/references/state-patterns.md +518 -0
  55. package/plugin/skills/specgen-flutter-riverpod/references/storage-patterns.md +370 -0
  56. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/SKILL.md +984 -0
  57. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/batch-patterns.md +359 -0
  58. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/messaging-patterns.md +468 -0
  59. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/modulith-patterns.md +461 -0
  60. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/reporting-patterns.md +803 -0
  61. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/security-patterns.md +501 -0
  62. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/spec-template.md +1268 -0
  63. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/web-patterns.md +301 -0
  64. package/plugin/skills/specgen-node-cli-web/SKILL.md +561 -0
  65. package/plugin/skills/specgen-node-cli-web/references/auth-patterns.md +293 -0
  66. package/plugin/skills/specgen-node-cli-web/references/cli-packaging-patterns.md +250 -0
  67. package/plugin/skills/specgen-node-cli-web/references/server-patterns.md +247 -0
  68. package/plugin/skills/specgen-node-cli-web/references/spec-template.md +401 -0
  69. package/plugin/skills/specgen-node-cli-web/references/testing-patterns.md +174 -0
  70. package/plugin/skills/specgen-react-mui/SKILL.md +789 -0
  71. package/plugin/skills/specgen-react-mui/references/component-patterns.md +1077 -0
  72. package/plugin/skills/specgen-react-mui/references/reporting-patterns.md +1998 -0
  73. package/plugin/skills/specgen-react-mui/references/routing-patterns.md +461 -0
  74. package/plugin/skills/specgen-react-mui/references/security-patterns.md +599 -0
  75. package/plugin/skills/specgen-react-mui/references/spec-template.md +1030 -0
  76. package/plugin/skills/specgen-react-mui/references/state-patterns.md +327 -0
  77. package/plugin/skills/specgen-react-tailwind/SKILL.md +859 -0
  78. package/plugin/skills/specgen-react-tailwind/references/component-patterns.md +1212 -0
  79. package/plugin/skills/specgen-react-tailwind/references/reporting-patterns.md +1529 -0
  80. package/plugin/skills/specgen-react-tailwind/references/routing-patterns.md +669 -0
  81. package/plugin/skills/specgen-react-tailwind/references/security-patterns.md +656 -0
  82. package/plugin/skills/specgen-react-tailwind/references/spec-template.md +1237 -0
  83. package/plugin/skills/specgen-react-tailwind/references/state-patterns.md +327 -0
  84. package/plugin/skills/specgen-sdk-java/SKILL.md +933 -0
  85. package/plugin/skills/specgen-sdk-java/references/api-extraction-patterns.md +247 -0
  86. package/plugin/skills/specgen-sdk-java/references/http-client-patterns.md +495 -0
  87. package/plugin/skills/specgen-sdk-java/references/packaging-patterns.md +366 -0
  88. package/plugin/skills/specgen-sdk-java/references/spec-template.md +956 -0
  89. package/plugin/skills/specgen-spring-jpa-jtehtmx/SKILL.md +1284 -0
  90. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/batch-patterns.md +1224 -0
  91. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/jasper-patterns.md +1638 -0
  92. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/messaging-patterns.md +512 -0
  93. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/modulith-patterns.md +354 -0
  94. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/security-patterns.md +551 -0
  95. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/spec-template.md +2502 -0
  96. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/web-patterns.md +503 -0
  97. package/plugin/skills/specgen-spring-jpa-restapi/SKILL.md +990 -0
  98. package/plugin/skills/specgen-spring-jpa-restapi/references/batch-patterns.md +1224 -0
  99. package/plugin/skills/specgen-spring-jpa-restapi/references/messaging-patterns.md +512 -0
  100. package/plugin/skills/specgen-spring-jpa-restapi/references/modulith-patterns.md +279 -0
  101. package/plugin/skills/specgen-spring-jpa-restapi/references/reporting-patterns.md +1536 -0
  102. package/plugin/skills/specgen-spring-jpa-restapi/references/restapi-patterns.md +722 -0
  103. package/plugin/skills/specgen-spring-jpa-restapi/references/security-patterns.md +351 -0
  104. package/plugin/skills/specgen-spring-jpa-restapi/references/spec-template.md +553 -0
  105. package/plugin/skills/specgen-ts-cli/SKILL.md +861 -0
  106. package/plugin/skills/specgen-ts-cli/references/async-patterns.md +1101 -0
  107. package/plugin/skills/specgen-ts-cli/references/command-patterns.md +383 -0
  108. package/plugin/skills/specgen-ts-cli/references/config-patterns.md +430 -0
  109. package/plugin/skills/specgen-ts-cli/references/database-patterns.md +750 -0
  110. package/plugin/skills/specgen-ts-cli/references/packaging-patterns.md +426 -0
  111. package/plugin/skills/specgen-ts-cli/references/spec-template.md +1391 -0
  112. package/plugin/skills/specgen-ts-cli/references/testing-patterns.md +909 -0
  113. package/plugin/skills/testgen-functional/SKILL.md +1381 -0
  114. package/plugin/skills/tracegen-matrix/SKILL.md +243 -0
  115. package/plugin/skills/util-preparek8senv/SKILL.md +422 -0
  116. package/plugin/skills/util-projectinit/SKILL.md +307 -0
  117. package/plugin/skills/util-projectinit/reference/brainstorm-loop.md +129 -0
  118. package/plugin/skills/util-projectinit/reference/brainstorm-protocol.md +144 -0
  119. package/plugin/skills/util-projectinit/reference/brainstorm-setup-guide.md +153 -0
  120. package/plugin/skills/util-projectinit/reference/brainstorm-template-appdev.md +87 -0
  121. package/plugin/skills/util-projectinit/reference/co2-context-generation-guide.md +761 -0
  122. package/plugin/skills/util-projectsync/SKILL.md +501 -0
  123. package/plugin/skills/util-updprd/SKILL.md +324 -0
  124. package/plugin/skills/util-usanalyzer/SKILL.md +307 -0
  125. package/plugin/skills/util-ustagger/SKILL.md +154 -0
  126. package/static/assets/abnfDiagram-VRR7QNED-DvPuwyCV.js +1 -0
  127. package/static/assets/arc-DRKw7sjs.js +1 -0
  128. package/static/assets/architectureDiagram-ZJ3FMSHR-CaNcxUbC.js +36 -0
  129. package/static/assets/blockDiagram-677ZJIJ3-Demd4AOK.js +132 -0
  130. package/static/assets/c4Diagram-LMCZKHZV-PVKUfc2x.js +10 -0
  131. package/static/assets/channel-C2nVbuwv.js +1 -0
  132. package/static/assets/chunk-2Q5K7J3B-CExf8ww_.js +1 -0
  133. package/static/assets/chunk-32BRIVSS-CFPaoONF.js +1 -0
  134. package/static/assets/chunk-5VM5RSS4-DE2esM67.js +15 -0
  135. package/static/assets/chunk-EX3LRPZG-SON_YdDG.js +231 -0
  136. package/static/assets/chunk-JWPE2WC7-Qxah8LNr.js +1 -0
  137. package/static/assets/chunk-MOJQB5TN-BcWGQV3j.js +88 -0
  138. package/static/assets/chunk-RYQCIY6F-DkDj-OR5.js +1 -0
  139. package/static/assets/chunk-V7JOEXUC-BlI1Mrah.js +206 -0
  140. package/static/assets/chunk-VR4S4FIN-Agf__duj.js +1 -0
  141. package/static/assets/chunk-XXDRQBXY-B5CfAh85.js +1 -0
  142. package/static/assets/classDiagram-OUVF2IWQ-DXJIljsR.js +1 -0
  143. package/static/assets/classDiagram-v2-EOCWNBFH-DXJIljsR.js +1 -0
  144. package/static/assets/cose-bilkent-JH36ORCC-DO0cQkcy.js +1 -0
  145. package/static/assets/cynefin-VYW2F7L2-CfOnsnXc.js +178 -0
  146. package/static/assets/cynefinDiagram-TSTJHNR4-BJ8tULAv.js +62 -0
  147. package/static/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
  148. package/static/assets/dagre-VKFMJZFB-DfjwzHqP.js +4 -0
  149. package/static/assets/defaultLocale-DX6XiGOO.js +1 -0
  150. package/static/assets/diagram-FQU43EPY-CndSOIIM.js +3 -0
  151. package/static/assets/diagram-G47NLZAW-Czf5hN0Z.js +24 -0
  152. package/static/assets/diagram-NH7WQ7WH-COAyojGh.js +24 -0
  153. package/static/assets/diagram-OA4YK3LP-H8PoiN2m.js +30 -0
  154. package/static/assets/diagram-WEI45ONY-DzcaVzjp.js +41 -0
  155. package/static/assets/ebnfDiagram-CCIWWBDH-DPazGYLJ.js +1 -0
  156. package/static/assets/erDiagram-Q63AITRT-Bo-fubgF.js +85 -0
  157. package/static/assets/flowDiagram-23GEKE2U-CSvxUDwh.js +156 -0
  158. package/static/assets/ganttDiagram-NO4QXBWP-DnBveWX6.js +292 -0
  159. package/static/assets/gitGraphDiagram-IHSO6WYX-B3rVL8c-.js +106 -0
  160. package/static/assets/graph-C9eacEi8.js +1 -0
  161. package/static/assets/index-LZSQkLE2.css +41 -0
  162. package/static/assets/index-y7XDSgMG.js +495 -0
  163. package/static/assets/infoDiagram-FWYZ7A6U-CvWV9yy2.js +2 -0
  164. package/static/assets/init-Gi6I4Gst.js +1 -0
  165. package/static/assets/ishikawaDiagram-FXEZZL3T-DF28a1N6.js +70 -0
  166. package/static/assets/journeyDiagram-5HDEW3XC-CB6CugYk.js +139 -0
  167. package/static/assets/kanban-definition-HUTT4EX6-ResSLF6d.js +89 -0
  168. package/static/assets/katex-C5jXJg4s.js +257 -0
  169. package/static/assets/layout-DEXfKzaS.js +1 -0
  170. package/static/assets/linear-CLO--ryk.js +1 -0
  171. package/static/assets/map-Czzmt4hB.js +1 -0
  172. package/static/assets/mindmap-definition-LN4V7U3C-BvW3EbxX.js +96 -0
  173. package/static/assets/ordinal-Cboi1Yqb.js +1 -0
  174. package/static/assets/pegDiagram-2B236MQR-BHCgqrrM.js +1 -0
  175. package/static/assets/pieDiagram-ENE6RG2P-DVmCQ5FA.js +39 -0
  176. package/static/assets/quadrantDiagram-ABIIQ3AL-B3e5KR52.js +7 -0
  177. package/static/assets/railroadDiagram-RFXS5EU6-Csv5ysLc.js +1 -0
  178. package/static/assets/requirementDiagram-TGXJPOKE-DOMVzvnB.js +84 -0
  179. package/static/assets/sankeyDiagram-HTMAVEWB-B1dcTOvy.js +40 -0
  180. package/static/assets/sequenceDiagram-DBY2YBRQ-BY414yeO.js +162 -0
  181. package/static/assets/sizeCapture-X5ZJPWSS-CuJfrfBN.js +1 -0
  182. package/static/assets/stateDiagram-2N3HPSRC-FX4rJkzV.js +1 -0
  183. package/static/assets/stateDiagram-v2-6OUMAXLB-BVufbO_H.js +1 -0
  184. package/static/assets/swimlanes-5IMT3BWC-Sfy58mmv.js +2 -0
  185. package/static/assets/swimlanesDiagram-G3AALYLV-wDCDw08H.js +8 -0
  186. package/static/assets/timeline-definition-FHXFAJF6-CmV7by8p.js +120 -0
  187. package/static/assets/vennDiagram-L72KCM5P-FqIyFZFt.js +34 -0
  188. package/static/assets/wardleyDiagram-EHGQE667-BZ06O45i.js +78 -0
  189. package/static/assets/xychartDiagram-FW5EYKEG-BOkffR06.js +7 -0
  190. package/static/index.html +24 -0
@@ -0,0 +1,971 @@
1
+ # Component Patterns — ThemeData, Shared Widgets, Forms
2
+
3
+ This reference describes the reusable widget and theme patterns for the spec.
4
+ Include this content in Sections 5, 11, 12, and 14 of the generated specification.
5
+
6
+ ---
7
+
8
+ ## Material 3 Theme Configuration
9
+
10
+ ### Color Tokens
11
+
12
+ ```dart
13
+ // lib/core/theme/app_colors.dart
14
+ import 'package:flutter/material.dart';
15
+
16
+ /// Design tokens extracted from the application mockup.
17
+ /// Replace hex values with those found in MOCKUP.html CSS / inline styles.
18
+ class AppColors {
19
+ AppColors._();
20
+
21
+ static const primary = Color(0xFF{{PRIMARY_COLOR_HEX_NO_HASH}}); // e.g., 0xFF2271B1
22
+ static const primaryDark = Color(0xFF{{PRIMARY_DARK_HEX_NO_HASH}});
23
+ static const secondary = Color(0xFF{{SECONDARY_COLOR_HEX_NO_HASH}});
24
+
25
+ static const error = Color(0xFFD63638);
26
+ static const warning = Color(0xFFDBA617);
27
+ static const success = Color(0xFF00A32A);
28
+ static const info = Color(0xFF2196F3);
29
+
30
+ static const backgroundLight = Color(0xFFF0F0F1);
31
+ static const surfaceLight = Color(0xFFFFFFFF);
32
+ static const textPrimaryLight = Color(0xFF1E1E1E);
33
+ static const textSecondaryLight = Color(0xFF646970);
34
+ static const borderLight = Color(0xFFC3C4C7);
35
+
36
+ static const backgroundDark = Color(0xFF121212);
37
+ static const surfaceDark = Color(0xFF1E1E1E);
38
+ static const textPrimaryDark = Color(0xFFE0E0E0);
39
+ static const textSecondaryDark = Color(0xFFA0A0A0);
40
+ static const borderDark = Color(0xFF3A3A3A);
41
+ }
42
+ ```
43
+
44
+ ### Typography Tokens
45
+
46
+ ```dart
47
+ // lib/core/theme/app_typography.dart
48
+ import 'package:flutter/material.dart';
49
+
50
+ class AppTypography {
51
+ AppTypography._();
52
+
53
+ /// Replace with font family from MOCKUP.html (added to pubspec.yaml fonts:)
54
+ static const String fontFamily = '{{PRIMARY_FONT_FAMILY}}';
55
+
56
+ static const TextTheme textTheme = TextTheme(
57
+ displayLarge: TextStyle(fontFamily: fontFamily, fontSize: 32, fontWeight: FontWeight.w700),
58
+ displayMedium: TextStyle(fontFamily: fontFamily, fontSize: 28, fontWeight: FontWeight.w700),
59
+ displaySmall: TextStyle(fontFamily: fontFamily, fontSize: 24, fontWeight: FontWeight.w600),
60
+ headlineLarge: TextStyle(fontFamily: fontFamily, fontSize: 22, fontWeight: FontWeight.w600),
61
+ headlineMedium: TextStyle(fontFamily: fontFamily, fontSize: 20, fontWeight: FontWeight.w600),
62
+ headlineSmall: TextStyle(fontFamily: fontFamily, fontSize: 18, fontWeight: FontWeight.w600),
63
+ titleLarge: TextStyle(fontFamily: fontFamily, fontSize: 16, fontWeight: FontWeight.w600),
64
+ titleMedium: TextStyle(fontFamily: fontFamily, fontSize: 14, fontWeight: FontWeight.w600),
65
+ titleSmall: TextStyle(fontFamily: fontFamily, fontSize: 12, fontWeight: FontWeight.w600),
66
+ bodyLarge: TextStyle(fontFamily: fontFamily, fontSize: 16, fontWeight: FontWeight.w400),
67
+ bodyMedium: TextStyle(fontFamily: fontFamily, fontSize: 14, fontWeight: FontWeight.w400),
68
+ bodySmall: TextStyle(fontFamily: fontFamily, fontSize: 12, fontWeight: FontWeight.w400),
69
+ labelLarge: TextStyle(fontFamily: fontFamily, fontSize: 14, fontWeight: FontWeight.w500),
70
+ labelMedium: TextStyle(fontFamily: fontFamily, fontSize: 12, fontWeight: FontWeight.w500),
71
+ labelSmall: TextStyle(fontFamily: fontFamily, fontSize: 11, fontWeight: FontWeight.w500),
72
+ );
73
+ }
74
+ ```
75
+
76
+ ### ThemeData
77
+
78
+ ```dart
79
+ // lib/core/theme/app_theme.dart
80
+ import 'package:flutter/material.dart';
81
+
82
+ import 'app_colors.dart';
83
+ import 'app_typography.dart';
84
+
85
+ class AppTheme {
86
+ AppTheme._();
87
+
88
+ static const double _radius = {{BORDER_RADIUS_BASE}};
89
+
90
+ static ThemeData light() {
91
+ final scheme = ColorScheme.fromSeed(
92
+ seedColor: AppColors.primary,
93
+ brightness: Brightness.light,
94
+ error: AppColors.error,
95
+ surface: AppColors.surfaceLight,
96
+ );
97
+ return _buildTheme(scheme);
98
+ }
99
+
100
+ static ThemeData dark() {
101
+ final scheme = ColorScheme.fromSeed(
102
+ seedColor: AppColors.primary,
103
+ brightness: Brightness.dark,
104
+ error: AppColors.error,
105
+ surface: AppColors.surfaceDark,
106
+ );
107
+ return _buildTheme(scheme);
108
+ }
109
+
110
+ static ThemeData _buildTheme(ColorScheme scheme) {
111
+ return ThemeData(
112
+ useMaterial3: true,
113
+ colorScheme: scheme,
114
+ brightness: scheme.brightness,
115
+ fontFamily: AppTypography.fontFamily,
116
+ textTheme: AppTypography.textTheme.apply(
117
+ bodyColor: scheme.onSurface,
118
+ displayColor: scheme.onSurface,
119
+ ),
120
+ scaffoldBackgroundColor: scheme.surface,
121
+ appBarTheme: AppBarTheme(
122
+ backgroundColor: scheme.surface,
123
+ foregroundColor: scheme.onSurface,
124
+ elevation: 0,
125
+ scrolledUnderElevation: 1,
126
+ centerTitle: false,
127
+ titleTextStyle: AppTypography.textTheme.titleLarge?.copyWith(
128
+ color: scheme.onSurface,
129
+ ),
130
+ ),
131
+ cardTheme: CardTheme(
132
+ elevation: 1,
133
+ shape: RoundedRectangleBorder(
134
+ borderRadius: BorderRadius.circular(_radius * 2),
135
+ ),
136
+ margin: EdgeInsets.zero,
137
+ ),
138
+ elevatedButtonTheme: ElevatedButtonThemeData(
139
+ style: ElevatedButton.styleFrom(
140
+ backgroundColor: scheme.primary,
141
+ foregroundColor: scheme.onPrimary,
142
+ padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
143
+ shape: RoundedRectangleBorder(
144
+ borderRadius: BorderRadius.circular(_radius),
145
+ ),
146
+ textStyle: AppTypography.textTheme.labelLarge,
147
+ ),
148
+ ),
149
+ filledButtonTheme: FilledButtonThemeData(
150
+ style: FilledButton.styleFrom(
151
+ padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
152
+ shape: RoundedRectangleBorder(
153
+ borderRadius: BorderRadius.circular(_radius),
154
+ ),
155
+ ),
156
+ ),
157
+ outlinedButtonTheme: OutlinedButtonThemeData(
158
+ style: OutlinedButton.styleFrom(
159
+ padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
160
+ shape: RoundedRectangleBorder(
161
+ borderRadius: BorderRadius.circular(_radius),
162
+ ),
163
+ ),
164
+ ),
165
+ inputDecorationTheme: InputDecorationTheme(
166
+ filled: true,
167
+ fillColor: scheme.surfaceContainerHighest,
168
+ contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
169
+ border: OutlineInputBorder(
170
+ borderRadius: BorderRadius.circular(_radius),
171
+ borderSide: BorderSide.none,
172
+ ),
173
+ enabledBorder: OutlineInputBorder(
174
+ borderRadius: BorderRadius.circular(_radius),
175
+ borderSide: BorderSide.none,
176
+ ),
177
+ focusedBorder: OutlineInputBorder(
178
+ borderRadius: BorderRadius.circular(_radius),
179
+ borderSide: BorderSide(color: scheme.primary, width: 1.5),
180
+ ),
181
+ errorBorder: OutlineInputBorder(
182
+ borderRadius: BorderRadius.circular(_radius),
183
+ borderSide: BorderSide(color: scheme.error, width: 1.5),
184
+ ),
185
+ ),
186
+ chipTheme: ChipThemeData(
187
+ shape: RoundedRectangleBorder(
188
+ borderRadius: BorderRadius.circular(_radius * 2),
189
+ ),
190
+ ),
191
+ floatingActionButtonTheme: FloatingActionButtonThemeData(
192
+ shape: RoundedRectangleBorder(
193
+ borderRadius: BorderRadius.circular(_radius * 2),
194
+ ),
195
+ ),
196
+ navigationBarTheme: NavigationBarThemeData(
197
+ indicatorColor: scheme.primaryContainer,
198
+ labelTextStyle: WidgetStatePropertyAll(
199
+ AppTypography.textTheme.labelSmall,
200
+ ),
201
+ ),
202
+ dividerTheme: DividerThemeData(
203
+ color: scheme.outlineVariant,
204
+ thickness: 1,
205
+ space: 1,
206
+ ),
207
+ );
208
+ }
209
+ }
210
+ ```
211
+
212
+ ---
213
+
214
+ ## Shared Widgets
215
+
216
+ ### AppScaffold
217
+
218
+ ```dart
219
+ // lib/shared/widgets/app_scaffold.dart
220
+ import 'package:flutter/material.dart';
221
+
222
+ class AppScaffold extends StatelessWidget {
223
+ const AppScaffold({
224
+ super.key,
225
+ required this.title,
226
+ required this.body,
227
+ this.actions,
228
+ this.floatingActionButton,
229
+ this.leading,
230
+ this.bottom,
231
+ });
232
+
233
+ final String title;
234
+ final Widget body;
235
+ final List<Widget>? actions;
236
+ final Widget? floatingActionButton;
237
+ final Widget? leading;
238
+ final PreferredSizeWidget? bottom;
239
+
240
+ @override
241
+ Widget build(BuildContext context) {
242
+ return Scaffold(
243
+ appBar: AppBar(
244
+ title: Text(title),
245
+ leading: leading,
246
+ actions: actions,
247
+ bottom: bottom,
248
+ ),
249
+ body: body,
250
+ floatingActionButton: floatingActionButton,
251
+ );
252
+ }
253
+ }
254
+ ```
255
+
256
+ ### LoadingIndicator
257
+
258
+ ```dart
259
+ // lib/shared/widgets/loading_indicator.dart
260
+ import 'package:flutter/material.dart';
261
+
262
+ class LoadingIndicator extends StatelessWidget {
263
+ const LoadingIndicator({super.key, this.message});
264
+ final String? message;
265
+
266
+ @override
267
+ Widget build(BuildContext context) {
268
+ return Center(
269
+ child: Column(
270
+ mainAxisSize: MainAxisSize.min,
271
+ children: [
272
+ const CircularProgressIndicator(),
273
+ if (message != null) ...[
274
+ const SizedBox(height: 16),
275
+ Text(message!, style: Theme.of(context).textTheme.bodyMedium),
276
+ ],
277
+ ],
278
+ ),
279
+ );
280
+ }
281
+ }
282
+ ```
283
+
284
+ ### EmptyState
285
+
286
+ ```dart
287
+ // lib/shared/widgets/empty_state.dart
288
+ import 'package:flutter/material.dart';
289
+
290
+ class EmptyState extends StatelessWidget {
291
+ const EmptyState({
292
+ super.key,
293
+ required this.message,
294
+ this.icon = Icons.inbox_outlined,
295
+ this.action,
296
+ });
297
+
298
+ final String message;
299
+ final IconData icon;
300
+ final Widget? action;
301
+
302
+ @override
303
+ Widget build(BuildContext context) {
304
+ final colors = Theme.of(context).colorScheme;
305
+ return Center(
306
+ child: Padding(
307
+ padding: const EdgeInsets.all(24),
308
+ child: Column(
309
+ mainAxisSize: MainAxisSize.min,
310
+ children: [
311
+ Icon(icon, size: 64, color: colors.onSurfaceVariant),
312
+ const SizedBox(height: 12),
313
+ Text(
314
+ message,
315
+ style: Theme.of(context).textTheme.bodyLarge?.copyWith(
316
+ color: colors.onSurfaceVariant,
317
+ ),
318
+ textAlign: TextAlign.center,
319
+ ),
320
+ if (action != null) ...[
321
+ const SizedBox(height: 16),
322
+ action!,
323
+ ],
324
+ ],
325
+ ),
326
+ ),
327
+ );
328
+ }
329
+ }
330
+ ```
331
+
332
+ ### ConfirmDialog
333
+
334
+ ```dart
335
+ // lib/shared/widgets/confirm_dialog.dart
336
+ import 'package:flutter/material.dart';
337
+
338
+ class ConfirmDialog {
339
+ ConfirmDialog._();
340
+
341
+ static Future<bool> show(
342
+ BuildContext context, {
343
+ required String title,
344
+ required String message,
345
+ String confirmLabel = 'Confirm',
346
+ String cancelLabel = 'Cancel',
347
+ bool destructive = false,
348
+ }) async {
349
+ final result = await showDialog<bool>(
350
+ context: context,
351
+ builder: (context) {
352
+ final scheme = Theme.of(context).colorScheme;
353
+ return AlertDialog(
354
+ title: Text(title),
355
+ content: Text(message),
356
+ actions: [
357
+ TextButton(
358
+ onPressed: () => Navigator.of(context).pop(false),
359
+ child: Text(cancelLabel),
360
+ ),
361
+ FilledButton(
362
+ style: destructive
363
+ ? FilledButton.styleFrom(backgroundColor: scheme.error)
364
+ : null,
365
+ onPressed: () => Navigator.of(context).pop(true),
366
+ child: Text(confirmLabel),
367
+ ),
368
+ ],
369
+ );
370
+ },
371
+ );
372
+ return result ?? false;
373
+ }
374
+ }
375
+ ```
376
+
377
+ ### StatusChip
378
+
379
+ ```dart
380
+ // lib/shared/widgets/status_chip.dart
381
+ import 'package:flutter/material.dart';
382
+
383
+ class StatusChipConfig {
384
+ const StatusChipConfig({required this.label, required this.color});
385
+ final String label;
386
+ final Color color;
387
+ }
388
+
389
+ class StatusChip extends StatelessWidget {
390
+ const StatusChip({super.key, required this.config});
391
+ final StatusChipConfig config;
392
+
393
+ @override
394
+ Widget build(BuildContext context) {
395
+ return Chip(
396
+ label: Text(
397
+ config.label,
398
+ style: Theme.of(context).textTheme.labelSmall?.copyWith(
399
+ color: ThemeData.estimateBrightnessForColor(config.color) == Brightness.dark
400
+ ? Colors.white
401
+ : Colors.black87,
402
+ ),
403
+ ),
404
+ backgroundColor: config.color.withValues(alpha: 0.15),
405
+ side: BorderSide(color: config.color),
406
+ visualDensity: VisualDensity.compact,
407
+ materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
408
+ );
409
+ }
410
+ }
411
+ ```
412
+
413
+ ### AppNetworkImage (cached_network_image)
414
+
415
+ ```dart
416
+ // lib/shared/widgets/app_network_image.dart
417
+ import 'package:cached_network_image/cached_network_image.dart';
418
+ import 'package:flutter/material.dart';
419
+
420
+ class AppNetworkImage extends StatelessWidget {
421
+ const AppNetworkImage({
422
+ super.key,
423
+ required this.imageUrl,
424
+ this.width,
425
+ this.height,
426
+ this.fit = BoxFit.cover,
427
+ this.borderRadius,
428
+ });
429
+
430
+ final String imageUrl;
431
+ final double? width;
432
+ final double? height;
433
+ final BoxFit fit;
434
+ final BorderRadius? borderRadius;
435
+
436
+ @override
437
+ Widget build(BuildContext context) {
438
+ final image = CachedNetworkImage(
439
+ imageUrl: imageUrl,
440
+ width: width,
441
+ height: height,
442
+ fit: fit,
443
+ placeholder: (context, url) => ColoredBox(
444
+ color: Theme.of(context).colorScheme.surfaceContainerHighest,
445
+ child: const Center(
446
+ child: SizedBox(
447
+ width: 24,
448
+ height: 24,
449
+ child: CircularProgressIndicator(strokeWidth: 2),
450
+ ),
451
+ ),
452
+ ),
453
+ errorWidget: (context, url, error) => ColoredBox(
454
+ color: Theme.of(context).colorScheme.surfaceContainerHighest,
455
+ child: const Icon(Icons.broken_image_outlined),
456
+ ),
457
+ );
458
+ if (borderRadius == null) return image;
459
+ return ClipRRect(borderRadius: borderRadius!, child: image);
460
+ }
461
+ }
462
+ ```
463
+
464
+ ### CachedSvgIcon (flutter_svg)
465
+
466
+ ```dart
467
+ // lib/shared/widgets/cached_svg_icon.dart
468
+ import 'package:flutter/material.dart';
469
+ import 'package:flutter_svg/flutter_svg.dart';
470
+
471
+ class CachedSvgIcon extends StatelessWidget {
472
+ const CachedSvgIcon.asset(this.path, {super.key, this.size = 24, this.color})
473
+ : isAsset = true;
474
+ const CachedSvgIcon.network(this.path, {super.key, this.size = 24, this.color})
475
+ : isAsset = false;
476
+
477
+ final String path;
478
+ final double size;
479
+ final Color? color;
480
+ final bool isAsset;
481
+
482
+ @override
483
+ Widget build(BuildContext context) {
484
+ final ColorFilter? filter = color == null
485
+ ? null
486
+ : ColorFilter.mode(color!, BlendMode.srcIn);
487
+ if (isAsset) {
488
+ return SvgPicture.asset(path, width: size, height: size, colorFilter: filter);
489
+ }
490
+ return SvgPicture.network(path, width: size, height: size, colorFilter: filter);
491
+ }
492
+ }
493
+ ```
494
+
495
+ ### PullToRefreshList (pull_to_refresh)
496
+
497
+ ```dart
498
+ // lib/shared/widgets/pull_to_refresh_list.dart
499
+ import 'package:flutter/material.dart';
500
+ import 'package:pull_to_refresh/pull_to_refresh.dart';
501
+
502
+ class PullToRefreshList extends StatefulWidget {
503
+ const PullToRefreshList({
504
+ super.key,
505
+ required this.onRefresh,
506
+ required this.child,
507
+ this.onLoadMore,
508
+ });
509
+
510
+ final Future<void> Function() onRefresh;
511
+ final Future<void> Function()? onLoadMore;
512
+ final Widget child;
513
+
514
+ @override
515
+ State<PullToRefreshList> createState() => _PullToRefreshListState();
516
+ }
517
+
518
+ class _PullToRefreshListState extends State<PullToRefreshList> {
519
+ late final RefreshController _controller =
520
+ RefreshController(initialRefresh: false);
521
+
522
+ @override
523
+ void dispose() {
524
+ _controller.dispose();
525
+ super.dispose();
526
+ }
527
+
528
+ Future<void> _refresh() async {
529
+ try {
530
+ await widget.onRefresh();
531
+ _controller.refreshCompleted();
532
+ } catch (_) {
533
+ _controller.refreshFailed();
534
+ }
535
+ }
536
+
537
+ Future<void> _loadMore() async {
538
+ if (widget.onLoadMore == null) {
539
+ _controller.loadComplete();
540
+ return;
541
+ }
542
+ try {
543
+ await widget.onLoadMore!();
544
+ _controller.loadComplete();
545
+ } catch (_) {
546
+ _controller.loadFailed();
547
+ }
548
+ }
549
+
550
+ @override
551
+ Widget build(BuildContext context) {
552
+ return SmartRefresher(
553
+ controller: _controller,
554
+ enablePullDown: true,
555
+ enablePullUp: widget.onLoadMore != null,
556
+ onRefresh: _refresh,
557
+ onLoading: _loadMore,
558
+ header: const WaterDropHeader(),
559
+ child: widget.child,
560
+ );
561
+ }
562
+ }
563
+ ```
564
+
565
+ ---
566
+
567
+ ## Form Field Widgets
568
+
569
+ ### FormTextField
570
+
571
+ ```dart
572
+ // lib/shared/form/form_text_field.dart
573
+ import 'package:flutter/material.dart';
574
+
575
+ class FormTextField extends StatelessWidget {
576
+ const FormTextField({
577
+ super.key,
578
+ required this.controller,
579
+ required this.label,
580
+ this.hint,
581
+ this.validator,
582
+ this.keyboardType,
583
+ this.obscureText = false,
584
+ this.maxLength,
585
+ this.maxLines = 1,
586
+ this.prefixIcon,
587
+ this.suffixIcon,
588
+ this.enabled = true,
589
+ });
590
+
591
+ final TextEditingController controller;
592
+ final String label;
593
+ final String? hint;
594
+ final FormFieldValidator<String>? validator;
595
+ final TextInputType? keyboardType;
596
+ final bool obscureText;
597
+ final int? maxLength;
598
+ final int maxLines;
599
+ final Widget? prefixIcon;
600
+ final Widget? suffixIcon;
601
+ final bool enabled;
602
+
603
+ @override
604
+ Widget build(BuildContext context) {
605
+ return TextFormField(
606
+ controller: controller,
607
+ validator: validator,
608
+ keyboardType: keyboardType,
609
+ obscureText: obscureText,
610
+ maxLength: maxLength,
611
+ maxLines: obscureText ? 1 : maxLines,
612
+ enabled: enabled,
613
+ decoration: InputDecoration(
614
+ labelText: label,
615
+ hintText: hint,
616
+ prefixIcon: prefixIcon,
617
+ suffixIcon: suffixIcon,
618
+ counterText: '',
619
+ ),
620
+ );
621
+ }
622
+ }
623
+ ```
624
+
625
+ ### FormDropdownField
626
+
627
+ ```dart
628
+ // lib/shared/form/form_dropdown_field.dart
629
+ import 'package:flutter/material.dart';
630
+
631
+ class DropdownOption<T> {
632
+ const DropdownOption({required this.value, required this.label});
633
+ final T value;
634
+ final String label;
635
+ }
636
+
637
+ class FormDropdownField<T> extends StatelessWidget {
638
+ const FormDropdownField({
639
+ super.key,
640
+ required this.value,
641
+ required this.options,
642
+ required this.label,
643
+ required this.onChanged,
644
+ this.validator,
645
+ this.hint,
646
+ });
647
+
648
+ final T? value;
649
+ final List<DropdownOption<T>> options;
650
+ final String label;
651
+ final ValueChanged<T?> onChanged;
652
+ final FormFieldValidator<T>? validator;
653
+ final String? hint;
654
+
655
+ @override
656
+ Widget build(BuildContext context) {
657
+ return DropdownButtonFormField<T>(
658
+ value: value,
659
+ onChanged: onChanged,
660
+ validator: validator,
661
+ items: options
662
+ .map((o) => DropdownMenuItem<T>(value: o.value, child: Text(o.label)))
663
+ .toList(),
664
+ decoration: InputDecoration(labelText: label, hintText: hint),
665
+ );
666
+ }
667
+ }
668
+ ```
669
+
670
+ ### FormDatePickerField
671
+
672
+ ```dart
673
+ // lib/shared/form/form_date_picker_field.dart
674
+ import 'package:flutter/material.dart';
675
+ import 'package:intl/intl.dart';
676
+
677
+ class FormDatePickerField extends StatelessWidget {
678
+ const FormDatePickerField({
679
+ super.key,
680
+ required this.controller,
681
+ required this.label,
682
+ this.firstDate,
683
+ this.lastDate,
684
+ this.validator,
685
+ });
686
+
687
+ final TextEditingController controller;
688
+ final String label;
689
+ final DateTime? firstDate;
690
+ final DateTime? lastDate;
691
+ final FormFieldValidator<String>? validator;
692
+
693
+ Future<void> _pickDate(BuildContext context) async {
694
+ final now = DateTime.now();
695
+ final initial = controller.text.isEmpty
696
+ ? now
697
+ : DateTime.tryParse(controller.text) ?? now;
698
+ final picked = await showDatePicker(
699
+ context: context,
700
+ initialDate: initial,
701
+ firstDate: firstDate ?? DateTime(now.year - 50),
702
+ lastDate: lastDate ?? DateTime(now.year + 50),
703
+ );
704
+ if (picked != null) {
705
+ controller.text = DateFormat('yyyy-MM-dd').format(picked);
706
+ }
707
+ }
708
+
709
+ @override
710
+ Widget build(BuildContext context) {
711
+ return TextFormField(
712
+ controller: controller,
713
+ readOnly: true,
714
+ onTap: () => _pickDate(context),
715
+ validator: validator,
716
+ decoration: InputDecoration(
717
+ labelText: label,
718
+ suffixIcon: const Icon(Icons.calendar_today_outlined),
719
+ ),
720
+ );
721
+ }
722
+ }
723
+ ```
724
+
725
+ ### FormSwitchField
726
+
727
+ ```dart
728
+ // lib/shared/form/form_switch_field.dart
729
+ import 'package:flutter/material.dart';
730
+
731
+ class FormSwitchField extends StatelessWidget {
732
+ const FormSwitchField({
733
+ super.key,
734
+ required this.value,
735
+ required this.onChanged,
736
+ required this.label,
737
+ this.subtitle,
738
+ });
739
+
740
+ final bool value;
741
+ final ValueChanged<bool> onChanged;
742
+ final String label;
743
+ final String? subtitle;
744
+
745
+ @override
746
+ Widget build(BuildContext context) {
747
+ return SwitchListTile(
748
+ value: value,
749
+ onChanged: onChanged,
750
+ title: Text(label),
751
+ subtitle: subtitle != null ? Text(subtitle!) : null,
752
+ contentPadding: EdgeInsets.zero,
753
+ );
754
+ }
755
+ }
756
+ ```
757
+
758
+ ### FormImagePickerField (if ImagePicker = yes)
759
+
760
+ ```dart
761
+ // lib/shared/form/form_image_picker_field.dart
762
+ import 'dart:io';
763
+
764
+ import 'package:flutter/material.dart';
765
+ import 'package:image_picker/image_picker.dart';
766
+
767
+ class FormImagePickerField extends StatelessWidget {
768
+ const FormImagePickerField({
769
+ super.key,
770
+ required this.imageFile,
771
+ required this.onChanged,
772
+ required this.label,
773
+ this.helperText,
774
+ });
775
+
776
+ final File? imageFile;
777
+ final ValueChanged<File?> onChanged;
778
+ final String label;
779
+ final String? helperText;
780
+
781
+ Future<void> _pick(BuildContext context, ImageSource source) async {
782
+ final picker = ImagePicker();
783
+ final picked = await picker.pickImage(source: source, imageQuality: 80);
784
+ if (picked != null) {
785
+ onChanged(File(picked.path));
786
+ }
787
+ }
788
+
789
+ @override
790
+ Widget build(BuildContext context) {
791
+ return InputDecorator(
792
+ decoration: InputDecoration(labelText: label, helperText: helperText),
793
+ child: Column(
794
+ crossAxisAlignment: CrossAxisAlignment.stretch,
795
+ children: [
796
+ if (imageFile != null) ...[
797
+ ClipRRect(
798
+ borderRadius: BorderRadius.circular(8),
799
+ child: Image.file(imageFile!, height: 160, fit: BoxFit.cover),
800
+ ),
801
+ const SizedBox(height: 8),
802
+ ],
803
+ Row(
804
+ children: [
805
+ Expanded(
806
+ child: OutlinedButton.icon(
807
+ onPressed: () => _pick(context, ImageSource.camera),
808
+ icon: const Icon(Icons.camera_alt_outlined),
809
+ label: const Text('Camera'),
810
+ ),
811
+ ),
812
+ const SizedBox(width: 12),
813
+ Expanded(
814
+ child: OutlinedButton.icon(
815
+ onPressed: () => _pick(context, ImageSource.gallery),
816
+ icon: const Icon(Icons.photo_library_outlined),
817
+ label: const Text('Gallery'),
818
+ ),
819
+ ),
820
+ ],
821
+ ),
822
+ ],
823
+ ),
824
+ );
825
+ }
826
+ }
827
+ ```
828
+
829
+ ---
830
+
831
+ ## Form Screen Pattern
832
+
833
+ ```dart
834
+ // Example: lib/features/task/presentation/task_form_screen.dart
835
+ import 'package:flutter/material.dart';
836
+ import 'package:flutter_hooks/flutter_hooks.dart';
837
+ import 'package:hooks_riverpod/hooks_riverpod.dart';
838
+ import 'package:go_router/go_router.dart';
839
+
840
+ import '../application/task_providers.dart';
841
+ import '../domain/task.dart';
842
+ import '../../../core/utils/validators.dart';
843
+ import '../../../shared/form/form_text_field.dart';
844
+ import '../../../shared/form/form_dropdown_field.dart';
845
+ import '../../../shared/form/form_date_picker_field.dart';
846
+ import '../../../shared/widgets/error_state.dart';
847
+ import '../../../shared/widgets/loading_indicator.dart';
848
+
849
+ class TaskFormScreen extends HookConsumerWidget {
850
+ const TaskFormScreen({super.key, this.taskId});
851
+ final String? taskId;
852
+ bool get isEditMode => taskId != null;
853
+
854
+ @override
855
+ Widget build(BuildContext context, WidgetRef ref) {
856
+ final formKey = useMemoized(GlobalKey<FormState>.new);
857
+ final titleCtrl = useTextEditingController();
858
+ final descCtrl = useTextEditingController();
859
+ final dueDateCtrl = useTextEditingController();
860
+ final status = useState<TaskStatus>(TaskStatus.draft);
861
+
862
+ final formState = ref.watch(taskFormControllerProvider);
863
+
864
+ if (isEditMode) {
865
+ final taskAsync = ref.watch(taskByIdProvider(taskId!));
866
+ taskAsync.whenData((task) {
867
+ if (titleCtrl.text.isEmpty) {
868
+ titleCtrl.text = task.title;
869
+ descCtrl.text = task.description ?? '';
870
+ dueDateCtrl.text = task.dueDate.toIso8601String().split('T').first;
871
+ status.value = task.status;
872
+ }
873
+ });
874
+ if (taskAsync.isLoading) return const Scaffold(body: LoadingIndicator());
875
+ if (taskAsync.hasError) {
876
+ return Scaffold(body: ErrorState(error: taskAsync.error!));
877
+ }
878
+ }
879
+
880
+ Future<void> submit() async {
881
+ if (!formKey.currentState!.validate()) return;
882
+ final controller = ref.read(taskFormControllerProvider.notifier);
883
+ try {
884
+ if (isEditMode) {
885
+ await controller.update(
886
+ taskId!,
887
+ TaskUpdate(
888
+ title: titleCtrl.text,
889
+ description: descCtrl.text,
890
+ dueDate: DateTime.parse(dueDateCtrl.text),
891
+ status: status.value,
892
+ ),
893
+ );
894
+ } else {
895
+ await controller.create(
896
+ TaskCreate(
897
+ title: titleCtrl.text,
898
+ description: descCtrl.text,
899
+ dueDate: DateTime.parse(dueDateCtrl.text),
900
+ ),
901
+ );
902
+ }
903
+ if (context.mounted) context.pop();
904
+ } catch (_) {
905
+ // Error already surfaced via ref.listen in build()
906
+ }
907
+ }
908
+
909
+ return Scaffold(
910
+ appBar: AppBar(title: Text(isEditMode ? 'Edit Task' : 'New Task')),
911
+ body: SingleChildScrollView(
912
+ padding: const EdgeInsets.all(16),
913
+ child: Form(
914
+ key: formKey,
915
+ child: Column(
916
+ children: [
917
+ FormTextField(
918
+ controller: titleCtrl,
919
+ label: 'Title',
920
+ maxLength: 100,
921
+ validator: Validators.compose([
922
+ (v) => Validators.required(v, label: 'Title'),
923
+ (v) => Validators.maxLength(v, 100, label: 'Title'),
924
+ ]),
925
+ ),
926
+ const SizedBox(height: 16),
927
+ FormTextField(
928
+ controller: descCtrl,
929
+ label: 'Description',
930
+ maxLines: 4,
931
+ ),
932
+ const SizedBox(height: 16),
933
+ FormDatePickerField(
934
+ controller: dueDateCtrl,
935
+ label: 'Due Date',
936
+ validator: (v) => Validators.required(v, label: 'Due date'),
937
+ ),
938
+ const SizedBox(height: 16),
939
+ FormDropdownField<TaskStatus>(
940
+ value: status.value,
941
+ label: 'Status',
942
+ options: const [
943
+ DropdownOption(value: TaskStatus.draft, label: 'Draft'),
944
+ DropdownOption(value: TaskStatus.inProgress, label: 'In Progress'),
945
+ DropdownOption(value: TaskStatus.completed, label: 'Completed'),
946
+ ],
947
+ onChanged: (v) {
948
+ if (v != null) status.value = v;
949
+ },
950
+ ),
951
+ const SizedBox(height: 24),
952
+ SizedBox(
953
+ width: double.infinity,
954
+ child: FilledButton(
955
+ onPressed: formState.isLoading ? null : submit,
956
+ child: formState.isLoading
957
+ ? const SizedBox(
958
+ width: 18, height: 18,
959
+ child: CircularProgressIndicator(strokeWidth: 2),
960
+ )
961
+ : Text(isEditMode ? 'Update' : 'Create'),
962
+ ),
963
+ ),
964
+ ],
965
+ ),
966
+ ),
967
+ ),
968
+ );
969
+ }
970
+ }
971
+ ```