getlotui 0.1.0

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 (128) hide show
  1. package/README.md +78 -0
  2. package/dist/bin.d.ts +2 -0
  3. package/dist/bin.js +5 -0
  4. package/dist/commands/add.d.ts +1 -0
  5. package/dist/commands/add.js +37 -0
  6. package/dist/commands/init.d.ts +1 -0
  7. package/dist/commands/init.js +93 -0
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.js +22 -0
  10. package/dist/templates/expo/Accordion.d.ts +14 -0
  11. package/dist/templates/expo/Accordion.js +118 -0
  12. package/dist/templates/expo/Accordion.tsx +152 -0
  13. package/dist/templates/expo/AlertDialog.d.ts +12 -0
  14. package/dist/templates/expo/AlertDialog.js +126 -0
  15. package/dist/templates/expo/AlertDialog.tsx +147 -0
  16. package/dist/templates/expo/Avatar.d.ts +8 -0
  17. package/dist/templates/expo/Avatar.js +81 -0
  18. package/dist/templates/expo/Avatar.tsx +78 -0
  19. package/dist/templates/expo/Badge.d.ts +6 -0
  20. package/dist/templates/expo/Badge.js +60 -0
  21. package/dist/templates/expo/Badge.tsx +67 -0
  22. package/dist/templates/expo/Button.d.ts +9 -0
  23. package/dist/templates/expo/Button.js +37 -0
  24. package/dist/templates/expo/Button.tsx +53 -0
  25. package/dist/templates/expo/Dropdown.d.ts +12 -0
  26. package/dist/templates/expo/Dropdown.js +91 -0
  27. package/dist/templates/expo/Dropdown.tsx +100 -0
  28. package/dist/templates/expo/Input.d.ts +11 -0
  29. package/dist/templates/expo/Input.js +43 -0
  30. package/dist/templates/expo/Input.tsx +67 -0
  31. package/dist/templates/expo/Toast.d.ts +16 -0
  32. package/dist/templates/expo/Toast.js +142 -0
  33. package/dist/templates/expo/Toast.tsx +161 -0
  34. package/dist/templates/expo/utils.d.ts +4 -0
  35. package/dist/templates/expo/utils.js +10 -0
  36. package/dist/templates/expo/utils.ts +8 -0
  37. package/dist/templates/flutter/Accordion.dart +142 -0
  38. package/dist/templates/flutter/Alert.dart +96 -0
  39. package/dist/templates/flutter/AlertDialog.dart +175 -0
  40. package/dist/templates/flutter/Avatar.dart +82 -0
  41. package/dist/templates/flutter/Badge.dart +89 -0
  42. package/dist/templates/flutter/Button.dart +116 -0
  43. package/dist/templates/flutter/Card.dart +91 -0
  44. package/dist/templates/flutter/Input.dart +73 -0
  45. package/dist/templates/flutter/Text.dart +87 -0
  46. package/dist/templates/flutter/utils.dart +13 -0
  47. package/dist/templates/templates/expo/Button.tsx +50 -0
  48. package/dist/templates/templates/expo/Input.tsx +67 -0
  49. package/dist/templates/web/Accordion.d.ts +7 -0
  50. package/dist/templates/web/Accordion.js +59 -0
  51. package/dist/templates/web/Accordion.tsx +64 -0
  52. package/dist/templates/web/Alert.d.ts +9 -0
  53. package/dist/templates/web/Alert.js +64 -0
  54. package/dist/templates/web/Alert.tsx +71 -0
  55. package/dist/templates/web/AlertDialog.d.ts +14 -0
  56. package/dist/templates/web/AlertDialog.js +85 -0
  57. package/dist/templates/web/AlertDialog.tsx +164 -0
  58. package/dist/templates/web/Avatar.d.ts +6 -0
  59. package/dist/templates/web/Avatar.js +50 -0
  60. package/dist/templates/web/Avatar.tsx +51 -0
  61. package/dist/templates/web/Badge.d.ts +9 -0
  62. package/dist/templates/web/Badge.js +59 -0
  63. package/dist/templates/web/Badge.tsx +38 -0
  64. package/dist/templates/web/Button.d.ts +10 -0
  65. package/dist/templates/web/Button.js +70 -0
  66. package/dist/templates/web/Button.tsx +60 -0
  67. package/dist/templates/web/Card.d.ts +9 -0
  68. package/dist/templates/web/Card.js +65 -0
  69. package/dist/templates/web/Card.tsx +92 -0
  70. package/dist/templates/web/Dropdown.d.ts +27 -0
  71. package/dist/templates/web/Dropdown.js +95 -0
  72. package/dist/templates/web/Dropdown.tsx +198 -0
  73. package/dist/templates/web/Input.d.ts +3 -0
  74. package/dist/templates/web/Input.js +41 -0
  75. package/dist/templates/web/Input.tsx +21 -0
  76. package/dist/templates/web/Tabs.d.ts +7 -0
  77. package/dist/templates/web/Tabs.js +55 -0
  78. package/dist/templates/web/Tabs.tsx +66 -0
  79. package/dist/templates/web/Toast.d.ts +15 -0
  80. package/dist/templates/web/Toast.js +75 -0
  81. package/dist/templates/web/Toast.tsx +126 -0
  82. package/dist/templates/web/utils.d.ts +2 -0
  83. package/dist/templates/web/utils.js +8 -0
  84. package/dist/templates/web/utils.ts +6 -0
  85. package/dist/utils/detect.d.ts +19 -0
  86. package/dist/utils/detect.js +90 -0
  87. package/dist/utils/fs.d.ts +5 -0
  88. package/dist/utils/fs.js +35 -0
  89. package/getlotui.config.json +4 -0
  90. package/package.json +31 -0
  91. package/src/bin.ts +5 -0
  92. package/src/commands/add.ts +50 -0
  93. package/src/commands/init.ts +108 -0
  94. package/src/index.ts +23 -0
  95. package/src/templates/expo/Accordion.tsx +152 -0
  96. package/src/templates/expo/AlertDialog.tsx +147 -0
  97. package/src/templates/expo/Avatar.tsx +78 -0
  98. package/src/templates/expo/Badge.tsx +67 -0
  99. package/src/templates/expo/Button.tsx +53 -0
  100. package/src/templates/expo/Dropdown.tsx +100 -0
  101. package/src/templates/expo/Input.tsx +67 -0
  102. package/src/templates/expo/Toast.tsx +161 -0
  103. package/src/templates/expo/utils.ts +8 -0
  104. package/src/templates/flutter/Accordion.dart +142 -0
  105. package/src/templates/flutter/Alert.dart +96 -0
  106. package/src/templates/flutter/AlertDialog.dart +175 -0
  107. package/src/templates/flutter/Avatar.dart +82 -0
  108. package/src/templates/flutter/Badge.dart +89 -0
  109. package/src/templates/flutter/Button.dart +116 -0
  110. package/src/templates/flutter/Card.dart +91 -0
  111. package/src/templates/flutter/Input.dart +73 -0
  112. package/src/templates/flutter/Text.dart +87 -0
  113. package/src/templates/flutter/utils.dart +13 -0
  114. package/src/templates/web/Accordion.tsx +64 -0
  115. package/src/templates/web/Alert.tsx +71 -0
  116. package/src/templates/web/AlertDialog.tsx +164 -0
  117. package/src/templates/web/Avatar.tsx +51 -0
  118. package/src/templates/web/Badge.tsx +38 -0
  119. package/src/templates/web/Button.tsx +60 -0
  120. package/src/templates/web/Card.tsx +92 -0
  121. package/src/templates/web/Dropdown.tsx +198 -0
  122. package/src/templates/web/Input.tsx +21 -0
  123. package/src/templates/web/Tabs.tsx +66 -0
  124. package/src/templates/web/Toast.tsx +126 -0
  125. package/src/templates/web/utils.ts +6 -0
  126. package/src/utils/detect.ts +81 -0
  127. package/src/utils/fs.ts +32 -0
  128. package/tsconfig.json +17 -0
