Compare commits

..

1 Commits

Author SHA1 Message Date
c362268567 Use latest pnpm
Some checks failed
CI / Lint (pull_request) Failing after 8s
CI / Test (pull_request) Failing after 6s
2026-03-14 12:37:31 +04:00
23 changed files with 213 additions and 740 deletions

View File

@@ -1,14 +0,0 @@
name-template: 'v$RESOLVED_VERSION'
tag-template: 'v$RESOLVED_VERSION'
version-resolver:
major:
labels:
- 'release/major'
minor:
labels:
- 'release/minor'
- 'feature'
patch:
labels:
- 'release/patch'
default: patch

View File

@@ -1,13 +0,0 @@
name: Release Drafter
on:
pull_request:
types: [closed]
jobs:
release_drafter_job:
name: Update release notes draft
runs-on: ubuntu-latest
steps:
- name: Release Drafter
uses: https://github.com/raucao/gitea-release-drafter@dev
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,152 +0,0 @@
import { on } from '@ember/modifier';
import Icon from '#components/icon';
<template>
{{! template-lint-disable no-nested-interactive }}
<div class="sidebar-header">
<button type="button" class="back-btn" {{on "click" @onBack}}>
<Icon @name="arrow-left" @size={{20}} @color="#333" />
</button>
<h2>About</h2>
<button type="button" class="close-btn" {{on "click" @onClose}}>
<Icon @name="x" @size={{20}} @color="#333" />
</button>
</div>
<div class="sidebar-content">
<section class="about-section">
<p>
<strong>Marco</strong>
(as in
<a
href="https://en.wikipedia.org/wiki/Marco_Polo"
target="_blank"
rel="noopener"
>Marco Polo</a>) is an unhosted maps application that respects your
privacy and choices.
</p>
<p>
Connect your own
<a
href="https://remotestorage.io/"
target="_blank"
rel="noopener"
>remote storage</a>
to sync place bookmarks across apps and devices.
</p>
<details>
<summary>
<Icon @name="gift" @size={{20}} />
<span>Open Source</span>
</summary>
<div class="details-content">
<table>
<thead>
<tr>
<th>Source</th>
<th>License</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<a
href="https://gitea.kosmos.org/raucao/marco"
target="_blank"
rel="noopener"
>
Marco App
</a>
</td>
<td>
<a
href="https://en.wikipedia.org/wiki/GNU_Affero_General_Public_License"
target="_blank"
rel="noopener"
>
<abbr title="GNU Affero General Public License">AGPL</abbr>
</a>
</td>
</tr>
<tr>
<td>
<a
href="https://openstreetmap.org/copyright"
target="_blank"
rel="noopener"
>
Map Data
</a>
</td>
<td>
<a
href="https://opendatacommons.org/licenses/odbl/"
target="_blank"
rel="noopener"
>
<abbr
title="Open Data Commons Open Database License"
>ODbL</abbr>
</a>
</td>
</tr>
<tr>
<td>
<a
href="https://github.com/feathericons/feather"
target="_blank"
rel="noopener"
>
Feather Icons
</a>
</td>
<td>
<a
href="https://en.wikipedia.org/wiki/MIT_License"
target="_blank"
rel="noopener"
>
<abbr title="MIT License">MIT</abbr>
</a>
</td>
</tr>
</tbody>
</table>
</div>
</details>
<details>
<summary>
<Icon @name="heart" @size={{20}} @color="#e5533d" />
<span>Contribute</span>
</summary>
<div class="details-content">
<p>
<strong>Most impactful:</strong>
Add and improve data for points of interest in
<a
href="https://www.openstreetmap.org"
target="_blank"
rel="noopener"
>OpenStreetMap</a>.
</p>
<p>
<strong>Most appreciated:</strong>
Use this app as much as you can and
<a
href="https://community.remotestorage.io/t/marco-an-unhosted-maps-app/941"
target="_blank"
rel="noopener"
>submit feedback</a>
about your experience, problems, feature wishes, etc.
</p>
<p>
<strong>Most supportive:</strong>
Tell others about this app, on social media, in blog posts,
educational videos, etc.
</p>
</div>
</details>
</section>
</div>
</template>

View File

