@imjp/writenex-astro 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/README.md +539 -0
- package/dist/chunk-5PM6EQE5.js +151 -0
- package/dist/chunk-5PM6EQE5.js.map +1 -0
- package/dist/chunk-7XU5X6CW.js +1331 -0
- package/dist/chunk-7XU5X6CW.js.map +1 -0
- package/dist/chunk-AAOQHQPU.js +574 -0
- package/dist/chunk-AAOQHQPU.js.map +1 -0
- package/dist/chunk-CF2XXJFF.js +1410 -0
- package/dist/chunk-CF2XXJFF.js.map +1 -0
- package/dist/chunk-CRPZUUDU.js +52 -0
- package/dist/chunk-CRPZUUDU.js.map +1 -0
- package/dist/chunk-CYLDJ3HZ.js +310 -0
- package/dist/chunk-CYLDJ3HZ.js.map +1 -0
- package/dist/chunk-KIKIPIFA.js +1 -0
- package/dist/chunk-KIKIPIFA.js.map +1 -0
- package/dist/chunk-XNTQTTJU.js +145 -0
- package/dist/chunk-XNTQTTJU.js.map +1 -0
- package/dist/client/index.css +2 -0
- package/dist/client/index.css.map +1 -0
- package/dist/client/index.js +375 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/styles.css +584 -0
- package/dist/client/variables.css +304 -0
- package/dist/config/index.d.ts +54 -0
- package/dist/config/index.js +38 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config-BmEdBDo_.d.ts +220 -0
- package/dist/content-BWR52vD-.d.ts +64 -0
- package/dist/discovery/index.d.ts +310 -0
- package/dist/discovery/index.js +38 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/errors-C0iYiDTv.d.ts +107 -0
- package/dist/filesystem/index.d.ts +1292 -0
- package/dist/filesystem/index.js +203 -0
- package/dist/filesystem/index.js.map +1 -0
- package/dist/image-FP7w5ZIs.d.ts +47 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +151 -0
- package/dist/index.js.map +1 -0
- package/dist/loader-55LWCXHA.js +12 -0
- package/dist/loader-55LWCXHA.js.map +1 -0
- package/dist/loader-CrdnaAWR.d.ts +327 -0
- package/dist/server/index.d.ts +357 -0
- package/dist/server/index.js +37 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +94 -0
- package/src/client/App.tsx +900 -0
- package/src/client/components/ConfigPanel/ConfigPanel.css +553 -0
- package/src/client/components/ConfigPanel/ConfigPanel.tsx +396 -0
- package/src/client/components/ConfigPanel/index.ts +6 -0
- package/src/client/components/CreateContentModal/CreateContentModal.css +327 -0
- package/src/client/components/CreateContentModal/CreateContentModal.tsx +216 -0
- package/src/client/components/CreateContentModal/index.ts +7 -0
- package/src/client/components/Editor/Editor.css +885 -0
- package/src/client/components/Editor/Editor.tsx +484 -0
- package/src/client/components/Editor/ImageDialog.css +344 -0
- package/src/client/components/Editor/ImageDialog.tsx +367 -0
- package/src/client/components/Editor/LinkDialog.css +326 -0
- package/src/client/components/Editor/LinkDialog.tsx +332 -0
- package/src/client/components/Editor/index.ts +6 -0
- package/src/client/components/FrontmatterForm/FrontmatterForm.css +468 -0
- package/src/client/components/FrontmatterForm/FrontmatterForm.tsx +914 -0
- package/src/client/components/FrontmatterForm/index.ts +7 -0
- package/src/client/components/Header/Header.css +300 -0
- package/src/client/components/Header/Header.tsx +300 -0
- package/src/client/components/Header/index.ts +7 -0
- package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.css +239 -0
- package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.tsx +151 -0
- package/src/client/components/KeyboardShortcuts/index.ts +6 -0
- package/src/client/components/LazyEditor.tsx +75 -0
- package/src/client/components/LiveRegion/LiveRegion.css +19 -0
- package/src/client/components/LiveRegion/LiveRegion.tsx +60 -0
- package/src/client/components/LiveRegion/index.ts +7 -0
- package/src/client/components/SearchReplace/SearchReplacePanel.css +300 -0
- package/src/client/components/SearchReplace/SearchReplacePanel.tsx +332 -0
- package/src/client/components/SearchReplace/index.ts +7 -0
- package/src/client/components/SelectCollectionModal/SelectCollectionModal.css +308 -0
- package/src/client/components/SelectCollectionModal/SelectCollectionModal.tsx +223 -0
- package/src/client/components/SelectCollectionModal/index.ts +7 -0
- package/src/client/components/Sidebar/Sidebar.css +570 -0
- package/src/client/components/Sidebar/Sidebar.tsx +617 -0
- package/src/client/components/Sidebar/index.ts +7 -0
- package/src/client/components/SkipLink/SkipLink.css +51 -0
- package/src/client/components/SkipLink/SkipLink.tsx +67 -0
- package/src/client/components/SkipLink/index.ts +7 -0
- package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.css +233 -0
- package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.tsx +160 -0
- package/src/client/components/UnsavedChangesModal/index.ts +1 -0
- package/src/client/components/VersionHistory/DiffViewer.css +430 -0
- package/src/client/components/VersionHistory/DiffViewer.tsx +383 -0
- package/src/client/components/VersionHistory/VersionActions.css +318 -0
- package/src/client/components/VersionHistory/VersionActions.tsx +277 -0
- package/src/client/components/VersionHistory/VersionHistoryPanel.css +369 -0
- package/src/client/components/VersionHistory/VersionHistoryPanel.tsx +469 -0
- package/src/client/components/VersionHistory/index.ts +9 -0
- package/src/client/context/ApiContext.tsx +154 -0
- package/src/client/context/ThemeContext.tsx +172 -0
- package/src/client/hooks/useAnnounce.ts +201 -0
- package/src/client/hooks/useApi.ts +374 -0
- package/src/client/hooks/useArrowNavigation.ts +286 -0
- package/src/client/hooks/useAutosave.ts +241 -0
- package/src/client/hooks/useFocusTrap.ts +178 -0
- package/src/client/hooks/useKeyboardShortcuts.ts +203 -0
- package/src/client/hooks/useSearch.ts +206 -0
- package/src/client/hooks/useVersionHistory.ts +451 -0
- package/src/client/index.tsx +70 -0
- package/src/client/styles.css +584 -0
- package/src/client/utils/focus.ts +57 -0
- package/src/client/utils/openInEditor.ts +130 -0
- package/src/client/variables.css +304 -0
- package/src/config/defaults.ts +109 -0
- package/src/config/index.ts +32 -0
- package/src/config/loader.ts +174 -0
- package/src/config/schema.ts +161 -0
- package/src/core/constants.ts +39 -0
- package/src/core/errors.ts +739 -0
- package/src/core/index.ts +11 -0
- package/src/discovery/collections.ts +216 -0
- package/src/discovery/index.ts +33 -0
- package/src/discovery/patterns.ts +702 -0
- package/src/discovery/schema.ts +453 -0
- package/src/filesystem/images.ts +798 -0
- package/src/filesystem/index.ts +107 -0
- package/src/filesystem/reader.ts +452 -0
- package/src/filesystem/version-config.ts +390 -0
- package/src/filesystem/versions.ts +1339 -0
- package/src/filesystem/watcher.ts +226 -0
- package/src/filesystem/writer.ts +540 -0
- package/src/index.ts +61 -0
- package/src/integration.ts +228 -0
- package/src/server/assets.ts +254 -0
- package/src/server/cache.ts +355 -0
- package/src/server/index.ts +33 -0
- package/src/server/middleware.ts +209 -0
- package/src/server/routes.ts +1428 -0
- package/src/types/api.ts +61 -0
- package/src/types/config.ts +134 -0
- package/src/types/content.ts +64 -0
- package/src/types/image.ts +48 -0
- package/src/types/index.ts +58 -0
- package/src/types/version.ts +117 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Version History Panel component
|
|
3
|
+
*
|
|
4
|
+
* Slide-in panel for viewing and managing content version history.
|
|
5
|
+
* Displays version list with timestamps, previews, and action buttons.
|
|
6
|
+
*
|
|
7
|
+
* @module @writenex/astro/client/components/VersionHistory/VersionHistoryPanel
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useEffect, useState, useRef } from "react";
|
|
11
|
+
import {
|
|
12
|
+
X,
|
|
13
|
+
History,
|
|
14
|
+
Clock,
|
|
15
|
+
Tag,
|
|
16
|
+
RefreshCw,
|
|
17
|
+
Loader2,
|
|
18
|
+
Trash2,
|
|
19
|
+
AlertTriangle,
|
|
20
|
+
} from "lucide-react";
|
|
21
|
+
import type { VersionEntry } from "../../../types";
|
|
22
|
+
import {
|
|
23
|
+
useVersionHistory,
|
|
24
|
+
type DiffData,
|
|
25
|
+
} from "../../hooks/useVersionHistory";
|
|
26
|
+
import { useSharedVersionApi } from "../../context/ApiContext";
|
|
27
|
+
import { useFocusTrap } from "../../hooks/useFocusTrap";
|
|
28
|
+
import { VersionActions } from "./VersionActions";
|
|
29
|
+
import { DiffViewer } from "./DiffViewer";
|
|
30
|
+
import "./VersionHistoryPanel.css";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Props for the VersionHistoryPanel component
|
|
34
|
+
*/
|
|
35
|
+
interface VersionHistoryPanelProps {
|
|
36
|
+
/** Whether the panel is open */
|
|
37
|
+
isOpen: boolean;
|
|
38
|
+
/** Callback to close the panel */
|
|
39
|
+
onClose: () => void;
|
|
40
|
+
/** Collection name */
|
|
41
|
+
collection: string | null;
|
|
42
|
+
/** Content ID (slug) */
|
|
43
|
+
contentId: string | null;
|
|
44
|
+
/** Current content body for comparison (reserved for future use) */
|
|
45
|
+
currentContent: string;
|
|
46
|
+
/** Callback when content is restored */
|
|
47
|
+
onRestore: (content: string) => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Format timestamp for display
|
|
52
|
+
*/
|
|
53
|
+
function formatTimestamp(timestamp: string): string {
|
|
54
|
+
const date = new Date(timestamp);
|
|
55
|
+
const now = new Date();
|
|
56
|
+
const diffMs = now.getTime() - date.getTime();
|
|
57
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
58
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
59
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
60
|
+
|
|
61
|
+
if (diffMins < 1) return "Just now";
|
|
62
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
63
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
64
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
65
|
+
|
|
66
|
+
return date.toLocaleDateString(undefined, {
|
|
67
|
+
month: "short",
|
|
68
|
+
day: "numeric",
|
|
69
|
+
hour: "2-digit",
|
|
70
|
+
minute: "2-digit",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Format file size for display
|
|
76
|
+
*/
|
|
77
|
+
function formatSize(bytes: number): string {
|
|
78
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
79
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
80
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Version History Panel component
|
|
85
|
+
*
|
|
86
|
+
* @component
|
|
87
|
+
* @example
|
|
88
|
+
* ```tsx
|
|
89
|
+
* <VersionHistoryPanel
|
|
90
|
+
* isOpen={showVersionHistory}
|
|
91
|
+
* onClose={() => setShowVersionHistory(false)}
|
|
92
|
+
* apiBase={apiBase}
|
|
93
|
+
* collection="blog"
|
|
94
|
+
* contentId="my-post"
|
|
95
|
+
* currentContent={content.body}
|
|
96
|
+
* onRestore={handleRestore}
|
|
97
|
+
* />
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export function VersionHistoryPanel({
|
|
101
|
+
isOpen,
|
|
102
|
+
onClose,
|
|
103
|
+
collection,
|
|
104
|
+
contentId,
|
|
105
|
+
currentContent: _currentContent,
|
|
106
|
+
onRestore,
|
|
107
|
+
}: VersionHistoryPanelProps): React.ReactElement | null {
|
|
108
|
+
const versionApi = useSharedVersionApi();
|
|
109
|
+
const {
|
|
110
|
+
versions,
|
|
111
|
+
loading,
|
|
112
|
+
error,
|
|
113
|
+
refresh,
|
|
114
|
+
restoreVersion,
|
|
115
|
+
deleteVersion,
|
|
116
|
+
clearVersions,
|
|
117
|
+
getDiff,
|
|
118
|
+
} = useVersionHistory(versionApi, collection, contentId);
|
|
119
|
+
|
|
120
|
+
const [selectedVersionId, setSelectedVersionId] = useState<string | null>(
|
|
121
|
+
null
|
|
122
|
+
);
|
|
123
|
+
const [diffData, setDiffData] = useState<DiffData | null>(null);
|
|
124
|
+
const [showDiff, setShowDiff] = useState(false);
|
|
125
|
+
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
|
126
|
+
const [showClearAllConfirm, setShowClearAllConfirm] = useState(false);
|
|
127
|
+
|
|
128
|
+
// Fetch versions when panel opens
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (isOpen && collection && contentId) {
|
|
131
|
+
refresh();
|
|
132
|
+
}
|
|
133
|
+
}, [isOpen, collection, contentId, refresh]);
|
|
134
|
+
|
|
135
|
+
// Reset state when panel closes
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (!isOpen) {
|
|
138
|
+
setSelectedVersionId(null);
|
|
139
|
+
setDiffData(null);
|
|
140
|
+
setShowDiff(false);
|
|
141
|
+
setShowClearAllConfirm(false);
|
|
142
|
+
}
|
|
143
|
+
}, [isOpen]);
|
|
144
|
+
|
|
145
|
+
if (!isOpen) return null;
|
|
146
|
+
|
|
147
|
+
const handleVersionClick = (versionId: string) => {
|
|
148
|
+
setSelectedVersionId(selectedVersionId === versionId ? null : versionId);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const handleRestore = async (versionId: string) => {
|
|
152
|
+
setActionLoading("restore");
|
|
153
|
+
try {
|
|
154
|
+
const content = await restoreVersion(versionId);
|
|
155
|
+
if (content) {
|
|
156
|
+
onRestore(content);
|
|
157
|
+
setSelectedVersionId(null);
|
|
158
|
+
}
|
|
159
|
+
} finally {
|
|
160
|
+
setActionLoading(null);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const handleCompare = async (versionId: string) => {
|
|
165
|
+
setActionLoading("compare");
|
|
166
|
+
try {
|
|
167
|
+
const data = await getDiff(versionId);
|
|
168
|
+
if (data) {
|
|
169
|
+
setDiffData(data);
|
|
170
|
+
setShowDiff(true);
|
|
171
|
+
}
|
|
172
|
+
} finally {
|
|
173
|
+
setActionLoading(null);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const handleDelete = async (versionId: string) => {
|
|
178
|
+
setActionLoading("delete");
|
|
179
|
+
try {
|
|
180
|
+
await deleteVersion(versionId);
|
|
181
|
+
setSelectedVersionId(null);
|
|
182
|
+
} finally {
|
|
183
|
+
setActionLoading(null);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const handleClearAll = async () => {
|
|
188
|
+
setActionLoading("clearAll");
|
|
189
|
+
try {
|
|
190
|
+
await clearVersions();
|
|
191
|
+
setSelectedVersionId(null);
|
|
192
|
+
setShowClearAllConfirm(false);
|
|
193
|
+
} finally {
|
|
194
|
+
setActionLoading(null);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const handleDownload = (version: VersionEntry) => {
|
|
199
|
+
// Create a blob with the version content
|
|
200
|
+
// We need to fetch the full version content first
|
|
201
|
+
getDiff(version.id).then((data) => {
|
|
202
|
+
if (data) {
|
|
203
|
+
const blob = new Blob([data.version.content], {
|
|
204
|
+
type: "text/markdown",
|
|
205
|
+
});
|
|
206
|
+
const url = URL.createObjectURL(blob);
|
|
207
|
+
const a = document.createElement("a");
|
|
208
|
+
a.href = url;
|
|
209
|
+
a.download = `${contentId}-${version.id}.md`;
|
|
210
|
+
document.body.appendChild(a);
|
|
211
|
+
a.click();
|
|
212
|
+
document.body.removeChild(a);
|
|
213
|
+
URL.revokeObjectURL(url);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const selectedVersion = versions.find((v) => v.id === selectedVersionId);
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<>
|
|
222
|
+
<div className="wn-version-panel" aria-label="Version history">
|
|
223
|
+
{/* Header */}
|
|
224
|
+
<div className="wn-version-panel-header">
|
|
225
|
+
<h2 className="wn-version-panel-title">
|
|
226
|
+
<History size={16} />
|
|
227
|
+
Version History
|
|
228
|
+
</h2>
|
|
229
|
+
<div className="wn-version-panel-actions">
|
|
230
|
+
<button
|
|
231
|
+
className="wn-version-panel-btn wn-version-panel-btn--danger"
|
|
232
|
+
onClick={() => setShowClearAllConfirm(true)}
|
|
233
|
+
disabled={loading || versions.length === 0}
|
|
234
|
+
title="Clear all history"
|
|
235
|
+
aria-label="Clear all version history"
|
|
236
|
+
>
|
|
237
|
+
<Trash2 size={14} />
|
|
238
|
+
</button>
|
|
239
|
+
<button
|
|
240
|
+
className="wn-version-panel-btn"
|
|
241
|
+
onClick={() => refresh()}
|
|
242
|
+
disabled={loading}
|
|
243
|
+
title="Refresh"
|
|
244
|
+
aria-label="Refresh version list"
|
|
245
|
+
>
|
|
246
|
+
<RefreshCw size={14} className={loading ? "wn-spin" : ""} />
|
|
247
|
+
</button>
|
|
248
|
+
<button
|
|
249
|
+
className="wn-version-panel-btn"
|
|
250
|
+
onClick={onClose}
|
|
251
|
+
title="Close"
|
|
252
|
+
aria-label="Close version history panel"
|
|
253
|
+
>
|
|
254
|
+
<X size={16} />
|
|
255
|
+
</button>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
{/* Content */}
|
|
260
|
+
<div className="wn-version-panel-content">
|
|
261
|
+
{loading && versions.length === 0 ? (
|
|
262
|
+
<div className="wn-version-panel-loading">
|
|
263
|
+
<Loader2 size={24} className="wn-spin" />
|
|
264
|
+
<span>Loading versions...</span>
|
|
265
|
+
</div>
|
|
266
|
+
) : error ? (
|
|
267
|
+
<div className="wn-version-panel-error">
|
|
268
|
+
<span>{error}</span>
|
|
269
|
+
<button onClick={() => refresh()}>Retry</button>
|
|
270
|
+
</div>
|
|
271
|
+
) : versions.length === 0 ? (
|
|
272
|
+
<div className="wn-version-panel-empty">
|
|
273
|
+
<History size={32} />
|
|
274
|
+
<p>No versions yet</p>
|
|
275
|
+
<span>Versions are created automatically when you save</span>
|
|
276
|
+
</div>
|
|
277
|
+
) : (
|
|
278
|
+
<div className="wn-version-list">
|
|
279
|
+
{versions.map((version) => (
|
|
280
|
+
<VersionItem
|
|
281
|
+
key={version.id}
|
|
282
|
+
version={version}
|
|
283
|
+
isSelected={selectedVersionId === version.id}
|
|
284
|
+
onClick={() => handleVersionClick(version.id)}
|
|
285
|
+
/>
|
|
286
|
+
))}
|
|
287
|
+
</div>
|
|
288
|
+
)}
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
{/* Actions for selected version */}
|
|
292
|
+
{selectedVersion && (
|
|
293
|
+
<VersionActions
|
|
294
|
+
version={selectedVersion}
|
|
295
|
+
onRestore={() => handleRestore(selectedVersion.id)}
|
|
296
|
+
onCompare={() => handleCompare(selectedVersion.id)}
|
|
297
|
+
onDownload={() => handleDownload(selectedVersion)}
|
|
298
|
+
onDelete={() => handleDelete(selectedVersion.id)}
|
|
299
|
+
loading={actionLoading}
|
|
300
|
+
/>
|
|
301
|
+
)}
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
{/* Diff Viewer Modal */}
|
|
305
|
+
{showDiff && diffData && (
|
|
306
|
+
<DiffViewer
|
|
307
|
+
oldContent={diffData.version.body}
|
|
308
|
+
newContent={diffData.current.body}
|
|
309
|
+
oldLabel={`Version: ${formatTimestamp(diffData.version.timestamp)}`}
|
|
310
|
+
newLabel="Current"
|
|
311
|
+
onClose={() => setShowDiff(false)}
|
|
312
|
+
/>
|
|
313
|
+
)}
|
|
314
|
+
|
|
315
|
+
{/* Clear All Confirmation Modal */}
|
|
316
|
+
{showClearAllConfirm && (
|
|
317
|
+
<ClearAllConfirmModal
|
|
318
|
+
versionCount={versions.length}
|
|
319
|
+
onConfirm={handleClearAll}
|
|
320
|
+
onCancel={() => setShowClearAllConfirm(false)}
|
|
321
|
+
loading={actionLoading === "clearAll"}
|
|
322
|
+
/>
|
|
323
|
+
)}
|
|
324
|
+
</>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Version item component
|
|
330
|
+
*/
|
|
331
|
+
function VersionItem({
|
|
332
|
+
version,
|
|
333
|
+
isSelected,
|
|
334
|
+
onClick,
|
|
335
|
+
}: {
|
|
336
|
+
version: VersionEntry;
|
|
337
|
+
isSelected: boolean;
|
|
338
|
+
onClick: () => void;
|
|
339
|
+
}): React.ReactElement {
|
|
340
|
+
return (
|
|
341
|
+
<button
|
|
342
|
+
className={`wn-version-item ${isSelected ? "wn-version-item--selected" : ""}`}
|
|
343
|
+
onClick={onClick}
|
|
344
|
+
aria-pressed={isSelected}
|
|
345
|
+
>
|
|
346
|
+
<div className="wn-version-item-header">
|
|
347
|
+
<span className="wn-version-item-time">
|
|
348
|
+
<Clock size={12} />
|
|
349
|
+
{formatTimestamp(version.timestamp)}
|
|
350
|
+
</span>
|
|
351
|
+
<span className="wn-version-item-size">{formatSize(version.size)}</span>
|
|
352
|
+
</div>
|
|
353
|
+
{version.label && (
|
|
354
|
+
<span className="wn-version-item-label">
|
|
355
|
+
<Tag size={10} />
|
|
356
|
+
{version.label}
|
|
357
|
+
</span>
|
|
358
|
+
)}
|
|
359
|
+
<p className="wn-version-item-preview">{version.preview}</p>
|
|
360
|
+
</button>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Props for ClearAllConfirmModal component
|
|
366
|
+
*/
|
|
367
|
+
interface ClearAllConfirmModalProps {
|
|
368
|
+
/** Number of versions to be deleted */
|
|
369
|
+
versionCount: number;
|
|
370
|
+
/** Callback when user confirms */
|
|
371
|
+
onConfirm: () => void;
|
|
372
|
+
/** Callback when user cancels */
|
|
373
|
+
onCancel: () => void;
|
|
374
|
+
/** Loading state */
|
|
375
|
+
loading?: boolean;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Confirmation modal for clearing all version history
|
|
380
|
+
*/
|
|
381
|
+
function ClearAllConfirmModal({
|
|
382
|
+
versionCount,
|
|
383
|
+
onConfirm,
|
|
384
|
+
onCancel,
|
|
385
|
+
loading,
|
|
386
|
+
}: ClearAllConfirmModalProps): React.ReactElement {
|
|
387
|
+
const triggerRef = useRef<HTMLElement | null>(null);
|
|
388
|
+
const cancelButtonRef = useRef<HTMLButtonElement>(null);
|
|
389
|
+
|
|
390
|
+
// Store the trigger element when modal mounts
|
|
391
|
+
useEffect(() => {
|
|
392
|
+
triggerRef.current = document.activeElement as HTMLElement;
|
|
393
|
+
}, []);
|
|
394
|
+
|
|
395
|
+
// Focus trap for accessibility
|
|
396
|
+
const { containerRef } = useFocusTrap({
|
|
397
|
+
enabled: true,
|
|
398
|
+
onEscape: loading ? undefined : onCancel,
|
|
399
|
+
returnFocusTo: triggerRef.current,
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// Focus cancel button when modal opens
|
|
403
|
+
useEffect(() => {
|
|
404
|
+
setTimeout(() => {
|
|
405
|
+
cancelButtonRef.current?.focus();
|
|
406
|
+
}, 50);
|
|
407
|
+
}, []);
|
|
408
|
+
|
|
409
|
+
const handleOverlayClick = (e: React.MouseEvent) => {
|
|
410
|
+
if (e.target === e.currentTarget && !loading) onCancel();
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
return (
|
|
414
|
+
<div className="wn-confirm-overlay" onClick={handleOverlayClick}>
|
|
415
|
+
<div
|
|
416
|
+
ref={containerRef}
|
|
417
|
+
className="wn-confirm-modal"
|
|
418
|
+
role="alertdialog"
|
|
419
|
+
aria-modal="true"
|
|
420
|
+
aria-labelledby="clear-all-modal-title"
|
|
421
|
+
aria-describedby="clear-all-modal-message"
|
|
422
|
+
>
|
|
423
|
+
<div className="wn-confirm-header">
|
|
424
|
+
<AlertTriangle size={20} className="wn-confirm-icon" />
|
|
425
|
+
<h3 id="clear-all-modal-title" className="wn-confirm-title">
|
|
426
|
+
Clear All History
|
|
427
|
+
</h3>
|
|
428
|
+
<button
|
|
429
|
+
className="wn-confirm-close"
|
|
430
|
+
onClick={onCancel}
|
|
431
|
+
disabled={loading}
|
|
432
|
+
aria-label="Close"
|
|
433
|
+
>
|
|
434
|
+
<X size={16} />
|
|
435
|
+
</button>
|
|
436
|
+
</div>
|
|
437
|
+
<p id="clear-all-modal-message" className="wn-confirm-message">
|
|
438
|
+
Are you sure you want to delete all {versionCount} version
|
|
439
|
+
{versionCount !== 1 ? "s" : ""} for this content? This action cannot
|
|
440
|
+
be undone.
|
|
441
|
+
</p>
|
|
442
|
+
<div className="wn-confirm-actions">
|
|
443
|
+
<button
|
|
444
|
+
ref={cancelButtonRef}
|
|
445
|
+
className="wn-confirm-btn wn-confirm-btn--cancel"
|
|
446
|
+
onClick={onCancel}
|
|
447
|
+
disabled={loading}
|
|
448
|
+
>
|
|
449
|
+
Cancel
|
|
450
|
+
</button>
|
|
451
|
+
<button
|
|
452
|
+
className="wn-confirm-btn wn-confirm-btn--danger"
|
|
453
|
+
onClick={onConfirm}
|
|
454
|
+
disabled={loading}
|
|
455
|
+
>
|
|
456
|
+
{loading ? (
|
|
457
|
+
<>
|
|
458
|
+
<Loader2 size={14} className="wn-spin" />
|
|
459
|
+
Clearing...
|
|
460
|
+
</>
|
|
461
|
+
) : (
|
|
462
|
+
"Clear All"
|
|
463
|
+
)}
|
|
464
|
+
</button>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Version History components exports
|
|
3
|
+
*
|
|
4
|
+
* @module @writenex/astro/client/components/VersionHistory
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { VersionHistoryPanel } from "./VersionHistoryPanel";
|
|
8
|
+
export { VersionActions } from "./VersionActions";
|
|
9
|
+
export { DiffViewer } from "./DiffViewer";
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview API Context for Writenex client
|
|
3
|
+
*
|
|
4
|
+
* Provides a shared API client instance across all components using React Context.
|
|
5
|
+
* This prevents unnecessary recreation of the API client on every hook call.
|
|
6
|
+
*
|
|
7
|
+
* @module @writenex/astro/client/context/ApiContext
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
createContext,
|
|
12
|
+
useContext,
|
|
13
|
+
useMemo,
|
|
14
|
+
type ReactNode,
|
|
15
|
+
type ReactElement,
|
|
16
|
+
} from "react";
|
|
17
|
+
import { createApiClient } from "../hooks/useApi";
|
|
18
|
+
import { createVersionApiClient } from "../hooks/useVersionHistory";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* API client type inferred from createApiClient
|
|
22
|
+
*/
|
|
23
|
+
type ApiClient = ReturnType<typeof createApiClient>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Version API client type inferred from createVersionApiClient
|
|
27
|
+
*/
|
|
28
|
+
type VersionApiClient = ReturnType<typeof createVersionApiClient>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Context value containing the API client and base URL
|
|
32
|
+
*/
|
|
33
|
+
interface ApiContextValue {
|
|
34
|
+
client: ApiClient;
|
|
35
|
+
versionClient: VersionApiClient;
|
|
36
|
+
apiBase: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* API Context - holds the shared API client instance
|
|
41
|
+
*/
|
|
42
|
+
const ApiContext = createContext<ApiContextValue | null>(null);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Props for ApiProvider
|
|
46
|
+
*/
|
|
47
|
+
interface ApiProviderProps {
|
|
48
|
+
/** Base URL for API requests */
|
|
49
|
+
apiBase: string;
|
|
50
|
+
/** Child components */
|
|
51
|
+
children: ReactNode;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* API Provider component
|
|
56
|
+
*
|
|
57
|
+
* Wraps the application and provides a shared API client instance
|
|
58
|
+
* to all child components via context.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```tsx
|
|
62
|
+
* <ApiProvider apiBase="/_writenex/api">
|
|
63
|
+
* <App />
|
|
64
|
+
* </ApiProvider>
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export function ApiProvider({
|
|
68
|
+
apiBase,
|
|
69
|
+
children,
|
|
70
|
+
}: ApiProviderProps): ReactElement {
|
|
71
|
+
// Memoize the API clients to prevent recreation on re-renders
|
|
72
|
+
const value = useMemo(
|
|
73
|
+
() => ({
|
|
74
|
+
client: createApiClient({ apiBase }),
|
|
75
|
+
versionClient: createVersionApiClient({ apiBase }),
|
|
76
|
+
apiBase,
|
|
77
|
+
}),
|
|
78
|
+
[apiBase]
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return <ApiContext.Provider value={value}>{children}</ApiContext.Provider>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Hook to access the shared API client
|
|
86
|
+
*
|
|
87
|
+
* Must be used within an ApiProvider.
|
|
88
|
+
*
|
|
89
|
+
* @returns The API context value containing client and apiBase
|
|
90
|
+
* @throws Error if used outside of ApiProvider
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```tsx
|
|
94
|
+
* function MyComponent() {
|
|
95
|
+
* const { client } = useApiContext();
|
|
96
|
+
* // Use client.getCollections(), client.getContent(), etc.
|
|
97
|
+
* }
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export function useApiContext(): ApiContextValue {
|
|
101
|
+
const context = useContext(ApiContext);
|
|
102
|
+
if (!context) {
|
|
103
|
+
throw new Error("useApiContext must be used within an ApiProvider");
|
|
104
|
+
}
|
|
105
|
+
return context;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Hook to access just the API client
|
|
110
|
+
*
|
|
111
|
+
* Convenience wrapper around useApiContext for simpler access to the client.
|
|
112
|
+
*
|
|
113
|
+
* @returns The shared API client instance
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```tsx
|
|
117
|
+
* function MyComponent() {
|
|
118
|
+
* const api = useSharedApi();
|
|
119
|
+
* const collections = await api.getCollections();
|
|
120
|
+
* }
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
export function useSharedApi(): ApiClient {
|
|
124
|
+
const { client } = useApiContext();
|
|
125
|
+
return client;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Hook to access the API base URL from context
|
|
130
|
+
*
|
|
131
|
+
* @returns The API base URL string
|
|
132
|
+
*/
|
|
133
|
+
export function useApiBase(): string {
|
|
134
|
+
const { apiBase } = useApiContext();
|
|
135
|
+
return apiBase;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Hook to access the shared version API client
|
|
140
|
+
*
|
|
141
|
+
* @returns The shared version API client instance
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```tsx
|
|
145
|
+
* function MyComponent() {
|
|
146
|
+
* const versionApi = useSharedVersionApi();
|
|
147
|
+
* const versions = await versionApi.listVersions(collection, contentId);
|
|
148
|
+
* }
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
export function useSharedVersionApi(): VersionApiClient {
|
|
152
|
+
const { versionClient } = useApiContext();
|
|
153
|
+
return versionClient;
|
|
154
|
+
}
|