@autumnsgrove/groveengine 0.4.8 → 0.4.9

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.
@@ -39,6 +39,7 @@
39
39
  let showPreview = $state(true);
40
40
  let cursorLine = $state(1);
41
41
  let cursorCol = $state(1);
42
+ let isUpdating = $state(false);
42
43
 
43
44
  // Image upload state
44
45
  let isDragging = $state(false);
@@ -326,7 +327,8 @@
326
327
 
327
328
  // Text manipulation helpers
328
329
  function wrapSelection(before, after) {
329
- if (!textareaRef) return;
330
+ if (!textareaRef || isUpdating) return;
331
+ isUpdating = true;
330
332
  const start = textareaRef.selectionStart;
331
333
  const end = textareaRef.selectionEnd;
332
334
  const selectedText = content.substring(start, end);
@@ -335,16 +337,19 @@
335
337
  textareaRef.selectionStart = start + before.length;
336
338
  textareaRef.selectionEnd = end + before.length;
337
339
  textareaRef.focus();
340
+ isUpdating = false;
338
341
  }, 0);
339
342
  }
340
343
 
341
344
  function insertAtCursor(text) {
342
- if (!textareaRef) return;
345
+ if (!textareaRef || isUpdating) return;
346
+ isUpdating = true;
343
347
  const start = textareaRef.selectionStart;
344
348
  content = content.substring(0, start) + text + content.substring(start);
345
349
  setTimeout(() => {
346
350
  textareaRef.selectionStart = textareaRef.selectionEnd = start + text.length;
347
351
  textareaRef.focus();
352
+ isUpdating = false;
348
353
  }, 0);
349
354
  }
350
355
 
@@ -362,10 +367,18 @@
362
367
  }
363
368
 
364
369
  function insertCodeBlock() {
370
+ if (!textareaRef || isUpdating) return;
371
+ isUpdating = true;
365
372
  const start = textareaRef.selectionStart;
366
- const selectedText = content.substring(start, textareaRef.selectionEnd);
373
+ const end = textareaRef.selectionEnd;
374
+ const selectedText = content.substring(start, end);
367
375
  const codeBlock = "```\n" + (selectedText || "code here") + "\n```";
368
- content = content.substring(0, start) + codeBlock + content.substring(textareaRef.selectionEnd);
376
+ content = content.substring(0, start) + codeBlock + content.substring(end);
377
+ setTimeout(() => {
378
+ textareaRef.selectionStart = textareaRef.selectionEnd = start + codeBlock.length;
379
+ textareaRef.focus();
380
+ isUpdating = false;
381
+ }, 0);
369
382
  }
370
383
 