@@ -1,36 +0,0 @@
import { on } from '@ember/modifier';
import { fn } from '@ember/helper';
import { htmlSafe } from '@ember/template';
import Icon from '#components/icon';
import iconRounded from '../../icons/icon-rounded.svg?raw';
<template>
<div class="sidebar-header">
<h2>
<span class="app-logo-icon">
{{htmlSafe iconRounded}}
</span>
Marco
</h2>
<button type="button" class="close-btn" {{on "click" @onClose}}>
<Icon @name="x" @size={{20}} @color="#333" />
</button>
</div>
<div class="sidebar-content">
<ul class="app-menu">
<li>
<button type="button" {{on "click" (fn @onNavigate "settings")}}>
<Icon @name="settings" @size={{20}} />
<span>Settings</span>
</button>
</li>
<li>
<button type="button" {{on "click" (fn @onNavigate "about")}}>
<Icon @name="info" @size={{20}} />
<span>About</span>
</button>
</li>
</ul>
</div>
</template>

View File

@@ -1,38 +0,0 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { fn } from '@ember/helper';
import eq from 'ember-truth-helpers/helpers/eq';
import AppMenuHome from './home';
import AppMenuSettings from './settings';
import AppMenuAbout from './about';
export default class AppMenu extends Component {
@tracked currentView = 'menu'; // 'menu', 'settings', 'about'
@action
setView(view) {
this.currentView = view;
}
<template>
<div class="sidebar settings-pane">
{{#if (eq this.currentView "menu")}}
<AppMenuHome @onNavigate={{this.setView}} @onClose={{@onClose}} />
{{else if (eq this.currentView "settings")}}
<AppMenuSettings
@onBack={{fn this.setView "menu"}}
@onClose={{@onClose}}
/>
{{else if (eq this.currentView "about")}}
<AppMenuAbout
@onBack={{fn this.setView "menu"}}
@onClose={{@onClose}}
/>
{{/if}}
</div>
</template>
}

View File

@@ -1,100 +0,0 @@
import Component from '@glimmer/component';
import { on } from '@ember/modifier';
import { service } from '@ember/service';
import { action } from '@ember/object';
import Icon from '#components/icon';
import eq from 'ember-truth-helpers/helpers/eq';
export default class AppMenuSettings extends Component {
@service settings;
@action
updateApi(event) {
this.settings.updateOverpassApi(event.target.value);
}
@action
toggleKinetic(event) {
this.settings.updateMapKinetic(event.target.value === 'true');
}
@action
updatePhotonApi(event) {
this.settings.updatePhotonApi(event.target.value);
}
<template>
<div class="sidebar-header">
<button type="button" class="back-btn" {{on "click" @onBack}}>
<Icon @name="arrow-left" @size={{20}} @color="#333" />
</button>
<h2>Settings</h2>
<button type="button" class="close-btn" {{on "click" @onClose}}>
<Icon @name="x" @size={{20}} @color="#333" />
</button>
</div>
<div class="sidebar-content">
<section class="settings-section">
<div class="form-group">
<label for="map-kinetic">Map Inertia (Kinetic Panning)</label>
<select
id="map-kinetic"
class="form-control"
{{on "change" this.toggleKinetic}}
>
<option
value="true"
selected={{if this.settings.mapKinetic "selected"}}
>
On
</option>
<option
value="false"
selected={{unless this.settings.mapKinetic "selected"}}
>
Off
</option>
</select>
</div>
<div class="form-group">
<label for="overpass-api">Overpass API Provider</label>
<select
id="overpass-api"
class="form-control"
{{on "change" this.updateApi}}
>
{{#each this.settings.overpassApis as |api|}}
<option
value={{api.url}}
selected={{if
(eq api.url this.settings.overpassApi)
"selected"
}}
>
{{api.name}}
</option>
{{/each}}
</select>
</div>
<div class="form-group">
<label for="photon-api">Photon API Provider</label>
<select
id="photon-api"
class="form-control"
{{on "change" this.updatePhotonApi}}
>
{{#each this.settings.photonApis as |api|}}
<option
value={{api.url}}
selected={{if (eq api.url this.settings.photonApi) "selected"}}
>
{{api.name}}
</option>
{{/each}}
</select>
</div>
</section>
</div>
</template>
}

View File

@@ -499,9 +499,10 @@ export default class MapComponent extends Component {
} }
if (options.preventZoom) { if (options.preventZoom) {
// If we are preventing zoom (e.g. user clicked a bookmark), we rely on visibility check. // If we are preventing zoom (e.g. user clicked a bookmark), we still need to center
// This avoids unnecessary panning if the place is already visible. // but without changing the zoom level.
this.handlePinVisibility(coords, { maintainZoom: true }); // We use animateToSmartCenter without a second argument (zoom=null).
this.animateToSmartCenter(coords);
} else if (selected.bbox) { } else if (selected.bbox) {
this.zoomToBbox(selected.bbox); this.zoomToBbox(selected.bbox);
} else { } else {
@@ -546,10 +547,7 @@ export default class MapComponent extends Component {
} }
// Desktop: Sidebar covers left side (approx 400px) // Desktop: Sidebar covers left side (approx 400px)
else if (this.args.isSidebarOpen) { else if (this.args.isSidebarOpen) {
const sidebarWidthVar = getComputedStyle(document.documentElement) const sidebarWidth = 400;
.getPropertyValue('--sidebar-width')
.trim();
const sidebarWidth = parseInt(sidebarWidthVar, 10) || 360;
const visibleWidth = size[0] - sidebarWidth; const visibleWidth = size[0] - sidebarWidth;
// Left padding: Sidebar + 15% of visible width // Left padding: Sidebar + 15% of visible width
@@ -568,15 +566,14 @@ export default class MapComponent extends Component {
}); });
} }
handlePinVisibility(coords, options = {}) { handlePinVisibility(coords) {
if (!this.mapInstance) return; if (!this.mapInstance) return;
const view = this.mapInstance.getView(); const view = this.mapInstance.getView();
const currentZoom = view.getZoom(); const currentZoom = view.getZoom();
// If too far out (e.g. world view), zoom in to neighborhood level (16) // If too far out (e.g. world view), zoom in to neighborhood level (16)
// UNLESS we want to maintain the current zoom if (currentZoom < 16) {
if (!options.maintainZoom && currentZoom < 16) {
this.animateToSmartCenter(coords, 16); this.animateToSmartCenter(coords, 16);
return; return;
} }
@@ -593,12 +590,8 @@ export default class MapComponent extends Component {
pixel[1] > size[1]; pixel[1] > size[1];
if (isOffScreen) { if (isOffScreen) {
// If off-screen, center it smartly (considering sidebar/bottom sheet) this.animateToSmartCenter(coords);
// Pass maintainZoom to prevent zoom reset if desired
const zoom = options.maintainZoom ? null : 16;
this.animateToSmartCenter(coords, zoom);
} else { } else {
// If on-screen, only pan if obscured by UI
this.panIfObscured(coords); this.panIfObscured(coords);
} }
} }
@@ -634,28 +627,6 @@ export default class MapComponent extends Component {
// To move the camera South (Lower Y), we subtract. // To move the camera South (Lower Y), we subtract.
targetCenter = [coords[0], coords[1] - offsetMapUnits]; 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 = { const animationOptions = {
center: targetCenter, center: targetCenter,
@@ -674,73 +645,33 @@ export default class MapComponent extends Component {
if (!this.mapInstance) return; if (!this.mapInstance) return;
const size = this.mapInstance.getSize(); const size = this.mapInstance.getSize();
// Check if mobile (width <= 768px matches CSS)
if (size[0] > 768) return;
const pixel = this.mapInstance.getPixelFromCoordinate(coords); const pixel = this.mapInstance.getPixelFromCoordinate(coords);
if (!pixel) return; if (!pixel) return;
const view = this.mapInstance.getView(); const height = size[1];
const center = view.getCenter();
const resolution = view.getResolution();
// Default targets (current position) // Sidebar covers the bottom 50%
let targetPixelX = pixel[0]; const splitPoint = height / 2;
let targetPixelY = pixel[1];
let needsPan = false;
// 1. Mobile Bottom Sheet Logic (Screen <= 768px) // If the pin is in the bottom half (y > splitPoint), it is obscured
if (size[0] <= 768) { if (pixel[1] > splitPoint) {
const height = size[1]; // Target position: Center of top half = height * 0.25
const splitPoint = height / 2; const targetY = height * 0.25;
const deltaY = pixel[1] - targetY;
// If in bottom half const view = this.mapInstance.getView();
if (pixel[1] > splitPoint) { const center = view.getCenter();
targetPixelY = height * 0.25; // Target: Center of top half const resolution = view.getResolution();
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;
// If under sidebar // Move the map center SOUTH (decrease Y) to move the pin UP (decrease pixel Y)
if (pixel[0] < sidebarWidth) { const deltaMapUnits = deltaY * resolution;
const visibleWidth = size[0] - sidebarWidth; const newCenter = [center[0], center[1] - deltaMapUnits];
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({ view.animate({
center: [newCenterX, newCenterY], center: newCenter,
duration: 500, duration: 500,
easing: (t) => t * (2 - t), // Ease-out easing: (t) => t * (2 - t), // Ease-out
}); });

View File

@@ -286,7 +286,7 @@ export default class PlaceDetails extends Component {
> >
<Icon <Icon
@name="bookmark" @name="bookmark"
@color={{if this.isSaved "currentColor" "var(--link-color)"}} @color={{if this.isSaved "currentColor" "#007bff"}}
/> />
{{if this.isSaved "Saved" "Save"}} {{if this.isSaved "Saved" "Save"}}
</button> </button>
@@ -307,7 +307,7 @@ export default class PlaceDetails extends Component {
title="Edit" title="Edit"
{{on "click" this.startEditing}} {{on "click" this.startEditing}}
> >
<Icon @name="edit" @color="var(--link-color)" /> <Icon @name="edit" @color="#007bff" />
Edit Edit
</button> </button>
{{/if}} {{/if}}

View File

@@ -226,7 +226,7 @@ export default class PlacesSidebar extends Component {
class="btn btn-outline create-place" class="btn btn-outline create-place"
{{on "click" this.createNewPlace}} {{on "click" this.createNewPlace}}
> >
<Icon @name="plus" @size={{18}} @color="var(--link-color)" /> <Icon @name="plus" @size={{18}} @color="#007bff" />
Create new place Create new place
</button> </button>
{{/if}} {{/if}}

View File

@@ -0,0 +1,128 @@
import Component from '@glimmer/component';
import { on } from '@ember/modifier';
import { service } from '@ember/service';
import { action } from '@ember/object';
import Icon from '#components/icon';
import eq from 'ember-truth-helpers/helpers/eq';
export default class SettingsPane extends Component {
@service settings;
@action
updateApi(event) {
this.settings.updateOverpassApi(event.target.value);
}
@action
toggleKinetic(event) {
this.settings.updateMapKinetic(event.target.value === 'true');
}
<template>
<div class="sidebar settings-pane">
<div class="sidebar-header">
<h2>
<img src="/icons/icon-rounded.svg" alt="" width="32" height="32" />
Marco
</h2>
<button type="button" class="close-btn" {{on "click" @onClose}}>
<Icon @name="x" @size={{20}} @color="#333" />
</button>
</div>
<div class="sidebar-content">
<section class="settings-section">
<h3>Settings</h3>
<div class="form-group">
<label for="map-kinetic">Map Inertia (Kinetic Panning)</label>
<select
id="map-kinetic"
class="form-control"
{{on "change" this.toggleKinetic}}
>
<option
value="true"
selected={{if this.settings.mapKinetic "selected"}}
>
On
</option>
<option
value="false"
selected={{unless this.settings.mapKinetic "selected"}}
>
Off
</option>
</select>
</div>
<div class="form-group">
<label for="overpass-api">Overpass API Provider</label>
<select
id="overpass-api"
class="form-control"
{{on "change" this.updateApi}}
>
{{#each this.settings.overpassApis as |api|}}
<option
value={{api.url}}
selected={{if
(eq api.url this.settings.overpassApi)
"selected"
}}
>
{{api.name}}
</option>
{{/each}}
</select>
</div>
</section>
<section class="settings-section">
<h3>About</h3>
<p>
<strong>Marco</strong>
(as in
<a
href="https://en.wikipedia.org/wiki/Marco_Polo"
target="_blank"
rel="noopener"
>Marco Polo</a>) is an unhosted maps application that respects your
privacy and choices.
</p>
<p>
Connect your own
<a
href="https://remotestorage.io/"
target="_blank"
rel="noopener"
>remote storage</a>
to sync place bookmarks across apps and devices.
</p>
<ul class="link-list">
<li>
<a
href="https://gitea.kosmos.org/raucao/marco"
target="_blank"
rel="noopener"
>
Source Code
</a>
(<a
href="https://en.wikipedia.org/wiki/GNU_Affero_General_Public_License"
target="_blank"
rel="noopener"
>AGPL</a>)
</li>
<li>
<a
href="https://openstreetmap.org/copyright"
target="_blank"
rel="noopener"
>
Map Data © OpenStreetMap
</a>
</li>
</ul>
</section>
</div>
</div>
</template>
}

View File

@@ -1,45 +0,0 @@
<svg
width="1024"
height="1024"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
>
<!-- Background -->
<rect
x="0"
y="0"
width="1024"
height="1024"
rx="220"
fill="#F6E9A6"
/>
<!-- Subtle map grid (kept well outside safe zone) -->
<g stroke="#E6D88A" stroke-width="10" opacity="0.6">
<line x1="256" y1="0" x2="256" y2="1024" />
<line x1="512" y1="0" x2="512" y2="1024" />
<line x1="768" y1="0" x2="768" y2="1024" />
<line x1="0" y1="256" x2="1024" y2="256" />
<line x1="0" y1="512" x2="1024" y2="512" />
<line x1="0" y1="768" x2="1024" y2="768" />
</g>
<!-- Location pin (exact app shape, larger, centered, safe-zone compliant) -->
<!-- Safe zone target: ~680px diameter -->
<g
transform="
translate(512 512)
scale(22)
translate(-12 -12)
"
fill="#ea4335"
stroke="#b31412"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
<circle cx="12" cy="10" r="3" fill="#b31412" stroke="none" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,13 +1,9 @@
import Service, { service } from '@ember/service'; import Service from '@ember/service';
import { getPlaceType } from '../utils/osm'; import { getPlaceType } from '../utils/osm';
import { humanizeOsmTag } from '../utils/format-text'; import { humanizeOsmTag } from '../utils/format-text';
export default class PhotonService extends Service { export default class PhotonService extends Service {
@service settings; baseUrl = 'https://photon.komoot.io/api/';
get baseUrl() {
return this.settings.photonApi;
}
async search(query, lat, lon, limit = 10) { async search(query, lat, lon, limit = 10) {
if (!query || query.length < 2) return []; if (!query || query.length < 2) return [];

View File

@@ -4,7 +4,6 @@ import { tracked } from '@glimmer/tracking';
export default class SettingsService extends Service { export default class SettingsService extends Service {
@tracked overpassApi = 'https://overpass-api.de/api/interpreter'; @tracked overpassApi = 'https://overpass-api.de/api/interpreter';
@tracked mapKinetic = true; @tracked mapKinetic = true;
@tracked photonApi = 'https://photon.komoot.io/api/';
overpassApis = [ overpassApis = [
{ {
@@ -25,13 +24,6 @@ export default class SettingsService extends Service {
// }, // },
]; ];
photonApis = [
{
name: 'photon.komoot.io',
url: 'https://photon.komoot.io/api/',
},
];
constructor() { constructor() {
super(...arguments); super(...arguments);
this.loadSettings(); this.loadSettings();
@@ -67,8 +59,4 @@ export default class SettingsService extends Service {
this.mapKinetic = enabled; this.mapKinetic = enabled;
localStorage.setItem('marco:map-kinetic', String(enabled)); localStorage.setItem('marco:map-kinetic', String(enabled));
} }
updatePhotonApi(url) {
this.photonApi = url;
}
} }

View File

@@ -2,10 +2,6 @@
:root { :root {
--default-list-color: #fc3; --default-list-color: #fc3;
--hover-bg: #f8f9fa;
--sidebar-width: 360px;
--link-color: #2a7fff;
--link-color-visited: #6a4fbf;
} }
html, html,
@@ -188,7 +184,7 @@ body {
} }
.text-primary { .text-primary {
color: var(--link-color); color: #007bff;
} }
.text-danger { .text-danger {
@@ -205,7 +201,7 @@ body {
top: 0; top: 0;
left: 0; left: 0;
bottom: 0; bottom: 0;
width: var(--sidebar-width); width: 300px;
background: white; background: white;
z-index: 3100; /* Higher than Header (3000) */ z-index: 3100; /* Higher than Header (3000) */
box-shadow: 2px 0 5px rgb(0 0 0 / 10%); box-shadow: 2px 0 5px rgb(0 0 0 / 10%);
@@ -246,7 +242,7 @@ body {
} }
.sidebar-content { .sidebar-content {
padding: 0.5rem 1rem 1rem; padding: 1rem;
overflow-y: auto; overflow-y: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
flex: 1; flex: 1;
@@ -255,120 +251,10 @@ body {
overscroll-behavior: contain; overscroll-behavior: contain;
} }
.app-menu {
list-style: none;
padding: 0;
margin: 0 -1rem;
}
.app-menu button {
width: 100%;
display: flex;
align-items: center;
gap: 0.8rem;
padding: 1rem;
padding-left: 1.4rem;
background: none;
border: none;
color: #333;
cursor: pointer;
text-align: left;
font-size: 0.95rem;
font-family: inherit;
transition: background-color 0.2s;
}
.app-menu button:hover {
background-color: var(--hover-bg);
}
.app-menu .icon {
color: #666;
width: 20px;
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: 0 1.4rem 1rem;
animation: details-slide-down 0.2s ease-out;
font-size: 0.9rem;
}
.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 { .edit-form {
margin: -1rem; margin: -1rem;
margin-bottom: 1rem; margin-bottom: 1rem;
background: var(--hover-bg); background: #f8f9fa;
padding: 1rem; padding: 1rem;
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
} }
@@ -392,25 +278,12 @@ body {
font-family: inherit; font-family: inherit;
font-size: 1rem; font-size: 1rem;
box-sizing: border-box; /* Ensure padding doesn't overflow width */ box-sizing: border-box; /* Ensure padding doesn't overflow width */
color: #333;
background-color: #fff;
} }
.form-control:focus { .form-control:focus {
outline: none; outline: none;
border-color: var(--link-color); border-color: #007bff;
box-shadow: 0 0 0 2px rgb(42 127 255 / 10%); 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 { .edit-actions {
@@ -421,27 +294,27 @@ select.form-control {
.settings-section { .settings-section {
margin-bottom: 2rem; margin-bottom: 2rem;
font-size: 0.95rem; }
.settings-section h3 {
font-size: 1rem;
font-weight: bold;
color: #666;
margin: 0 0 0.5rem;
text-transform: uppercase;
letter-spacing: 0.5px;
} }
.settings-section .form-group { .settings-section .form-group {
margin-top: 1rem; margin-top: 1rem;
} }
.about-section { .settings-section p a {
margin-bottom: 2rem; color: #007bff;
}
.about-section a {
color: var(--link-color);
text-decoration: none; text-decoration: none;
} }
.about-section a:visited { .settings-section p a:hover {
color: var(--link-color-visited);
}
.about-section a:hover {
text-decoration: underline; text-decoration: underline;
} }
@@ -450,7 +323,7 @@ select.form-control {
} }
.btn-primary { .btn-primary {
background: var(--link-color); background: #007bff;
color: white; color: white;
border: none; border: none;
padding: 0.75rem; padding: 0.75rem;
@@ -479,7 +352,7 @@ select.form-control {
} }
.meta-info a { .meta-info a {
color: var(--link-color); color: #007bff;
text-decoration: none; text-decoration: none;
} }
@@ -487,36 +360,6 @@ select.form-control {
text-decoration: underline; text-decoration: underline;
} }
.sidebar-content table {
width: 100%;
border-collapse: collapse;
}
.sidebar-content table th,
.sidebar-content table td {
padding: 0.5rem 0;
text-align: left;
}
.sidebar-content table th {
font-size: 0.75rem;
font-weight: bold;
text-transform: uppercase;
color: #898989;
}
.sidebar-content table td {
border-bottom: 1px solid #f9f9f9;
}
.sidebar-content table tr:last-child td {
border-bottom: none;
}
abbr[title] {
text-decoration: underline dotted;
}
.link-list { .link-list {
list-style: none; list-style: none;
padding: 0; padding: 0;
@@ -528,7 +371,7 @@ abbr[title] {
} }
.link-list a { .link-list a {
color: var(--link-color); color: #007bff;
text-decoration: none; text-decoration: none;
font-size: 0.95rem; font-size: 0.95rem;
} }
@@ -557,7 +400,7 @@ abbr[title] {
} }
.place-item:hover { .place-item:hover {
background: var(--hover-bg); background: #eee;
} }
.place-name { .place-name {
@@ -653,7 +496,7 @@ abbr[title] {
} }
.btn-blue { .btn-blue {
background: var(--link-color); background: #007bff;
color: white; color: white;
border: none; border: none;
} }
@@ -763,17 +606,6 @@ abbr[title] {
/* Icons */ /* Icons */
.app-logo-icon {
display: inline-flex;
width: 32px;
height: 32px;
}
.app-logo-icon svg {
width: 100%;
height: 100%;
}
span.icon { span.icon {
display: inline-block; display: inline-block;
} }
@@ -898,14 +730,15 @@ span.icon {
display: block; display: block;
} }
/* Sidebar is open (Desktop: Left var(--sidebar-width)) */ /* Sidebar is open (Desktop: Left 300px) */
/* We want to center in the remaining space (width - var(--sidebar-width)) */ /* We want to center in the remaining space (width - 300px) */
/* Center X = var(--sidebar-width) + (width - var(--sidebar-width)) / 2 = var(--sidebar-width)/2 + 50% */ /* Center X = 300 + (width - 300) / 2 = 300 + width/2 - 150 = width/2 + 150 */
/* So shift left by 150px from center */
.map-container.sidebar-open .map-crosshair { .map-container.sidebar-open .map-crosshair {
left: calc(50% + var(--sidebar-width) / 2); left: calc(50% + 150px);
} }
@media (width <= 768px) { @media (width <= 768px) {
@@ -1113,7 +946,7 @@ button.create-place {
.search-result-item:hover, .search-result-item:hover,
.search-result-item:focus { .search-result-item:focus {
background: var(--hover-bg); background: #f5f5f5;
outline: none; outline: none;
} }
@@ -1178,7 +1011,7 @@ button.create-place {
} }
.place-lists-manager .list-item:hover { .place-lists-manager .list-item:hover {
background: var(--hover-bg); background: #f8f9fa;
} }
.place-lists-manager label { .place-lists-manager label {
@@ -1193,7 +1026,7 @@ button.create-place {
} }
.place-lists-manager input[type='checkbox'] { .place-lists-manager input[type='checkbox'] {
accent-color: var(--link-color); accent-color: #007bff;
width: 16px; width: 16px;
height: 16px; height: 16px;
cursor: pointer; cursor: pointer;

View File

@@ -2,7 +2,7 @@ import Component from '@glimmer/component';
import { pageTitle } from 'ember-page-title'; import { pageTitle } from 'ember-page-title';
import Map from '#components/map'; import Map from '#components/map';
import AppHeader from '#components/app-header'; import AppHeader from '#components/app-header';
import AppMenu from '#components/app-menu/index'; import SettingsPane from '#components/settings-pane';
import { service } from '@ember/service'; import { service } from '@ember/service';
import { tracked } from '@glimmer/tracking'; import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object'; import { action } from '@ember/object';
@@ -14,7 +14,7 @@ export default class ApplicationComponent extends Component {
@service mapUi; @service mapUi;
@service router; @service router;
@tracked isAppMenuOpen = false; @tracked isSettingsOpen = false;
get isSidebarOpen() { get isSidebarOpen() {
// We consider the sidebar "open" if we are in search or place routes. // We consider the sidebar "open" if we are in search or place routes.
@@ -34,19 +34,19 @@ export default class ApplicationComponent extends Component {
} }
@action @action
toggleAppMenu() { toggleSettings() {
this.isAppMenuOpen = !this.isAppMenuOpen; this.isSettingsOpen = !this.isSettingsOpen;
} }
@action @action
closeAppMenu() { closeSettings() {
this.isAppMenuOpen = false; this.isSettingsOpen = false;
} }
@action @action
handleOutsideClick() { handleOutsideClick() {
if (this.isAppMenuOpen) { if (this.isSettingsOpen) {
this.closeAppMenu(); this.closeSettings();
} else if (this.router.currentRouteName === 'search') { } else if (this.router.currentRouteName === 'search') {
this.router.transitionTo('index'); this.router.transitionTo('index');
} else if (this.router.currentRouteName === 'place') { } else if (this.router.currentRouteName === 'place') {
@@ -65,7 +65,7 @@ export default class ApplicationComponent extends Component {
<template> <template>
{{pageTitle "Marco"}} {{pageTitle "Marco"}}
<AppHeader @onToggleMenu={{this.toggleAppMenu}} /> <AppHeader @onToggleMenu={{this.toggleSettings}} />
<div <div
id="rs-widget-container" id="rs-widget-container"
@@ -81,12 +81,12 @@ export default class ApplicationComponent extends Component {
{{/if}} {{/if}}
<Map <Map
@isSidebarOpen={{or this.isSidebarOpen this.isAppMenuOpen}} @isSidebarOpen={{or this.isSidebarOpen this.isSettingsOpen}}
@onOutsideClick={{this.handleOutsideClick}} @onOutsideClick={{this.handleOutsideClick}}
/> />
{{#if this.isAppMenuOpen}} {{#if this.isSettingsOpen}}
<AppMenu @onClose={{this.closeAppMenu}} /> <SettingsPane @onClose={{this.closeSettings}} />
{{/if}} {{/if}}
{{outlet}} {{outlet}}

View File

@@ -5,11 +5,8 @@ import checkSquare from 'feather-icons/dist/icons/check-square.svg?raw';
import clock from 'feather-icons/dist/icons/clock.svg?raw'; import clock from 'feather-icons/dist/icons/clock.svg?raw';
import edit from 'feather-icons/dist/icons/edit.svg?raw'; import edit from 'feather-icons/dist/icons/edit.svg?raw';
import facebook from 'feather-icons/dist/icons/facebook.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 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 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'; import instagram from 'feather-icons/dist/icons/instagram.svg?raw';
import logIn from 'feather-icons/dist/icons/log-in.svg?raw'; import logIn from 'feather-icons/dist/icons/log-in.svg?raw';
import logOut from 'feather-icons/dist/icons/log-out.svg?raw'; import logOut from 'feather-icons/dist/icons/log-out.svg?raw';
@@ -37,11 +34,8 @@ const ICONS = {
clock, clock,
edit, edit,
facebook, facebook,
gift,
globe, globe,
heart,
home, home,
info,
instagram, instagram,
'log-in': logIn, 'log-in': logIn,
'log-out': logOut, 'log-out': logOut,

View File

@@ -1,6 +1,6 @@
{ {
"name": "marco", "name": "marco",
"version": "1.15.2", "version": "1.13.3",
"private": true, "private": true,
"description": "Unhosted maps app", "description": "Unhosted maps app",
"repository": { "repository": {
@@ -21,7 +21,7 @@
}, },
"scripts": { "scripts": {
"build": "vite build --outDir release/", "build": "vite build --outDir release/",
"build:icons": "cp public/icons/icon-rounded.svg app/icons/icon-rounded.svg && for size in 32 48 144 180 192 512; do if [ \"$size\" -le 64 ]; then magick public/icons/icon.svg -define svg:remove-groups=map-grid -resize ${size}x${size} public/icons/icon-${size}.png; else rsvg-convert -w $size -h $size public/icons/icon.svg -o public/icons/icon-${size}.png; fi; done && rsvg-convert -w 512 -h 512 public/icons/icon.svg -o public/icons/icon-maskable.png", "build:icons": "for size in 32 48 144 180 192 512; do if [ \"$size\" -le 64 ]; then magick public/icons/icon.svg -define svg:remove-groups=map-grid -resize ${size}x${size} public/icons/icon-${size}.png; else rsvg-convert -w $size -h $size public/icons/icon.svg -o public/icons/icon-${size}.png; fi; done && rsvg-convert -w 512 -h 512 public/icons/icon.svg -o public/icons/icon-maskable.png",
"format": "prettier . --cache --write", "format": "prettier . --cache --write",
"lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", "lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto",
"lint:css": "stylelint \"**/*.css\"", "lint:css": "stylelint \"**/*.css\"",
@@ -35,7 +35,7 @@
"lint:js:fix": "eslint . --fix", "lint:js:fix": "eslint . --fix",
"start": "vite", "start": "vite",
"test": "vite build --mode development && testem ci --port 0", "test": "vite build --mode development && testem ci --port 0",
"preversion": "pnpm lint && pnpm test", "preversion": "pnpm test",
"version": "pnpm build && git add release/" "version": "pnpm build && git add release/"
}, },
"devDependencies": { "devDependencies": {
@@ -104,5 +104,6 @@
"dependencies": { "dependencies": {
"ember-concurrency": "^5.2.0", "ember-concurrency": "^5.2.0",
"ember-lifeline": "^7.0.0" "ember-lifeline": "^7.0.0"
} },
"packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be"
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -39,8 +39,8 @@
<meta name="msapplication-TileColor" content="#F6E9A6"> <meta name="msapplication-TileColor" content="#F6E9A6">
<meta name="msapplication-TileImage" content="/icons/icon-144.png"> <meta name="msapplication-TileImage" content="/icons/icon-144.png">
<script type="module" crossorigin src="/assets/main-_2wecUUD.js"></script> <script type="module" crossorigin src="/assets/main-gjk9d6Ld.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-ib8GDjrs.css"> <link rel="stylesheet" crossorigin href="/assets/main-DAo4Q0R2.css">
</head> </head>
<body> <body>
</body> </body>