@pantheon-systems/create-p1-starter-kit 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.
- package/index.js +5 -0
- package/lib/cli.js +149 -0
- package/lib/copy-template.js +67 -0
- package/lib/install-deps.js +68 -0
- package/lib/messages.js +28 -0
- package/package.json +38 -0
- package/template/.env.example +10 -0
- package/template/README.md +53 -0
- package/template/__tests__/editor-integration.test.ts +53 -0
- package/template/__tests__/remote-datasource-fetchers.test.ts +226 -0
- package/template/app/[...puckPath]/client.tsx +9 -0
- package/template/app/[...puckPath]/page.tsx +131 -0
- package/template/app/collection-nav.tsx +47 -0
- package/template/app/layout.tsx +13 -0
- package/template/app/p1/[[...p1]]/editor-client.tsx +116 -0
- package/template/app/p1/[[...p1]]/page.tsx +19 -0
- package/template/app/p1/[[...p1]]/render-client.tsx +9 -0
- package/template/app/p1/api/[...p1]/route.ts +16 -0
- package/template/app/p1/auth/[...action]/route.ts +9 -0
- package/template/app/p1/merge/merge-client.tsx +635 -0
- package/template/app/p1/merge/merge.css +257 -0
- package/template/app/p1/merge/page.tsx +12 -0
- package/template/app/page.tsx +130 -0
- package/template/app/styles.css +18 -0
- package/template/components/puck/block-padding.ts +2 -0
- package/template/components/puck/button-block.tsx +41 -0
- package/template/components/puck/divider-block.tsx +10 -0
- package/template/components/puck/grid-block.tsx +80 -0
- package/template/components/puck/heading-block.tsx +38 -0
- package/template/components/puck/image-block.tsx +33 -0
- package/template/components/puck/list-block.tsx +72 -0
- package/template/components/puck/paragraph-block.tsx +44 -0
- package/template/components/puck/quote-block.tsx +23 -0
- package/template/components/puck/root.tsx +20 -0
- package/template/components/puck/spacer-block.tsx +19 -0
- package/template/eslint.config.js +161 -0
- package/template/lib/content-publisher.ts +128 -0
- package/template/lib/fetcher-helpers.ts +17 -0
- package/template/lib/monsters-api.ts +125 -0
- package/template/lib/remote-datasource-fetchers.ts +10 -0
- package/template/lib/remote-datasources.ts +154 -0
- package/template/lib/swapi.ts +75 -0
- package/template/next-env.d.ts +6 -0
- package/template/next.config.mjs +13 -0
- package/template/package.json +42 -0
- package/template/postcss.config.mjs +8 -0
- package/template/public/sw.js +8 -0
- package/template/puck.config.tsx +51 -0
- package/template/tsconfig/base.json +20 -0
- package/template/tsconfig/nextjs.json +21 -0
- package/template/tsconfig.json +16 -0
- package/template/vitest.config.ts +7 -0
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback, useMemo } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import {
|
|
6
|
+
P1App,
|
|
7
|
+
createNextConfig,
|
|
8
|
+
useP1Auth,
|
|
9
|
+
DocumentDiffList,
|
|
10
|
+
PuckFieldResolutionPanel,
|
|
11
|
+
MergePreviewPanel,
|
|
12
|
+
MergePreviewRenderer,
|
|
13
|
+
ViewModeSelector,
|
|
14
|
+
createBranchDocumentComparison,
|
|
15
|
+
diffPuckDataWithPositions,
|
|
16
|
+
isPuckData,
|
|
17
|
+
} from "@pantheon-systems/puck-css";
|
|
18
|
+
import type {
|
|
19
|
+
BranchDocumentComparison,
|
|
20
|
+
DocumentDiffSummary,
|
|
21
|
+
ViewMode,
|
|
22
|
+
PuckData,
|
|
23
|
+
Branch,
|
|
24
|
+
} from "@pantheon-systems/puck-css";
|
|
25
|
+
|
|
26
|
+
import "@pantheon-systems/puck-css/styles.css";
|
|
27
|
+
import "@pantheon-systems/puck-css/pds/styles.css";
|
|
28
|
+
import "./merge.css";
|
|
29
|
+
|
|
30
|
+
import puckConfig from "../../../puck.config";
|
|
31
|
+
|
|
32
|
+
const p1Config = createNextConfig();
|
|
33
|
+
|
|
34
|
+
export function MergeReviewClient() {
|
|
35
|
+
return (
|
|
36
|
+
<P1App
|
|
37
|
+
config={p1Config}
|
|
38
|
+
loginPageProps={{
|
|
39
|
+
title: "P1 Starter",
|
|
40
|
+
subtitle: "Sign in to review merges",
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
<MergeReviewContent />
|
|
44
|
+
</P1App>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface MergePreviewResponse {
|
|
49
|
+
canMerge: boolean;
|
|
50
|
+
hasConflicts: boolean;
|
|
51
|
+
conflicts: {
|
|
52
|
+
documentConflicts: {
|
|
53
|
+
documentId: string;
|
|
54
|
+
documentPath: string;
|
|
55
|
+
conflictType: string;
|
|
56
|
+
sourceVersion?: number;
|
|
57
|
+
targetVersion?: number;
|
|
58
|
+
}[];
|
|
59
|
+
};
|
|
60
|
+
sourceChanges: {
|
|
61
|
+
documentId: string;
|
|
62
|
+
documentPath: string;
|
|
63
|
+
latestVersionId: string;
|
|
64
|
+
}[];
|
|
65
|
+
targetChanges: {
|
|
66
|
+
documentId: string;
|
|
67
|
+
documentPath: string;
|
|
68
|
+
latestVersionId: string;
|
|
69
|
+
}[];
|
|
70
|
+
documentDiffs?: {
|
|
71
|
+
documentId: string;
|
|
72
|
+
documentPath: string;
|
|
73
|
+
sourceSnapshot: Record<string, unknown> | null;
|
|
74
|
+
targetSnapshot: Record<string, unknown> | null;
|
|
75
|
+
diffOperations: unknown[];
|
|
76
|
+
}[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type MergeTab =
|
|
80
|
+
| "diff-list"
|
|
81
|
+
| "visual-compare"
|
|
82
|
+
| "merge-preview"
|
|
83
|
+
| "conflict-resolution";
|
|
84
|
+
|
|
85
|
+
function MergeReviewContent() {
|
|
86
|
+
const { getToken } = useP1Auth();
|
|
87
|
+
|
|
88
|
+
const [branches, setBranches] = useState<Branch[]>([]);
|
|
89
|
+
const [sourceBranchId, setSourceBranchId] = useState("");
|
|
90
|
+
const [targetBranchId, setTargetBranchId] = useState("");
|
|
91
|
+
const [preview, setPreview] = useState<MergePreviewResponse | null>(null);
|
|
92
|
+
const [loading, setLoading] = useState(false);
|
|
93
|
+
const [error, setError] = useState<string | null>(null);
|
|
94
|
+
const [activeTab, setActiveTab] = useState<MergeTab>("diff-list");
|
|
95
|
+
const [selectedDocId, setSelectedDocId] = useState<string | null>(null);
|
|
96
|
+
const [viewMode, setViewMode] = useState<ViewMode>("side-by-side");
|
|
97
|
+
|
|
98
|
+
const apiFetch = useCallback(
|
|
99
|
+
async <T,>(path: string, options?: RequestInit): Promise<T> => {
|
|
100
|
+
const token = await getToken();
|
|
101
|
+
const res = await fetch(`${p1Config.baseUrl}${path}`, {
|
|
102
|
+
...options,
|
|
103
|
+
headers: {
|
|
104
|
+
"Content-Type": "application/json",
|
|
105
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
106
|
+
...options?.headers,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
if (!res.ok) {
|
|
110
|
+
const body = await res
|
|
111
|
+
.json()
|
|
112
|
+
.catch(() => ({ error: res.statusText }));
|
|
113
|
+
throw new Error(
|
|
114
|
+
(body as { error?: string }).error || `API error: ${res.status}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return res.json() as Promise<T>;
|
|
118
|
+
},
|
|
119
|
+
[getToken],
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!p1Config.siteId) return;
|
|
124
|
+
apiFetch<{ branches: Branch[] }>(
|
|
125
|
+
`/api/sites/${p1Config.siteId}/branches`,
|
|
126
|
+
)
|
|
127
|
+
.then((res) => {
|
|
128
|
+
setBranches(res.branches ?? []);
|
|
129
|
+
const main = res.branches?.find(
|
|
130
|
+
(b) => b.isMain || b.name === "main",
|
|
131
|
+
);
|
|
132
|
+
if (main) {
|
|
133
|
+
setTargetBranchId(main.id);
|
|
134
|
+
const other = res.branches?.find((b) => b.id !== main.id);
|
|
135
|
+
if (other) {
|
|
136
|
+
setSourceBranchId(other.id);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
.catch((err: unknown) =>
|
|
141
|
+
setError(err instanceof Error ? err.message : String(err)),
|
|
142
|
+
);
|
|
143
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
144
|
+
}, []);
|
|
145
|
+
|
|
146
|
+
const sourceBranch = branches.find((b) => b.id === sourceBranchId);
|
|
147
|
+
const sourceName = sourceBranch?.name ?? "Draft";
|
|
148
|
+
const targetName = "Live";
|
|
149
|
+
|
|
150
|
+
const fetchPreview = useCallback(async () => {
|
|
151
|
+
if (
|
|
152
|
+
!sourceBranchId ||
|
|
153
|
+
!targetBranchId ||
|
|
154
|
+
sourceBranchId === targetBranchId
|
|
155
|
+
)
|
|
156
|
+
return;
|
|
157
|
+
setLoading(true);
|
|
158
|
+
setError(null);
|
|
159
|
+
setPreview(null);
|
|
160
|
+
try {
|
|
161
|
+
const result = await apiFetch<MergePreviewResponse>(
|
|
162
|
+
`/api/sites/${p1Config.siteId}/merge/preview`,
|
|
163
|
+
{
|
|
164
|
+
method: "POST",
|
|
165
|
+
body: JSON.stringify({
|
|
166
|
+
sourceBranchId,
|
|
167
|
+
targetBranchId,
|
|
168
|
+
includeContent: true,
|
|
169
|
+
}),
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
setPreview(result);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
175
|
+
} finally {
|
|
176
|
+
setLoading(false);
|
|
177
|
+
}
|
|
178
|
+
}, [sourceBranchId, targetBranchId, apiFetch]);
|
|
179
|
+
|
|
180
|
+
const documentComparisons = useMemo((): BranchDocumentComparison[] => {
|
|
181
|
+
if (!preview?.documentDiffs) return [];
|
|
182
|
+
return preview.documentDiffs
|
|
183
|
+
.filter((d) => d.sourceSnapshot || d.targetSnapshot)
|
|
184
|
+
.map((d) => {
|
|
185
|
+
const src = d.sourceSnapshot as unknown as PuckData | null;
|
|
186
|
+
const tgt = d.targetSnapshot as unknown as PuckData | null;
|
|
187
|
+
if (src && tgt && isPuckData(src) && isPuckData(tgt)) {
|
|
188
|
+
return createBranchDocumentComparison(
|
|
189
|
+
d.documentId,
|
|
190
|
+
d.documentPath,
|
|
191
|
+
src,
|
|
192
|
+
tgt,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
documentId: d.documentId,
|
|
197
|
+
documentPath: d.documentPath,
|
|
198
|
+
isPuckData: false,
|
|
199
|
+
diffs: [],
|
|
200
|
+
counts: {
|
|
201
|
+
added: 0,
|
|
202
|
+
removed: 0,
|
|
203
|
+
modified: d.diffOperations.length,
|
|
204
|
+
unchanged: 0,
|
|
205
|
+
},
|
|
206
|
+
} satisfies BranchDocumentComparison;
|
|
207
|
+
});
|
|
208
|
+
}, [preview]);
|
|
209
|
+
|
|
210
|
+
const mergePreviewDocs = useMemo((): DocumentDiffSummary[] => {
|
|
211
|
+
if (!preview?.documentDiffs) return [];
|
|
212
|
+
return preview.documentDiffs.map((d) => ({
|
|
213
|
+
documentId: d.documentId,
|
|
214
|
+
documentPath: d.documentPath,
|
|
215
|
+
sourceSnapshot: d.sourceSnapshot,
|
|
216
|
+
targetSnapshot: d.targetSnapshot,
|
|
217
|
+
}));
|
|
218
|
+
}, [preview]);
|
|
219
|
+
|
|
220
|
+
const selectedDoc = preview?.documentDiffs?.find(
|
|
221
|
+
(d) => d.documentId === selectedDocId,
|
|
222
|
+
);
|
|
223
|
+
const selectedDiffs = useMemo(() => {
|
|
224
|
+
if (!selectedDoc?.sourceSnapshot || !selectedDoc?.targetSnapshot) return [];
|
|
225
|
+
const src = selectedDoc.sourceSnapshot as unknown as PuckData;
|
|
226
|
+
const tgt = selectedDoc.targetSnapshot as unknown as PuckData;
|
|
227
|
+
if (isPuckData(src) && isPuckData(tgt)) {
|
|
228
|
+
return diffPuckDataWithPositions(tgt, src);
|
|
229
|
+
}
|
|
230
|
+
return [];
|
|
231
|
+
}, [selectedDoc]);
|
|
232
|
+
|
|
233
|
+
const conflictDoc = useMemo(() => {
|
|
234
|
+
if (!preview) return null;
|
|
235
|
+
const conflict = preview.conflicts.documentConflicts[0];
|
|
236
|
+
if (!conflict) return null;
|
|
237
|
+
const diff = preview.documentDiffs?.find(
|
|
238
|
+
(d) => d.documentId === conflict.documentId,
|
|
239
|
+
);
|
|
240
|
+
if (!diff?.sourceSnapshot || !diff?.targetSnapshot) return null;
|
|
241
|
+
const src = diff.sourceSnapshot as unknown as PuckData;
|
|
242
|
+
const tgt = diff.targetSnapshot as unknown as PuckData;
|
|
243
|
+
if (!isPuckData(src) || !isPuckData(tgt)) return null;
|
|
244
|
+
return { conflict, source: src, target: tgt, path: diff.documentPath };
|
|
245
|
+
}, [preview]);
|
|
246
|
+
|
|
247
|
+
if (!p1Config.siteId) {
|
|
248
|
+
return (
|
|
249
|
+
<div style={styles.page}>
|
|
250
|
+
<p>Set NEXT_PUBLIC_CSS_SITE_ID to use merge review.</p>
|
|
251
|
+
<Link href="/p1" style={styles.backLink}>
|
|
252
|
+
Back to editor
|
|
253
|
+
</Link>
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<div style={styles.page}>
|
|
260
|
+
<header style={styles.header}>
|
|
261
|
+
<div style={styles.headerLeft}>
|
|
262
|
+
<Link href="/p1" style={styles.backLink}>
|
|
263
|
+
← Editor
|
|
264
|
+
</Link>
|
|
265
|
+
<h1 style={styles.title}>Merge review</h1>
|
|
266
|
+
</div>
|
|
267
|
+
</header>
|
|
268
|
+
|
|
269
|
+
{/* Branch Selectors */}
|
|
270
|
+
<div style={styles.branchSelectors}>
|
|
271
|
+
<div style={styles.branchField}>
|
|
272
|
+
<label style={styles.label}>Draft</label>
|
|
273
|
+
<select
|
|
274
|
+
value={sourceBranchId}
|
|
275
|
+
onChange={(e) => setSourceBranchId(e.target.value)}
|
|
276
|
+
style={styles.select}
|
|
277
|
+
>
|
|
278
|
+
<option value="">Select Draft</option>
|
|
279
|
+
{branches
|
|
280
|
+
.filter((b) => !b.isMain && b.name !== "main")
|
|
281
|
+
.map((b) => (
|
|
282
|
+
<option key={b.id} value={b.id}>
|
|
283
|
+
{b.name}
|
|
284
|
+
</option>
|
|
285
|
+
))}
|
|
286
|
+
</select>
|
|
287
|
+
</div>
|
|
288
|
+
<span style={styles.arrow}>→</span>
|
|
289
|
+
<div style={styles.branchField}>
|
|
290
|
+
<label style={styles.label}>Live</label>
|
|
291
|
+
<div
|
|
292
|
+
style={{
|
|
293
|
+
...styles.select,
|
|
294
|
+
display: "flex",
|
|
295
|
+
alignItems: "center",
|
|
296
|
+
background: "#f5f5f5",
|
|
297
|
+
color: "#666",
|
|
298
|
+
}}
|
|
299
|
+
>
|
|
300
|
+
Live
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
<button
|
|
304
|
+
onClick={fetchPreview}
|
|
305
|
+
disabled={
|
|
306
|
+
loading ||
|
|
307
|
+
!sourceBranchId ||
|
|
308
|
+
!targetBranchId ||
|
|
309
|
+
sourceBranchId === targetBranchId
|
|
310
|
+
}
|
|
311
|
+
style={styles.compareBtn}
|
|
312
|
+
>
|
|
313
|
+
{loading ? "Loading..." : "Compare Draft to Live"}
|
|
314
|
+
</button>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
{error && <div style={styles.error}>{error}</div>}
|
|
318
|
+
|
|
319
|
+
{preview && (
|
|
320
|
+
<>
|
|
321
|
+
{/* Status Banner */}
|
|
322
|
+
<div
|
|
323
|
+
style={{
|
|
324
|
+
...styles.statusBanner,
|
|
325
|
+
backgroundColor: preview.hasConflicts ? "#fff3cd" : "#d4edda",
|
|
326
|
+
borderColor: preview.hasConflicts ? "#ffc107" : "#28a745",
|
|
327
|
+
}}
|
|
328
|
+
>
|
|
329
|
+
{preview.hasConflicts ? (
|
|
330
|
+
<span>
|
|
331
|
+
{preview.conflicts.documentConflicts.length} conflict(s)
|
|
332
|
+
detected between <strong>{sourceName}</strong> and{" "}
|
|
333
|
+
<strong>{targetName}</strong>
|
|
334
|
+
</span>
|
|
335
|
+
) : (
|
|
336
|
+
<span>
|
|
337
|
+
No conflicts. {sourceName} can be merged into {targetName}.
|
|
338
|
+
</span>
|
|
339
|
+
)}
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
{/* Tab Navigation */}
|
|
343
|
+
<div style={styles.tabs}>
|
|
344
|
+
{(
|
|
345
|
+
[
|
|
346
|
+
"diff-list",
|
|
347
|
+
"visual-compare",
|
|
348
|
+
"merge-preview",
|
|
349
|
+
"conflict-resolution",
|
|
350
|
+
] as MergeTab[]
|
|
351
|
+
).map((tab) => (
|
|
352
|
+
<button
|
|
353
|
+
key={tab}
|
|
354
|
+
onClick={() => setActiveTab(tab)}
|
|
355
|
+
style={{
|
|
356
|
+
...styles.tab,
|
|
357
|
+
...(activeTab === tab ? styles.activeTab : {}),
|
|
358
|
+
}}
|
|
359
|
+
disabled={tab === "conflict-resolution" && !conflictDoc}
|
|
360
|
+
>
|
|
361
|
+
{tab === "diff-list" && "Document diffs"}
|
|
362
|
+
{tab === "visual-compare" && "Visual compare"}
|
|
363
|
+
{tab === "merge-preview" && "Merge preview"}
|
|
364
|
+
{tab === "conflict-resolution" &&
|
|
365
|
+
`Conflict resolution${!conflictDoc ? " (no conflicts)" : ""}`}
|
|
366
|
+
</button>
|
|
367
|
+
))}
|
|
368
|
+
</div>
|
|
369
|
+
|
|
370
|
+
{/* Tab Content */}
|
|
371
|
+
<div style={styles.content}>
|
|
372
|
+
{activeTab === "diff-list" && (
|
|
373
|
+
<DocumentDiffList
|
|
374
|
+
documents={documentComparisons}
|
|
375
|
+
sourceBranchName={sourceName}
|
|
376
|
+
targetBranchName={targetName}
|
|
377
|
+
/>
|
|
378
|
+
)}
|
|
379
|
+
|
|
380
|
+
{activeTab === "visual-compare" && (
|
|
381
|
+
<div>
|
|
382
|
+
{!selectedDocId && (
|
|
383
|
+
<div style={styles.selectPrompt}>
|
|
384
|
+
Select a document to compare visually:
|
|
385
|
+
<div style={styles.docList}>
|
|
386
|
+
{(preview.documentDiffs ?? []).map((d) => (
|
|
387
|
+
<button
|
|
388
|
+
key={d.documentId}
|
|
389
|
+
onClick={() => setSelectedDocId(d.documentId)}
|
|
390
|
+
style={styles.docBtn}
|
|
391
|
+
>
|
|
392
|
+
{d.documentPath}
|
|
393
|
+
<span style={styles.changeCount}>
|
|
394
|
+
{d.diffOperations.length} change(s)
|
|
395
|
+
</span>
|
|
396
|
+
</button>
|
|
397
|
+
))}
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
)}
|
|
401
|
+
{selectedDocId &&
|
|
402
|
+
selectedDoc?.sourceSnapshot &&
|
|
403
|
+
selectedDoc?.targetSnapshot && (
|
|
404
|
+
<div>
|
|
405
|
+
<div style={styles.visualCompareToolbar}>
|
|
406
|
+
<button
|
|
407
|
+
onClick={() => setSelectedDocId(null)}
|
|
408
|
+
style={styles.backBtn}
|
|
409
|
+
>
|
|
410
|
+
← Back to documents
|
|
411
|
+
</button>
|
|
412
|
+
<ViewModeSelector
|
|
413
|
+
viewMode={viewMode}
|
|
414
|
+
onViewModeChange={setViewMode}
|
|
415
|
+
/>
|
|
416
|
+
</div>
|
|
417
|
+
<MergePreviewRenderer
|
|
418
|
+
sourceData={
|
|
419
|
+
selectedDoc.sourceSnapshot as unknown as PuckData
|
|
420
|
+
}
|
|
421
|
+
targetData={
|
|
422
|
+
selectedDoc.targetSnapshot as unknown as PuckData
|
|
423
|
+
}
|
|
424
|
+
diffs={selectedDiffs}
|
|
425
|
+
config={puckConfig}
|
|
426
|
+
viewMode={viewMode}
|
|
427
|
+
sourceBranchName={sourceName}
|
|
428
|
+
targetBranchName={targetName}
|
|
429
|
+
/>
|
|
430
|
+
</div>
|
|
431
|
+
)}
|
|
432
|
+
</div>
|
|
433
|
+
)}
|
|
434
|
+
|
|
435
|
+
{activeTab === "merge-preview" && (
|
|
436
|
+
<MergePreviewPanel
|
|
437
|
+
documents={mergePreviewDocs}
|
|
438
|
+
sourceBranchName={sourceName}
|
|
439
|
+
targetBranchName={targetName}
|
|
440
|
+
config={puckConfig}
|
|
441
|
+
onDocumentSelect={(docId: string) => {
|
|
442
|
+
setSelectedDocId(docId);
|
|
443
|
+
setActiveTab("visual-compare");
|
|
444
|
+
}}
|
|
445
|
+
/>
|
|
446
|
+
)}
|
|
447
|
+
|
|
448
|
+
{activeTab === "conflict-resolution" && conflictDoc && (
|
|
449
|
+
<div>
|
|
450
|
+
<h3 style={styles.sectionTitle}>
|
|
451
|
+
Resolving: {conflictDoc.path}
|
|
452
|
+
</h3>
|
|
453
|
+
<PuckFieldResolutionPanel
|
|
454
|
+
sourceSnapshot={conflictDoc.source}
|
|
455
|
+
targetSnapshot={conflictDoc.target}
|
|
456
|
+
baseSnapshot={null}
|
|
457
|
+
sourceBranchName={sourceName}
|
|
458
|
+
targetBranchName={targetName}
|
|
459
|
+
// TODO: Wire up to the merge API — currently a stub that logs the resolved snapshot.
|
|
460
|
+
onResolve={(merged: PuckData) => {
|
|
461
|
+
console.log("Resolved merge snapshot:", merged);
|
|
462
|
+
alert(
|
|
463
|
+
"Resolution applied (logged to console). In production, this would submit via the merge API.",
|
|
464
|
+
);
|
|
465
|
+
}}
|
|
466
|
+
/>
|
|
467
|
+
</div>
|
|
468
|
+
)}
|
|
469
|
+
</div>
|
|
470
|
+
</>
|
|
471
|
+
)}
|
|
472
|
+
</div>
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const styles: Record<string, React.CSSProperties> = {
|
|
477
|
+
page: {
|
|
478
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
479
|
+
maxWidth: "1200px",
|
|
480
|
+
margin: "0 auto",
|
|
481
|
+
padding: "24px",
|
|
482
|
+
},
|
|
483
|
+
header: {
|
|
484
|
+
display: "flex",
|
|
485
|
+
justifyContent: "space-between",
|
|
486
|
+
alignItems: "center",
|
|
487
|
+
marginBottom: "24px",
|
|
488
|
+
},
|
|
489
|
+
headerLeft: {
|
|
490
|
+
display: "flex",
|
|
491
|
+
alignItems: "center",
|
|
492
|
+
gap: "16px",
|
|
493
|
+
},
|
|
494
|
+
backLink: {
|
|
495
|
+
color: "#0066cc",
|
|
496
|
+
textDecoration: "none",
|
|
497
|
+
fontSize: "14px",
|
|
498
|
+
},
|
|
499
|
+
title: {
|
|
500
|
+
fontSize: "24px",
|
|
501
|
+
fontWeight: 600,
|
|
502
|
+
margin: 0,
|
|
503
|
+
},
|
|
504
|
+
branchSelectors: {
|
|
505
|
+
display: "flex",
|
|
506
|
+
alignItems: "flex-end",
|
|
507
|
+
gap: "12px",
|
|
508
|
+
marginBottom: "24px",
|
|
509
|
+
flexWrap: "wrap" as const,
|
|
510
|
+
},
|
|
511
|
+
branchField: {
|
|
512
|
+
display: "flex",
|
|
513
|
+
flexDirection: "column" as const,
|
|
514
|
+
gap: "4px",
|
|
515
|
+
},
|
|
516
|
+
label: {
|
|
517
|
+
fontSize: "12px",
|
|
518
|
+
fontWeight: 500,
|
|
519
|
+
color: "#666",
|
|
520
|
+
textTransform: "uppercase" as const,
|
|
521
|
+
letterSpacing: "0.5px",
|
|
522
|
+
},
|
|
523
|
+
select: {
|
|
524
|
+
padding: "8px 12px",
|
|
525
|
+
borderRadius: "6px",
|
|
526
|
+
border: "1px solid #ccc",
|
|
527
|
+
fontSize: "14px",
|
|
528
|
+
minWidth: "200px",
|
|
529
|
+
},
|
|
530
|
+
arrow: {
|
|
531
|
+
fontSize: "20px",
|
|
532
|
+
color: "#666",
|
|
533
|
+
paddingBottom: "6px",
|
|
534
|
+
},
|
|
535
|
+
compareBtn: {
|
|
536
|
+
padding: "8px 20px",
|
|
537
|
+
borderRadius: "6px",
|
|
538
|
+
border: "none",
|
|
539
|
+
background: "#0066cc",
|
|
540
|
+
color: "white",
|
|
541
|
+
cursor: "pointer",
|
|
542
|
+
fontSize: "14px",
|
|
543
|
+
fontWeight: 500,
|
|
544
|
+
},
|
|
545
|
+
error: {
|
|
546
|
+
background: "#fde8e8",
|
|
547
|
+
color: "#c53030",
|
|
548
|
+
padding: "12px 16px",
|
|
549
|
+
borderRadius: "6px",
|
|
550
|
+
marginBottom: "16px",
|
|
551
|
+
},
|
|
552
|
+
statusBanner: {
|
|
553
|
+
padding: "12px 16px",
|
|
554
|
+
borderRadius: "6px",
|
|
555
|
+
border: "1px solid",
|
|
556
|
+
marginBottom: "16px",
|
|
557
|
+
fontSize: "14px",
|
|
558
|
+
},
|
|
559
|
+
tabs: {
|
|
560
|
+
display: "flex",
|
|
561
|
+
gap: "0",
|
|
562
|
+
borderBottom: "2px solid #e5e7eb",
|
|
563
|
+
marginBottom: "24px",
|
|
564
|
+
},
|
|
565
|
+
tab: {
|
|
566
|
+
padding: "10px 20px",
|
|
567
|
+
border: "none",
|
|
568
|
+
borderBottom: "2px solid transparent",
|
|
569
|
+
background: "none",
|
|
570
|
+
cursor: "pointer",
|
|
571
|
+
fontSize: "14px",
|
|
572
|
+
fontWeight: 500,
|
|
573
|
+
color: "#666",
|
|
574
|
+
marginBottom: "-2px",
|
|
575
|
+
},
|
|
576
|
+
activeTab: {
|
|
577
|
+
color: "#0066cc",
|
|
578
|
+
borderBottomColor: "#0066cc",
|
|
579
|
+
},
|
|
580
|
+
content: {
|
|
581
|
+
minHeight: "400px",
|
|
582
|
+
},
|
|
583
|
+
selectPrompt: {
|
|
584
|
+
textAlign: "center" as const,
|
|
585
|
+
padding: "40px 20px",
|
|
586
|
+
color: "#666",
|
|
587
|
+
},
|
|
588
|
+
docList: {
|
|
589
|
+
display: "flex",
|
|
590
|
+
flexDirection: "column" as const,
|
|
591
|
+
gap: "8px",
|
|
592
|
+
marginTop: "16px",
|
|
593
|
+
alignItems: "center",
|
|
594
|
+
},
|
|
595
|
+
docBtn: {
|
|
596
|
+
padding: "10px 20px",
|
|
597
|
+
border: "1px solid #ddd",
|
|
598
|
+
borderRadius: "6px",
|
|
599
|
+
background: "white",
|
|
600
|
+
cursor: "pointer",
|
|
601
|
+
fontSize: "14px",
|
|
602
|
+
display: "flex",
|
|
603
|
+
alignItems: "center",
|
|
604
|
+
gap: "12px",
|
|
605
|
+
},
|
|
606
|
+
changeCount: {
|
|
607
|
+
fontSize: "12px",
|
|
608
|
+
color: "#666",
|
|
609
|
+
background: "#f0f0f0",
|
|
610
|
+
padding: "2px 8px",
|
|
611
|
+
borderRadius: "10px",
|
|
612
|
+
},
|
|
613
|
+
sectionTitle: {
|
|
614
|
+
fontSize: "16px",
|
|
615
|
+
fontWeight: 500,
|
|
616
|
+
marginBottom: "16px",
|
|
617
|
+
},
|
|
618
|
+
visualCompareToolbar: {
|
|
619
|
+
display: "flex",
|
|
620
|
+
justifyContent: "space-between",
|
|
621
|
+
alignItems: "center",
|
|
622
|
+
marginBottom: "16px",
|
|
623
|
+
padding: "8px 0",
|
|
624
|
+
borderBottom: "1px solid #e5e7eb",
|
|
625
|
+
},
|
|
626
|
+
backBtn: {
|
|
627
|
+
padding: "6px 14px",
|
|
628
|
+
border: "1px solid #ddd",
|
|
629
|
+
borderRadius: "6px",
|
|
630
|
+
background: "white",
|
|
631
|
+
cursor: "pointer",
|
|
632
|
+
fontSize: "13px",
|
|
633
|
+
color: "#333",
|
|
634
|
+
},
|
|
635
|
+
};
|