vyasa 0.3.6__py3-none-any.whl
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.
- vyasa/__init__.py +5 -0
- vyasa/agent.py +116 -0
- vyasa/build.py +660 -0
- vyasa/config.py +224 -0
- vyasa/core.py +2825 -0
- vyasa/helpers.py +349 -0
- vyasa/layout_helpers.py +40 -0
- vyasa/main.py +108 -0
- vyasa/static/scripts.js +1202 -0
- vyasa/static/sidenote.css +21 -0
- vyasa-0.3.6.dist-info/METADATA +227 -0
- vyasa-0.3.6.dist-info/RECORD +16 -0
- vyasa-0.3.6.dist-info/WHEEL +5 -0
- vyasa-0.3.6.dist-info/entry_points.txt +2 -0
- vyasa-0.3.6.dist-info/licenses/LICENSE +201 -0
- vyasa-0.3.6.dist-info/top_level.txt +1 -0
vyasa/static/scripts.js
ADDED
|
@@ -0,0 +1,1202 @@
|
|
|
1
|
+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
|
2
|
+
|
|
3
|
+
const mermaidStates = {};
|
|
4
|
+
const mermaidDebugEnabled = () => (
|
|
5
|
+
window.VYASA_DEBUG_MERMAID === true ||
|
|
6
|
+
localStorage.getItem('vyasaDebugMermaid') === '1'
|
|
7
|
+
);
|
|
8
|
+
const mermaidDebugLog = (...args) => {
|
|
9
|
+
if (mermaidDebugEnabled()) {
|
|
10
|
+
console.log('[vyasa][mermaid]', ...args);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const mermaidDebugSnapshot = (label) => {
|
|
14
|
+
if (!mermaidDebugEnabled()) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const wrappers = Array.from(document.querySelectorAll('.mermaid-wrapper'));
|
|
18
|
+
const withSvg = wrappers.filter(w => w.querySelector('svg'));
|
|
19
|
+
const interactive = wrappers.filter(w => w.dataset.mermaidInteractive === 'true');
|
|
20
|
+
const last = wrappers[wrappers.length - 1];
|
|
21
|
+
let lastRect = null;
|
|
22
|
+
if (last) {
|
|
23
|
+
const rect = last.getBoundingClientRect();
|
|
24
|
+
lastRect = {
|
|
25
|
+
id: last.id,
|
|
26
|
+
width: Math.round(rect.width),
|
|
27
|
+
height: Math.round(rect.height),
|
|
28
|
+
hasSvg: !!last.querySelector('svg'),
|
|
29
|
+
interactive: last.dataset.mermaidInteractive === 'true'
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
mermaidDebugLog(label, {
|
|
33
|
+
total: wrappers.length,
|
|
34
|
+
withSvg: withSvg.length,
|
|
35
|
+
interactive: interactive.length,
|
|
36
|
+
last: lastRect
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
const GANTT_WIDTH = 1200;
|
|
40
|
+
|
|
41
|
+
function handleCodeCopyClick(event) {
|
|
42
|
+
const button = event.target.closest('.code-copy-button, .hljs-copy-button');
|
|
43
|
+
if (!button) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
event.stopPropagation();
|
|
48
|
+
const container = button.closest('.code-block') || button.closest('pre') || button.parentElement;
|
|
49
|
+
const textarea = container ? container.querySelector('textarea[id$="-clipboard"]') : null;
|
|
50
|
+
let text = '';
|
|
51
|
+
if (textarea && textarea.value) {
|
|
52
|
+
text = textarea.value;
|
|
53
|
+
} else {
|
|
54
|
+
const codeEl = (container && container.querySelector('pre > code')) ||
|
|
55
|
+
(container && container.querySelector('code')) ||
|
|
56
|
+
button.closest('pre');
|
|
57
|
+
if (!codeEl) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
text = codeEl.innerText || codeEl.textContent || '';
|
|
61
|
+
}
|
|
62
|
+
const showToast = () => {
|
|
63
|
+
let toast = document.getElementById('code-copy-toast');
|
|
64
|
+
if (!toast) {
|
|
65
|
+
toast = document.createElement('div');
|
|
66
|
+
toast.id = 'code-copy-toast';
|
|
67
|
+
toast.className = 'fixed top-6 right-6 z-[10000] text-xs bg-slate-900 text-white px-3 py-2 rounded shadow-lg opacity-0 transition-opacity duration-300';
|
|
68
|
+
toast.textContent = 'Copied';
|
|
69
|
+
document.body.appendChild(toast);
|
|
70
|
+
}
|
|
71
|
+
toast.classList.remove('opacity-0');
|
|
72
|
+
toast.classList.add('opacity-100');
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
toast.classList.remove('opacity-100');
|
|
75
|
+
toast.classList.add('opacity-0');
|
|
76
|
+
}, 1400);
|
|
77
|
+
};
|
|
78
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
79
|
+
navigator.clipboard.writeText(text).then(showToast).catch(() => {
|
|
80
|
+
const textarea = document.createElement('textarea');
|
|
81
|
+
textarea.value = text;
|
|
82
|
+
textarea.setAttribute('readonly', '');
|
|
83
|
+
textarea.style.position = 'absolute';
|
|
84
|
+
textarea.style.left = '-9999px';
|
|
85
|
+
document.body.appendChild(textarea);
|
|
86
|
+
textarea.select();
|
|
87
|
+
document.execCommand('copy');
|
|
88
|
+
document.body.removeChild(textarea);
|
|
89
|
+
showToast();
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
const textarea = document.createElement('textarea');
|
|
93
|
+
textarea.value = text;
|
|
94
|
+
textarea.setAttribute('readonly', '');
|
|
95
|
+
textarea.style.position = 'absolute';
|
|
96
|
+
textarea.style.left = '-9999px';
|
|
97
|
+
document.body.appendChild(textarea);
|
|
98
|
+
textarea.select();
|
|
99
|
+
document.execCommand('copy');
|
|
100
|
+
document.body.removeChild(textarea);
|
|
101
|
+
showToast();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
document.addEventListener('click', handleCodeCopyClick, true);
|
|
106
|
+
|
|
107
|
+
function initMermaidInteraction() {
|
|
108
|
+
const wrappers = Array.from(document.querySelectorAll('.mermaid-wrapper'));
|
|
109
|
+
if (mermaidDebugEnabled()) {
|
|
110
|
+
const pending = wrappers.filter(w => !w.querySelector('svg'));
|
|
111
|
+
const last = wrappers[wrappers.length - 1];
|
|
112
|
+
mermaidDebugLog('initMermaidInteraction: total', wrappers.length, 'pending', pending.length);
|
|
113
|
+
if (last) {
|
|
114
|
+
mermaidDebugLog('initMermaidInteraction: last wrapper', last.id, 'hasSvg', !!last.querySelector('svg'));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
wrappers.forEach((wrapper, idx) => {
|
|
118
|
+
const svg = wrapper.querySelector('svg');
|
|
119
|
+
const alreadyInteractive = wrapper.dataset.mermaidInteractive === 'true';
|
|
120
|
+
if (mermaidDebugEnabled()) {
|
|
121
|
+
mermaidDebugLog(
|
|
122
|
+
'initMermaidInteraction: wrapper',
|
|
123
|
+
idx,
|
|
124
|
+
wrapper.id,
|
|
125
|
+
'hasSvg',
|
|
126
|
+
!!svg,
|
|
127
|
+
'interactive',
|
|
128
|
+
alreadyInteractive
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const getSvg = () => wrapper.querySelector('svg');
|
|
132
|
+
const applySvgState = (currentSvg) => {
|
|
133
|
+
if (!currentSvg) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
currentSvg.style.pointerEvents = 'none';
|
|
137
|
+
currentSvg.style.transform = `translate(${state.translateX}px, ${state.translateY}px) scale(${state.scale})`;
|
|
138
|
+
currentSvg.style.transformOrigin = 'center center';
|
|
139
|
+
};
|
|
140
|
+
if (svg) {
|
|
141
|
+
svg.style.pointerEvents = 'none';
|
|
142
|
+
}
|
|
143
|
+
if (!svg || alreadyInteractive) return;
|
|
144
|
+
|
|
145
|
+
// DEBUG: Log initial state
|
|
146
|
+
console.group(`🔍 initMermaidInteraction: ${wrapper.id}`);
|
|
147
|
+
console.log('Theme:', getCurrentTheme());
|
|
148
|
+
console.log('Wrapper computed style height:', window.getComputedStyle(wrapper).height);
|
|
149
|
+
console.log('Wrapper inline style:', wrapper.getAttribute('style'));
|
|
150
|
+
|
|
151
|
+
// Scale SVG to fit container (maintain aspect ratio, fit to width or height whichever is smaller)
|
|
152
|
+
const wrapperRect = wrapper.getBoundingClientRect();
|
|
153
|
+
const svgRect = svg.getBoundingClientRect();
|
|
154
|
+
console.log('Wrapper rect:', { width: wrapperRect.width, height: wrapperRect.height });
|
|
155
|
+
console.log('SVG rect:', { width: svgRect.width, height: svgRect.height });
|
|
156
|
+
|
|
157
|
+
const scaleX = (wrapperRect.width - 32) / svgRect.width; // 32 for p-4 padding (16px each side)
|
|
158
|
+
const scaleY = (wrapperRect.height - 32) / svgRect.height;
|
|
159
|
+
console.log('Scale factors:', { scaleX, scaleY });
|
|
160
|
+
|
|
161
|
+
// For very wide diagrams (like Gantt charts), prefer width scaling even if it exceeds height
|
|
162
|
+
const aspectRatio = svgRect.width / svgRect.height;
|
|
163
|
+
const maxUpscale = 1;
|
|
164
|
+
let initialScale;
|
|
165
|
+
if (aspectRatio > 3) {
|
|
166
|
+
// Wide diagram: scale to fit width, but do not upscale by default
|
|
167
|
+
initialScale = Math.min(scaleX, maxUpscale);
|
|
168
|
+
console.log('Wide diagram detected (aspect ratio > 3):', aspectRatio, 'Using scaleX:', initialScale);
|
|
169
|
+
} else {
|
|
170
|
+
// Normal diagram: fit to smaller dimension, but do not upscale by default
|
|
171
|
+
initialScale = Math.min(scaleX, scaleY, maxUpscale);
|
|
172
|
+
console.log('Normal diagram (aspect ratio <=3):', aspectRatio, 'Using min scale:', initialScale);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (mermaidDebugEnabled()) {
|
|
176
|
+
mermaidDebugLog('initMermaidInteraction: sizing', {
|
|
177
|
+
id: wrapper.id,
|
|
178
|
+
wrapperWidth: wrapperRect.width,
|
|
179
|
+
wrapperHeight: wrapperRect.height,
|
|
180
|
+
svgWidth: svgRect.width,
|
|
181
|
+
svgHeight: svgRect.height,
|
|
182
|
+
initialScale
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const state = {
|
|
187
|
+
scale: initialScale,
|
|
188
|
+
translateX: 0,
|
|
189
|
+
translateY: 0,
|
|
190
|
+
isPanning: false,
|
|
191
|
+
startX: 0,
|
|
192
|
+
startY: 0
|
|
193
|
+
};
|
|
194
|
+
mermaidStates[wrapper.id] = state;
|
|
195
|
+
wrapper.dataset.mermaidInteractive = 'true';
|
|
196
|
+
console.log('Final state:', state);
|
|
197
|
+
console.groupEnd();
|
|
198
|
+
|
|
199
|
+
if (mermaidDebugEnabled() && !wrapper.dataset.mermaidDebugBound) {
|
|
200
|
+
wrapper.dataset.mermaidDebugBound = 'true';
|
|
201
|
+
const logEvent = (name, event) => {
|
|
202
|
+
const target = event.target && event.target.tagName ? event.target.tagName : 'unknown';
|
|
203
|
+
mermaidDebugLog(`${name} on ${wrapper.id}`, { type: event.type, target });
|
|
204
|
+
};
|
|
205
|
+
wrapper.addEventListener('pointerdown', (e) => logEvent('pointerdown', e));
|
|
206
|
+
wrapper.addEventListener('pointermove', (e) => logEvent('pointermove', e));
|
|
207
|
+
wrapper.addEventListener('pointerup', (e) => logEvent('pointerup', e));
|
|
208
|
+
wrapper.addEventListener('wheel', (e) => logEvent('wheel', e));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function updateTransform() {
|
|
212
|
+
applySvgState(getSvg());
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Apply initial scale
|
|
216
|
+
updateTransform();
|
|
217
|
+
|
|
218
|
+
if (!wrapper.dataset.mermaidObserver) {
|
|
219
|
+
const observer = new MutationObserver(() => {
|
|
220
|
+
applySvgState(getSvg());
|
|
221
|
+
});
|
|
222
|
+
observer.observe(wrapper, { childList: true, subtree: true });
|
|
223
|
+
wrapper.dataset.mermaidObserver = 'true';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Mouse wheel zoom (zooms towards cursor position)
|
|
227
|
+
wrapper.addEventListener('wheel', (e) => {
|
|
228
|
+
e.preventDefault();
|
|
229
|
+
|
|
230
|
+
const currentSvg = getSvg();
|
|
231
|
+
if (!currentSvg) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const rect = currentSvg.getBoundingClientRect();
|
|
235
|
+
|
|
236
|
+
// Mouse position relative to SVG's current position
|
|
237
|
+
const mouseX = e.clientX - rect.left - rect.width / 2;
|
|
238
|
+
const mouseY = e.clientY - rect.top - rect.height / 2;
|
|
239
|
+
|
|
240
|
+
const zoomIntensity = 0.01;
|
|
241
|
+
const delta = e.deltaY > 0 ? 1 - zoomIntensity : 1 + zoomIntensity; // Zoom out or in speed
|
|
242
|
+
const newScale = Math.min(Math.max(0.1, state.scale * delta), 55);
|
|
243
|
+
|
|
244
|
+
// Calculate how much to adjust translation to keep point under cursor fixed
|
|
245
|
+
// With center origin, we need to account for the scale change around center
|
|
246
|
+
const scaleFactor = newScale / state.scale - 1;
|
|
247
|
+
state.translateX -= mouseX * scaleFactor;
|
|
248
|
+
state.translateY -= mouseY * scaleFactor;
|
|
249
|
+
state.scale = newScale;
|
|
250
|
+
|
|
251
|
+
updateTransform();
|
|
252
|
+
}, { passive: false });
|
|
253
|
+
|
|
254
|
+
// Pan with pointer drag (mouse + touch)
|
|
255
|
+
wrapper.style.cursor = 'grab';
|
|
256
|
+
wrapper.style.touchAction = 'none';
|
|
257
|
+
wrapper.addEventListener('pointerdown', (e) => {
|
|
258
|
+
if (e.pointerType === 'mouse' && e.button !== 0) return;
|
|
259
|
+
state.isPanning = true;
|
|
260
|
+
state.startX = e.clientX - state.translateX;
|
|
261
|
+
state.startY = e.clientY - state.translateY;
|
|
262
|
+
wrapper.setPointerCapture(e.pointerId);
|
|
263
|
+
wrapper.style.cursor = 'grabbing';
|
|
264
|
+
e.preventDefault();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
wrapper.addEventListener('pointermove', (e) => {
|
|
268
|
+
if (!state.isPanning) return;
|
|
269
|
+
state.translateX = e.clientX - state.startX;
|
|
270
|
+
state.translateY = e.clientY - state.startY;
|
|
271
|
+
updateTransform();
|
|
272
|
+
if (mermaidDebugEnabled()) {
|
|
273
|
+
mermaidDebugLog('pan update', wrapper.id, {
|
|
274
|
+
translateX: state.translateX,
|
|
275
|
+
translateY: state.translateY,
|
|
276
|
+
scale: state.scale,
|
|
277
|
+
svgTransform: (getSvg() && getSvg().style.transform) || ''
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const stopPanning = (e) => {
|
|
283
|
+
if (!state.isPanning) return;
|
|
284
|
+
state.isPanning = false;
|
|
285
|
+
try {
|
|
286
|
+
wrapper.releasePointerCapture(e.pointerId);
|
|
287
|
+
} catch {
|
|
288
|
+
// Ignore if pointer capture is not active
|
|
289
|
+
}
|
|
290
|
+
wrapper.style.cursor = 'grab';
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
wrapper.addEventListener('pointerup', stopPanning);
|
|
294
|
+
wrapper.addEventListener('pointercancel', stopPanning);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function scheduleMermaidInteraction({ maxAttempts = 12, delayMs = 80, onReady } = {}) {
|
|
299
|
+
let attempt = 0;
|
|
300
|
+
const check = () => {
|
|
301
|
+
const wrappers = Array.from(document.querySelectorAll('.mermaid-wrapper'));
|
|
302
|
+
const pending = wrappers.filter(wrapper => !wrapper.querySelector('svg'));
|
|
303
|
+
if (mermaidDebugEnabled()) {
|
|
304
|
+
const last = wrappers[wrappers.length - 1];
|
|
305
|
+
mermaidDebugLog('scheduleMermaidInteraction attempt', attempt, 'pending', pending.length);
|
|
306
|
+
if (last) {
|
|
307
|
+
mermaidDebugLog('scheduleMermaidInteraction last wrapper', last.id, 'hasSvg', !!last.querySelector('svg'));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (pending.length === 0 || attempt >= maxAttempts) {
|
|
311
|
+
initMermaidInteraction();
|
|
312
|
+
if (typeof onReady === 'function') {
|
|
313
|
+
onReady();
|
|
314
|
+
}
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
attempt += 1;
|
|
318
|
+
setTimeout(check, delayMs);
|
|
319
|
+
};
|
|
320
|
+
check();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
window.resetMermaidZoom = function(id) {
|
|
324
|
+
const state = mermaidStates[id];
|
|
325
|
+
if (state) {
|
|
326
|
+
state.scale = 1;
|
|
327
|
+
state.translateX = 0;
|
|
328
|
+
state.translateY = 0;
|
|
329
|
+
const svg = document.getElementById(id).querySelector('svg');
|
|
330
|
+
svg.style.transform = 'translate(0px, 0px) scale(1)';
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
window.zoomMermaidIn = function(id) {
|
|
335
|
+
const state = mermaidStates[id];
|
|
336
|
+
if (state) {
|
|
337
|
+
state.scale = Math.min(state.scale * 1.1, 10);
|
|
338
|
+
const svg = document.getElementById(id).querySelector('svg');
|
|
339
|
+
svg.style.transform = `translate(${state.translateX}px, ${state.translateY}px) scale(${state.scale})`;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
window.zoomMermaidOut = function(id) {
|
|
344
|
+
const state = mermaidStates[id];
|
|
345
|
+
if (state) {
|
|
346
|
+
state.scale = Math.max(state.scale * 0.9, 0.1);
|
|
347
|
+
const svg = document.getElementById(id).querySelector('svg');
|
|
348
|
+
svg.style.transform = `translate(${state.translateX}px, ${state.translateY}px) scale(${state.scale})`;
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
window.openMermaidFullscreen = function(id) {
|
|
353
|
+
const wrapper = document.getElementById(id);
|
|
354
|
+
if (!wrapper) return;
|
|
355
|
+
|
|
356
|
+
const originalCode = wrapper.getAttribute('data-mermaid-code');
|
|
357
|
+
if (!originalCode) return;
|
|
358
|
+
|
|
359
|
+
// Decode HTML entities
|
|
360
|
+
const textarea = document.createElement('textarea');
|
|
361
|
+
textarea.innerHTML = originalCode;
|
|
362
|
+
const code = textarea.value;
|
|
363
|
+
|
|
364
|
+
// Create modal
|
|
365
|
+
const modal = document.createElement('div');
|
|
366
|
+
modal.id = 'mermaid-fullscreen-modal';
|
|
367
|
+
modal.className = 'fixed inset-0 z-[10000] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4';
|
|
368
|
+
modal.style.animation = 'fadeIn 0.2s ease-in';
|
|
369
|
+
|
|
370
|
+
// Create modal content container
|
|
371
|
+
const modalContent = document.createElement('div');
|
|
372
|
+
modalContent.className = 'relative bg-white dark:bg-slate-900 rounded-lg shadow-2xl w-full h-full max-w-[95vw] max-h-[95vh] flex flex-col';
|
|
373
|
+
|
|
374
|
+
// Create header with close button
|
|
375
|
+
const header = document.createElement('div');
|
|
376
|
+
header.className = 'flex items-center justify-between p-4 border-b border-slate-200 dark:border-slate-700';
|
|
377
|
+
|
|
378
|
+
const title = document.createElement('h3');
|
|
379
|
+
title.className = 'text-lg font-semibold text-slate-800 dark:text-slate-200';
|
|
380
|
+
title.textContent = 'Diagram';
|
|
381
|
+
|
|
382
|
+
const closeBtn = document.createElement('button');
|
|
383
|
+
closeBtn.innerHTML = '✕';
|
|
384
|
+
closeBtn.className = 'px-3 py-1 text-xl text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 rounded transition-colors';
|
|
385
|
+
closeBtn.title = 'Close (Esc)';
|
|
386
|
+
closeBtn.onclick = () => document.body.removeChild(modal);
|
|
387
|
+
|
|
388
|
+
header.appendChild(title);
|
|
389
|
+
header.appendChild(closeBtn);
|
|
390
|
+
|
|
391
|
+
// Create diagram container
|
|
392
|
+
const diagramContainer = document.createElement('div');
|
|
393
|
+
diagramContainer.className = 'flex-1 overflow-auto p-4 flex items-center justify-center';
|
|
394
|
+
|
|
395
|
+
const fullscreenId = `${id}-fullscreen`;
|
|
396
|
+
const fullscreenWrapper = document.createElement('div');
|
|
397
|
+
fullscreenWrapper.id = fullscreenId;
|
|
398
|
+
fullscreenWrapper.className = 'mermaid-wrapper w-full h-full flex items-center justify-center';
|
|
399
|
+
fullscreenWrapper.setAttribute('data-mermaid-code', originalCode);
|
|
400
|
+
|
|
401
|
+
const pre = document.createElement('pre');
|
|
402
|
+
pre.className = 'mermaid';
|
|
403
|
+
pre.textContent = code;
|
|
404
|
+
fullscreenWrapper.appendChild(pre);
|
|
405
|
+
|
|
406
|
+
diagramContainer.appendChild(fullscreenWrapper);
|
|
407
|
+
|
|
408
|
+
// Assemble modal
|
|
409
|
+
modalContent.appendChild(header);
|
|
410
|
+
modalContent.appendChild(diagramContainer);
|
|
411
|
+
modal.appendChild(modalContent);
|
|
412
|
+
document.body.appendChild(modal);
|
|
413
|
+
|
|
414
|
+
// Close on Esc key
|
|
415
|
+
const escHandler = (e) => {
|
|
416
|
+
if (e.key === 'Escape') {
|
|
417
|
+
document.body.removeChild(modal);
|
|
418
|
+
document.removeEventListener('keydown', escHandler);
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
document.addEventListener('keydown', escHandler);
|
|
422
|
+
|
|
423
|
+
// Close on background click
|
|
424
|
+
modal.addEventListener('click', (e) => {
|
|
425
|
+
if (e.target === modal) {
|
|
426
|
+
document.body.removeChild(modal);
|
|
427
|
+
document.removeEventListener('keydown', escHandler);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Render mermaid in the fullscreen view
|
|
432
|
+
mermaid.run({ nodes: [pre] }).then(() => {
|
|
433
|
+
setTimeout(() => initMermaidInteraction(), 100);
|
|
434
|
+
});
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
function getCurrentTheme() {
|
|
438
|
+
return document.documentElement.classList.contains('dark') ? 'dark' : 'default';
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function getDynamicGanttWidth() {
|
|
442
|
+
// Check if any mermaid wrapper has custom gantt width
|
|
443
|
+
const wrappers = document.querySelectorAll('.mermaid-wrapper[data-gantt-width]');
|
|
444
|
+
if (wrappers.length > 0) {
|
|
445
|
+
// Use the first custom width found, or max width if multiple
|
|
446
|
+
const widths = Array.from(wrappers).map(w => parseInt(w.getAttribute('data-gantt-width')) || GANTT_WIDTH);
|
|
447
|
+
return Math.max(...widths);
|
|
448
|
+
}
|
|
449
|
+
return GANTT_WIDTH;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function reinitializeMermaid() {
|
|
453
|
+
console.group('🔄 reinitializeMermaid called');
|
|
454
|
+
console.log('Switching to theme:', getCurrentTheme());
|
|
455
|
+
console.log('Is initial load?', isInitialLoad);
|
|
456
|
+
|
|
457
|
+
// Skip if this is the initial load (let it render naturally first)
|
|
458
|
+
if (isInitialLoad) {
|
|
459
|
+
console.log('Skipping reinitialize on initial load');
|
|
460
|
+
console.groupEnd();
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const dynamicWidth = getDynamicGanttWidth();
|
|
465
|
+
console.log('Using dynamic Gantt width:', dynamicWidth);
|
|
466
|
+
|
|
467
|
+
mermaid.initialize({
|
|
468
|
+
startOnLoad: false,
|
|
469
|
+
theme: getCurrentTheme(),
|
|
470
|
+
gantt: {
|
|
471
|
+
useWidth: dynamicWidth,
|
|
472
|
+
useMaxWidth: false
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// Find all mermaid wrappers and re-render them
|
|
477
|
+
const shouldLockHeight = (wrapper) => {
|
|
478
|
+
const height = (wrapper.style.height || '').trim();
|
|
479
|
+
return height && height !== 'auto' && height !== 'initial' && height !== 'unset';
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
document.querySelectorAll('.mermaid-wrapper').forEach(wrapper => {
|
|
483
|
+
const originalCode = wrapper.getAttribute('data-mermaid-code');
|
|
484
|
+
if (originalCode) {
|
|
485
|
+
console.log(`Processing wrapper: ${wrapper.id}`);
|
|
486
|
+
console.log('BEFORE clear - wrapper height:', window.getComputedStyle(wrapper).height);
|
|
487
|
+
console.log('BEFORE clear - wrapper rect:', wrapper.getBoundingClientRect());
|
|
488
|
+
|
|
489
|
+
// Preserve the current computed height before clearing (height should already be set explicitly)
|
|
490
|
+
if (shouldLockHeight(wrapper)) {
|
|
491
|
+
const currentHeight = wrapper.getBoundingClientRect().height;
|
|
492
|
+
console.log('Preserving height:', currentHeight);
|
|
493
|
+
wrapper.style.height = currentHeight + 'px';
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Delete the old state so it can be recreated
|
|
497
|
+
delete mermaidStates[wrapper.id];
|
|
498
|
+
delete wrapper.dataset.mermaidInteractive;
|
|
499
|
+
|
|
500
|
+
// Decode HTML entities
|
|
501
|
+
const textarea = document.createElement('textarea');
|
|
502
|
+
textarea.innerHTML = originalCode;
|
|
503
|
+
const code = textarea.value;
|
|
504
|
+
|
|
505
|
+
// Clear the wrapper
|
|
506
|
+
wrapper.innerHTML = '';
|
|
507
|
+
console.log('AFTER clear - wrapper height:', window.getComputedStyle(wrapper).height);
|
|
508
|
+
console.log('AFTER clear - wrapper rect:', wrapper.getBoundingClientRect());
|
|
509
|
+
|
|
510
|
+
// Re-add the pre element with mermaid code
|
|
511
|
+
const newPre = document.createElement('pre');
|
|
512
|
+
newPre.className = 'mermaid';
|
|
513
|
+
newPre.textContent = code;
|
|
514
|
+
wrapper.appendChild(newPre);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// Re-run mermaid
|
|
519
|
+
mermaid.run().then(() => {
|
|
520
|
+
console.log('Mermaid re-render complete, scheduling initMermaidInteraction');
|
|
521
|
+
scheduleMermaidInteraction({
|
|
522
|
+
onReady: () => {
|
|
523
|
+
console.groupEnd();
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
console.log('🚀 Initial Mermaid setup - Theme:', getCurrentTheme());
|
|
530
|
+
|
|
531
|
+
const initialGanttWidth = getDynamicGanttWidth();
|
|
532
|
+
console.log('Using initial Gantt width:', initialGanttWidth);
|
|
533
|
+
|
|
534
|
+
mermaid.initialize({
|
|
535
|
+
startOnLoad: false,
|
|
536
|
+
theme: getCurrentTheme(),
|
|
537
|
+
gantt: {
|
|
538
|
+
useWidth: initialGanttWidth,
|
|
539
|
+
useMaxWidth: false
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// Track if this is the initial load
|
|
544
|
+
let isInitialLoad = true;
|
|
545
|
+
|
|
546
|
+
// Initialize interaction after mermaid renders
|
|
547
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
548
|
+
mermaidDebugSnapshot('before mermaid.run (DOMContentLoaded)');
|
|
549
|
+
mermaid.run().then(() => {
|
|
550
|
+
mermaidDebugSnapshot('after mermaid.run (DOMContentLoaded)');
|
|
551
|
+
console.log('Initial mermaid render complete');
|
|
552
|
+
scheduleMermaidInteraction({
|
|
553
|
+
onReady: () => {
|
|
554
|
+
console.log('Calling initial initMermaidInteraction');
|
|
555
|
+
|
|
556
|
+
// After initial render, set explicit heights on all wrappers so theme switching works
|
|
557
|
+
const shouldLockHeight = (wrapper) => {
|
|
558
|
+
const height = (wrapper.style.height || '').trim();
|
|
559
|
+
return height && height !== 'auto' && height !== 'initial' && height !== 'unset';
|
|
560
|
+
};
|
|
561
|
+
document.querySelectorAll('.mermaid-wrapper').forEach(wrapper => {
|
|
562
|
+
if (!shouldLockHeight(wrapper)) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
const currentHeight = wrapper.getBoundingClientRect().height;
|
|
566
|
+
console.log(`Setting initial height for ${wrapper.id}:`, currentHeight);
|
|
567
|
+
wrapper.style.height = currentHeight + 'px';
|
|
568
|
+
});
|
|
569
|
+
isInitialLoad = false;
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// Reveal current file in sidebar
|
|
576
|
+
function revealInSidebar(rootElement = document) {
|
|
577
|
+
if (!window.location.pathname.startsWith('/posts/')) {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Decode the URL path to handle special characters and spaces
|
|
582
|
+
const currentPath = decodeURIComponent(window.location.pathname.replace(/^\/posts\//, ''));
|
|
583
|
+
const activeLink = rootElement.querySelector(`.post-link[data-path="${currentPath}"]`);
|
|
584
|
+
|
|
585
|
+
if (activeLink) {
|
|
586
|
+
// Expand all parent details elements within this sidebar
|
|
587
|
+
let parent = activeLink.closest('details');
|
|
588
|
+
while (parent && rootElement.contains(parent)) {
|
|
589
|
+
parent.open = true;
|
|
590
|
+
if (parent === rootElement) {
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
parent = parent.parentElement.closest('details');
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Scroll to the active link
|
|
597
|
+
const scrollContainer = rootElement.querySelector('#sidebar-scroll-container');
|
|
598
|
+
if (scrollContainer) {
|
|
599
|
+
const linkRect = activeLink.getBoundingClientRect();
|
|
600
|
+
const containerRect = scrollContainer.getBoundingClientRect();
|
|
601
|
+
const scrollTop = scrollContainer.scrollTop;
|
|
602
|
+
const offset = linkRect.top - containerRect.top + scrollTop - (containerRect.height / 2) + (linkRect.height / 2);
|
|
603
|
+
|
|
604
|
+
scrollContainer.scrollTo({
|
|
605
|
+
top: offset,
|
|
606
|
+
behavior: 'smooth'
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Highlight the active link temporarily
|
|
611
|
+
activeLink.classList.remove('fade-out');
|
|
612
|
+
activeLink.classList.add('sidebar-highlight');
|
|
613
|
+
requestAnimationFrame(() => {
|
|
614
|
+
setTimeout(() => {
|
|
615
|
+
activeLink.classList.add('fade-out');
|
|
616
|
+
setTimeout(() => {
|
|
617
|
+
activeLink.classList.remove('sidebar-highlight', 'fade-out');
|
|
618
|
+
}, 10000);
|
|
619
|
+
}, 1000);
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function initPostsSidebarAutoReveal() {
|
|
625
|
+
const postSidebars = document.querySelectorAll('details[data-sidebar="posts"]');
|
|
626
|
+
|
|
627
|
+
postSidebars.forEach((sidebar) => {
|
|
628
|
+
if (sidebar.dataset.revealBound === 'true') {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
sidebar.dataset.revealBound = 'true';
|
|
632
|
+
|
|
633
|
+
// Reveal immediately if sidebar is already open
|
|
634
|
+
if (sidebar.open) {
|
|
635
|
+
revealInSidebar(sidebar);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
sidebar.addEventListener('toggle', () => {
|
|
639
|
+
if (!sidebar.open) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
revealInSidebar(sidebar);
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function initFolderChevronState(rootElement = document) {
|
|
648
|
+
rootElement.querySelectorAll('details[data-folder="true"]').forEach((details) => {
|
|
649
|
+
details.classList.toggle('is-open', details.open);
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function initSearchPlaceholderCycle(rootElement = document) {
|
|
654
|
+
const inputs = rootElement.querySelectorAll('input[data-placeholder-cycle]');
|
|
655
|
+
inputs.forEach((input) => {
|
|
656
|
+
if (input.dataset.placeholderCycleBound === 'true') {
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
input.dataset.placeholderCycleBound = 'true';
|
|
660
|
+
const primary = input.dataset.placeholderPrimary || input.getAttribute('placeholder') || '';
|
|
661
|
+
const alt = input.dataset.placeholderAlt || '';
|
|
662
|
+
if (!alt) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
let showAlt = false;
|
|
666
|
+
setInterval(() => {
|
|
667
|
+
if (input.value) {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
showAlt = !showAlt;
|
|
671
|
+
input.setAttribute('placeholder', showAlt ? alt : primary);
|
|
672
|
+
}, 10000);
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function initCodeBlockCopyButtons(rootElement = document) {
|
|
677
|
+
const buttons = rootElement.querySelectorAll('.code-copy-button');
|
|
678
|
+
buttons.forEach((button) => {
|
|
679
|
+
if (button.dataset.copyBound === 'true') {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
button.dataset.copyBound = 'true';
|
|
683
|
+
button.addEventListener('click', () => {
|
|
684
|
+
const container = button.closest('.code-block');
|
|
685
|
+
const codeEl = container ? container.querySelector('pre > code') : null;
|
|
686
|
+
if (!codeEl) {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const text = codeEl.innerText || codeEl.textContent || '';
|
|
690
|
+
const done = () => {
|
|
691
|
+
button.classList.add('is-copied');
|
|
692
|
+
setTimeout(() => button.classList.remove('is-copied'), 1200);
|
|
693
|
+
};
|
|
694
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
695
|
+
navigator.clipboard.writeText(text).then(done).catch(() => {
|
|
696
|
+
const textarea = document.createElement('textarea');
|
|
697
|
+
textarea.value = text;
|
|
698
|
+
textarea.setAttribute('readonly', '');
|
|
699
|
+
textarea.style.position = 'absolute';
|
|
700
|
+
textarea.style.left = '-9999px';
|
|
701
|
+
document.body.appendChild(textarea);
|
|
702
|
+
textarea.select();
|
|
703
|
+
document.execCommand('copy');
|
|
704
|
+
document.body.removeChild(textarea);
|
|
705
|
+
done();
|
|
706
|
+
});
|
|
707
|
+
} else {
|
|
708
|
+
const textarea = document.createElement('textarea');
|
|
709
|
+
textarea.value = text;
|
|
710
|
+
textarea.setAttribute('readonly', '');
|
|
711
|
+
textarea.style.position = 'absolute';
|
|
712
|
+
textarea.style.left = '-9999px';
|
|
713
|
+
document.body.appendChild(textarea);
|
|
714
|
+
textarea.select();
|
|
715
|
+
document.execCommand('copy');
|
|
716
|
+
document.body.removeChild(textarea);
|
|
717
|
+
done();
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function initPostsSearchPersistence(rootElement = document) {
|
|
724
|
+
const input = rootElement.querySelector('.posts-search-block input[type="search"][name="q"]');
|
|
725
|
+
const results = rootElement.querySelector('.posts-search-results');
|
|
726
|
+
if (!input || !results) {
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
if (input.dataset.searchPersistenceBound === 'true') {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
input.dataset.searchPersistenceBound = 'true';
|
|
733
|
+
const termKey = 'vyasa:postsSearchTerm';
|
|
734
|
+
const resultsKey = 'vyasa:postsSearchResults';
|
|
735
|
+
const enhanceGatherLink = () => {
|
|
736
|
+
const gatherLink = results.querySelector('a[href^="/search/gather"]');
|
|
737
|
+
if (!gatherLink) {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const href = gatherLink.getAttribute('href');
|
|
741
|
+
if (!href) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
gatherLink.setAttribute('hx_get', href);
|
|
745
|
+
gatherLink.setAttribute('hx_target', '#main-content');
|
|
746
|
+
gatherLink.setAttribute('hx_push_url', 'true');
|
|
747
|
+
gatherLink.setAttribute('hx_swap', 'outerHTML show:window:top settle:0.1s');
|
|
748
|
+
};
|
|
749
|
+
let storedTerm = '';
|
|
750
|
+
let storedResults = null;
|
|
751
|
+
try {
|
|
752
|
+
storedTerm = localStorage.getItem(termKey) || '';
|
|
753
|
+
storedResults = localStorage.getItem(resultsKey);
|
|
754
|
+
} catch (err) {
|
|
755
|
+
storedTerm = '';
|
|
756
|
+
storedResults = null;
|
|
757
|
+
}
|
|
758
|
+
if (storedTerm && !input.value) {
|
|
759
|
+
input.value = storedTerm;
|
|
760
|
+
}
|
|
761
|
+
if (storedResults && input.value) {
|
|
762
|
+
try {
|
|
763
|
+
const payload = JSON.parse(storedResults);
|
|
764
|
+
if (payload && payload.term === input.value && payload.html) {
|
|
765
|
+
results.innerHTML = payload.html;
|
|
766
|
+
enhanceGatherLink();
|
|
767
|
+
}
|
|
768
|
+
} catch (err) {
|
|
769
|
+
// Ignore malformed cached payloads.
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
const persistTerm = () => {
|
|
773
|
+
try {
|
|
774
|
+
if (input.value) {
|
|
775
|
+
localStorage.setItem(termKey, input.value);
|
|
776
|
+
} else {
|
|
777
|
+
localStorage.removeItem(termKey);
|
|
778
|
+
localStorage.removeItem(resultsKey);
|
|
779
|
+
}
|
|
780
|
+
} catch (err) {
|
|
781
|
+
// Ignore storage failures.
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
input.addEventListener('input', persistTerm);
|
|
785
|
+
const fetchResults = (query) => {
|
|
786
|
+
return fetch(`/_sidebar/posts/search?q=${query}`)
|
|
787
|
+
.then((response) => response.text())
|
|
788
|
+
.then((html) => {
|
|
789
|
+
results.innerHTML = html;
|
|
790
|
+
enhanceGatherLink();
|
|
791
|
+
try {
|
|
792
|
+
localStorage.setItem(resultsKey, JSON.stringify({
|
|
793
|
+
term: input.value,
|
|
794
|
+
html: results.innerHTML
|
|
795
|
+
}));
|
|
796
|
+
} catch (err) {
|
|
797
|
+
// Ignore storage failures.
|
|
798
|
+
}
|
|
799
|
+
})
|
|
800
|
+
.catch(() => {});
|
|
801
|
+
};
|
|
802
|
+
document.body.addEventListener('htmx:afterSwap', (event) => {
|
|
803
|
+
if (event.target !== results) {
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
enhanceGatherLink();
|
|
807
|
+
try {
|
|
808
|
+
localStorage.setItem(resultsKey, JSON.stringify({
|
|
809
|
+
term: input.value,
|
|
810
|
+
html: results.innerHTML
|
|
811
|
+
}));
|
|
812
|
+
} catch (err) {
|
|
813
|
+
// Ignore storage failures.
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
if (input.value) {
|
|
817
|
+
const query = encodeURIComponent(input.value);
|
|
818
|
+
if (window.htmx && typeof window.htmx.ajax === 'function') {
|
|
819
|
+
window.htmx.ajax('GET', `/_sidebar/posts/search?q=${query}`, { target: results, swap: 'innerHTML' });
|
|
820
|
+
} else {
|
|
821
|
+
fetchResults(query);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function initSearchClearButtons(rootElement = document) {
|
|
827
|
+
const blocks = rootElement.querySelectorAll('.posts-search-block');
|
|
828
|
+
blocks.forEach((block) => {
|
|
829
|
+
const input = block.querySelector('input[type="search"][name="q"]');
|
|
830
|
+
const button = block.querySelector('.posts-search-clear-button');
|
|
831
|
+
const results = block.querySelector('.posts-search-results');
|
|
832
|
+
if (!input || !button) {
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (button.dataset.clearBound === 'true') {
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
button.dataset.clearBound = 'true';
|
|
839
|
+
const updateVisibility = () => {
|
|
840
|
+
button.style.opacity = input.value ? '1' : '0';
|
|
841
|
+
button.style.pointerEvents = input.value ? 'auto' : 'none';
|
|
842
|
+
};
|
|
843
|
+
updateVisibility();
|
|
844
|
+
input.addEventListener('input', updateVisibility);
|
|
845
|
+
button.addEventListener('click', () => {
|
|
846
|
+
input.value = '';
|
|
847
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
848
|
+
if (results) {
|
|
849
|
+
results.innerHTML = '';
|
|
850
|
+
}
|
|
851
|
+
try {
|
|
852
|
+
localStorage.removeItem('vyasa:postsSearchTerm');
|
|
853
|
+
localStorage.removeItem('vyasa:postsSearchResults');
|
|
854
|
+
} catch (err) {
|
|
855
|
+
// Ignore storage failures.
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
document.addEventListener('toggle', (event) => {
|
|
862
|
+
const details = event.target;
|
|
863
|
+
if (!(details instanceof HTMLDetailsElement)) {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
if (!details.matches('details[data-folder="true"]')) {
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
details.classList.toggle('is-open', details.open);
|
|
870
|
+
}, true);
|
|
871
|
+
|
|
872
|
+
// Update active post link in sidebar
|
|
873
|
+
function updateActivePostLink() {
|
|
874
|
+
const currentPath = window.location.pathname.replace(/^\/posts\//, '');
|
|
875
|
+
document.querySelectorAll('.post-link').forEach(link => {
|
|
876
|
+
const linkPath = link.getAttribute('data-path');
|
|
877
|
+
if (linkPath === currentPath) {
|
|
878
|
+
link.classList.add('bg-blue-50', 'dark:bg-blue-900/20', 'text-blue-600', 'dark:text-blue-400', 'font-medium');
|
|
879
|
+
link.classList.remove('text-slate-700', 'dark:text-slate-300', 'hover:text-blue-600');
|
|
880
|
+
} else {
|
|
881
|
+
link.classList.remove('bg-blue-50', 'dark:bg-blue-900/20', 'text-blue-600', 'dark:text-blue-400', 'font-medium');
|
|
882
|
+
link.classList.add('text-slate-700', 'dark:text-slate-300', 'hover:text-blue-600');
|
|
883
|
+
}
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Update active TOC link based on scroll position
|
|
888
|
+
let lastActiveTocAnchor = null;
|
|
889
|
+
function updateActiveTocLink() {
|
|
890
|
+
const headings = document.querySelectorAll('h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]');
|
|
891
|
+
const tocLinks = document.querySelectorAll('.toc-link');
|
|
892
|
+
|
|
893
|
+
let activeHeading = null;
|
|
894
|
+
let nearestBelow = null;
|
|
895
|
+
let nearestBelowTop = Infinity;
|
|
896
|
+
const offset = 140;
|
|
897
|
+
headings.forEach(heading => {
|
|
898
|
+
const rect = heading.getBoundingClientRect();
|
|
899
|
+
if (rect.top <= offset) {
|
|
900
|
+
activeHeading = heading;
|
|
901
|
+
} else if (rect.top < nearestBelowTop) {
|
|
902
|
+
nearestBelowTop = rect.top;
|
|
903
|
+
nearestBelow = heading;
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
if (!activeHeading && nearestBelow) {
|
|
907
|
+
activeHeading = nearestBelow;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
tocLinks.forEach(link => {
|
|
911
|
+
const anchor = link.getAttribute('data-anchor');
|
|
912
|
+
if (activeHeading && anchor === activeHeading.id) {
|
|
913
|
+
link.classList.add('bg-blue-50', 'dark:bg-blue-900/20', 'text-blue-600', 'dark:text-blue-400', 'font-semibold');
|
|
914
|
+
} else {
|
|
915
|
+
link.classList.remove('bg-blue-50', 'dark:bg-blue-900/20', 'text-blue-600', 'dark:text-blue-400', 'font-semibold');
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
const activeId = activeHeading ? activeHeading.id : null;
|
|
920
|
+
if (activeId && activeId !== lastActiveTocAnchor) {
|
|
921
|
+
document.querySelectorAll(`.toc-link[data-anchor="${activeId}"]`).forEach(link => {
|
|
922
|
+
link.scrollIntoView({ block: 'nearest' });
|
|
923
|
+
});
|
|
924
|
+
lastActiveTocAnchor = activeId;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// Listen for scroll events to update active TOC link
|
|
929
|
+
let ticking = false;
|
|
930
|
+
window.addEventListener('scroll', () => {
|
|
931
|
+
if (!ticking) {
|
|
932
|
+
window.requestAnimationFrame(() => {
|
|
933
|
+
updateActiveTocLink();
|
|
934
|
+
ticking = false;
|
|
935
|
+
});
|
|
936
|
+
ticking = true;
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
// Sync TOC highlight on hash changes and TOC clicks
|
|
941
|
+
window.addEventListener('hashchange', () => {
|
|
942
|
+
requestAnimationFrame(updateActiveTocLink);
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
document.addEventListener('click', (event) => {
|
|
946
|
+
const link = event.target.closest('.toc-link');
|
|
947
|
+
if (!link) {
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
const anchor = link.getAttribute('data-anchor');
|
|
951
|
+
if (!anchor) {
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
requestAnimationFrame(() => {
|
|
955
|
+
document.querySelectorAll('.toc-link').forEach(item => {
|
|
956
|
+
item.classList.toggle(
|
|
957
|
+
'bg-blue-50',
|
|
958
|
+
item.getAttribute('data-anchor') === anchor
|
|
959
|
+
);
|
|
960
|
+
item.classList.toggle(
|
|
961
|
+
'dark:bg-blue-900/20',
|
|
962
|
+
item.getAttribute('data-anchor') === anchor
|
|
963
|
+
);
|
|
964
|
+
item.classList.toggle(
|
|
965
|
+
'text-blue-600',
|
|
966
|
+
item.getAttribute('data-anchor') === anchor
|
|
967
|
+
);
|
|
968
|
+
item.classList.toggle(
|
|
969
|
+
'dark:text-blue-400',
|
|
970
|
+
item.getAttribute('data-anchor') === anchor
|
|
971
|
+
);
|
|
972
|
+
item.classList.toggle(
|
|
973
|
+
'font-semibold',
|
|
974
|
+
item.getAttribute('data-anchor') === anchor
|
|
975
|
+
);
|
|
976
|
+
});
|
|
977
|
+
lastActiveTocAnchor = anchor;
|
|
978
|
+
updateActiveTocLink();
|
|
979
|
+
});
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
// Re-run mermaid on HTMX content swaps
|
|
983
|
+
document.body.addEventListener('htmx:afterSwap', function(event) {
|
|
984
|
+
mermaidDebugSnapshot('before mermaid.run (htmx:afterSwap)');
|
|
985
|
+
document.querySelectorAll('.mermaid-wrapper').forEach(wrapper => {
|
|
986
|
+
if (!wrapper.id) {
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
// HTMX swaps can trigger a mermaid re-run that replaces SVGs.
|
|
990
|
+
// Clear interaction state so we always re-bind after mermaid.run().
|
|
991
|
+
delete mermaidStates[wrapper.id];
|
|
992
|
+
delete wrapper.dataset.mermaidInteractive;
|
|
993
|
+
});
|
|
994
|
+
mermaid.run().then(() => {
|
|
995
|
+
mermaidDebugSnapshot('after mermaid.run (htmx:afterSwap)');
|
|
996
|
+
scheduleMermaidInteraction();
|
|
997
|
+
});
|
|
998
|
+
updateActivePostLink();
|
|
999
|
+
updateActiveTocLink();
|
|
1000
|
+
initMobileMenus(); // Reinitialize mobile menu handlers
|
|
1001
|
+
initPostsSidebarAutoReveal();
|
|
1002
|
+
initFolderChevronState();
|
|
1003
|
+
initSearchPlaceholderCycle(event.target || document);
|
|
1004
|
+
initCodeBlockCopyButtons(event.target || document);
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
// Watch for theme changes and re-render mermaid diagrams
|
|
1008
|
+
const observer = new MutationObserver((mutations) => {
|
|
1009
|
+
mutations.forEach((mutation) => {
|
|
1010
|
+
if (mutation.attributeName === 'class') {
|
|
1011
|
+
reinitializeMermaid();
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
observer.observe(document.documentElement, {
|
|
1017
|
+
attributes: true,
|
|
1018
|
+
attributeFilter: ['class']
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
// Mobile menu toggle functionality
|
|
1022
|
+
function initMobileMenus() {
|
|
1023
|
+
const postsToggle = document.getElementById('mobile-posts-toggle');
|
|
1024
|
+
const tocToggle = document.getElementById('mobile-toc-toggle');
|
|
1025
|
+
const postsPanel = document.getElementById('mobile-posts-panel');
|
|
1026
|
+
const tocPanel = document.getElementById('mobile-toc-panel');
|
|
1027
|
+
const closePostsBtn = document.getElementById('close-mobile-posts');
|
|
1028
|
+
const closeTocBtn = document.getElementById('close-mobile-toc');
|
|
1029
|
+
|
|
1030
|
+
// Open posts panel
|
|
1031
|
+
if (postsToggle) {
|
|
1032
|
+
postsToggle.addEventListener('click', () => {
|
|
1033
|
+
if (postsPanel) {
|
|
1034
|
+
postsPanel.classList.remove('-translate-x-full');
|
|
1035
|
+
postsPanel.classList.add('translate-x-0');
|
|
1036
|
+
// Close TOC panel if open
|
|
1037
|
+
if (tocPanel) {
|
|
1038
|
+
tocPanel.classList.remove('translate-x-0');
|
|
1039
|
+
tocPanel.classList.add('translate-x-full');
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Open TOC panel
|
|
1046
|
+
if (tocToggle) {
|
|
1047
|
+
tocToggle.addEventListener('click', () => {
|
|
1048
|
+
if (tocPanel) {
|
|
1049
|
+
tocPanel.classList.remove('translate-x-full');
|
|
1050
|
+
tocPanel.classList.add('translate-x-0');
|
|
1051
|
+
// Close posts panel if open
|
|
1052
|
+
if (postsPanel) {
|
|
1053
|
+
postsPanel.classList.remove('translate-x-0');
|
|
1054
|
+
postsPanel.classList.add('-translate-x-full');
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Close posts panel
|
|
1061
|
+
if (closePostsBtn) {
|
|
1062
|
+
closePostsBtn.addEventListener('click', () => {
|
|
1063
|
+
if (postsPanel) {
|
|
1064
|
+
postsPanel.classList.remove('translate-x-0');
|
|
1065
|
+
postsPanel.classList.add('-translate-x-full');
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// Close TOC panel
|
|
1071
|
+
if (closeTocBtn) {
|
|
1072
|
+
closeTocBtn.addEventListener('click', () => {
|
|
1073
|
+
if (tocPanel) {
|
|
1074
|
+
tocPanel.classList.remove('translate-x-0');
|
|
1075
|
+
tocPanel.classList.add('translate-x-full');
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Close panels on link click (for better mobile UX)
|
|
1081
|
+
if (postsPanel) {
|
|
1082
|
+
postsPanel.addEventListener('click', (e) => {
|
|
1083
|
+
if (e.target.tagName === 'A' || e.target.closest('a')) {
|
|
1084
|
+
setTimeout(() => {
|
|
1085
|
+
postsPanel.classList.remove('translate-x-0');
|
|
1086
|
+
postsPanel.classList.add('-translate-x-full');
|
|
1087
|
+
}, 100);
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
if (tocPanel) {
|
|
1093
|
+
tocPanel.addEventListener('click', (e) => {
|
|
1094
|
+
if (e.target.tagName === 'A' || e.target.closest('a')) {
|
|
1095
|
+
setTimeout(() => {
|
|
1096
|
+
tocPanel.classList.remove('translate-x-0');
|
|
1097
|
+
tocPanel.classList.add('translate-x-full');
|
|
1098
|
+
}, 100);
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// Keyboard shortcuts for toggling sidebars
|
|
1105
|
+
function initKeyboardShortcuts() {
|
|
1106
|
+
// Prewarm the selectors to avoid lazy compilation delays
|
|
1107
|
+
const postsSidebars = document.querySelectorAll('details[data-sidebar="posts"]');
|
|
1108
|
+
const tocSidebar = document.querySelector('#toc-sidebar details');
|
|
1109
|
+
|
|
1110
|
+
document.addEventListener('keydown', (e) => {
|
|
1111
|
+
// Skip if user is typing in an input field
|
|
1112
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) {
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// Z: Toggle posts panel
|
|
1117
|
+
if (e.key === 'z' || e.key === 'Z') {
|
|
1118
|
+
e.preventDefault();
|
|
1119
|
+
const postsSidebars = document.querySelectorAll('details[data-sidebar="posts"]');
|
|
1120
|
+
postsSidebars.forEach(sidebar => {
|
|
1121
|
+
sidebar.open = !sidebar.open;
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// X: Toggle TOC panel
|
|
1126
|
+
if (e.key === 'x' || e.key === 'X') {
|
|
1127
|
+
e.preventDefault();
|
|
1128
|
+
const tocSidebar = document.querySelector('#toc-sidebar details');
|
|
1129
|
+
if (tocSidebar) {
|
|
1130
|
+
tocSidebar.open = !tocSidebar.open;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function syncPdfFocusButtons(root = document) {
|
|
1137
|
+
const isFocused = document.body.classList.contains('pdf-focus');
|
|
1138
|
+
root.querySelectorAll('[data-pdf-focus-toggle]').forEach((button) => {
|
|
1139
|
+
const focusLabel = button.getAttribute('data-pdf-focus-label') || 'Focus PDF';
|
|
1140
|
+
const exitLabel = button.getAttribute('data-pdf-exit-label') || 'Exit focus';
|
|
1141
|
+
button.textContent = isFocused ? exitLabel : focusLabel;
|
|
1142
|
+
button.setAttribute('aria-pressed', isFocused ? 'true' : 'false');
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function ensurePdfFocusState() {
|
|
1147
|
+
const hasPdfViewer = document.querySelector('.pdf-viewer') || document.querySelector('[data-pdf-focus-toggle]');
|
|
1148
|
+
if (!hasPdfViewer) {
|
|
1149
|
+
document.body.classList.remove('pdf-focus');
|
|
1150
|
+
}
|
|
1151
|
+
syncPdfFocusButtons(document);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function initPdfFocusToggle() {
|
|
1155
|
+
document.addEventListener('click', (event) => {
|
|
1156
|
+
const button = event.target.closest('[data-pdf-focus-toggle]');
|
|
1157
|
+
if (!button) {
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
event.preventDefault();
|
|
1161
|
+
document.body.classList.toggle('pdf-focus');
|
|
1162
|
+
syncPdfFocusButtons(document);
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
document.addEventListener('keydown', (event) => {
|
|
1166
|
+
if (event.key !== 'Escape') {
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
if (!document.body.classList.contains('pdf-focus')) {
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
document.body.classList.remove('pdf-focus');
|
|
1173
|
+
syncPdfFocusButtons(document);
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// Initialize on page load
|
|
1178
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
1179
|
+
updateActivePostLink();
|
|
1180
|
+
updateActiveTocLink();
|
|
1181
|
+
initMobileMenus();
|
|
1182
|
+
initPostsSidebarAutoReveal();
|
|
1183
|
+
initFolderChevronState();
|
|
1184
|
+
initKeyboardShortcuts();
|
|
1185
|
+
initPdfFocusToggle();
|
|
1186
|
+
initSearchPlaceholderCycle(document);
|
|
1187
|
+
initPostsSearchPersistence(document);
|
|
1188
|
+
initCodeBlockCopyButtons(document);
|
|
1189
|
+
initSearchClearButtons(document);
|
|
1190
|
+
ensurePdfFocusState();
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
document.body.addEventListener('htmx:afterSwap', (event) => {
|
|
1194
|
+
if (!event.target) {
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
initSearchPlaceholderCycle(event.target);
|
|
1198
|
+
initPostsSearchPersistence(event.target);
|
|
1199
|
+
initCodeBlockCopyButtons(event.target);
|
|
1200
|
+
initSearchClearButtons(event.target);
|
|
1201
|
+
ensurePdfFocusState();
|
|
1202
|
+
});
|