@autumnsgrove/groveengine 0.1.0

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 (219) hide show
  1. package/README.md +163 -0
  2. package/dist/auth/jwt.d.ts +14 -0
  3. package/dist/auth/jwt.js +109 -0
  4. package/dist/auth/session.d.ts +42 -0
  5. package/dist/auth/session.js +105 -0
  6. package/dist/components/admin/GutterManager.svelte +910 -0
  7. package/dist/components/admin/GutterManager.svelte.d.ts +15 -0
  8. package/dist/components/admin/MarkdownEditor.svelte +3114 -0
  9. package/dist/components/admin/MarkdownEditor.svelte.d.ts +43 -0
  10. package/dist/components/custom/CollapsibleSection.svelte +74 -0
  11. package/dist/components/custom/CollapsibleSection.svelte.d.ts +15 -0
  12. package/dist/components/custom/ContentWithGutter.svelte +646 -0
  13. package/dist/components/custom/ContentWithGutter.svelte.d.ts +19 -0
  14. package/dist/components/custom/GutterItem.svelte +201 -0
  15. package/dist/components/custom/GutterItem.svelte.d.ts +11 -0
  16. package/dist/components/custom/LeftGutter.svelte +271 -0
  17. package/dist/components/custom/LeftGutter.svelte.d.ts +17 -0
  18. package/dist/components/custom/MobileTOC.svelte +273 -0
  19. package/dist/components/custom/MobileTOC.svelte.d.ts +11 -0
  20. package/dist/components/custom/TableOfContents.svelte +163 -0
  21. package/dist/components/custom/TableOfContents.svelte.d.ts +11 -0
  22. package/dist/components/gallery/ImageGallery.svelte +681 -0
  23. package/dist/components/gallery/ImageGallery.svelte.d.ts +11 -0
  24. package/dist/components/gallery/Lightbox.svelte +107 -0
  25. package/dist/components/gallery/Lightbox.svelte.d.ts +19 -0
  26. package/dist/components/gallery/LightboxCaption.svelte +25 -0
  27. package/dist/components/gallery/LightboxCaption.svelte.d.ts +11 -0
  28. package/dist/components/gallery/ZoomableImage.svelte +163 -0
  29. package/dist/components/gallery/ZoomableImage.svelte.d.ts +17 -0
  30. package/dist/components/ui/Accordion.svelte +74 -0
  31. package/dist/components/ui/Accordion.svelte.d.ts +42 -0
  32. package/dist/components/ui/Badge.svelte +48 -0
  33. package/dist/components/ui/Badge.svelte.d.ts +26 -0
  34. package/dist/components/ui/Button.svelte +74 -0
  35. package/dist/components/ui/Button.svelte.d.ts +34 -0
  36. package/dist/components/ui/Card.svelte +102 -0
  37. package/dist/components/ui/Card.svelte.d.ts +46 -0
  38. package/dist/components/ui/Dialog.svelte +91 -0
  39. package/dist/components/ui/Dialog.svelte.d.ts +43 -0
  40. package/dist/components/ui/Input.svelte +81 -0
  41. package/dist/components/ui/Input.svelte.d.ts +35 -0
  42. package/dist/components/ui/Select.svelte +69 -0
  43. package/dist/components/ui/Select.svelte.d.ts +36 -0
  44. package/dist/components/ui/Sheet.svelte +98 -0
  45. package/dist/components/ui/Sheet.svelte.d.ts +45 -0
  46. package/dist/components/ui/Skeleton.svelte +31 -0
  47. package/dist/components/ui/Skeleton.svelte.d.ts +26 -0
  48. package/dist/components/ui/Table.svelte +59 -0
  49. package/dist/components/ui/Table.svelte.d.ts +44 -0
  50. package/dist/components/ui/Tabs.svelte +76 -0
  51. package/dist/components/ui/Tabs.svelte.d.ts +41 -0
  52. package/dist/components/ui/Textarea.svelte +81 -0
  53. package/dist/components/ui/Textarea.svelte.d.ts +35 -0
  54. package/dist/components/ui/Toast.svelte +18 -0
  55. package/dist/components/ui/Toast.svelte.d.ts +7 -0
  56. package/dist/components/ui/accordion/accordion-content.svelte +24 -0
  57. package/dist/components/ui/accordion/accordion-content.svelte.d.ts +4 -0
  58. package/dist/components/ui/accordion/accordion-item.svelte +12 -0
  59. package/dist/components/ui/accordion/accordion-item.svelte.d.ts +4 -0
  60. package/dist/components/ui/accordion/accordion-trigger.svelte +29 -0
  61. package/dist/components/ui/accordion/accordion-trigger.svelte.d.ts +7 -0
  62. package/dist/components/ui/accordion/index.d.ts +6 -0
  63. package/dist/components/ui/accordion/index.js +8 -0
  64. package/dist/components/ui/badge/badge.svelte +50 -0
  65. package/dist/components/ui/badge/badge.svelte.d.ts +60 -0
  66. package/dist/components/ui/badge/index.d.ts +2 -0
  67. package/dist/components/ui/badge/index.js +2 -0
  68. package/dist/components/ui/button/button.svelte +82 -0
  69. package/dist/components/ui/button/button.svelte.d.ts +132 -0
  70. package/dist/components/ui/button/index.d.ts +2 -0
  71. package/dist/components/ui/button/index.js +4 -0
  72. package/dist/components/ui/card/card-content.svelte +16 -0
  73. package/dist/components/ui/card/card-content.svelte.d.ts +5 -0
  74. package/dist/components/ui/card/card-description.svelte +16 -0
  75. package/dist/components/ui/card/card-description.svelte.d.ts +5 -0
  76. package/dist/components/ui/card/card-footer.svelte +16 -0
  77. package/dist/components/ui/card/card-footer.svelte.d.ts +5 -0
  78. package/dist/components/ui/card/card-header.svelte +16 -0
  79. package/dist/components/ui/card/card-header.svelte.d.ts +5 -0
  80. package/dist/components/ui/card/card-title.svelte +25 -0
  81. package/dist/components/ui/card/card-title.svelte.d.ts +8 -0
  82. package/dist/components/ui/card/card.svelte +20 -0
  83. package/dist/components/ui/card/card.svelte.d.ts +5 -0
  84. package/dist/components/ui/card/index.d.ts +7 -0
  85. package/dist/components/ui/card/index.js +9 -0
  86. package/dist/components/ui/dialog/dialog-content.svelte +38 -0
  87. package/dist/components/ui/dialog/dialog-content.svelte.d.ts +9 -0
  88. package/dist/components/ui/dialog/dialog-description.svelte +16 -0
  89. package/dist/components/ui/dialog/dialog-description.svelte.d.ts +4 -0
  90. package/dist/components/ui/dialog/dialog-footer.svelte +20 -0
  91. package/dist/components/ui/dialog/dialog-footer.svelte.d.ts +5 -0
  92. package/dist/components/ui/dialog/dialog-header.svelte +20 -0
  93. package/dist/components/ui/dialog/dialog-header.svelte.d.ts +5 -0
  94. package/dist/components/ui/dialog/dialog-overlay.svelte +19 -0
  95. package/dist/components/ui/dialog/dialog-overlay.svelte.d.ts +4 -0
  96. package/dist/components/ui/dialog/dialog-title.svelte +16 -0
  97. package/dist/components/ui/dialog/dialog-title.svelte.d.ts +4 -0
  98. package/dist/components/ui/dialog/index.d.ts +12 -0
  99. package/dist/components/ui/dialog/index.js +14 -0
  100. package/dist/components/ui/index.d.ts +26 -0
  101. package/dist/components/ui/index.js +29 -0
  102. package/dist/components/ui/input/index.d.ts +2 -0
  103. package/dist/components/ui/input/index.js +4 -0
  104. package/dist/components/ui/input/input.svelte +46 -0
  105. package/dist/components/ui/input/input.svelte.d.ts +13 -0
  106. package/dist/components/ui/select/index.d.ts +11 -0
  107. package/dist/components/ui/select/index.js +13 -0
  108. package/dist/components/ui/select/select-content.svelte +39 -0
  109. package/dist/components/ui/select/select-content.svelte.d.ts +7 -0
  110. package/dist/components/ui/select/select-group-heading.svelte +16 -0
  111. package/dist/components/ui/select/select-group-heading.svelte.d.ts +4 -0
  112. package/dist/components/ui/select/select-item.svelte +37 -0
  113. package/dist/components/ui/select/select-item.svelte.d.ts +4 -0
  114. package/dist/components/ui/select/select-scroll-down-button.svelte +19 -0
  115. package/dist/components/ui/select/select-scroll-down-button.svelte.d.ts +4 -0
  116. package/dist/components/ui/select/select-scroll-up-button.svelte +19 -0
  117. package/dist/components/ui/select/select-scroll-up-button.svelte.d.ts +4 -0
  118. package/dist/components/ui/select/select-separator.svelte +13 -0
  119. package/dist/components/ui/select/select-separator.svelte.d.ts +4 -0
  120. package/dist/components/ui/select/select-trigger.svelte +24 -0
  121. package/dist/components/ui/select/select-trigger.svelte.d.ts +4 -0
  122. package/dist/components/ui/separator/index.d.ts +2 -0
  123. package/dist/components/ui/separator/index.js +4 -0
  124. package/dist/components/ui/separator/separator.svelte +22 -0
  125. package/dist/components/ui/separator/separator.svelte.d.ts +4 -0
  126. package/dist/components/ui/sheet/index.d.ts +12 -0
  127. package/dist/components/ui/sheet/index.js +14 -0
  128. package/dist/components/ui/sheet/sheet-content.svelte +53 -0
  129. package/dist/components/ui/sheet/sheet-content.svelte.d.ts +62 -0
  130. package/dist/components/ui/sheet/sheet-description.svelte +16 -0
  131. package/dist/components/ui/sheet/sheet-description.svelte.d.ts +4 -0
  132. package/dist/components/ui/sheet/sheet-footer.svelte +20 -0
  133. package/dist/components/ui/sheet/sheet-footer.svelte.d.ts +5 -0
  134. package/dist/components/ui/sheet/sheet-header.svelte +20 -0
  135. package/dist/components/ui/sheet/sheet-header.svelte.d.ts +5 -0
  136. package/dist/components/ui/sheet/sheet-overlay.svelte +21 -0
  137. package/dist/components/ui/sheet/sheet-overlay.svelte.d.ts +6 -0
  138. package/dist/components/ui/sheet/sheet-title.svelte +16 -0
  139. package/dist/components/ui/sheet/sheet-title.svelte.d.ts +4 -0
  140. package/dist/components/ui/skeleton/index.d.ts +2 -0
  141. package/dist/components/ui/skeleton/index.js +4 -0
  142. package/dist/components/ui/skeleton/skeleton.svelte +17 -0
  143. package/dist/components/ui/skeleton/skeleton.svelte.d.ts +5 -0
  144. package/dist/components/ui/table/index.d.ts +9 -0
  145. package/dist/components/ui/table/index.js +11 -0
  146. package/dist/components/ui/table/table-body.svelte +16 -0
  147. package/dist/components/ui/table/table-body.svelte.d.ts +5 -0
  148. package/dist/components/ui/table/table-caption.svelte +16 -0
  149. package/dist/components/ui/table/table-caption.svelte.d.ts +5 -0
  150. package/dist/components/ui/table/table-cell.svelte +20 -0
  151. package/dist/components/ui/table/table-cell.svelte.d.ts +5 -0
  152. package/dist/components/ui/table/table-footer.svelte +16 -0
  153. package/dist/components/ui/table/table-footer.svelte.d.ts +5 -0
  154. package/dist/components/ui/table/table-head.svelte +23 -0
  155. package/dist/components/ui/table/table-head.svelte.d.ts +5 -0
  156. package/dist/components/ui/table/table-header.svelte +16 -0
  157. package/dist/components/ui/table/table-header.svelte.d.ts +5 -0
  158. package/dist/components/ui/table/table-row.svelte +23 -0
  159. package/dist/components/ui/table/table-row.svelte.d.ts +5 -0
  160. package/dist/components/ui/table/table.svelte +18 -0
  161. package/dist/components/ui/table/table.svelte.d.ts +5 -0
  162. package/dist/components/ui/tabs/index.d.ts +6 -0
  163. package/dist/components/ui/tabs/index.js +8 -0
  164. package/dist/components/ui/tabs/tabs-content.svelte +19 -0
  165. package/dist/components/ui/tabs/tabs-content.svelte.d.ts +4 -0
  166. package/dist/components/ui/tabs/tabs-list.svelte +19 -0
  167. package/dist/components/ui/tabs/tabs-list.svelte.d.ts +4 -0
  168. package/dist/components/ui/tabs/tabs-trigger.svelte +19 -0
  169. package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
  170. package/dist/components/ui/textarea/index.d.ts +2 -0
  171. package/dist/components/ui/textarea/index.js +4 -0
  172. package/dist/components/ui/textarea/textarea.svelte +24 -0
  173. package/dist/components/ui/textarea/textarea.svelte.d.ts +6 -0
  174. package/dist/components/ui/toast.d.ts +86 -0
  175. package/dist/components/ui/toast.js +99 -0
  176. package/dist/db/schema.sql +238 -0
  177. package/dist/index.d.ts +14 -0
  178. package/dist/index.js +20 -0
  179. package/dist/payments/index.d.ts +33 -0
  180. package/dist/payments/index.js +47 -0
  181. package/dist/payments/shop.d.ts +165 -0
  182. package/dist/payments/shop.js +588 -0
  183. package/dist/payments/stripe/client.d.ts +231 -0
  184. package/dist/payments/stripe/client.js +198 -0
  185. package/dist/payments/stripe/index.d.ts +18 -0
  186. package/dist/payments/stripe/index.js +17 -0
  187. package/dist/payments/stripe/provider.d.ts +50 -0
  188. package/dist/payments/stripe/provider.js +530 -0
  189. package/dist/payments/types.d.ts +355 -0
  190. package/dist/payments/types.js +7 -0
  191. package/dist/server/logger.d.ts +53 -0
  192. package/dist/server/logger.js +252 -0
  193. package/dist/styles/content.css +514 -0
  194. package/dist/styles/tokens.css +175 -0
  195. package/dist/utils/api.d.ts +20 -0
  196. package/dist/utils/api.js +109 -0
  197. package/dist/utils/cn.d.ts +15 -0
  198. package/dist/utils/cn.js +18 -0
  199. package/dist/utils/csrf.d.ts +22 -0
  200. package/dist/utils/csrf.js +72 -0
  201. package/dist/utils/debounce.d.ts +7 -0
  202. package/dist/utils/debounce.js +14 -0
  203. package/dist/utils/gallery.d.ts +66 -0
  204. package/dist/utils/gallery.js +181 -0
  205. package/dist/utils/gutter.d.ts +54 -0
  206. package/dist/utils/gutter.js +169 -0
  207. package/dist/utils/imageProcessor.d.ts +58 -0
  208. package/dist/utils/imageProcessor.js +205 -0
  209. package/dist/utils/json.d.ts +17 -0
  210. package/dist/utils/json.js +26 -0
  211. package/dist/utils/markdown.d.ts +101 -0
  212. package/dist/utils/markdown.js +947 -0
  213. package/dist/utils/sanitize.d.ts +25 -0
  214. package/dist/utils/sanitize.js +127 -0
  215. package/dist/utils/validation.d.ts +46 -0
  216. package/dist/utils/validation.js +169 -0
  217. package/dist/utils.d.ts +5 -0
  218. package/dist/utils.js +5 -0
  219. package/package.json +129 -0
