fix: resolve marker positioning bug and integrate Kirby design system
All checks were successful
Deploy / Build and Deploy to Production (push) Successful in 17s

- Fix marker positioning issue where markers would glide and misalign during zoom
- Implement two-wrapper structure to isolate CSS transforms from MapLibre positioning
- Outer .custom-marker: MapLibre handles positioning via translate3d()
- Inner .marker-inner: Visual transforms (rotate, scale) isolated from MapLibre
- Remove debug console.log statements
- Integrate Kirby design system in MarkerList and GeocodeSearch components
- Use Kirby CSS variables (--input-color-back, --color-border, etc.)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
isUnknown 2026-01-29 15:10:32 +01:00
parent 63dc136309
commit bad465406d
5 changed files with 170 additions and 163 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -215,6 +215,7 @@ export default {
background: var(--color-white); background: var(--color-white);
transition: border-color 0.2s; transition: border-color 0.2s;
color: #000; color: #000;
background: var(--input-color-back);
} }
.search-input:focus { .search-input:focus {

View file

@ -138,10 +138,11 @@ export default {
const isMarkerClick = target.closest('.custom-marker'); const isMarkerClick = target.closest('.custom-marker');
if (!isMarkerClick) { if (!isMarkerClick) {
emit("map-click", { const clickPos = {
lat: e.lngLat.lat, lat: e.lngLat.lat,
lng: e.lngLat.lng lng: e.lngLat.lng
}); };
emit("map-click", clickPos);
} }
}); });
} catch (error) { } catch (error) {
@ -176,20 +177,27 @@ export default {
return; return;
} }
// Create marker element // Create marker element (outer wrapper for MapLibre positioning)
const el = document.createElement("div"); const el = document.createElement("div");
el.className = "custom-marker"; el.className = "custom-marker";
// Create inner wrapper for visual transforms (isolates from MapLibre transforms)
const inner = document.createElement("div");
inner.className = "marker-inner";
if (props.selectedMarkerId === markerData.id) { if (props.selectedMarkerId === markerData.id) {
el.classList.add("selected"); inner.classList.add("selected");
} }
// Add marker number // Add marker number
const numberEl = document.createElement("div"); const numberEl = document.createElement("div");
numberEl.className = "marker-number"; numberEl.className = "marker-number";
numberEl.textContent = index + 1; numberEl.textContent = index + 1;
el.appendChild(numberEl); inner.appendChild(numberEl);
el.appendChild(inner);
try { try {
const coords = [markerData.position.lon, markerData.position.lat];
// Create MapLibre marker // Create MapLibre marker
// Anchor at bottom-center (where the pin tip is) // Anchor at bottom-center (where the pin tip is)
const marker = new maplibregl.Marker({ const marker = new maplibregl.Marker({
@ -197,7 +205,7 @@ export default {
draggable: true, draggable: true,
anchor: 'bottom' anchor: 'bottom'
}) })
.setLngLat([markerData.position.lon, markerData.position.lat]) .setLngLat(coords)
.addTo(map.value); .addTo(map.value);
// Handle marker drag // Handle marker drag
@ -244,10 +252,13 @@ export default {
function updateMarkerSelection(selectedId) { function updateMarkerSelection(selectedId) {
markerElements.value.forEach(({ element }, markerId) => { markerElements.value.forEach(({ element }, markerId) => {
if (element) { if (element) {
if (markerId === selectedId) { const inner = element.querySelector('.marker-inner');
element.classList.add("selected"); if (inner) {
} else { if (markerId === selectedId) {
element.classList.remove("selected"); inner.classList.add("selected");
} else {
inner.classList.remove("selected");
}
} }
} }
}); });
@ -332,22 +343,27 @@ export default {
} }
} }
/* Custom marker styles */ /* Custom marker outer wrapper - NO transforms here, MapLibre handles positioning */
.custom-marker { .custom-marker {
width: 40px; /* MapLibre will position this element via transform: translate3d() */
height: 40px;
cursor: grab; cursor: grab;
display: flex;
align-items: center;
justify-content: center;
position: relative;
} }
.custom-marker:active { .custom-marker:active {
cursor: grabbing; cursor: grabbing;
} }
.custom-marker::before { /* Inner wrapper for visual styling - transforms are isolated here */
.marker-inner {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.marker-inner::before {
content: ""; content: "";
position: absolute; position: absolute;
width: 40px; width: 40px;
@ -360,12 +376,12 @@ export default {
transition: all 0.2s; transition: all 0.2s;
} }
.custom-marker:hover::before { .marker-inner:hover::before {
background: #c0392b; background: #c0392b;
transform: rotate(-45deg) scale(1.1); transform: rotate(-45deg) scale(1.1);
} }
.custom-marker.selected::before { .marker-inner.selected::before {
background: #3498db; background: #3498db;
border-color: #2980b9; border-color: #2980b9;
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.6); box-shadow: 0 4px 12px rgba(52, 152, 219, 0.6);
@ -378,7 +394,7 @@ export default {
font-weight: 700; font-weight: 700;
font-size: 14px; font-size: 14px;
line-height: 1; line-height: 1;
transform: translateY(-4px); /* transform removed - was causing positioning issues */
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
pointer-events: none; pointer-events: none;
} }

View file

@ -1,64 +1,65 @@
<template> <template>
<div class="marker-list-sidebar"> <aside class="k-map-markers-sidebar">
<!-- Header with add button --> <!-- Header with counter and add button -->
<div class="marker-list-header"> <header class="k-section-header">
<h3>Marqueurs ({{ markers.length }}/{{ maxMarkers }})</h3> <k-headline>
<button Marqueurs
type="button" <k-counter>{{ markers.length }}/{{ maxMarkers }}</k-counter>
class="k-button k-button-icon" </k-headline>
<k-button
icon="add"
size="xs"
variant="filled"
@click="$emit('add-marker')" @click="$emit('add-marker')"
:disabled="!canAddMarker" :disabled="!canAddMarker"
title="Ajouter un marqueur"
> >
<k-icon type="add" /> Ajouter
</button> </k-button>
</div> </header>
<!-- Geocode search --> <!-- Geocode search -->
<div class="geocode-search-container"> <div class="k-map-markers-search">
<GeocodeSearch @select-location="$emit('select-location', $event)" /> <GeocodeSearch @select-location="$emit('select-location', $event)" />
</div> </div>
<!-- Marker list items --> <!-- Marker list -->
<div class="marker-list-items"> <div class="k-map-markers-list">
<div <div
v-for="(marker, index) in markers" v-for="(marker, index) in markers"
:key="marker.id" :key="marker.id"
class="marker-item" class="k-map-marker-item"
:class="{ active: selectedMarkerId === marker.id }" :class="{ 'is-selected': selectedMarkerId === marker.id }"
@click="$emit('select-marker', marker.id)" @click="$emit('select-marker', marker.id)"
> >
<div class="marker-item-content"> <span class="k-map-marker-icon">
<span class="marker-number">{{ index + 1 }}</span> {{ index + 1 }}
<span class="marker-title"> </span>
{{ marker.title || `Marqueur ${index + 1}` }} <span class="k-map-marker-text">
</span> {{ marker.title || `Marqueur ${index + 1}` }}
</div> </span>
<div class="marker-item-actions"> <span class="k-map-marker-options">
<button <k-button
type="button" icon="open"
class="k-button k-button-small" size="xs"
@click.stop="$emit('edit-marker', marker.id)" @click.stop="$emit('edit-marker', marker.id)"
title="Modifier le marqueur" />
> <k-button
<k-icon type="edit" /> icon="trash"
</button> size="xs"
<button
type="button"
class="k-button k-button-small"
@click.stop="$emit('delete-marker', marker.id)" @click.stop="$emit('delete-marker', marker.id)"
title="Supprimer le marqueur" />
> </span>
<k-icon type="trash" />
</button>
</div>
</div> </div>
<div v-if="markers.length === 0" class="marker-list-empty"> <div v-if="markers.length === 0" class="k-map-markers-empty">
Cliquez sur la carte pour ajouter des marqueurs <k-icon type="map-pin" />
<p class="k-map-markers-empty-text">Aucun marqueur</p>
<p class="k-map-markers-empty-info">
Cliquez sur la carte ou sur "Ajouter" pour créer un marqueur
</p>
</div> </div>
</div> </div>
</div> </aside>
</template> </template>
<script> <script>
@ -94,7 +95,9 @@ export default {
], ],
setup(props) { setup(props) {
const canAddMarker = computed(() => props.markers.length < props.maxMarkers); const canAddMarker = computed(
() => props.markers.length < props.maxMarkers
);
return { return {
canAddMarker, canAddMarker,
@ -104,143 +107,130 @@ export default {
</script> </script>
<style scoped> <style scoped>
.marker-list-sidebar { /* Sidebar container - uses Kirby's layout system */
width: var(--marker-list-width, 250px); .k-map-markers-sidebar {
width: var(--marker-list-width, 280px);
flex-shrink: 0; flex-shrink: 0;
border-right: 1px solid var(--color-border);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: var(--color-background); border-right: 1px solid var(--color-border);
background: var(--color-white);
} }
.marker-list-header { /* Header - minimal override of k-section-header */
.k-section-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 0.5rem; gap: var(--spacing-2);
padding: 0.75rem 1rem; padding: var(--spacing-3);
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border);
background: var(--color-white); background: var(--panel-color-back);
margin-bottom: 0;
} }
.marker-list-header h3 { .k-section-header .k-headline {
margin: 0;
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
color: var(--color-text-light);
flex: 1;
}
.marker-list-header .k-button-icon {
width: 2rem;
height: 2rem;
min-width: 2rem;
padding: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; gap: var(--spacing-2);
color: var(--color-black);
} }
.marker-list-header .k-button-icon .k-icon { /* Search container */
color: var(--color-black); .k-map-markers-search {
} padding: var(--spacing-3);
border-bottom: 1px solid var(--color-border);
.marker-list-header .k-button-icon:hover {
background: var(--color-background); background: var(--color-background);
} }
.marker-list-header .k-button-icon:disabled { /* List container */
opacity: 0.4; .k-map-markers-list {
cursor: not-allowed;
}
.geocode-search-container {
padding: 0.75rem;
border-bottom: 1px solid var(--color-border);
background: var(--color-white);
}
.marker-list-items {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 0.5rem; padding: var(--spacing-2);
background: var(--color-background);
} }
.marker-item { /* Marker item - styled like Kirby's k-item */
.k-map-marker-item {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; gap: var(--spacing-2);
padding: 0.75rem; padding: var(--spacing-2);
margin-bottom: 0.5rem; margin-bottom: var(--spacing-1);
background: var(--color-white); background: var(--color-white);
border: 1px solid var(--color-border); border-radius: var(--rounded);
border-radius: var(--rounded-sm);
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
color: #000;
} }
.marker-item:hover { .k-map-marker-item:hover {
background: var(--color-background); background: var(--color-gray-200);
border-color: var(--color-focus);
color: #fff;
} }
.marker-item.active { .k-map-marker-item.is-selected {
background: var(--color-focus-outline); background: var(--color-blue-300);
border-color: var(--color-focus); color: var(--color-white);
} }
.marker-item-content { .k-map-marker-icon {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem;
flex: 1;
min-width: 0;
}
.marker-number {
display: inline-flex;
align-items: center;
justify-content: center; justify-content: center;
width: 24px; width: 2rem;
height: 24px; height: 2rem;
background: var(--color-gray-400);
color: var(--color-white);
border-radius: 50%; border-radius: 50%;
font-size: 0.75rem; background: var(--color-gray-300);
font-weight: 600; color: var(--color-white);
flex-shrink: 0; flex-shrink: 0;
} }
.marker-item.active .marker-number { .k-map-marker-item.is-selected .k-map-marker-icon {
background: var(--color-focus); background: var(--color-blue-600);
} }
.marker-title { .k-map-marker-text {
font-size: 0.875rem; flex: 1;
min-width: 0;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
font-size: var(--text-sm);
} }
.marker-item-actions { .k-map-marker-options {
display: flex; display: flex;
gap: 0.25rem; gap: var(--spacing-1);
flex-shrink: 0; flex-shrink: 0;
} }
.k-button-small { /* Empty state - styled like Kirby's k-empty */
padding: 0.25rem 0.5rem; .k-map-markers-empty {
min-height: auto; display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--spacing-12) var(--spacing-6);
text-align: center;
color: var(--color-gray-600);
} }
.marker-list-empty { .k-map-markers-empty .k-icon {
padding: 2rem 1rem; width: 3rem;
text-align: center; height: 3rem;
color: var(--color-text-light); margin-bottom: var(--spacing-3);
font-size: 0.875rem; opacity: 0.25;
}
.k-map-markers-empty-text {
margin: 0 0 var(--spacing-2);
font-size: var(--text-base);
font-weight: 500;
color: var(--color-gray-800);
}
.k-map-markers-empty-info {
margin: 0;
font-size: var(--text-sm);
color: var(--color-gray-600);
} }
</style> </style>