@gogitcms/design-system 0.15.0-next.3

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 (70) hide show
  1. package/README.md +77 -0
  2. package/css/components.css +312 -0
  3. package/css/tokens.css +212 -0
  4. package/package.json +65 -0
  5. package/src/ThemeProvider.tsx +86 -0
  6. package/src/__tests__/ApplyChangesModal.test.tsx +148 -0
  7. package/src/__tests__/BranchImport.test.tsx +46 -0
  8. package/src/__tests__/Button.test.tsx +45 -0
  9. package/src/__tests__/ChangeRequestSummary.test.tsx +57 -0
  10. package/src/__tests__/ContentBrowser.changes.test.tsx +611 -0
  11. package/src/__tests__/ContentBrowser.collab.test.tsx +322 -0
  12. package/src/__tests__/ContentBrowser.collabsync.test.tsx +264 -0
  13. package/src/__tests__/ContentBrowser.contentslot.test.tsx +53 -0
  14. package/src/__tests__/ContentBrowser.discriminator.test.tsx +142 -0
  15. package/src/__tests__/ContentBrowser.drafts.test.tsx +271 -0
  16. package/src/__tests__/ContentBrowser.fields.test.tsx +117 -0
  17. package/src/__tests__/ContentBrowser.media.test.tsx +140 -0
  18. package/src/__tests__/ContentBrowser.mixedcollab.test.tsx +63 -0
  19. package/src/__tests__/ContentBrowser.mixedvalues.test.tsx +38 -0
  20. package/src/__tests__/ContentBrowser.pagination.test.tsx +62 -0
  21. package/src/__tests__/ContentBrowser.previewtab.test.tsx +212 -0
  22. package/src/__tests__/ContentBrowser.reorder.test.tsx +45 -0
  23. package/src/__tests__/ContentBrowser.search.test.tsx +135 -0
  24. package/src/__tests__/ContentBrowser.selectvalue.test.tsx +185 -0
  25. package/src/__tests__/ContentBrowser.staged.test.tsx +132 -0
  26. package/src/__tests__/ContentBrowser.usermenu.test.tsx +56 -0
  27. package/src/__tests__/MediaBrowser.test.tsx +353 -0
  28. package/src/__tests__/MediaField.test.tsx +185 -0
  29. package/src/__tests__/Notifications.test.tsx +69 -0
  30. package/src/__tests__/Onboarding.test.tsx +287 -0
  31. package/src/__tests__/cssTokens.test.ts +201 -0
  32. package/src/__tests__/fieldComponents.test.ts +43 -0
  33. package/src/__tests__/reorder.test.ts +58 -0
  34. package/src/components/ApplyChangesModal.tsx +348 -0
  35. package/src/components/BranchImport.tsx +157 -0
  36. package/src/components/BranchMenu.tsx +192 -0
  37. package/src/components/Button.tsx +131 -0
  38. package/src/components/ChangeDetail.tsx +472 -0
  39. package/src/components/ChangeRequestSummary.tsx +173 -0
  40. package/src/components/CollabField.tsx +388 -0
  41. package/src/components/ContentBrowser.tsx +5073 -0
  42. package/src/components/Icon.tsx +28 -0
  43. package/src/components/Icon.web.tsx +31 -0
  44. package/src/components/Input.tsx +106 -0
  45. package/src/components/MediaBrowser.tsx +766 -0
  46. package/src/components/MediaField.tsx +670 -0
  47. package/src/components/MediaPreview.tsx +91 -0
  48. package/src/components/MediaPreview.web.tsx +169 -0
  49. package/src/components/NavRow.tsx +105 -0
  50. package/src/components/Notifications.tsx +301 -0
  51. package/src/components/Onboarding.tsx +751 -0
  52. package/src/components/ProjectMenu.tsx +124 -0
  53. package/src/components/Segment.tsx +87 -0
  54. package/src/components/Skeleton.tsx +216 -0
  55. package/src/components/Spinner.tsx +44 -0
  56. package/src/components/Text.tsx +85 -0
  57. package/src/components/documentDrafts.ts +213 -0
  58. package/src/components/layout.tsx +284 -0
  59. package/src/components/primitives.tsx +143 -0
  60. package/src/components/reorder.ts +40 -0
  61. package/src/fieldComponents.ts +63 -0
  62. package/src/icons.ts +102 -0
  63. package/src/index.ts +198 -0
  64. package/src/media.ts +229 -0
  65. package/src/theme.ts +116 -0
  66. package/src/web/Button.tsx +110 -0
  67. package/src/web/Icon.tsx +52 -0
  68. package/src/web/Input.tsx +39 -0
  69. package/src/web/index.ts +43 -0
  70. package/src/web/primitives.tsx +119 -0
