From aee7f9d1816f8c05cd172c735128789e82ff406a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Mon, 16 Mar 2026 16:58:20 +0400 Subject: [PATCH 1/7] Move Photon API URL to Settings --- app/components/app-menu/settings.gjs | 22 ++++++++++++++++++++++ app/services/photon.js | 8 ++++++-- app/services/settings.js | 12 ++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/app/components/app-menu/settings.gjs b/app/components/app-menu/settings.gjs index f85b7c7..e4cfdbc 100644 --- a/app/components/app-menu/settings.gjs +++ b/app/components/app-menu/settings.gjs @@ -18,6 +18,11 @@ export default class AppMenuSettings extends Component { this.settings.updateMapKinetic(event.target.value === 'true'); } + @action + updatePhotonApi(event) { + this.settings.updatePhotonApi(event.target.value); + } + diff --git a/app/services/photon.js b/app/services/photon.js index 4367d06..b17ae66 100644 --- a/app/services/photon.js +++ b/app/services/photon.js @@ -1,9 +1,13 @@ -import Service from '@ember/service'; +import Service, { service } from '@ember/service'; import { getPlaceType } from '../utils/osm'; import { humanizeOsmTag } from '../utils/format-text'; export default class PhotonService extends Service { - baseUrl = 'https://photon.komoot.io/api/'; + @service settings; + + get baseUrl() { + return this.settings.photonApi; + } async search(query, lat, lon, limit = 10) { if (!query || query.length < 2) return []; diff --git a/app/services/settings.js b/app/services/settings.js index 3c91153..a571b7f 100644 --- a/app/services/settings.js +++ b/app/services/settings.js @@ -4,6 +4,7 @@ import { tracked } from '@glimmer/tracking'; export default class SettingsService extends Service { @tracked overpassApi = 'https://overpass-api.de/api/interpreter'; @tracked mapKinetic = true; + @tracked photonApi = 'https://photon.komoot.io/api/'; overpassApis = [ { @@ -24,6 +25,13 @@ export default class SettingsService extends Service { // }, ]; + photonApis = [ + { + name: 'photon.komoot.io', + url: 'https://photon.komoot.io/api/', + }, + ]; + constructor() { super(...arguments); this.loadSettings(); @@ -59,4 +67,8 @@ export default class SettingsService extends Service { this.mapKinetic = enabled; localStorage.setItem('marco:map-kinetic', String(enabled)); } + + updatePhotonApi(url) { + this.photonApi = url; + } } From dca299175440835818cb734b44d89c017c56a752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Mon, 16 Mar 2026 17:07:38 +0400 Subject: [PATCH 2/7] Style dropdown menu form controls --- app/styles/app.css | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/styles/app.css b/app/styles/app.css index b68e4a3..5fc475c 100644 --- a/app/styles/app.css +++ b/app/styles/app.css @@ -312,6 +312,8 @@ body { font-family: inherit; font-size: 0.95rem; box-sizing: border-box; /* Ensure padding doesn't overflow width */ + color: #333; + background-color: #fff; } .form-control:focus { @@ -320,6 +322,17 @@ body { box-shadow: 0 0 0 2px rgb(0 123 255 / 10%); } +select.form-control { + appearance: none; + background-color: #fff; + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 0.75rem center; + background-size: 16px 16px; + padding-right: 2.5rem; + cursor: pointer; +} + .edit-actions { display: flex; gap: 0.5rem; From 5baebbb8461370187f4a2c8dfc9b52ef689af17a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Mon, 16 Mar 2026 18:12:05 +0400 Subject: [PATCH 3/7] Add details elements/sections to App Menu sidebar Starting with some About details --- app/components/app-menu/about.gjs | 68 +++++++++++++++++---------- app/styles/app.css | 76 +++++++++++++++++++++++++++++++ app/utils/icons.js | 4 ++ 3 files changed, 123 insertions(+), 25 deletions(-) diff --git a/app/components/app-menu/about.gjs b/app/components/app-menu/about.gjs index b6d793e..22d057d 100644 --- a/app/components/app-menu/about.gjs +++ b/app/components/app-menu/about.gjs @@ -2,6 +2,7 @@ import { on } from '@ember/modifier'; import Icon from '#components/icon'; diff --git a/app/styles/app.css b/app/styles/app.css index 5fc475c..9c36378 100644 --- a/app/styles/app.css +++ b/app/styles/app.css @@ -285,6 +285,82 @@ body { height: 20px; } +.sidebar-content details { + margin: 0 -1rem; /* Top margin, negative side margins to span full width */ +} + +.sidebar-content details summary { + list-style: none; /* Hide default triangle */ + display: flex; + align-items: center; + gap: 0.8rem; + padding: 1rem; + padding-left: 1.4rem; + cursor: pointer; + font-size: 0.95rem; + color: #333; + transition: background-color 0.2s; +} + +.sidebar-content details summary::-webkit-details-marker { + display: none; /* Hide default triangle in WebKit */ +} + +.sidebar-content details summary:hover { + background-color: var(--hover-bg); +} + +.sidebar-content details summary .icon { + width: 20px; + height: 20px; +} + +.sidebar-content details summary::after { + content: ''; + width: 20px; + height: 20px; + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='9 18 15 12 9 6'/%3E%3C/svg%3E"); + background-size: 20px 20px; + background-repeat: no-repeat; + background-position: center; + margin-left: auto; + transition: transform 0.2s ease; +} + +.sidebar-content details[open] summary::after { + transform: rotate(90deg); +} + +.sidebar-content details .details-content { + padding: 1rem 1.4rem; + animation: details-slide-down 0.2s ease-out; +} + +.sidebar-content details .link-list { + padding: 0; + margin: 0; +} + +@keyframes details-slide-down { + from { + opacity: 0; + transform: translateY(-5px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.sidebar-content details .link-list li { + margin-bottom: 0.5rem; +} + +.sidebar-content details .link-list li:last-child { + margin-bottom: 0; +} + .edit-form { margin: -1rem; margin-bottom: 1rem; diff --git a/app/utils/icons.js b/app/utils/icons.js index 83e0cf8..ec0d034 100644 --- a/app/utils/icons.js +++ b/app/utils/icons.js @@ -5,7 +5,9 @@ import checkSquare from 'feather-icons/dist/icons/check-square.svg?raw'; import clock from 'feather-icons/dist/icons/clock.svg?raw'; import edit from 'feather-icons/dist/icons/edit.svg?raw'; import facebook from 'feather-icons/dist/icons/facebook.svg?raw'; +import gift from 'feather-icons/dist/icons/gift.svg?raw'; import globe from 'feather-icons/dist/icons/globe.svg?raw'; +import heart from 'feather-icons/dist/icons/heart.svg?raw'; import home from 'feather-icons/dist/icons/home.svg?raw'; import info from 'feather-icons/dist/icons/info.svg?raw'; import instagram from 'feather-icons/dist/icons/instagram.svg?raw'; @@ -35,7 +37,9 @@ const ICONS = { clock, edit, facebook, + gift, globe, + heart, home, info, instagram, From 03d6a86569b7660427884050988128af171d1b4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Tue, 17 Mar 2026 13:14:21 +0400 Subject: [PATCH 4/7] Increase sidebar width, improve auto-panning * Increase sidebar with on desktop to fit more content * Move sidebar width to CSS variable * Auto-pan when a selected place would be obstructed by the desktop sidebar * Consider the header obscuring selected places, too * Only autopan when actually necessary --- app/components/map.gjs | 121 ++++++++++++++++++++++++++++++++--------- app/styles/app.css | 12 ++-- 2 files changed, 101 insertions(+), 32 deletions(-) diff --git a/app/components/map.gjs b/app/components/map.gjs index e434fc6..d72039e 100644 --- a/app/components/map.gjs +++ b/app/components/map.gjs @@ -499,10 +499,9 @@ export default class MapComponent extends Component { } if (options.preventZoom) { - // If we are preventing zoom (e.g. user clicked a bookmark), we still need to center - // but without changing the zoom level. - // We use animateToSmartCenter without a second argument (zoom=null). - this.animateToSmartCenter(coords); + // If we are preventing zoom (e.g. user clicked a bookmark), we rely on visibility check. + // This avoids unnecessary panning if the place is already visible. + this.handlePinVisibility(coords, { maintainZoom: true }); } else if (selected.bbox) { this.zoomToBbox(selected.bbox); } else { @@ -547,7 +546,10 @@ export default class MapComponent extends Component { } // Desktop: Sidebar covers left side (approx 400px) else if (this.args.isSidebarOpen) { - const sidebarWidth = 400; + const sidebarWidthVar = getComputedStyle(document.documentElement) + .getPropertyValue('--sidebar-width') + .trim(); + const sidebarWidth = parseInt(sidebarWidthVar, 10) || 360; const visibleWidth = size[0] - sidebarWidth; // Left padding: Sidebar + 15% of visible width @@ -566,14 +568,15 @@ export default class MapComponent extends Component { }); } - handlePinVisibility(coords) { + handlePinVisibility(coords, options = {}) { if (!this.mapInstance) return; const view = this.mapInstance.getView(); const currentZoom = view.getZoom(); // If too far out (e.g. world view), zoom in to neighborhood level (16) - if (currentZoom < 16) { + // UNLESS we want to maintain the current zoom + if (!options.maintainZoom && currentZoom < 16) { this.animateToSmartCenter(coords, 16); return; } @@ -590,8 +593,12 @@ export default class MapComponent extends Component { pixel[1] > size[1]; if (isOffScreen) { - this.animateToSmartCenter(coords); + // If off-screen, center it smartly (considering sidebar/bottom sheet) + // Pass maintainZoom to prevent zoom reset if desired + const zoom = options.maintainZoom ? null : 16; + this.animateToSmartCenter(coords, zoom); } else { + // If on-screen, only pan if obscured by UI this.panIfObscured(coords); } } @@ -627,6 +634,28 @@ export default class MapComponent extends Component { // To move the camera South (Lower Y), we subtract. targetCenter = [coords[0], coords[1] - offsetMapUnits]; } + // Desktop: Check if sidebar is open + else if (this.args.isSidebarOpen) { + const sidebarWidthVar = getComputedStyle(document.documentElement) + .getPropertyValue('--sidebar-width') + .trim(); + const sidebarWidth = parseInt(sidebarWidthVar, 10) || 360; + + // We want the pin to be in the center of the remaining space. + // Remaining space starts at x = sidebarWidth. + // Center of remaining space = sidebarWidth + (totalWidth - sidebarWidth) / 2 + // = sidebarWidth/2 + totalWidth/2 + // Map Center is totalWidth/2 + // Offset = sidebarWidth/2 (to the right) + + const offsetPixels = sidebarWidth / 2; + const offsetMapUnits = offsetPixels * resolution; + + // We want pin at center + offset. + // So map center must be pin - offset. + // X increases to the right. + targetCenter = [coords[0] - offsetMapUnits, coords[1]]; + } const animationOptions = { center: targetCenter, @@ -645,33 +674,73 @@ export default class MapComponent extends Component { if (!this.mapInstance) return; const size = this.mapInstance.getSize(); - // Check if mobile (width <= 768px matches CSS) - if (size[0] > 768) return; - const pixel = this.mapInstance.getPixelFromCoordinate(coords); if (!pixel) return; - const height = size[1]; + const view = this.mapInstance.getView(); + const center = view.getCenter(); + const resolution = view.getResolution(); - // Sidebar covers the bottom 50% - const splitPoint = height / 2; + // Default targets (current position) + let targetPixelX = pixel[0]; + let targetPixelY = pixel[1]; + let needsPan = false; - // If the pin is in the bottom half (y > splitPoint), it is obscured - if (pixel[1] > splitPoint) { - // Target position: Center of top half = height * 0.25 - const targetY = height * 0.25; - const deltaY = pixel[1] - targetY; + // 1. Mobile Bottom Sheet Logic (Screen <= 768px) + if (size[0] <= 768) { + const height = size[1]; + const splitPoint = height / 2; - const view = this.mapInstance.getView(); - const center = view.getCenter(); - const resolution = view.getResolution(); + // If in bottom half + if (pixel[1] > splitPoint) { + targetPixelY = height * 0.25; // Target: Center of top half + needsPan = true; + } + } + // 2. Desktop Sidebar Logic (Screen > 768px + Sidebar Open) + else if (this.args.isSidebarOpen) { + const sidebarWidthVar = getComputedStyle(document.documentElement) + .getPropertyValue('--sidebar-width') + .trim(); + const sidebarWidth = parseInt(sidebarWidthVar, 10) || 360; - // Move the map center SOUTH (decrease Y) to move the pin UP (decrease pixel Y) - const deltaMapUnits = deltaY * resolution; - const newCenter = [center[0], center[1] - deltaMapUnits]; + // If under sidebar + if (pixel[0] < sidebarWidth) { + const visibleWidth = size[0] - sidebarWidth; + targetPixelX = sidebarWidth + visibleWidth / 2; // Target: Center of visible area + needsPan = true; + } + } + + // 3. Header Logic (Any screen size) + // Check if the (potentially new) target Y is under the header + const headerHeight = 60; + const minTopDistance = headerHeight + 20; // 80px + + if (targetPixelY < minTopDistance) { + targetPixelY = minTopDistance + 30; // Move it to ~110px, clear of header + needsPan = true; + } + + if (needsPan) { + const deltaPixelX = pixel[0] - targetPixelX; + const deltaPixelY = pixel[1] - targetPixelY; + + // X: Camera moves same direction as we want the world to move? No. + // If we want pin to move RIGHT (pixel increases), Camera must move LEFT (X decreases). + // deltaPixelX = current - target. If current < target (want move right), delta is negative. + // center + negative = decrease. Correct. + const newCenterX = center[0] + deltaPixelX * resolution; + + // Y: Camera moves opposite direction to world relative to pixel coords. + // Pixel Y increases DOWN. Map Y increases UP. + // If we want pin to move DOWN (pixel increases), Camera must move UP (Y increases). + // deltaPixelY = current - target. If current < target (want move down), delta is negative. + // center - negative = increase. Correct. + const newCenterY = center[1] - deltaPixelY * resolution; view.animate({ - center: newCenter, + center: [newCenterX, newCenterY], duration: 500, easing: (t) => t * (2 - t), // Ease-out }); diff --git a/app/styles/app.css b/app/styles/app.css index 9c36378..9210057 100644 --- a/app/styles/app.css +++ b/app/styles/app.css @@ -3,6 +3,7 @@ :root { --default-list-color: #fc3; --hover-bg: #f8f9fa; + --sidebar-width: 360px; } html, @@ -202,7 +203,7 @@ body { top: 0; left: 0; bottom: 0; - width: 300px; + width: var(--sidebar-width); background: white; z-index: 3100; /* Higher than Header (3000) */ box-shadow: 2px 0 5px rgb(0 0 0 / 10%); @@ -856,15 +857,14 @@ span.icon { display: block; } -/* Sidebar is open (Desktop: Left 300px) */ +/* Sidebar is open (Desktop: Left var(--sidebar-width)) */ -/* We want to center in the remaining space (width - 300px) */ +/* We want to center in the remaining space (width - var(--sidebar-width)) */ -/* Center X = 300 + (width - 300) / 2 = 300 + width/2 - 150 = width/2 + 150 */ +/* Center X = var(--sidebar-width) + (width - var(--sidebar-width)) / 2 = var(--sidebar-width)/2 + 50% */ -/* So shift left by 150px from center */ .map-container.sidebar-open .map-crosshair { - left: calc(50% + 150px); + left: calc(50% + var(--sidebar-width) / 2); } @media (width <= 768px) { From 8877a9e1c82c8198ae6ee235c297b6114ea326ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Tue, 17 Mar 2026 13:18:17 +0400 Subject: [PATCH 5/7] Remove unnecessary whitespace --- app/components/map.gjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/map.gjs b/app/components/map.gjs index d72039e..c766f64 100644 --- a/app/components/map.gjs +++ b/app/components/map.gjs @@ -647,10 +647,10 @@ export default class MapComponent extends Component { // = sidebarWidth/2 + totalWidth/2 // Map Center is totalWidth/2 // Offset = sidebarWidth/2 (to the right) - + const offsetPixels = sidebarWidth / 2; const offsetMapUnits = offsetPixels * resolution; - + // We want pin at center + offset. // So map center must be pin - offset. // X increases to the right. From 88eb0ac0c1445d85b6ef71b3d21c282be6b77e55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Tue, 17 Mar 2026 13:55:38 +0400 Subject: [PATCH 6/7] WIP About section * Add fold-out sections for additional details * Use table for Open Source info, add licenses * Move link styles to CSS variables --- app/components/app-menu/about.gjs | 99 +++++++++++++++++++++++-------- app/components/place-details.gjs | 4 +- app/components/places-sidebar.gjs | 2 +- app/styles/app.css | 66 +++++++++++++++++---- 4 files changed, 130 insertions(+), 41 deletions(-) diff --git a/app/components/app-menu/about.gjs b/app/components/app-menu/about.gjs index 22d057d..4e395d6 100644 --- a/app/components/app-menu/about.gjs +++ b/app/components/app-menu/about.gjs @@ -14,7 +14,7 @@ import Icon from '#components/icon';