diff --git a/public/assets/css/src/_forms.scss b/public/assets/css/src/_forms.scss index 585a9ca..75f67da 100644 --- a/public/assets/css/src/_forms.scss +++ b/public/assets/css/src/_forms.scss @@ -8,6 +8,41 @@ input[type="number"] { opacity: 0.3; } +/* Label with CSS tooltip */ +.label-with-tooltip { + text-decoration: underline dotted; + text-decoration-color: var(--color-browngray-200); + text-underline-offset: 2px; + cursor: help; + position: relative; + + &::after { + content: attr(data-css); + position: absolute; + bottom: 100%; + left: 0; + margin-bottom: 4px; + padding: 0.25rem 0.5rem; + background: var(--color-browngray-700); + color: var(--color-browngray-100); + font-family: "Courier New", Courier, monospace; + font-size: 0.75rem; + border-radius: 4px; + white-space: nowrap; + opacity: 0; + visibility: hidden; + transition: + opacity 0.15s ease, + visibility 0.15s ease; + z-index: 10; + } + + &:hover::after { + opacity: 1; + visibility: visible; + } +} + .settings-section { h2 { border-bottom: 1px solid #000; diff --git a/public/assets/css/style.css b/public/assets/css/style.css index a7c9ce8..4be3450 100644 --- a/public/assets/css/style.css +++ b/public/assets/css/style.css @@ -80,6 +80,38 @@ input[type=number] { opacity: 0.3; } +/* Label with CSS tooltip */ +.label-with-tooltip { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + text-decoration-color: var(--color-browngray-200); + text-underline-offset: 2px; + cursor: help; + position: relative; +} +.label-with-tooltip::after { + content: attr(data-css); + position: absolute; + bottom: 100%; + left: 0; + margin-bottom: 4px; + padding: 0.25rem 0.5rem; + background: var(--color-browngray-700); + color: var(--color-browngray-100); + font-family: "Courier New", Courier, monospace; + font-size: 0.75rem; + border-radius: 4px; + white-space: nowrap; + opacity: 0; + visibility: hidden; + transition: opacity 0.15s ease, visibility 0.15s ease; + z-index: 10; +} +.label-with-tooltip:hover::after { + opacity: 1; + visibility: visible; +} + .settings-section h2 { border-bottom: 1px solid #000; margin-bottom: var(--space-xs); diff --git a/public/assets/css/style.css.map b/public/assets/css/style.css.map index 5e35a94..6140ba4 100644 --- a/public/assets/css/style.css.map +++ b/public/assets/css/style.css.map @@ -1 +1 @@ -{"version":3,"sources":["src/_reset.scss","style.css","src/_variables.scss","src/_text.scss","src/_print-styles.scss","src/_forms.scss","src/_buttons.scss"],"names":[],"mappings":"AAAA;;EAEE,UAAA;EACA,SAAA;ACCF;;ADEA;;;;;;EAME,SAAA;ACCF;;ADEA;;EAEE,YAAA;EACA,aAAA;EAEA,mCAAA;ACAF;;ADGA;EACE,6BAAA;EACA,YAAA;ACAF;;ACzBA;EACE,yBAAA;EACA,8BAAA;EACA,8BAAA;EACA,8BAAA;EAEA,uBAAA;EAEA,kBAAA;EAEA,uCAAA;ADyBF;;AEnCA;;;;;;;;;;;;;EAaE,uBAAA;AFsCF;;AGnDA,yBAAA;AACA;EACE,QAAA;EACA,2BAAA;AHsDF;AGpDA;EACE,8BAAA;OAAA,kBAAA;AHsDF;;AGnDA;EACE;IACE,sBAAA;EHsDF;AACF;AGpDA;EACE,+BAAA;AHsDF;;AIrEA;;;EAGE,4CAAA;AJwEF;;AIrEA;EACE,YAAA;AJwEF;;AIpEE;EACE,6BAAA;EACA,8BAAA;AJuEJ;AIrEE;EACE,mDAAA;AJuEJ;AIpEE;EACE,0BAAA;AJsEJ;AIpEI;EACE,8BAAA;AJsEN;AInEI;EACE,aAAA;AJqEN;AInEM;;EAEE,UAAA;AJqER;AIlEM;EACE,oCAAA;AJoER;AIjEM;EACE,aAAA;EACA,WAAA;AJmER;AIjEQ;EACE,aAAA;EACA,WAAA;AJmEV;AI/DM;EACE,UAAA;AJiER;AIhEQ;EACE,aAAA;AJkEV;AIjEU;EACE,kBAAA;EACA,eAAA;EACA,cAAA;EACA,SAAA;EACA,WAAA;EACA,eAAA;AJmEZ;AIhEU;EACE,oBAAA;EACA,WAAA;AJkEZ;AI5DI;EACE,aAAA;EACA,eAAA;EACA,wBAAA;AJ8DN;AI5DM;EACE,WAAA;AJ8DR;AI5DM;EACE,UAAA;AJ8DR;AI5DQ;EACE,UAAA;AJ8DV;AI1DU;EACE,UAAA;AJ4DZ;;AKlJA;EACE,eAAA;EAEA,4CAAA;EACA,iCAAA;EACA,uCAAA;EACA,mCAAA;EACA,sBAAA;ALoJF;AKlJE;EACE,sBAAA;EACA,WAAA;ALoJJ;AKhJI;EACE,sBAAA;EACA,WAAA;EACA,YAAA;ALkJN","file":"style.css"} \ No newline at end of file +{"version":3,"sources":["src/_reset.scss","style.css","src/_variables.scss","src/_text.scss","src/_print-styles.scss","src/_forms.scss","src/_buttons.scss"],"names":[],"mappings":"AAAA;;EAEE,UAAA;EACA,SAAA;ACCF;;ADEA;;;;;;EAME,SAAA;ACCF;;ADEA;;EAEE,YAAA;EACA,aAAA;EAEA,mCAAA;ACAF;;ADGA;EACE,6BAAA;EACA,YAAA;ACAF;;ACzBA;EACE,yBAAA;EACA,8BAAA;EACA,8BAAA;EACA,8BAAA;EAEA,uBAAA;EAEA,kBAAA;EAEA,uCAAA;ADyBF;;AEnCA;;;;;;;;;;;;;EAaE,uBAAA;AFsCF;;AGnDA,yBAAA;AACA;EACE,QAAA;EACA,2BAAA;AHsDF;AGpDA;EACE,8BAAA;OAAA,kBAAA;AHsDF;;AGnDA;EACE;IACE,sBAAA;EHsDF;AACF;AGpDA;EACE,+BAAA;AHsDF;;AIrEA;;;EAGE,4CAAA;AJwEF;;AIrEA;EACE,YAAA;AJwEF;;AIrEA,2BAAA;AACA;EACE,yCAAA;UAAA,iCAAA;EACA,iDAAA;EACA,0BAAA;EACA,YAAA;EACA,kBAAA;AJwEF;AItEE;EACE,uBAAA;EACA,kBAAA;EACA,YAAA;EACA,OAAA;EACA,kBAAA;EACA,uBAAA;EACA,sCAAA;EACA,iCAAA;EACA,8CAAA;EACA,kBAAA;EACA,kBAAA;EACA,mBAAA;EACA,UAAA;EACA,kBAAA;EACA,qDACE;EAEF,WAAA;AJsEJ;AInEE;EACE,UAAA;EACA,mBAAA;AJqEJ;;AIhEE;EACE,6BAAA;EACA,8BAAA;AJmEJ;AIjEE;EACE,mDAAA;AJmEJ;AIhEE;EACE,0BAAA;AJkEJ;AIhEI;EACE,8BAAA;AJkEN;AI/DI;EACE,aAAA;AJiEN;AI/DM;;EAEE,UAAA;AJiER;AI9DM;EACE,oCAAA;AJgER;AI7DM;EACE,aAAA;EACA,WAAA;AJ+DR;AI7DQ;EACE,aAAA;EACA,WAAA;AJ+DV;AI3DM;EACE,UAAA;AJ6DR;AI5DQ;EACE,aAAA;AJ8DV;AI7DU;EACE,kBAAA;EACA,eAAA;EACA,cAAA;EACA,SAAA;EACA,WAAA;EACA,eAAA;AJ+DZ;AI5DU;EACE,oBAAA;EACA,WAAA;AJ8DZ;AIxDI;EACE,aAAA;EACA,eAAA;EACA,wBAAA;AJ0DN;AIxDM;EACE,WAAA;AJ0DR;AIxDM;EACE,UAAA;AJ0DR;AIxDQ;EACE,UAAA;AJ0DV;AItDU;EACE,UAAA;AJwDZ;;AKjLA;EACE,eAAA;EAEA,4CAAA;EACA,iCAAA;EACA,uCAAA;EACA,mCAAA;EACA,sBAAA;ALmLF;AKjLE;EACE,sBAAA;EACA,WAAA;ALmLJ;AK/KI;EACE,sBAAA;EACA,WAAA;EACA,YAAA;ALiLN","file":"style.css"} \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index 1b8746a..59bee17 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,6 +2,7 @@ import PagedJsWrapper from './components/PagedJsWrapper.vue'; import EditorPanel from './components/editor/EditorPanel.vue'; import ElementPopup from './components/ElementPopup.vue'; +import PagePopup from './components/PagePopup.vue'; import PreviewLoader from './components/PreviewLoader.vue'; import { onMounted, ref, watch, computed, provide } from 'vue'; import { useStylesheetStore } from './stores/stylesheet'; @@ -11,10 +12,15 @@ const stylesheetStore = useStylesheetStore(); const previewFrame1 = ref(null); const previewFrame2 = ref(null); const elementPopup = ref(null); +const pagePopup = ref(null); const activeTab = ref(''); provide('activeTab', activeTab); +// Page interaction state +const hoveredPage = ref(null); +const EDGE_THRESHOLD = 30; // px from edge to trigger hover + let savedScrollPercentage = 0; const currentFrameIndex = ref(1); // 1 or 2, which iframe is currently visible const isTransitioning = ref(false); @@ -25,6 +31,77 @@ const activeFrame = computed(() => { : previewFrame2.value; }); +// 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) + ); +}; + +// 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) { + // Remove highlight from previous page + if (hoveredPage.value) { + hoveredPage.value.style.outline = ''; + } + + // Add highlight to new page + if (foundPage) { + foundPage.style.outline = '2px solid rgba(97, 175, 239, 0.3)'; + } + + hoveredPage.value = foundPage; + } +}; + +// Handle click in iframe +const handleIframeClick = (event) => { + const element = event.target; + + // Check if clicking near a page edge + if (hoveredPage.value) { + event.stopPropagation(); + pagePopup.value.open(hoveredPage.value, event); + elementPopup.value.close(); + return; + } + + // Otherwise handle as element click + if (element.tagName === 'BODY' || element.tagName === 'HTML') { + elementPopup.value.close(); + pagePopup.value.close(); + return; + } + + elementPopup.value.handleIframeClick(event); + pagePopup.value.close(); +}; + const renderPreview = async (shouldReloadFromFile = false) => { if (isTransitioning.value) return; isTransitioning.value = true; @@ -73,10 +150,9 @@ const renderPreview = async (shouldReloadFromFile = false) => { `; hiddenFrame.onload = () => { - hiddenFrame.contentDocument.addEventListener( - 'click', - elementPopup.value.handleIframeClick - ); + // Add event listeners for page and element interactions + hiddenFrame.contentDocument.addEventListener('mousemove', handleIframeMouseMove); + hiddenFrame.contentDocument.addEventListener('click', handleIframeClick); // Close Coloris when clicking in the iframe hiddenFrame.contentDocument.addEventListener('click', () => { @@ -151,6 +227,7 @@ onMounted(() => renderPreview(true)); + diff --git a/src/components/PagePopup.vue b/src/components/PagePopup.vue new file mode 100644 index 0000000..804ab50 --- /dev/null +++ b/src/components/PagePopup.vue @@ -0,0 +1,807 @@ + + + + + diff --git a/src/components/StylesheetViewer.vue b/src/components/StylesheetViewer.vue index e8bc63d..4cd72d6 100644 --- a/src/components/StylesheetViewer.vue +++ b/src/components/StylesheetViewer.vue @@ -24,7 +24,7 @@