@@ -0,0 +1,161 @@
1
+ import React, { useState, useEffect, createContext, useContext } from "react";
2
+ import {
3
+ View,
4
+ Text,
5
+ StyleSheet,
6
+ Animated,
7
+ Dimensions,
8
+ Pressable,
9
+ } from "react-native";
10
+
11
+ type ToastType = "default" | "success" | "error";
12
+
13
+ interface Toast {
14
+ id: string;
15
+ title: string;
16
+ description?: string;
17
+ type?: ToastType;
18
+ }
19
+
20
+ interface ToastContextType {
21
+ toast: (toast: Omit<Toast, "id">) => void;
22
+ }
23
+
24
+ const ToastContext = createContext<ToastContextType | undefined>(undefined);
25
+
26
+ export const ToastProvider = ({ children }: { children: React.ReactNode }) => {
27
+ const [toasts, setToasts] = useState<Toast[]>([]);
28
+
29
+ const toast = ({
30
+ title,
31
+ description,
32
+ type = "default",
33
+ }: Omit<Toast, "id">) => {
34
+ const id = Math.random().toString(36).substr(2, 9);
35
+ setToasts((prev) => [...prev, { id, title, description, type }]);
36
+ };
37
+
38
+ const removeToast = (id: string) => {
39
+ setToasts((prev) => prev.filter((t) => t.id !== id));
40
+ };
41
+
42
+ return (
43
+ <ToastContext.Provider value={{ toast }}>
44
+ {children}
45
+ <View style={styles.container} pointerEvents="box-none">
46
+ {toasts.map((t) => (
47
+ <ToastItem key={t.id} toast={t} onRemove={() => removeToast(t.id)} />
48
+ ))}
49
+ </View>
50
+ </ToastContext.Provider>
51
+ );
52
+ };
53
+
54
+ export const useToast = () => {
55
+ const context = useContext(ToastContext);
56
+ if (!context) throw new Error("useToast must be used within ToastProvider");
57
+ return context;
58
+ };
59
+
60
+ const ToastItem = ({
61
+ toast,
62
+ onRemove,
63
+ }: {
64
+ toast: Toast;
65
+ onRemove: () => void;
66
+ }) => {
67
+ const opacity = useState(new Animated.Value(0))[0];
68
+ const translateY = useState(new Animated.Value(20))[0];
69
+
70
+ useEffect(() => {
71
+ Animated.parallel([
72
+ Animated.timing(opacity, {
73
+ toValue: 1,
74
+ duration: 300,
75
+ useNativeDriver: true,
76
+ }),
77
+ Animated.timing(translateY, {
78
+ toValue: 0,
79
+ duration: 300,
80
+ useNativeDriver: true,
81
+ }),
82
+ ]).start();
83
+
84
+ const timer = setTimeout(() => {
85
+ Animated.parallel([
86
+ Animated.timing(opacity, {
87
+ toValue: 0,
88
+ duration: 300,
89
+ useNativeDriver: true,
90
+ }),
91
+ Animated.timing(translateY, {
92
+ toValue: 20,
93
+ duration: 300,
94
+ useNativeDriver: true,
95
+ }),
96
+ ]).start(() => onRemove());
97
+ }, 3000);
98
+
99
+ return () => clearTimeout(timer);
100
+ }, []);
101
+
102
+ const bgColors = {
103
+ default: "white",
104
+ success: "#f0fdf4",
105
+ error: "#fef2f2",
106
+ };
107
+
108
+ return (
109
+ <Animated.View
110
+ style={[
111
+ styles.toast,
112
+ {
113
+ opacity,
114
+ transform: [{ translateY }],
115
+ backgroundColor: bgColors[toast.type || "default"],
116
+ },
117
+ ]}
118
+ >
119
+ <View>
120
+ <Text style={styles.title}>{toast.title}</Text>
121
+ {toast.description && (
122
+ <Text style={styles.description}>{toast.description}</Text>
123
+ )}
124
+ </View>
125
+ </Animated.View>
126
+ );
127
+ };
128
+
129
+ const styles = StyleSheet.create({
130
+ container: {
131
+ position: "absolute",
132
+ bottom: 40,
133
+ left: 20,
134
+ right: 20,
135
+ alignItems: "center",
136
+ },
137
+ toast: {
138
+ width: "100%",
139
+ maxWidth: 400,
140
+ padding: 16,
141
+ borderRadius: 8,
142
+ marginBottom: 8,
143
+ shadowColor: "#000",
144
+ shadowOffset: { width: 0, height: 2 },
145
+ shadowOpacity: 0.1,
146
+ shadowRadius: 4,
147
+ elevation: 3,
148
+ borderWidth: 1,
149
+ borderColor: "#e4e4e7",
150
+ },
151
+ title: {
152
+ fontSize: 14,
153
+ fontWeight: "600",
154
+ color: "#18181b",
155
+ },
156
+ description: {
157
+ fontSize: 12,
158
+ color: "#71717a",
159
+ marginTop: 2,
160
+ },
161
+ });
@@ -0,0 +1,8 @@
1
+ import { StyleSheet } from "react-native";
2
+
3
+ /**
4
+ * A utility to merge React Native styles.
5
+ */
6
+ export function cn(...inputs: any[]) {
7
+ return StyleSheet.flatten(inputs.filter(Boolean));
8
+ }
@@ -0,0 +1,142 @@
1
+ import 'package:flutter/material.dart';
2
+
3
+ enum CrossUIAccordionType { single, multiple }
4
+
5
+ class CrossUIAccordionItem {
6
+ final String value;
7
+ final String trigger;
8
+ final String content;
9
+
10
+ const CrossUIAccordionItem({
11
+ required this.value,
12
+ required this.trigger,
13
+ required this.content,
14
+ });
15
+ }
16
+
17
+ class CrossUIAccordion extends StatefulWidget {
18
+ final List<CrossUIAccordionItem> items;
19
+ final CrossUIAccordionType type;
20
+ final bool collapsible;
21
+ final dynamic defaultValue;
22
+
23
+ const CrossUIAccordion({
24
+ super.key,
25
+ required this.items,
26
+ this.type = CrossUIAccordionType.single,
27
+ this.collapsible = false,
28
+ this.defaultValue,
29
+ });
30
+
31
+ @override
32
+ State<CrossUIAccordion> createState() => _CrossUIAccordionState();
33
+ }
34
+
35
+ class _CrossUIAccordionState extends State<CrossUIAccordion> {
36
+ late Set<String> _openItems;
37
+
38
+ @override
39
+ void initState() {
40
+ super.initState();
41
+ _openItems = {};
42
+
43
+ if (widget.defaultValue != null) {
44
+ if (widget.defaultValue is String) {
45
+ _openItems.add(widget.defaultValue as String);
46
+ } else if (widget.defaultValue is List) {
47
+ _openItems.addAll((widget.defaultValue as List).cast<String>());
48
+ }
49
+ }
50
+ }
51
+
52
+ void _toggleItem(String value) {
53
+ setState(() {
54
+ if (widget.type == CrossUIAccordionType.single) {
55
+ if (_openItems.contains(value)) {
56
+ if (widget.collapsible) {
57
+ _openItems.clear();
58
+ }
59
+ } else {
60
+ _openItems.clear();
61
+ _openItems.add(value);
62
+ }
63
+ } else {
64
+ if (_openItems.contains(value)) {
65
+ _openItems.remove(value);
66
+ } else {
67
+ _openItems.add(value);
68
+ }
69
+ }
70
+ });
71
+ }
72
+
73
+ @override
74
+ Widget build(BuildContext context) {
75
+ return Column(
76
+ children: widget.items.asMap().entries.map((entry) {
77
+ final index = entry.key;
78
+ final item = entry.value;
79
+ final isOpen = _openItems.contains(item.value);
80
+ final isLast = index == widget.items.length - 1;
81
+
82
+ return Container(
83
+ decoration: BoxDecoration(
84
+ border: Border(
85
+ bottom: isLast
86
+ ? BorderSide.none
87
+ : const BorderSide(color: Colors.grey, width: 1),
88
+ ),
89
+ ),
90
+ child: Column(
91
+ crossAxisAlignment: CrossAxisAlignment.start,
92
+ children: [
93
+ InkWell(
94
+ onTap: () => _toggleItem(item.value),
95
+ child: Padding(
96
+ padding: const EdgeInsets.symmetric(vertical: 16),
97
+ child: Row(
98
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
99
+ children: [
100
+ Expanded(
101
+ child: Text(
102
+ item.trigger,
103
+ style: const TextStyle(
104
+ fontSize: 16,
105
+ fontWeight: FontWeight.w500,
106
+ ),
107
+ ),
108
+ ),
109
+ AnimatedRotation(
110
+ turns: isOpen ? 0.75 : 0.25,
111
+ duration: const Duration(milliseconds: 200),
112
+ child: const Icon(Icons.chevron_right, size: 20),
113
+ ),
114
+ ],
115
+ ),
116
+ ),
117
+ ),
118
+ AnimatedCrossFade(
119
+ firstChild: const SizedBox.shrink(),
120
+ secondChild: Padding(
121
+ padding: const EdgeInsets.only(bottom: 16),
122
+ child: Text(
123
+ item.content,
124
+ style: TextStyle(
125
+ fontSize: 14,
126
+ color: Colors.grey[600],
127
+ height: 1.5,
128
+ ),
129
+ ),
130
+ ),
131
+ crossFadeState: isOpen
132
+ ? CrossFadeState.showSecond
133
+ : CrossFadeState.showFirst,
134
+ duration: const Duration(milliseconds: 200),
135
+ ),
136
+ ],
137
+ ),
138
+ );
139
+ }).toList(),
140
+ );
141
+ }
142
+ }
@@ -0,0 +1,96 @@
1
+ import 'package:flutter/material.dart';
2
+ import '../theme/tokens.dart';
3
+
4
+ enum CrossUIAlertVariant { info, success, warning, destructive }
5
+
6
+ class CrossUIAlert extends StatelessWidget {
7
+ final String title;
8
+ final String? description;
9
+ final CrossUIAlertVariant variant;
10
+ final IconData? icon;
11
+
12
+ const CrossUIAlert({
13
+ Key? key,
14
+ required this.title,
15
+ this.description,
16
+ this.variant = CrossUIAlertVariant.info,
17
+ this.icon,
18
+ }) : super(key: key);
19
+
20
+ @override
21
+ Widget build(BuildContext context) {
22
+ Color borderColor;
23
+ Color backgroundColor;
24
+ Color iconColor;
25
+ IconData defaultIcon;
26
+
27
+ switch (variant) {
28
+ case CrossUIAlertVariant.success:
29
+ borderColor = CrossUITokens.success;
30
+ backgroundColor = CrossUITokens.successLight;
31
+ iconColor = CrossUITokens.onSuccessLight;
32
+ defaultIcon = Icons.check_circle_outline;
33
+ break;
34
+ case CrossUIAlertVariant.warning:
35
+ borderColor = CrossUITokens.warning;
36
+ backgroundColor = CrossUITokens.warningLight;
37
+ iconColor = CrossUITokens.onWarningLight;
38
+ defaultIcon = Icons.warning_amber_rounded;
39
+ break;
40
+ case CrossUIAlertVariant.destructive:
41
+ borderColor = CrossUITokens.danger;
42
+ backgroundColor = CrossUITokens.dangerLight;
43
+ iconColor = CrossUITokens.onDangerLight;
44
+ defaultIcon = Icons.error_outline;
45
+ break;
46
+ case CrossUIAlertVariant.info:
47
+ default:
48
+ borderColor = CrossUITokens.info;
49
+ backgroundColor = CrossUITokens.infoLight;
50
+ iconColor = CrossUITokens.onInfoLight;
51
+ defaultIcon = Icons.info_outline;
52
+ break;
53
+ }
54
+
55
+ return Container(
56
+ padding: const EdgeInsets.all(CrossUITokens.spacingM),
57
+ decoration: BoxDecoration(
58
+ color: backgroundColor,
59
+ borderRadius: BorderRadius.circular(CrossUITokens.radiusMedium),
60
+ border: Border.all(color: borderColor.withOpacity(0.5)),
61
+ ),
62
+ child: Row(
63
+ crossAxisAlignment: CrossAxisAlignment.start,
64
+ children: [
65
+ Icon(icon ?? defaultIcon, color: iconColor, size: 20),
66
+ const SizedBox(width: CrossUITokens.spacingSm),
67
+ Expanded(
68
+ child: Column(
69
+ crossAxisAlignment: CrossAxisAlignment.start,
70
+ children: [
71
+ Text(
72
+ title,
73
+ style: TextStyle(
74
+ fontWeight: FontWeight.bold,
75
+ fontSize: CrossUITokens.fontSizeBase,
76
+ color: iconColor,
77
+ ),
78
+ ),
79
+ if (description != null) ...[
80
+ const SizedBox(height: 4),
81
+ Text(
82
+ description!,
83
+ style: TextStyle(
84
+ fontSize: CrossUITokens.fontSizeSm,
85
+ color: iconColor.withOpacity(0.8),
86
+ ),
87
+ ),
88
+ ],
89
+ ],
90
+ ),
91
+ ),
92
+ ],
93
+ ),
94
+ );
95
+ }
96
+ }
@@ -0,0 +1,175 @@
1
+ import 'package:flutter/material.dart';
2
+
3
+ enum CrossUIAlertDialogVariant { defaultVariant, destructive }
4
+
5
+ class CrossUIAlertDialog extends StatelessWidget {
6
+ final String title;
7
+ final String? description;
8
+ final String cancelText;
9
+ final String actionText;
10
+ final VoidCallback? onCancel;
11
+ final VoidCallback? onAction;
12
+ final CrossUIAlertDialogVariant variant;
13
+ final Widget? content;
14
+
15
+ const CrossUIAlertDialog({
16
+ super.key,
17
+ required this.title,
18
+ this.description,
19
+ this.cancelText = 'Cancel',
20
+ this.actionText = 'Continue',
21
+ this.onCancel,
22
+ this.onAction,
23
+ this.variant = CrossUIAlertDialogVariant.defaultVariant,
24
+ this.content,
25
+ });
26
+
27
+ static Future<T?> show<T>({
28
+ required BuildContext context,
29
+ required String title,
30
+ String? description,
31
+ String cancelText = 'Cancel',
32
+ String actionText = 'Continue',
33
+ VoidCallback? onCancel,
34
+ VoidCallback? onAction,
35
+ CrossUIAlertDialogVariant variant =
36
+ CrossUIAlertDialogVariant.defaultVariant,
37
+ Widget? content,
38
+ }) {
39
+ return showDialog<T>(
40
+ context: context,
41
+ barrierDismissible: true,
42
+ builder: (BuildContext context) {
43
+ return CrossUIAlertDialog(
44
+ title: title,
45
+ description: description,
46
+ cancelText: cancelText,
47
+ actionText: actionText,
48
+ onCancel: onCancel,
49
+ onAction: onAction,
50
+ variant: variant,
51
+ content: content,
52
+ );
53
+ },
54
+ );
55
+ }
56
+
57
+ @override
58
+ Widget build(BuildContext context) {
59
+ Color actionBackgroundColor;
60
+ Color actionTextColor;
61
+
62
+ if (variant == CrossUIAlertDialogVariant.destructive) {
63
+ actionBackgroundColor = Colors.red;
64
+ actionTextColor = Colors.white;
65
+ } else {
66
+ actionBackgroundColor = Colors.blue;
67
+ actionTextColor = Colors.white;
68
+ }
69
+
70
+ return Dialog(
71
+ shape: RoundedRectangleBorder(
72
+ borderRadius: BorderRadius.circular(16),
73
+ side: BorderSide(color: Colors.grey.withValues(alpha: 0.2), width: 1),
74
+ ),
75
+ child: Container(
76
+ constraints: const BoxConstraints(maxWidth: 400),
77
+ padding: const EdgeInsets.all(24),
78
+ child: Column(
79
+ mainAxisSize: MainAxisSize.min,
80
+ crossAxisAlignment: CrossAxisAlignment.start,
81
+ children: [
82
+ Column(
83
+ crossAxisAlignment: CrossAxisAlignment.start,
84
+ children: [
85
+ Text(
86
+ title,
87
+ style: const TextStyle(
88
+ fontSize: 18,
89
+ fontWeight: FontWeight.w700,
90
+ ),
91
+ ),
92
+ if (description != null) ...[
93
+ const SizedBox(height: 8),
94
+ Text(
95
+ description!,
96
+ style: TextStyle(
97
+ fontSize: 14,
98
+ color: Colors.grey[600],
99
+ height: 1.5,
100
+ ),
101
+ ),
102
+ ],
103
+ ],
104
+ ),
105
+ if (content != null) ...[const SizedBox(height: 16), content!],
106
+ const SizedBox(height: 24),
107
+ Row(
108
+ mainAxisAlignment: MainAxisAlignment.end,
109
+ children: [
110
+ Expanded(
111
+ child: InkWell(
112
+ onTap: () {
113
+ onCancel?.call();
114
+ Navigator.of(context).pop(false);
115
+ },
116
+ borderRadius: BorderRadius.circular(8),
117
+ child: Container(
118
+ padding: const EdgeInsets.symmetric(
119
+ horizontal: 16,
120
+ vertical: 12,
121
+ ),
122
+ decoration: BoxDecoration(
123
+ border: Border.all(
124
+ color: Colors.grey.withValues(alpha: 0.3),
125
+ ),
126
+ borderRadius: BorderRadius.circular(8),
127
+ ),
128
+ child: const Text(
129
+ 'Cancel',
130
+ textAlign: TextAlign.center,
131
+ style: TextStyle(
132
+ fontSize: 16,
133
+ fontWeight: FontWeight.w600,
134
+ ),
135
+ ),
136
+ ),
137
+ ),
138
+ ),
139
+ const SizedBox(width: 8),
140
+ Expanded(
141
+ child: InkWell(
142
+ onTap: () {
143
+ onAction?.call();
144
+ Navigator.of(context).pop(true);
145
+ },
146
+ borderRadius: BorderRadius.circular(8),
147
+ child: Container(
148
+ padding: const EdgeInsets.symmetric(
149
+ horizontal: 16,
150
+ vertical: 12,
151
+ ),
152
+ decoration: BoxDecoration(
153
+ color: actionBackgroundColor,
154
+ borderRadius: BorderRadius.circular(8),
155
+ ),
156
+ child: Text(
157
+ actionText,
158
+ textAlign: TextAlign.center,
159
+ style: TextStyle(
160
+ fontSize: 16,
161
+ fontWeight: FontWeight.w600,
162
+ color: actionTextColor,
163
+ ),
164
+ ),
165
+ ),
166
+ ),
167
+ ),
168
+ ],
169
+ ),
170
+ ],
171
+ ),
172
+ ),
173
+ );
174
+ }
175
+ }
@@ -0,0 +1,82 @@
1
+ import 'package:flutter/material.dart';
2
+
3
+ enum CrossUIAvatarSize { xs, sm, md, lg, xl, xxl }
4
+
5
+ class CrossUIAvatar extends StatelessWidget {
6
+ final String? src;
7
+ final String? fallback;
8
+ final CrossUIAvatarSize size;
9
+
10
+ const CrossUIAvatar({
11
+ super.key,
12
+ this.src,
13
+ this.fallback,
14
+ this.size = CrossUIAvatarSize.md,
15
+ });
16
+
17
+ @override
18
+ Widget build(BuildContext context) {
19
+ double dimension;
20
+ double fontSize;
21
+
22
+ switch (size) {
23
+ case CrossUIAvatarSize.xs:
24
+ dimension = 24.0;
25
+ fontSize = 10.0;
26
+ break;
27
+ case CrossUIAvatarSize.sm:
28
+ dimension = 32.0;
29
+ fontSize = 12.0;
30
+ break;
31
+ case CrossUIAvatarSize.lg:
32
+ dimension = 48.0;
33
+ fontSize = 16.0;
34
+ break;
35
+ case CrossUIAvatarSize.xl:
36
+ dimension = 64.0;
37
+ fontSize = 20.0;
38
+ break;
39
+ case CrossUIAvatarSize.xxl:
40
+ dimension = 80.0;
41
+ fontSize = 24.0;
42
+ break;
43
+ case CrossUIAvatarSize.md:
44
+ dimension = 40.0;
45
+ fontSize = 14.0;
46
+ break;
47
+ }
48
+
49
+ return Container(
50
+ width: dimension,
51
+ height: dimension,
52
+ decoration: BoxDecoration(
53
+ color: Colors.blue.withValues(alpha: 0.1),
54
+ shape: BoxShape.circle,
55
+ border: Border.all(color: Colors.grey.withValues(alpha: 0.2), width: 1),
56
+ ),
57
+ clipBehavior: Clip.antiAlias,
58
+ child: src != null
59
+ ? Image.network(
60
+ src!,
61
+ fit: BoxFit.cover,
62
+ errorBuilder: (context, error, stackTrace) =>
63
+ _buildFallback(fontSize),
64
+ )
65
+ : _buildFallback(fontSize),
66
+ );
67
+ }
68
+
69
+ Widget _buildFallback(double fontSize) {
70
+ return Center(
71
+ child: Text(
72
+ fallback?.substring(0, fallback!.length >= 2 ? 2 : 1).toUpperCase() ??
73
+ "??",
74
+ style: TextStyle(
75
+ color: Colors.blue,
76
+ fontSize: fontSize,
77
+ fontWeight: FontWeight.w600,
78
+ ),
79
+ ),
80
+ );
81
+ }
82
+ }