2025-11-24 16:51:55 +01:00
|
|
|
<template>
|
2026-02-26 15:35:45 +01:00
|
|
|
<BasePopup
|
|
|
|
|
ref="basePopup"
|
2025-11-24 16:51:55 +01:00
|
|
|
id="element-popup"
|
2026-02-26 15:35:45 +01:00
|
|
|
:display-css="displayedCss"
|
|
|
|
|
:editable-css="elementCss"
|
|
|
|
|
:popup-width="800"
|
|
|
|
|
:popup-height="600"
|
2026-03-05 11:42:18 +01:00
|
|
|
:show-inheritance="false"
|
2026-02-26 15:35:45 +01:00
|
|
|
@close="close"
|
|
|
|
|
@css-input="handleCssInput"
|
2025-11-24 16:51:55 +01:00
|
|
|
>
|
2026-02-26 15:35:45 +01:00
|
|
|
<template #header-left>
|
2026-02-26 15:47:58 +01:00
|
|
|
<span class="element-label">{{ selector || '' }}</span>
|
|
|
|
|
<span class="instance-count">{{ elementInstanceCount }} instances</span>
|
2026-02-26 15:35:45 +01:00
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<template #controls>
|
|
|
|
|
<!-- Font Family -->
|
|
|
|
|
<div class="settings-subsection">
|
2026-03-05 11:42:18 +01:00
|
|
|
<div class="field field-font" >
|
2026-02-26 15:35:45 +01:00
|
|
|
<label class="label-with-tooltip" data-css="font-family">Police</label>
|
2026-03-05 11:08:44 +01:00
|
|
|
<div class="field-font__options">
|
2026-03-05 11:42:18 +01:00
|
|
|
<select v-model="fontFamily" >
|
2026-02-26 15:35:45 +01:00
|
|
|
<option v-for="f in fonts" :key="f" :value="f">{{ f }}</option>
|
|
|
|
|
</select>
|
|
|
|
|
<div class="field-checkbox">
|
2026-03-05 11:42:18 +01:00
|
|
|
<input type="checkbox" v-model="italic" />
|
2026-02-26 15:35:45 +01:00
|
|
|
<label class="label-with-tooltip" data-css="font-style">Italique</label>
|
2025-12-11 13:39:23 +01:00
|
|
|
</div>
|
2026-03-05 11:08:44 +01:00
|
|
|
<div class="field-checkbox">
|
2026-03-05 11:42:18 +01:00
|
|
|
<input type="checkbox" v-model="bold" />
|
2026-03-05 11:08:44 +01:00
|
|
|
<label class="label-with-tooltip" data-css="font-weight">Gras</label>
|
|
|
|
|
</div>
|
2025-12-05 18:21:54 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-02-26 15:35:45 +01:00
|
|
|
</div>
|
2025-12-05 18:21:54 +01:00
|
|
|
|
2026-02-26 15:35:45 +01:00
|
|
|
<!-- Font Size -->
|
|
|
|
|
<div class="settings-subsection">
|
2026-03-05 11:42:18 +01:00
|
|
|
<div class="field field-text-size" >
|
2026-02-26 15:35:45 +01:00
|
|
|
<label class="label-with-tooltip" data-css="font-size">Taille du texte</label>
|
2026-03-05 11:42:18 +01:00
|
|
|
<InputWithUnit
|
|
|
|
|
v-model="fontSizeModel"
|
|
|
|
|
:units="['px']"
|
|
|
|
|
:min="6"
|
|
|
|
|
:max="72"
|
|
|
|
|
showRange
|
|
|
|
|
/>
|
2025-12-05 18:21:54 +01:00
|
|
|
</div>
|
2026-02-26 15:35:45 +01:00
|
|
|
</div>
|
2025-12-05 18:21:54 +01:00
|
|
|
|
2026-03-02 17:29:49 +01:00
|
|
|
|
|
|
|
|
<!-- LineHeight -->
|
|
|
|
|
<div class="settings-subsection">
|
2026-03-05 11:42:18 +01:00
|
|
|
<div class="field field-text-size" >
|
2026-03-02 17:29:49 +01:00
|
|
|
<label class="label-with-tooltip" data-css="line-height">Interlignage</label>
|
2026-03-05 11:42:18 +01:00
|
|
|
<InputWithUnit
|
|
|
|
|
v-model="lineHeightModel"
|
|
|
|
|
:units="['px']"
|
|
|
|
|
:min="0"
|
|
|
|
|
:max="72"
|
|
|
|
|
showRange
|
|
|
|
|
/>
|
2026-03-02 17:29:49 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
2026-02-26 15:35:45 +01:00
|
|
|
<!-- Text Alignment -->
|
|
|
|
|
<div class="settings-subsection">
|
2026-03-05 11:42:18 +01:00
|
|
|
<div class="field" >
|
2026-02-26 15:35:45 +01:00
|
|
|
<label class="label-with-tooltip" data-css="text-align">Alignement</label>
|
2026-03-05 11:42:18 +01:00
|
|
|
<select v-model="textAlign" >
|
2026-02-26 15:35:45 +01:00
|
|
|
<option value="left">Gauche</option>
|
|
|
|
|
<option value="center">Centre</option>
|
|
|
|
|
<option value="right">Droite</option>
|
|
|
|
|
<option value="justify">Justifié</option>
|
|
|
|
|
</select>
|
2025-12-05 18:21:54 +01:00
|
|
|
</div>
|
2026-02-26 15:35:45 +01:00
|
|
|
</div>
|
2025-12-05 18:21:54 +01:00
|
|
|
|
2026-03-05 11:08:44 +01:00
|
|
|
<!-- Bordure -->
|
|
|
|
|
<div class="settings-subsection">
|
2026-03-05 11:42:18 +01:00
|
|
|
<div class="field field-border" >
|
2026-03-05 11:08:44 +01:00
|
|
|
<div class="settings-subsection-header">
|
|
|
|
|
<label class="label-with-tooltip" data-css="border">Bordure</label>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="field-border__options">
|
|
|
|
|
<div class="field-border__option">
|
|
|
|
|
<label class="label-with-tooltip" data-css="border-width">Épaisseur</label>
|
|
|
|
|
<div class="input-with-unit">
|
|
|
|
|
<NumberInput
|
|
|
|
|
v-model="borderWidth.value"
|
|
|
|
|
:min="0"
|
|
|
|
|
:step="1"
|
2026-03-05 11:42:18 +01:00
|
|
|
/>
|
2026-03-05 11:08:44 +01:00
|
|
|
<div class="unit-toggle">
|
2026-03-05 11:42:18 +01:00
|
|
|
<button type="button" class="active" >px</button>
|
2026-03-05 11:08:44 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="field-border__option">
|
|
|
|
|
<label class="label-with-tooltip" data-css="border-style">Style</label>
|
2026-03-05 11:42:18 +01:00
|
|
|
<select v-model="borderStyle" >
|
2026-03-05 11:08:44 +01:00
|
|
|
<option value="solid">Plein</option>
|
|
|
|
|
<option value="dotted">Pointillés</option>
|
|
|
|
|
<option value="dashed">Tirets</option>
|
|
|
|
|
<option value="double">Double</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="field-border__option">
|
|
|
|
|
<label class="label-with-tooltip" data-css="border-color">Couleur</label>
|
|
|
|
|
<div class="input-with-color">
|
|
|
|
|
<input
|
|
|
|
|
ref="borderColorInput"
|
|
|
|
|
type="text"
|
|
|
|
|
v-model="borderColor"
|
2026-03-05 11:42:18 +01:00
|
|
|
data-coloris
|
2026-03-05 11:08:44 +01:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-26 15:35:45 +01:00
|
|
|
<!-- Color -->
|
|
|
|
|
<div class="settings-subsection">
|
2026-03-05 11:42:18 +01:00
|
|
|
<div class="field field-simple" >
|
2026-03-05 11:08:44 +01:00
|
|
|
<label class="label-with-tooltip" data-css="color">Couleur du texte</label>
|
2026-02-26 15:35:45 +01:00
|
|
|
<div class="input-with-color">
|
|
|
|
|
<input
|
|
|
|
|
ref="colorInput"
|
|
|
|
|
type="text"
|
2026-02-26 15:47:58 +01:00
|
|
|
v-model="color"
|
2026-03-05 11:42:18 +01:00
|
|
|
data-coloris
|
2026-02-26 15:35:45 +01:00
|
|
|
/>
|
2025-12-05 18:21:54 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-02-26 15:35:45 +01:00
|
|
|
</div>
|
2025-12-05 18:21:54 +01:00
|
|
|
|
2026-02-26 15:35:45 +01:00
|
|
|
<!-- Background -->
|
|
|
|
|
<div class="settings-subsection">
|
2026-03-05 11:42:18 +01:00
|
|
|
<div class="field field-simple" >
|
2026-02-26 15:35:45 +01:00
|
|
|
<label class="label-with-tooltip" data-css="background">Arrière-plan</label>
|
|
|
|
|
<div class="input-with-color">
|
|
|
|
|
<input
|
|
|
|
|
ref="backgroundInput"
|
|
|
|
|
type="text"
|
2026-02-26 15:47:58 +01:00
|
|
|
v-model="background"
|
2026-03-05 11:42:18 +01:00
|
|
|
data-coloris
|
2026-02-26 15:35:45 +01:00
|
|
|
/>
|
2025-12-05 18:21:54 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-02-26 15:35:45 +01:00
|
|
|
</div>
|
2025-12-05 18:21:54 +01:00
|
|
|
|
2026-02-26 15:35:45 +01:00
|
|
|
<!-- Outer Margins -->
|
|
|
|
|
<div class="settings-subsection">
|
2026-03-05 10:45:55 +01:00
|
|
|
<div class="settings-subsection-header">
|
|
|
|
|
<span class="label-with-tooltip" data-css="margin">Marges extérieures</span>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="lock-toggle"
|
|
|
|
|
:class="{ locked: marginLocked }"
|
2026-03-05 11:42:18 +01:00
|
|
|
@click="marginLocked = !marginLocked"
|
2026-03-05 10:45:55 +01:00
|
|
|
:title="marginLocked ? 'Déverrouiller (modifier indépendamment)' : 'Verrouiller (modifier ensemble)'"
|
|
|
|
|
>
|
|
|
|
|
<svg v-if="marginLocked" width="11" height="13" viewBox="0 0 11 13" fill="none">
|
|
|
|
|
<rect x="1" y="5.5" width="9" height="7" rx="1" fill="currentColor"/>
|
|
|
|
|
<path d="M2.5 5.5V3.5a3 3 0 0 1 6 0v2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
|
|
|
</svg>
|
|
|
|
|
<svg v-else width="11" height="13" viewBox="0 0 11 13" fill="none">
|
|
|
|
|
<rect x="1" y="5.5" width="9" height="7" rx="1" fill="currentColor"/>
|
|
|
|
|
<path d="M2.5 5.5V3.5a3 3 0 0 1 6 0" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
v-for="side in sides"
|
|
|
|
|
:key="side.key"
|
|
|
|
|
class="field field-margin"
|
2026-03-05 11:42:18 +01:00
|
|
|
>
|
2026-03-05 10:45:55 +01:00
|
|
|
<label class="label-with-tooltip" :data-css="`margin-${side.key}`">{{ side.label }}</label>
|
2026-02-26 15:35:45 +01:00
|
|
|
<div class="input-with-unit">
|
|
|
|
|
<NumberInput
|
2026-03-05 10:45:55 +01:00
|
|
|
:modelValue="margin[side.key].value"
|
2026-02-26 15:35:45 +01:00
|
|
|
:min="0"
|
|
|
|
|
:step="1"
|
2026-03-05 11:42:18 +01:00
|
|
|
@update:modelValue="(v) => updateMarginValue(side.key, v)"
|
2026-02-26 15:35:45 +01:00
|
|
|
/>
|
|
|
|
|
<div class="unit-toggle">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
2026-03-05 10:45:55 +01:00
|
|
|
:class="{ active: margin[side.key].unit === 'mm' }"
|
2026-03-05 11:42:18 +01:00
|
|
|
@click="updateMarginUnit(side.key, 'mm')"
|
2026-03-05 10:45:55 +01:00
|
|
|
>mm</button>
|
2026-02-26 15:35:45 +01:00
|
|
|
<button
|
|
|
|
|
type="button"
|
2026-03-05 10:45:55 +01:00
|
|
|
:class="{ active: margin[side.key].unit === 'px' }"
|
2026-03-05 11:42:18 +01:00
|
|
|
@click="updateMarginUnit(side.key, 'px')"
|
2026-03-05 10:45:55 +01:00
|
|
|
>px</button>
|
2025-12-05 18:21:54 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-11-24 16:51:55 +01:00
|
|
|
</div>
|
2025-12-05 18:21:54 +01:00
|
|
|
|
2026-02-26 15:35:45 +01:00
|
|
|
<!-- Inner Margins (Padding) -->
|
|
|
|
|
<div class="settings-subsection">
|
2026-03-05 10:45:55 +01:00
|
|
|
<div class="settings-subsection-header">
|
|
|
|
|
<span class="label-with-tooltip" data-css="padding">Marges intérieures</span>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="lock-toggle"
|
|
|
|
|
:class="{ locked: paddingLocked }"
|
2026-03-05 11:42:18 +01:00
|
|
|
@click="paddingLocked = !paddingLocked"
|
2026-03-05 10:45:55 +01:00
|
|
|
:title="paddingLocked ? 'Déverrouiller (modifier indépendamment)' : 'Verrouiller (modifier ensemble)'"
|
|
|
|
|
>
|
|
|
|
|
<svg v-if="paddingLocked" width="11" height="13" viewBox="0 0 11 13" fill="none">
|
|
|
|
|
<rect x="1" y="5.5" width="9" height="7" rx="1" fill="currentColor"/>
|
|
|
|
|
<path d="M2.5 5.5V3.5a3 3 0 0 1 6 0v2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
|
|
|
</svg>
|
|
|
|
|
<svg v-else width="11" height="13" viewBox="0 0 11 13" fill="none">
|
|
|
|
|
<rect x="1" y="5.5" width="9" height="7" rx="1" fill="currentColor"/>
|
|
|
|
|
<path d="M2.5 5.5V3.5a3 3 0 0 1 6 0" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
v-for="side in sides"
|
|
|
|
|
:key="side.key"
|
|
|
|
|
class="field field-margin"
|
2026-03-05 11:42:18 +01:00
|
|
|
>
|
2026-03-05 10:45:55 +01:00
|
|
|
<label class="label-with-tooltip" :data-css="`padding-${side.key}`">{{ side.label }}</label>
|
2026-02-26 15:35:45 +01:00
|
|
|
<div class="input-with-unit">
|
|
|
|
|
<NumberInput
|
2026-03-05 10:45:55 +01:00
|
|
|
:modelValue="padding[side.key].value"
|
2026-02-26 15:35:45 +01:00
|
|
|
:min="0"
|
|
|
|
|
:step="1"
|
2026-03-05 11:42:18 +01:00
|
|
|
@update:modelValue="(v) => updatePaddingValue(side.key, v)"
|
2025-12-05 18:21:54 +01:00
|
|
|
/>
|
2026-02-26 15:35:45 +01:00
|
|
|
<div class="unit-toggle">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
2026-03-05 10:45:55 +01:00
|
|
|
:class="{ active: padding[side.key].unit === 'mm' }"
|
2026-03-05 11:42:18 +01:00
|
|
|
@click="updatePaddingUnit(side.key, 'mm')"
|
2026-03-05 10:45:55 +01:00
|
|
|
>mm</button>
|
2026-02-26 15:35:45 +01:00
|
|
|
<button
|
|
|
|
|
type="button"
|
2026-03-05 10:45:55 +01:00
|
|
|
:class="{ active: padding[side.key].unit === 'px' }"
|
2026-03-05 11:42:18 +01:00
|
|
|
@click="updatePaddingUnit(side.key, 'px')"
|
2026-03-05 10:45:55 +01:00
|
|
|
>px</button>
|
2026-02-26 15:35:45 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-05 18:21:54 +01:00
|
|
|
</div>
|
2025-11-24 16:51:55 +01:00
|
|
|
</div>
|
2026-02-26 15:35:45 +01:00
|
|
|
</template>
|
|
|
|
|
</BasePopup>
|
2025-11-24 16:51:55 +01:00
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2026-02-26 15:47:58 +01:00
|
|
|
import { ref, reactive, computed, watch, nextTick } from 'vue';
|
2025-11-24 17:55:42 +01:00
|
|
|
import { useStylesheetStore } from '../stores/stylesheet';
|
2025-12-10 11:51:53 +01:00
|
|
|
import { useDebounce } from '../composables/useDebounce';
|
2025-12-09 17:08:40 +01:00
|
|
|
import NumberInput from './ui/NumberInput.vue';
|
2026-03-05 11:42:18 +01:00
|
|
|
import InputWithUnit from './ui/InputWithUnit.vue';
|
2026-02-26 15:35:45 +01:00
|
|
|
import BasePopup from './ui/BasePopup.vue';
|
2026-02-24 14:08:30 +01:00
|
|
|
import { convertUnit } from '../utils/unit-conversion';
|
2025-11-24 16:51:55 +01:00
|
|
|
|
2025-11-24 17:55:42 +01:00
|
|
|
const stylesheetStore = useStylesheetStore();
|
|
|
|
|
|
2025-11-24 16:51:55 +01:00
|
|
|
const props = defineProps({
|
2025-12-04 15:55:52 +01:00
|
|
|
iframeRef: Object,
|
2025-11-24 17:55:42 +01:00
|
|
|
});
|
|
|
|
|
|
2025-12-08 14:15:06 +01:00
|
|
|
const emit = defineEmits(['close']);
|
|
|
|
|
|
2026-02-26 15:35:45 +01:00
|
|
|
const basePopup = ref(null);
|
2025-12-05 18:21:54 +01:00
|
|
|
|
2026-02-26 15:35:45 +01:00
|
|
|
const visible = computed(() => basePopup.value?.visible ?? false);
|
2025-12-05 18:21:54 +01:00
|
|
|
|
2025-11-24 18:18:27 +01:00
|
|
|
const selector = ref('');
|
2025-12-05 18:21:54 +01:00
|
|
|
const selectedElement = ref(null);
|
2026-02-26 15:35:45 +01:00
|
|
|
const elementInstanceCount = ref(0);
|
2025-12-05 18:21:54 +01:00
|
|
|
const colorInput = ref(null);
|
|
|
|
|
const backgroundInput = ref(null);
|
2026-03-05 11:08:44 +01:00
|
|
|
const borderColorInput = ref(null);
|
2025-12-05 18:21:54 +01:00
|
|
|
|
|
|
|
|
let isUpdatingFromStore = false;
|
2025-12-10 11:51:53 +01:00
|
|
|
const { debouncedUpdate } = useDebounce(500);
|
2025-12-05 18:21:54 +01:00
|
|
|
|
2026-02-26 15:47:58 +01:00
|
|
|
// Style properties — flat refs for simple values, reactive for value+unit
|
|
|
|
|
const fontFamily = ref('Alegreya Sans');
|
|
|
|
|
const italic = ref(false);
|
2026-03-05 11:08:44 +01:00
|
|
|
const bold = ref(false);
|
2026-02-26 15:47:58 +01:00
|
|
|
const textAlign = ref('left');
|
|
|
|
|
const color = ref('rgb(0, 0, 0)');
|
|
|
|
|
const background = ref('transparent');
|
|
|
|
|
const fontSize = reactive({ value: 23, unit: 'px' });
|
2026-03-05 11:42:18 +01:00
|
|
|
const fontSizeModel = computed({
|
|
|
|
|
get: () => ({ value: fontSize.value, unit: fontSize.unit }),
|
|
|
|
|
set: (v) => { fontSize.value = v.value; fontSize.unit = v.unit; },
|
|
|
|
|
});
|
2026-03-02 17:29:49 +01:00
|
|
|
const lineHeight = reactive({ value: 28, unit: 'px' });
|
2026-03-05 11:42:18 +01:00
|
|
|
const lineHeightModel = computed({
|
|
|
|
|
get: () => ({ value: lineHeight.value, unit: lineHeight.unit }),
|
|
|
|
|
set: (v) => { lineHeight.value = v.value; lineHeight.unit = v.unit; },
|
|
|
|
|
});
|
2026-03-05 11:08:44 +01:00
|
|
|
const borderWidth = reactive({ value: 0, unit: 'px' });
|
|
|
|
|
const borderStyle = ref('solid');
|
|
|
|
|
const borderColor = ref('#000000');
|
2026-03-05 10:45:55 +01:00
|
|
|
|
|
|
|
|
const marginLocked = ref(true);
|
|
|
|
|
const margin = reactive({
|
|
|
|
|
top: { value: 0, unit: 'mm' },
|
|
|
|
|
right: { value: 0, unit: 'mm' },
|
|
|
|
|
bottom: { value: 0, unit: 'mm' },
|
|
|
|
|
left: { value: 0, unit: 'mm' },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const paddingLocked = ref(true);
|
|
|
|
|
const padding = reactive({
|
|
|
|
|
top: { value: 0, unit: 'mm' },
|
|
|
|
|
right: { value: 0, unit: 'mm' },
|
|
|
|
|
bottom: { value: 0, unit: 'mm' },
|
|
|
|
|
left: { value: 0, unit: 'mm' },
|
|
|
|
|
});
|
2025-12-05 18:21:54 +01:00
|
|
|
|
2025-12-11 13:39:23 +01:00
|
|
|
// Constants
|
|
|
|
|
const fonts = ['Alegreya Sans', 'Alegreya', 'Arial', 'Georgia', 'Times New Roman'];
|
|
|
|
|
|
2026-02-26 15:47:58 +01:00
|
|
|
// Style property descriptors
|
|
|
|
|
const styleProps = [
|
|
|
|
|
{ css: 'font-family', get: () => fontFamily.value, set: v => fontFamily.value = v.replace(/['"]/g, ''), debounce: false },
|
|
|
|
|
{ css: 'font-style', get: () => italic.value ? 'italic' : 'normal', set: v => italic.value = v === 'italic', debounce: false },
|
2026-03-05 11:08:44 +01:00
|
|
|
{ css: 'font-weight', get: () => bold.value ? 'bold' : 'normal', set: v => bold.value = v === 'bold' || parseInt(v) >= 700, debounce: false },
|
2026-02-26 15:47:58 +01:00
|
|
|
{ css: 'text-align', get: () => textAlign.value, set: v => textAlign.value = v, debounce: false },
|
|
|
|
|
{ css: 'color', get: () => color.value, set: v => color.value = v, debounce: true },
|
|
|
|
|
{ css: 'background', get: () => background.value, set: v => background.value = v, debounce: true },
|
2026-03-05 11:08:44 +01:00
|
|
|
{ css: 'border-style', get: () => borderStyle.value, set: v => borderStyle.value = v || 'solid', debounce: false },
|
|
|
|
|
{ css: 'border-color', get: () => borderColor.value, set: v => borderColor.value = v, debounce: true },
|
2026-02-26 15:47:58 +01:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const unitProps = [
|
|
|
|
|
{ css: 'font-size', ref: fontSize, debounce: true },
|
2026-03-02 17:29:49 +01:00
|
|
|
{ css: 'line-height', ref: lineHeight, debounce: true },
|
2026-03-05 11:08:44 +01:00
|
|
|
{ css: 'border-width', ref: borderWidth, debounce: true },
|
2026-02-26 15:47:58 +01:00
|
|
|
];
|
|
|
|
|
|
2026-03-05 10:45:55 +01:00
|
|
|
const sides = [
|
|
|
|
|
{ key: 'top', label: 'Haut' },
|
|
|
|
|
{ key: 'bottom', label: 'Bas' },
|
|
|
|
|
{ key: 'left', label: 'Gauche' },
|
|
|
|
|
{ key: 'right', label: 'Droite' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const updateMarginValue = (side, value) => {
|
|
|
|
|
if (marginLocked.value) {
|
|
|
|
|
for (const s of ['top', 'right', 'bottom', 'left']) margin[s].value = value;
|
|
|
|
|
} else {
|
|
|
|
|
margin[side].value = value;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const updateMarginUnit = (side, unit) => {
|
|
|
|
|
if (marginLocked.value) {
|
|
|
|
|
for (const s of ['top', 'right', 'bottom', 'left']) {
|
|
|
|
|
margin[s].value = convertUnit(margin[s].value, margin[s].unit, unit);
|
|
|
|
|
margin[s].unit = unit;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
margin[side].value = convertUnit(margin[side].value, margin[side].unit, unit);
|
|
|
|
|
margin[side].unit = unit;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const updatePaddingValue = (side, value) => {
|
|
|
|
|
if (paddingLocked.value) {
|
|
|
|
|
for (const s of ['top', 'right', 'bottom', 'left']) padding[s].value = value;
|
|
|
|
|
} else {
|
|
|
|
|
padding[side].value = value;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const updatePaddingUnit = (side, unit) => {
|
|
|
|
|
if (paddingLocked.value) {
|
|
|
|
|
for (const s of ['top', 'right', 'bottom', 'left']) {
|
|
|
|
|
padding[s].value = convertUnit(padding[s].value, padding[s].unit, unit);
|
|
|
|
|
padding[s].unit = unit;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
padding[side].value = convertUnit(padding[side].value, padding[side].unit, unit);
|
|
|
|
|
padding[side].unit = unit;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-26 15:47:58 +01:00
|
|
|
// Generic update: push a single property to the stylesheet store
|
|
|
|
|
const updateProp = (cssProp, value, unit) => {
|
|
|
|
|
if (!selector.value) return;
|
|
|
|
|
stylesheetStore.updateProperty(selector.value, cssProp, value, unit);
|
2026-02-24 14:08:30 +01:00
|
|
|
};
|
|
|
|
|
|
2026-02-26 15:47:58 +01:00
|
|
|
const updateUnitPropUnit = (prop, newUnit) => {
|
|
|
|
|
prop.value = convertUnit(prop.value, prop.unit, newUnit);
|
|
|
|
|
prop.unit = newUnit;
|
2026-02-24 14:08:30 +01:00
|
|
|
};
|
|
|
|
|
|
2025-12-05 18:21:54 +01:00
|
|
|
const getSelectorFromElement = (element) => {
|
|
|
|
|
if (element.id) {
|
|
|
|
|
return `#${element.id}`;
|
|
|
|
|
}
|
2025-11-24 18:18:27 +01:00
|
|
|
|
2025-12-05 18:21:54 +01:00
|
|
|
const tagName = element.tagName.toLowerCase();
|
|
|
|
|
|
2025-12-08 16:35:28 +01:00
|
|
|
const classes = Array.from(element.classList).filter(
|
|
|
|
|
(cls) => !['element-hovered', 'element-selected', 'page-hovered', 'page-selected'].includes(cls)
|
|
|
|
|
);
|
2025-12-05 18:21:54 +01:00
|
|
|
if (classes.length > 0) {
|
|
|
|
|
return `${tagName}.${classes[0]}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return tagName;
|
2025-11-24 18:18:27 +01:00
|
|
|
};
|
|
|
|
|
|
2026-02-26 15:47:58 +01:00
|
|
|
const getInstanceCount = (sel) => {
|
2025-12-05 18:21:54 +01:00
|
|
|
if (!props.iframeRef || !props.iframeRef.contentDocument) return 0;
|
2025-11-24 18:18:27 +01:00
|
|
|
|
2025-12-05 18:21:54 +01:00
|
|
|
try {
|
2026-02-26 15:47:58 +01:00
|
|
|
return props.iframeRef.contentDocument.querySelectorAll(sel).length;
|
2025-12-05 18:21:54 +01:00
|
|
|
} catch (e) {
|
|
|
|
|
return 0;
|
2025-11-24 18:18:27 +01:00
|
|
|
}
|
|
|
|
|
};
|
2025-11-24 17:55:42 +01:00
|
|
|
|
|
|
|
|
const elementCss = computed(() => {
|
2025-11-24 18:18:27 +01:00
|
|
|
if (!selector.value) return '';
|
2025-12-05 18:21:54 +01:00
|
|
|
return stylesheetStore.extractBlock(selector.value) || '';
|
2025-11-24 16:51:55 +01:00
|
|
|
});
|
|
|
|
|
|
2025-12-10 12:11:53 +01:00
|
|
|
const generatePreviewCss = () => {
|
|
|
|
|
if (!selector.value) return '';
|
|
|
|
|
|
|
|
|
|
const properties = [];
|
|
|
|
|
|
2026-02-26 15:47:58 +01:00
|
|
|
for (const prop of styleProps) {
|
|
|
|
|
const val = prop.get();
|
2026-03-05 11:08:44 +01:00
|
|
|
if (!val) continue;
|
|
|
|
|
if (prop.css === 'font-style' && val !== 'italic') continue;
|
|
|
|
|
if (prop.css === 'font-weight' && val === 'normal') continue;
|
|
|
|
|
if ((prop.css === 'border-style' || prop.css === 'border-color') && borderWidth.value === 0) continue;
|
|
|
|
|
properties.push(` ${prop.css}: ${val};`);
|
2025-12-10 12:11:53 +01:00
|
|
|
}
|
2026-02-26 15:47:58 +01:00
|
|
|
|
|
|
|
|
for (const prop of unitProps) {
|
|
|
|
|
if (prop.ref.value !== undefined && prop.ref.value !== null) {
|
|
|
|
|
properties.push(` ${prop.css}: ${prop.ref.value}${prop.ref.unit};`);
|
|
|
|
|
}
|
2025-12-10 12:11:53 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-05 10:45:55 +01:00
|
|
|
for (const side of ['top', 'right', 'bottom', 'left']) {
|
|
|
|
|
if (margin[side].value !== undefined && margin[side].value !== null) {
|
|
|
|
|
properties.push(` margin-${side}: ${margin[side].value}${margin[side].unit};`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (const side of ['top', 'right', 'bottom', 'left']) {
|
|
|
|
|
if (padding[side].value !== undefined && padding[side].value !== null) {
|
|
|
|
|
properties.push(` padding-${side}: ${padding[side].value}${padding[side].unit};`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 12:11:53 +01:00
|
|
|
if (properties.length === 0) return '';
|
|
|
|
|
|
|
|
|
|
return `${selector.value} {\n${properties.join('\n')}\n}`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const displayedCss = computed(() => {
|
|
|
|
|
if (!selector.value) return '';
|
2026-03-05 11:42:18 +01:00
|
|
|
return elementCss.value || generatePreviewCss();
|
2025-12-10 12:11:53 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const applyAllStyles = () => {
|
|
|
|
|
if (!selector.value) return;
|
2026-02-26 15:47:58 +01:00
|
|
|
for (const prop of styleProps) {
|
|
|
|
|
updateProp(prop.css, prop.get());
|
|
|
|
|
}
|
|
|
|
|
for (const prop of unitProps) {
|
|
|
|
|
updateProp(prop.css, prop.ref.value, prop.ref.unit);
|
|
|
|
|
}
|
2026-03-05 10:45:55 +01:00
|
|
|
for (const side of ['top', 'right', 'bottom', 'left']) {
|
|
|
|
|
updateProp(`margin-${side}`, margin[side].value, margin[side].unit);
|
|
|
|
|
updateProp(`padding-${side}`, padding[side].value, padding[side].unit);
|
|
|
|
|
}
|
2025-12-10 12:11:53 +01:00
|
|
|
};
|
|
|
|
|
|
2026-02-26 15:47:58 +01:00
|
|
|
// Watchers — simple props
|
|
|
|
|
for (const prop of styleProps) {
|
|
|
|
|
watch(prop.get, () => {
|
2026-02-26 15:35:45 +01:00
|
|
|
if (isUpdatingFromStore) return;
|
2026-02-26 15:47:58 +01:00
|
|
|
const fn = () => updateProp(prop.css, prop.get());
|
|
|
|
|
prop.debounce ? debouncedUpdate(fn) : fn();
|
2026-02-26 15:35:45 +01:00
|
|
|
});
|
2026-02-26 15:47:58 +01:00
|
|
|
}
|
2025-11-24 18:39:29 +01:00
|
|
|
|
2026-02-26 15:47:58 +01:00
|
|
|
// Watchers — unit props (watch both value and unit)
|
|
|
|
|
for (const prop of unitProps) {
|
|
|
|
|
watch(() => prop.ref.value, () => {
|
|
|
|
|
if (isUpdatingFromStore) return;
|
|
|
|
|
const fn = () => updateProp(prop.css, prop.ref.value, prop.ref.unit);
|
|
|
|
|
prop.debounce ? debouncedUpdate(fn) : fn();
|
|
|
|
|
});
|
|
|
|
|
watch(() => prop.ref.unit, () => {
|
|
|
|
|
if (isUpdatingFromStore) return;
|
|
|
|
|
updateProp(prop.css, prop.ref.value, prop.ref.unit);
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-02-26 15:35:45 +01:00
|
|
|
|
2026-03-05 10:45:55 +01:00
|
|
|
// Watchers — margin sides
|
|
|
|
|
watch(
|
|
|
|
|
() => [margin.top.value, margin.right.value, margin.bottom.value, margin.left.value],
|
|
|
|
|
() => {
|
|
|
|
|
if (isUpdatingFromStore) return;
|
|
|
|
|
debouncedUpdate(() => {
|
|
|
|
|
for (const side of ['top', 'right', 'bottom', 'left']) {
|
|
|
|
|
updateProp(`margin-${side}`, margin[side].value, margin[side].unit);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
watch(
|
|
|
|
|
() => [margin.top.unit, margin.right.unit, margin.bottom.unit, margin.left.unit],
|
|
|
|
|
() => {
|
|
|
|
|
if (isUpdatingFromStore) return;
|
|
|
|
|
for (const side of ['top', 'right', 'bottom', 'left']) {
|
|
|
|
|
updateProp(`margin-${side}`, margin[side].value, margin[side].unit);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Watchers — padding sides
|
|
|
|
|
watch(
|
|
|
|
|
() => [padding.top.value, padding.right.value, padding.bottom.value, padding.left.value],
|
|
|
|
|
() => {
|
|
|
|
|
if (isUpdatingFromStore) return;
|
|
|
|
|
debouncedUpdate(() => {
|
|
|
|
|
for (const side of ['top', 'right', 'bottom', 'left']) {
|
|
|
|
|
updateProp(`padding-${side}`, padding[side].value, padding[side].unit);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
watch(
|
|
|
|
|
() => [padding.top.unit, padding.right.unit, padding.bottom.unit, padding.left.unit],
|
|
|
|
|
() => {
|
|
|
|
|
if (isUpdatingFromStore) return;
|
|
|
|
|
for (const side of ['top', 'right', 'bottom', 'left']) {
|
|
|
|
|
updateProp(`padding-${side}`, padding[side].value, padding[side].unit);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
2026-02-26 15:35:45 +01:00
|
|
|
const handleCssInput = (newCss) => {
|
|
|
|
|
const oldBlock = elementCss.value;
|
|
|
|
|
if (oldBlock) {
|
|
|
|
|
stylesheetStore.replaceInCustomCss(oldBlock, newCss);
|
2025-12-05 18:21:54 +01:00
|
|
|
}
|
2026-02-26 15:35:45 +01:00
|
|
|
};
|
2025-12-05 18:21:54 +01:00
|
|
|
|
|
|
|
|
// Watch stylesheet changes to sync values
|
|
|
|
|
watch(
|
2026-01-09 14:35:23 +01:00
|
|
|
() => stylesheetStore.customCss,
|
2025-12-05 18:21:54 +01:00
|
|
|
() => {
|
2026-02-26 15:35:45 +01:00
|
|
|
if (basePopup.value?.visible && !isUpdatingFromStore) {
|
2026-01-09 14:35:23 +01:00
|
|
|
isUpdatingFromStore = true;
|
2025-12-05 18:21:54 +01:00
|
|
|
loadValuesFromStylesheet();
|
2026-02-26 15:47:58 +01:00
|
|
|
nextTick(() => { isUpdatingFromStore = false; });
|
2026-01-09 14:35:23 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Also watch when exiting edit mode
|
|
|
|
|
watch(
|
|
|
|
|
() => stylesheetStore.isEditing,
|
|
|
|
|
(isEditing, wasEditing) => {
|
2026-02-26 15:35:45 +01:00
|
|
|
if (basePopup.value?.visible && wasEditing && !isEditing && !isUpdatingFromStore) {
|
2026-01-09 14:35:23 +01:00
|
|
|
isUpdatingFromStore = true;
|
|
|
|
|
loadValuesFromStylesheet();
|
2026-02-26 15:47:58 +01:00
|
|
|
nextTick(() => { isUpdatingFromStore = false; });
|
2025-12-05 18:21:54 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const loadValuesFromStylesheet = () => {
|
|
|
|
|
if (!selector.value) return;
|
|
|
|
|
|
|
|
|
|
try {
|
2026-02-26 15:47:58 +01:00
|
|
|
// Simple props
|
|
|
|
|
for (const prop of styleProps) {
|
|
|
|
|
const data = stylesheetStore.extractValue(selector.value, prop.css);
|
|
|
|
|
if (data) {
|
|
|
|
|
const value = typeof data === 'string' ? data : data.value;
|
|
|
|
|
prop.set(value);
|
|
|
|
|
}
|
2025-12-05 18:21:54 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-26 15:47:58 +01:00
|
|
|
// Unit props
|
|
|
|
|
for (const prop of unitProps) {
|
|
|
|
|
const data = stylesheetStore.extractValue(selector.value, prop.css);
|
|
|
|
|
if (data && data.value !== undefined) {
|
|
|
|
|
prop.ref.value = data.value;
|
|
|
|
|
prop.ref.unit = data.unit;
|
|
|
|
|
}
|
2025-12-05 18:21:54 +01:00
|
|
|
}
|
2026-03-05 10:45:55 +01:00
|
|
|
|
|
|
|
|
// Margin sides — try individual first, fallback to shorthand
|
|
|
|
|
const spacingSides = ['top', 'right', 'bottom', 'left'];
|
|
|
|
|
let anyMarginFound = false;
|
|
|
|
|
for (const side of spacingSides) {
|
|
|
|
|
const data = stylesheetStore.extractValue(selector.value, `margin-${side}`);
|
|
|
|
|
if (data && data.value !== undefined) {
|
|
|
|
|
margin[side].value = data.value;
|
|
|
|
|
margin[side].unit = data.unit;
|
|
|
|
|
anyMarginFound = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!anyMarginFound) {
|
|
|
|
|
const data = stylesheetStore.extractValue(selector.value, 'margin');
|
|
|
|
|
if (data && data.value !== undefined) {
|
|
|
|
|
for (const side of spacingSides) {
|
|
|
|
|
margin[side].value = data.value;
|
|
|
|
|
margin[side].unit = data.unit;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Padding sides — try individual first, fallback to shorthand
|
|
|
|
|
let anyPaddingFound = false;
|
|
|
|
|
for (const side of spacingSides) {
|
|
|
|
|
const data = stylesheetStore.extractValue(selector.value, `padding-${side}`);
|
|
|
|
|
if (data && data.value !== undefined) {
|
|
|
|
|
padding[side].value = data.value;
|
|
|
|
|
padding[side].unit = data.unit;
|
|
|
|
|
anyPaddingFound = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!anyPaddingFound) {
|
|
|
|
|
const data = stylesheetStore.extractValue(selector.value, 'padding');
|
|
|
|
|
if (data && data.value !== undefined) {
|
|
|
|
|
for (const side of spacingSides) {
|
|
|
|
|
padding[side].value = data.value;
|
|
|
|
|
padding[side].unit = data.unit;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-05 18:21:54 +01:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error loading values from stylesheet:', error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-08 14:15:06 +01:00
|
|
|
const open = (element, event, count = null) => {
|
2026-01-09 14:31:42 +01:00
|
|
|
isUpdatingFromStore = true;
|
|
|
|
|
|
2025-12-05 18:21:54 +01:00
|
|
|
selectedElement.value = element;
|
|
|
|
|
selector.value = getSelectorFromElement(element);
|
|
|
|
|
|
2025-12-08 14:15:06 +01:00
|
|
|
elementInstanceCount.value = count !== null ? count : getInstanceCount(selector.value);
|
|
|
|
|
|
2025-12-05 18:21:54 +01:00
|
|
|
loadValuesFromStylesheet();
|
2026-02-26 15:35:45 +01:00
|
|
|
basePopup.value.open(event);
|
2025-12-05 18:21:54 +01:00
|
|
|
|
2026-02-26 15:47:58 +01:00
|
|
|
nextTick(() => { isUpdatingFromStore = false; });
|
2025-12-05 18:21:54 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const close = () => {
|
2026-02-26 15:35:45 +01:00
|
|
|
basePopup.value?.close();
|
2025-12-05 18:21:54 +01:00
|
|
|
selector.value = '';
|
|
|
|
|
selectedElement.value = null;
|
2025-12-08 14:15:06 +01:00
|
|
|
emit('close');
|
2025-12-05 18:21:54 +01:00
|
|
|
};
|
|
|
|
|
|
2025-12-08 14:15:06 +01:00
|
|
|
const handleIframeClick = (event, targetElement = null, elementCount = null) => {
|
2025-12-08 12:41:14 +01:00
|
|
|
const element = targetElement || event.target;
|
2025-12-05 18:21:54 +01:00
|
|
|
|
|
|
|
|
if (element.tagName === 'BODY' || element.tagName === 'HTML') {
|
|
|
|
|
close();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 15:35:45 +01:00
|
|
|
if (basePopup.value?.visible) {
|
2025-12-05 18:21:54 +01:00
|
|
|
close();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 14:15:06 +01:00
|
|
|
open(element, event, elementCount);
|
2025-12-05 18:21:54 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
defineExpose({ handleIframeClick, close, visible });
|
2025-11-24 16:51:55 +01:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2025-12-08 16:17:34 +01:00
|
|
|
/* ElementPopup-specific styles (purple theme) */
|
2025-12-05 18:21:54 +01:00
|
|
|
.element-label {
|
2025-12-08 14:15:06 +01:00
|
|
|
background: var(--color-purple);
|
2025-12-05 18:21:54 +01:00
|
|
|
color: white;
|
|
|
|
|
padding: 0.25rem 0.5rem;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.instance-count {
|
2025-12-08 14:15:06 +01:00
|
|
|
color: var(--color-purple);
|
2025-11-24 16:51:55 +01:00
|
|
|
font-size: 0.875rem;
|
|
|
|
|
}
|
2026-03-05 10:45:55 +01:00
|
|
|
|
|
|
|
|
.settings-subsection-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
margin-bottom: 0.25rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.lock-toggle {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
background: none;
|
|
|
|
|
border: 1px solid transparent;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
padding: 2px 4px;
|
|
|
|
|
color: var(--color-text-muted, #999);
|
|
|
|
|
transition: color 0.15s, border-color 0.15s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.lock-toggle:hover:not(:disabled) {
|
|
|
|
|
color: var(--color-text, #333);
|
|
|
|
|
border-color: var(--color-border, #ccc);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.lock-toggle.locked {
|
|
|
|
|
color: var(--color-purple, #7c3aed);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.lock-toggle:disabled {
|
|
|
|
|
opacity: 0.4;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
2025-11-24 16:51:55 +01:00
|
|
|
</style>
|