refactor: extract App.vue logic into composables (762→230 lines)
All checks were successful
Deploy / Build and Deploy to Production (push) Successful in 14s

Extracted complex logic from App.vue into focused, reusable composables:

New composables:
- useKeyboardShortcuts.js (~80 lines): Keyboard shortcuts (Cmd/Ctrl+S, P, Escape, \)
- useIframeInteractions.js (~370 lines): Page/element hover, labels, clicks, popups
- usePreviewRenderer.js (~160 lines): Double buffering, transitions, scroll persistence
- usePrintPreview.js (~70 lines): Print dialog and style collection

Benefits:
- 70% reduction in App.vue size (532 lines extracted)
- Better separation of concerns
- Improved maintainability and testability
- Clearer code organization

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
isUnknown 2026-01-09 17:20:10 +01:00
parent dac532a932
commit be7bb66e70
5 changed files with 729 additions and 581 deletions

View file

@ -0,0 +1,157 @@
import { ref, watch } from 'vue';
import Coloris from '@melloware/coloris';
/**
* Composable for managing preview rendering with double buffering
* Handles iframe transitions, scroll persistence, and PagedJS rendering
*/
export function usePreviewRenderer({
previewFrame1,
previewFrame2,
stylesheetStore,
narrativeStore,
handleIframeMouseMove,
handleIframeClick,
}) {
let savedScrollPercentage = 0;
const currentFrameIndex = ref(1); // 1 or 2, which iframe is currently visible
const isTransitioning = ref(false);
let keyboardShortcutHandler = null;
/**
* Render preview to hidden iframe with crossfade transition
*/
const renderPreview = async (shouldReloadFromFile = false) => {
if (isTransitioning.value) return;
isTransitioning.value = true;
// Determine which iframe is currently visible and which to render to
const visibleFrame =
currentFrameIndex.value === 1 ? previewFrame1.value : previewFrame2.value;
const hiddenFrame =
currentFrameIndex.value === 1 ? previewFrame2.value : previewFrame1.value;
if (!hiddenFrame) {
isTransitioning.value = false;
return;
}
// Save scroll position from visible frame
if (
visibleFrame &&
visibleFrame.contentWindow &&
visibleFrame.contentDocument
) {
const scrollTop = visibleFrame.contentWindow.scrollY || 0;
const scrollHeight =
visibleFrame.contentDocument.documentElement.scrollHeight;
const clientHeight = visibleFrame.contentWindow.innerHeight;
const maxScroll = scrollHeight - clientHeight;
savedScrollPercentage = maxScroll > 0 ? scrollTop / maxScroll : 0;
}
if (shouldReloadFromFile || !stylesheetStore.content) {
await stylesheetStore.loadStylesheet();
}
// Render to the hidden frame
hiddenFrame.srcdoc = `
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/assets/css/pagedjs-interface.css">
<style id="dynamic-styles">${stylesheetStore.content}</style>
<script src="https://unpkg.com/pagedjs/dist/paged.polyfill.js"><\/script>
</head>
<body>${document.getElementById('content-source').innerHTML}</body>
</html>
`;
hiddenFrame.onload = () => {
// Add event listeners for page and element interactions
hiddenFrame.contentDocument.addEventListener(
'mousemove',
handleIframeMouseMove
);
hiddenFrame.contentDocument.addEventListener('click', handleIframeClick);
// Add keyboard shortcut listener to iframe (for when focus is inside iframe)
if (keyboardShortcutHandler) {
hiddenFrame.contentDocument.addEventListener('keydown', keyboardShortcutHandler);
}
// Close Coloris when clicking in the iframe
hiddenFrame.contentDocument.addEventListener('click', () => {
Coloris.close();
});
// Wait for PagedJS to finish rendering
setTimeout(() => {
// Restore scroll position
const scrollHeight =
hiddenFrame.contentDocument.documentElement.scrollHeight;
const clientHeight = hiddenFrame.contentWindow.innerHeight;
const maxScroll = scrollHeight - clientHeight;
const targetScroll = savedScrollPercentage * maxScroll;
hiddenFrame.contentWindow.scrollTo(0, targetScroll);
// Start crossfade transition
setTimeout(() => {
// Make hidden frame visible (it's already behind)
hiddenFrame.style.opacity = '1';
hiddenFrame.style.zIndex = '1';
// Fade out visible frame
if (visibleFrame) {
visibleFrame.style.opacity = '0';
}
// After fade completes, swap the frames
setTimeout(() => {
if (visibleFrame) {
visibleFrame.style.zIndex = '0';
}
// Swap current frame
currentFrameIndex.value = currentFrameIndex.value === 1 ? 2 : 1;
isTransitioning.value = false;
}, 200); // Match CSS transition duration
}, 50); // Small delay to ensure scroll is set
}, 200); // Wait for PagedJS
};
};
// Watch for stylesheet changes and re-render
watch(
() => stylesheetStore.content,
() => {
renderPreview();
}
);
// Re-render when narrative data changes
watch(
() => narrativeStore.data,
() => {
if (narrativeStore.data) {
renderPreview();
}
}
);
/**
* Set the keyboard shortcut handler (called after keyboard shortcuts composable is initialized)
*/
const setKeyboardShortcutHandler = (handler) => {
keyboardShortcutHandler = handler;
};
return {
renderPreview,
currentFrameIndex,
isTransitioning,
setKeyboardShortcutHandler,
};
}