paseo-kanban 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.
@@ -0,0 +1,267 @@
1
+ import { useState, useEffect, useMemo, useSyncExternalStore } from "react";
2
+ import { View, Text, Pressable, StyleSheet } from "react-native";
3
+ import { Modal, TextInput, Icon } from "@getpaseo/plugin/client/react-native";
4
+ import type { PluginSurfaceProps } from "@getpaseo/plugin/client";
5
+ import type { LaneEditSession } from "./kanban-session";
6
+ import { useI18n } from "./i18n";
7
+
8
+ type PluginTheme = PluginSurfaceProps["theme"];
9
+
10
+ export interface LaneModalProps {
11
+ open: boolean;
12
+ onClose: () => void;
13
+ session: LaneEditSession;
14
+ theme: PluginTheme;
15
+ layout: { compact: boolean };
16
+ }
17
+
18
+ export function LaneModal({
19
+ open,
20
+ onClose,
21
+ session,
22
+ theme,
23
+ layout,
24
+ }: LaneModalProps) {
25
+ const { t } = useI18n();
26
+ const snapshot = useSyncExternalStore(
27
+ session.subscribe,
28
+ session.getSnapshot,
29
+ session.getSnapshot
30
+ );
31
+
32
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
33
+
34
+ useEffect(() => {
35
+ if (open) {
36
+ setShowDeleteConfirm(false);
37
+ }
38
+ }, [open, session]);
39
+
40
+ const canDelete = snapshot.canDelete;
41
+ const isSaving = snapshot.isSaving;
42
+ const errorMessage = snapshot.error;
43
+
44
+ const styles = useMemo(
45
+ () =>
46
+ StyleSheet.create({
47
+ container: {
48
+ gap: layout.compact ? 12 : 16,
49
+ },
50
+ fieldGroup: {
51
+ gap: 6,
52
+ },
53
+ label: {
54
+ fontSize: 13,
55
+ fontWeight: "600",
56
+ color: theme.colors.foreground,
57
+ },
58
+ input: {
59
+ backgroundColor: theme.colors.surface2,
60
+ color: theme.colors.foreground,
61
+ borderColor: theme.colors.border,
62
+ borderWidth: 1,
63
+ borderRadius: 8,
64
+ paddingHorizontal: 12,
65
+ paddingVertical: 8,
66
+ fontSize: 14,
67
+ },
68
+ errorBanner: {
69
+ backgroundColor: theme.colors.surface2,
70
+ borderColor: theme.colors.statusDanger,
71
+ borderWidth: 1,
72
+ borderRadius: 8,
73
+ padding: 10,
74
+ },
75
+ errorText: {
76
+ color: theme.colors.statusDanger,
77
+ fontSize: 13,
78
+ },
79
+ warningBanner: {
80
+ backgroundColor: theme.colors.surface2,
81
+ borderColor: theme.colors.statusWarning ?? theme.colors.border,
82
+ borderWidth: 1,
83
+ borderRadius: 8,
84
+ padding: 10,
85
+ },
86
+ warningText: {
87
+ color: theme.colors.statusWarning ?? theme.colors.foreground,
88
+ fontSize: 13,
89
+ },
90
+ buttonRow: {
91
+ flexDirection: "row",
92
+ justifyContent: "flex-end",
93
+ gap: 10,
94
+ marginTop: 10,
95
+ },
96
+ cancelButton: {
97
+ paddingHorizontal: 16,
98
+ paddingVertical: 10,
99
+ borderRadius: 8,
100
+ borderWidth: 1,
101
+ borderColor: theme.colors.border,
102
+ backgroundColor: theme.colors.surface1,
103
+ },
104
+ cancelButtonText: {
105
+ color: theme.colors.foreground,
106
+ fontSize: 14,
107
+ },
108
+ saveButton: {
109
+ paddingHorizontal: 18,
110
+ paddingVertical: 10,
111
+ borderRadius: 8,
112
+ backgroundColor: theme.colors.accent,
113
+ },
114
+ saveButtonDisabled: {
115
+ opacity: 0.6,
116
+ },
117
+ saveButtonText: {
118
+ color: theme.colors.accentForeground,
119
+ fontSize: 14,
120
+ fontWeight: "600",
121
+ },
122
+ deleteSection: {
123
+ marginTop: 12,
124
+ paddingTop: 12,
125
+ borderTopWidth: 1,
126
+ borderTopColor: theme.colors.border,
127
+ flexDirection: "row",
128
+ justifyContent: "space-between",
129
+ alignItems: "center",
130
+ },
131
+ deleteButton: {
132
+ paddingHorizontal: 12,
133
+ paddingVertical: 8,
134
+ borderRadius: 6,
135
+ backgroundColor: theme.colors.surface1,
136
+ borderWidth: 1,
137
+ borderColor: theme.colors.statusDanger,
138
+ },
139
+ deleteButtonText: {
140
+ color: theme.colors.statusDanger,
141
+ fontSize: 13,
142
+ fontWeight: "500",
143
+ },
144
+ }),
145
+ [theme, layout.compact]
146
+ );
147
+
148
+ const handleSave = async () => {
149
+ const result = await session.save();
150
+ if (result.success) {
151
+ onClose();
152
+ }
153
+ };
154
+
155
+ const handleDelete = async () => {
156
+ const result = await session.deleteLane();
157
+ if (result.success) {
158
+ onClose();
159
+ }
160
+ };
161
+
162
+ return (
163
+ <Modal
164
+ title={session.mode === "edit" ? t("laneModal.titleEdit") : t("laneModal.titleCreate")}
165
+ icon={<Icon name="Columns3" size={18} color={theme.colors.foreground} />}
166
+ open={open}
167
+ onOpenChange={(next) => {
168
+ if (!next && !isSaving) {
169
+ onClose();
170
+ }
171
+ }}
172
+ >
173
+ <Modal.Content
174
+ style={{ backgroundColor: theme.colors.surface0 }}
175
+ contentContainerStyle={styles.container}
176
+ >
177
+ {errorMessage && (
178
+ <View style={styles.errorBanner}>
179
+ <Text style={styles.errorText}>{errorMessage}</Text>
180
+ </View>
181
+ )}
182
+
183
+ <View style={styles.fieldGroup}>
184
+ <Text style={styles.label}>{t("laneModal.nameLabel")}</Text>
185
+ <TextInput
186
+ style={styles.input}
187
+ placeholder={t("laneModal.namePlaceholder")}
188
+ placeholderTextColor={theme.colors.foregroundMuted}
189
+ value={snapshot.title}
190
+ onChangeText={(t) => session.setTitle(t)}
191
+ />
192
+ </View>
193
+
194
+ {session.mode === "edit" && !canDelete && (
195
+ <View style={styles.warningBanner}>
196
+ <Text style={styles.warningText}>
197
+ {snapshot.lanesCount <= 1
198
+ ? t("laneModal.cannotDeleteLast")
199
+ : t("laneModal.cannotDeleteHasTasks", { count: snapshot.tasksInLaneCount })}
200
+ </Text>
201
+ </View>
202
+ )}
203
+
204
+ {/* Delete Lane Section for Edit Mode */}
205
+ {session.mode === "edit" && canDelete && (
206
+ <View style={styles.deleteSection}>
207
+ {!showDeleteConfirm ? (
208
+ <Pressable
209
+ onPress={() => setShowDeleteConfirm(true)}
210
+ style={styles.deleteButton}
211
+ disabled={isSaving}
212
+ >
213
+ <Text style={styles.deleteButtonText}>{t("laneModal.deleteLane")}</Text>
214
+ </Pressable>
215
+ ) : (
216
+ <View style={{ flexDirection: "row", gap: 8, alignItems: "center" }}>
217
+ <Text style={{ fontSize: 13, color: theme.colors.statusDanger }}>
218
+ {t("laneModal.confirmDeletePrompt")}
219
+ </Text>
220
+ <Pressable
221
+ onPress={handleDelete}
222
+ style={[styles.deleteButton, { backgroundColor: theme.colors.statusDanger }]}
223
+ disabled={isSaving}
224
+ >
225
+ <Text style={[styles.deleteButtonText, { color: "#fff" }]}>
226
+ {t("laneModal.confirmDelete")}
227
+ </Text>
228
+ </Pressable>
229
+ <Pressable
230
+ onPress={() => setShowDeleteConfirm(false)}
231
+ style={styles.cancelButton}
232
+ >
233
+ <Text style={styles.cancelButtonText}>{t("laneModal.cancel")}</Text>
234
+ </Pressable>
235
+ </View>
236
+ )}
237
+ </View>
238
+ )}
239
+
240
+ {/* Action Buttons */}
241
+ <View style={styles.buttonRow}>
242
+ <Pressable
243
+ onPress={onClose}
244
+ style={styles.cancelButton}
245
+ disabled={isSaving}
246
+ >
247
+ <Text style={styles.cancelButtonText}>{t("laneModal.cancel")}</Text>
248
+ </Pressable>
249
+
250
+ <Pressable
251
+ onPress={handleSave}
252
+ style={[styles.saveButton, isSaving && styles.saveButtonDisabled]}
253
+ disabled={isSaving}
254
+ >
255
+ <Text style={styles.saveButtonText}>
256
+ {isSaving
257
+ ? t("laneModal.saving")
258
+ : session.mode === "edit"
259
+ ? t("laneModal.saveChanges")
260
+ : t("laneModal.submitCreate")}
261
+ </Text>
262
+ </Pressable>
263
+ </View>
264
+ </Modal.Content>
265
+ </Modal>
266
+ );
267
+ }