371
384
  function insertList() {
@@ -691,7 +704,9 @@
691
704
  </div>
692
705
  <div class="preview-content" bind:this={previewRef}>
693
706
  {#if previewHtml}
694
- {@html previewHtml}
707
+ {#key previewHtml}
708
+ <div>{@html previewHtml}</div>
709
+ {/key}
695
710
  {:else}
696
711
  <p class="preview-placeholder">
697
712
  Your rendered markdown will appear here...
@@ -1006,7 +1021,9 @@
1006
1021
 
1007
1022
  <div class="content-body">
1008
1023
  {#if previewHtml}
1009
- {@html previewHtml}
1024
+ {#key previewHtml}
1025
+ <div>{@html previewHtml}</div>
1026
+ {/key}
1010
1027
  {:else}
1011
1028
  <p class="preview-placeholder">Start writing to see your content here...</p>
1012
1029
  {/if}
@@ -1845,8 +1862,9 @@
1845
1862
  border: 1px solid var(--light-border-primary);
1846
1863
  border-radius: 12px;
1847
1864
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
1848
- z-index: 1001;
1865
+ z-index: 1003; /* above modals and gutters */
1849
1866
  animation: slide-up 0.2s ease;
1867
+ overflow: hidden;
1850
1868
  }
1851
1869
  @keyframes slide-up {
1852
1870
  from { opacity: 0; transform: translateY(10px); }
package/dist/utils/api.js CHANGED
@@ -11,99 +11,114 @@
11
11
  * @throws {Error} If request fails
12
12
  */
13
13
  export async function apiRequest(url, options = {}) {
14
- const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
14
+ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
15
15
 
16
- // Build headers - don't set Content-Type for FormData (browser sets it with boundary)
17
- const headers = {
18
- ...(csrfToken && { 'X-CSRF-Token': csrfToken }),
19
- ...options.headers
20
- };
16
+ // Debug logging
17
+ if (typeof console !== "undefined" && process.env.NODE_ENV !== "production") {
18
+ console.debug("[apiRequest]", {
19
+ url,
20
+ csrfToken: csrfToken ? "present" : "missing",
21
+ });
22
+ }
21
23
 
22
- // Only add Content-Type if not FormData
23
- if (!(options.body instanceof FormData)) {
24
- headers['Content-Type'] = 'application/json';
25
- }
24
+ // Build headers - don't set Content-Type for FormData (browser sets it with boundary)
25
+ const headers = {
26
+ ...(csrfToken && { "X-CSRF-Token": csrfToken }),
27
+ ...options.headers,
28
+ };
26
29
 
27
- const response = await fetch(url, {
28
- ...options,
29
- headers
30
- });
30
+ // Only add Content-Type if not FormData
31
+ if (!(options.body instanceof FormData)) {
32
+ headers["Content-Type"] = "application/json";
33
+ }
31
34
 
32
- if (!response.ok) {
33
- let errorMessage = 'Request failed';
34
- try {
35
- const error = await response.json();
36
- errorMessage = error.message || errorMessage;
37
- } catch {
38
- errorMessage = `${response.status} ${response.statusText}`;
39
- }
40
- throw new Error(errorMessage);
41
- }
35
+ const response = await fetch(url, {
36
+ ...options,
37
+ headers,
38
+ });
42
39
 
43
- // Handle empty responses (204 No Content)
44
- if (response.status === 204) {
45
- return null;
46
- }
40
+ if (!response.ok) {
41
+ let errorMessage = "Request failed";
42
+ try {
43
+ const error = await response.json();
44
+ errorMessage = error.message || errorMessage;
45
+ } catch {
46
+ errorMessage = `${response.status} ${response.statusText}`;
47
+ }
48
+ // Include CSRF debug info
49
+ if (response.status === 403 && errorMessage.includes("CSRF")) {
50
+ console.error("[apiRequest] CSRF token validation failed", {
51
+ csrfToken,
52
+ headers: Object.fromEntries(response.headers.entries()),
53
+ url,
54
+ });
55
+ }
56
+ throw new Error(errorMessage);
57
+ }
47
58
 
48
- return response.json();
59
+ // Handle empty responses (204 No Content)
60
+ if (response.status === 204) {
61
+ return null;
62
+ }
63
+
64
+ return response.json();
49
65
  }
50
66
 
51
67
  /**
52
68
  * Convenience methods for common HTTP verbs
53
69
  */
54
70
  export const api = {
55
- /**
56
- * GET request
57
- * @param {string} url - API endpoint
58
- * @param {RequestInit} options - Additional fetch options
59
- */
60
- get: (url, options = {}) =>
61
- apiRequest(url, { ...options, method: 'GET' }),
71
+ /**
72
+ * GET request
73
+ * @param {string} url - API endpoint
74
+ * @param {RequestInit} options - Additional fetch options
75
+ */
76
+ get: (url, options = {}) => apiRequest(url, { ...options, method: "GET" }),
62
77
 
63
- /**
64
- * POST request
65
- * @param {string} url - API endpoint
66
- * @param {any} body - Request body (will be JSON stringified)
67
- * @param {RequestInit} options - Additional fetch options
68
- */
69
- post: (url, body, options = {}) =>
70
- apiRequest(url, {
71
- ...options,
72
- method: 'POST',
73
- body: JSON.stringify(body)
74
- }),
78
+ /**
79
+ * POST request
80
+ * @param {string} url - API endpoint
81
+ * @param {any} body - Request body (will be JSON stringified)
82
+ * @param {RequestInit} options - Additional fetch options
83
+ */
84
+ post: (url, body, options = {}) =>
85
+ apiRequest(url, {
86
+ ...options,
87
+ method: "POST",
88
+ body: JSON.stringify(body),
89
+ }),
75
90
 
76
- /**
77
- * PUT request
78
- * @param {string} url - API endpoint
79
- * @param {any} body - Request body (will be JSON stringified)
80
- * @param {RequestInit} options - Additional fetch options
81
- */
82
- put: (url, body, options = {}) =>
83
- apiRequest(url, {
84
- ...options,
85
- method: 'PUT',
86
- body: JSON.stringify(body)
87
- }),
91
+ /**
92
+ * PUT request
93
+ * @param {string} url - API endpoint
94
+ * @param {any} body - Request body (will be JSON stringified)
95
+ * @param {RequestInit} options - Additional fetch options
96
+ */
97
+ put: (url, body, options = {}) =>
98
+ apiRequest(url, {
99
+ ...options,
100
+ method: "PUT",
101
+ body: JSON.stringify(body),
102
+ }),
88
103
 
89
- /**
90
- * DELETE request
91
- * @param {string} url - API endpoint
92
- * @param {RequestInit} options - Additional fetch options
93
- */
94
- delete: (url, options = {}) =>
95
- apiRequest(url, { ...options, method: 'DELETE' }),
104
+ /**
105
+ * DELETE request
106
+ * @param {string} url - API endpoint
107
+ * @param {RequestInit} options - Additional fetch options
108
+ */
109
+ delete: (url, options = {}) =>
110
+ apiRequest(url, { ...options, method: "DELETE" }),
96
111
 
97
- /**
98
- * PATCH request
99
- * @param {string} url - API endpoint
100
- * @param {any} body - Request body (will be JSON stringified)
101
- * @param {RequestInit} options - Additional fetch options
102
- */
103
- patch: (url, body, options = {}) =>
104
- apiRequest(url, {
105
- ...options,
106
- method: 'PATCH',
107
- body: JSON.stringify(body)
108
- })
112
+ /**
113
+ * PATCH request
114
+ * @param {string} url - API endpoint
115
+ * @param {any} body - Request body (will be JSON stringified)
116
+ * @param {RequestInit} options - Additional fetch options
117
+ */
118
+ patch: (url, body, options = {}) =>
119
+ apiRequest(url, {
120
+ ...options,
121
+ method: "PATCH",
122
+ body: JSON.stringify(body),
123
+ }),
109
124
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autumnsgrove/groveengine",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "description": "Multi-tenant blog engine for Grove Platform. Features gutter annotations, markdown editing, magic code auth, and Cloudflare Workers deployment.",
5
5
  "author": "AutumnsGrove",
6
6
  "license": "MIT",