관제 지도 갱신 안정화
This commit is contained in:
@@ -6,6 +6,11 @@ const state = {
|
||||
routeOverlay: null,
|
||||
routeDestinationOverlay: null,
|
||||
markerAnimationMs: 4800,
|
||||
hasFittedInitialLocations: false,
|
||||
isProgrammaticMapChange: false,
|
||||
programmaticMapChangeTimer: 0,
|
||||
pendingLocationUpdates: new Map(),
|
||||
locationUpdateBatchTimer: 0,
|
||||
drivers: new Map(),
|
||||
locations: new Map(),
|
||||
selectedDriverId: "",
|
||||
@@ -123,8 +128,9 @@ async function initializeMap() {
|
||||
|
||||
state.map.addControl(new kakao.maps.ZoomControl(), kakao.maps.ControlPosition.RIGHT);
|
||||
state.map.addControl(new kakao.maps.MapTypeControl(), kakao.maps.ControlPosition.TOPRIGHT);
|
||||
bindMapInteractionTracking();
|
||||
hideMapMessage();
|
||||
updateMarkers({ fitBounds: state.locations.size > 0 });
|
||||
updateInitialMapFit();
|
||||
return;
|
||||
} catch (error) {
|
||||
console.warn("Kakao map unavailable. Falling back to OpenStreetMap.", error);
|
||||
@@ -174,9 +180,10 @@ async function initializeLeafletMap() {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(state.map);
|
||||
|
||||
bindMapInteractionTracking();
|
||||
showMapMessage("카카오맵 권한 전까지 OpenStreetMap으로 표시 중");
|
||||
window.setTimeout(hideMapMessage, 4500);
|
||||
updateMarkers({ fitBounds: state.locations.size > 0 });
|
||||
updateInitialMapFit();
|
||||
}
|
||||
|
||||
function loadLeafletAssets() {
|
||||
@@ -316,7 +323,7 @@ function connectEvents() {
|
||||
});
|
||||
state.eventSource.addEventListener("location.updated", (event) => {
|
||||
const location = JSON.parse(event.data);
|
||||
upsertLocation(location, { focus: state.selectedDriverId === location.driverId });
|
||||
queueLocationUpdate(location);
|
||||
});
|
||||
state.eventSource.addEventListener("drivers.updated", (event) => {
|
||||
const payload = JSON.parse(event.data);
|
||||
@@ -377,7 +384,8 @@ function replaceLocations(locations) {
|
||||
state.locations.set(location.driverId, location);
|
||||
}
|
||||
render();
|
||||
updateMarkers({ fitBounds: locations.length > 0 });
|
||||
updateMarkers();
|
||||
updateInitialMapFit();
|
||||
|
||||
if (state.selectedDriverId) {
|
||||
const selectedLocation = state.locations.get(state.selectedDriverId);
|
||||
@@ -387,6 +395,7 @@ function replaceLocations(locations) {
|
||||
renderRouteFocusPanel(selectedLocation);
|
||||
} else {
|
||||
clearRouteOverlay();
|
||||
clearInfoOverlay();
|
||||
renderRouteFocusPanel(null);
|
||||
}
|
||||
}
|
||||
@@ -400,24 +409,48 @@ function replaceDrivers(drivers) {
|
||||
renderDrivers();
|
||||
}
|
||||
|
||||
function upsertLocation(location, options = {}) {
|
||||
state.locations.set(location.driverId, location);
|
||||
render();
|
||||
updateMarker(location);
|
||||
function queueLocationUpdate(location) {
|
||||
state.pendingLocationUpdates.set(location.driverId, location);
|
||||
|
||||
if (state.selectedDriverId === location.driverId && !elements.historyDrawer.classList.contains("is-hidden")) {
|
||||
loadLocationHistory(location);
|
||||
renderRoutePreview(location, { fit: false });
|
||||
showInfoOverlay(location);
|
||||
renderRouteFocusPanel(location);
|
||||
if (state.locationUpdateBatchTimer) return;
|
||||
|
||||
state.locationUpdateBatchTimer = window.setTimeout(() => {
|
||||
state.locationUpdateBatchTimer = 0;
|
||||
flushLocationUpdates();
|
||||
}, 180);
|
||||
}
|
||||
|
||||
function flushLocationUpdates() {
|
||||
if (!state.pendingLocationUpdates.size) return;
|
||||
|
||||
const locations = [...state.pendingLocationUpdates.values()];
|
||||
state.pendingLocationUpdates.clear();
|
||||
applyLocationUpdates(locations);
|
||||
}
|
||||
|
||||
function applyLocationUpdates(locations) {
|
||||
if (!locations.length) return;
|
||||
|
||||
for (const location of locations) {
|
||||
state.locations.set(location.driverId, location);
|
||||
}
|
||||
|
||||
if (options.focus) {
|
||||
fitMapToRoute(location) || centerMapOnLocation(location);
|
||||
render();
|
||||
updateMarkers();
|
||||
|
||||
const selectedLocation = state.selectedDriverId
|
||||
? state.locations.get(state.selectedDriverId)
|
||||
: null;
|
||||
|
||||
if (selectedLocation && !elements.historyDrawer.classList.contains("is-hidden")) {
|
||||
renderRoutePreview(selectedLocation, { fit: false });
|
||||
showInfoOverlay(selectedLocation);
|
||||
renderRouteFocusPanel(selectedLocation);
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
const vehicleListScrollTop = elements.vehicleList.scrollTop;
|
||||
const locations = [...state.locations.values()].sort((left, right) => {
|
||||
return new Date(right.receivedAt).getTime() - new Date(left.receivedAt).getTime();
|
||||
});
|
||||
@@ -450,12 +483,15 @@ function render() {
|
||||
empty.className = "empty-state";
|
||||
empty.textContent = locations.length ? "조건에 맞는 차량이 없습니다." : "수신 위치가 없습니다.";
|
||||
elements.vehicleList.append(empty);
|
||||
elements.vehicleList.scrollTop = vehicleListScrollTop;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const location of visibleLocations) {
|
||||
elements.vehicleList.append(createVehicleItem(location));
|
||||
}
|
||||
|
||||
elements.vehicleList.scrollTop = vehicleListScrollTop;
|
||||
}
|
||||
|
||||
function renderOperationsSummary(locations, counts) {
|
||||
@@ -769,6 +805,46 @@ function updateMarkers({ fitBounds = false } = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function updateInitialMapFit() {
|
||||
if (state.hasFittedInitialLocations || !state.map || !state.locations.size) return;
|
||||
|
||||
updateMarkers({ fitBounds: true });
|
||||
state.hasFittedInitialLocations = true;
|
||||
}
|
||||
|
||||
function bindMapInteractionTracking() {
|
||||
if (!state.map || !state.mapProvider) return;
|
||||
|
||||
const markUserInteraction = () => {
|
||||
if (!state.isProgrammaticMapChange) {
|
||||
state.hasFittedInitialLocations = true;
|
||||
}
|
||||
};
|
||||
|
||||
if (state.mapProvider === "kakao") {
|
||||
kakao.maps.event.addListener(state.map, "dragstart", markUserInteraction);
|
||||
kakao.maps.event.addListener(state.map, "zoom_changed", markUserInteraction);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.mapProvider === "leaflet") {
|
||||
state.map.on("dragstart", markUserInteraction);
|
||||
state.map.on("zoomstart", markUserInteraction);
|
||||
}
|
||||
}
|
||||
|
||||
function runProgrammaticMapChange(callback) {
|
||||
state.isProgrammaticMapChange = true;
|
||||
window.clearTimeout(state.programmaticMapChangeTimer);
|
||||
try {
|
||||
return callback();
|
||||
} finally {
|
||||
state.programmaticMapChangeTimer = window.setTimeout(() => {
|
||||
state.isProgrammaticMapChange = false;
|
||||
}, 350);
|
||||
}
|
||||
}
|
||||
|
||||
function updateMarker(location) {
|
||||
if (!state.map || !state.mapProvider) return;
|
||||
|
||||
@@ -784,9 +860,9 @@ function updateKakaoMarker(location) {
|
||||
const targetPosition = createPosition(location.latitude, location.longitude);
|
||||
const kakaoPosition = new kakao.maps.LatLng(targetPosition.latitude, targetPosition.longitude);
|
||||
const status = getLocationStatus(location);
|
||||
const markerElement = createTruckMarkerElement(location, status);
|
||||
|
||||
let marker = state.markers.get(location.driverId);
|
||||
const markerHeading = resolveMarkerHeading(location, marker?.currentPosition, targetPosition);
|
||||
const markerElement = createTruckMarkerElement(location, status, markerHeading);
|
||||
|
||||
if (!marker) {
|
||||
marker = {
|
||||
@@ -811,15 +887,17 @@ function updateLeafletMarker(location) {
|
||||
const vehicleType = getVehicleType(location);
|
||||
const targetPosition = createPosition(location.latitude, location.longitude);
|
||||
const leafletPosition = [targetPosition.latitude, targetPosition.longitude];
|
||||
const existingMarker = state.markers.get(location.driverId);
|
||||
const markerHeading = resolveMarkerHeading(location, existingMarker?.currentPosition, targetPosition);
|
||||
const icon = L.divIcon({
|
||||
className: `leaflet-truck-marker truck-marker ${status.key} ${vehicleType.className}`,
|
||||
html: createTruckMarkerHtml(location, vehicleType),
|
||||
iconSize: [74, 38],
|
||||
iconAnchor: [37, 38],
|
||||
popupAnchor: [0, -30]
|
||||
html: createTruckMarkerHtml(location, vehicleType, markerHeading),
|
||||
iconSize: [86, 56],
|
||||
iconAnchor: [43, 56],
|
||||
popupAnchor: [0, -44]
|
||||
});
|
||||
|
||||
let marker = state.markers.get(location.driverId);
|
||||
let marker = existingMarker;
|
||||
|
||||
if (!marker) {
|
||||
marker = {
|
||||
@@ -838,29 +916,69 @@ function updateLeafletMarker(location) {
|
||||
}
|
||||
}
|
||||
|
||||
function createTruckMarkerElement(location, status) {
|
||||
function createTruckMarkerElement(location, status, heading) {
|
||||
const vehicleType = getVehicleType(location);
|
||||
const markerElement = document.createElement("button");
|
||||
markerElement.className = `truck-marker ${status.key} ${vehicleType.className}`;
|
||||
markerElement.type = "button";
|
||||
markerElement.title = `${vehicleType.label} ${location.vehicleNo || location.driverId}`;
|
||||
markerElement.setAttribute("aria-label", `${vehicleType.label} ${location.vehicleNo || location.driverId} 위치 보기`);
|
||||
markerElement.innerHTML = createTruckMarkerHtml(location, vehicleType);
|
||||
markerElement.innerHTML = createTruckMarkerHtml(location, vehicleType, heading);
|
||||
markerElement.addEventListener("click", () => focusLocation(location));
|
||||
return markerElement;
|
||||
}
|
||||
|
||||
function createTruckMarkerHtml(location, vehicleType = getVehicleType(location)) {
|
||||
function createTruckMarkerHtml(location, vehicleType = getVehicleType(location), heading = getLocationHeading(location)) {
|
||||
const label = location.vehicleNo ? location.vehicleNo.slice(-4) : location.driverId.slice(0, 4);
|
||||
const rotation = normalizeMarkerRotation(heading);
|
||||
return `
|
||||
<span class="truck-marker__body">
|
||||
<span class="truck-marker__vehicle" style="--truck-heading: ${rotation}deg">
|
||||
<span class="truck-marker__body"></span>
|
||||
<span class="truck-marker__cab"></span>
|
||||
</span>
|
||||
<span class="truck-marker__label">
|
||||
<span class="truck-marker__type">${escapeHtml(vehicleType.shortLabel)}</span>
|
||||
<span class="truck-marker__plate">${escapeHtml(label)}</span>
|
||||
</span>
|
||||
<span class="truck-marker__cab"></span>
|
||||
`;
|
||||
}
|
||||
|
||||
function resolveMarkerHeading(location, fromPosition, targetPosition) {
|
||||
const routeHeading = getRouteStartHeading(location);
|
||||
if (routeHeading !== null) return routeHeading;
|
||||
|
||||
if (fromPosition && targetPosition && calculateDistanceKm(fromPosition, targetPosition) >= 0.003) {
|
||||
return calculateBearing(fromPosition, targetPosition);
|
||||
}
|
||||
|
||||
return getLocationHeading(location);
|
||||
}
|
||||
|
||||
function getRouteStartHeading(location) {
|
||||
const routePath = getRoutePath(location);
|
||||
if (routePath.length < 2) return null;
|
||||
|
||||
const from = routePath[0];
|
||||
for (let index = 1; index < routePath.length; index += 1) {
|
||||
const to = routePath[index];
|
||||
if (calculateDistanceKm(from, to) >= 0.003) {
|
||||
return calculateBearing(from, to);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getLocationHeading(location) {
|
||||
const heading = parseFiniteNumber(location.heading);
|
||||
return heading === null ? 90 : heading;
|
||||
}
|
||||
|
||||
function normalizeMarkerRotation(heading) {
|
||||
const normalizedHeading = ((Number(heading) % 360) + 360) % 360;
|
||||
return Math.round(normalizedHeading - 90);
|
||||
}
|
||||
|
||||
function animateMarkerPosition(marker, targetPosition) {
|
||||
const fromPosition = marker.currentPosition || getMarkerPosition(marker) || targetPosition;
|
||||
|
||||
@@ -956,19 +1074,24 @@ function fitMapToLocations() {
|
||||
for (const location of state.locations.values()) {
|
||||
bounds.extend(new kakao.maps.LatLng(location.latitude, location.longitude));
|
||||
}
|
||||
state.map.setBounds(bounds);
|
||||
return;
|
||||
runProgrammaticMapChange(() => state.map.setBounds(bounds));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (state.mapProvider === "leaflet") {
|
||||
const bounds = L.latLngBounds([...state.locations.values()].map((location) => {
|
||||
return [location.latitude, location.longitude];
|
||||
}));
|
||||
state.map.fitBounds(bounds, {
|
||||
maxZoom: 14,
|
||||
padding: [40, 40]
|
||||
runProgrammaticMapChange(() => {
|
||||
state.map.fitBounds(bounds, {
|
||||
maxZoom: 14,
|
||||
padding: [40, 40]
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function centerMapOnLocation(location, options = {}) {
|
||||
@@ -976,16 +1099,20 @@ function centerMapOnLocation(location, options = {}) {
|
||||
|
||||
if (state.mapProvider === "kakao") {
|
||||
const position = new kakao.maps.LatLng(location.latitude, location.longitude);
|
||||
state.map.setCenter(position);
|
||||
if (options.zoom && state.map.getLevel() > 4) {
|
||||
state.map.setLevel(4);
|
||||
}
|
||||
runProgrammaticMapChange(() => {
|
||||
state.map.setCenter(position);
|
||||
if (options.zoom && state.map.getLevel() > 4) {
|
||||
state.map.setLevel(4);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.mapProvider === "leaflet") {
|
||||
const nextZoom = options.zoom ? Math.max(state.map.getZoom(), 14) : state.map.getZoom();
|
||||
state.map.setView([location.latitude, location.longitude], nextZoom, { animate: true });
|
||||
runProgrammaticMapChange(() => {
|
||||
state.map.setView([location.latitude, location.longitude], nextZoom, { animate: true });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1070,18 +1197,20 @@ function fitMapToRoute(location) {
|
||||
for (const point of routePath) {
|
||||
bounds.extend(new kakao.maps.LatLng(point.latitude, point.longitude));
|
||||
}
|
||||
state.map.setBounds(bounds);
|
||||
runProgrammaticMapChange(() => state.map.setBounds(bounds));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (state.mapProvider === "leaflet") {
|
||||
state.map.fitBounds(
|
||||
L.latLngBounds(routePath.map((point) => [point.latitude, point.longitude])),
|
||||
{
|
||||
maxZoom: 13,
|
||||
padding: [48, 48]
|
||||
}
|
||||
);
|
||||
runProgrammaticMapChange(() => {
|
||||
state.map.fitBounds(
|
||||
L.latLngBounds(routePath.map((point) => [point.latitude, point.longitude])),
|
||||
{
|
||||
maxZoom: 13,
|
||||
padding: [48, 48]
|
||||
}
|
||||
);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1194,15 +1323,28 @@ function closeHistoryDrawer() {
|
||||
state.selectedDriverId = "";
|
||||
elements.historyDrawer.classList.add("is-hidden");
|
||||
clearRouteOverlay();
|
||||
clearInfoOverlay();
|
||||
renderRouteFocusPanel(null);
|
||||
render();
|
||||
}
|
||||
|
||||
function showInfoOverlay(location) {
|
||||
if (shouldSuppressMapPopup()) {
|
||||
clearInfoOverlay();
|
||||
return;
|
||||
}
|
||||
|
||||
const status = getLocationStatus(location);
|
||||
const content = createPopupElement(location, status);
|
||||
|
||||
if (state.mapProvider === "leaflet") {
|
||||
if (state.infoOverlay) {
|
||||
state.infoOverlay
|
||||
.setLatLng([location.latitude, location.longitude])
|
||||
.setContent(content);
|
||||
return;
|
||||
}
|
||||
|
||||
state.infoOverlay = L.popup({
|
||||
closeButton: true,
|
||||
offset: [0, -24]
|
||||
@@ -1223,6 +1365,22 @@ function showInfoOverlay(location) {
|
||||
state.infoOverlay.setMap(state.map);
|
||||
}
|
||||
|
||||
function clearInfoOverlay() {
|
||||
if (!state.infoOverlay) return;
|
||||
|
||||
if (typeof state.infoOverlay.setMap === "function") {
|
||||
state.infoOverlay.setMap(null);
|
||||
} else if (state.mapProvider === "leaflet" && state.map?.closePopup) {
|
||||
state.map.closePopup(state.infoOverlay);
|
||||
}
|
||||
|
||||
state.infoOverlay = null;
|
||||
}
|
||||
|
||||
function shouldSuppressMapPopup() {
|
||||
return window.matchMedia("(max-width: 560px)").matches;
|
||||
}
|
||||
|
||||
function createPopupElement(location, status) {
|
||||
const vehicleType = getVehicleType(location);
|
||||
const container = document.createElement("div");
|
||||
@@ -1560,6 +1718,16 @@ function calculateDistanceKm(from, to) {
|
||||
return earthRadiusKm * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
}
|
||||
|
||||
function calculateBearing(from, to) {
|
||||
const fromLatitude = toRadians(from.latitude);
|
||||
const toLatitude = toRadians(to.latitude);
|
||||
const deltaLongitude = toRadians(to.longitude - from.longitude);
|
||||
const y = Math.sin(deltaLongitude) * Math.cos(toLatitude);
|
||||
const x = Math.cos(fromLatitude) * Math.sin(toLatitude) -
|
||||
Math.sin(fromLatitude) * Math.cos(toLatitude) * Math.cos(deltaLongitude);
|
||||
return (Math.atan2(y, x) * 180 / Math.PI + 360) % 360;
|
||||
}
|
||||
|
||||
function interpolateNumber(from, to, progress) {
|
||||
return from + (to - from) * progress;
|
||||
}
|
||||
|
||||
@@ -932,17 +932,19 @@ button:hover {
|
||||
--truck-color: var(--blue);
|
||||
--truck-cab: #1556b8;
|
||||
--truck-accent: rgba(255, 255, 255, 0.2);
|
||||
--truck-heading: 0deg;
|
||||
--body-width: 42px;
|
||||
--body-height: 24px;
|
||||
--cab-width: 19px;
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: flex-end;
|
||||
width: max-content;
|
||||
min-width: calc(var(--body-width) + var(--cab-width));
|
||||
min-height: 38px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 86px;
|
||||
min-width: 86px;
|
||||
height: 56px;
|
||||
min-height: 56px;
|
||||
border: 0;
|
||||
padding: 0 0 8px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: #ffffff;
|
||||
filter: drop-shadow(0 8px 13px rgba(16, 32, 48, 0.28));
|
||||
@@ -1017,8 +1019,19 @@ button:hover {
|
||||
--truck-cab: #8f1d15;
|
||||
}
|
||||
|
||||
.truck-marker::before,
|
||||
.truck-marker::after {
|
||||
.truck-marker__vehicle {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
padding-bottom: 8px;
|
||||
transform: rotate(var(--truck-heading));
|
||||
transform-origin: 50% 44%;
|
||||
transition: transform 0.35s ease;
|
||||
}
|
||||
|
||||
.truck-marker__vehicle::before,
|
||||
.truck-marker__vehicle::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 1px;
|
||||
@@ -1029,19 +1042,17 @@ button:hover {
|
||||
background: #17212b;
|
||||
}
|
||||
|
||||
.truck-marker::before {
|
||||
.truck-marker__vehicle::before {
|
||||
left: 11px;
|
||||
}
|
||||
|
||||
.truck-marker::after {
|
||||
.truck-marker__vehicle::after {
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.truck-marker__body {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
place-items: center;
|
||||
display: block;
|
||||
width: var(--body-width);
|
||||
height: var(--body-height);
|
||||
border: 2px solid #ffffff;
|
||||
@@ -1050,31 +1061,43 @@ button:hover {
|
||||
background:
|
||||
var(--truck-accent),
|
||||
var(--truck-color);
|
||||
font-size: 10px;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.truck-marker.type-van .truck-marker__body {
|
||||
grid-template-rows: 1fr;
|
||||
border-radius: 9px 0 0 7px;
|
||||
}
|
||||
|
||||
.truck-marker.type-van .truck-marker__type {
|
||||
display: none;
|
||||
.truck-marker__label {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
max-width: 82px;
|
||||
min-height: 18px;
|
||||
padding: 2px 6px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.7);
|
||||
border-radius: 999px;
|
||||
background: rgba(15, 23, 42, 0.88);
|
||||
box-shadow: 0 4px 10px rgba(15, 23, 42, 0.22);
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
font-weight: 900;
|
||||
line-height: 1.1;
|
||||
transform: translateX(-50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.truck-marker__type {
|
||||
align-self: end;
|
||||
font-size: 9px;
|
||||
opacity: 0.88;
|
||||
.truck-marker__type,
|
||||
.truck-marker__plate {
|
||||
display: inline-block;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.truck-marker__plate {
|
||||
align-self: start;
|
||||
font-size: 11px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.truck-marker__cab {
|
||||
@@ -1319,6 +1342,35 @@ button:hover {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.history-drawer {
|
||||
right: 8px;
|
||||
bottom: 8px;
|
||||
left: 8px;
|
||||
max-height: min(178px, calc(100% - 86px));
|
||||
}
|
||||
|
||||
.history-heading {
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.history-heading strong {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.history-heading span {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.history-list {
|
||||
max-height: 122px;
|
||||
}
|
||||
|
||||
.history-row {
|
||||
padding: 7px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.vehicle-item {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user