@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.
- package/README.md +163 -0
- package/dist/auth/jwt.d.ts +14 -0
- package/dist/auth/jwt.js +109 -0
- package/dist/auth/session.d.ts +42 -0
- package/dist/auth/session.js +105 -0
- package/dist/components/admin/GutterManager.svelte +910 -0
- package/dist/components/admin/GutterManager.svelte.d.ts +15 -0
- package/dist/components/admin/MarkdownEditor.svelte +3114 -0
- package/dist/components/admin/MarkdownEditor.svelte.d.ts +43 -0
- package/dist/components/custom/CollapsibleSection.svelte +74 -0
- package/dist/components/custom/CollapsibleSection.svelte.d.ts +15 -0
- package/dist/components/custom/ContentWithGutter.svelte +646 -0
- package/dist/components/custom/ContentWithGutter.svelte.d.ts +19 -0
- package/dist/components/custom/GutterItem.svelte +201 -0
- package/dist/components/custom/GutterItem.svelte.d.ts +11 -0
- package/dist/components/custom/LeftGutter.svelte +271 -0
- package/dist/components/custom/LeftGutter.svelte.d.ts +17 -0
- package/dist/components/custom/MobileTOC.svelte +273 -0
- package/dist/components/custom/MobileTOC.svelte.d.ts +11 -0
- package/dist/components/custom/TableOfContents.svelte +163 -0
- package/dist/components/custom/TableOfContents.svelte.d.ts +11 -0
- package/dist/components/gallery/ImageGallery.svelte +681 -0
- package/dist/components/gallery/ImageGallery.svelte.d.ts +11 -0
- package/dist/components/gallery/Lightbox.svelte +107 -0
- package/dist/components/gallery/Lightbox.svelte.d.ts +19 -0
- package/dist/components/gallery/LightboxCaption.svelte +25 -0
- package/dist/components/gallery/LightboxCaption.svelte.d.ts +11 -0
- package/dist/components/gallery/ZoomableImage.svelte +163 -0
- package/dist/components/gallery/ZoomableImage.svelte.d.ts +17 -0
- package/dist/components/ui/Accordion.svelte +74 -0
- package/dist/components/ui/Accordion.svelte.d.ts +42 -0
- package/dist/components/ui/Badge.svelte +48 -0
- package/dist/components/ui/Badge.svelte.d.ts +26 -0
- package/dist/components/ui/Button.svelte +74 -0
- package/dist/components/ui/Button.svelte.d.ts +34 -0
- package/dist/components/ui/Card.svelte +102 -0
- package/dist/components/ui/Card.svelte.d.ts +46 -0
- package/dist/components/ui/Dialog.svelte +91 -0
- package/dist/components/ui/Dialog.svelte.d.ts +43 -0
- package/dist/components/ui/Input.svelte +81 -0
- package/dist/components/ui/Input.svelte.d.ts +35 -0
- package/dist/components/ui/Select.svelte +69 -0
- package/dist/components/ui/Select.svelte.d.ts +36 -0
- package/dist/components/ui/Sheet.svelte +98 -0
- package/dist/components/ui/Sheet.svelte.d.ts +45 -0
- package/dist/components/ui/Skeleton.svelte +31 -0
- package/dist/components/ui/Skeleton.svelte.d.ts +26 -0
- package/dist/components/ui/Table.svelte +59 -0
- package/dist/components/ui/Table.svelte.d.ts +44 -0
- package/dist/components/ui/Tabs.svelte +76 -0
- package/dist/components/ui/Tabs.svelte.d.ts +41 -0
- package/dist/components/ui/Textarea.svelte +81 -0
- package/dist/components/ui/Textarea.svelte.d.ts +35 -0
- package/dist/components/ui/Toast.svelte +18 -0
- package/dist/components/ui/Toast.svelte.d.ts +7 -0
- package/dist/components/ui/accordion/accordion-content.svelte +24 -0
- package/dist/components/ui/accordion/accordion-content.svelte.d.ts +4 -0
- package/dist/components/ui/accordion/accordion-item.svelte +12 -0
- package/dist/components/ui/accordion/accordion-item.svelte.d.ts +4 -0
- package/dist/components/ui/accordion/accordion-trigger.svelte +29 -0
- package/dist/components/ui/accordion/accordion-trigger.svelte.d.ts +7 -0
- package/dist/components/ui/accordion/index.d.ts +6 -0
- package/dist/components/ui/accordion/index.js +8 -0
- package/dist/components/ui/badge/badge.svelte +50 -0
- package/dist/components/ui/badge/badge.svelte.d.ts +60 -0
- package/dist/components/ui/badge/index.d.ts +2 -0
- package/dist/components/ui/badge/index.js +2 -0
- package/dist/components/ui/button/button.svelte +82 -0
- package/dist/components/ui/button/button.svelte.d.ts +132 -0
- package/dist/components/ui/button/index.d.ts +2 -0
- package/dist/components/ui/button/index.js +4 -0
- package/dist/components/ui/card/card-content.svelte +16 -0
- package/dist/components/ui/card/card-content.svelte.d.ts +5 -0
- package/dist/components/ui/card/card-description.svelte +16 -0
- package/dist/components/ui/card/card-description.svelte.d.ts +5 -0
- package/dist/components/ui/card/card-footer.svelte +16 -0
- package/dist/components/ui/card/card-footer.svelte.d.ts +5 -0
- package/dist/components/ui/card/card-header.svelte +16 -0
- package/dist/components/ui/card/card-header.svelte.d.ts +5 -0
- package/dist/components/ui/card/card-title.svelte +25 -0
- package/dist/components/ui/card/card-title.svelte.d.ts +8 -0
- package/dist/components/ui/card/card.svelte +20 -0
- package/dist/components/ui/card/card.svelte.d.ts +5 -0
- package/dist/components/ui/card/index.d.ts +7 -0
- package/dist/components/ui/card/index.js +9 -0
- package/dist/components/ui/dialog/dialog-content.svelte +38 -0
- package/dist/components/ui/dialog/dialog-content.svelte.d.ts +9 -0
- package/dist/components/ui/dialog/dialog-description.svelte +16 -0
- package/dist/components/ui/dialog/dialog-description.svelte.d.ts +4 -0
- package/dist/components/ui/dialog/dialog-footer.svelte +20 -0
- package/dist/components/ui/dialog/dialog-footer.svelte.d.ts +5 -0
- package/dist/components/ui/dialog/dialog-header.svelte +20 -0
- package/dist/components/ui/dialog/dialog-header.svelte.d.ts +5 -0
- package/dist/components/ui/dialog/dialog-overlay.svelte +19 -0
- package/dist/components/ui/dialog/dialog-overlay.svelte.d.ts +4 -0
- package/dist/components/ui/dialog/dialog-title.svelte +16 -0
- package/dist/components/ui/dialog/dialog-title.svelte.d.ts +4 -0
- package/dist/components/ui/dialog/index.d.ts +12 -0
- package/dist/components/ui/dialog/index.js +14 -0
- package/dist/components/ui/index.d.ts +26 -0
- package/dist/components/ui/index.js +29 -0
- package/dist/components/ui/input/index.d.ts +2 -0
- package/dist/components/ui/input/index.js +4 -0
- package/dist/components/ui/input/input.svelte +46 -0
- package/dist/components/ui/input/input.svelte.d.ts +13 -0
- package/dist/components/ui/select/index.d.ts +11 -0
- package/dist/components/ui/select/index.js +13 -0
- package/dist/components/ui/select/select-content.svelte +39 -0
- package/dist/components/ui/select/select-content.svelte.d.ts +7 -0
- package/dist/components/ui/select/select-group-heading.svelte +16 -0
- package/dist/components/ui/select/select-group-heading.svelte.d.ts +4 -0
- package/dist/components/ui/select/select-item.svelte +37 -0
- package/dist/components/ui/select/select-item.svelte.d.ts +4 -0
- package/dist/components/ui/select/select-scroll-down-button.svelte +19 -0
- package/dist/components/ui/select/select-scroll-down-button.svelte.d.ts +4 -0
- package/dist/components/ui/select/select-scroll-up-button.svelte +19 -0
- package/dist/components/ui/select/select-scroll-up-button.svelte.d.ts +4 -0
- package/dist/components/ui/select/select-separator.svelte +13 -0
- package/dist/components/ui/select/select-separator.svelte.d.ts +4 -0
- package/dist/components/ui/select/select-trigger.svelte +24 -0
- package/dist/components/ui/select/select-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/separator/index.d.ts +2 -0
- package/dist/components/ui/separator/index.js +4 -0
- package/dist/components/ui/separator/separator.svelte +22 -0
- package/dist/components/ui/separator/separator.svelte.d.ts +4 -0
- package/dist/components/ui/sheet/index.d.ts +12 -0
- package/dist/components/ui/sheet/index.js +14 -0
- package/dist/components/ui/sheet/sheet-content.svelte +53 -0
- package/dist/components/ui/sheet/sheet-content.svelte.d.ts +62 -0
- package/dist/components/ui/sheet/sheet-description.svelte +16 -0
- package/dist/components/ui/sheet/sheet-description.svelte.d.ts +4 -0
- package/dist/components/ui/sheet/sheet-footer.svelte +20 -0
- package/dist/components/ui/sheet/sheet-footer.svelte.d.ts +5 -0
- package/dist/components/ui/sheet/sheet-header.svelte +20 -0
- package/dist/components/ui/sheet/sheet-header.svelte.d.ts +5 -0
- package/dist/components/ui/sheet/sheet-overlay.svelte +21 -0
- package/dist/components/ui/sheet/sheet-overlay.svelte.d.ts +6 -0
- package/dist/components/ui/sheet/sheet-title.svelte +16 -0
- package/dist/components/ui/sheet/sheet-title.svelte.d.ts +4 -0
- package/dist/components/ui/skeleton/index.d.ts +2 -0
- package/dist/components/ui/skeleton/index.js +4 -0
- package/dist/components/ui/skeleton/skeleton.svelte +17 -0
- package/dist/components/ui/skeleton/skeleton.svelte.d.ts +5 -0
- package/dist/components/ui/table/index.d.ts +9 -0
- package/dist/components/ui/table/index.js +11 -0
- package/dist/components/ui/table/table-body.svelte +16 -0
- package/dist/components/ui/table/table-body.svelte.d.ts +5 -0
- package/dist/components/ui/table/table-caption.svelte +16 -0
- package/dist/components/ui/table/table-caption.svelte.d.ts +5 -0
- package/dist/components/ui/table/table-cell.svelte +20 -0
- package/dist/components/ui/table/table-cell.svelte.d.ts +5 -0
- package/dist/components/ui/table/table-footer.svelte +16 -0
- package/dist/components/ui/table/table-footer.svelte.d.ts +5 -0
- package/dist/components/ui/table/table-head.svelte +23 -0
- package/dist/components/ui/table/table-head.svelte.d.ts +5 -0
- package/dist/components/ui/table/table-header.svelte +16 -0
- package/dist/components/ui/table/table-header.svelte.d.ts +5 -0
- package/dist/components/ui/table/table-row.svelte +23 -0
- package/dist/components/ui/table/table-row.svelte.d.ts +5 -0
- package/dist/components/ui/table/table.svelte +18 -0
- package/dist/components/ui/table/table.svelte.d.ts +5 -0
- package/dist/components/ui/tabs/index.d.ts +6 -0
- package/dist/components/ui/tabs/index.js +8 -0
- package/dist/components/ui/tabs/tabs-content.svelte +19 -0
- package/dist/components/ui/tabs/tabs-content.svelte.d.ts +4 -0
- package/dist/components/ui/tabs/tabs-list.svelte +19 -0
- package/dist/components/ui/tabs/tabs-list.svelte.d.ts +4 -0
- package/dist/components/ui/tabs/tabs-trigger.svelte +19 -0
- package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/textarea/index.d.ts +2 -0
- package/dist/components/ui/textarea/index.js +4 -0
- package/dist/components/ui/textarea/textarea.svelte +24 -0
- package/dist/components/ui/textarea/textarea.svelte.d.ts +6 -0
- package/dist/components/ui/toast.d.ts +86 -0
- package/dist/components/ui/toast.js +99 -0
- package/dist/db/schema.sql +238 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +20 -0
- package/dist/payments/index.d.ts +33 -0
- package/dist/payments/index.js +47 -0
- package/dist/payments/shop.d.ts +165 -0
- package/dist/payments/shop.js +588 -0
- package/dist/payments/stripe/client.d.ts +231 -0
- package/dist/payments/stripe/client.js +198 -0
- package/dist/payments/stripe/index.d.ts +18 -0
- package/dist/payments/stripe/index.js +17 -0
- package/dist/payments/stripe/provider.d.ts +50 -0
- package/dist/payments/stripe/provider.js +530 -0
- package/dist/payments/types.d.ts +355 -0
- package/dist/payments/types.js +7 -0
- package/dist/server/logger.d.ts +53 -0
- package/dist/server/logger.js +252 -0
- package/dist/styles/content.css +514 -0
- package/dist/styles/tokens.css +175 -0
- package/dist/utils/api.d.ts +20 -0
- package/dist/utils/api.js +109 -0
- package/dist/utils/cn.d.ts +15 -0
- package/dist/utils/cn.js +18 -0
- package/dist/utils/csrf.d.ts +22 -0
- package/dist/utils/csrf.js +72 -0
- package/dist/utils/debounce.d.ts +7 -0
- package/dist/utils/debounce.js +14 -0
- package/dist/utils/gallery.d.ts +66 -0
- package/dist/utils/gallery.js +181 -0
- package/dist/utils/gutter.d.ts +54 -0
- package/dist/utils/gutter.js +169 -0
- package/dist/utils/imageProcessor.d.ts +58 -0
- package/dist/utils/imageProcessor.js +205 -0
- package/dist/utils/json.d.ts +17 -0
- package/dist/utils/json.js +26 -0
- package/dist/utils/markdown.d.ts +101 -0
- package/dist/utils/markdown.js +947 -0
- package/dist/utils/sanitize.d.ts +25 -0
- package/dist/utils/sanitize.js +127 -0
- package/dist/utils/validation.d.ts +46 -0
- package/dist/utils/validation.js +169 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +5 -0
- 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
|
+
};
|