2025-11-24 14:01:48 +01:00
|
|
|
<script setup>
|
|
|
|
|
import PagedJsWrapper from './components/PagedJsWrapper.vue';
|
2025-12-03 15:20:49 +01:00
|
|
|
import EditorPanel from './components/editor/EditorPanel.vue';
|
2025-11-24 16:51:55 +01:00
|
|
|
import ElementPopup from './components/ElementPopup.vue';
|
2025-12-05 17:32:39 +01:00
|
|
|
import PagePopup from './components/PagePopup.vue';
|
2025-12-05 16:45:57 +01:00
|
|
|
import PreviewLoader from './components/PreviewLoader.vue';
|
2025-12-08 13:59:33 +01:00
|
|
|
import { onMounted, onUnmounted, ref, watch, computed, provide } from 'vue';
|
2025-11-24 17:55:42 +01:00
|
|
|
import { useStylesheetStore } from './stores/stylesheet';
|
2025-12-04 16:14:50 +01:00
|
|
|
import Coloris from '@melloware/coloris';
|
2025-11-24 17:55:42 +01:00
|
|
|
|
|
|
|
|
const stylesheetStore = useStylesheetStore();
|
2025-12-04 15:27:23 +01:00
|
|
|
const previewFrame1 = ref(null);
|
|
|
|
|
const previewFrame2 = ref(null);
|
2025-11-24 18:18:27 +01:00
|
|
|
const elementPopup = ref(null);
|
2025-12-05 17:32:39 +01:00
|
|
|
const pagePopup = ref(null);
|
2025-12-05 11:02:18 +01:00
|
|
|
const activeTab = ref('');
|
|
|
|
|
|
|
|
|
|
provide('activeTab', activeTab);
|
2025-11-24 18:18:27 +01:00
|
|
|
|
2025-12-05 17:32:39 +01:00
|
|
|
// Page interaction state
|
|
|
|
|
const hoveredPage = ref(null);
|
2025-12-08 12:29:55 +01:00
|
|
|
const selectedPages = ref([]); // Pages with active border (when popup is open)
|
2025-12-05 17:32:39 +01:00
|
|
|
const EDGE_THRESHOLD = 30; // px from edge to trigger hover
|
2025-12-08 12:29:55 +01:00
|
|
|
const PAGE_HIGHLIGHT_COLOR = '#ff8a50';
|
2025-12-05 17:32:39 +01:00
|
|
|
|
2025-12-03 15:20:49 +01:00
|
|
|
let savedScrollPercentage = 0;
|
2025-12-04 15:27:23 +01:00
|
|
|
const currentFrameIndex = ref(1); // 1 or 2, which iframe is currently visible
|
2025-12-05 16:41:07 +01:00
|
|
|
const isTransitioning = ref(false);
|
2025-12-04 15:27:23 +01:00
|
|
|
|
|
|
|
|
const activeFrame = computed(() => {
|
2025-12-04 15:55:52 +01:00
|
|
|
return currentFrameIndex.value === 1
|
|
|
|
|
? previewFrame1.value
|
|
|
|
|
: previewFrame2.value;
|
2025-12-04 15:27:23 +01:00
|
|
|
});
|
2025-12-03 15:20:49 +01:00
|
|
|
|
2025-12-05 17:32:39 +01:00
|
|
|
// Check if mouse position is near the edges of a page element
|
|
|
|
|
const isNearPageEdge = (pageElement, mouseX, mouseY) => {
|
|
|
|
|
const rect = pageElement.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
const nearLeft = mouseX >= rect.left && mouseX <= rect.left + EDGE_THRESHOLD;
|
|
|
|
|
const nearRight = mouseX >= rect.right - EDGE_THRESHOLD && mouseX <= rect.right;
|
|
|
|
|
const nearTop = mouseY >= rect.top && mouseY <= rect.top + EDGE_THRESHOLD;
|
|
|
|
|
const nearBottom = mouseY >= rect.bottom - EDGE_THRESHOLD && mouseY <= rect.bottom;
|
|
|
|
|
|
|
|
|
|
const inHorizontalRange = mouseY >= rect.top && mouseY <= rect.bottom;
|
|
|
|
|
const inVerticalRange = mouseX >= rect.left && mouseX <= rect.right;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
(nearLeft && inHorizontalRange) ||
|
|
|
|
|
(nearRight && inHorizontalRange) ||
|
|
|
|
|
(nearTop && inVerticalRange) ||
|
|
|
|
|
(nearBottom && inVerticalRange)
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-08 12:29:55 +01:00
|
|
|
// Get all pages using the same template as the given page
|
|
|
|
|
const getPagesWithSameTemplate = (page, doc) => {
|
|
|
|
|
const pageType = page.getAttribute('data-page-type') || 'default';
|
|
|
|
|
const allPages = doc.querySelectorAll('.pagedjs_page');
|
|
|
|
|
return Array.from(allPages).filter(
|
|
|
|
|
(p) => (p.getAttribute('data-page-type') || 'default') === pageType
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-05 17:32:39 +01:00
|
|
|
// Handle mouse movement in iframe
|
|
|
|
|
const handleIframeMouseMove = (event) => {
|
|
|
|
|
const pages = event.target.ownerDocument.querySelectorAll('.pagedjs_page');
|
|
|
|
|
let foundPage = null;
|
|
|
|
|
|
|
|
|
|
for (const page of pages) {
|
|
|
|
|
if (isNearPageEdge(page, event.clientX, event.clientY)) {
|
|
|
|
|
foundPage = page;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update hover state
|
|
|
|
|
if (foundPage !== hoveredPage.value) {
|
2025-12-08 12:29:55 +01:00
|
|
|
// Remove highlight from previous page (only if not in selectedPages)
|
|
|
|
|
if (hoveredPage.value && !selectedPages.value.includes(hoveredPage.value)) {
|
2025-12-05 17:32:39 +01:00
|
|
|
hoveredPage.value.style.outline = '';
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 12:29:55 +01:00
|
|
|
// Add highlight to new page (only if not already selected)
|
|
|
|
|
if (foundPage && !selectedPages.value.includes(foundPage)) {
|
|
|
|
|
foundPage.style.outline = `2px solid ${PAGE_HIGHLIGHT_COLOR}50`;
|
2025-12-05 17:32:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hoveredPage.value = foundPage;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-08 12:29:55 +01:00
|
|
|
// Clear selection highlight from all selected pages
|
|
|
|
|
const clearSelectedPages = () => {
|
|
|
|
|
selectedPages.value.forEach((page) => {
|
|
|
|
|
page.style.outline = '';
|
|
|
|
|
});
|
|
|
|
|
selectedPages.value = [];
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-08 12:41:14 +01:00
|
|
|
// Content elements that can trigger ElementPopup
|
|
|
|
|
const CONTENT_ELEMENTS = [
|
|
|
|
|
'P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
|
|
|
|
|
'IMG', 'FIGURE', 'FIGCAPTION',
|
|
|
|
|
'UL', 'OL', 'LI',
|
|
|
|
|
'BLOCKQUOTE', 'PRE', 'CODE',
|
|
|
|
|
'TABLE', 'THEAD', 'TBODY', 'TR', 'TH', 'TD',
|
|
|
|
|
'A', 'SPAN', 'STRONG', 'EM', 'B', 'I', 'U',
|
|
|
|
|
'ARTICLE', 'SECTION', 'ASIDE', 'HEADER', 'FOOTER', 'NAV'
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Check if element is a content element (or find closest content parent)
|
|
|
|
|
const getContentElement = (element) => {
|
|
|
|
|
let current = element;
|
|
|
|
|
while (current && current.tagName !== 'BODY') {
|
|
|
|
|
if (CONTENT_ELEMENTS.includes(current.tagName)) {
|
|
|
|
|
return current;
|
|
|
|
|
}
|
|
|
|
|
current = current.parentElement;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-05 17:32:39 +01:00
|
|
|
// Handle click in iframe
|
|
|
|
|
const handleIframeClick = (event) => {
|
|
|
|
|
const element = event.target;
|
|
|
|
|
|
|
|
|
|
// Check if clicking near a page edge
|
|
|
|
|
if (hoveredPage.value) {
|
|
|
|
|
event.stopPropagation();
|
2025-12-08 12:29:55 +01:00
|
|
|
|
|
|
|
|
// Clear previous selection
|
|
|
|
|
clearSelectedPages();
|
|
|
|
|
|
|
|
|
|
// Get all pages with same template and highlight them
|
|
|
|
|
const doc = event.target.ownerDocument;
|
|
|
|
|
const sameTemplatePages = getPagesWithSameTemplate(hoveredPage.value, doc);
|
|
|
|
|
sameTemplatePages.forEach((page) => {
|
|
|
|
|
page.style.outline = `2px solid ${PAGE_HIGHLIGHT_COLOR}`;
|
|
|
|
|
});
|
|
|
|
|
selectedPages.value = sameTemplatePages;
|
|
|
|
|
|
|
|
|
|
pagePopup.value.open(hoveredPage.value, event, sameTemplatePages.length);
|
2025-12-05 17:32:39 +01:00
|
|
|
elementPopup.value.close();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 12:27:15 +01:00
|
|
|
// Only show popup for elements inside the page template
|
|
|
|
|
const isInsidePage = element.closest('.pagedjs_page');
|
|
|
|
|
if (!isInsidePage) {
|
2025-12-08 12:29:55 +01:00
|
|
|
clearSelectedPages();
|
2025-12-05 17:32:39 +01:00
|
|
|
elementPopup.value.close();
|
|
|
|
|
pagePopup.value.close();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 12:41:14 +01:00
|
|
|
// Only show ElementPopup for content elements, not divs
|
|
|
|
|
const contentElement = getContentElement(element);
|
|
|
|
|
if (!contentElement) {
|
|
|
|
|
clearSelectedPages();
|
|
|
|
|
elementPopup.value.close();
|
|
|
|
|
pagePopup.value.close();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 12:29:55 +01:00
|
|
|
clearSelectedPages();
|
2025-12-08 12:41:14 +01:00
|
|
|
elementPopup.value.handleIframeClick(event, contentElement);
|
2025-12-05 17:32:39 +01:00
|
|
|
pagePopup.value.close();
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-08 12:29:55 +01:00
|
|
|
// Expose clearSelectedPages for PagePopup to call when closing
|
|
|
|
|
const handlePagePopupClose = () => {
|
|
|
|
|
clearSelectedPages();
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-03 15:20:49 +01:00
|
|
|
const renderPreview = async (shouldReloadFromFile = false) => {
|
2025-12-05 16:41:07 +01:00
|
|
|
if (isTransitioning.value) return;
|
|
|
|
|
isTransitioning.value = true;
|
2025-12-04 15:27:23 +01:00
|
|
|
|
|
|
|
|
// Determine which iframe is currently visible and which to render to
|
2025-12-04 15:55:52 +01:00
|
|
|
const visibleFrame =
|
|
|
|
|
currentFrameIndex.value === 1 ? previewFrame1.value : previewFrame2.value;
|
|
|
|
|
const hiddenFrame =
|
|
|
|
|
currentFrameIndex.value === 1 ? previewFrame2.value : previewFrame1.value;
|
2025-12-04 15:27:23 +01:00
|
|
|
|
|
|
|
|
if (!hiddenFrame) {
|
2025-12-05 16:41:07 +01:00
|
|
|
isTransitioning.value = false;
|
2025-12-04 15:27:23 +01:00
|
|
|
return;
|
|
|
|
|
}
|
2025-11-24 16:51:55 +01:00
|
|
|
|
2025-12-04 15:27:23 +01:00
|
|
|
// Save scroll position from visible frame
|
2025-12-04 15:55:52 +01:00
|
|
|
if (
|
|
|
|
|
visibleFrame &&
|
|
|
|
|
visibleFrame.contentWindow &&
|
|
|
|
|
visibleFrame.contentDocument
|
|
|
|
|
) {
|
2025-12-04 15:27:23 +01:00
|
|
|
const scrollTop = visibleFrame.contentWindow.scrollY || 0;
|
2025-12-04 15:55:52 +01:00
|
|
|
const scrollHeight =
|
|
|
|
|
visibleFrame.contentDocument.documentElement.scrollHeight;
|
2025-12-04 15:27:23 +01:00
|
|
|
const clientHeight = visibleFrame.contentWindow.innerHeight;
|
2025-12-03 15:20:49 +01:00
|
|
|
const maxScroll = scrollHeight - clientHeight;
|
|
|
|
|
|
|
|
|
|
savedScrollPercentage = maxScroll > 0 ? scrollTop / maxScroll : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (shouldReloadFromFile || !stylesheetStore.content) {
|
|
|
|
|
await stylesheetStore.loadStylesheet();
|
|
|
|
|
}
|
2025-11-24 16:51:55 +01:00
|
|
|
|
2025-12-04 15:27:23 +01:00
|
|
|
// Render to the hidden frame
|
|
|
|
|
hiddenFrame.srcdoc = `
|
2025-11-24 16:51:55 +01:00
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<html>
|
|
|
|
|
<head>
|
|
|
|
|
<link rel="stylesheet" href="/assets/css/pagedjs-interface.css">
|
2025-11-24 17:55:42 +01:00
|
|
|
<style id="dynamic-styles">${stylesheetStore.content}</style>
|
2025-11-24 16:51:55 +01:00
|
|
|
<script src="https://unpkg.com/pagedjs/dist/paged.polyfill.js"><\/script>
|
|
|
|
|
</head>
|
2025-11-24 17:55:42 +01:00
|
|
|
<body>${document.getElementById('content-source').innerHTML}</body>
|
2025-11-24 16:51:55 +01:00
|
|
|
</html>
|
2025-11-24 17:55:42 +01:00
|
|
|
`;
|
2025-11-24 16:51:55 +01:00
|
|
|
|
2025-12-04 15:27:23 +01:00
|
|
|
hiddenFrame.onload = () => {
|
2025-12-05 17:32:39 +01:00
|
|
|
// Add event listeners for page and element interactions
|
|
|
|
|
hiddenFrame.contentDocument.addEventListener('mousemove', handleIframeMouseMove);
|
|
|
|
|
hiddenFrame.contentDocument.addEventListener('click', handleIframeClick);
|
2025-12-03 15:20:49 +01:00
|
|
|
|
2025-12-04 16:14:50 +01:00
|
|
|
// Close Coloris when clicking in the iframe
|
|
|
|
|
hiddenFrame.contentDocument.addEventListener('click', () => {
|
|
|
|
|
Coloris.close();
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-04 15:27:23 +01:00
|
|
|
// Wait for PagedJS to finish rendering
|
2025-12-03 15:20:49 +01:00
|
|
|
setTimeout(() => {
|
2025-12-04 15:27:23 +01:00
|
|
|
// Restore scroll position
|
2025-12-04 15:55:52 +01:00
|
|
|
const scrollHeight =
|
|
|
|
|
hiddenFrame.contentDocument.documentElement.scrollHeight;
|
2025-12-04 15:27:23 +01:00
|
|
|
const clientHeight = hiddenFrame.contentWindow.innerHeight;
|
2025-12-03 15:20:49 +01:00
|
|
|
const maxScroll = scrollHeight - clientHeight;
|
|
|
|
|
const targetScroll = savedScrollPercentage * maxScroll;
|
|
|
|
|
|
2025-12-04 15:27:23 +01:00
|
|
|
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;
|
2025-12-05 16:41:07 +01:00
|
|
|
isTransitioning.value = false;
|
2025-12-05 15:05:40 +01:00
|
|
|
}, 200); // Match CSS transition duration
|
2025-12-04 15:55:52 +01:00
|
|
|
}, 50); // Small delay to ensure scroll is set
|
|
|
|
|
}, 200); // Wait for PagedJS
|
2025-11-24 18:01:47 +01:00
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-04 13:34:33 +01:00
|
|
|
watch(
|
|
|
|
|
() => stylesheetStore.content,
|
|
|
|
|
() => {
|
|
|
|
|
renderPreview();
|
|
|
|
|
}
|
|
|
|
|
);
|
2025-12-03 15:20:49 +01:00
|
|
|
|
2025-12-08 13:59:33 +01:00
|
|
|
// Handle Cmd+P / Ctrl+P to print the iframe content
|
|
|
|
|
const handlePrint = (event) => {
|
|
|
|
|
if ((event.metaKey || event.ctrlKey) && event.key === 'p') {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
const frame = activeFrame.value;
|
2025-12-08 14:01:27 +01:00
|
|
|
if (frame && frame.contentDocument) {
|
|
|
|
|
// Get the full HTML content of the iframe
|
|
|
|
|
const content = frame.contentDocument.documentElement.outerHTML;
|
|
|
|
|
|
|
|
|
|
// Open a new window with the content
|
|
|
|
|
const printWindow = window.open('', '_blank');
|
|
|
|
|
printWindow.document.write(content);
|
|
|
|
|
printWindow.document.close();
|
|
|
|
|
|
|
|
|
|
// Wait for content to load then print
|
|
|
|
|
printWindow.onload = () => {
|
|
|
|
|
printWindow.print();
|
|
|
|
|
printWindow.close();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Fallback if onload doesn't fire
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
if (!printWindow.closed) {
|
|
|
|
|
printWindow.print();
|
|
|
|
|
printWindow.close();
|
|
|
|
|
}
|
|
|
|
|
}, 500);
|
2025-12-08 13:59:33 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
renderPreview(true);
|
|
|
|
|
window.addEventListener('keydown', handlePrint);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
window.removeEventListener('keydown', handlePrint);
|
|
|
|
|
});
|
2025-11-24 14:01:48 +01:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
2025-11-24 16:51:55 +01:00
|
|
|
<div id="content-source" style="display: none">
|
2025-11-24 14:01:48 +01:00
|
|
|
<PagedJsWrapper />
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-11-24 18:18:27 +01:00
|
|
|
<EditorPanel />
|
2025-11-24 14:01:48 +01:00
|
|
|
|
2025-12-05 11:02:18 +01:00
|
|
|
<iframe
|
|
|
|
|
ref="previewFrame1"
|
|
|
|
|
class="preview-frame"
|
|
|
|
|
:class="{ shifted: activeTab.length > 0 }"
|
|
|
|
|
></iframe>
|
|
|
|
|
<iframe
|
|
|
|
|
ref="previewFrame2"
|
|
|
|
|
class="preview-frame"
|
|
|
|
|
:class="{ shifted: activeTab.length > 0 }"
|
|
|
|
|
></iframe>
|
2025-11-24 14:01:48 +01:00
|
|
|
|
2025-12-05 16:45:57 +01:00
|
|
|
<PreviewLoader :isLoading="isTransitioning" :shifted="activeTab.length > 0" />
|
2025-12-05 16:41:07 +01:00
|
|
|
|
2025-12-04 15:27:23 +01:00
|
|
|
<ElementPopup ref="elementPopup" :iframeRef="activeFrame" />
|
2025-12-08 12:29:55 +01:00
|
|
|
<PagePopup ref="pagePopup" :iframeRef="activeFrame" @close="handlePagePopupClose" />
|
2025-11-24 16:51:55 +01:00
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style>
|
2025-12-04 15:27:23 +01:00
|
|
|
.preview-frame {
|
2025-11-24 16:51:55 +01:00
|
|
|
position: fixed;
|
|
|
|
|
top: 0;
|
2025-12-04 13:34:33 +01:00
|
|
|
left: 0;
|
|
|
|
|
width: 100vw;
|
2025-11-24 16:51:55 +01:00
|
|
|
height: 100vh;
|
|
|
|
|
border: none;
|
2025-12-05 15:05:40 +01:00
|
|
|
margin-left: 0;
|
|
|
|
|
transform: scale(1) translateY(0);
|
|
|
|
|
height: 100vh;
|
|
|
|
|
transition: all 0.2s ease-in-out var(--curve);
|
2025-12-05 11:02:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.preview-frame.shifted {
|
2025-12-05 17:32:39 +01:00
|
|
|
margin-left: 17.55rem;
|
|
|
|
|
transform: scale(0.65) translateY(-40vh);
|
|
|
|
|
height: 155vh;
|
2025-12-04 15:27:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.preview-frame:nth-of-type(1) {
|
|
|
|
|
z-index: 1;
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.preview-frame:nth-of-type(2) {
|
|
|
|
|
z-index: 0;
|
|
|
|
|
opacity: 0;
|
2025-11-24 14:01:48 +01:00
|
|
|
}
|
2025-12-08 13:59:57 +01:00
|
|
|
|
|
|
|
|
/* Hide UI elements when printing */
|
|
|
|
|
@media print {
|
|
|
|
|
#editor-panel,
|
|
|
|
|
#element-popup,
|
|
|
|
|
#page-popup,
|
|
|
|
|
.preview-loader {
|
|
|
|
|
display: none !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.preview-frame {
|
|
|
|
|
position: static !important;
|
|
|
|
|
margin-left: 0 !important;
|
|
|
|
|
transform: none !important;
|
|
|
|
|
width: 100% !important;
|
|
|
|
|
height: auto !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.preview-frame:not(:first-of-type) {
|
|
|
|
|
display: none !important;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-24 14:01:48 +01:00
|
|
|
</style>
|