diff --git a/public/assets/css/pagedjs-interface.css b/public/assets/css/pagedjs-interface.css
index ad8fec3..41c3268 100644
--- a/public/assets/css/pagedjs-interface.css
+++ b/public/assets/css/pagedjs-interface.css
@@ -193,13 +193,15 @@
}
.block-image.element-hovered,
-.geoformat-cover-image.element-hovered {
+.geoformat-cover-image.element-hovered,
+.block-carte.element-hovered {
outline: 2px solid #0d996050 !important;
cursor: pointer !important;
}
.block-image.element-selected,
-.geoformat-cover-image.element-selected {
+.geoformat-cover-image.element-selected,
+.block-carte.element-selected {
outline: 2px dashed #0d9960 !important;
}
@@ -217,6 +219,20 @@
font-family: sans-serif;
}
+.carte-hover-label {
+ position: absolute;
+ background: #0d9960;
+ 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/public/assets/svg/carte.svg b/public/assets/svg/carte.svg
new file mode 100644
index 0000000..585776d
--- /dev/null
+++ b/public/assets/svg/carte.svg
@@ -0,0 +1 @@
+
diff --git a/src/App.vue b/src/App.vue
index 0aaed16..78bd81a 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -3,6 +3,7 @@ import PagedJsWrapper from './components/PagedJsWrapper.vue';
import EditorPanel from './components/editor/EditorPanel.vue';
import ElementPopup from './components/ElementPopup.vue';
import ImagePopup from './components/ImagePopup.vue';
+import CartePopup from './components/CartePopup.vue';
// import PagePopup from './components/PagePopup.vue'; // DISABLED: page template styling feature
import PreviewLoader from './components/PreviewLoader.vue';
import SaveButton from './components/SaveButton.vue';
@@ -25,6 +26,7 @@ const previewFrame1 = ref(null);
const previewFrame2 = ref(null);
const elementPopup = ref(null);
const imagePopup = ref(null);
+const cartePopup = ref(null);
// const pagePopup = ref(null); // DISABLED: page template styling feature
const activeTab = ref('');
@@ -41,7 +43,8 @@ const {
// handlePagePopupClose, // DISABLED: page template styling feature
handleElementPopupClose,
handleImagePopupClose,
-} = useIframeInteractions({ elementPopup, imagePopup });
+ handleCartePopupClose,
+} = useIframeInteractions({ elementPopup, imagePopup, cartePopup });
// Setup preview renderer with double buffering
const {
@@ -134,6 +137,11 @@ onMounted(async () => {
ref="imagePopup"
@close="handleImagePopupClose"
/>
+
+
+
+
@@ -72,6 +73,7 @@ import { inject } from 'vue';
import PageSettings from './PageSettings.vue';
import TextSettings from './TextSettings.vue';
import ImageSettings from './ImageSettings.vue';
+import CarteSettings from './CarteSettings.vue';
import StylesheetViewer from '../StylesheetViewer.vue';
const activeTab = inject('activeTab');
diff --git a/src/composables/useCarteDefaults.js b/src/composables/useCarteDefaults.js
new file mode 100644
index 0000000..bc84f91
--- /dev/null
+++ b/src/composables/useCarteDefaults.js
@@ -0,0 +1,13 @@
+import { reactive } from 'vue';
+import { CARTE_DEFAULTS } from '../utils/defaults';
+
+// Singleton reactive — CarteSettings writes here, CartePopup reads when toggle is disabled
+const defaults = reactive({
+ width: { ...CARTE_DEFAULTS.width },
+ height: { auto: CARTE_DEFAULTS.height.auto, value: CARTE_DEFAULTS.height.value, unit: CARTE_DEFAULTS.height.unit },
+ _initialized: false,
+});
+
+export function useCarteDefaults() {
+ return defaults;
+}
diff --git a/src/composables/useIframeInteractions.js b/src/composables/useIframeInteractions.js
index dbf5e25..76ea3c7 100644
--- a/src/composables/useIframeInteractions.js
+++ b/src/composables/useIframeInteractions.js
@@ -4,14 +4,16 @@ import { ref } from 'vue';
* Composable for managing interactions with pages and elements in the iframe
* Handles hover effects, labels, and click events for both pages and content elements
*/
-export function useIframeInteractions({ elementPopup, imagePopup /*, pagePopup // DISABLED: page template styling feature */ }) {
+export function useIframeInteractions({ elementPopup, imagePopup, cartePopup /*, pagePopup // DISABLED: page template styling feature */ }) {
// DISABLED: page template styling feature
// const hoveredPage = ref(null);
// const selectedPages = ref([]); // Pages with active border (when popup is open)
- const hoveredElement = ref(null); // Currently hovered content element
- const hoveredFigure = ref(null); // Currently hovered block-image figure
- const selectedElement = ref(null); // Selected element (when popup is open)
- const selectedFigure = ref(null); // Selected figure (when image popup is open)
+ const hoveredElement = ref(null);
+ const hoveredFigure = ref(null); // block-image or geoformat-cover-image
+ const hoveredCarte = ref(null); // block-carte
+ const selectedElement = ref(null);
+ const selectedFigure = ref(null); // selected block-image
+ const selectedCarte = ref(null); // selected block-carte
// const EDGE_THRESHOLD = 30; // px from edge to trigger hover // DISABLED: page template styling feature
// Text elements that can trigger ElementPopup (excluding containers, images, etc.)
@@ -197,6 +199,42 @@ export function useIframeInteractions({ elementPopup, imagePopup /*, pagePopup /
return null;
};
+ // Find closest block-carte figure ancestor
+ const getCarteFigure = (element) => {
+ let current = element;
+ while (current && current.tagName !== 'BODY') {
+ if (current.tagName === 'FIGURE' && current.classList.contains('block-carte')) return current;
+ current = current.parentElement;
+ }
+ return null;
+ };
+
+ // Create and position carte label on hover
+ const createCarteLabel = (figure) => {
+ const doc = figure.ownerDocument;
+ const existing = doc.querySelector('.carte-hover-label');
+ if (existing) existing.remove();
+
+ const uniqueClass = Array.from(figure.classList).find(cls => cls.startsWith('block-carte--'));
+ const labelText = uniqueClass ? `.${uniqueClass}` : '.block-carte';
+
+ const rect = figure.getBoundingClientRect();
+ const scrollTop = doc.documentElement.scrollTop || doc.body.scrollTop;
+ const scrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft;
+
+ const label = doc.createElement('div');
+ label.className = 'carte-hover-label';
+ label.textContent = labelText;
+ label.style.top = `${rect.top + scrollTop - 32}px`;
+ label.style.left = `${rect.left + scrollLeft}px`;
+ doc.body.appendChild(label);
+ };
+
+ const removeCarteLabel = (doc) => {
+ const label = doc.querySelector('.carte-hover-label');
+ if (label) label.remove();
+ };
+
// Check if element is a content element (or find closest content parent)
const getContentElement = (element) => {
let current = element;
@@ -285,8 +323,22 @@ export function useIframeInteractions({ elementPopup, imagePopup /*, pagePopup /
hoveredFigure.value = imageFigure;
}
+ // Check for block-carte figure hover
+ const carteFigure = (!imageFigure) ? getCarteFigure(event.target) : null;
+ if (carteFigure !== hoveredCarte.value) {
+ if (hoveredCarte.value && hoveredCarte.value !== selectedCarte.value) {
+ hoveredCarte.value.classList.remove('element-hovered');
+ }
+ removeCarteLabel(doc);
+ if (carteFigure && carteFigure !== selectedCarte.value) {
+ carteFigure.classList.add('element-hovered');
+ createCarteLabel(carteFigure);
+ }
+ hoveredCarte.value = carteFigure;
+ }
+
// Check for content element hover
- const contentElement = imageFigure ? null : getContentElement(event.target);
+ const contentElement = (imageFigure || carteFigure) ? null : getContentElement(event.target);
if (contentElement !== hoveredElement.value) {
// Remove highlight from previous element (only if not selected)
@@ -347,6 +399,7 @@ export function useIframeInteractions({ elementPopup, imagePopup /*, pagePopup /
clearSelectedElement();
if (elementPopup.value.visible) elementPopup.value.close();
imagePopup.value?.close();
+ cartePopup.value?.close();
return;
}
@@ -376,8 +429,31 @@ export function useIframeInteractions({ elementPopup, imagePopup /*, pagePopup /
return;
}
- // Close image popup if open when clicking a text element
+ // Check if clicking a block-carte figure
+ const carteFigure = getCarteFigure(element);
+ if (carteFigure) {
+ clearSelectedElement();
+ if (elementPopup.value.visible) elementPopup.value.close();
+ imagePopup.value?.close();
+ const doc2 = event.target.ownerDocument;
+ removeElementLabel(doc2);
+ removeCarteLabel(doc2);
+ if (cartePopup.value?.visible) {
+ if (selectedCarte.value) { selectedCarte.value.classList.remove('element-selected'); selectedCarte.value = null; }
+ cartePopup.value.close();
+ return;
+ }
+ carteFigure.classList.remove('element-hovered');
+ carteFigure.classList.add('element-selected');
+ hoveredCarte.value = null;
+ selectedCarte.value = carteFigure;
+ cartePopup.value?.handleCarteClick(carteFigure, event);
+ return;
+ }
+
+ // Close image and carte popups when clicking a text element
imagePopup.value?.close();
+ cartePopup.value?.close();
// Only show ElementPopup for content elements, not divs
const contentElement = getContentElement(element);
@@ -437,6 +513,13 @@ export function useIframeInteractions({ elementPopup, imagePopup /*, pagePopup /
}
};
+ const handleCartePopupClose = () => {
+ if (selectedCarte.value) {
+ selectedCarte.value.classList.remove('element-selected');
+ selectedCarte.value = null;
+ }
+ };
+
return {
// State
// hoveredPage, // DISABLED: page template styling feature
@@ -449,6 +532,7 @@ export function useIframeInteractions({ elementPopup, imagePopup /*, pagePopup /
// handlePagePopupClose, // DISABLED: page template styling feature
handleElementPopupClose,
handleImagePopupClose,
+ handleCartePopupClose,
// Utilities
// clearSelectedPages, // DISABLED: page template styling feature
clearSelectedElement,
diff --git a/src/stores/stylesheet.js b/src/stores/stylesheet.js
index bc59568..000436e 100644
--- a/src/stores/stylesheet.js
+++ b/src/stores/stylesheet.js
@@ -5,7 +5,7 @@ import * as cssComments from '../utils/css-comments';
import prettier from 'prettier/standalone';
import parserPostcss from 'prettier/plugins/postcss';
import { getCsrfToken } from '../utils/kirby-auth';
-import { PAGE_DEFAULTS, TEXT_DEFAULTS, IMAGE_DEFAULTS, HEADING_DEFAULTS, INLINE_DEFAULTS, PARAGRAPH_CLASS_DEFAULTS } from '../utils/defaults';
+import { PAGE_DEFAULTS, TEXT_DEFAULTS, IMAGE_DEFAULTS, CARTE_DEFAULTS, HEADING_DEFAULTS, INLINE_DEFAULTS, PARAGRAPH_CLASS_DEFAULTS } from '../utils/defaults';
export const useStylesheetStore = defineStore('stylesheet', () => {
// Base state
@@ -268,7 +268,7 @@ export const useStylesheetStore = defineStore('stylesheet', () => {
set('.geoformat-cover-image', 'height', 'auto');
set('.geoformat-cover-image img', 'object-fit', 'cover');
set('.geoformat-cover-image img', 'display', 'flex');
- set('.block-carte', 'width', IMAGE_DEFAULTS.width.value, IMAGE_DEFAULTS.width.unit);
+ set('.block-carte', 'width', CARTE_DEFAULTS.width.value, CARTE_DEFAULTS.width.unit);
set('.block-carte', 'height', 'auto');
set('.block-carte img', 'object-fit', 'cover');
set('.block-carte img', 'display', 'flex');
diff --git a/src/utils/defaults.js b/src/utils/defaults.js
index 6171278..daa3a47 100644
--- a/src/utils/defaults.js
+++ b/src/utils/defaults.js
@@ -131,6 +131,11 @@ export const IMAGE_DEFAULTS = Object.freeze({
height: Object.freeze({ auto: true, value: 400, unit: 'px' }),
});
+export const CARTE_DEFAULTS = Object.freeze({
+ width: Object.freeze({ value: 100, unit: '%' }),
+ height: Object.freeze({ auto: true, value: 400, unit: 'px' }),
+});
+
export const INLINE_DEFAULTS = Object.freeze({
em: Object.freeze({ fontStyle: 'italic' }),
i: Object.freeze({ fontStyle: 'italic' }),