Refactor blocks architecture to modular approach

- Restore "1/2, 1/2" layout for flexible column combinations
- Simplify beforeafter block: remove toggle and text field, keep only image comparison
- Create new video block with URL support (YouTube/Vimeo/direct files)
- Create horizontal-gallery block for scrollable image galleries
- Add H4 heading level support
- All blocks now modular: combine with text blocks in 2-column layouts

Blocks available:
- Text, Heading (h2-h4), Image, Video
- Before/After comparison (no text)
- Horizontal gallery (with text below)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
isUnknown 2026-02-09 18:59:08 +01:00
parent 6251d8f09f
commit 561932724b
23 changed files with 539 additions and 252 deletions

View file

@ -1,51 +1,27 @@
<template>
<div @click="$emit('open')" class="imagetext-preview">
<div class="imagetext-preview__container">
<!-- Zone image / slider -->
<div class="imagetext-preview__media">
<!-- Mode avant/après -->
<div v-if="isBeforeAfter && (imageBeforeUrl || imageAfterUrl)" class="imagetext-preview__slider">
<img
v-if="imageAfterUrl"
:src="imageAfterUrl"
class="imagetext-preview__image imagetext-preview__image--after"
alt="Après"
/>
<img
v-if="imageBeforeUrl"
:src="imageBeforeUrl"
class="imagetext-preview__image imagetext-preview__image--before"
alt="Avant"
/>
<div v-if="imageBeforeUrl && imageAfterUrl" class="imagetext-preview__divider"></div>
</div>
<div @click="$emit('open')" class="beforeafter-preview">
<div v-if="imageBeforeUrl || imageAfterUrl" class="beforeafter-preview__slider">
<img
v-if="imageAfterUrl"
:src="imageAfterUrl"
class="beforeafter-preview__image beforeafter-preview__image--after"
alt="Après"
/>
<img
v-if="imageBeforeUrl"
:src="imageBeforeUrl"
class="beforeafter-preview__image beforeafter-preview__image--before"
alt="Avant"
/>
<div v-if="imageBeforeUrl && imageAfterUrl" class="beforeafter-preview__divider"></div>
</div>
<!-- Mode simple -->
<img
v-else-if="!isBeforeAfter && imageUrl"
:src="imageUrl"
class="imagetext-preview__single-image"
alt="Image"
/>
<p v-if="content.caption" class="beforeafter-preview__caption">
{{ content.caption }}
</p>
<!-- Placeholder si pas d'image -->
<div v-else class="imagetext-preview__empty-media">
Aucune image
</div>
<!-- Légende -->
<p v-if="content.caption" class="imagetext-preview__caption">
{{ content.caption }}
</p>
</div>
<!-- Zone texte -->
<div class="imagetext-preview__text">
<div v-if="content.text" v-html="content.text"></div>
<div v-else class="imagetext-preview__empty-text">
Aucun texte
</div>
</div>
<div v-if="!imageBeforeUrl && !imageAfterUrl" class="beforeafter-preview__empty">
Cliquer pour ajouter des images
</div>
</div>
</template>
@ -57,21 +33,6 @@ const props = defineProps({
content: Object
});
// Attention: les clés sont en minuscules dans Kirby
const isBeforeAfter = computed(() => {
return props.content?.isbeforeafter === true || props.content?.isbeforeafter === "true";
});
// Mode simple : une seule image
const imageUrl = computed(() => {
if (!props.content?.image || !props.content.image.length) {
return null;
}
const file = props.content.image[0];
return file?.url || null;
});
// Mode avant/après : deux images
const imageBeforeUrl = computed(() => {
if (!props.content?.imagebefore || !props.content.imagebefore.length) {
return null;
@ -90,7 +51,7 @@ const imageAfterUrl = computed(() => {
</script>
<style scoped>
.imagetext-preview {
.beforeafter-preview {
cursor: pointer;
border-radius: var(--rounded);
overflow: hidden;
@ -98,30 +59,14 @@ const imageAfterUrl = computed(() => {
border: 1px solid var(--color-gray-300);
}
.imagetext-preview__container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
padding: 1rem;
}
/* Zone média (image ou slider) */
.imagetext-preview__media {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.imagetext-preview__slider {
.beforeafter-preview__slider {
position: relative;
width: 100%;
height: 200px;
background: var(--color-gray-200);
border-radius: var(--rounded-sm);
overflow: hidden;
}
.imagetext-preview__image {
.beforeafter-preview__image {
position: absolute;
top: 0;
left: 0;
@ -130,11 +75,11 @@ const imageAfterUrl = computed(() => {
object-fit: cover;
}
.imagetext-preview__image--before {
.beforeafter-preview__image--before {
clip-path: polygon(0 0, 50% 0, 50% 100%, 0 100%);
}
.imagetext-preview__divider {
.beforeafter-preview__divider {
position: absolute;
left: 50%;
top: 0;
@ -145,47 +90,24 @@ const imageAfterUrl = computed(() => {
transform: translateX(-1px);
}
.imagetext-preview__single-image {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: var(--rounded-sm);
background: var(--color-gray-200);
}
.imagetext-preview__empty-media {
width: 100%;
height: 200px;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-gray-200);
border-radius: var(--rounded-sm);
color: var(--color-gray-500);
font-size: var(--text-sm);
}
.imagetext-preview__caption {
.beforeafter-preview__caption {
padding: 0.75rem;
font-size: var(--text-sm);
color: var(--color-gray-600);
font-style: italic;
background: var(--color-background);
margin: 0;
padding: 0 0.25rem;
}
/* Zone texte */
.imagetext-preview__text {
font-size: var(--text-sm);
color: var(--color-gray-700);
line-height: 1.5;
}
.imagetext-preview__empty-text {
.beforeafter-preview__empty {
padding: 3rem 1rem;
text-align: center;
color: var(--color-gray-500);
font-style: italic;
font-size: var(--text-sm);
background: var(--color-gray-100);
}
.imagetext-preview:hover {
.beforeafter-preview:hover {
border-color: var(--color-gray-400);
}
</style>