@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,646 @@
1
+ <script>
2
+ import { tick, untrack, onMount } from 'svelte';
3
+ import { browser } from '$app/environment';
4
+ import TableOfContents from './TableOfContents.svelte';
5
+ import MobileTOC from './MobileTOC.svelte';
6
+ import GutterItem from './GutterItem.svelte';
7
+ import {
8
+ getAnchorKey,
9
+ getUniqueAnchors,
10
+ getAnchorLabel,
11
+ getItemsForAnchor,
12
+ getOrphanItems,
13
+ findAnchorElement,
14
+ parseAnchor
15
+ } from '../../utils/gutter.js';
16
+ import '../../styles/content.css';
17
+
18
+ // Constants for positioning calculations
19
+ const MIN_GAP = 16; // Minimum gap between items in pixels
20
+ const BOTTOM_PADDING = 32; // Padding from bottom of content
21
+ const HIDDEN_POSITION = -9999; // Position for hidden items
22
+ const DEBOUNCE_DELAY = 100; // Debounce delay for resize in ms
23
+
24
+ let {
25
+ content = '',
26
+ gutterContent = [],
27
+ headers = [],
28
+ showTableOfContents = true,
29
+ children
30
+ } = $props();
31
+
32
+ // References to mobile gutter containers for each anchor
33
+ let mobileGutterRefs = $state({});
34
+
35
+ // Track content height for overflow detection
36
+ let contentBodyElement = $state();
37
+ let contentHeight = $state(0);
38
+ let overflowingAnchorKeys = $state([]);
39
+
40
+ // Gutter positioning state
41
+ let gutterElement = $state();
42
+ let itemPositions = $state({});
43
+ let anchorGroupElements = $state({});
44
+
45
+ // Compute unique anchors once as a derived value (performance optimization)
46
+ let uniqueAnchors = $derived(getUniqueAnchors(gutterContent));
47
+ let orphanItems = $derived(getOrphanItems(gutterContent, headers));
48
+
49
+ // Check if we have content for gutters
50
+ let hasLeftGutter = $derived(gutterContent && gutterContent.length > 0);
51
+ let hasRightGutter = $derived(showTableOfContents && headers && headers.length > 0);
52
+ let hasGutters = $derived(hasLeftGutter || hasRightGutter);
53
+ let hasOverflow = $derived(overflowingAnchorKeys.length > 0);
54
+
55
+ // Helper to get anchor key with headers context
56
+ function getKey(anchor) {
57
+ return getAnchorKey(anchor, headers);
58
+ }
59
+
60
+ // Get items for a specific anchor
61
+ function getItems(anchor) {
62
+ return getItemsForAnchor(gutterContent, anchor);
63
+ }
64
+
65
+ // Generate unique key for a gutter item
66
+ function getItemKey(item, index) {
67
+ // Combine item properties to create a unique identifier
68
+ const parts = [
69
+ item.type || 'unknown',
70
+ item.file || item.src || item.url || '',
71
+ item.anchor || '',
72
+ index.toString()
73
+ ];
74
+ return parts.join('-');
75
+ }
76
+
77
+ /**
78
+ * Calculate positions based on anchor locations, with collision detection
79
+ */
80
+ async function updatePositions() {
81
+ if (!gutterElement || !contentBodyElement) return;
82
+
83
+ await tick(); // Wait for DOM to update
84
+
85
+ const gutterTop = gutterElement.offsetTop;
86
+
87
+ let lastBottom = 0; // Track the bottom edge of the last positioned item
88
+ const newOverflowingAnchors = [];
89
+ const newPositions = { ...itemPositions };
90
+
91
+ // Sort anchors by their position in the document
92
+ const anchorPositions = uniqueAnchors.map(anchor => {
93
+ const el = findAnchorElement(anchor, contentBodyElement, headers);
94
+ if (!el && import.meta.env.DEV) {
95
+ console.warn(`Anchor element not found for: ${anchor}`);
96
+ }
97
+ return {
98
+ anchor,
99
+ key: getKey(anchor),
100
+ element: el,
101
+ top: el ? el.offsetTop : Infinity
102
+ };
103
+ }).sort((a, b) => a.top - b.top);
104
+
105
+ anchorPositions.forEach(({ anchor, key, element }) => {
106
+ const groupEl = anchorGroupElements[key];
107
+
108
+ if (element && groupEl) {
109
+ // Desired position (aligned with anchor element)
110
+ let desiredTop = element.offsetTop - gutterTop;
111
+
112
+ // Get the height of this gutter group
113
+ const groupHeight = groupEl.offsetHeight;
114
+
115
+ // Check for collision with previous item
116
+ if (desiredTop < lastBottom + MIN_GAP) {
117
+ // Push down to avoid overlap
118
+ desiredTop = lastBottom + MIN_GAP;
119
+ }
120
+
121
+ // Check if this item would overflow past the content
122
+ const effectiveContentHeight = contentHeight > 0 ? contentHeight : Infinity;
123
+ if (desiredTop + groupHeight > effectiveContentHeight - BOTTOM_PADDING) {
124
+ // This item overflows - mark it and hide it in the gutter
125
+ newOverflowingAnchors.push(key);
126
+ newPositions[key] = HIDDEN_POSITION;
127
+ } else {
128
+ newPositions[key] = desiredTop;
129
+ // Update lastBottom for next iteration
130
+ lastBottom = desiredTop + groupHeight;
131
+ }
132
+ } else if (groupEl) {
133
+ // Element not found - hide this group
134
+ newPositions[key] = HIDDEN_POSITION;
135
+ }
136
+ });
137
+
138
+ // Update state with new objects (idiomatic Svelte 5)
139
+ itemPositions = newPositions;
140
+ overflowingAnchorKeys = newOverflowingAnchors;
141
+ }
142
+
143
+ // Setup resize listener on mount with proper cleanup
144
+ onMount(() => {
145
+ let resizeTimeoutId;
146
+ const handleResize = () => {
147
+ clearTimeout(resizeTimeoutId);
148
+ resizeTimeoutId = setTimeout(() => {
149
+ requestAnimationFrame(updatePositions);
150
+ }, DEBOUNCE_DELAY);
151
+ };
152
+
153
+ window.addEventListener('resize', handleResize);
154
+ return () => {
155
+ clearTimeout(resizeTimeoutId);
156
+ window.removeEventListener('resize', handleResize);
157
+ };
158
+ });
159
+
160
+ // Setup copy button functionality for code blocks
161
+ onMount(() => {
162
+ const handleCopyClick = async (event) => {
163
+ const button = event.currentTarget;
164
+ const codeText = button.getAttribute('data-code');
165
+
166
+ if (!codeText) return;
167
+
168
+ try {
169
+ // Decode HTML entities back to original text
170
+ const textarea = document.createElement('textarea');
171
+ textarea.innerHTML = codeText;
172
+ const decodedText = textarea.value;
173
+
174
+ await navigator.clipboard.writeText(decodedText);
175
+
176
+ // Update button text and style to show success
177
+ const copyText = button.querySelector('.copy-text');
178
+ const originalText = copyText.textContent;
179
+ copyText.textContent = 'Copied!';
180
+ button.classList.add('copied');
181
+
182
+ // Reset after 2 seconds
183
+ setTimeout(() => {
184
+ copyText.textContent = originalText;
185
+ button.classList.remove('copied');
186
+ }, 2000);
187
+ } catch (err) {
188
+ console.error('Failed to copy code:', err);
189
+ const copyText = button.querySelector('.copy-text');
190
+ copyText.textContent = 'Failed';
191
+ setTimeout(() => {
192
+ copyText.textContent = 'Copy';
193
+ }, 2000);
194
+ }
195
+ };
196
+
197
+ // Attach event listeners to all copy buttons
198
+ const copyButtons = document.querySelectorAll('.code-block-copy');
199
+ copyButtons.forEach(button => {
200
+ button.addEventListener('click', handleCopyClick);
201
+ });
202
+
203
+ // Cleanup
204
+ return () => {
205
+ copyButtons.forEach(button => {
206
+ button.removeEventListener('click', handleCopyClick);
207
+ });
208
+ };
209
+ });
210
+
211
+ // Handle initial positioning and re-calculate when dependencies change
212
+ $effect(() => {
213
+ // Explicitly reference dependencies to track changes
214
+ gutterContent;
215
+ headers;
216
+ contentHeight;
217
+ uniqueAnchors;
218
+
219
+ // Use requestAnimationFrame for smoother updates
220
+ requestAnimationFrame(updatePositions);
221
+ });
222
+
223
+ // Add IDs to headers and position mobile gutter items
224
+ $effect(() => {
225
+ // Track moved elements for cleanup
226
+ const movedElements = [];
227
+
228
+ untrack(() => {
229
+ if (!contentBodyElement) return;
230
+
231
+ // First, add IDs to headers
232
+ if (headers && headers.length > 0) {
233
+ const headerElements = contentBodyElement.querySelectorAll('h1, h2, h3, h4, h5, h6');
234
+ headerElements.forEach((el) => {
235
+ const text = el.textContent.trim();
236
+ const matchingHeader = headers.find(h => h.text === text);
237
+ if (matchingHeader) {
238
+ el.id = matchingHeader.id;
239
+ }
240
+ });
241
+ }
242
+
243
+ // Position mobile gutter items for all anchor types
244
+ for (const anchor of uniqueAnchors) {
245
+ const anchorKey = getKey(anchor);
246
+ const mobileGutterEl = mobileGutterRefs[anchorKey];
247
+ if (!mobileGutterEl || mobileGutterEl.children.length === 0) continue;
248
+
249
+ // Track original parent for cleanup
250
+ const originalParent = mobileGutterEl.parentElement;
251
+ const originalNextSibling = mobileGutterEl.nextSibling;
252
+
253
+ const targetEl = findAnchorElement(anchor, contentBodyElement, headers);
254
+
255
+ if (targetEl) {
256
+ targetEl.insertAdjacentElement('afterend', mobileGutterEl);
257
+ movedElements.push({ element: mobileGutterEl, originalParent, originalNextSibling });
258
+ }
259
+ }
260
+ });
261
+
262
+ // Cleanup: restore moved elements to their original positions
263
+ return () => {
264
+ for (const { element, originalParent, originalNextSibling } of movedElements) {
265
+ if (originalParent && element.parentElement !== originalParent) {
266
+ if (originalNextSibling) {
267
+ originalParent.insertBefore(element, originalNextSibling);
268
+ } else {
269
+ originalParent.appendChild(element);
270
+ }
271
+ }
272
+ }
273
+ };
274
+ });
275
+
276
+ // Track content height (only the content-body to avoid feedback loop with overflow section)
277
+ $effect(() => {
278
+ if (contentBodyElement) {
279
+ const updateHeight = () => {
280
+ // Get the bottom of content-body relative to the article
281
+ const rect = contentBodyElement.getBoundingClientRect();
282
+ const articleRect = contentBodyElement.closest('.content-article')?.getBoundingClientRect();
283
+ if (articleRect) {
284
+ contentHeight = rect.bottom - articleRect.top;
285
+ } else {
286
+ contentHeight = contentBodyElement.offsetTop + contentBodyElement.offsetHeight;
287
+ }
288
+ };
289
+ updateHeight();
290
+
291
+ // Create ResizeObserver to track height changes
292
+ const observer = new ResizeObserver(updateHeight);
293
+ observer.observe(contentBodyElement);
294
+
295
+ return () => observer.disconnect();
296
+ }
297
+ });
298
+
299
+ // Get items for overflowing anchors with reference numbers
300
+ function getOverflowItems() {
301
+ const items = [];
302
+ let refNum = 1;
303
+ for (const anchorKey of overflowingAnchorKeys) {
304
+ // Find the original anchor string that matches this key
305
+ const anchor = uniqueAnchors.find(a => getKey(a) === anchorKey);
306
+ if (anchor) {
307
+ const anchorItems = getItems(anchor);
308
+ const label = getAnchorLabel(anchor);
309
+ items.push({ anchorKey, label, items: anchorItems, refNum });
310
+ refNum++;
311
+ }
312
+ }
313
+ return items;
314
+ }
315
+
316
+ // Inject reference markers into content HTML for overflowing items
317
+ function injectReferenceMarkers(html, overflowKeys) {
318
+ if (!overflowKeys || overflowKeys.length === 0 || typeof window === 'undefined') {
319
+ return html;
320
+ }
321
+
322
+ const parser = new DOMParser();
323
+ const doc = parser.parseFromString(html, 'text/html');
324
+
325
+ let refNum = 1;
326
+ for (const anchorKey of overflowKeys) {
327
+ const anchor = uniqueAnchors.find(a => getKey(a) === anchorKey);
328
+ if (!anchor) continue;
329
+
330
+ const parsed = parseAnchor(anchor);
331
+ let targetEl = null;
332
+
333
+ switch (parsed.type) {
334
+ case 'header': {
335
+ const headerText = anchor.replace(/^#+\s*/, '');
336
+ // Find header by text content
337
+ const allHeaders = doc.body.querySelectorAll('h1, h2, h3, h4, h5, h6');
338
+ for (const h of allHeaders) {
339
+ if (h.textContent.trim() === headerText) {
340
+ targetEl = h;
341
+ break;
342
+ }
343
+ }
344
+ break;
345
+ }
346
+ case 'paragraph': {
347
+ const paragraphs = doc.body.querySelectorAll(':scope > p');
348
+ const index = parsed.value - 1;
349
+ if (index >= 0 && index < paragraphs.length) {
350
+ targetEl = paragraphs[index];
351
+ }
352
+ break;
353
+ }
354
+ case 'tag': {
355
+ targetEl = doc.body.querySelector(`[data-anchor="${parsed.value}"]`);
356
+ break;
357
+ }
358
+ }
359
+
360
+ if (targetEl) {
361
+ // Create reference marker
362
+ const marker = doc.createElement('sup');
363
+ marker.className = 'gutter-ref-marker';
364
+ marker.id = `ref-${refNum}`;
365
+
366
+ const link = doc.createElement('a');
367
+ link.href = `#overflow-${refNum}`;
368
+ link.textContent = refNum;
369
+ link.title = `See gutter content for: ${getAnchorLabel(anchor)}`;
370
+
371
+ marker.appendChild(link);
372
+
373
+ // Insert marker based on element type
374
+ if (parsed.type === 'header') {
375
+ // Insert after header text
376
+ targetEl.appendChild(doc.createTextNode(' '));
377
+ targetEl.appendChild(marker);
378
+ } else {
379
+ // Insert at start of paragraph/tag element
380
+ targetEl.insertBefore(marker, targetEl.firstChild);
381
+ targetEl.insertBefore(doc.createTextNode(' '), marker.nextSibling);
382
+ }
383
+ }
384
+
385
+ refNum++;
386
+ }
387
+
388
+ return doc.body.innerHTML;
389
+ }
390
+
391
+ // Derive content with reference markers injected
392
+ let processedContent = $derived(injectReferenceMarkers(content, overflowingAnchorKeys));
393
+
394
+ // Sanitize HTML content to prevent XSS attacks (browser-only for SSR compatibility)
395
+ let DOMPurify = $state(null);
396
+
397
+ // Load DOMPurify only in browser
398
+ onMount(async () => {
399
+ if (browser) {
400
+ const module = await import('dompurify');
401
+ DOMPurify = module.default;
402
+ }
403
+ });
404
+
405
+ let sanitizedContent = $derived(
406
+ DOMPurify
407
+ ? DOMPurify.sanitize(processedContent, {
408
+ ALLOWED_TAGS: [
409
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
410
+ 'p', 'a', 'ul', 'ol', 'li', 'blockquote',
411
+ 'code', 'pre', 'strong', 'em', 'img',
412
+ 'table', 'thead', 'tbody', 'tr', 'th', 'td',
413
+ 'br', 'hr', 'div', 'span', 'sup', 'sub',
414
+ 'del', 'ins'
415
+ ],
416
+ ALLOWED_ATTR: [
417
+ 'href', 'src', 'alt', 'title', 'class', 'id',
418
+ 'data-anchor', 'data-language', 'data-line-numbers'
419
+ ],
420
+ ALLOW_DATA_ATTR: true
421
+ })
422
+ : processedContent
423
+ );
424
+ </script>
425
+
426
+ <div class="content-layout"
427
+ class:has-gutters={hasGutters}
428
+ class:has-left-gutter={hasLeftGutter}
429
+ class:has-right-gutter={hasRightGutter}
430
+ class:has-both-gutters={hasLeftGutter && hasRightGutter}>
431
+ <!-- Left Gutter - Comments/Photos/Emojis -->
432
+ {#if hasLeftGutter}
433
+ <div class="left-gutter-container desktop-only">
434
+ <aside class="left-gutter" bind:this={gutterElement}>
435
+ <!-- Show orphan items at the top -->
436
+ {#each orphanItems as item, index (getItemKey(item, index))}
437
+ <div class="gutter-item-wrapper">
438
+ <GutterItem {item} />
439
+ </div>
440
+ {/each}
441
+
442
+ <!-- Show items positioned by anchor -->
443
+ {#each uniqueAnchors as anchor (anchor)}
444
+ {@const anchorKey = getKey(anchor)}
445
+ {@const anchorItems = getItems(anchor)}
446
+ {#if anchorItems.length > 0}
447
+ <div
448
+ class="anchor-group"
449
+ data-for-anchor={anchorKey}
450
+ style="top: {itemPositions[anchorKey] || 0}px"
451
+ bind:this={anchorGroupElements[anchorKey]}
452
+ >
453
+ {#each anchorItems as item, index (getItemKey(item, index))}
454
+ <GutterItem {item} />
455
+ {/each}
456
+ </div>
457
+ {/if}
458
+ {/each}
459
+ </aside>
460
+ </div>
461
+ {/if}
462
+
463
+ <!-- Main Content -->
464
+ <article class="content-article">
465
+ <!-- Custom header content via children/slot -->
466
+ {#if children}
467
+ {@render children()}
468
+ {/if}
469
+
470
+ <!-- Mobile gutter: orphan items at top (no matching anchor) -->
471
+ {#if hasLeftGutter && orphanItems.length > 0}
472
+ <div class="mobile-gutter-content">
473
+ {#each orphanItems as item, index (getItemKey(item, index))}
474
+ <GutterItem {item} />
475
+ {/each}
476
+ </div>
477
+ {/if}
478
+
479
+ <!-- Mobile gutter containers for each anchor (will be moved into position) -->
480
+ {#if hasLeftGutter}
481
+ {#each uniqueAnchors as anchor (anchor)}
482
+ {@const anchorKey = getKey(anchor)}
483
+ {@const anchorItems = getItems(anchor)}
484
+ {#if anchorItems.length > 0}
485
+ <div
486
+ class="mobile-gutter-content mobile-gutter-inline"
487
+ bind:this={mobileGutterRefs[anchorKey]}
488
+ >
489
+ {#each anchorItems as item, index (getItemKey(item, index))}
490
+ <GutterItem {item} />
491
+ {/each}
492
+ </div>
493
+ {/if}
494
+ {/each}
495
+ {/if}
496
+
497
+ <div class="prose prose-lg dark:prose-invert max-w-none content-body" bind:this={contentBodyElement}>
498
+ {@html sanitizedContent}
499
+ </div>
500
+
501
+ <!-- Overflow gutter items rendered inline -->
502
+ {#if hasOverflow}
503
+ <div class="overflow-gutter-section">
504
+ <div class="overflow-divider"></div>
505
+ {#each getOverflowItems() as group (group.anchorKey)}
506
+ <div class="overflow-group">
507
+ <h4 class="overflow-anchor-label">From: {group.label}</h4>
508
+ {#each group.items as item, index (getItemKey(item, index))}
509
+ <GutterItem {item} />
510
+ {/each}
511
+ </div>
512
+ {/each}
513
+ </div>
514
+ {/if}
515
+ </article>
516
+
517
+ <!-- Right Gutter - Table of Contents -->
518
+ {#if hasRightGutter}
519
+ <div class="right-gutter-container desktop-only">
520
+ <TableOfContents {headers} />
521
+ </div>
522
+ {/if}
523
+ </div>
524
+
525
+ <!-- Mobile TOC Button -->
526
+ {#if hasRightGutter}
527
+ <MobileTOC {headers} />
528
+ {/if}
529
+
530
+ <style>
531
+ /* Left gutter styles */
532
+ .left-gutter {
533
+ position: relative;
534
+ padding: 1rem;
535
+ min-height: 100%;
536
+ }
537
+ .gutter-item-wrapper {
538
+ margin-bottom: 1rem;
539
+ }
540
+ .anchor-group {
541
+ position: absolute;
542
+ left: 1rem;
543
+ right: 1rem;
544
+ }
545
+ /* Scrollbar styling */
546
+ .left-gutter::-webkit-scrollbar {
547
+ width: 4px;
548
+ }
549
+ .left-gutter::-webkit-scrollbar-track {
550
+ background: transparent;
551
+ }
552
+ .left-gutter::-webkit-scrollbar-thumb {
553
+ background: var(--light-text-secondary);
554
+ border-radius: 2px;
555
+ }
556
+ :global(.dark) .left-gutter::-webkit-scrollbar-thumb {
557
+ background: var(--light-border-light);
558
+ }
559
+ /* Overflow gutter section */
560
+ .overflow-gutter-section {
561
+ margin-top: 3rem;
562
+ padding-top: 2rem;
563
+ }
564
+ .overflow-divider {
565
+ height: 1px;
566
+ background: linear-gradient(to right, transparent, var(--light-border-primary), transparent);
567
+ margin-bottom: 2rem;
568
+ }
569
+ :global(.dark) .overflow-divider {
570
+ background: linear-gradient(to right, transparent, var(--light-border-primary), transparent);
571
+ }
572
+ .overflow-group {
573
+ margin-bottom: 2rem;
574
+ }
575
+ .overflow-anchor-label {
576
+ font-size: 0.85rem;
577
+ color: var(--light-text-light);
578
+ margin: 0 0 0.75rem 0;
579
+ font-weight: 500;
580
+ text-transform: uppercase;
581
+ letter-spacing: 0.05em;
582
+ }
583
+ :global(.dark) .overflow-anchor-label {
584
+ color: #666;
585
+ }
586
+ /* Reference number in overflow label */
587
+ .overflow-ref-num {
588
+ color: #2c5f2d;
589
+ font-weight: 600;
590
+ margin-right: 0.5rem;
591
+ }
592
+ :global(.dark) .overflow-ref-num {
593
+ color: var(--accent-success);
594
+ }
595
+ /* Reference markers in content (global because they're in @html) */
596
+ :global(.gutter-ref-marker) {
597
+ font-size: 0.75em;
598
+ vertical-align: super;
599
+ line-height: 0;
600
+ margin-left: 0.1em;
601
+ }
602
+ :global(.gutter-ref-marker a) {
603
+ color: #2c5f2d;
604
+ text-decoration: none;
605
+ font-weight: 600;
606
+ padding: 0.1em 0.3em;
607
+ background: rgba(44, 95, 45, 0.1);
608
+ border-radius: 3px;
609
+ transition: background-color 0.2s ease, color 0.2s ease;
610
+ }
611
+ :global(.dark .gutter-ref-marker a) {
612
+ color: var(--accent-success);
613
+ background: rgba(92, 184, 95, 0.15);
614
+ }
615
+ :global(.gutter-ref-marker a:hover) {
616
+ background: rgba(44, 95, 45, 0.2);
617
+ color: #4a9d4f;
618
+ }
619
+ :global(.dark .gutter-ref-marker a:hover) {
620
+ background: rgba(92, 184, 95, 0.25);
621
+ color: var(--accent-success-light);
622
+ }
623
+ /* Smooth scroll target highlighting */
624
+ .overflow-group:target {
625
+ animation: highlight-flash 1.5s ease-out;
626
+ }
627
+ @keyframes highlight-flash {
628
+ 0% {
629
+ background-color: rgba(44, 95, 45, 0.2);
630
+ }
631
+ 100% {
632
+ background-color: transparent;
633
+ }
634
+ }
635
+ :global(.dark) .overflow-group:target {
636
+ animation: highlight-flash-dark 1.5s ease-out;
637
+ }
638
+ @keyframes highlight-flash-dark {
639
+ 0% {
640
+ background-color: rgba(92, 184, 95, 0.2);
641
+ }
642
+ 100% {
643
+ background-color: transparent;
644
+ }
645
+ }
646
+ </style>
@@ -0,0 +1,19 @@
1
+ export default ContentWithGutter;
2
+ type ContentWithGutter = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ declare const ContentWithGutter: import("svelte").Component<{
7
+ content?: string;
8
+ gutterContent?: any[];
9
+ headers?: any[];
10
+ showTableOfContents?: boolean;
11
+ children: any;
12
+ }, {}, "">;
13
+ type $$ComponentProps = {
14
+ content?: string;
15
+ gutterContent?: any[];
16
+ headers?: any[];
17
+ showTableOfContents?: boolean;
18
+ children: any;
19
+ };