@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,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
+ }