@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.
Files changed (38) hide show
  1. package/dist/auth/session.d.ts +2 -2
  2. package/dist/components/admin/FloatingToolbar.svelte +373 -0
  3. package/dist/components/admin/FloatingToolbar.svelte.d.ts +17 -0
  4. package/dist/components/admin/MarkdownEditor.svelte +26 -347
  5. package/dist/components/admin/MarkdownEditor.svelte.d.ts +1 -1
  6. package/dist/components/admin/composables/index.d.ts +0 -2
  7. package/dist/components/admin/composables/index.js +0 -2
  8. package/dist/components/custom/MobileTOC.svelte +20 -13
  9. package/dist/components/quota/UpgradePrompt.svelte +1 -1
  10. package/dist/server/services/database.d.ts +138 -0
  11. package/dist/server/services/database.js +234 -0
  12. package/dist/server/services/index.d.ts +5 -1
  13. package/dist/server/services/index.js +24 -2
  14. package/dist/server/services/turnstile.d.ts +66 -0
  15. package/dist/server/services/turnstile.js +131 -0
  16. package/dist/server/services/users.d.ts +104 -0
  17. package/dist/server/services/users.js +158 -0
  18. package/dist/styles/README.md +50 -0
  19. package/dist/styles/vine-pattern.css +24 -0
  20. package/dist/types/turnstile.d.ts +42 -0
  21. package/dist/ui/components/forms/TurnstileWidget.svelte +111 -0
  22. package/dist/ui/components/forms/TurnstileWidget.svelte.d.ts +14 -0
  23. package/dist/ui/components/primitives/dialog/dialog-overlay.svelte +1 -1
  24. package/dist/ui/components/primitives/sheet/sheet-overlay.svelte +1 -1
  25. package/dist/ui/components/ui/Logo.svelte +161 -23
  26. package/dist/ui/components/ui/Logo.svelte.d.ts +4 -10
  27. package/dist/ui/tokens/fonts.d.ts +69 -0
  28. package/dist/ui/tokens/fonts.js +341 -0
  29. package/dist/ui/tokens/index.d.ts +6 -5
  30. package/dist/ui/tokens/index.js +7 -6
  31. package/package.json +22 -21
  32. package/static/fonts/alagard.ttf +0 -0
  33. package/static/robots.txt +487 -0
  34. package/LICENSE +0 -378
  35. package/dist/components/admin/composables/useCommandPalette.svelte.d.ts +0 -87
  36. package/dist/components/admin/composables/useCommandPalette.svelte.js +0 -158
  37. package/dist/components/admin/composables/useSlashCommands.svelte.d.ts +0 -104
  38. package/dist/components/admin/composables/useSlashCommands.svelte.js +0 -215
@@ -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: any, tenantId: string | undefined | null, userEmail: string): Promise<boolean>;
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: any, tenantId: string | undefined | null, user: User | null | undefined): Promise<string>;
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
+ };