@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,300 @@
1
+ /**
2
+ * @fileoverview Search and Replace Panel Styles
3
+ *
4
+ * Styles for the search and replace panel component.
5
+ * Uses CSS variables from the main styles.css.
6
+ */
7
+
8
+ /* Screen reader only - for ARIA announcements */
9
+ .wn-sr-only {
10
+ position: absolute;
11
+ width: 1px;
12
+ height: 1px;
13
+ padding: 0;
14
+ margin: -1px;
15
+ overflow: hidden;
16
+ clip: rect(0, 0, 0, 0);
17
+ white-space: nowrap;
18
+ border: 0;
19
+ }
20
+
21
+ /* Panel container */
22
+ .wn-search-panel {
23
+ position: absolute;
24
+ top: 0;
25
+ right: 0;
26
+ z-index: var(--wn-z-modal);
27
+ background-color: var(--wn-zinc-900);
28
+ border: 1px solid var(--wn-zinc-700);
29
+ border-top: none;
30
+ border-right: none;
31
+ border-bottom-left-radius: var(--wn-radius-lg);
32
+ box-shadow: var(--wn-shadow-md);
33
+ padding: var(--wn-space-4);
34
+ }
35
+
36
+ .wn-search-panel-content {
37
+ display: flex;
38
+ flex-direction: column;
39
+ gap: var(--wn-space-3);
40
+ }
41
+
42
+ /* Search row */
43
+ .wn-search-row {
44
+ display: flex;
45
+ align-items: center;
46
+ gap: var(--wn-space-3);
47
+ }
48
+
49
+ /* Input wrapper */
50
+ .wn-search-input-wrapper {
51
+ position: relative;
52
+ flex-shrink: 0;
53
+ }
54
+
55
+ /* Search input */
56
+ .wn-search-input {
57
+ width: 180px;
58
+ padding: var(--wn-space-2) var(--wn-icon-btn-md) var(--wn-space-2)
59
+ var(--wn-space-3);
60
+ font-size: var(--wn-font-sm);
61
+ color: var(--wn-zinc-100);
62
+ border-radius: var(--wn-radius-sm);
63
+ outline: none;
64
+ box-shadow: none;
65
+ -webkit-appearance: none;
66
+ -moz-appearance: none;
67
+ appearance: none;
68
+ transition: border-color var(--wn-transition-fast);
69
+ }
70
+
71
+ .wn-search-input::placeholder {
72
+ color: var(--wn-zinc-500);
73
+ }
74
+
75
+ .wn-search-input:focus {
76
+ border-color: var(--wn-brand-500);
77
+ outline: none;
78
+ box-shadow: none;
79
+ }
80
+
81
+ .wn-search-input:focus-visible {
82
+ outline: none;
83
+ box-shadow: none;
84
+ }
85
+
86
+ .wn-search-input--disabled {
87
+ opacity: 0.5;
88
+ cursor: not-allowed;
89
+ }
90
+
91
+ /* Clear button inside input */
92
+ .wn-search-clear {
93
+ position: absolute;
94
+ top: 50%;
95
+ right: var(--wn-space-2);
96
+ transform: translateY(-50%);
97
+ display: flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ width: 18px;
101
+ height: 18px;
102
+ padding: 0;
103
+ background: none;
104
+ border: none;
105
+ color: var(--wn-zinc-500);
106
+ cursor: pointer;
107
+ border-radius: var(--wn-radius-sm);
108
+ transition:
109
+ color var(--wn-transition-fast),
110
+ background-color var(--wn-transition-fast);
111
+ }
112
+
113
+ .wn-search-clear:hover {
114
+ color: var(--wn-zinc-200);
115
+ background-color: var(--wn-zinc-700);
116
+ }
117
+
118
+ /* Search options (toggle buttons) */
119
+ .wn-search-options {
120
+ display: flex;
121
+ gap: var(--wn-space-1);
122
+ }
123
+
124
+ .wn-search-option {
125
+ display: flex;
126
+ align-items: center;
127
+ justify-content: center;
128
+ width: var(--wn-icon-btn-md);
129
+ height: var(--wn-icon-btn-md);
130
+ padding: 0;
131
+ background: none;
132
+ border: none;
133
+ color: var(--wn-zinc-500);
134
+ cursor: pointer;
135
+ border-radius: var(--wn-radius-sm);
136
+ transition:
137
+ color var(--wn-transition-fast),
138
+ background-color var(--wn-transition-fast);
139
+ }
140
+
141
+ .wn-search-option:hover {
142
+ color: var(--wn-zinc-200);
143
+ background-color: var(--wn-zinc-800);
144
+ }
145
+
146
+ .wn-search-option--active {
147
+ color: var(--wn-brand-400);
148
+ background-color: var(--wn-brand-alpha-15);
149
+ }
150
+
151
+ .wn-search-option--active:hover {
152
+ color: var(--wn-brand-400);
153
+ background-color: var(--wn-brand-alpha-20);
154
+ }
155
+
156
+ /* Match counter */
157
+ .wn-search-counter {
158
+ min-width: 70px;
159
+ text-align: center;
160
+ font-size: var(--wn-font-xs);
161
+ color: var(--wn-zinc-500);
162
+ white-space: nowrap;
163
+ }
164
+
165
+ /* Navigation buttons */
166
+ .wn-search-nav {
167
+ display: flex;
168
+ gap: var(--wn-space-1);
169
+ }
170
+
171
+ .wn-search-nav-btn {
172
+ display: flex;
173
+ align-items: center;
174
+ justify-content: center;
175
+ width: var(--wn-icon-btn-md);
176
+ height: var(--wn-icon-btn-md);
177
+ padding: 0;
178
+ background: none;
179
+ border: none;
180
+ color: var(--wn-zinc-400);
181
+ cursor: pointer;
182
+ border-radius: var(--wn-radius-sm);
183
+ transition:
184
+ color var(--wn-transition-fast),
185
+ background-color var(--wn-transition-fast);
186
+ }
187
+
188
+ .wn-search-nav-btn:hover:not(:disabled) {
189
+ color: var(--wn-zinc-100);
190
+ background-color: var(--wn-zinc-800);
191
+ }
192
+
193
+ .wn-search-nav-btn:disabled {
194
+ opacity: 0.4;
195
+ cursor: not-allowed;
196
+ }
197
+
198
+ /* Replace action buttons */
199
+ .wn-search-actions {
200
+ display: flex;
201
+ gap: 2px;
202
+ margin-left: auto;
203
+ }
204
+
205
+ .wn-search-action-btn {
206
+ display: flex;
207
+ align-items: center;
208
+ justify-content: center;
209
+ width: var(--wn-icon-btn-md);
210
+ height: var(--wn-icon-btn-md);
211
+ padding: 0;
212
+ background: none;
213
+ border: none;
214
+ color: var(--wn-zinc-400);
215
+ cursor: pointer;
216
+ border-radius: var(--wn-radius-sm);
217
+ transition:
218
+ color var(--wn-transition-fast),
219
+ background-color var(--wn-transition-fast);
220
+ }
221
+
222
+ .wn-search-action-btn:hover:not(:disabled) {
223
+ color: var(--wn-zinc-100);
224
+ background-color: var(--wn-zinc-800);
225
+ }
226
+
227
+ .wn-search-action-btn:disabled {
228
+ opacity: 0.4;
229
+ cursor: not-allowed;
230
+ }
231
+
232
+ /* ============================================================================
233
+ LIGHT MODE OVERRIDES
234
+ ============================================================================ */
235
+
236
+ .wn-light .wn-search-panel {
237
+ background-color: #fff;
238
+ border-color: var(--wn-zinc-200);
239
+ box-shadow: 0 4px 12px var(--wn-overlay-light-10);
240
+ }
241
+
242
+ .wn-light .wn-search-input {
243
+ color: var(--wn-zinc-900);
244
+ background-color: var(--wn-zinc-50);
245
+ border-color: var(--wn-zinc-200);
246
+ }
247
+
248
+ .wn-light .wn-search-input::placeholder {
249
+ color: var(--wn-zinc-400);
250
+ }
251
+
252
+ .wn-light .wn-search-clear {
253
+ color: var(--wn-zinc-400);
254
+ }
255
+
256
+ .wn-light .wn-search-clear:hover {
257
+ color: var(--wn-zinc-900);
258
+ background-color: var(--wn-zinc-200);
259
+ }
260
+
261
+ .wn-light .wn-search-option {
262
+ color: var(--wn-zinc-400);
263
+ }
264
+
265
+ .wn-light .wn-search-option:hover {
266
+ color: var(--wn-zinc-900);
267
+ background-color: var(--wn-zinc-100);
268
+ }
269
+
270
+ .wn-light .wn-search-option--active {
271
+ color: var(--wn-brand-600);
272
+ background-color: var(--wn-brand-alpha-10);
273
+ }
274
+
275
+ .wn-light .wn-search-option--active:hover {
276
+ color: var(--wn-brand-600);
277
+ background-color: var(--wn-brand-alpha-15);
278
+ }
279
+
280
+ .wn-light .wn-search-counter {
281
+ color: var(--wn-zinc-500);
282
+ }
283
+
284
+ .wn-light .wn-search-nav-btn {
285
+ color: var(--wn-zinc-500);
286
+ }
287
+
288
+ .wn-light .wn-search-nav-btn:hover:not(:disabled) {
289
+ color: var(--wn-zinc-900);
290
+ background-color: var(--wn-zinc-100);
291
+ }
292
+
293
+ .wn-light .wn-search-action-btn {
294
+ color: var(--wn-zinc-500);
295
+ }
296
+
297
+ .wn-light .wn-search-action-btn:hover:not(:disabled) {
298
+ color: var(--wn-zinc-900);
299
+ background-color: var(--wn-zinc-100);
300
+ }
@@ -0,0 +1,332 @@
1
+ /**
2
+ * @fileoverview Search and Replace Panel Component
3
+ *
4
+ * Provides search and replace functionality for the markdown editor.
5
+ * Supports case sensitivity, whole word matching, and regular expressions.
6
+ *
7
+ * @module @writenex/astro/client/components/SearchReplace
8
+ */
9
+
10
+ import { useState, useCallback, useEffect, useRef } from "react";
11
+ import {
12
+ X,
13
+ ChevronUp,
14
+ ChevronDown,
15
+ CaseSensitive,
16
+ WholeWord,
17
+ Regex,
18
+ Replace,
19
+ ReplaceAll,
20
+ } from "lucide-react";
21
+ import "./SearchReplacePanel.css";
22
+
23
+ /**
24
+ * Search options configuration
25
+ */
26
+ export interface SearchOptions {
27
+ /** Whether to match case exactly */
28
+ caseSensitive: boolean;
29
+ /** Whether to match whole words only */
30
+ wholeWord: boolean;
31
+ /** Whether to treat query as a regular expression */
32
+ regex: boolean;
33
+ }
34
+
35
+ /**
36
+ * Props for the SearchReplacePanel component
37
+ */
38
+ interface SearchReplacePanelProps {
39
+ /** Whether the panel is open */
40
+ isOpen: boolean;
41
+ /** Callback to close the panel */
42
+ onClose: () => void;
43
+ /** Callback to perform search */
44
+ onSearch: (query: string, options: SearchOptions) => number;
45
+ /** Callback to navigate to next match */
46
+ onNextMatch: () => void;
47
+ /** Callback to navigate to previous match */
48
+ onPreviousMatch: () => void;
49
+ /** Callback to replace current match */
50
+ onReplace: (replacement: string) => void;
51
+ /** Callback to replace all matches */
52
+ onReplaceAll: (replacement: string) => number;
53
+ /** Current match index (1-based) */
54
+ currentMatch: number;
55
+ /** Total number of matches */
56
+ totalMatches: number;
57
+ /** Whether editor is read-only */
58
+ readOnly?: boolean;
59
+ }
60
+
61
+ /**
62
+ * Search and Replace panel component
63
+ *
64
+ * @component
65
+ */
66
+ export function SearchReplacePanel({
67
+ isOpen,
68
+ onClose,
69
+ onSearch,
70
+ onNextMatch,
71
+ onPreviousMatch,
72
+ onReplace,
73
+ onReplaceAll,
74
+ currentMatch,
75
+ totalMatches,
76
+ readOnly = false,
77
+ }: SearchReplacePanelProps): React.ReactElement | null {
78
+ const [searchQuery, setSearchQuery] = useState("");
79
+ const [replaceQuery, setReplaceQuery] = useState("");
80
+ const [caseSensitive, setCaseSensitive] = useState(false);
81
+ const [wholeWord, setWholeWord] = useState(false);
82
+ const [regex, setRegex] = useState(false);
83
+ const searchInputRef = useRef<HTMLInputElement>(null);
84
+
85
+ // Focus search input when panel opens
86
+ useEffect(() => {
87
+ if (isOpen && searchInputRef.current) {
88
+ searchInputRef.current.focus();
89
+ searchInputRef.current.select();
90
+ }
91
+ }, [isOpen]);
92
+
93
+ // Handle keyboard shortcuts
94
+ useEffect(() => {
95
+ if (!isOpen) return;
96
+
97
+ const handleKeyDown = (e: KeyboardEvent) => {
98
+ if (e.key === "Escape") {
99
+ onClose();
100
+ } else if (e.key === "Enter" && !e.shiftKey) {
101
+ e.preventDefault();
102
+ onNextMatch();
103
+ } else if (e.key === "Enter" && e.shiftKey) {
104
+ e.preventDefault();
105
+ onPreviousMatch();
106
+ }
107
+ };
108
+
109
+ window.addEventListener("keydown", handleKeyDown);
110
+ return () => window.removeEventListener("keydown", handleKeyDown);
111
+ }, [isOpen, onClose, onNextMatch, onPreviousMatch]);
112
+
113
+ const handleSearchChange = useCallback(
114
+ (value: string) => {
115
+ setSearchQuery(value);
116
+ onSearch(value, { caseSensitive, wholeWord, regex });
117
+ },
118
+ [caseSensitive, wholeWord, regex, onSearch]
119
+ );
120
+
121
+ const handleOptionsChange = useCallback(
122
+ (option: "caseSensitive" | "wholeWord" | "regex", value: boolean) => {
123
+ let newCaseSensitive = caseSensitive;
124
+ let newWholeWord = wholeWord;
125
+ let newRegex = regex;
126
+
127
+ switch (option) {
128
+ case "caseSensitive":
129
+ newCaseSensitive = value;
130
+ setCaseSensitive(value);
131
+ break;
132
+ case "wholeWord":
133
+ newWholeWord = value;
134
+ setWholeWord(value);
135
+ break;
136
+ case "regex":
137
+ newRegex = value;
138
+ setRegex(value);
139
+ break;
140
+ }
141
+
142
+ onSearch(searchQuery, {
143
+ caseSensitive: newCaseSensitive,
144
+ wholeWord: newWholeWord,
145
+ regex: newRegex,
146
+ });
147
+ },
148
+ [caseSensitive, wholeWord, regex, searchQuery, onSearch]
149
+ );
150
+
151
+ const handleReplace = useCallback(() => {
152
+ if (!readOnly && totalMatches > 0) {
153
+ onReplace(replaceQuery);
154
+ }
155
+ }, [readOnly, totalMatches, onReplace, replaceQuery]);
156
+
157
+ const handleReplaceAll = useCallback(() => {
158
+ if (!readOnly && totalMatches > 0) {
159
+ const count = onReplaceAll(replaceQuery);
160
+ // ARIA announcement
161
+ const announcement = document.createElement("div");
162
+ announcement.setAttribute("role", "status");
163
+ announcement.setAttribute("aria-live", "polite");
164
+ announcement.className = "wn-sr-only";
165
+ announcement.textContent = `Replaced ${count} occurrences`;
166
+ document.body.appendChild(announcement);
167
+ setTimeout(() => announcement.remove(), 1000);
168
+ }
169
+ }, [readOnly, totalMatches, onReplaceAll, replaceQuery]);
170
+
171
+ if (!isOpen) return null;
172
+
173
+ return (
174
+ <div
175
+ className="wn-search-panel"
176
+ role="search"
177
+ aria-label="Search and replace"
178
+ >
179
+ <div className="wn-search-panel-content">
180
+ {/* Search Row */}
181
+ <div className="wn-search-row">
182
+ {/* Search Input */}
183
+ <div className="wn-search-input-wrapper">
184
+ <input
185
+ ref={searchInputRef}
186
+ type="text"
187
+ placeholder="Search..."
188
+ value={searchQuery}
189
+ onChange={(e) => handleSearchChange(e.target.value)}
190
+ className="wn-search-input"
191
+ aria-label="Search query"
192
+ />
193
+ {searchQuery && (
194
+ <button
195
+ onClick={() => {
196
+ handleSearchChange("");
197
+ searchInputRef.current?.focus();
198
+ }}
199
+ className="wn-search-clear"
200
+ aria-label="Clear search"
201
+ >
202
+ <X size={12} />
203
+ </button>
204
+ )}
205
+ </div>
206
+
207
+ {/* Search Options */}
208
+ <div className="wn-search-options">
209
+ <button
210
+ className={`wn-search-option ${caseSensitive ? "wn-search-option--active" : ""}`}
211
+ onClick={() =>
212
+ handleOptionsChange("caseSensitive", !caseSensitive)
213
+ }
214
+ aria-pressed={caseSensitive}
215
+ aria-label="Case sensitive"
216
+ title="Case sensitive"
217
+ >
218
+ <CaseSensitive size={16} />
219
+ </button>
220
+ <button
221
+ className={`wn-search-option ${wholeWord ? "wn-search-option--active" : ""}`}
222
+ onClick={() => handleOptionsChange("wholeWord", !wholeWord)}
223
+ aria-pressed={wholeWord}
224
+ aria-label="Whole word"
225
+ title="Whole word"
226
+ >
227
+ <WholeWord size={16} />
228
+ </button>
229
+ <button
230
+ className={`wn-search-option ${regex ? "wn-search-option--active" : ""}`}
231
+ onClick={() => handleOptionsChange("regex", !regex)}
232
+ aria-pressed={regex}
233
+ aria-label="Regular expression"
234
+ title="Regular expression"
235
+ >
236
+ <Regex size={16} />
237
+ </button>
238
+ </div>
239
+
240
+ {/* Match Counter */}
241
+ <span className="wn-search-counter">
242
+ {totalMatches > 0
243
+ ? `${currentMatch} of ${totalMatches}`
244
+ : "No results"}
245
+ </span>
246
+
247
+ {/* Navigation */}
248
+ <div className="wn-search-nav">
249
+ <button
250
+ className="wn-search-nav-btn"
251
+ onClick={onPreviousMatch}
252
+ disabled={totalMatches === 0}
253
+ aria-label="Previous match"
254
+ title="Previous match (Shift+Enter)"
255
+ >
256
+ <ChevronUp size={16} />
257
+ </button>
258
+ <button
259
+ className="wn-search-nav-btn"
260
+ onClick={onNextMatch}
261
+ disabled={totalMatches === 0}
262
+ aria-label="Next match"
263
+ title="Next match (Enter)"
264
+ >
265
+ <ChevronDown size={16} />
266
+ </button>
267
+ <button
268
+ className="wn-search-nav-btn"
269
+ onClick={onClose}
270
+ aria-label="Close search"
271
+ title="Close (Escape)"
272
+ >
273
+ <X size={16} />
274
+ </button>
275
+ </div>
276
+ </div>
277
+
278
+ {/* Replace Row */}
279
+ <div className="wn-search-row">
280
+ <div className="wn-search-input-wrapper">
281
+ <input
282
+ type="text"
283
+ placeholder="Replace..."
284
+ value={replaceQuery}
285
+ onChange={(e) => setReplaceQuery(e.target.value)}
286
+ disabled={readOnly}
287
+ className={`wn-search-input ${readOnly ? "wn-search-input--disabled" : ""}`}
288
+ aria-label="Replace query"
289
+ />
290
+ {replaceQuery && !readOnly && (
291
+ <button
292
+ onClick={() => setReplaceQuery("")}
293
+ className="wn-search-clear"
294
+ aria-label="Clear replace"
295
+ >
296
+ <X size={12} />
297
+ </button>
298
+ )}
299
+ </div>
300
+
301
+ {/* Replace Actions */}
302
+ <div className="wn-search-actions">
303
+ <button
304
+ className="wn-search-action-btn"
305
+ onClick={handleReplace}
306
+ disabled={readOnly || totalMatches === 0}
307
+ aria-label="Replace current match"
308
+ title={
309
+ readOnly ? "Replace disabled in read-only mode" : "Replace"
310
+ }
311
+ >
312
+ <Replace size={16} />
313
+ </button>
314
+ <button
315
+ className="wn-search-action-btn"
316
+ onClick={handleReplaceAll}
317
+ disabled={readOnly || totalMatches === 0}
318
+ aria-label="Replace all matches"
319
+ title={
320
+ readOnly
321
+ ? "Replace all disabled in read-only mode"
322
+ : "Replace all"
323
+ }
324
+ >
325
+ <ReplaceAll size={16} />
326
+ </button>
327
+ </div>
328
+ </div>
329
+ </div>
330
+ </div>
331
+ );
332
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @fileoverview Search and Replace component exports
3
+ *
4
+ * @module @writenex/astro/client/components/SearchReplace
5
+ */
6
+
7
+ export { SearchReplacePanel, type SearchOptions } from "./SearchReplacePanel";