diff --git a/tasks/enduro-trails/prototype/static/app.css b/tasks/enduro-trails/prototype/static/app.css
index 54ed9b7..3738913 100644
--- a/tasks/enduro-trails/prototype/static/app.css
+++ b/tasks/enduro-trails/prototype/static/app.css
@@ -284,25 +284,26 @@ body.has-map-mode #sheet-backdrop.visible { pointer-events: none; }
.wl-remove:active { background: var(--red-bg); color: var(--red); }
.wl-remove svg { width: 14px; height: 14px; }
-/* Route actions */
-.route-actions {
- display: flex; gap: 8px; margin: 8px 0;
-}
-.btn-action {
- height: 36px; padding: 0 14px;
- display: flex; align-items: center; justify-content: center; gap: 6px;
- background: var(--surface2); border: 1px solid var(--border);
- border-radius: 12px; color: var(--text2);
- font-size: 13px; font-weight: 600; cursor: pointer;
+/* Sheet icon buttons (header) */
+.sheet-icon-btn {
+ width: 32px; height: 32px;
+ display: flex; align-items: center; justify-content: center;
+ background: none; border: none; color: var(--text3);
+ border-radius: 8px; cursor: pointer; padding: 0;
+ flex-shrink: 0;
+ transition: background 0.15s, color 0.15s;
-webkit-tap-highlight-color: transparent;
- transition: background 0.15s, transform 0.1s, color 0.15s;
}
-.btn-action svg { width: 16px; height: 16px; flex-shrink: 0; transition: transform 0.1s; }
-.btn-action:active { transform: scale(0.94); background: var(--surface3, var(--border)); }
-.btn-action.primary { border-color: var(--accent); color: var(--accent); }
-.btn-action.primary:active { background: var(--accent); color: #fff; border-color: var(--accent); }
-.btn-action.danger { color: var(--red); }
-.btn-action.danger:active { background: var(--red); color: #fff; border-color: var(--red); }
+.sheet-icon-btn svg { width: 18px; height: 18px; }
+.sheet-icon-btn:active { background: var(--surface2); }
+.sheet-icon-btn.danger { color: var(--red); }
+.sheet-icon-btn.danger:active { background: var(--red); color: #fff; }
+
+/* Add waypoint row */
+.wl-add { cursor: pointer; }
+.wl-add:active { background: var(--surface); }
+.wl-add .wl-pin svg path { fill: var(--text3) !important; }
+.wl-add .wl-label { color: var(--text3); }
/* ── Route Status ─────────────────────────────── */
#route-status { font-size: 13px; color: var(--text2); padding: 8px 0; display: flex; align-items: center; gap: 6px; }
diff --git a/tasks/enduro-trails/prototype/static/app.js b/tasks/enduro-trails/prototype/static/app.js
index 625fd66..26bcfca 100644
--- a/tasks/enduro-trails/prototype/static/app.js
+++ b/tasks/enduro-trails/prototype/static/app.js
@@ -443,7 +443,6 @@ function clearRoute() {
}
}
document.getElementById('route-status').textContent = 'Тапни точку старта на карте';
- document.getElementById('route-actions').style.display = 'none';
document.getElementById('route-cards').innerHTML = '';
document.getElementById('waypoints-list').innerHTML = '';
if (routeMode && map) map.getCanvas().style.cursor = 'crosshair';
@@ -533,7 +532,7 @@ async function renderWaypointsList() {
const list = document.getElementById('waypoints-list');
if (!routeWaypoints.length) { list.innerHTML = ''; return; }
- list.innerHTML = routeWaypoints.map((wp, i) => {
+ let html = routeWaypoints.map((wp, i) => {
const isStart = i === 0;
const isEnd = i === routeWaypoints.length - 1;
const label = isStart ? 'S' : isEnd ? 'F' : String(i);
@@ -548,6 +547,16 @@ async function renderWaypointsList() {
`;
}).join('');
+ // Кнопка «Добавить точку» в стиле wl-item
+ if (routeWaypoints.length < 10) {
+ html += `
+
${waypointPinSvg('+', 'var(--text3)')}
+
Добавить точку
+
`;
+ }
+
+ list.innerHTML = html;
+
// Async geocode
routeWaypoints.forEach(async (wp, i) => {
const name = await reverseGeocode(wp.lat, wp.lon);
@@ -571,8 +580,6 @@ function removeWaypoint(idx) {
}
routeResults = [];
document.getElementById('route-cards').innerHTML = '';
- document.getElementById('route-actions').style.display =
- routeWaypoints.length >= 2 ? 'block' : 'none';
document.getElementById('route-status').textContent =
routeWaypoints.length === 0 ? 'Тапни точку старта на карте' :
routeWaypoints.length === 1 ? 'Тапни точку финиша' : '';
@@ -607,7 +614,6 @@ async function buildRoute() {
drawRouteResults(routeResults, 0);
document.getElementById('route-status').textContent = `${routeResults.length} маршрут(ов)`;
- document.getElementById('route-actions').style.display = 'block';
} catch(e) {
document.getElementById('route-status').textContent = '❌ ' + e.message;
document.getElementById('route-cards').innerHTML = '';
@@ -737,16 +743,14 @@ function escapeXml(str) {
return (str || '').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');
}
-function downloadGPX() {
+function generateGPX() {
const route = routeResults[activeRouteIdx];
- if (!route) return;
+ if (!route) return '';
const now = new Date();
const dateStr = now.toISOString().slice(0, 10);
- const timeStr = now.toISOString().replace(/[-:]/g, '').slice(0, 15);
- const filename = `enduro-${timeStr}.gpx`;
const distKm = (route.distance_m / 1000).toFixed(1);
const dirtPct = route.stats ? route.stats.dirt_total_pct : '?';
-
+
const wpts = routeWaypoints.map((wp, i) => {
const name = i === 0 ? 'Старт' : i === routeWaypoints.length - 1 ? 'Финиш' : `Точка ${i}`;
return ` ${escapeXml(name)}`;
@@ -755,12 +759,12 @@ function downloadGPX() {
markers.forEach(m => {
wpts.push(` ${escapeXml(m.name)}${escapeXml(m.icon)}`);
});
-
+
const trkpts = route.geometry.coordinates.map(([lon, lat]) =>
` `
).join('\n');
-
- const gpx = `
+
+ return `
Enduro route ${dateStr}
@@ -775,7 +779,14 @@ ${trkpts}
`;
-
+}
+
+function downloadGPX() {
+ const gpx = generateGPX();
+ if (!gpx) return;
+ const now = new Date();
+ const timeStr = now.toISOString().replace(/[-:]/g, '').slice(0, 15);
+ const filename = `enduro-${timeStr}.gpx`;
const blob = new Blob([gpx], { type: 'application/gpx+xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
@@ -785,6 +796,38 @@ ${trkpts}
URL.revokeObjectURL(url);
}
+async function shareRoute() {
+ const route = routeResults[activeRouteIdx];
+ if (!route) { alert('Сначала построй маршрут'); return; }
+
+ const distKm = (route.distance_m / 1000).toFixed(0);
+ const dirtPct = route.stats ? route.stats.dirt_total_pct : 0;
+ const shareText = `Маршрут: ${distKm} км, ${dirtPct}% грунт`;
+ const gpx = generateGPX();
+ const blob = new Blob([gpx], { type: 'application/gpx+xml' });
+ const file = new File([blob], 'enduro-route.gpx', { type: 'application/gpx+xml' });
+
+ // 1. File share (HTTPS only)
+ if (navigator.share && location.protocol === 'https:') {
+ try {
+ const canShare = navigator.canShare && navigator.canShare({ files: [file] });
+ if (canShare) {
+ await navigator.share({ title: 'Enduro Route', text: shareText, files: [file] });
+ return;
+ }
+ // canShare rejected files → share text only
+ await navigator.share({ title: 'Enduro Route', text: shareText });
+ return;
+ } catch(e) {
+ if (e.name === 'AbortError') return;
+ console.warn('Share failed, falling back to download:', e);
+ }
+ }
+
+ // 2. HTTPS unavailable → download GPX directly
+ downloadGPX();
+}
+
// ─── Флажки / именованные метки ────────────────────────────────────
const MARKER_ICONS = ['🚩', '⛺', '🔧', '⛽', '💧', '📍'];
const MARKERS_KEY = 'enduro_markers';
@@ -1089,7 +1132,6 @@ function initRouteClicks(map) {
} else if (routeWaypoints.length === 1) {
routeWaypoints.push({ lon: lng, lat: lat });
rebuildWaypointMarkers(); renderWaypointsList();
- document.getElementById('route-actions').style.display = 'block';
buildRoute();
}
});