@autumnsgrove/groveengine 0.6.4 → 0.6.5
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.
- package/dist/auth/session.d.ts +2 -2
- package/dist/components/admin/FloatingToolbar.svelte +373 -0
- package/dist/components/admin/FloatingToolbar.svelte.d.ts +17 -0
- package/dist/components/admin/MarkdownEditor.svelte +26 -347
- package/dist/components/admin/MarkdownEditor.svelte.d.ts +1 -1
- package/dist/components/admin/composables/index.d.ts +0 -2
- package/dist/components/admin/composables/index.js +0 -2
- package/dist/components/custom/MobileTOC.svelte +20 -13
- package/dist/components/quota/UpgradePrompt.svelte +1 -1
- package/dist/server/services/database.d.ts +138 -0
- package/dist/server/services/database.js +234 -0
- package/dist/server/services/index.d.ts +5 -1
- package/dist/server/services/index.js +24 -2
- package/dist/server/services/turnstile.d.ts +66 -0
- package/dist/server/services/turnstile.js +131 -0
- package/dist/server/services/users.d.ts +104 -0
- package/dist/server/services/users.js +158 -0
- package/dist/styles/README.md +50 -0
- package/dist/styles/vine-pattern.css +24 -0
- package/dist/types/turnstile.d.ts +42 -0
- package/dist/ui/components/forms/TurnstileWidget.svelte +111 -0
- package/dist/ui/components/forms/TurnstileWidget.svelte.d.ts +14 -0
- package/dist/ui/components/primitives/dialog/dialog-overlay.svelte +1 -1
- package/dist/ui/components/primitives/sheet/sheet-overlay.svelte +1 -1
- package/dist/ui/components/ui/Logo.svelte +161 -23
- package/dist/ui/components/ui/Logo.svelte.d.ts +4 -10
- package/dist/ui/tokens/fonts.d.ts +69 -0
- package/dist/ui/tokens/fonts.js +341 -0
- package/dist/ui/tokens/index.d.ts +6 -5
- package/dist/ui/tokens/index.js +7 -6
- package/package.json +22 -21
- package/static/fonts/alagard.ttf +0 -0
- package/static/robots.txt +487 -0
- package/LICENSE +0 -378
- package/dist/components/admin/composables/useCommandPalette.svelte.d.ts +0 -87
- package/dist/components/admin/composables/useCommandPalette.svelte.js +0 -158
- package/dist/components/admin/composables/useSlashCommands.svelte.d.ts +0 -104
- package/dist/components/admin/composables/useSlashCommands.svelte.js +0 -215
package/dist/auth/session.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export function isAllowedAdmin(email: string, allowedList: string): boolean;
|
|
|
44
44
|
* @param {string} userEmail - User's email address
|
|
45
45
|
* @returns {Promise<boolean>} - Whether the user owns the tenant
|
|
46
46
|
*/
|
|
47
|
-
export function verifyTenantOwnership(db:
|
|
47
|
+
export function verifyTenantOwnership(db: import("@cloudflare/workers-types").D1Database, tenantId: string | undefined | null, userEmail: string): Promise<boolean>;
|
|
48
48
|
/**
|
|
49
49
|
* Get tenant ID with ownership verification
|
|
50
50
|
* Throws 403 if user doesn't own the tenant
|
|
@@ -54,7 +54,7 @@ export function verifyTenantOwnership(db: any, tenantId: string | undefined | nu
|
|
|
54
54
|
* @returns {Promise<string>} - Verified tenant ID
|
|
55
55
|
* @throws {SessionError} - If unauthorized
|
|
56
56
|
*/
|
|
57
|
-
export function getVerifiedTenantId(db:
|
|
57
|
+
export function getVerifiedTenantId(db: import("@cloudflare/workers-types").D1Database, tenantId: string | undefined | null, user: User | null | undefined): Promise<string>;
|
|
58
58
|
export type User = {
|
|
59
59
|
email: string;
|
|
60
60
|
};
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { Bold, Italic, Link, Heading1, Heading2, Heading3, Code } from "lucide-svelte";
|
|
3
|
+
import { tick, onMount } from "svelte";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* FloatingToolbar - Medium-style floating toolbar for text formatting
|
|
7
|
+
* Appears above selected text with formatting options
|
|
8
|
+
*
|
|
9
|
+
* @security This component modifies raw markdown content but does NOT sanitize input.
|
|
10
|
+
* The parent component MUST sanitize all content before persisting to the database
|
|
11
|
+
* to prevent XSS attacks. Use sanitizeMarkdown() when converting to HTML for display.
|
|
12
|
+
* See: $lib/utils/sanitize.js
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Props
|
|
16
|
+
/** @type {{ textareaRef?: HTMLTextAreaElement | null, content?: string, readonly?: boolean, onContentChange?: (content: string) => void }} */
|
|
17
|
+
let {
|
|
18
|
+
textareaRef = /** @type {HTMLTextAreaElement | null} */ (null),
|
|
19
|
+
content = $bindable(""),
|
|
20
|
+
readonly = false,
|
|
21
|
+
onContentChange = () => {},
|
|
22
|
+
} = $props();
|
|
23
|
+
|
|
24
|
+
// Toolbar state
|
|
25
|
+
let isVisible = $state(false);
|
|
26
|
+
let toolbarPosition = $state({ top: 0, left: 0 });
|
|
27
|
+
let selectionStart = $state(0);
|
|
28
|
+
let selectionEnd = $state(0);
|
|
29
|
+
|
|
30
|
+
/** @type {HTMLDivElement | null} */
|
|
31
|
+
let toolbarRef = $state(null);
|
|
32
|
+
|
|
33
|
+
// Track selection changes
|
|
34
|
+
function handleSelectionChange() {
|
|
35
|
+
if (!textareaRef || readonly) return;
|
|
36
|
+
|
|
37
|
+
const start = textareaRef.selectionStart;
|
|
38
|
+
const end = textareaRef.selectionEnd;
|
|
39
|
+
|
|
40
|
+
// Only show toolbar when there's actual selected text
|
|
41
|
+
if (start !== end && document.activeElement === textareaRef) {
|
|
42
|
+
selectionStart = start;
|
|
43
|
+
selectionEnd = end;
|
|
44
|
+
positionToolbar();
|
|
45
|
+
isVisible = true;
|
|
46
|
+
} else {
|
|
47
|
+
isVisible = false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function positionToolbar() {
|
|
52
|
+
if (!textareaRef || !toolbarRef) return;
|
|
53
|
+
|
|
54
|
+
// Get textarea bounding rect
|
|
55
|
+
const textareaRect = textareaRef.getBoundingClientRect();
|
|
56
|
+
|
|
57
|
+
// Calculate approximate position based on selection
|
|
58
|
+
// For textarea, we need to estimate position based on text metrics
|
|
59
|
+
const textBeforeSelection = content.substring(0, selectionStart);
|
|
60
|
+
const lines = textBeforeSelection.split('\n');
|
|
61
|
+
const currentLineIndex = lines.length - 1;
|
|
62
|
+
const lineHeight = parseFloat(getComputedStyle(textareaRef).lineHeight) || 24;
|
|
63
|
+
|
|
64
|
+
// Calculate vertical position (above the selection)
|
|
65
|
+
const scrollTop = textareaRef.scrollTop;
|
|
66
|
+
const lineTop = currentLineIndex * lineHeight - scrollTop;
|
|
67
|
+
const toolbarTop = textareaRect.top + lineTop - 48; // 48px gap above selection
|
|
68
|
+
|
|
69
|
+
// Calculate horizontal center
|
|
70
|
+
const toolbarWidth = toolbarRef?.offsetWidth || 200;
|
|
71
|
+
let toolbarLeft = textareaRect.left + (textareaRect.width / 2) - (toolbarWidth / 2);
|
|
72
|
+
|
|
73
|
+
// Viewport constraints
|
|
74
|
+
const viewportWidth = window.innerWidth;
|
|
75
|
+
const viewportHeight = window.innerHeight;
|
|
76
|
+
const padding = 12;
|
|
77
|
+
|
|
78
|
+
// Constrain to viewport
|
|
79
|
+
toolbarLeft = Math.max(padding, Math.min(toolbarLeft, viewportWidth - toolbarWidth - padding));
|
|
80
|
+
const finalTop = Math.max(padding, Math.min(toolbarTop, viewportHeight - 60));
|
|
81
|
+
|
|
82
|
+
toolbarPosition = {
|
|
83
|
+
top: finalTop,
|
|
84
|
+
left: toolbarLeft,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Wrap selected text with formatting markers
|
|
90
|
+
* @param {string} before
|
|
91
|
+
* @param {string} after
|
|
92
|
+
*/
|
|
93
|
+
async function wrapSelection(before, after) {
|
|
94
|
+
if (!textareaRef) return;
|
|
95
|
+
|
|
96
|
+
const selectedText = content.substring(selectionStart, selectionEnd);
|
|
97
|
+
const newContent =
|
|
98
|
+
content.substring(0, selectionStart) +
|
|
99
|
+
before + selectedText + after +
|
|
100
|
+
content.substring(selectionEnd);
|
|
101
|
+
|
|
102
|
+
content = newContent;
|
|
103
|
+
onContentChange(newContent);
|
|
104
|
+
|
|
105
|
+
await tick();
|
|
106
|
+
|
|
107
|
+
// Restore selection inside the wrapped text
|
|
108
|
+
textareaRef.selectionStart = selectionStart + before.length;
|
|
109
|
+
textareaRef.selectionEnd = selectionEnd + before.length;
|
|
110
|
+
textareaRef.focus();
|
|
111
|
+
|
|
112
|
+
isVisible = false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Insert text at beginning of selected line(s)
|
|
117
|
+
* @param {string} prefix
|
|
118
|
+
*/
|
|
119
|
+
async function insertLinePrefix(prefix) {
|
|
120
|
+
if (!textareaRef) return;
|
|
121
|
+
|
|
122
|
+
// Find the start of the current line
|
|
123
|
+
const beforeSelection = content.substring(0, selectionStart);
|
|
124
|
+
const lineStart = beforeSelection.lastIndexOf('\n') + 1;
|
|
125
|
+
|
|
126
|
+
const newContent =
|
|
127
|
+
content.substring(0, lineStart) +
|
|
128
|
+
prefix +
|
|
129
|
+
content.substring(lineStart);
|
|
130
|
+
|
|
131
|
+
content = newContent;
|
|
132
|
+
onContentChange(newContent);
|
|
133
|
+
|
|
134
|
+
await tick();
|
|
135
|
+
|
|
136
|
+
// Position cursor after the prefix
|
|
137
|
+
const newPos = selectionStart + prefix.length;
|
|
138
|
+
textareaRef.selectionStart = newPos;
|
|
139
|
+
textareaRef.selectionEnd = selectionEnd + prefix.length;
|
|
140
|
+
textareaRef.focus();
|
|
141
|
+
|
|
142
|
+
isVisible = false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function handleBold() {
|
|
146
|
+
wrapSelection("**", "**");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function handleItalic() {
|
|
150
|
+
wrapSelection("_", "_");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function handleCode() {
|
|
154
|
+
wrapSelection("`", "`");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function handleLink() {
|
|
158
|
+
wrapSelection("[", "](url)");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function handleH1() {
|
|
162
|
+
insertLinePrefix("# ");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function handleH2() {
|
|
166
|
+
insertLinePrefix("## ");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function handleH3() {
|
|
170
|
+
insertLinePrefix("### ");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function handleClickOutside(e) {
|
|
174
|
+
if (toolbarRef && !toolbarRef.contains(e.target) && e.target !== textareaRef) {
|
|
175
|
+
isVisible = false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Handle keyboard shortcuts for formatting
|
|
181
|
+
* @param {KeyboardEvent} e
|
|
182
|
+
*/
|
|
183
|
+
function handleKeyboardShortcuts(e) {
|
|
184
|
+
if (!textareaRef || readonly) return;
|
|
185
|
+
if (document.activeElement !== textareaRef) return;
|
|
186
|
+
|
|
187
|
+
const isMod = e.metaKey || e.ctrlKey;
|
|
188
|
+
if (!isMod) return;
|
|
189
|
+
|
|
190
|
+
// Update selection state before applying formatting
|
|
191
|
+
selectionStart = textareaRef.selectionStart;
|
|
192
|
+
selectionEnd = textareaRef.selectionEnd;
|
|
193
|
+
|
|
194
|
+
// Only apply if there's a selection
|
|
195
|
+
if (selectionStart === selectionEnd) return;
|
|
196
|
+
|
|
197
|
+
switch (e.key.toLowerCase()) {
|
|
198
|
+
case 'b':
|
|
199
|
+
e.preventDefault();
|
|
200
|
+
handleBold();
|
|
201
|
+
break;
|
|
202
|
+
case 'i':
|
|
203
|
+
e.preventDefault();
|
|
204
|
+
handleItalic();
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Set up selection monitoring and keyboard shortcuts
|
|
210
|
+
onMount(() => {
|
|
211
|
+
document.addEventListener("mouseup", handleSelectionChange);
|
|
212
|
+
document.addEventListener("keyup", handleSelectionChange);
|
|
213
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
214
|
+
document.addEventListener("keydown", handleKeyboardShortcuts);
|
|
215
|
+
|
|
216
|
+
return () => {
|
|
217
|
+
document.removeEventListener("mouseup", handleSelectionChange);
|
|
218
|
+
document.removeEventListener("keyup", handleSelectionChange);
|
|
219
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
220
|
+
document.removeEventListener("keydown", handleKeyboardShortcuts);
|
|
221
|
+
};
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Re-position when selection changes
|
|
225
|
+
$effect(() => {
|
|
226
|
+
if (isVisible && toolbarRef) {
|
|
227
|
+
positionToolbar();
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
</script>
|
|
231
|
+
|
|
232
|
+
{#if isVisible}
|
|
233
|
+
<div
|
|
234
|
+
bind:this={toolbarRef}
|
|
235
|
+
class="floating-toolbar"
|
|
236
|
+
style="top: {toolbarPosition.top}px; left: {toolbarPosition.left}px;"
|
|
237
|
+
role="toolbar"
|
|
238
|
+
aria-label="Text formatting toolbar"
|
|
239
|
+
>
|
|
240
|
+
<button
|
|
241
|
+
type="button"
|
|
242
|
+
class="toolbar-btn"
|
|
243
|
+
onclick={handleBold}
|
|
244
|
+
title="Bold (Cmd+B)"
|
|
245
|
+
aria-label="Bold"
|
|
246
|
+
>
|
|
247
|
+
<Bold size={16} />
|
|
248
|
+
</button>
|
|
249
|
+
|
|
250
|
+
<button
|
|
251
|
+
type="button"
|
|
252
|
+
class="toolbar-btn"
|
|
253
|
+
onclick={handleItalic}
|
|
254
|
+
title="Italic (Cmd+I)"
|
|
255
|
+
aria-label="Italic"
|
|
256
|
+
>
|
|
257
|
+
<Italic size={16} />
|
|
258
|
+
</button>
|
|
259
|
+
|
|
260
|
+
<button
|
|
261
|
+
type="button"
|
|
262
|
+
class="toolbar-btn"
|
|
263
|
+
onclick={handleCode}
|
|
264
|
+
title="Inline code"
|
|
265
|
+
aria-label="Code"
|
|
266
|
+
>
|
|
267
|
+
<Code size={16} />
|
|
268
|
+
</button>
|
|
269
|
+
|
|
270
|
+
<div class="toolbar-divider"></div>
|
|
271
|
+
|
|
272
|
+
<button
|
|
273
|
+
type="button"
|
|
274
|
+
class="toolbar-btn"
|
|
275
|
+
onclick={handleLink}
|
|
276
|
+
title="Insert link"
|
|
277
|
+
aria-label="Link"
|
|
278
|
+
>
|
|
279
|
+
<Link size={16} />
|
|
280
|
+
</button>
|
|
281
|
+
|
|
282
|
+
<div class="toolbar-divider"></div>
|
|
283
|
+
|
|
284
|
+
<button
|
|
285
|
+
type="button"
|
|
286
|
+
class="toolbar-btn"
|
|
287
|
+
onclick={handleH1}
|
|
288
|
+
title="Heading 1"
|
|
289
|
+
aria-label="Heading 1"
|
|
290
|
+
>
|
|
291
|
+
<Heading1 size={16} />
|
|
292
|
+
</button>
|
|
293
|
+
|
|
294
|
+
<button
|
|
295
|
+
type="button"
|
|
296
|
+
class="toolbar-btn"
|
|
297
|
+
onclick={handleH2}
|
|
298
|
+
title="Heading 2"
|
|
299
|
+
aria-label="Heading 2"
|
|
300
|
+
>
|
|
301
|
+
<Heading2 size={16} />
|
|
302
|
+
</button>
|
|
303
|
+
|
|
304
|
+
<button
|
|
305
|
+
type="button"
|
|
306
|
+
class="toolbar-btn"
|
|
307
|
+
onclick={handleH3}
|
|
308
|
+
title="Heading 3"
|
|
309
|
+
aria-label="Heading 3"
|
|
310
|
+
>
|
|
311
|
+
<Heading3 size={16} />
|
|
312
|
+
</button>
|
|
313
|
+
</div>
|
|
314
|
+
{/if}
|
|
315
|
+
|
|
316
|
+
<style>
|
|
317
|
+
.floating-toolbar {
|
|
318
|
+
position: fixed;
|
|
319
|
+
display: flex;
|
|
320
|
+
align-items: center;
|
|
321
|
+
gap: 0.25rem;
|
|
322
|
+
padding: 0.5rem 0.75rem;
|
|
323
|
+
background: rgba(30, 30, 30, 0.95);
|
|
324
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
325
|
+
border-radius: 9999px;
|
|
326
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.05);
|
|
327
|
+
backdrop-filter: blur(8px);
|
|
328
|
+
z-index: 1000;
|
|
329
|
+
animation: toolbar-appear 0.15s ease-out;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
@keyframes toolbar-appear {
|
|
333
|
+
from {
|
|
334
|
+
opacity: 0;
|
|
335
|
+
transform: translateY(4px);
|
|
336
|
+
}
|
|
337
|
+
to {
|
|
338
|
+
opacity: 1;
|
|
339
|
+
transform: translateY(0);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.toolbar-btn {
|
|
344
|
+
display: flex;
|
|
345
|
+
align-items: center;
|
|
346
|
+
justify-content: center;
|
|
347
|
+
width: 32px;
|
|
348
|
+
height: 32px;
|
|
349
|
+
padding: 0;
|
|
350
|
+
background: transparent;
|
|
351
|
+
border: none;
|
|
352
|
+
border-radius: 6px;
|
|
353
|
+
color: rgba(255, 255, 255, 0.7);
|
|
354
|
+
cursor: pointer;
|
|
355
|
+
transition: all 0.15s ease;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.toolbar-btn:hover {
|
|
359
|
+
background: rgba(255, 255, 255, 0.1);
|
|
360
|
+
color: rgba(255, 255, 255, 0.95);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.toolbar-btn:active {
|
|
364
|
+
transform: scale(0.95);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.toolbar-divider {
|
|
368
|
+
width: 1px;
|
|
369
|
+
height: 20px;
|
|
370
|
+
background: rgba(255, 255, 255, 0.15);
|
|
371
|
+
margin: 0 0.25rem;
|
|
372
|
+
}
|
|
373
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export default FloatingToolbar;
|
|
2
|
+
type FloatingToolbar = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
declare const FloatingToolbar: import("svelte").Component<{
|
|
7
|
+
textareaRef?: HTMLTextAreaElement | null;
|
|
8
|
+
content?: string;
|
|
9
|
+
readonly?: boolean;
|
|
10
|
+
onContentChange?: (content: string) => void;
|
|
11
|
+
}, {}, "content">;
|
|
12
|
+
type $$ComponentProps = {
|
|
13
|
+
textareaRef?: HTMLTextAreaElement | null;
|
|
14
|
+
content?: string;
|
|
15
|
+
readonly?: boolean;
|
|
16
|
+
onContentChange?: (content: string) => void;
|
|
17
|
+
};
|