@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,172 @@
1
+ /**
2
+ * @fileoverview Theme context and provider for Writenex Astro
3
+ *
4
+ * Manages theme state (light/dark/system) and applies the appropriate
5
+ * class to the document root element. Persists preference to localStorage.
6
+ *
7
+ * @module @writenex/astro/client/context/ThemeContext
8
+ */
9
+
10
+ import {
11
+ createContext,
12
+ useContext,
13
+ useEffect,
14
+ useState,
15
+ useCallback,
16
+ type ReactNode,
17
+ } from "react";
18
+
19
+ /**
20
+ * Available theme options
21
+ */
22
+ export type Theme = "light" | "dark" | "system";
23
+
24
+ /**
25
+ * Theme context value interface
26
+ */
27
+ interface ThemeContextValue {
28
+ /** Current theme setting */
29
+ theme: Theme;
30
+ /** Resolved theme (actual light/dark being displayed) */
31
+ resolvedTheme: "light" | "dark";
32
+ /** Update theme preference */
33
+ setTheme: (theme: Theme) => void;
34
+ }
35
+
36
+ const STORAGE_KEY = "writenex-astro-theme";
37
+
38
+ const ThemeContext = createContext<ThemeContextValue | null>(null);
39
+
40
+ /**
41
+ * Get system color scheme preference
42
+ */
43
+ function getSystemTheme(): "light" | "dark" {
44
+ if (typeof window === "undefined") return "dark";
45
+ return window.matchMedia("(prefers-color-scheme: dark)").matches
46
+ ? "dark"
47
+ : "light";
48
+ }
49
+
50
+ /**
51
+ * Get stored theme from localStorage
52
+ */
53
+ function getStoredTheme(): Theme {
54
+ if (typeof window === "undefined") return "system";
55
+ try {
56
+ const stored = localStorage.getItem(STORAGE_KEY);
57
+ if (stored === "light" || stored === "dark" || stored === "system") {
58
+ return stored;
59
+ }
60
+ } catch {
61
+ // localStorage not available
62
+ }
63
+ return "system";
64
+ }
65
+
66
+ /**
67
+ * Props for ThemeProvider component
68
+ */
69
+ interface ThemeProviderProps {
70
+ /** Child components */
71
+ children: ReactNode;
72
+ /** Default theme (optional) */
73
+ defaultTheme?: Theme;
74
+ }
75
+
76
+ /**
77
+ * Theme provider component that manages theme state and applies it to DOM.
78
+ *
79
+ * @component
80
+ * @example
81
+ * ```tsx
82
+ * <ThemeProvider>
83
+ * <App />
84
+ * </ThemeProvider>
85
+ * ```
86
+ */
87
+ export function ThemeProvider({
88
+ children,
89
+ defaultTheme = "system",
90
+ }: ThemeProviderProps): React.ReactElement {
91
+ const [theme, setThemeState] = useState<Theme>(() => {
92
+ if (typeof window === "undefined") return defaultTheme;
93
+ return getStoredTheme();
94
+ });
95
+
96
+ const [resolvedTheme, setResolvedTheme] = useState<"light" | "dark">(() => {
97
+ if (typeof window === "undefined") return "dark";
98
+ const stored = getStoredTheme();
99
+ if (stored === "system") return getSystemTheme();
100
+ return stored;
101
+ });
102
+
103
+ const setTheme = useCallback((newTheme: Theme) => {
104
+ setThemeState(newTheme);
105
+ try {
106
+ localStorage.setItem(STORAGE_KEY, newTheme);
107
+ } catch {
108
+ // localStorage not available
109
+ }
110
+ }, []);
111
+
112
+ // Apply theme to document and handle system preference changes
113
+ useEffect(() => {
114
+ const root = document.documentElement;
115
+
116
+ const applyTheme = (isDark: boolean) => {
117
+ if (isDark) {
118
+ root.classList.add("wn-dark");
119
+ root.classList.remove("wn-light");
120
+ } else {
121
+ root.classList.add("wn-light");
122
+ root.classList.remove("wn-dark");
123
+ }
124
+ setResolvedTheme(isDark ? "dark" : "light");
125
+ };
126
+
127
+ if (theme === "dark") {
128
+ applyTheme(true);
129
+ } else if (theme === "light") {
130
+ applyTheme(false);
131
+ } else {
132
+ // System preference
133
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
134
+ applyTheme(mediaQuery.matches);
135
+
136
+ const listener = (e: MediaQueryListEvent) => {
137
+ applyTheme(e.matches);
138
+ };
139
+
140
+ mediaQuery.addEventListener("change", listener);
141
+ return () => mediaQuery.removeEventListener("change", listener);
142
+ }
143
+ }, [theme]);
144
+
145
+ return (
146
+ <ThemeContext.Provider value={{ theme, resolvedTheme, setTheme }}>
147
+ {children}
148
+ </ThemeContext.Provider>
149
+ );
150
+ }
151
+
152
+ /**
153
+ * Hook to access theme context
154
+ *
155
+ * @returns Theme context value
156
+ * @throws Error if used outside ThemeProvider
157
+ *
158
+ * @example
159
+ * ```tsx
160
+ * function MyComponent() {
161
+ * const { theme, setTheme, resolvedTheme } = useTheme();
162
+ * return <button onClick={() => setTheme('dark')}>Dark</button>;
163
+ * }
164
+ * ```
165
+ */
166
+ export function useTheme(): ThemeContextValue {
167
+ const context = useContext(ThemeContext);
168
+ if (!context) {
169
+ throw new Error("useTheme must be used within a ThemeProvider");
170
+ }
171
+ return context;
172
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * @fileoverview Announce hook for screen reader announcements
3
+ *
4
+ * This hook provides a way to announce messages to screen readers
5
+ * using ARIA live regions. It supports both polite and assertive
6
+ * politeness levels and handles rapid announcement queuing.
7
+ *
8
+ * ## Features:
9
+ * - Polite and assertive announcement modes
10
+ * - Message queuing for rapid announcements
11
+ * - Automatic cleanup of old announcements
12
+ * - Integration with global live region
13
+ *
14
+ * @module @writenex/astro/client/hooks/useAnnounce
15
+ */
16
+
17
+ import { useCallback, useEffect, useRef, useState } from "react";
18
+
19
+ /**
20
+ * Politeness level for announcements
21
+ * - polite: Waits for user to finish current task before announcing
22
+ * - assertive: Interrupts current task to announce immediately
23
+ */
24
+ export type AnnouncePoliteness = "polite" | "assertive";
25
+
26
+ /**
27
+ * Announcement message structure
28
+ */
29
+ export interface Announcement {
30
+ /** Unique identifier for the announcement */
31
+ id: string;
32
+ /** Message to announce */
33
+ message: string;
34
+ /** Politeness level */
35
+ politeness: AnnouncePoliteness;
36
+ /** Timestamp when announcement was created */
37
+ timestamp: number;
38
+ }
39
+
40
+ /**
41
+ * Return value from useAnnounce hook
42
+ */
43
+ export interface UseAnnounceReturn {
44
+ /** Announce a message to screen readers */
45
+ announce: (message: string, politeness?: AnnouncePoliteness) => void;
46
+ /** Current announcement message (for live region) */
47
+ currentMessage: string;
48
+ /** Current politeness level */
49
+ currentPoliteness: AnnouncePoliteness;
50
+ /** Clear the current announcement */
51
+ clear: () => void;
52
+ }
53
+
54
+ /** Maximum age for announcements in milliseconds (5 seconds) */
55
+ const MAX_ANNOUNCEMENT_AGE = 5000;
56
+
57
+ /** Delay between processing queued announcements */
58
+ const ANNOUNCEMENT_DELAY = 100;
59
+
60
+ /** Counter for generating unique announcement IDs */
61
+ let announcementIdCounter = 0;
62
+
63
+ /**
64
+ * Generate a unique announcement ID
65
+ */
66
+ function generateAnnouncementId(): string {
67
+ return `announcement-${++announcementIdCounter}-${Date.now()}`;
68
+ }
69
+
70
+ /**
71
+ * Hook for announcing messages to screen readers via live regions
72
+ *
73
+ * This hook manages a queue of announcements and processes them
74
+ * sequentially to ensure screen readers can properly announce each
75
+ * message. It automatically discards stale announcements.
76
+ *
77
+ * @returns Object containing announce function and current message state
78
+ *
79
+ * @example
80
+ * ```tsx
81
+ * function SaveButton() {
82
+ * const { announce } = useAnnounce();
83
+ *
84
+ * const handleSave = async () => {
85
+ * try {
86
+ * await saveContent();
87
+ * announce("Content saved", "polite");
88
+ * } catch (error) {
89
+ * announce("Save failed", "assertive");
90
+ * }
91
+ * };
92
+ *
93
+ * return <button onClick={handleSave}>Save</button>;
94
+ * }
95
+ * ```
96
+ */
97
+ export function useAnnounce(): UseAnnounceReturn {
98
+ const [currentMessage, setCurrentMessage] = useState("");
99
+ const [currentPoliteness, setCurrentPoliteness] =
100
+ useState<AnnouncePoliteness>("polite");
101
+
102
+ const queueRef = useRef<Announcement[]>([]);
103
+ const isProcessingRef = useRef(false);
104
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
105
+
106
+ /**
107
+ * Process the next announcement in the queue
108
+ */
109
+ const processQueue = useCallback(() => {
110
+ if (isProcessingRef.current || queueRef.current.length === 0) {
111
+ return;
112
+ }
113
+
114
+ isProcessingRef.current = true;
115
+
116
+ // Get the next announcement
117
+ const announcement = queueRef.current.shift();
118
+
119
+ if (!announcement) {
120
+ isProcessingRef.current = false;
121
+ return;
122
+ }
123
+
124
+ // Check if announcement is stale
125
+ const age = Date.now() - announcement.timestamp;
126
+ if (age > MAX_ANNOUNCEMENT_AGE) {
127
+ // Skip stale announcement and process next
128
+ isProcessingRef.current = false;
129
+ processQueue();
130
+ return;
131
+ }
132
+
133
+ // Clear current message first to ensure screen reader announces new message
134
+ setCurrentMessage("");
135
+
136
+ // Set the new message after a brief delay
137
+ timeoutRef.current = setTimeout(() => {
138
+ setCurrentMessage(announcement.message);
139
+ setCurrentPoliteness(announcement.politeness);
140
+
141
+ // Clear after announcement and process next
142
+ timeoutRef.current = setTimeout(() => {
143
+ setCurrentMessage("");
144
+ isProcessingRef.current = false;
145
+ processQueue();
146
+ }, ANNOUNCEMENT_DELAY);
147
+ }, ANNOUNCEMENT_DELAY);
148
+ }, []);
149
+
150
+ /**
151
+ * Announce a message to screen readers
152
+ */
153
+ const announce = useCallback(
154
+ (message: string, politeness: AnnouncePoliteness = "polite") => {
155
+ if (!message.trim()) return;
156
+
157
+ const announcement: Announcement = {
158
+ id: generateAnnouncementId(),
159
+ message: message.trim(),
160
+ politeness,
161
+ timestamp: Date.now(),
162
+ };
163
+
164
+ // Add to queue
165
+ queueRef.current.push(announcement);
166
+
167
+ // Start processing if not already
168
+ processQueue();
169
+ },
170
+ [processQueue]
171
+ );
172
+
173
+ /**
174
+ * Clear the current announcement
175
+ */
176
+ const clear = useCallback(() => {
177
+ setCurrentMessage("");
178
+ queueRef.current = [];
179
+ if (timeoutRef.current) {
180
+ clearTimeout(timeoutRef.current);
181
+ timeoutRef.current = null;
182
+ }
183
+ isProcessingRef.current = false;
184
+ }, []);
185
+
186
+ // Cleanup on unmount
187
+ useEffect(() => {
188
+ return () => {
189
+ if (timeoutRef.current) {
190
+ clearTimeout(timeoutRef.current);
191
+ }
192
+ };
193
+ }, []);
194
+
195
+ return {
196
+ announce,
197
+ currentMessage,
198
+ currentPoliteness,
199
+ clear,
200
+ };
201
+ }