@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,484 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview MDXEditor wrapper component
|
|
3
|
+
*
|
|
4
|
+
* This component wraps MDXEditor with the necessary plugins and configuration
|
|
5
|
+
* for the Writenex Astro integration. Includes diffSourcePlugin for viewing
|
|
6
|
+
* source markdown and diff modes.
|
|
7
|
+
*
|
|
8
|
+
* @module @writenex/astro/client/components/Editor
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
12
|
+
import { FileText, Plus } from "lucide-react";
|
|
13
|
+
import {
|
|
14
|
+
MDXEditor,
|
|
15
|
+
headingsPlugin,
|
|
16
|
+
listsPlugin,
|
|
17
|
+
quotePlugin,
|
|
18
|
+
thematicBreakPlugin,
|
|
19
|
+
markdownShortcutPlugin,
|
|
20
|
+
linkPlugin,
|
|
21
|
+
linkDialogPlugin,
|
|
22
|
+
imagePlugin,
|
|
23
|
+
tablePlugin,
|
|
24
|
+
codeBlockPlugin,
|
|
25
|
+
codeMirrorPlugin,
|
|
26
|
+
toolbarPlugin,
|
|
27
|
+
diffSourcePlugin,
|
|
28
|
+
frontmatterPlugin,
|
|
29
|
+
searchPlugin,
|
|
30
|
+
editorSearchTerm$,
|
|
31
|
+
editorSearchCursor$,
|
|
32
|
+
usePublisher,
|
|
33
|
+
addComposerChild$,
|
|
34
|
+
BoldItalicUnderlineToggles,
|
|
35
|
+
BlockTypeSelect,
|
|
36
|
+
CreateLink,
|
|
37
|
+
InsertImage,
|
|
38
|
+
InsertTable,
|
|
39
|
+
ListsToggle,
|
|
40
|
+
UndoRedo,
|
|
41
|
+
CodeToggle,
|
|
42
|
+
InsertThematicBreak,
|
|
43
|
+
InsertCodeBlock,
|
|
44
|
+
DiffSourceToggleWrapper,
|
|
45
|
+
type MDXEditorMethods,
|
|
46
|
+
} from "@mdxeditor/editor";
|
|
47
|
+
import "@mdxeditor/editor/style.css";
|
|
48
|
+
import "./Editor.css";
|
|
49
|
+
import { ImageDialog } from "./ImageDialog";
|
|
50
|
+
import { LinkDialog } from "./LinkDialog";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Props for the Editor component
|
|
54
|
+
*/
|
|
55
|
+
interface EditorProps {
|
|
56
|
+
/** Initial markdown content */
|
|
57
|
+
initialContent: string;
|
|
58
|
+
/** Callback when content changes */
|
|
59
|
+
onChange: (markdown: string) => void;
|
|
60
|
+
/** Whether the editor is read-only */
|
|
61
|
+
readOnly?: boolean;
|
|
62
|
+
/** Placeholder text when empty */
|
|
63
|
+
placeholder?: string;
|
|
64
|
+
/** Handler for image uploads. Returns the image URL/path on success. */
|
|
65
|
+
onImageUpload?: (file: File) => Promise<string | null>;
|
|
66
|
+
/** Base path for API requests */
|
|
67
|
+
basePath?: string;
|
|
68
|
+
/** Current collection name (for image URL resolution) */
|
|
69
|
+
collection?: string;
|
|
70
|
+
/** Current content ID (for image URL resolution) */
|
|
71
|
+
contentId?: string;
|
|
72
|
+
/** Search query for highlighting */
|
|
73
|
+
searchQuery?: string;
|
|
74
|
+
/** Current search match index (1-based) */
|
|
75
|
+
searchActiveIndex?: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Module-level refs for sharing search state with SearchBridge inside MDXEditor.
|
|
80
|
+
* This is necessary because SearchBridge is mounted via addComposerChild$ which
|
|
81
|
+
* places it inside MDXEditor's internal tree, outside of React Context providers.
|
|
82
|
+
*/
|
|
83
|
+
const searchStateRef = {
|
|
84
|
+
query: "",
|
|
85
|
+
activeIndex: 0,
|
|
86
|
+
listeners: new Set<() => void>(),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
function setSearchState(query: string, activeIndex: number) {
|
|
90
|
+
searchStateRef.query = query;
|
|
91
|
+
searchStateRef.activeIndex = activeIndex;
|
|
92
|
+
// Notify all listeners
|
|
93
|
+
searchStateRef.listeners.forEach((listener) => listener());
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function useSearchState() {
|
|
97
|
+
const [, forceUpdate] = useState({});
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
const listener = () => forceUpdate({});
|
|
101
|
+
searchStateRef.listeners.add(listener);
|
|
102
|
+
return () => {
|
|
103
|
+
searchStateRef.listeners.delete(listener);
|
|
104
|
+
};
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
searchQuery: searchStateRef.query,
|
|
109
|
+
searchActiveIndex: searchStateRef.activeIndex,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* SearchBridge component that syncs search state with MDXEditor's searchPlugin.
|
|
115
|
+
* Uses module-level state because it's mounted inside MDXEditor via addComposerChild$,
|
|
116
|
+
* which is outside of React Context providers.
|
|
117
|
+
*/
|
|
118
|
+
function SearchBridge(): null {
|
|
119
|
+
const { searchQuery, searchActiveIndex } = useSearchState();
|
|
120
|
+
const updateSearch = usePublisher(editorSearchTerm$);
|
|
121
|
+
const updateCursor = usePublisher(editorSearchCursor$);
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
updateSearch(searchQuery);
|
|
125
|
+
}, [searchQuery, updateSearch]);
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (searchActiveIndex > 0) {
|
|
129
|
+
updateCursor(searchActiveIndex);
|
|
130
|
+
}
|
|
131
|
+
}, [searchActiveIndex, updateCursor]);
|
|
132
|
+
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* MDXEditor plugin that adds the SearchBridge component to the editor
|
|
138
|
+
*/
|
|
139
|
+
function createSearchBridgePlugin() {
|
|
140
|
+
return {
|
|
141
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
142
|
+
init: (realm: any) => {
|
|
143
|
+
realm.pub(addComposerChild$, SearchBridge);
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Toolbar separator component
|
|
150
|
+
*/
|
|
151
|
+
const ToolbarSeparator = () => (
|
|
152
|
+
<div
|
|
153
|
+
style={{
|
|
154
|
+
width: "1px",
|
|
155
|
+
height: "24px",
|
|
156
|
+
backgroundColor: "var(--wn-zinc-700)",
|
|
157
|
+
margin: "0 4px",
|
|
158
|
+
}}
|
|
159
|
+
/>
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Editor toolbar contents with DiffSourceToggleWrapper
|
|
164
|
+
*/
|
|
165
|
+
function EditorToolbarContents(): React.ReactElement {
|
|
166
|
+
return (
|
|
167
|
+
<DiffSourceToggleWrapper>
|
|
168
|
+
{/* Undo/Redo */}
|
|
169
|
+
<UndoRedo />
|
|
170
|
+
<ToolbarSeparator />
|
|
171
|
+
|
|
172
|
+
{/* Block Type */}
|
|
173
|
+
<BlockTypeSelect />
|
|
174
|
+
<ToolbarSeparator />
|
|
175
|
+
|
|
176
|
+
{/* Text Formatting */}
|
|
177
|
+
<BoldItalicUnderlineToggles />
|
|
178
|
+
<CodeToggle />
|
|
179
|
+
<ToolbarSeparator />
|
|
180
|
+
|
|
181
|
+
{/* Lists */}
|
|
182
|
+
<ListsToggle />
|
|
183
|
+
<ToolbarSeparator />
|
|
184
|
+
|
|
185
|
+
{/* Insert Link & Image */}
|
|
186
|
+
<CreateLink />
|
|
187
|
+
<InsertImage />
|
|
188
|
+
<ToolbarSeparator />
|
|
189
|
+
|
|
190
|
+
{/* Table & Thematic Break */}
|
|
191
|
+
<InsertTable />
|
|
192
|
+
<InsertThematicBreak />
|
|
193
|
+
<ToolbarSeparator />
|
|
194
|
+
|
|
195
|
+
{/* Code Block */}
|
|
196
|
+
<InsertCodeBlock />
|
|
197
|
+
</DiffSourceToggleWrapper>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* MDXEditor wrapper with Writenex configuration
|
|
203
|
+
*
|
|
204
|
+
* Features:
|
|
205
|
+
* - Full-width editor layout
|
|
206
|
+
* - Dark mode styling
|
|
207
|
+
* - diffSourcePlugin for source/diff view modes
|
|
208
|
+
* - Comprehensive toolbar with formatting options
|
|
209
|
+
*
|
|
210
|
+
* @component
|
|
211
|
+
* @example
|
|
212
|
+
* ```tsx
|
|
213
|
+
* <Editor
|
|
214
|
+
* initialContent={markdown}
|
|
215
|
+
* onChange={handleChange}
|
|
216
|
+
* placeholder="Start writing..."
|
|
217
|
+
* />
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
export function Editor({
|
|
221
|
+
initialContent,
|
|
222
|
+
onChange,
|
|
223
|
+
readOnly = false,
|
|
224
|
+
placeholder = "Start writing...",
|
|
225
|
+
onImageUpload,
|
|
226
|
+
basePath = "/_writenex",
|
|
227
|
+
collection,
|
|
228
|
+
contentId,
|
|
229
|
+
searchQuery = "",
|
|
230
|
+
searchActiveIndex = 0,
|
|
231
|
+
}: EditorProps): React.ReactElement {
|
|
232
|
+
const editorRef = useRef<MDXEditorMethods>(null);
|
|
233
|
+
const [isReady, setIsReady] = useState(false);
|
|
234
|
+
|
|
235
|
+
// Update editor content when initialContent changes
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
if (editorRef.current && isReady) {
|
|
238
|
+
editorRef.current.setMarkdown(initialContent);
|
|
239
|
+
}
|
|
240
|
+
}, [initialContent, isReady]);
|
|
241
|
+
|
|
242
|
+
// Mark editor as ready after initial mount
|
|
243
|
+
useEffect(() => {
|
|
244
|
+
setIsReady(true);
|
|
245
|
+
}, []);
|
|
246
|
+
|
|
247
|
+
const handleChange = useCallback(
|
|
248
|
+
(markdown: string) => {
|
|
249
|
+
onChange(markdown);
|
|
250
|
+
},
|
|
251
|
+
[onChange]
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// Update module-level search state when props change
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
setSearchState(searchQuery, searchActiveIndex);
|
|
257
|
+
}, [searchQuery, searchActiveIndex]);
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<div className="wn-editor">
|
|
261
|
+
<div className="wn-editor-content">
|
|
262
|
+
<div className="wn-editor-wrapper">
|
|
263
|
+
<MDXEditor
|
|
264
|
+
ref={editorRef}
|
|
265
|
+
markdown={initialContent}
|
|
266
|
+
onChange={handleChange}
|
|
267
|
+
readOnly={readOnly}
|
|
268
|
+
placeholder={placeholder}
|
|
269
|
+
contentEditableClassName="prose prose-invert max-w-none focus:outline-none"
|
|
270
|
+
onError={(error) => {
|
|
271
|
+
console.error("[writenex] Editor error:", error);
|
|
272
|
+
}}
|
|
273
|
+
plugins={[
|
|
274
|
+
// Basic formatting
|
|
275
|
+
headingsPlugin(),
|
|
276
|
+
listsPlugin(),
|
|
277
|
+
quotePlugin(),
|
|
278
|
+
thematicBreakPlugin(),
|
|
279
|
+
markdownShortcutPlugin(),
|
|
280
|
+
|
|
281
|
+
// Frontmatter support
|
|
282
|
+
frontmatterPlugin(),
|
|
283
|
+
|
|
284
|
+
// Links and images
|
|
285
|
+
linkPlugin(),
|
|
286
|
+
linkDialogPlugin({
|
|
287
|
+
LinkDialog: LinkDialog,
|
|
288
|
+
}),
|
|
289
|
+
imagePlugin({
|
|
290
|
+
ImageDialog: ImageDialog,
|
|
291
|
+
imageUploadHandler: async (file: File) => {
|
|
292
|
+
if (onImageUpload) {
|
|
293
|
+
const result = await onImageUpload(file);
|
|
294
|
+
if (result) {
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
// If upload failed, throw to prevent inserting broken image
|
|
298
|
+
throw new Error("Image upload failed");
|
|
299
|
+
}
|
|
300
|
+
// Fallback: return data URL if no upload handler provided
|
|
301
|
+
return new Promise((resolve) => {
|
|
302
|
+
const reader = new FileReader();
|
|
303
|
+
reader.onload = () => resolve(reader.result as string);
|
|
304
|
+
reader.readAsDataURL(file);
|
|
305
|
+
});
|
|
306
|
+
},
|
|
307
|
+
imagePreviewHandler: (src: string) => {
|
|
308
|
+
// If it's already an absolute URL or data URL, return as-is
|
|
309
|
+
if (
|
|
310
|
+
src.startsWith("http://") ||
|
|
311
|
+
src.startsWith("https://") ||
|
|
312
|
+
src.startsWith("data:")
|
|
313
|
+
) {
|
|
314
|
+
return Promise.resolve(src);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Convert relative path to API URL for preview
|
|
318
|
+
if (collection && contentId && src.startsWith("./")) {
|
|
319
|
+
// Remove ./ prefix
|
|
320
|
+
const imagePath = src.slice(2);
|
|
321
|
+
|
|
322
|
+
// Check if imagePath already starts with contentId (flat file structure)
|
|
323
|
+
// e.g., "./post-example/image.webp" -> "post-example/image.webp"
|
|
324
|
+
// In this case, don't add contentId again to avoid double slug
|
|
325
|
+
if (imagePath.startsWith(`${contentId}/`)) {
|
|
326
|
+
const apiUrl = `${basePath}/api/images/${collection}/${imagePath}`;
|
|
327
|
+
return Promise.resolve(apiUrl);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// For folder-based structure, imagePath is just the filename
|
|
331
|
+
// e.g., "./image.webp" -> "image.webp"
|
|
332
|
+
const apiUrl = `${basePath}/api/images/${collection}/${contentId}/${imagePath}`;
|
|
333
|
+
return Promise.resolve(apiUrl);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Fallback: return original src
|
|
337
|
+
return Promise.resolve(src);
|
|
338
|
+
},
|
|
339
|
+
}),
|
|
340
|
+
|
|
341
|
+
// Tables
|
|
342
|
+
tablePlugin(),
|
|
343
|
+
|
|
344
|
+
// Code blocks
|
|
345
|
+
codeBlockPlugin({ defaultCodeBlockLanguage: "typescript" }),
|
|
346
|
+
codeMirrorPlugin({
|
|
347
|
+
codeBlockLanguages: {
|
|
348
|
+
// Empty string fallback for unknown languages
|
|
349
|
+
"": "Custom",
|
|
350
|
+
// JavaScript family
|
|
351
|
+
js: "JavaScript",
|
|
352
|
+
javascript: "JavaScript",
|
|
353
|
+
ts: "TypeScript",
|
|
354
|
+
typescript: "TypeScript",
|
|
355
|
+
jsx: "JSX",
|
|
356
|
+
tsx: "TSX",
|
|
357
|
+
// Web languages
|
|
358
|
+
html: "HTML",
|
|
359
|
+
css: "CSS",
|
|
360
|
+
json: "JSON",
|
|
361
|
+
xml: "XML",
|
|
362
|
+
// Scripting & Shell
|
|
363
|
+
bash: "Bash",
|
|
364
|
+
shell: "Shell",
|
|
365
|
+
sh: "Shell",
|
|
366
|
+
zsh: "Zsh",
|
|
367
|
+
// Configuration
|
|
368
|
+
yaml: "YAML",
|
|
369
|
+
yml: "YAML",
|
|
370
|
+
dockerfile: "Dockerfile",
|
|
371
|
+
docker: "Dockerfile",
|
|
372
|
+
toml: "TOML",
|
|
373
|
+
ini: "INI",
|
|
374
|
+
env: "ENV",
|
|
375
|
+
// Systems programming
|
|
376
|
+
c: "C",
|
|
377
|
+
cpp: "C++",
|
|
378
|
+
csharp: "C#",
|
|
379
|
+
rust: "Rust",
|
|
380
|
+
go: "Go",
|
|
381
|
+
// JVM languages
|
|
382
|
+
java: "Java",
|
|
383
|
+
kotlin: "Kotlin",
|
|
384
|
+
scala: "Scala",
|
|
385
|
+
// Scripting languages
|
|
386
|
+
python: "Python",
|
|
387
|
+
py: "Python",
|
|
388
|
+
ruby: "Ruby",
|
|
389
|
+
rb: "Ruby",
|
|
390
|
+
php: "PHP",
|
|
391
|
+
perl: "Perl",
|
|
392
|
+
lua: "Lua",
|
|
393
|
+
r: "R",
|
|
394
|
+
// Mobile
|
|
395
|
+
swift: "Swift",
|
|
396
|
+
// Database
|
|
397
|
+
sql: "SQL",
|
|
398
|
+
graphql: "GraphQL",
|
|
399
|
+
// Markdown & Documentation
|
|
400
|
+
md: "Markdown",
|
|
401
|
+
markdown: "Markdown",
|
|
402
|
+
mdx: "MDX",
|
|
403
|
+
astro: "Astro",
|
|
404
|
+
// Diagrams
|
|
405
|
+
mermaid: "Mermaid",
|
|
406
|
+
// Plain text
|
|
407
|
+
txt: "Plain Text",
|
|
408
|
+
text: "Plain Text",
|
|
409
|
+
plaintext: "Plain Text",
|
|
410
|
+
},
|
|
411
|
+
}),
|
|
412
|
+
|
|
413
|
+
// Diff source plugin for source/diff view modes
|
|
414
|
+
diffSourcePlugin({
|
|
415
|
+
viewMode: "rich-text",
|
|
416
|
+
}),
|
|
417
|
+
|
|
418
|
+
// Toolbar
|
|
419
|
+
toolbarPlugin({
|
|
420
|
+
toolbarContents: () => <EditorToolbarContents />,
|
|
421
|
+
}),
|
|
422
|
+
|
|
423
|
+
// Search plugin for highlighting matches (must be after toolbar)
|
|
424
|
+
searchPlugin(),
|
|
425
|
+
createSearchBridgePlugin(),
|
|
426
|
+
]}
|
|
427
|
+
/>
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Loading placeholder for editor
|
|
436
|
+
*/
|
|
437
|
+
export function EditorLoading(): React.ReactElement {
|
|
438
|
+
return (
|
|
439
|
+
<div className="wn-editor-loading" aria-busy="true" aria-live="polite">
|
|
440
|
+
<div className="wn-editor-loading-spinner" />
|
|
441
|
+
<span className="wn-editor-loading-text">Loading editor...</span>
|
|
442
|
+
</div>
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Props for EditorEmpty component
|
|
448
|
+
*/
|
|
449
|
+
interface EditorEmptyProps {
|
|
450
|
+
/** Callback when new content button is clicked */
|
|
451
|
+
onNewContent?: () => void;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Empty state when no content is selected
|
|
456
|
+
*/
|
|
457
|
+
export function EditorEmpty({
|
|
458
|
+
onNewContent,
|
|
459
|
+
}: EditorEmptyProps): React.ReactElement {
|
|
460
|
+
return (
|
|
461
|
+
<div className="wn-editor-empty">
|
|
462
|
+
<div className="wn-editor-empty-icon">
|
|
463
|
+
<FileText size={48} strokeWidth={1.5} />
|
|
464
|
+
</div>
|
|
465
|
+
<h2 className="wn-editor-empty-title">Select content to edit</h2>
|
|
466
|
+
<p className="wn-editor-empty-text">
|
|
467
|
+
Choose a collection and content item from the sidebar, or create new
|
|
468
|
+
content.
|
|
469
|
+
</p>
|
|
470
|
+
<button className="wn-editor-empty-btn" onClick={onNewContent}>
|
|
471
|
+
<Plus size={16} />
|
|
472
|
+
New Content
|
|
473
|
+
</button>
|
|
474
|
+
<div className="wn-editor-empty-shortcuts">
|
|
475
|
+
<span className="wn-editor-empty-shortcut">
|
|
476
|
+
<kbd>Alt</kbd> + <kbd>N</kbd> New content
|
|
477
|
+
</span>
|
|
478
|
+
<span className="wn-editor-empty-shortcut">
|
|
479
|
+
<kbd>Ctrl</kbd> + <kbd>/</kbd> Keyboard shortcuts
|
|
480
|
+
</span>
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
);
|
|
484
|
+
}
|