feat: implement reactive EditorPanel with bidirectional sync

- Reorganize editor components into dedicated folder
- Create PageSettings component with page format, margins, background controls
- Create TextSettings component (structure only, to be populated)
- Implement debounced updates (1s delay) to stylesheet store
- Add bidirectional sync between EditorPanel and StylesheetViewer
- Preserve scroll position as percentage when reloading preview
- Move @page rules from App.vue to stylesheet.css for unified management
- Extend css-parsing utils to handle text values (e.g., 'A4', 'portrait')
- Remove unnecessary comments, use explicit naming instead

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
isUnknown 2025-12-03 15:20:49 +01:00
parent b8cb77c0e5
commit 9f10971041
7 changed files with 1104 additions and 166 deletions

View file

@ -1,59 +1,33 @@
<script setup>
import PagedJsWrapper from './components/PagedJsWrapper.vue';
import EditorPanel from './components/EditorPanel.vue';
import EditorPanel from './components/editor/EditorPanel.vue';
import StylesheetViewer from './components/StylesheetViewer.vue';
import ElementPopup from './components/ElementPopup.vue';
import { onMounted, ref, watch } from 'vue';
import { useStylesheetStore } from './stores/stylesheet';
// ============================================================================
// Store
// ============================================================================
const stylesheetStore = useStylesheetStore();
// ============================================================================
// PagedJS configuration
// ============================================================================
const printStyles = `
h2 { break-before: page; }
@page {
size: A4;
margin: 20mm 15mm 26mm 15mm;
}
@page {
@bottom-center { content: string(title); }
}
.chapter > h2 { string-set: title content(text); }
`;
// ============================================================================
// Iframe preview
// ============================================================================
const previewFrame = ref(null);
const injectStylesToIframe = () => {
const iframe = previewFrame.value;
if (!iframe?.contentDocument) return;
let styleElement = iframe.contentDocument.getElementById('dynamic-styles');
if (!styleElement) {
styleElement = iframe.contentDocument.createElement('style');
styleElement.id = 'dynamic-styles';
iframe.contentDocument.head.appendChild(styleElement);
}
styleElement.textContent = stylesheetStore.content;
};
const elementPopup = ref(null);
const renderPreview = async () => {
let savedScrollPercentage = 0;
const renderPreview = async (shouldReloadFromFile = false) => {
const iframe = previewFrame.value;
if (!iframe) return;
await stylesheetStore.loadStylesheet();
if (iframe.contentWindow && iframe.contentDocument) {
const scrollTop = iframe.contentWindow.scrollY || 0;
const scrollHeight = iframe.contentDocument.documentElement.scrollHeight;
const clientHeight = iframe.contentWindow.innerHeight;
const maxScroll = scrollHeight - clientHeight;
savedScrollPercentage = maxScroll > 0 ? scrollTop / maxScroll : 0;
}
if (shouldReloadFromFile || !stylesheetStore.content) {
await stylesheetStore.loadStylesheet();
}
iframe.srcdoc = `
<!DOCTYPE html>
@ -61,7 +35,6 @@ const renderPreview = async () => {
<head>
<link rel="stylesheet" href="/assets/css/pagedjs-interface.css">
<style id="dynamic-styles">${stylesheetStore.content}</style>
<style>${printStyles}</style>
<script src="https://unpkg.com/pagedjs/dist/paged.polyfill.js"><\/script>
</head>
<body>${document.getElementById('content-source').innerHTML}</body>
@ -70,14 +43,23 @@ const renderPreview = async () => {
iframe.onload = () => {
iframe.contentDocument.addEventListener('click', elementPopup.value.handleIframeClick);
setTimeout(() => {
const scrollHeight = iframe.contentDocument.documentElement.scrollHeight;
const clientHeight = iframe.contentWindow.innerHeight;
const maxScroll = scrollHeight - clientHeight;
const targetScroll = savedScrollPercentage * maxScroll;
iframe.contentWindow.scrollTo(0, targetScroll);
}, 500);
};
};
// ============================================================================
// Lifecycle
// ============================================================================
watch(() => stylesheetStore.content, injectStylesToIframe);
onMounted(renderPreview);
watch(() => stylesheetStore.content, () => {
renderPreview();
});
onMounted(() => renderPreview(true));
</script>
<template>