@@ -0,0 +1,910 @@
1
+ <script>
2
+ import { marked } from "marked";
3
+ import Dialog from "../ui/Dialog.svelte";
4
+ import Input from "../ui/Input.svelte";
5
+ import Button from "../ui/Button.svelte";
6
+ import Select from "../ui/Select.svelte";
7
+ import { toast } from "../ui/toast";
8
+
9
+ // Props
10
+ let {
11
+ gutterItems = $bindable([]),
12
+ onInsertAnchor = (anchorName) => {},
13
+ availableAnchors = [],
14
+ } = $props();
15
+
16
+ // State
17
+ let showAddModal = $state(false);
18
+ let editingIndex = $state(null);
19
+ let showImagePicker = $state(false);
20
+ let imagePickerCallback = $state(null);
21
+
22
+ // Form state for add/edit
23
+ let itemType = $state("comment");
24
+ let itemAnchor = $state("");
25
+ let itemContent = $state("");
26
+ let itemCaption = $state("");
27
+ let itemUrl = $state("");
28
+ let galleryImages = $state([]);
29
+
30
+ // Image picker state
31
+ let cdnImages = $state([]);
32
+ let cdnLoading = $state(false);
33
+ let cdnFilter = $state("");
34
+
35
+ function resetForm() {
36
+ itemType = "comment";
37
+ itemAnchor = "";
38
+ itemContent = "";
39
+ itemCaption = "";
40
+ itemUrl = "";
41
+ galleryImages = [];
42
+ }
43
+
44
+ function openAddModal() {
45
+ resetForm();
46
+ editingIndex = null;
47
+ showAddModal = true;
48
+ }
49
+
50
+ function openEditModal(index) {
51
+ const item = gutterItems[index];
52
+ itemType = item.type;
53
+ itemAnchor = item.anchor || "";
54
+ itemContent = item.content || "";
55
+ itemCaption = item.caption || "";
56
+ itemUrl = item.url || item.file || "";
57
+ galleryImages = item.images ? [...item.images] : [];
58
+ editingIndex = index;
59
+ showAddModal = true;
60
+ }
61
+
62
+ function closeModal() {
63
+ showAddModal = false;
64
+ editingIndex = null;
65
+ resetForm();
66
+ }
67
+
68
+ function saveItem() {
69
+ const newItem = {
70
+ type: itemType,
71
+ anchor: itemAnchor,
72
+ };
73
+
74
+ if (itemType === "comment") {
75
+ newItem.content = itemContent;
76
+ } else if (itemType === "photo") {
77
+ newItem.url = itemUrl;
78
+ if (itemCaption) newItem.caption = itemCaption;
79
+ } else if (itemType === "gallery") {
80
+ newItem.images = galleryImages;
81
+ }
82
+
83
+ if (editingIndex !== null) {
84
+ gutterItems[editingIndex] = newItem;
85
+ gutterItems = [...gutterItems]; // Trigger reactivity
86
+ } else {
87
+ gutterItems = [...gutterItems, newItem];
88
+ }
89
+
90
+ closeModal();
91
+ }
92
+
93
+ function deleteItem(index) {
94
+ gutterItems = gutterItems.filter((_, i) => i !== index);
95
+ toast.success("Gutter item deleted");
96
+ }
97
+
98
+ function moveItem(index, direction) {
99
+ const newIndex = index + direction;
100
+ if (newIndex < 0 || newIndex >= gutterItems.length) return;
101
+
102
+ const items = [...gutterItems];
103
+ const temp = items[index];
104
+ items[index] = items[newIndex];
105
+ items[newIndex] = temp;
106
+ gutterItems = items;
107
+ }
108
+
109
+ // Generate anchor name from text
110
+ function generateAnchorName(text) {
111
+ return text
112
+ .toLowerCase()
113
+ .replace(/[^a-z0-9\s-]/g, "")
114
+ .replace(/\s+/g, "-")
115
+ .substring(0, 30);
116
+ }
117
+
118
+ // Insert anchor at cursor in editor
119
+ function handleInsertAnchor() {
120
+ const name = prompt("Enter anchor name (e.g., my-note):");
121
+ if (name) {
122
+ const safeName = generateAnchorName(name);
123
+ onInsertAnchor(safeName);
124
+ // Update the anchor field
125
+ itemAnchor = `anchor:${safeName}`;
126
+ }
127
+ }
128
+
129
+ // CDN Image Picker
130
+ async function loadCdnImages() {
131
+ cdnLoading = true;
132
+ try {
133
+ const params = new URLSearchParams();
134
+ if (cdnFilter) params.set("prefix", cdnFilter);
135
+ params.set("limit", "50");
136
+
137
+ const response = await fetch(`/api/images/list?${params}`);
138
+ const data = await response.json();
139
+
140
+ if (response.ok) {
141
+ const imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"];
142
+ cdnImages = data.images.filter((img) => {
143
+ const key = img.key.toLowerCase();
144
+ return imageExtensions.some((ext) => key.endsWith(ext));
145
+ });
146
+ }
147
+ } catch (err) {
148
+ toast.error('Failed to load CDN images');
149
+ console.error("Failed to load CDN images:", err);
150
+ cdnImages = [];
151
+ } finally {
152
+ cdnLoading = false;
153
+ }
154
+ }
155
+
156
+ function openImagePicker(callback) {
157
+ imagePickerCallback = callback;
158
+ showImagePicker = true;
159
+ loadCdnImages();
160
+ }
161
+
162
+ function selectImage(image) {
163
+ if (imagePickerCallback) {
164
+ imagePickerCallback(image.url);
165
+ }
166
+ showImagePicker = false;
167
+ imagePickerCallback = null;
168
+ }
169
+
170
+ function closeImagePicker() {
171
+ showImagePicker = false;
172
+ imagePickerCallback = null;
173
+ }
174
+
175
+ // Gallery helpers
176
+ function addGalleryImage() {
177
+ openImagePicker((url) => {
178
+ galleryImages = [
179
+ ...galleryImages,
180
+ { url, alt: "", caption: "" },
181
+ ];
182
+ });
183
+ }
184
+
185
+ function removeGalleryImage(index) {
186
+ galleryImages = galleryImages.filter((_, i) => i !== index);
187
+ }
188
+
189
+ function updateGalleryImage(index, field, value) {
190
+ galleryImages[index][field] = value;
191
+ galleryImages = [...galleryImages];
192
+ }
193
+
194
+ // Get preview of item content
195
+ function getItemPreview(item) {
196
+ if (item.type === "comment" && item.content) {
197
+ return item.content.substring(0, 50) + (item.content.length > 50 ? "..." : "");
198
+ }
199
+ if (item.type === "photo") {
200
+ return item.caption || item.url || "Photo";
201
+ }
202
+ if (item.type === "gallery") {
203
+ return `${item.images?.length || 0} images`;
204
+ }
205
+ return "";
206
+ }
207
+
208
+ function getTypeIcon(type) {
209
+ switch (type) {
210
+ case "comment":
211
+ return "💬";
212
+ case "photo":
213
+ return "🖼️";
214
+ case "gallery":
215
+ return "🎞️";
216
+ default:
217
+ return "📌";
218
+ }
219
+ }
220
+ </script>
221
+
222
+ <div class="gutter-manager">
223
+ <div class="gutter-header">
224
+ <h3>Gutter Content</h3>
225
+ <button class="add-btn" onclick={openAddModal}>+ Add Item</button>
226
+ </div>
227
+
228
+ {#if gutterItems.length === 0}
229
+ <div class="empty-state">
230
+ <p>No gutter items yet.</p>
231
+ <p class="hint">Add comments, images, or galleries that appear alongside your content.</p>
232
+ </div>
233
+ {:else}
234
+ <div class="gutter-list">
235
+ {#each gutterItems as item, index (index)}
236
+ <div class="gutter-item">
237
+ <div class="item-header">
238
+ <span class="item-type">{getTypeIcon(item.type)}</span>
239
+ <span class="item-anchor" title={item.anchor}>{item.anchor || "No anchor"}</span>
240
+ <div class="item-actions">
241
+ <button
242
+ class="action-btn"
243
+ onclick={() => moveItem(index, -1)}
244
+ disabled={index === 0}
245
+ title="Move up"
246
+ >↑</button>
247
+ <button
248
+ class="action-btn"
249
+ onclick={() => moveItem(index, 1)}
250
+ disabled={index === gutterItems.length - 1}
251
+ title="Move down"
252
+ >↓</button>
253
+ <button
254
+ class="action-btn"
255
+ onclick={() => openEditModal(index)}
256
+ title="Edit"
257
+ >✎</button>
258
+ <button
259
+ class="action-btn delete"
260
+ onclick={() => deleteItem(index)}
261
+ title="Delete"
262
+ >×</button>
263
+ </div>
264
+ </div>
265
+ <div class="item-preview">{getItemPreview(item)}</div>
266
+ </div>
267
+ {/each}
268
+ </div>
269
+ {/if}
270
+ </div>
271
+
272
+ <!-- Add/Edit Modal -->
273
+ <Dialog bind:open={showAddModal}>
274
+ <h3 slot="title">{editingIndex !== null ? "Edit" : "Add"} Gutter Item</h3>
275
+
276
+ <div class="form-group">
277
+ <label for="item-type">Type</label>
278
+ <Select id="item-type" bind:value={itemType}>
279
+ <option value="comment">Comment (Markdown)</option>
280
+ <option value="photo">Photo</option>
281
+ <option value="gallery">Image Gallery</option>
282
+ </Select>
283
+ </div>
284
+
285
+ <div class="form-group">
286
+ <label for="item-anchor">Anchor</label>
287
+ <div class="anchor-input-row">
288
+ <Input
289
+ type="text"
290
+ id="item-anchor"
291
+ bind:value={itemAnchor}
292
+ placeholder="## Heading or anchor:name"
293
+ />
294
+ <Button
295
+ variant="outline"
296
+ onclick={handleInsertAnchor}
297
+ title="Insert new anchor in editor"
298
+ >
299
+ + Anchor
300
+ </Button>
301
+ </div>
302
+ <span class="form-hint">
303
+ Use <code>## Heading</code>, <code>paragraph:N</code>, or <code>anchor:name</code>
304
+ </span>
305
+ </div>
306
+
307
+ {#if availableAnchors.length > 0}
308
+ <div class="available-anchors">
309
+ <span class="anchors-label">Available:</span>
310
+ {#each availableAnchors as anchor}
311
+ <button
312
+ type="button"
313
+ class="anchor-chip"
314
+ onclick={() => (itemAnchor = anchor)}
315
+ >
316
+ {anchor}
317
+ </button>
318
+ {/each}
319
+ </div>
320
+ {/if}
321
+
322
+ {#if itemType === "comment"}
323
+ <div class="form-group">
324
+ <label for="item-content">Content (Markdown)</label>
325
+ <textarea
326
+ id="item-content"
327
+ bind:value={itemContent}
328
+ placeholder="Write your note in markdown..."
329
+ rows="6"
330
+ class="form-input form-textarea"
331
+ ></textarea>
332
+ </div>
333
+ {/if}
334
+
335
+ {#if itemType === "photo"}
336
+ <div class="form-group">
337
+ <label for="item-url">Image URL</label>
338
+ <div class="url-input-row">
339
+ <Input
340
+ type="text"
341
+ id="item-url"
342
+ bind:value={itemUrl}
343
+ placeholder="https://cdn.autumnsgrove.com/..."
344
+ />
345
+ <Button
346
+ variant="outline"
347
+ onclick={() => openImagePicker((url) => (itemUrl = url))}
348
+ >
349
+ Browse CDN
350
+ </Button>
351
+ </div>
352
+ </div>
353
+
354
+ <div class="form-group">
355
+ <label for="item-caption">Caption (optional)</label>
356
+ <Input
357
+ type="text"
358
+ id="item-caption"
359
+ bind:value={itemCaption}
360
+ placeholder="Photo caption"
361
+ />
362
+ </div>
363
+
364
+ {#if itemUrl}
365
+ <div class="image-preview">
366
+ <img src={itemUrl} alt="Preview" />
367
+ </div>
368
+ {/if}
369
+ {/if}
370
+
371
+ {#if itemType === "gallery"}
372
+ <div class="form-group">
373
+ <label>Gallery Images</label>
374
+ <div class="gallery-list">
375
+ {#each galleryImages as image, i (i)}
376
+ <div class="gallery-image-item">
377
+ <img src={image.url} alt={image.alt || "Gallery image"} class="gallery-thumb" />
378
+ <div class="gallery-image-fields">
379
+ <Input
380
+ type="text"
381
+ value={image.alt}
382
+ oninput={(e) => updateGalleryImage(i, "alt", e.target.value)}
383
+ placeholder="Alt text"
384
+ class="small"
385
+ />
386
+ <Input
387
+ type="text"
388
+ value={image.caption}
389
+ oninput={(e) => updateGalleryImage(i, "caption", e.target.value)}
390
+ placeholder="Caption"
391
+ class="small"
392
+ />
393
+ </div>
394
+ <button
395
+ type="button"
396
+ class="remove-btn"
397
+ onclick={() => removeGalleryImage(i)}
398
+ >×</button>
399
+ </div>
400
+ {/each}
401
+ </div>
402
+ <button type="button" class="add-image-btn" onclick={addGalleryImage}>
403
+ + Add Image
404
+ </button>
405
+ </div>
406
+ {/if}
407
+
408
+ <div slot="footer" style="display: flex; gap: 0.75rem; justify-content: flex-end;">
409
+ <Button variant="outline" onclick={closeModal}>Cancel</Button>
410
+ <Button onclick={saveItem}>
411
+ {editingIndex !== null ? "Update" : "Add"} Item
412
+ </Button>
413
+ </div>
414
+ </Dialog>
415
+
416
+ <!-- Image Picker Modal -->
417
+ <Dialog bind:open={showImagePicker}>
418
+ <h3 slot="title">Select Image from CDN</h3>
419
+
420
+ <div class="picker-controls">
421
+ <Input
422
+ type="text"
423
+ bind:value={cdnFilter}
424
+ placeholder="Filter by folder (e.g., blog/)"
425
+ />
426
+ <Button onclick={loadCdnImages} disabled={cdnLoading}>
427
+ {cdnLoading ? "Loading..." : "Filter"}
428
+ </Button>
429
+ </div>
430
+
431
+ <div class="image-grid">
432
+ {#if cdnLoading}
433
+ <div class="loading">Loading images...</div>
434
+ {:else if cdnImages.length === 0}
435
+ <div class="no-images">No images found</div>
436
+ {:else}
437
+ {#each cdnImages as image (image.key)}
438
+ <button
439
+ class="image-option"
440
+ onclick={() => selectImage(image)}
441
+ >
442
+ <img src={image.url} alt={image.key} />
443
+ <span class="image-name">{image.key.split("/").pop()}</span>
444
+ </button>
445
+ {/each}
446
+ {/if}
447
+ </div>
448
+
449
+ <div slot="footer" style="display: flex; gap: 0.75rem; justify-content: flex-end;">
450
+ <Button variant="outline" onclick={closeImagePicker}>Cancel</Button>
451
+ </div>
452
+ </Dialog>
453
+
454
+ <style>
455
+ .gutter-manager {
456
+ background: #1e1e1e;
457
+ border: 1px solid #3a3a3a;
458
+ border-radius: 8px;
459
+ overflow: hidden;
460
+ }
461
+
462
+ .gutter-header {
463
+ display: flex;
464
+ justify-content: space-between;
465
+ align-items: center;
466
+ padding: 0.75rem 1rem;
467
+ background: #252526;
468
+ border-bottom: 1px solid #3a3a3a;
469
+ }
470
+
471
+ .gutter-header h3 {
472
+ margin: 0;
473
+ font-size: 0.9rem;
474
+ color: #8bc48b;
475
+ font-weight: 600;
476
+ }
477
+
478
+ .add-btn {
479
+ padding: 0.35rem 0.75rem;
480
+ background: #2d4a2d;
481
+ color: #a8dca8;
482
+ border: 1px solid #3d5a3d;
483
+ border-radius: 4px;
484
+ font-size: 0.8rem;
485
+ cursor: pointer;
486
+ transition: all 0.15s ease;
487
+ }
488
+
489
+ .add-btn:hover {
490
+ background: #3d5a3d;
491
+ color: #c8f0c8;
492
+ }
493
+
494
+ .empty-state {
495
+ padding: 2rem 1rem;
496
+ text-align: center;
497
+ color: #6a6a6a;
498
+ }
499
+
500
+ .empty-state p {
501
+ margin: 0.5rem 0;
502
+ }
503
+
504
+ .empty-state .hint {
505
+ font-size: 0.85rem;
506
+ color: #5a5a5a;
507
+ }
508
+
509
+ .gutter-list {
510
+ padding: 0.5rem;
511
+ }
512
+
513
+ .gutter-item {
514
+ background: #252526;
515
+ border: 1px solid #3a3a3a;
516
+ border-radius: 4px;
517
+ padding: 0.5rem 0.75rem;
518
+ margin-bottom: 0.5rem;
519
+ }
520
+
521
+ .item-header {
522
+ display: flex;
523
+ align-items: center;
524
+ gap: 0.5rem;
525
+ }
526
+
527
+ .item-type {
528
+ font-size: 1rem;
529
+ }
530
+
531
+ .item-anchor {
532
+ flex: 1;
533
+ font-family: monospace;
534
+ font-size: 0.8rem;
535
+ color: #9d9d9d;
536
+ white-space: nowrap;
537
+ overflow: hidden;
538
+ text-overflow: ellipsis;
539
+ }
540
+
541
+ .item-actions {
542
+ display: flex;
543
+ gap: 0.25rem;
544
+ }
545
+
546
+ .action-btn {
547
+ padding: 0.2rem 0.4rem;
548
+ background: transparent;
549
+ border: 1px solid transparent;
550
+ color: #6a6a6a;
551
+ border-radius: 3px;
552
+ cursor: pointer;
553
+ font-size: 0.85rem;
554
+ transition: all 0.15s ease;
555
+ }
556
+
557
+ .action-btn:hover:not(:disabled) {
558
+ background: #3a3a3a;
559
+ color: #d4d4d4;
560
+ }
561
+
562
+ .action-btn:disabled {
563
+ opacity: 0.3;
564
+ cursor: not-allowed;
565
+ }
566
+
567
+ .action-btn.delete:hover {
568
+ background: rgba(215, 58, 73, 0.2);
569
+ color: #f85149;
570
+ }
571
+
572
+ .item-preview {
573
+ margin-top: 0.35rem;
574
+ font-size: 0.8rem;
575
+ color: #6a6a6a;
576
+ white-space: nowrap;
577
+ overflow: hidden;
578
+ text-overflow: ellipsis;
579
+ }
580
+
581
+ /* Modal Styles */
582
+ .modal-overlay {
583
+ position: fixed;
584
+ top: 0;
585
+ left: 0;
586
+ right: 0;
587
+ bottom: 0;
588
+ background: rgba(0, 0, 0, 0.7);
589
+ display: flex;
590
+ align-items: center;
591
+ justify-content: center;
592
+ z-index: 1000;
593
+ padding: 1rem;
594
+ }
595
+
596
+ .modal-content {
597
+ background: #1e1e1e;
598
+ border: 1px solid #3a3a3a;
599
+ border-radius: 8px;
600
+ padding: 1.5rem;
601
+ max-width: 500px;
602
+ width: 100%;
603
+ max-height: 80vh;
604
+ overflow-y: auto;
605
+ }
606
+
607
+ .modal-content h3 {
608
+ margin: 0 0 1.25rem 0;
609
+ color: #d4d4d4;
610
+ font-size: 1.1rem;
611
+ }
612
+
613
+ .form-group {
614
+ margin-bottom: 1rem;
615
+ }
616
+
617
+ .form-group label {
618
+ display: block;
619
+ margin-bottom: 0.4rem;
620
+ font-size: 0.85rem;
621
+ color: #9d9d9d;
622
+ }
623
+
624
+ .form-input {
625
+ width: 100%;
626
+ padding: 0.5rem 0.75rem;
627
+ background: #252526;
628
+ border: 1px solid #3a3a3a;
629
+ border-radius: 4px;
630
+ color: #d4d4d4;
631
+ font-size: 0.9rem;
632
+ font-family: inherit;
633
+ }
634
+
635
+ .form-input:focus {
636
+ outline: none;
637
+ border-color: #4a7c4a;
638
+ }
639
+
640
+ .form-input.small {
641
+ padding: 0.35rem 0.5rem;
642
+ font-size: 0.8rem;
643
+ }
644
+
645
+ .form-textarea {
646
+ resize: vertical;
647
+ min-height: 100px;
648
+ font-family: "JetBrains Mono", "Fira Code", monospace;
649
+ }
650
+
651
+ .form-hint {
652
+ display: block;
653
+ margin-top: 0.35rem;
654
+ font-size: 0.75rem;
655
+ color: #6a6a6a;
656
+ }
657
+
658
+ .form-hint code {
659
+ background: #252526;
660
+ padding: 0.1rem 0.3rem;
661
+ border-radius: 2px;
662
+ color: #ce9178;
663
+ }
664
+
665
+ .anchor-input-row,
666
+ .url-input-row {
667
+ display: flex;
668
+ gap: 0.5rem;
669
+ }
670
+
671
+ .anchor-input-row .form-input,
672
+ .url-input-row .form-input {
673
+ flex: 1;
674
+ }
675
+
676
+ .insert-anchor-btn,
677
+ .browse-btn {
678
+ padding: 0.5rem 0.75rem;
679
+ background: #2d4a2d;
680
+ color: #a8dca8;
681
+ border: 1px solid #3d5a3d;
682
+ border-radius: 4px;
683
+ font-size: 0.8rem;
684
+ white-space: nowrap;
685
+ cursor: pointer;
686
+ }
687
+
688
+ .insert-anchor-btn:hover,
689
+ .browse-btn:hover {
690
+ background: #3d5a3d;
691
+ }
692
+
693
+ .available-anchors {
694
+ display: flex;
695
+ flex-wrap: wrap;
696
+ gap: 0.35rem;
697
+ align-items: center;
698
+ margin-bottom: 1rem;
699
+ }
700
+
701
+ .anchors-label {
702
+ font-size: 0.75rem;
703
+ color: #6a6a6a;
704
+ }
705
+
706
+ .anchor-chip {
707
+ padding: 0.2rem 0.5rem;
708
+ background: #252526;
709
+ border: 1px solid #3a3a3a;
710
+ border-radius: 12px;
711
+ color: #9d9d9d;
712
+ font-size: 0.7rem;
713
+ font-family: monospace;
714
+ cursor: pointer;
715
+ }
716
+
717
+ .anchor-chip:hover {
718
+ background: #3a3a3a;
719
+ color: #d4d4d4;
720
+ }
721
+
722
+ .image-preview {
723
+ margin-top: 0.5rem;
724
+ max-height: 150px;
725
+ overflow: hidden;
726
+ border-radius: 4px;
727
+ background: #252526;
728
+ }
729
+
730
+ .image-preview img {
731
+ width: 100%;
732
+ height: auto;
733
+ -o-object-fit: contain;
734
+ object-fit: contain;
735
+ }
736
+
737
+ .gallery-list {
738
+ display: flex;
739
+ flex-direction: column;
740
+ gap: 0.5rem;
741
+ margin-bottom: 0.75rem;
742
+ }
743
+
744
+ .gallery-image-item {
745
+ display: flex;
746
+ gap: 0.5rem;
747
+ align-items: center;
748
+ background: #252526;
749
+ padding: 0.5rem;
750
+ border-radius: 4px;
751
+ border: 1px solid #3a3a3a;
752
+ }
753
+
754
+ .gallery-thumb {
755
+ width: 50px;
756
+ height: 50px;
757
+ -o-object-fit: cover;
758
+ object-fit: cover;
759
+ border-radius: 3px;
760
+ }
761
+
762
+ .gallery-image-fields {
763
+ flex: 1;
764
+ display: flex;
765
+ flex-direction: column;
766
+ gap: 0.35rem;
767
+ }
768
+
769
+ .remove-btn {
770
+ padding: 0.25rem 0.5rem;
771
+ background: transparent;
772
+ border: none;
773
+ color: #f85149;
774
+ font-size: 1.2rem;
775
+ cursor: pointer;
776
+ }
777
+
778
+ .add-image-btn {
779
+ padding: 0.5rem;
780
+ background: transparent;
781
+ border: 1px dashed #3a3a3a;
782
+ border-radius: 4px;
783
+ color: #6a6a6a;
784
+ cursor: pointer;
785
+ font-size: 0.85rem;
786
+ width: 100%;
787
+ }
788
+
789
+ .add-image-btn:hover {
790
+ border-color: #4a7c4a;
791
+ color: #8bc48b;
792
+ }
793
+
794
+ .modal-actions {
795
+ display: flex;
796
+ justify-content: flex-end;
797
+ gap: 0.75rem;
798
+ margin-top: 1.5rem;
799
+ padding-top: 1rem;
800
+ border-top: 1px solid #3a3a3a;
801
+ }
802
+
803
+ .cancel-btn,
804
+ .save-btn {
805
+ padding: 0.5rem 1rem;
806
+ border-radius: 4px;
807
+ font-size: 0.9rem;
808
+ cursor: pointer;
809
+ transition: all 0.15s ease;
810
+ }
811
+
812
+ .cancel-btn {
813
+ background: transparent;
814
+ border: 1px solid #3a3a3a;
815
+ color: #9d9d9d;
816
+ }
817
+
818
+ .cancel-btn:hover {
819
+ background: #3a3a3a;
820
+ }
821
+
822
+ .save-btn {
823
+ background: #4a7c4a;
824
+ border: none;
825
+ color: #c8f0c8;
826
+ }
827
+
828
+ .save-btn:hover {
829
+ background: #5a9c5a;
830
+ }
831
+
832
+ /* Image Picker Modal */
833
+ .image-picker-modal {
834
+ max-width: 700px;
835
+ }
836
+
837
+ .picker-controls {
838
+ display: flex;
839
+ gap: 0.5rem;
840
+ margin-bottom: 1rem;
841
+ }
842
+
843
+ .picker-controls .form-input {
844
+ flex: 1;
845
+ }
846
+
847
+ .filter-btn {
848
+ padding: 0.5rem 1rem;
849
+ background: #3a3a3a;
850
+ border: none;
851
+ border-radius: 4px;
852
+ color: #d4d4d4;
853
+ cursor: pointer;
854
+ }
855
+
856
+ .filter-btn:hover {
857
+ background: #4a4a4a;
858
+ }
859
+
860
+ .image-grid {
861
+ display: grid;
862
+ grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
863
+ gap: 0.5rem;
864
+ max-height: 400px;
865
+ overflow-y: auto;
866
+ padding: 0.5rem;
867
+ background: #252526;
868
+ border-radius: 4px;
869
+ }
870
+
871
+ .loading,
872
+ .no-images {
873
+ grid-column: 1 / -1;
874
+ text-align: center;
875
+ padding: 2rem;
876
+ color: #6a6a6a;
877
+ }
878
+
879
+ .image-option {
880
+ display: flex;
881
+ flex-direction: column;
882
+ background: #1e1e1e;
883
+ border: 2px solid transparent;
884
+ border-radius: 4px;
885
+ padding: 0.25rem;
886
+ cursor: pointer;
887
+ transition: border-color 0.15s ease;
888
+ }
889
+
890
+ .image-option:hover {
891
+ border-color: #4a7c4a;
892
+ }
893
+
894
+ .image-option img {
895
+ width: 100%;
896
+ aspect-ratio: 1;
897
+ -o-object-fit: cover;
898
+ object-fit: cover;
899
+ border-radius: 2px;
900
+ }
901
+
902
+ .image-name {
903
+ font-size: 0.65rem;
904
+ color: #6a6a6a;
905
+ margin-top: 0.25rem;
906
+ white-space: nowrap;
907
+ overflow: hidden;
908
+ text-overflow: ellipsis;
909
+ }
910
+ </style>