diff --git a/package-lock.json b/package-lock.json index 331bddd..be5ff4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "highlight.js": "^11.11.1", "pagedjs": "^0.4.3", + "pinia": "^3.0.4", "vue": "^3.5.24" }, "devDependencies": { @@ -920,6 +921,39 @@ "@vue/shared": "3.5.25" } }, + "node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, "node_modules/@vue/reactivity": { "version": "3.5.25", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.25.tgz", @@ -970,12 +1004,36 @@ "integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==", "license": "MIT" }, + "node_modules/birpc": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.8.0.tgz", + "integrity": "sha512-Bz2a4qD/5GRhiHSwj30c/8kC8QGj12nNDwz3D4ErQ4Xhy35dsSDvF+RA/tWpjyU0pdGtSDiEk6B5fBGE1qNVhw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/clear-cut": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/clear-cut/-/clear-cut-2.0.2.tgz", "integrity": "sha512-WVgn/gSejQ+0aoR8ucbKIdo6icduPZW6AbWwyUmAUgxy63rUYjwa5rj/HeoNPhf0/XPrl82X8bO/hwBkSmsFtg==", "license": "MIT" }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/core-js": { "version": "2.6.12", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", @@ -1192,6 +1250,24 @@ "node": ">=12.0.0" } }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -1207,6 +1283,12 @@ "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", "license": "CC0-1.0" }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -1244,6 +1326,12 @@ "event-emitter": "^0.3.5" } }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1263,6 +1351,27 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pinia": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", + "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.7" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.5.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -1297,6 +1406,12 @@ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", "license": "MIT" }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, "node_modules/rollup": { "version": "4.53.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", @@ -1357,6 +1472,27 @@ "node": ">=0.10.0" } }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/superjson": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.5.tgz", + "integrity": "sha512-zWPTX96LVsA/eVYnqOM2+ofcdPqdS1dAF1LN4TS2/MWuUpfitd9ctTa87wt4xrYnZnkLtS69xpBdSxVBP5Rm6w==", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", diff --git a/package.json b/package.json index 2e1896a..03d6604 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "highlight.js": "^11.11.1", "pagedjs": "^0.4.3", + "pinia": "^3.0.4", "vue": "^3.5.24" }, "devDependencies": { diff --git a/public/assets/css/stylesheet.css b/public/assets/css/stylesheet.css index 98badef..ea6e1c4 100644 --- a/public/assets/css/stylesheet.css +++ b/public/assets/css/stylesheet.css @@ -1,7 +1,7 @@ -.about { - font-size: 1rem; -} - #chapter-2 { font-size: 2rem; } + +p { + font-size: 1rem; +} diff --git a/src/App.vue b/src/App.vue index 2372aa6..99dca4e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -4,22 +4,16 @@ import EditorPanel from './components/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'; -// Main state -const previewFrame = ref(null); -const stylesheetContent = ref(''); -const aboutFontSize = ref(2); -const aboutFontSizeUnit = ref('rem'); +// ============================================================================ +// Store +// ============================================================================ +const stylesheetStore = useStylesheetStore(); -// Popup state -const popupVisible = ref(false); -const popupPosition = ref({ x: 0, y: 0 }); -const popupSelector = ref(''); -const popupElementCss = ref(''); -const popupFontSize = ref(null); -const popupFontSizeUnit = ref('rem'); - -// PagedJS print rules +// ============================================================================ +// PagedJS configuration +// ============================================================================ const printStyles = ` h2 { break-before: page; } @@ -35,25 +29,11 @@ h2 { break-before: page; } .chapter > h2 { string-set: title content(text); } `; -// CSS parsing utilities -const extractCssBlock = (css, selector) => { - const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const match = css.match(new RegExp(`${escaped}\\s*{[^}]*}`, 'gi')); - return match ? match[0] : ''; -}; +// ============================================================================ +// Iframe preview +// ============================================================================ +const previewFrame = ref(null); -const extractCssValue = (css, selector, property) => { - const regex = new RegExp(`${selector}\\s*{[^}]*${property}:\\s*([\\d.]+)(px|rem|em)`, 'i'); - const match = css.match(regex); - return match ? { value: parseFloat(match[1]), unit: match[2] } : null; -}; - -const updateCssValue = (selector, property, value, unit) => { - const regex = new RegExp(`(${selector}\\s*{[^}]*${property}:\\s*)[\\d.]+(px|rem|em)`, 'gi'); - stylesheetContent.value = stylesheetContent.value.replace(regex, `$1${value}${unit}`); -}; - -// Iframe style injection const injectStylesToIframe = () => { const iframe = previewFrame.value; if (!iframe?.contentDocument) return; @@ -64,93 +44,39 @@ const injectStylesToIframe = () => { styleElement.id = 'dynamic-styles'; iframe.contentDocument.head.appendChild(styleElement); } - styleElement.textContent = stylesheetContent.value; + styleElement.textContent = stylesheetStore.content; }; -// Popup handlers -const handleIframeClick = (event) => { - const element = event.target; +const elementPopup = ref(null); - if (element.tagName === 'BODY' || element.tagName === 'HTML') { - popupVisible.value = false; - return; - } - - const selector = element.id - ? `#${element.id}` - : `.${element.className.split(' ')[0]}`; - - popupSelector.value = selector; - popupElementCss.value = extractCssBlock(stylesheetContent.value, selector); - - const fontSizeData = extractCssValue(stylesheetContent.value, selector, 'font-size'); - popupFontSize.value = fontSizeData?.value ?? null; - popupFontSizeUnit.value = fontSizeData?.unit ?? 'rem'; - - const rect = element.getBoundingClientRect(); - const iframeRect = previewFrame.value.getBoundingClientRect(); - popupPosition.value = { - x: iframeRect.left + rect.left, - y: iframeRect.top + rect.bottom + 5 - }; - - popupVisible.value = true; -}; - -const closePopup = () => { - popupVisible.value = false; -}; - -const updatePopupFontSize = (newValue) => { - updateCssValue(popupSelector.value, 'font-size', newValue, popupFontSizeUnit.value); - popupFontSize.value = newValue; - popupElementCss.value = extractCssBlock(stylesheetContent.value, popupSelector.value); -}; - -// Watchers -watch(aboutFontSize, (newVal) => { - updateCssValue('.about', 'font-size', newVal, aboutFontSizeUnit.value); -}); - -watch(stylesheetContent, injectStylesToIframe); - -// Initial render const renderPreview = async () => { const iframe = previewFrame.value; if (!iframe) return; - const response = await fetch('/assets/css/stylesheet.css'); - stylesheetContent.value = await response.text(); + await stylesheetStore.loadStylesheet(); - const initialFontSize = extractCssValue(stylesheetContent.value, '.about', 'font-size'); - if (initialFontSize) { - aboutFontSize.value = initialFontSize.value; - aboutFontSizeUnit.value = initialFontSize.unit; - } - - const contentSource = document.getElementById('content-source'); - const iframeDoc = iframe.contentDocument; - - iframeDoc.open(); - iframeDoc.write(` + iframe.srcdoc = `
- + @@ -159,26 +85,13 @@ onMounted(renderPreview);