@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,206 @@
1
+ /**
2
+ * @fileoverview Search hook for editor
3
+ *
4
+ * Provides search and replace functionality for markdown content.
5
+ *
6
+ * @module @writenex/astro/client/hooks/useSearch
7
+ */
8
+
9
+ import { useState, useCallback } from "react";
10
+ import type { SearchOptions } from "../components/SearchReplace";
11
+
12
+ /**
13
+ * Search state and handlers
14
+ */
15
+ export interface UseSearchResult {
16
+ /** Whether search panel is open */
17
+ isSearchOpen: boolean;
18
+ /** Open search panel */
19
+ openSearch: () => void;
20
+ /** Close search panel */
21
+ closeSearch: () => void;
22
+ /** Toggle search panel */
23
+ toggleSearch: () => void;
24
+ /** Current search query */
25
+ searchQuery: string;
26
+ /** Array of match positions */
27
+ searchMatches: number[];
28
+ /** Current active match index (1-based) */
29
+ searchActiveIndex: number;
30
+ /** Total number of matches */
31
+ totalMatches: number;
32
+ /** Perform search */
33
+ handleFind: (query: string, options: SearchOptions) => number;
34
+ /** Navigate to next match */
35
+ handleNextMatch: () => void;
36
+ /** Navigate to previous match */
37
+ handlePreviousMatch: () => void;
38
+ /** Replace current match */
39
+ handleReplace: (
40
+ replacement: string,
41
+ content: string,
42
+ setContent: (content: string) => void
43
+ ) => void;
44
+ /** Replace all matches */
45
+ handleReplaceAll: (
46
+ replacement: string,
47
+ content: string,
48
+ setContent: (content: string) => void
49
+ ) => number;
50
+ }
51
+
52
+ /**
53
+ * Hook for search and replace functionality
54
+ *
55
+ * @param getContent - Function to get current content
56
+ * @returns Search state and handlers
57
+ */
58
+ export function useSearch(getContent: () => string): UseSearchResult {
59
+ const [isSearchOpen, setIsSearchOpen] = useState(false);
60
+ const [searchQuery, setSearchQuery] = useState("");
61
+ const [searchMatches, setSearchMatches] = useState<number[]>([]);
62
+ const [searchActiveIndex, setSearchActiveIndex] = useState(0);
63
+
64
+ const openSearch = useCallback(() => setIsSearchOpen(true), []);
65
+ const closeSearch = useCallback(() => setIsSearchOpen(false), []);
66
+ const toggleSearch = useCallback(() => setIsSearchOpen((prev) => !prev), []);
67
+
68
+ const handleFind = useCallback(
69
+ (query: string, options: SearchOptions): number => {
70
+ setSearchQuery(query);
71
+
72
+ if (!query) {
73
+ setSearchMatches([]);
74
+ setSearchActiveIndex(0);
75
+ return 0;
76
+ }
77
+
78
+ const content = getContent();
79
+ let pattern: RegExp;
80
+
81
+ try {
82
+ let regexPattern = options.regex
83
+ ? query
84
+ : query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
85
+
86
+ if (options.wholeWord) {
87
+ regexPattern = `\\b${regexPattern}\\b`;
88
+ }
89
+
90
+ pattern = new RegExp(regexPattern, options.caseSensitive ? "g" : "gi");
91
+ } catch {
92
+ // Invalid regex
93
+ setSearchMatches([]);
94
+ setSearchActiveIndex(0);
95
+ return 0;
96
+ }
97
+
98
+ const matches: number[] = [];
99
+ let match;
100
+ while ((match = pattern.exec(content)) !== null) {
101
+ matches.push(match.index);
102
+ }
103
+
104
+ setSearchMatches(matches);
105
+ setSearchActiveIndex(matches.length > 0 ? 1 : 0);
106
+ return matches.length;
107
+ },
108
+ [getContent]
109
+ );
110
+
111
+ const handleNextMatch = useCallback(() => {
112
+ if (searchMatches.length === 0) return;
113
+ setSearchActiveIndex((prev) =>
114
+ prev >= searchMatches.length ? 1 : prev + 1
115
+ );
116
+ }, [searchMatches.length]);
117
+
118
+ const handlePreviousMatch = useCallback(() => {
119
+ if (searchMatches.length === 0) return;
120
+ setSearchActiveIndex((prev) =>
121
+ prev <= 1 ? searchMatches.length : prev - 1
122
+ );
123
+ }, [searchMatches.length]);
124
+
125
+ const handleReplace = useCallback(
126
+ (
127
+ replacement: string,
128
+ content: string,
129
+ setContent: (content: string) => void
130
+ ) => {
131
+ if (searchMatches.length === 0) return;
132
+
133
+ const matchIndex = searchMatches[searchActiveIndex - 1];
134
+ if (matchIndex === undefined) return;
135
+
136
+ const beforeMatch = content.substring(0, matchIndex);
137
+ const afterMatch = content.substring(matchIndex + searchQuery.length);
138
+ const newContent = beforeMatch + replacement + afterMatch;
139
+
140
+ setContent(newContent);
141
+
142
+ // Re-run search to update matches
143
+ // Matches after the replacement point need to be adjusted
144
+ const adjustment = replacement.length - searchQuery.length;
145
+ const newMatches = searchMatches
146
+ .filter((_, i) => i !== searchActiveIndex - 1)
147
+ .map((pos, i) => {
148
+ if (i >= searchActiveIndex - 1) {
149
+ return pos + adjustment;
150
+ }
151
+ return pos;
152
+ });
153
+
154
+ setSearchMatches(newMatches);
155
+ if (searchActiveIndex > newMatches.length) {
156
+ setSearchActiveIndex(newMatches.length > 0 ? 1 : 0);
157
+ }
158
+ },
159
+ [searchMatches, searchActiveIndex, searchQuery]
160
+ );
161
+
162
+ const handleReplaceAll = useCallback(
163
+ (
164
+ replacement: string,
165
+ content: string,
166
+ setContent: (content: string) => void
167
+ ): number => {
168
+ if (searchMatches.length === 0) return 0;
169
+
170
+ let newContent = content;
171
+ let count = 0;
172
+
173
+ // Replace from end to start to preserve indices
174
+ [...searchMatches].reverse().forEach((index) => {
175
+ newContent =
176
+ newContent.substring(0, index) +
177
+ replacement +
178
+ newContent.substring(index + searchQuery.length);
179
+ count++;
180
+ });
181
+
182
+ setContent(newContent);
183
+ setSearchMatches([]);
184
+ setSearchActiveIndex(0);
185
+
186
+ return count;
187
+ },
188
+ [searchMatches, searchQuery]
189
+ );
190
+
191
+ return {
192
+ isSearchOpen,
193
+ openSearch,
194
+ closeSearch,
195
+ toggleSearch,
196
+ searchQuery,
197
+ searchMatches,
198
+ searchActiveIndex,
199
+ totalMatches: searchMatches.length,
200
+ handleFind,
201
+ handleNextMatch,
202
+ handlePreviousMatch,
203
+ handleReplace,
204
+ handleReplaceAll,
205
+ };
206
+ }
@@ -0,0 +1,451 @@
1
+ /**
2
+ * @fileoverview Version history hook for Writenex client
3
+ *
4
+ * Custom React hook for managing version history operations.
5
+ * Provides methods for listing, creating, restoring, and deleting versions.
6
+ *
7
+ * @module @writenex/astro/client/hooks/useVersionHistory
8
+ */
9
+
10
+ import { useState, useCallback, useMemo } from "react";
11
+ import type { VersionEntry, Version } from "../../types";
12
+
13
+ /**
14
+ * Version history API client configuration
15
+ */
16
+ interface VersionApiConfig {
17
+ apiBase: string;
18
+ }
19
+
20
+ /** Version API client type */
21
+ export type VersionApiClient = ReturnType<typeof createVersionApiClient>;
22
+
23
+ /**
24
+ * Diff data returned from API
25
+ */
26
+ export interface DiffData {
27
+ version: Version;
28
+ current: {
29
+ content: string;
30
+ frontmatter: Record<string, unknown>;
31
+ body: string;
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Create version history API client functions
37
+ */
38
+ export function createVersionApiClient(config: VersionApiConfig) {
39
+ const { apiBase } = config;
40
+
41
+ return {
42
+ /**
43
+ * List all versions for a content item
44
+ */
45
+ async listVersions(
46
+ collection: string,
47
+ contentId: string
48
+ ): Promise<VersionEntry[]> {
49
+ const response = await fetch(
50
+ `${apiBase}/versions/${collection}/${contentId}`
51
+ );
52
+ if (!response.ok) {
53
+ throw new Error("Failed to fetch versions");
54
+ }
55
+ const data = await response.json();
56
+ return data.versions;
57
+ },
58
+
59
+ /**
60
+ * Get a specific version with full content
61
+ */
62
+ async getVersion(
63
+ collection: string,
64
+ contentId: string,
65
+ versionId: string
66
+ ): Promise<Version> {
67
+ const response = await fetch(
68
+ `${apiBase}/versions/${collection}/${contentId}/${versionId}`
69
+ );
70
+ if (!response.ok) {
71
+ if (response.status === 404) {
72
+ throw new Error("Version not found");
73
+ }
74
+ throw new Error("Failed to fetch version");
75
+ }
76
+ const data = await response.json();
77
+ return data.version;
78
+ },
79
+
80
+ /**
81
+ * Create a manual version snapshot
82
+ */
83
+ async createVersion(
84
+ collection: string,
85
+ contentId: string,
86
+ label?: string
87
+ ): Promise<{ success: boolean; version?: VersionEntry; error?: string }> {
88
+ const response = await fetch(
89
+ `${apiBase}/versions/${collection}/${contentId}`,
90
+ {
91
+ method: "POST",
92
+ headers: { "Content-Type": "application/json" },
93
+ body: JSON.stringify({ label }),
94
+ }
95
+ );
96
+ return response.json();
97
+ },
98
+
99
+ /**
100
+ * Restore a version to current content
101
+ */
102
+ async restoreVersion(
103
+ collection: string,
104
+ contentId: string,
105
+ versionId: string
106
+ ): Promise<{
107
+ success: boolean;
108
+ content?: string;
109
+ safetySnapshot?: VersionEntry;
110
+ error?: string;
111
+ }> {
112
+ const response = await fetch(
113
+ `${apiBase}/versions/${collection}/${contentId}/${versionId}/restore`,
114
+ { method: "POST" }
115
+ );
116
+ return response.json();
117
+ },
118
+
119
+ /**
120
+ * Get diff data between version and current content
121
+ */
122
+ async getDiff(
123
+ collection: string,
124
+ contentId: string,
125
+ versionId: string
126
+ ): Promise<DiffData> {
127
+ const response = await fetch(
128
+ `${apiBase}/versions/${collection}/${contentId}/${versionId}/diff`
129
+ );
130
+ if (!response.ok) {
131
+ if (response.status === 404) {
132
+ throw new Error("Version not found");
133
+ }
134
+ throw new Error("Failed to fetch diff data");
135
+ }
136
+ const data = await response.json();
137
+ return { version: data.version, current: data.current };
138
+ },
139
+
140
+ /**
141
+ * Delete a specific version
142
+ */
143
+ async deleteVersion(
144
+ collection: string,
145
+ contentId: string,
146
+ versionId: string
147
+ ): Promise<{ success: boolean; error?: string }> {
148
+ const response = await fetch(
149
+ `${apiBase}/versions/${collection}/${contentId}/${versionId}`,
150
+ { method: "DELETE" }
151
+ );
152
+ return response.json();
153
+ },
154
+
155
+ /**
156
+ * Clear all versions for a content item
157
+ */
158
+ async clearVersions(
159
+ collection: string,
160
+ contentId: string
161
+ ): Promise<{ success: boolean; error?: string }> {
162
+ const response = await fetch(
163
+ `${apiBase}/versions/${collection}/${contentId}`,
164
+ { method: "DELETE" }
165
+ );
166
+ return response.json();
167
+ },
168
+ };
169
+ }
170
+
171
+ /**
172
+ * Hook state for version history
173
+ */
174
+ export interface UseVersionHistoryState {
175
+ /** List of versions */
176
+ versions: VersionEntry[];
177
+ /** Loading state */
178
+ loading: boolean;
179
+ /** Error message if any */
180
+ error: string | null;
181
+ /** Currently selected version for viewing */
182
+ selectedVersion: Version | null;
183
+ /** Loading state for selected version */
184
+ loadingVersion: boolean;
185
+ }
186
+
187
+ /**
188
+ * Hook actions for version history
189
+ */
190
+ export interface UseVersionHistoryActions {
191
+ /** Refresh the version list */
192
+ refresh: () => Promise<void>;
193
+ /** Get a specific version with full content */
194
+ getVersion: (versionId: string) => Promise<Version | null>;
195
+ /** Create a manual version snapshot */
196
+ createVersion: (label?: string) => Promise<boolean>;
197
+ /** Restore a version to current content */
198
+ restoreVersion: (versionId: string) => Promise<string | null>;
199
+ /** Delete a specific version */
200
+ deleteVersion: (versionId: string) => Promise<boolean>;
201
+ /** Clear all versions */
202
+ clearVersions: () => Promise<boolean>;
203
+ /** Get diff data between version and current */
204
+ getDiff: (versionId: string) => Promise<DiffData | null>;
205
+ /** Clear selected version */
206
+ clearSelectedVersion: () => void;
207
+ }
208
+
209
+ /**
210
+ * Hook for managing version history
211
+ *
212
+ * Provides state and actions for version history operations.
213
+ *
214
+ * @param apiBaseOrClient - Base URL for API calls or a pre-created version API client
215
+ * @param collection - Collection name
216
+ * @param contentId - Content ID (slug)
217
+ * @returns State and actions for version history
218
+ *
219
+ * @example
220
+ * ```tsx
221
+ * const { versions, loading, refresh, restoreVersion } = useVersionHistory(
222
+ * apiBase,
223
+ * "blog",
224
+ * "my-post"
225
+ * );
226
+ * ```
227
+ */
228
+ export function useVersionHistory(
229
+ apiBaseOrClient: string | VersionApiClient,
230
+ collection: string | null,
231
+ contentId: string | null
232
+ ): UseVersionHistoryState & UseVersionHistoryActions {
233
+ const client = useMemo(() => {
234
+ if (typeof apiBaseOrClient === "string") {
235
+ return createVersionApiClient({ apiBase: apiBaseOrClient });
236
+ }
237
+ return apiBaseOrClient;
238
+ }, [apiBaseOrClient]);
239
+
240
+ const [versions, setVersions] = useState<VersionEntry[]>([]);
241
+ const [loading, setLoading] = useState(false);
242
+ const [error, setError] = useState<string | null>(null);
243
+ const [selectedVersion, setSelectedVersion] = useState<Version | null>(null);
244
+ const [loadingVersion, setLoadingVersion] = useState(false);
245
+
246
+ /**
247
+ * Refresh the version list
248
+ */
249
+ const refresh = useCallback(async () => {
250
+ if (!collection || !contentId) {
251
+ setVersions([]);
252
+ return;
253
+ }
254
+
255
+ setLoading(true);
256
+ setError(null);
257
+
258
+ try {
259
+ const data = await client.listVersions(collection, contentId);
260
+ setVersions(data);
261
+ } catch (err) {
262
+ setError(err instanceof Error ? err.message : "Failed to fetch versions");
263
+ setVersions([]);
264
+ } finally {
265
+ setLoading(false);
266
+ }
267
+ }, [client, collection, contentId]);
268
+
269
+ /**
270
+ * Get a specific version with full content
271
+ */
272
+ const getVersion = useCallback(
273
+ async (versionId: string): Promise<Version | null> => {
274
+ if (!collection || !contentId) return null;
275
+
276
+ setLoadingVersion(true);
277
+ setError(null);
278
+
279
+ try {
280
+ const version = await client.getVersion(
281
+ collection,
282
+ contentId,
283
+ versionId
284
+ );
285
+ setSelectedVersion(version);
286
+ return version;
287
+ } catch (err) {
288
+ setError(
289
+ err instanceof Error ? err.message : "Failed to fetch version"
290
+ );
291
+ return null;
292
+ } finally {
293
+ setLoadingVersion(false);
294
+ }
295
+ },
296
+ [client, collection, contentId]
297
+ );
298
+
299
+ /**
300
+ * Create a manual version snapshot
301
+ */
302
+ const createVersion = useCallback(
303
+ async (label?: string): Promise<boolean> => {
304
+ if (!collection || !contentId) return false;
305
+
306
+ setError(null);
307
+
308
+ try {
309
+ const result = await client.createVersion(collection, contentId, label);
310
+ if (result.success) {
311
+ await refresh();
312
+ return true;
313
+ }
314
+ setError(result.error ?? "Failed to create version");
315
+ return false;
316
+ } catch (err) {
317
+ setError(
318
+ err instanceof Error ? err.message : "Failed to create version"
319
+ );
320
+ return false;
321
+ }
322
+ },
323
+ [client, collection, contentId, refresh]
324
+ );
325
+
326
+ /**
327
+ * Restore a version to current content
328
+ */
329
+ const restoreVersion = useCallback(
330
+ async (versionId: string): Promise<string | null> => {
331
+ if (!collection || !contentId) return null;
332
+
333
+ setError(null);
334
+
335
+ try {
336
+ const result = await client.restoreVersion(
337
+ collection,
338
+ contentId,
339
+ versionId
340
+ );
341
+ if (result.success && result.content) {
342
+ await refresh();
343
+ return result.content;
344
+ }
345
+ setError(result.error ?? "Failed to restore version");
346
+ return null;
347
+ } catch (err) {
348
+ setError(
349
+ err instanceof Error ? err.message : "Failed to restore version"
350
+ );
351
+ return null;
352
+ }
353
+ },
354
+ [client, collection, contentId, refresh]
355
+ );
356
+
357
+ /**
358
+ * Delete a specific version
359
+ */
360
+ const deleteVersion = useCallback(
361
+ async (versionId: string): Promise<boolean> => {
362
+ if (!collection || !contentId) return false;
363
+
364
+ setError(null);
365
+
366
+ try {
367
+ const result = await client.deleteVersion(
368
+ collection,
369
+ contentId,
370
+ versionId
371
+ );
372
+ if (result.success) {
373
+ await refresh();
374
+ return true;
375
+ }
376
+ setError(result.error ?? "Failed to delete version");
377
+ return false;
378
+ } catch (err) {
379
+ setError(
380
+ err instanceof Error ? err.message : "Failed to delete version"
381
+ );
382
+ return false;
383
+ }
384
+ },
385
+ [client, collection, contentId, refresh]
386
+ );
387
+
388
+ /**
389
+ * Clear all versions
390
+ */
391
+ const clearVersions = useCallback(async (): Promise<boolean> => {
392
+ if (!collection || !contentId) return false;
393
+
394
+ setError(null);
395
+
396
+ try {
397
+ const result = await client.clearVersions(collection, contentId);
398
+ if (result.success) {
399
+ setVersions([]);
400
+ return true;
401
+ }
402
+ setError(result.error ?? "Failed to clear versions");
403
+ return false;
404
+ } catch (err) {
405
+ setError(err instanceof Error ? err.message : "Failed to clear versions");
406
+ return false;
407
+ }
408
+ }, [client, collection, contentId]);
409
+
410
+ /**
411
+ * Get diff data between version and current content
412
+ */
413
+ const getDiff = useCallback(
414
+ async (versionId: string): Promise<DiffData | null> => {
415
+ if (!collection || !contentId) return null;
416
+
417
+ setError(null);
418
+
419
+ try {
420
+ return await client.getDiff(collection, contentId, versionId);
421
+ } catch (err) {
422
+ setError(err instanceof Error ? err.message : "Failed to fetch diff");
423
+ return null;
424
+ }
425
+ },
426
+ [client, collection, contentId]
427
+ );
428
+
429
+ /**
430
+ * Clear selected version
431
+ */
432
+ const clearSelectedVersion = useCallback(() => {
433
+ setSelectedVersion(null);
434
+ }, []);
435
+
436
+ return {
437
+ versions,
438
+ loading,
439
+ error,
440
+ selectedVersion,
441
+ loadingVersion,
442
+ refresh,
443
+ getVersion,
444
+ createVersion,
445
+ restoreVersion,
446
+ deleteVersion,
447
+ clearVersions,
448
+ getDiff,
449
+ clearSelectedVersion,
450
+ };
451
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @fileoverview Client entry point for Writenex editor
3
+ *
4
+ * This is the main entry point for the React-based editor UI.
5
+ * It bootstraps the application and mounts it to the DOM.
6
+ *
7
+ * @module @writenex/astro/client
8
+ */
9
+
10
+ import { StrictMode } from "react";
11
+ import { createRoot } from "react-dom/client";
12
+ import { App } from "./App";
13
+ import { ThemeProvider } from "./context/ThemeContext";
14
+ import { ApiProvider } from "./context/ApiContext";
15
+
16
+ /**
17
+ * Configuration injected by the server
18
+ */
19
+ declare global {
20
+ interface Window {
21
+ __WRITENEX_CONFIG__?: {
22
+ basePath: string;
23
+ apiBase: string;
24
+ };
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Get the configuration from the window object
30
+ */
31
+ function getConfig() {
32
+ return (
33
+ window.__WRITENEX_CONFIG__ ?? {
34
+ basePath: "/_writenex",
35
+ apiBase: "/_writenex/api",
36
+ }
37
+ );
38
+ }
39
+
40
+ /**
41
+ * Initialize and mount the application
42
+ */
43
+ function mount() {
44
+ const container = document.getElementById("root");
45
+
46
+ if (!container) {
47
+ console.error("[writenex] Root element not found");
48
+ return;
49
+ }
50
+
51
+ const config = getConfig();
52
+ const root = createRoot(container);
53
+
54
+ root.render(
55
+ <StrictMode>
56
+ <ThemeProvider>
57
+ <ApiProvider apiBase={config.apiBase}>
58
+ <App />
59
+ </ApiProvider>
60
+ </ThemeProvider>
61
+ </StrictMode>
62
+ );
63
+ }
64
+
65
+ // Mount when DOM is ready
66
+ if (document.readyState === "loading") {
67
+ document.addEventListener("DOMContentLoaded", mount);
68
+ } else {
69
+ mount();
70
+ }