@autumnsgrove/groveengine 0.4.8 → 0.4.10

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,115 @@
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
+ ...(csrfToken && { "csrf-token": csrfToken }), // fallback header
28
+ ...options.headers,
29
+ };
26
30
 
27
- const response = await fetch(url, {
28
- ...options,
29
- headers
30
- });
31
+ // Only add Content-Type if not FormData
32
+ if (!(options.body instanceof FormData)) {
33
+ headers["Content-Type"] = "application/json";
34
+ }
31
35
 
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
- }
36
+ const response = await fetch(url, {
37
+ ...options,
38
+ headers,
39
+ });
42
40
 
43
- // Handle empty responses (204 No Content)
44
- if (response.status === 204) {
45
- return null;
46
- }
41
+ if (!response.ok) {
42
+ let errorMessage = "Request failed";
43
+ try {
44
+ const error = await response.json();
45
+ errorMessage = error.message || errorMessage;
46
+ } catch {
47
+ errorMessage = `${response.status} ${response.statusText}`;
48
+ }
49
+ // Include CSRF debug info
50
+ if (response.status === 403 && errorMessage.includes("CSRF")) {
51
+ console.error("[apiRequest] CSRF token validation failed", {
52
+ csrfToken,
53
+ headers: Object.fromEntries(response.headers.entries()),
54
+ url,
55
+ });
56
+ }
57
+ throw new Error(errorMessage);
58
+ }
47
59
 
48
- return response.json();
60
+ // Handle empty responses (204 No Content)
61
+ if (response.status === 204) {
62
+ return null;
63
+ }
64
+
65
+ return response.json();
49
66
  }
50
67
 
51
68
  /**
52
69
  * Convenience methods for common HTTP verbs
53
70
  */
54
71
  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' }),
72
+ /**
73
+ * GET request
74
+ * @param {string} url - API endpoint
75
+ * @param {RequestInit} options - Additional fetch options
76
+ */
77
+ get: (url, options = {}) => apiRequest(url, { ...options, method: "GET" }),
62
78
 
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
- }),
79
+ /**
80
+ * POST request
81
+ * @param {string} url - API endpoint
82
+ * @param {any} body - Request body (will be JSON stringified)
83
+ * @param {RequestInit} options - Additional fetch options
84
+ */
85
+ post: (url, body, options = {}) =>
86
+ apiRequest(url, {
87
+ ...options,
88
+ method: "POST",
89
+ body: JSON.stringify(body),
90
+ }),
75
91
 
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
- }),
92
+ /**
93
+ * PUT request
94
+ * @param {string} url - API endpoint
95
+ * @param {any} body - Request body (will be JSON stringified)
96
+ * @param {RequestInit} options - Additional fetch options
97
+ */
98
+ put: (url, body, options = {}) =>
99
+ apiRequest(url, {
100
+ ...options,
101
+ method: "PUT",
102
+ body: JSON.stringify(body),
103
+ }),
88
104
 
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' }),
105
+ /**
106
+ * DELETE request
107
+ * @param {string} url - API endpoint
108
+ * @param {RequestInit} options - Additional fetch options
109
+ */
110
+ delete: (url, options = {}) =>
111
+ apiRequest(url, { ...options, method: "DELETE" }),
96
112
 
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
- })
113
+ /**
114
+ * PATCH request
115
+ * @param {string} url - API endpoint
116
+ * @param {any} body - Request body (will be JSON stringified)
117
+ * @param {RequestInit} options - Additional fetch options
118
+ */
119
+ patch: (url, body, options = {}) =>
120
+ apiRequest(url, {
121
+ ...options,
122
+ method: "PATCH",
123
+ body: JSON.stringify(body),
124
+ }),
109
125
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autumnsgrove/groveengine",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
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",