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);
transition: border-color 0.2s;
color: #000;
background: var(--input-color-back);
}
.search-input:focus {

View file

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

View file

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