@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
|
|
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(
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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:
|
|
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
|
-
|
|
14
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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.
|
|
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",
|