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 @@