@@ -0,0 +1,348 @@
1
+ import React, { useState } from "react";
2
+ import { View, Pressable, ActivityIndicator } from "react-native";
3
+ import { useTheme } from "../ThemeProvider";
4
+ import { Text } from "./Text";
5
+ import { Icon } from "./Icon";
6
+ import { Button } from "./Button";
7
+ import { Input } from "./Input";
8
+
9
+ /** The merge run's coarse phase, matching the server's step enum. */
10
+ export type MergeStep = "comparing" | "merging" | "pushing";
11
+ export type MergeStatus = "pending" | "running" | "conflicted" | "needs_review" | "succeeded" | "failed";
12
+
13
+ export type MergeProgress = {
14
+ status: MergeStatus;
15
+ step: MergeStep;
16
+ sourceBranch: string;
17
+ targetBranch: string;
18
+ commit?: string | null;
19
+ filesChanged?: number;
20
+ added?: number;
21
+ removed?: number;
22
+ error?: string | null;
23
+ /** When status is needs_review: the non-CMS files that need a developer. */
24
+ escalationFiles?: string[];
25
+ };
26
+
27
+ export type ApplyChangesModalProps = {
28
+ sourceBranch: string;
29
+ targetBranch: string;
30
+ /**
31
+ * The run's live state. Absent → the confirmation step (nothing has run yet):
32
+ * the modal explains what applying will do and waits for onConfirm. Present →
33
+ * the progress checklist and result panels.
34
+ */
35
+ progress?: MergeProgress;
36
+ /** A short summary of what will be merged, shown on the confirmation step. */
37
+ summary?: string;
38
+ /** Number of unresolved conflicts, shown on the conflicted state. */
39
+ conflictCount?: number;
40
+ /**
41
+ * When true and on the confirmation step, an open change request already
42
+ * exists for this source→target: the modal explains that and offers to view it
43
+ * instead of applying.
44
+ */
45
+ changeRequestOpen?: boolean;
46
+ /** The open change request's pull-request URL, to share with a developer. */
47
+ changeRequestUrl?: string;
48
+ /** True while the createChangeRequest mutation is in flight. */
49
+ creatingChangeRequest?: boolean;
50
+ /** Start the merge, from the confirmation step. */
51
+ onConfirm?: () => void;
52
+ /** Go resolve conflicts, from the conflicted state (closes the modal). */
53
+ onReviewConflicts?: () => void;
54
+ /** Escalate a needs_review merge with the entered explanation. */
55
+ onCreateChangeRequest?: (explanation: string) => void;
56
+ /** Open the summary of the existing change request. */
57
+ onViewChangeRequest?: () => void;
58
+ /** Cancel / dismiss. */
59
+ onClose: () => void;
60
+ /** Done, on success. Defaults to onClose. */
61
+ onDone?: () => void;
62
+ };
63
+
64
+ // The checklist rows, in order. A step is done once the run has advanced past it
65
+ // (or finished), active while it is the current step, and pending before then.
66
+ const STEPS: { key: MergeStep; label: string }[] = [
67
+ { key: "comparing", label: "Comparing" },
68
+ { key: "merging", label: "Fast-forward not possible — creating merge commit" },
69
+ { key: "pushing", label: "Pushing merge commit to origin" },
70
+ ];
71
+
72
+ const stepOrder: Record<MergeStep, number> = { comparing: 0, merging: 1, pushing: 2 };
73
+
74
+ function StepRow({ label, state }: { label: string; state: "done" | "active" | "pending" }) {
75
+ const t = useTheme();
76
+ return (
77
+ <View testID={`merge-step-${state}`} style={{ flexDirection: "row", alignItems: "center", gap: t.space(2) }}>
78
+ {state === "done" ? (
79
+ <View
80
+ style={{
81
+ width: 18, height: 18, borderRadius: 999, alignItems: "center", justifyContent: "center",
82
+ backgroundColor: t.color.diffAddFg,
83
+ }}
84
+ >
85
+ <Icon name="check" size={12} color={t.color.surfacePage} />
86
+ </View>
87
+ ) : state === "active" ? (
88
+ <ActivityIndicator size="small" />
89
+ ) : (
90
+ <View style={{ width: 18, height: 18, borderRadius: 999, borderWidth: 1, borderColor: t.color.borderDefault }} />
91
+ )}
92
+ <Text variant="body" color={state === "pending" ? "tertiary" : "primary"}>{label}</Text>
93
+ </View>
94
+ );
95
+ }
96
+
97
+ /**
98
+ * The apply-changes progress modal. It runs through the three merge steps
99
+ * (spinner → green check), then shows a success panel with the commit + stats,
100
+ * or an error. Read-only: it reflects a run the host drives via a subscription.
101
+ */
102
+ export function ApplyChangesModal({ sourceBranch, targetBranch, progress, summary, conflictCount, changeRequestOpen, changeRequestUrl, creatingChangeRequest, onConfirm, onReviewConflicts, onCreateChangeRequest, onViewChangeRequest, onClose, onDone }: ApplyChangesModalProps) {
103
+ const t = useTheme();
104
+ const done = onDone ?? onClose;
105
+ const [explanation, setExplanation] = useState("");
106
+ const [copied, setCopied] = useState(false);
107
+ const copyLink = () => {
108
+ try {
109
+ if (typeof navigator !== "undefined" && navigator.clipboard && changeRequestUrl) {
110
+ void navigator.clipboard.writeText(changeRequestUrl);
111
+ }
112
+ } catch {
113
+ // Clipboard unavailable (permissions / older browser) — the readonly field
114
+ // still lets the user select and copy manually.
115
+ }
116
+ setCopied(true);
117
+ setTimeout(() => setCopied(false), 1500);
118
+ };
119
+ // No run yet → the confirmation step.
120
+ const confirming = !progress;
121
+ const status = progress?.status;
122
+ const conflicted = status === "conflicted";
123
+ const needsReview = status === "needs_review";
124
+ const terminal = status === "succeeded" || status === "failed" || conflicted || needsReview;
125
+ const current = progress ? stepOrder[progress.step] : 0;
126
+
127
+ const stepState = (i: number): "done" | "active" | "pending" => {
128
+ if (status === "succeeded") return "done";
129
+ if (i < current) return "done";
130
+ if (i === current) return status === "failed" ? "pending" : "active";
131
+ return "pending";
132
+ };
133
+
134
+ return (
135
+ <View
136
+ testID="apply-changes-modal"
137
+ style={{
138
+ position: "absolute", top: 0, left: 0, right: 0, bottom: 0, zIndex: 50,
139
+ alignItems: "center", justifyContent: "center", padding: t.space(4),
140
+ backgroundColor: "rgba(0,0,0,0.45)",
141
+ }}
142
+ >
143
+ <View
144
+ style={{
145
+ width: 460, maxWidth: "100%", gap: t.space(4), padding: t.space(5),
146
+ borderRadius: t.radius.lg, borderWidth: 1, borderColor: t.color.borderDefault,
147
+ backgroundColor: t.color.surfaceRaised,
148
+ }}
149
+ >
150
+ <View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between" }}>
151
+ <Text variant="h3" weight="semibold">Apply changes</Text>
152
+ {confirming || terminal ? (
153
+ <Pressable testID="apply-close" onPress={onClose} style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })}>
154
+ <Icon name="x" size={18} color={t.color.textSecondary} />
155
+ </Pressable>
156
+ ) : null}
157
+ </View>
158
+
159
+ {/* source → target */}
160
+ <View style={{ flexDirection: "row", alignItems: "center", gap: t.space(2) }}>
161
+ <BranchChip name={sourceBranch} />
162
+ <Text variant="monoSm" color="tertiary">→</Text>
163
+ <BranchChip name={targetBranch} />
164
+ </View>
165
+
166
+ {confirming && changeRequestOpen ? (
167
+ // An escalated change request is already open for this pair: don't let
168
+ // the user start a second merge — send them to its summary.
169
+ <View testID="apply-cr-open" style={{ gap: t.space(3) }}>
170
+ <View style={{ flexDirection: "row", alignItems: "center", gap: t.space(2) }}>
171
+ <Icon name="gitPullRequest" size={16} color={t.color.textSecondary} />
172
+ <Text variant="body" weight="medium">A change request is already open</Text>
173
+ </View>
174
+ <Text variant="body" color="secondary">
175
+ {`Merging ${sourceBranch} into ${targetBranch} needs a developer to resolve code conflicts. Send them this link so they can pick it up:`}
176
+ </Text>
177
+ {changeRequestUrl ? (
178
+ <View style={{ flexDirection: "row", alignItems: "center", gap: t.space(2) }}>
179
+ <View style={{ flex: 1 }}>
180
+ <Input
181
+ value={changeRequestUrl}
182
+ onChangeText={() => {}}
183
+ editable={false}
184
+ mono
185
+ size="md"
186
+ testID="change-request-link"
187
+ />
188
+ </View>
189
+ <Button
190
+ title={copied ? "Copied" : "Copy"}
191
+ variant="default"
192
+ size="md"
193
+ iconLeft={copied ? "check" : "copy"}
194
+ onPress={copyLink}
195
+ testID="copy-change-request-link"
196
+ />
197
+ </View>
198
+ ) : null}
199
+ </View>
200
+ ) : confirming ? (
201
+ // Confirmation step: spell out the irreversible, outward-facing effects
202
+ // before anything is pushed to the remote.
203
+ <View testID="apply-confirm" style={{ gap: t.space(3) }}>
204
+ <Text variant="body" color="secondary">
205
+ {`This merges ${sourceBranch} into ${targetBranch}, commits the result, and pushes a merge commit to origin/${targetBranch}.`}
206
+ </Text>
207
+ <Text variant="body" color="secondary">
208
+ {`${sourceBranch} is then deleted. This can’t be undone.`}
209
+ </Text>
210
+ {summary ? <Text variant="monoSm" color="tertiary">{summary}</Text> : null}
211
+ </View>
212
+ ) : needsReview ? (
213
+ // Content merged, but code/config files conflict. A content editor can't
214
+ // resolve these — collect an explanation and escalate to a developer.
215
+ <View testID="merge-needs-review" style={{ gap: t.space(3) }}>
216
+ <View style={{ flexDirection: "row", alignItems: "center", gap: t.space(2) }}>
217
+ <Icon name="gitPullRequest" size={16} color={t.color.diffDelFg} />
218
+ <Text variant="body" weight="medium">Code conflicts need a developer</Text>
219
+ </View>
220
+ <Text variant="body" color="secondary">
221
+ {`The content merged cleanly, but these files changed on both ${sourceBranch} and ${targetBranch} and need a developer to resolve:`}
222
+ </Text>
223
+ <View style={{ gap: 2, padding: t.space(2), borderRadius: t.radius.md, backgroundColor: t.color.diffDelBg }}>
224
+ {(progress?.escalationFiles ?? []).map((f) => (
225
+ <Text key={f} variant="monoSm" color={t.color.diffDelFg} testID={`escalation-file-${f}`}>{f}</Text>
226
+ ))}
227
+ </View>
228
+ <Input
229
+ value={explanation}
230
+ onChangeText={setExplanation}
231
+ label="Explain what changed (added to the pull request)"
232
+ placeholder="e.g. Updated the pricing table and the checkout copy"
233
+ autoCapitalize="sentences"
234
+ testID="change-request-explanation"
235
+ />
236
+ </View>
237
+ ) : conflicted ? (
238
+ // Conflicts stopped the merge: nothing was pushed. Point the user at
239
+ // the inline resolution rather than leaving the checklist half-lit.
240
+ <View testID="merge-conflicted" style={{ gap: t.space(2) }}>
241
+ <View style={{ flexDirection: "row", alignItems: "center", gap: t.space(2) }}>
242
+ <Icon name="gitMerge" size={16} color={t.color.diffDelFg} />
243
+ <Text variant="body" weight="medium">
244
+ {conflictCount
245
+ ? `${conflictCount} conflict${conflictCount === 1 ? "" : "s"} to resolve`
246
+ : "This merge has conflicts to resolve"}
247
+ </Text>
248
+ </View>
249
+ <Text variant="body" color="secondary">
250
+ {`${sourceBranch} and ${targetBranch} changed the same fields. Nothing was pushed. Resolve each conflict, then apply again.`}
251
+ </Text>
252
+ </View>
253
+ ) : (
254
+ // Progress checklist.
255
+ <View style={{ gap: t.space(3) }}>
256
+ {STEPS.map((s, i) => (
257
+ <StepRow key={s.key} label={s.label} state={stepState(i)} />
258
+ ))}
259
+ </View>
260
+ )}
261
+
262
+ {status === "succeeded" && progress ? (
263
+ <View
264
+ testID="merge-success"
265
+ style={{
266
+ gap: t.space(1), padding: t.space(3), borderRadius: t.radius.md,
267
+ backgroundColor: t.color.diffAddBg,
268
+ }}
269
+ >
270
+ <View style={{ flexDirection: "row", alignItems: "center", gap: t.space(2) }}>
271
+ <Icon name="check" size={15} color={t.color.diffAddFg} />
272
+ <Text variant="body" weight="medium" color={t.color.diffAddFg}>
273
+ {`Merged ${progress.sourceBranch} into ${progress.targetBranch}`}
274
+ </Text>
275
+ </View>
276
+ <Text variant="monoSm" color="secondary">
277
+ {[
278
+ progress.commit ? progress.commit.slice(0, 7) : null,
279
+ progress.filesChanged != null ? `${progress.filesChanged} file${progress.filesChanged === 1 ? "" : "s"}` : null,
280
+ (progress.added || progress.removed) ? `+${progress.added ?? 0} −${progress.removed ?? 0}` : null,
281
+ ].filter(Boolean).join(" · ")}
282
+ </Text>
283
+ </View>
284
+ ) : null}
285
+
286
+ {status === "failed" ? (
287
+ <View
288
+ testID="merge-error"
289
+ style={{ padding: t.space(3), borderRadius: t.radius.md, backgroundColor: t.color.diffDelBg }}
290
+ >
291
+ <Text variant="monoSm" color={t.color.diffDelFg}>{progress?.error || "The merge could not be completed."}</Text>
292
+ </View>
293
+ ) : null}
294
+
295
+ <View style={{ flexDirection: "row", justifyContent: "flex-end", gap: t.space(2) }}>
296
+ {confirming && changeRequestOpen ? (
297
+ <>
298
+ <Button title="Cancel" variant="ghost" size="sm" onPress={onClose} testID="apply-cancel" />
299
+ <Button title="View change request" variant="primary" size="sm" iconLeft="gitPullRequest" onPress={onViewChangeRequest ?? (() => {})} testID="apply-view-change-request" />
300
+ </>
301
+ ) : confirming ? (
302
+ <>
303
+ <Button title="Cancel" variant="ghost" size="sm" onPress={onClose} testID="apply-cancel" />
304
+ <Button title="Apply changes" variant="primary" size="sm" iconLeft="gitMerge" onPress={onConfirm ?? (() => {})} testID="apply-confirm-submit" />
305
+ </>
306
+ ) : needsReview ? (
307
+ <>
308
+ <Button title="Cancel" variant="ghost" size="sm" onPress={onClose} testID="apply-cancel" />
309
+ <Button
310
+ title={creatingChangeRequest ? "Opening…" : "Open change request"}
311
+ variant="primary"
312
+ size="sm"
313
+ iconLeft="gitPullRequest"
314
+ disabled={creatingChangeRequest || explanation.trim() === ""}
315
+ onPress={() => onCreateChangeRequest?.(explanation.trim())}
316
+ testID="create-change-request-submit"
317
+ />
318
+ </>
319
+ ) : conflicted ? (
320
+ <Button title="Review conflicts" variant="primary" size="sm" onPress={onReviewConflicts ?? onClose} testID="apply-review-conflicts" />
321
+ ) : status === "succeeded" ? (
322
+ <Button title="Done" variant="primary" size="sm" onPress={done} testID="apply-done" />
323
+ ) : status === "failed" ? (
324
+ <Button title="Close" variant="default" size="sm" onPress={onClose} testID="apply-dismiss" />
325
+ ) : (
326
+ <Button title="Cancel" variant="ghost" size="sm" onPress={onClose} testID="apply-cancel" />
327
+ )}
328
+ </View>
329
+ </View>
330
+ </View>
331
+ );
332
+ }
333
+
334
+ function BranchChip({ name }: { name: string }) {
335
+ const t = useTheme();
336
+ return (
337
+ <View
338
+ style={{
339
+ flexDirection: "row", alignItems: "center", gap: 6,
340
+ paddingHorizontal: t.space(2), height: t.control.sm,
341
+ borderRadius: t.radius.md, borderWidth: 1, borderColor: t.color.borderDefault,
342
+ }}
343
+ >
344
+ <Icon name="gitBranch" size={13} color={t.color.textSecondary} />
345
+ <Text variant="monoSm" color="secondary">{name}</Text>
346
+ </View>
347
+ );
348
+ }
@@ -0,0 +1,157 @@
1
+ import React from "react";
2
+ import { View } from "react-native";
3
+ import { useTheme } from "../ThemeProvider";
4
+ import { Text } from "./Text";
5
+ import { Icon } from "./Icon";
6
+ import { Button } from "./Button";
7
+ import { Spinner } from "./Spinner";
8
+ import { ContentBrowserSkeleton } from "./Skeleton";
9
+
10
+ // The full-screen states an editor shows when the branch it opened has never
11
+ // been imported. Both are gates rather than banners: until the first import lands
12
+ // there is no content behind them, so the ContentBrowser is not mounted at all.
13
+ // The skeleton shows through as a backdrop so the shell the editor is about to
14
+ // become is already in place — the real panes drop into the same layout rather
15
+ // than replacing a different screen.
16
+
17
+ // Overlay centers a message card over the app-shell skeleton. The card carries
18
+ // its own surface so the shimmer behind it never fights the text.
19
+ function Overlay({ children, testID }: { children: React.ReactNode; testID?: string }) {
20
+ const t = useTheme();
21
+ return (
22
+ <View style={{ flex: 1 }} testID={testID}>
23
+ <ContentBrowserSkeleton />
24
+ <View
25
+ style={{
26
+ position: "absolute",
27
+ top: 0,
28
+ left: 0,
29
+ right: 0,
30
+ bottom: 0,
31
+ alignItems: "center",
32
+ justifyContent: "center",
33
+ padding: t.space(6),
34
+ }}
35
+ >
36
+ <View
37
+ style={{
38
+ maxWidth: 460,
39
+ alignItems: "center",
40
+ gap: t.space(3),
41
+ paddingHorizontal: t.space(7),
42
+ paddingVertical: t.space(8),
43
+ backgroundColor: t.color.surfaceRaised,
44
+ borderWidth: 1,
45
+ borderColor: t.color.borderDefault,
46
+ borderRadius: t.radius.lg,
47
+ }}
48
+ >
49
+ {children}
50
+ </View>
51
+ </View>
52
+ </View>
53
+ );
54
+ }
55
+
56
+ export type BranchImportingScreenProps = {
57
+ // The branch being imported, named so a user who switched branches knows which
58
+ // one they are waiting on.
59
+ branch: string;
60
+ // "owner/name", when the host knows it.
61
+ repository?: string;
62
+ testID?: string;
63
+ };
64
+
65
+ /**
66
+ * Held while a branch's FIRST import runs. There is nothing to edit yet — an
67
+ * empty editor would read as "this repository has no content", which is a
68
+ * different and wrong statement — so the whole screen waits. The host swaps it
69
+ * for the real editor when the import subscription says the import landed.
70
+ */
71
+ export function BranchImportingScreen({ branch, repository, testID }: BranchImportingScreenProps) {
72
+ const t = useTheme();
73
+ return (
74
+ <Overlay testID={testID ?? "branch-importing"}>
75
+ <Spinner size={28} color={t.color.textSecondary} />
76
+ <Text variant="body" weight="semibold" style={{ textAlign: "center" }}>
77
+ {`Importing ${branch}`}
78
+ </Text>
79
+ <Text variant="sm" color="secondary" style={{ textAlign: "center" }}>
80
+ {repository
81
+ ? `Reading the content models and documents on this branch of ${repository}. This runs once per branch.`
82
+ : "Reading the content models and documents on this branch. This runs once per branch."}
83
+ </Text>
84
+ <Text variant="monoSm" color="tertiary" style={{ textAlign: "center" }}>
85
+ The editor opens by itself as soon as it finishes.
86
+ </Text>
87
+ </Overlay>
88
+ );
89
+ }
90
+
91
+ export type BranchImportFailedScreenProps = {
92
+ branch: string;
93
+ repository?: string;
94
+ // The run's error message. Shown verbatim, because the person who can fix it
95
+ // is often the person reading it.
96
+ error?: string;
97
+ // The run's machine-readable error code. "config_not_found" is the one this
98
+ // screen rewrites into setup instructions rather than an error.
99
+ errorCode?: string;
100
+ // Re-reads the branch's import state. "Check again" rather than "Retry",
101
+ // because the editor cannot start an import — it can only look again, which is
102
+ // the useful action right after the reader commits the config file the setup
103
+ // message asked for. Omitted → the screen only explains.
104
+ onRecheck?: () => void;
105
+ rechecking?: boolean;
106
+ testID?: string;
107
+ };
108
+
109
+ /**
110
+ * Shown when a branch's first import failed. A dead end rather than a warning:
111
+ * nothing was ever imported, so there is no content to fall back to and the
112
+ * editor cannot open. The one failure with a real remedy — no config file on the
113
+ * branch — is stated as the setup step it is.
114
+ */
115
+ export function BranchImportFailedScreen({
116
+ branch,
117
+ repository,
118
+ error,
119
+ errorCode,
120
+ onRecheck,
121
+ rechecking,
122
+ testID,
123
+ }: BranchImportFailedScreenProps) {
124
+ const t = useTheme();
125
+ const needsSetup = errorCode === "config_not_found";
126
+ const title = needsSetup ? `${branch} has no CMS config yet` : `Couldn’t import ${branch}`;
127
+ const detail = needsSetup
128
+ ? `Commit a go-git-cms.yml at the root of ${repository ?? "the repository"} and push it to ${branch}. It declares the content models this editor edits — until it exists there is nothing to import.`
129
+ : `The import of this branch failed, so there is no content to edit here yet.`;
130
+ return (
131
+ <Overlay testID={testID ?? "branch-import-failed"}>
132
+ <Icon
133
+ name={needsSetup ? "settings" : "alert"}
134
+ size={28}
135
+ color={needsSetup ? t.color.textSecondary : t.color.diffDelFg}
136
+ />
137
+ <Text variant="body" weight="semibold" style={{ textAlign: "center" }}>{title}</Text>
138
+ <Text variant="sm" color="secondary" style={{ textAlign: "center" }}>{detail}</Text>
139
+ {/* The raw message, for the non-setup failures where it is the only thing
140
+ that says what actually went wrong. */}
141
+ {!needsSetup && error ? (
142
+ <Text variant="monoSm" color="tertiary" style={{ textAlign: "center" }} testID="branch-import-error">
143
+ {error}
144
+ </Text>
145
+ ) : null}
146
+ {onRecheck ? (
147
+ <Button
148
+ title={rechecking ? "Checking…" : "Check again"}
149
+ onPress={onRecheck}
150
+ disabled={rechecking}
151
+ size="sm"
152
+ testID="branch-import-recheck"
153
+ />
154
+ ) : null}
155
+ </Overlay>
156
+ );
157
+ }