@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.
- package/dist/index.js +25449 -0
- package/drizzle/0000_remarkable_charles_xavier.sql +210 -0
- package/drizzle/0001_wise_wolf_cub.sql +13 -0
- package/drizzle/0002_secret_spyke.sql +16 -0
- package/drizzle/0003_workspace_config_singleton.sql +16 -0
- package/drizzle/0004_terminal_claim_v120.sql +11 -0
- package/drizzle/0005_brainstorm_v124.sql +10 -0
- package/drizzle/0006_project_v140.sql +6 -0
- package/drizzle/0007_prd_wizard_v124.sql +14 -0
- package/drizzle/0008_workflow_artifact_v126.sql +24 -0
- package/drizzle/0009_project_import_v150.sql +7 -0
- package/drizzle/0010_quality_v151.sql +3 -0
- package/drizzle/meta/0000_snapshot.json +1335 -0
- package/drizzle/meta/0001_snapshot.json +1416 -0
- package/drizzle/meta/0002_snapshot.json +1510 -0
- package/drizzle/meta/_journal.json +83 -0
- package/package.json +41 -0
- package/plugin/.claude-plugin/marketplace.json +36 -0
- package/plugin/.claude-plugin/plugin.json +22 -0
- package/plugin/README.md +625 -0
- package/plugin/SKILLS.md +350 -0
- package/plugin/co2.svg +5 -0
- package/plugin/commands/brainstorm-loop.md +129 -0
- package/plugin/skills/conductor-defect/SKILL.md +898 -0
- package/plugin/skills/conductor-feature-develop/SKILL.md +1383 -0
- package/plugin/skills/conductor-feature-develop/references/implementation-domain-template.md +131 -0
- package/plugin/skills/conductor-feature-develop/references/implementation-master-template.md +89 -0
- package/plugin/skills/conductor-feature-develop/references/playwright-setup.md +224 -0
- package/plugin/skills/conductor-feature-prepare/SKILL.md +604 -0
- package/plugin/skills/depgen-k8s/SKILL.md +468 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-laravel.md +197 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-nodejs.md +163 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-spring-boot.md +174 -0
- package/plugin/skills/depgen-k8s/references/k8s-patterns.md +869 -0
- package/plugin/skills/mockgen-shadcn/SKILL.md +1067 -0
- package/plugin/skills/mockgen-shadcn/references/admin-layout-template.md +1467 -0
- package/plugin/skills/mockgen-shadcn/references/mockup-hub-template.md +498 -0
- package/plugin/skills/mockgen-shadcn/references/mockup-index-template.md +216 -0
- package/plugin/skills/mockgen-tailwind/SKILL.md +904 -0
- package/plugin/skills/mockgen-tailwind/references/admin-layout-template.md +720 -0
- package/plugin/skills/mockgen-tailwind/references/mockup-hub-template.md +498 -0
- package/plugin/skills/mockgen-tailwind/references/mockup-index-template.md +190 -0
- package/plugin/skills/modelgen-nosql/SKILL.md +617 -0
- package/plugin/skills/modelgen-nosql/references/model-extraction-methodology.md +497 -0
- package/plugin/skills/modelgen-nosql/references/nosql-design-guide.md +258 -0
- package/plugin/skills/modelgen-relational/SKILL.md +707 -0
- package/plugin/skills/modelgen-relational/references/model-extraction-methodology.md +499 -0
- package/plugin/skills/specgen-flutter-riverpod/SKILL.md +898 -0
- package/plugin/skills/specgen-flutter-riverpod/references/component-patterns.md +971 -0
- package/plugin/skills/specgen-flutter-riverpod/references/notification-patterns.md +457 -0
- package/plugin/skills/specgen-flutter-riverpod/references/routing-patterns.md +362 -0
- package/plugin/skills/specgen-flutter-riverpod/references/security-patterns.md +709 -0
- package/plugin/skills/specgen-flutter-riverpod/references/spec-template.md +1566 -0
- package/plugin/skills/specgen-flutter-riverpod/references/state-patterns.md +518 -0
- package/plugin/skills/specgen-flutter-riverpod/references/storage-patterns.md +370 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/SKILL.md +984 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/batch-patterns.md +359 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/messaging-patterns.md +468 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/modulith-patterns.md +461 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/reporting-patterns.md +803 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/security-patterns.md +501 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/spec-template.md +1268 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/web-patterns.md +301 -0
- package/plugin/skills/specgen-node-cli-web/SKILL.md +561 -0
- package/plugin/skills/specgen-node-cli-web/references/auth-patterns.md +293 -0
- package/plugin/skills/specgen-node-cli-web/references/cli-packaging-patterns.md +250 -0
- package/plugin/skills/specgen-node-cli-web/references/server-patterns.md +247 -0
- package/plugin/skills/specgen-node-cli-web/references/spec-template.md +401 -0
- package/plugin/skills/specgen-node-cli-web/references/testing-patterns.md +174 -0
- package/plugin/skills/specgen-react-mui/SKILL.md +789 -0
- package/plugin/skills/specgen-react-mui/references/component-patterns.md +1077 -0
- package/plugin/skills/specgen-react-mui/references/reporting-patterns.md +1998 -0
- package/plugin/skills/specgen-react-mui/references/routing-patterns.md +461 -0
- package/plugin/skills/specgen-react-mui/references/security-patterns.md +599 -0
- package/plugin/skills/specgen-react-mui/references/spec-template.md +1030 -0
- package/plugin/skills/specgen-react-mui/references/state-patterns.md +327 -0
- package/plugin/skills/specgen-react-tailwind/SKILL.md +859 -0
- package/plugin/skills/specgen-react-tailwind/references/component-patterns.md +1212 -0
- package/plugin/skills/specgen-react-tailwind/references/reporting-patterns.md +1529 -0
- package/plugin/skills/specgen-react-tailwind/references/routing-patterns.md +669 -0
- package/plugin/skills/specgen-react-tailwind/references/security-patterns.md +656 -0
- package/plugin/skills/specgen-react-tailwind/references/spec-template.md +1237 -0
- package/plugin/skills/specgen-react-tailwind/references/state-patterns.md +327 -0
- package/plugin/skills/specgen-sdk-java/SKILL.md +933 -0
- package/plugin/skills/specgen-sdk-java/references/api-extraction-patterns.md +247 -0
- package/plugin/skills/specgen-sdk-java/references/http-client-patterns.md +495 -0
- package/plugin/skills/specgen-sdk-java/references/packaging-patterns.md +366 -0
- package/plugin/skills/specgen-sdk-java/references/spec-template.md +956 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/SKILL.md +1284 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/batch-patterns.md +1224 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/jasper-patterns.md +1638 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/messaging-patterns.md +512 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/modulith-patterns.md +354 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/security-patterns.md +551 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/spec-template.md +2502 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/web-patterns.md +503 -0
- package/plugin/skills/specgen-spring-jpa-restapi/SKILL.md +990 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/batch-patterns.md +1224 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/messaging-patterns.md +512 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/modulith-patterns.md +279 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/reporting-patterns.md +1536 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/restapi-patterns.md +722 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/security-patterns.md +351 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/spec-template.md +553 -0
- package/plugin/skills/specgen-ts-cli/SKILL.md +861 -0
- package/plugin/skills/specgen-ts-cli/references/async-patterns.md +1101 -0
- package/plugin/skills/specgen-ts-cli/references/command-patterns.md +383 -0
- package/plugin/skills/specgen-ts-cli/references/config-patterns.md +430 -0
- package/plugin/skills/specgen-ts-cli/references/database-patterns.md +750 -0
- package/plugin/skills/specgen-ts-cli/references/packaging-patterns.md +426 -0
- package/plugin/skills/specgen-ts-cli/references/spec-template.md +1391 -0
- package/plugin/skills/specgen-ts-cli/references/testing-patterns.md +909 -0
- package/plugin/skills/testgen-functional/SKILL.md +1381 -0
- package/plugin/skills/tracegen-matrix/SKILL.md +243 -0
- package/plugin/skills/util-preparek8senv/SKILL.md +422 -0
- package/plugin/skills/util-projectinit/SKILL.md +307 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-loop.md +129 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-protocol.md +144 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-setup-guide.md +153 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-template-appdev.md +87 -0
- package/plugin/skills/util-projectinit/reference/co2-context-generation-guide.md +761 -0
- package/plugin/skills/util-projectsync/SKILL.md +501 -0
- package/plugin/skills/util-updprd/SKILL.md +324 -0
- package/plugin/skills/util-usanalyzer/SKILL.md +307 -0
- package/plugin/skills/util-ustagger/SKILL.md +154 -0
- package/static/assets/abnfDiagram-VRR7QNED-DvPuwyCV.js +1 -0
- package/static/assets/arc-DRKw7sjs.js +1 -0
- package/static/assets/architectureDiagram-ZJ3FMSHR-CaNcxUbC.js +36 -0
- package/static/assets/blockDiagram-677ZJIJ3-Demd4AOK.js +132 -0
- package/static/assets/c4Diagram-LMCZKHZV-PVKUfc2x.js +10 -0
- package/static/assets/channel-C2nVbuwv.js +1 -0
- package/static/assets/chunk-2Q5K7J3B-CExf8ww_.js +1 -0
- package/static/assets/chunk-32BRIVSS-CFPaoONF.js +1 -0
- package/static/assets/chunk-5VM5RSS4-DE2esM67.js +15 -0
- package/static/assets/chunk-EX3LRPZG-SON_YdDG.js +231 -0
- package/static/assets/chunk-JWPE2WC7-Qxah8LNr.js +1 -0
- package/static/assets/chunk-MOJQB5TN-BcWGQV3j.js +88 -0
- package/static/assets/chunk-RYQCIY6F-DkDj-OR5.js +1 -0
- package/static/assets/chunk-V7JOEXUC-BlI1Mrah.js +206 -0
- package/static/assets/chunk-VR4S4FIN-Agf__duj.js +1 -0
- package/static/assets/chunk-XXDRQBXY-B5CfAh85.js +1 -0
- package/static/assets/classDiagram-OUVF2IWQ-DXJIljsR.js +1 -0
- package/static/assets/classDiagram-v2-EOCWNBFH-DXJIljsR.js +1 -0
- package/static/assets/cose-bilkent-JH36ORCC-DO0cQkcy.js +1 -0
- package/static/assets/cynefin-VYW2F7L2-CfOnsnXc.js +178 -0
- package/static/assets/cynefinDiagram-TSTJHNR4-BJ8tULAv.js +62 -0
- package/static/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
- package/static/assets/dagre-VKFMJZFB-DfjwzHqP.js +4 -0
- package/static/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/static/assets/diagram-FQU43EPY-CndSOIIM.js +3 -0
- package/static/assets/diagram-G47NLZAW-Czf5hN0Z.js +24 -0
- package/static/assets/diagram-NH7WQ7WH-COAyojGh.js +24 -0
- package/static/assets/diagram-OA4YK3LP-H8PoiN2m.js +30 -0
- package/static/assets/diagram-WEI45ONY-DzcaVzjp.js +41 -0
- package/static/assets/ebnfDiagram-CCIWWBDH-DPazGYLJ.js +1 -0
- package/static/assets/erDiagram-Q63AITRT-Bo-fubgF.js +85 -0
- package/static/assets/flowDiagram-23GEKE2U-CSvxUDwh.js +156 -0
- package/static/assets/ganttDiagram-NO4QXBWP-DnBveWX6.js +292 -0
- package/static/assets/gitGraphDiagram-IHSO6WYX-B3rVL8c-.js +106 -0
- package/static/assets/graph-C9eacEi8.js +1 -0
- package/static/assets/index-LZSQkLE2.css +41 -0
- package/static/assets/index-y7XDSgMG.js +495 -0
- package/static/assets/infoDiagram-FWYZ7A6U-CvWV9yy2.js +2 -0
- package/static/assets/init-Gi6I4Gst.js +1 -0
- package/static/assets/ishikawaDiagram-FXEZZL3T-DF28a1N6.js +70 -0
- package/static/assets/journeyDiagram-5HDEW3XC-CB6CugYk.js +139 -0
- package/static/assets/kanban-definition-HUTT4EX6-ResSLF6d.js +89 -0
- package/static/assets/katex-C5jXJg4s.js +257 -0
- package/static/assets/layout-DEXfKzaS.js +1 -0
- package/static/assets/linear-CLO--ryk.js +1 -0
- package/static/assets/map-Czzmt4hB.js +1 -0
- package/static/assets/mindmap-definition-LN4V7U3C-BvW3EbxX.js +96 -0
- package/static/assets/ordinal-Cboi1Yqb.js +1 -0
- package/static/assets/pegDiagram-2B236MQR-BHCgqrrM.js +1 -0
- package/static/assets/pieDiagram-ENE6RG2P-DVmCQ5FA.js +39 -0
- package/static/assets/quadrantDiagram-ABIIQ3AL-B3e5KR52.js +7 -0
- package/static/assets/railroadDiagram-RFXS5EU6-Csv5ysLc.js +1 -0
- package/static/assets/requirementDiagram-TGXJPOKE-DOMVzvnB.js +84 -0
- package/static/assets/sankeyDiagram-HTMAVEWB-B1dcTOvy.js +40 -0
- package/static/assets/sequenceDiagram-DBY2YBRQ-BY414yeO.js +162 -0
- package/static/assets/sizeCapture-X5ZJPWSS-CuJfrfBN.js +1 -0
- package/static/assets/stateDiagram-2N3HPSRC-FX4rJkzV.js +1 -0
- package/static/assets/stateDiagram-v2-6OUMAXLB-BVufbO_H.js +1 -0
- package/static/assets/swimlanes-5IMT3BWC-Sfy58mmv.js +2 -0
- package/static/assets/swimlanesDiagram-G3AALYLV-wDCDw08H.js +8 -0
- package/static/assets/timeline-definition-FHXFAJF6-CmV7by8p.js +120 -0
- package/static/assets/vennDiagram-L72KCM5P-FqIyFZFt.js +34 -0
- package/static/assets/wardleyDiagram-EHGQE667-BZ06O45i.js +78 -0
- package/static/assets/xychartDiagram-FW5EYKEG-BOkffR06.js +7 -0
- 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
|
+
```
|