@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.
Files changed (141) hide show
  1. package/README.md +539 -0
  2. package/dist/chunk-5PM6EQE5.js +151 -0
  3. package/dist/chunk-5PM6EQE5.js.map +1 -0
  4. package/dist/chunk-7XU5X6CW.js +1331 -0
  5. package/dist/chunk-7XU5X6CW.js.map +1 -0
  6. package/dist/chunk-AAOQHQPU.js +574 -0
  7. package/dist/chunk-AAOQHQPU.js.map +1 -0
  8. package/dist/chunk-CF2XXJFF.js +1410 -0
  9. package/dist/chunk-CF2XXJFF.js.map +1 -0
  10. package/dist/chunk-CRPZUUDU.js +52 -0
  11. package/dist/chunk-CRPZUUDU.js.map +1 -0
  12. package/dist/chunk-CYLDJ3HZ.js +310 -0
  13. package/dist/chunk-CYLDJ3HZ.js.map +1 -0
  14. package/dist/chunk-KIKIPIFA.js +1 -0
  15. package/dist/chunk-KIKIPIFA.js.map +1 -0
  16. package/dist/chunk-XNTQTTJU.js +145 -0
  17. package/dist/chunk-XNTQTTJU.js.map +1 -0
  18. package/dist/client/index.css +2 -0
  19. package/dist/client/index.css.map +1 -0
  20. package/dist/client/index.js +375 -0
  21. package/dist/client/index.js.map +1 -0
  22. package/dist/client/styles.css +584 -0
  23. package/dist/client/variables.css +304 -0
  24. package/dist/config/index.d.ts +54 -0
  25. package/dist/config/index.js +38 -0
  26. package/dist/config/index.js.map +1 -0
  27. package/dist/config-BmEdBDo_.d.ts +220 -0
  28. package/dist/content-BWR52vD-.d.ts +64 -0
  29. package/dist/discovery/index.d.ts +310 -0
  30. package/dist/discovery/index.js +38 -0
  31. package/dist/discovery/index.js.map +1 -0
  32. package/dist/errors-C0iYiDTv.d.ts +107 -0
  33. package/dist/filesystem/index.d.ts +1292 -0
  34. package/dist/filesystem/index.js +203 -0
  35. package/dist/filesystem/index.js.map +1 -0
  36. package/dist/image-FP7w5ZIs.d.ts +47 -0
  37. package/dist/index.d.ts +64 -0
  38. package/dist/index.js +151 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/loader-55LWCXHA.js +12 -0
  41. package/dist/loader-55LWCXHA.js.map +1 -0
  42. package/dist/loader-CrdnaAWR.d.ts +327 -0
  43. package/dist/server/index.d.ts +357 -0
  44. package/dist/server/index.js +37 -0
  45. package/dist/server/index.js.map +1 -0
  46. package/package.json +94 -0
  47. package/src/client/App.tsx +900 -0
  48. package/src/client/components/ConfigPanel/ConfigPanel.css +553 -0
  49. package/src/client/components/ConfigPanel/ConfigPanel.tsx +396 -0
  50. package/src/client/components/ConfigPanel/index.ts +6 -0
  51. package/src/client/components/CreateContentModal/CreateContentModal.css +327 -0
  52. package/src/client/components/CreateContentModal/CreateContentModal.tsx +216 -0
  53. package/src/client/components/CreateContentModal/index.ts +7 -0
  54. package/src/client/components/Editor/Editor.css +885 -0
  55. package/src/client/components/Editor/Editor.tsx +484 -0
  56. package/src/client/components/Editor/ImageDialog.css +344 -0
  57. package/src/client/components/Editor/ImageDialog.tsx +367 -0
  58. package/src/client/components/Editor/LinkDialog.css +326 -0
  59. package/src/client/components/Editor/LinkDialog.tsx +332 -0
  60. package/src/client/components/Editor/index.ts +6 -0
  61. package/src/client/components/FrontmatterForm/FrontmatterForm.css +468 -0
  62. package/src/client/components/FrontmatterForm/FrontmatterForm.tsx +914 -0
  63. package/src/client/components/FrontmatterForm/index.ts +7 -0
  64. package/src/client/components/Header/Header.css +300 -0
  65. package/src/client/components/Header/Header.tsx +300 -0
  66. package/src/client/components/Header/index.ts +7 -0
  67. package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.css +239 -0
  68. package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.tsx +151 -0
  69. package/src/client/components/KeyboardShortcuts/index.ts +6 -0
  70. package/src/client/components/LazyEditor.tsx +75 -0
  71. package/src/client/components/LiveRegion/LiveRegion.css +19 -0
  72. package/src/client/components/LiveRegion/LiveRegion.tsx +60 -0
  73. package/src/client/components/LiveRegion/index.ts +7 -0
  74. package/src/client/components/SearchReplace/SearchReplacePanel.css +300 -0
  75. package/src/client/components/SearchReplace/SearchReplacePanel.tsx +332 -0
  76. package/src/client/components/SearchReplace/index.ts +7 -0
  77. package/src/client/components/SelectCollectionModal/SelectCollectionModal.css +308 -0
  78. package/src/client/components/SelectCollectionModal/SelectCollectionModal.tsx +223 -0
  79. package/src/client/components/SelectCollectionModal/index.ts +7 -0
  80. package/src/client/components/Sidebar/Sidebar.css +570 -0
  81. package/src/client/components/Sidebar/Sidebar.tsx +617 -0
  82. package/src/client/components/Sidebar/index.ts +7 -0
  83. package/src/client/components/SkipLink/SkipLink.css +51 -0
  84. package/src/client/components/SkipLink/SkipLink.tsx +67 -0
  85. package/src/client/components/SkipLink/index.ts +7 -0
  86. package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.css +233 -0
  87. package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.tsx +160 -0
  88. package/src/client/components/UnsavedChangesModal/index.ts +1 -0
  89. package/src/client/components/VersionHistory/DiffViewer.css +430 -0
  90. package/src/client/components/VersionHistory/DiffViewer.tsx +383 -0
  91. package/src/client/components/VersionHistory/VersionActions.css +318 -0
  92. package/src/client/components/VersionHistory/VersionActions.tsx +277 -0
  93. package/src/client/components/VersionHistory/VersionHistoryPanel.css +369 -0
  94. package/src/client/components/VersionHistory/VersionHistoryPanel.tsx +469 -0
  95. package/src/client/components/VersionHistory/index.ts +9 -0
  96. package/src/client/context/ApiContext.tsx +154 -0
  97. package/src/client/context/ThemeContext.tsx +172 -0
  98. package/src/client/hooks/useAnnounce.ts +201 -0
  99. package/src/client/hooks/useApi.ts +374 -0
  100. package/src/client/hooks/useArrowNavigation.ts +286 -0
  101. package/src/client/hooks/useAutosave.ts +241 -0
  102. package/src/client/hooks/useFocusTrap.ts +178 -0
  103. package/src/client/hooks/useKeyboardShortcuts.ts +203 -0
  104. package/src/client/hooks/useSearch.ts +206 -0
  105. package/src/client/hooks/useVersionHistory.ts +451 -0
  106. package/src/client/index.tsx +70 -0
  107. package/src/client/styles.css +584 -0
  108. package/src/client/utils/focus.ts +57 -0
  109. package/src/client/utils/openInEditor.ts +130 -0
  110. package/src/client/variables.css +304 -0
  111. package/src/config/defaults.ts +109 -0
  112. package/src/config/index.ts +32 -0
  113. package/src/config/loader.ts +174 -0
  114. package/src/config/schema.ts +161 -0
  115. package/src/core/constants.ts +39 -0
  116. package/src/core/errors.ts +739 -0
  117. package/src/core/index.ts +11 -0
  118. package/src/discovery/collections.ts +216 -0
  119. package/src/discovery/index.ts +33 -0
  120. package/src/discovery/patterns.ts +702 -0
  121. package/src/discovery/schema.ts +453 -0
  122. package/src/filesystem/images.ts +798 -0
  123. package/src/filesystem/index.ts +107 -0
  124. package/src/filesystem/reader.ts +452 -0
  125. package/src/filesystem/version-config.ts +390 -0
  126. package/src/filesystem/versions.ts +1339 -0
  127. package/src/filesystem/watcher.ts +226 -0
  128. package/src/filesystem/writer.ts +540 -0
  129. package/src/index.ts +61 -0
  130. package/src/integration.ts +228 -0
  131. package/src/server/assets.ts +254 -0
  132. package/src/server/cache.ts +355 -0
  133. package/src/server/index.ts +33 -0
  134. package/src/server/middleware.ts +209 -0
  135. package/src/server/routes.ts +1428 -0
  136. package/src/types/api.ts +61 -0
  137. package/src/types/config.ts +134 -0
  138. package/src/types/content.ts +64 -0
  139. package/src/types/image.ts +48 -0
  140. package/src/types/index.ts +58 -0
  141. 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
+ }