From c523b4e3359deced514ffca1390e40a927c1126e Mon Sep 17 00:00:00 2001 From: isUnknown Date: Mon, 8 Dec 2025 16:20:20 +0100 Subject: [PATCH 1/4] fix: improve element hover and selection styling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update element highlight styles for better visual consistency: - Hover: solid purple border with 50% opacity (matches page hover style) - Selection: dashed purple border + 10% opacity background (was 30%) The lighter background on selection reduces visual clutter while maintaining clear indication of selected state. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/App.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/App.vue b/src/App.vue index 44ce2a6..238a104 100644 --- a/src/App.vue +++ b/src/App.vue @@ -105,7 +105,7 @@ const handleIframeMouseMove = (event) => { // Add highlight to new element (only if not already selected) if (contentElement && contentElement !== selectedElement.value) { - contentElement.style.outline = `2px dashed ${ELEMENT_HIGHLIGHT_COLOR}`; + contentElement.style.outline = `2px solid ${ELEMENT_HIGHLIGHT_COLOR}50`; } hoveredElement.value = contentElement; @@ -219,8 +219,8 @@ const handleIframeClick = (event) => { // Select the new element selectedElement.value = contentElement; - contentElement.style.outline = ''; - contentElement.style.backgroundColor = `${ELEMENT_HIGHLIGHT_COLOR}4D`; // 30% opacity + contentElement.style.outline = `2px dashed ${ELEMENT_HIGHLIGHT_COLOR}`; + contentElement.style.backgroundColor = `${ELEMENT_HIGHLIGHT_COLOR}1A`; // 10% opacity // Get count of similar elements const doc = event.target.ownerDocument; From f9e9e657127fcaa9dd18a0f90ba61682d8714fff Mon Sep 17 00:00:00 2001 From: isUnknown Date: Mon, 8 Dec 2025 16:26:39 +0100 Subject: [PATCH 2/4] feat: add element hover label and refactor to use CSS classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add visual feedback for hovered elements: - Display element selector label (e.g., "p", "h1.title") on hover - Label positioned at top-left of element with 30% opacity Refactor all hover/selection styles to use CSS classes instead of inline styles: - .page-hovered, .page-selected for page states - .element-hovered, .element-selected for element states - .element-hover-label for the floating label This improves maintainability and separation of concerns by moving styling logic to CSS files instead of JavaScript. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- public/assets/css/pagedjs-interface.css | 32 ++++++++++++ src/App.vue | 68 +++++++++++++++++++++---- 2 files changed, 89 insertions(+), 11 deletions(-) diff --git a/public/assets/css/pagedjs-interface.css b/public/assets/css/pagedjs-interface.css index bcef24a..b4ad502 100644 --- a/public/assets/css/pagedjs-interface.css +++ b/public/assets/css/pagedjs-interface.css @@ -144,6 +144,38 @@ /*--------------------------------------------------------------------------------------*/ } +/* Hover and selection states for pages and elements */ +.page-hovered { + outline: 2px solid #ff8a5050 !important; +} + +.page-selected { + outline: 2px solid #ff8a50 !important; +} + +.element-hovered { + outline: 2px solid #7136ff50 !important; +} + +.element-selected { + outline: 2px dashed #7136ff !important; + background-color: #7136ff1a !important; +} + +.element-hover-label { + position: absolute; + background: #7136ff; + color: white; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.875rem; + font-weight: 600; + opacity: 0.3; + pointer-events: none; + z-index: 9999; + font-family: sans-serif; +} + /* Marks (to delete when merge in paged.js) */ .pagedjs_marks-crop { diff --git a/src/App.vue b/src/App.vue index 238a104..2826c7f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -65,6 +65,45 @@ const getPagesWithSameTemplate = (page, doc) => { ); }; +// Get selector for element (same logic as ElementPopup) +const getSelectorFromElement = (element) => { + if (element.id) { + return `#${element.id}`; + } + const tagName = element.tagName.toLowerCase(); + const classes = Array.from(element.classList); + if (classes.length > 0) { + return `${tagName}.${classes[0]}`; + } + return tagName; +}; + +// Create and position element label on hover +const createElementLabel = (element) => { + const doc = element.ownerDocument; + const existingLabel = doc.querySelector('.element-hover-label'); + if (existingLabel) { + existingLabel.remove(); + } + + const label = doc.createElement('div'); + label.className = 'element-hover-label'; + label.textContent = getSelectorFromElement(element); + label.style.top = `${element.offsetTop}px`; + label.style.left = `${element.offsetLeft}px`; + + doc.body.appendChild(label); + return label; +}; + +// Remove element label +const removeElementLabel = (doc) => { + const label = doc.querySelector('.element-hover-label'); + if (label) { + label.remove(); + } +}; + // Handle mouse movement in iframe const handleIframeMouseMove = (event) => { const pages = event.target.ownerDocument.querySelectorAll('.pagedjs_page'); @@ -82,12 +121,12 @@ const handleIframeMouseMove = (event) => { if (foundPage !== hoveredPage.value) { // Remove highlight from previous page (only if not in selectedPages) if (hoveredPage.value && !selectedPages.value.includes(hoveredPage.value)) { - hoveredPage.value.style.outline = ''; + hoveredPage.value.classList.remove('page-hovered'); } // 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`; + foundPage.classList.add('page-hovered'); } hoveredPage.value = foundPage; @@ -96,16 +135,21 @@ const handleIframeMouseMove = (event) => { // If not near page edge, check for content element hover if (!foundPage) { const contentElement = getContentElement(event.target); + const doc = event.target.ownerDocument; if (contentElement !== hoveredElement.value) { // Remove highlight from previous element (only if not selected) if (hoveredElement.value && hoveredElement.value !== selectedElement.value) { - hoveredElement.value.style.outline = ''; + hoveredElement.value.classList.remove('element-hovered'); } + // Remove previous label + removeElementLabel(doc); + // Add highlight to new element (only if not already selected) if (contentElement && contentElement !== selectedElement.value) { - contentElement.style.outline = `2px solid ${ELEMENT_HIGHLIGHT_COLOR}50`; + contentElement.classList.add('element-hovered'); + createElementLabel(contentElement); } hoveredElement.value = contentElement; @@ -113,16 +157,18 @@ const handleIframeMouseMove = (event) => { } else { // Clear element hover when hovering page edge if (hoveredElement.value && hoveredElement.value !== selectedElement.value) { - hoveredElement.value.style.outline = ''; + hoveredElement.value.classList.remove('element-hovered'); hoveredElement.value = null; } + // Remove label when hovering page edge + removeElementLabel(event.target.ownerDocument); } }; // Clear selection highlight from all selected pages const clearSelectedPages = () => { selectedPages.value.forEach((page) => { - page.style.outline = ''; + page.classList.remove('page-selected'); }); selectedPages.value = []; }; @@ -153,8 +199,9 @@ const getContentElement = (element) => { // Clear selected element highlight const clearSelectedElement = () => { if (selectedElement.value) { - selectedElement.value.style.outline = ''; - selectedElement.value.style.backgroundColor = ''; + selectedElement.value.classList.remove('element-selected'); + const doc = selectedElement.value.ownerDocument; + removeElementLabel(doc); selectedElement.value = null; } }; @@ -182,7 +229,7 @@ const handleIframeClick = (event) => { const doc = event.target.ownerDocument; const sameTemplatePages = getPagesWithSameTemplate(hoveredPage.value, doc); sameTemplatePages.forEach((page) => { - page.style.outline = `2px solid ${PAGE_HIGHLIGHT_COLOR}`; + page.classList.add('page-selected'); }); selectedPages.value = sameTemplatePages; @@ -219,8 +266,7 @@ const handleIframeClick = (event) => { // Select the new element selectedElement.value = contentElement; - contentElement.style.outline = `2px dashed ${ELEMENT_HIGHLIGHT_COLOR}`; - contentElement.style.backgroundColor = `${ELEMENT_HIGHLIGHT_COLOR}1A`; // 10% opacity + contentElement.classList.add('element-selected'); // Get count of similar elements const doc = event.target.ownerDocument; From 5b5c65722bf64aba8b36687f59bb3aeb7339fecd Mon Sep 17 00:00:00 2001 From: isUnknown Date: Mon, 8 Dec 2025 16:35:28 +0100 Subject: [PATCH 3/4] fix: improve element label positioning and state management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix element label positioning using getBoundingClientRect + scroll offset - Filter out state classes from element selectors (element-hovered, etc.) - Add cursor pointer on hovered elements and pages - Prevent elements from having both hovered and selected classes - Fix issue where closing popup left previous element in hovered state Ensures only one visual state per element at a time and cleaner selector display in labels. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- public/assets/css/pagedjs-interface.css | 2 + src/App.vue | 118 ++++++++++++++++++++---- src/components/ElementPopup.vue | 6 +- 3 files changed, 104 insertions(+), 22 deletions(-) diff --git a/public/assets/css/pagedjs-interface.css b/public/assets/css/pagedjs-interface.css index b4ad502..6a530f3 100644 --- a/public/assets/css/pagedjs-interface.css +++ b/public/assets/css/pagedjs-interface.css @@ -147,6 +147,7 @@ /* Hover and selection states for pages and elements */ .page-hovered { outline: 2px solid #ff8a5050 !important; + cursor: pointer !important; } .page-selected { @@ -155,6 +156,7 @@ .element-hovered { outline: 2px solid #7136ff50 !important; + cursor: pointer !important; } .element-selected { diff --git a/src/App.vue b/src/App.vue index 2826c7f..d6db086 100644 --- a/src/App.vue +++ b/src/App.vue @@ -41,9 +41,11 @@ 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 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 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; @@ -71,7 +73,10 @@ const getSelectorFromElement = (element) => { return `#${element.id}`; } const tagName = element.tagName.toLowerCase(); - const classes = Array.from(element.classList); + // Filter out state classes (element-hovered, element-selected, page-hovered, page-selected) + const classes = Array.from(element.classList).filter( + (cls) => !['element-hovered', 'element-selected', 'page-hovered', 'page-selected'].includes(cls) + ); if (classes.length > 0) { return `${tagName}.${classes[0]}`; } @@ -86,11 +91,15 @@ const createElementLabel = (element) => { existingLabel.remove(); } + const rect = element.getBoundingClientRect(); + const scrollTop = doc.documentElement.scrollTop || doc.body.scrollTop; + const scrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft; + const label = doc.createElement('div'); label.className = 'element-hover-label'; label.textContent = getSelectorFromElement(element); - label.style.top = `${element.offsetTop}px`; - label.style.left = `${element.offsetLeft}px`; + label.style.top = `${rect.top + scrollTop - 30}px`; + label.style.left = `${rect.left + scrollLeft}px`; doc.body.appendChild(label); return label; @@ -139,7 +148,10 @@ const handleIframeMouseMove = (event) => { if (contentElement !== hoveredElement.value) { // Remove highlight from previous element (only if not selected) - if (hoveredElement.value && hoveredElement.value !== selectedElement.value) { + if ( + hoveredElement.value && + hoveredElement.value !== selectedElement.value + ) { hoveredElement.value.classList.remove('element-hovered'); } @@ -156,7 +168,10 @@ const handleIframeMouseMove = (event) => { } } else { // Clear element hover when hovering page edge - if (hoveredElement.value && hoveredElement.value !== selectedElement.value) { + if ( + hoveredElement.value && + hoveredElement.value !== selectedElement.value + ) { hoveredElement.value.classList.remove('element-hovered'); hoveredElement.value = null; } @@ -175,13 +190,41 @@ const clearSelectedPages = () => { // 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' + '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) @@ -261,9 +304,25 @@ const handleIframeClick = (event) => { // Clear page selections clearSelectedPages(); + // If popup is already open and we're clicking another element, close it + if (elementPopup.value.visible) { + clearSelectedElement(); + elementPopup.value.close(); + pagePopup.value.close(); + return; + } + // Clear previous element selection clearSelectedElement(); + // Remove hovered class from the element we're about to select + contentElement.classList.remove('element-hovered'); + + // Clear the hoveredElement ref if it's the same as what we're selecting + if (hoveredElement.value === contentElement) { + hoveredElement.value = null; + } + // Select the new element selectedElement.value = contentElement; contentElement.classList.add('element-selected'); @@ -335,7 +394,10 @@ const renderPreview = async (shouldReloadFromFile = false) => { hiddenFrame.onload = () => { // Add event listeners for page and element interactions - hiddenFrame.contentDocument.addEventListener('mousemove', handleIframeMouseMove); + hiddenFrame.contentDocument.addEventListener( + 'mousemove', + handleIframeMouseMove + ); hiddenFrame.contentDocument.addEventListener('click', handleIframeClick); // Close Coloris when clicking in the iframe @@ -469,12 +531,26 @@ onMounted(() => renderPreview(true)); - - + + @@ -524,7 +600,9 @@ onMounted(() => renderPreview(true)); align-items: center; justify-content: center; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); - transition: transform 0.2s ease, box-shadow 0.2s ease; + transition: + transform 0.2s ease, + box-shadow 0.2s ease; z-index: 1000; } diff --git a/src/components/ElementPopup.vue b/src/components/ElementPopup.vue index a6ebd79..a8d9a73 100644 --- a/src/components/ElementPopup.vue +++ b/src/components/ElementPopup.vue @@ -337,8 +337,10 @@ const getSelectorFromElement = (element) => { // Get tag name const tagName = element.tagName.toLowerCase(); - // Get first class if available - const classes = Array.from(element.classList); + // Get first class if available (filter out state classes) + const classes = Array.from(element.classList).filter( + (cls) => !['element-hovered', 'element-selected', 'page-hovered', 'page-selected'].includes(cls) + ); if (classes.length > 0) { return `${tagName}.${classes[0]}`; } From 446b6cd9e77bb61ddfb6f140792df419613dcb2b Mon Sep 17 00:00:00 2001 From: isUnknown Date: Mon, 8 Dec 2025 16:41:13 +0100 Subject: [PATCH 4/4] feat: add page template hover label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add visual feedback for hovered page templates: - Display "@page {templateName}" label on page edge hover - Label positioned at top-left of page with 30% opacity - Orange background matching page highlight color - Automatically removed when hovering elements or clicking Ensures consistent UX between page and element hover states. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- public/assets/css/pagedjs-interface.css | 14 ++++++ src/App.vue | 61 +++++++++++++++++++++++-- 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/public/assets/css/pagedjs-interface.css b/public/assets/css/pagedjs-interface.css index 6a530f3..c03eeb7 100644 --- a/public/assets/css/pagedjs-interface.css +++ b/public/assets/css/pagedjs-interface.css @@ -178,6 +178,20 @@ font-family: sans-serif; } +.page-hover-label { + position: absolute; + background: #ff8a50; + color: white; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.875rem; + font-weight: 600; + opacity: 0.3; + pointer-events: none; + z-index: 9999; + font-family: sans-serif; +} + /* Marks (to delete when merge in paged.js) */ .pagedjs_marks-crop { diff --git a/src/App.vue b/src/App.vue index d6db086..5d129f6 100644 --- a/src/App.vue +++ b/src/App.vue @@ -75,7 +75,13 @@ const getSelectorFromElement = (element) => { const tagName = element.tagName.toLowerCase(); // Filter out state classes (element-hovered, element-selected, page-hovered, page-selected) const classes = Array.from(element.classList).filter( - (cls) => !['element-hovered', 'element-selected', 'page-hovered', 'page-selected'].includes(cls) + (cls) => + ![ + 'element-hovered', + 'element-selected', + 'page-hovered', + 'page-selected', + ].includes(cls) ); if (classes.length > 0) { return `${tagName}.${classes[0]}`; @@ -98,7 +104,7 @@ const createElementLabel = (element) => { const label = doc.createElement('div'); label.className = 'element-hover-label'; label.textContent = getSelectorFromElement(element); - label.style.top = `${rect.top + scrollTop - 30}px`; + label.style.top = `${rect.top + scrollTop - 32}px`; label.style.left = `${rect.left + scrollLeft}px`; doc.body.appendChild(label); @@ -113,6 +119,38 @@ const removeElementLabel = (doc) => { } }; +// Create and position page label on hover +const createPageLabel = (page) => { + const doc = page.ownerDocument; + const existingLabel = doc.querySelector('.page-hover-label'); + if (existingLabel) { + existingLabel.remove(); + } + + const rect = page.getBoundingClientRect(); + const scrollTop = doc.documentElement.scrollTop || doc.body.scrollTop; + const scrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft; + + const templateName = page.getAttribute('data-page-type') || 'default'; + + const label = doc.createElement('div'); + label.className = 'page-hover-label'; + label.textContent = `@page ${templateName}`; + label.style.top = `${rect.top + scrollTop - 32}px`; + label.style.left = `${rect.left + scrollLeft}px`; + + doc.body.appendChild(label); + return label; +}; + +// Remove page label +const removePageLabel = (doc) => { + const label = doc.querySelector('.page-hover-label'); + if (label) { + label.remove(); + } +}; + // Handle mouse movement in iframe const handleIframeMouseMove = (event) => { const pages = event.target.ownerDocument.querySelectorAll('.pagedjs_page'); @@ -133,9 +171,13 @@ const handleIframeMouseMove = (event) => { hoveredPage.value.classList.remove('page-hovered'); } + // Remove previous page label + removePageLabel(event.target.ownerDocument); + // Add highlight to new page (only if not already selected) if (foundPage && !selectedPages.value.includes(foundPage)) { foundPage.classList.add('page-hovered'); + createPageLabel(foundPage); } hoveredPage.value = foundPage; @@ -155,8 +197,9 @@ const handleIframeMouseMove = (event) => { hoveredElement.value.classList.remove('element-hovered'); } - // Remove previous label + // Remove previous labels removeElementLabel(doc); + removePageLabel(doc); // Add highlight to new element (only if not already selected) if (contentElement && contentElement !== selectedElement.value) { @@ -175,7 +218,7 @@ const handleIframeMouseMove = (event) => { hoveredElement.value.classList.remove('element-hovered'); hoveredElement.value = null; } - // Remove label when hovering page edge + // Remove element label when hovering page edge removeElementLabel(event.target.ownerDocument); } }; @@ -276,6 +319,10 @@ const handleIframeClick = (event) => { }); selectedPages.value = sameTemplatePages; + // Remove labels when opening popup + removePageLabel(doc); + removeElementLabel(doc); + pagePopup.value.open(hoveredPage.value, event, sameTemplatePages.length); elementPopup.value.close(); return; @@ -323,12 +370,16 @@ const handleIframeClick = (event) => { hoveredElement.value = null; } + // Get document and remove labels when opening popup + const doc = event.target.ownerDocument; + removeElementLabel(doc); + removePageLabel(doc); + // Select the new element selectedElement.value = contentElement; contentElement.classList.add('element-selected'); // Get count of similar elements - const doc = event.target.ownerDocument; const count = getSimilarElementsCount(contentElement, doc); elementPopup.value.handleIframeClick(event, contentElement, count);