Compare commits

13 Commits

Author SHA1 Message Date
808c1ee37b 1.12.3
All checks were successful
CI / Lint (push) Successful in 21s
CI / Test (push) Successful in 35s
2026-02-24 22:28:56 +04:00
34bc15cfa9 Only zoom out, not in, when fitting ways/relations 2026-02-24 22:27:52 +04:00
ee5e56910d 1.12.2
All checks were successful
CI / Lint (push) Successful in 21s
CI / Test (push) Successful in 37s
2026-02-24 21:51:21 +04:00
e019fc2d6b Don't include roads and similar in nearby search
Meant to include bus stops, but have to be more specific when re-adding
2026-02-24 21:49:59 +04:00
9e03426b2e 1.12.1
All checks were successful
CI / Lint (push) Successful in 22s
CI / Test (push) Successful in 33s
2026-02-24 20:49:35 +04:00
ecbf77c573 Add OpenGraph and Twitter Card metadata 2026-02-24 20:48:37 +04:00
703a5e8de0 1.12.0 2026-02-24 18:53:27 +04:00
b3c733769c Add app icon before app name in Settings header
All checks were successful
CI / Lint (push) Successful in 26s
CI / Test (push) Successful in 39s
2026-02-24 18:52:07 +04:00
60b2548efd Set up Gitea Actions/CI (#23)
All checks were successful
CI / Lint (push) Successful in 19s
CI / Test (push) Successful in 33s
Reviewed-on: #23
2026-02-24 14:23:01 +00:00
2e632658ad Fix warning 2026-02-24 16:43:06 +04:00
845be96b71 Fix linting errors, improve lint scripts 2026-02-24 16:31:22 +04:00
9ac4273fae Don't use outdated Overpass providers 2026-02-24 15:13:44 +04:00
3a825c3d6c Merge pull request 'Add full-text search' (#20) from feature/10-fulltext_search into master
Reviewed-on: #20
2026-02-24 11:03:57 +00:00
24 changed files with 118 additions and 66 deletions

View File

@@ -18,15 +18,15 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- name: Install Node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Lint
@@ -35,18 +35,16 @@ jobs:
test:
name: "Test"
runs-on: ubuntu-latest
container:
image: cypress/browsers:node-22.19.0-chrome-139.0.7258.154-1-ff-142.0.1-edge-139.0.3405.125-1
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: 22
cache: pnpm
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Run Tests

View File

@@ -1,3 +1,7 @@
export default {
extends: ['stylelint-config-standard'],
rules: {
'no-descending-specificity': null,
'property-no-vendor-prefix': null,
},
};

View File

@@ -1,4 +1,3 @@
<br>
<div align="center">

View File

@@ -65,7 +65,9 @@ export default class IconComponent extends Component {
}
get style() {
return `width:${this.size}px;height:${this.size}px;color:${this.color}`;
return htmlSafe(
`width:${this.size}px;height:${this.size}px;color:${this.color}`
);
}
get title() {

View File

@@ -508,27 +508,29 @@ export default class MapComponent extends Component {
// Top padding: 15% of the VISIBLE height (size[1] * 0.5)
const visibleHeight = size[1] * 0.5;
const topPadding = visibleHeight * 0.15;
const bottomPadding = (size[1] * 0.5) + (visibleHeight * 0.15); // Sheet + padding
const bottomPadding = size[1] * 0.5 + visibleHeight * 0.15; // Sheet + padding
padding[0] = topPadding;
padding[2] = bottomPadding;
}
// Desktop: Sidebar covers left side (approx 400px)
else if (this.args.isSidebarOpen) {
const sidebarWidth = 400;
const sidebarWidth = 400;
const visibleWidth = size[0] - sidebarWidth;
// Left padding: Sidebar + 15% of visible width
padding[3] = sidebarWidth + (visibleWidth * 0.15);
padding[3] = sidebarWidth + visibleWidth * 0.15;
// Right padding: 15% of visible width
padding[1] = visibleWidth * 0.15;
}
const currentZoom = view.getZoom();
view.fit(extent, {
padding: padding,
duration: 1000,
easing: (t) => t * (2 - t),
maxZoom: 19,
maxZoom: currentZoom,
});
}

View File

@@ -98,7 +98,10 @@ export default class PlaceDetails extends Component {
formatMultiLine(val, type) {
if (!val) return null;
const parts = val.split(';').map((s) => s.trim()).filter(Boolean);
const parts = val
.split(';')
.map((s) => s.trim())
.filter(Boolean);
if (parts.length === 0) return null;
if (type === 'phone') {
@@ -129,7 +132,8 @@ export default class PlaceDetails extends Component {
}
get website() {
const val = this.place.url || this.tags.website || this.tags['contact:website'];
const val =
this.place.url || this.tags.website || this.tags['contact:website'];
return this.formatMultiLine(val, 'url');
}
@@ -158,7 +162,10 @@ export default class PlaceDetails extends Component {
get wikipedia() {
const val = this.tags.wikipedia;
if (!val) return null;
return val.split(';').map((s) => s.trim()).filter(Boolean)[0];
return val
.split(';')
.map((s) => s.trim())
.filter(Boolean)[0];
}
get geoLink() {

View File

@@ -45,6 +45,7 @@ export default class PlaceEditForm extends Component {
<form class="edit-form" {{on "submit" this.handleSubmit}}>
<div class="form-group">
<label for="edit-title">Title</label>
{{! template-lint-disable no-autofocus-attribute }}
<input
id="edit-title"
type="text"

View File

@@ -161,7 +161,8 @@ export default class PlacesSidebar extends Component {
><Icon @name="arrow-left" @size={{20}} @color="#333" /></button>
{{else}}
{{#if this.isNearbySearch}}
<h2><Icon @name="target" @size={{20}} @color="#ea4335" /> Nearby</h2>
<h2><Icon @name="target" @size={{20}} @color="#ea4335" />
Nearby</h2>
{{else}}
<h2><Icon @name="search" @size={{20}} @color="#333" /> Results</h2>
{{/if}}

View File

@@ -181,9 +181,11 @@ export default class SearchBoxComponent extends Component {
<div class="result-info">
<span class="result-title">{{result.title}}</span>
{{#if (eq result.source "osm")}}
<span class="result-desc">{{humanizeOsmTag result.type}}</span>
<span class="result-desc">{{humanizeOsmTag
result.type
}}</span>
{{else}}
{{#if result.description}}
{{#if result.description}}
<span class="result-desc">{{result.description}}</span>
{{/if}}
{{/if}}

View File

@@ -4,7 +4,6 @@ import { service } from '@ember/service';
import { action } from '@ember/object';
import Icon from '#components/icon';
import eq from 'ember-truth-helpers/helpers/eq';
import not from 'ember-truth-helpers/helpers/not';
export default class SettingsPane extends Component {
@service settings;
@@ -22,7 +21,10 @@ export default class SettingsPane extends Component {
<template>
<div class="sidebar settings-pane">
<div class="sidebar-header">
<h2>Marco</h2>
<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>

View File

@@ -35,7 +35,6 @@ export default class OsmService extends Service {
'building',
'landuse',
'public_transport',
'highway',
'aeroway',
];
const typeKeysQuery = [`~"^(${typeKeys.join('|')})$"~".*"`];

View File

@@ -8,7 +8,7 @@ export default class SettingsService extends Service {
overpassApis = [
{
name: 'overpass-api.de (DE)',
url: 'https://overpass-api.de/api/interpreter'
url: 'https://overpass-api.de/api/interpreter',
},
{
name: 'private.coffee (AT)',
@@ -32,7 +32,15 @@ export default class SettingsService extends Service {
loadSettings() {
const savedApi = localStorage.getItem('marco:overpass-api');
if (savedApi) {
this.overpassApi = savedApi;
// Check if saved API is still in the allowed list
const isValid = this.overpassApis.some((api) => api.url === savedApi);
if (isValid) {
this.overpassApi = savedApi;
} else {
// If not valid, revert to default
this.overpassApi = 'https://overpass-api.de/api/interpreter';
localStorage.setItem('marco:overpass-api', this.overpassApi);
}
}
const savedKinetic = localStorage.getItem('marco:map-kinetic');

View File

@@ -374,10 +374,7 @@ body {
.places-list {
list-style: none;
padding: 0;
margin: -1rem -1rem 0 -1rem;
}
.places-list li {
margin: -1rem -1rem 0;
}
.place-item {
@@ -511,6 +508,7 @@ body {
border: 2px solid rgb(255 204 51 / 80%); /* Gold/Yellow to match markers */
background: rgb(255 204 51 / 20%);
position: absolute;
/* Use translate3d for GPU acceleration on iOS */
transform: translate3d(-50%, -50%, 0);
pointer-events: none;
@@ -548,6 +546,7 @@ body {
.ol-control.ol-attribution {
bottom: 1rem;
}
.ol-touch .ol-control.ol-attribution {
bottom: 0.5rem;
}
@@ -555,6 +554,7 @@ body {
.ol-control.ol-zoom {
bottom: 3rem;
}
.ol-touch .ol-control.ol-zoom {
bottom: 3.5rem;
}
@@ -562,6 +562,7 @@ body {
.ol-control.ol-locate {
bottom: 6.5rem;
}
.ol-touch .ol-control.ol-locate {
bottom: 8.5rem;
}
@@ -569,6 +570,7 @@ body {
.ol-control.ol-rotate {
bottom: 9rem;
}
.ol-touch .ol-control.ol-rotate {
bottom: 11.5rem;
}
@@ -695,6 +697,7 @@ span.icon {
/* Map Crosshair for "Create Place" mode */
.map-crosshair {
position: absolute;
/* Default Center */
top: 50%;
left: 50%;
@@ -715,8 +718,11 @@ span.icon {
}
/* Sidebar is open (Desktop: Left 300px) */
/* We want to center in the remaining space (width - 300px) */
/* 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 {
left: calc(50% + 150px);
@@ -724,6 +730,7 @@ span.icon {
@media (width <= 768px) {
/* Sidebar/Bottom Sheet is open (Mobile: Bottom 50%) */
/* Center Y = (height/2) / 2 = height/4 = 25% */
.map-container.sidebar-open .map-crosshair {
left: 50%; /* Reset desktop shift */
@@ -788,14 +795,14 @@ button.create-place {
align-items: center;
background: white;
border-radius: 24px; /* Pill shape */
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
box-shadow: 0 2px 5px rgb(0 0 0 / 15%);
padding: 0 0.5rem;
height: 48px; /* Slightly taller for touch targets */
transition: box-shadow 0.2s;
}
.search-form:focus-within {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
}
/* Integrated Menu Button */
@@ -813,7 +820,7 @@ button.create-place {
}
.menu-btn-integrated:hover {
background: rgba(0, 0, 0, 0.05);
background: rgb(0 0 0 / 5%);
}
/* Fallback Search Icon (Left) */
@@ -837,6 +844,7 @@ button.create-place {
outline: none;
width: 100%;
padding: 0 4px;
/* Remove native search cancel button in WebKit */
-webkit-appearance: none;
}
@@ -858,25 +866,11 @@ button.create-place {
color: #5f6368;
border-radius: 50%;
margin-left: 4px;
border-left: 1px solid #ddd; /* Separator like Google Maps */
padding-left: 12px;
border-radius: 0; /* Reset for separator look */
}
.search-submit-btn:hover {
/* No background on hover if we use separator style, or maybe just change icon color */
color: #1a73e8; /* Blue on hover */
}
/* If we want the separator style, we need to adjust border-radius carefully or use a pseudo element */
/* Let's stick to a simple button for now, maybe without the separator if it looks cleaner */
.search-submit-btn {
border-left: none; /* Remove separator for cleaner look */
padding-left: 8px;
border-radius: 50%;
}
.search-submit-btn:hover {
background: rgba(0, 0, 0, 0.05);
background: rgb(0 0 0 / 5%);
color: #333;
}
@@ -894,7 +888,7 @@ button.create-place {
}
.search-clear-btn:hover {
background: rgba(0, 0, 0, 0.05);
background: rgb(0 0 0 / 5%);
color: #333;
}
@@ -907,7 +901,7 @@ button.create-place {
margin-top: 8px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
overflow: hidden;
max-height: 400px;
overflow-y: auto;

View File

@@ -3,9 +3,22 @@
<head>
<meta charset="utf-8">
<title>Marco</title>
<meta name="description" content="">
<meta name="description" content="Unhosted maps app that respects your privacy and choices.">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Open Graph -->
<meta property="og:title" content="Marco">
<meta property="og:description" content="Unhosted maps app that respects your privacy and choices.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://marco.kosmos.org">
<meta property="og:image" content="https://marco.kosmos.org/icons/icon-512.png">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Marco">
<meta name="twitter:description" content="Unhosted maps app that respects your privacy and choices.">
<meta name="twitter:image" content="https://marco.kosmos.org/icons/icon-512.png">
<!-- App identity -->
<meta name="application-name" content="Marco">
<meta name="apple-mobile-web-app-title" content="Marco">

View File

@@ -1,6 +1,6 @@
{
"name": "marco",
"version": "1.11.4",
"version": "1.12.3",
"private": true,
"description": "Unhosted maps app",
"repository": {
@@ -25,9 +25,10 @@
"format": "prettier . --cache --write",
"lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto",
"lint:css": "stylelint \"**/*.css\"",
"lint:css:fix": "concurrently \"pnpm:lint:css -- --fix\"",
"lint:css:fix": "stylelint \"**/*.css\" --fix",
"lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\" --prefixColors auto && pnpm format",
"lint:format": "prettier . --cache --check",
"lint:format:fix": "prettier . --cache --write",
"lint:hbs": "ember-template-lint .",
"lint:hbs:fix": "ember-template-lint . --fix",
"lint:js": "eslint . --cache",

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

@@ -3,9 +3,22 @@
<head>
<meta charset="utf-8">
<title>Marco</title>
<meta name="description" content="">
<meta name="description" content="Unhosted maps app that respects your privacy and choices.">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Open Graph -->
<meta property="og:title" content="Marco">
<meta property="og:description" content="Unhosted maps app that respects your privacy and choices.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://marco.kosmos.org">
<meta property="og:image" content="https://marco.kosmos.org/icons/icon-512.png">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Marco">
<meta name="twitter:description" content="Unhosted maps app that respects your privacy and choices.">
<meta name="twitter:image" content="https://marco.kosmos.org/icons/icon-512.png">
<!-- App identity -->
<meta name="application-name" content="Marco">
<meta name="apple-mobile-web-app-title" content="Marco">
@@ -26,8 +39,8 @@
<meta name="msapplication-TileColor" content="#F6E9A6">
<meta name="msapplication-TileImage" content="/icons/icon-144.png">
<script type="module" crossorigin src="/assets/main-ji2SNMnp.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-G8wPYi_P.css">
<script type="module" crossorigin src="/assets/main-dKBF4UHr.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-DoLYcE7E.css">
</head>
<body>
</body>

View File

@@ -1,5 +1,5 @@
import { module, test } from 'qunit';
import { visit, currentURL, click, settled } from '@ember/test-helpers';
import { visit, currentURL, click } from '@ember/test-helpers';
import { setupApplicationTest } from 'marco/tests/helpers';
import Service from '@ember/service';
import sinon from 'sinon';

View File

@@ -37,7 +37,9 @@ module('Integration | Component | search-box', function (hooks) {
this.owner.register('service:router', MockRouterService);
this.noop = () => {};
await render(<template><SearchBox @onToggleMenu={{this.noop}} /></template>);
await render(
<template><SearchBox @onToggleMenu={{this.noop}} /></template>
);
assert.dom('.search-input').exists();
assert.dom('.search-results-popover').doesNotExist();
@@ -86,7 +88,9 @@ module('Integration | Component | search-box', function (hooks) {
this.owner.register('service:router', MockRouterService);
this.noop = () => {};
await render(<template><SearchBox @onToggleMenu={{this.noop}} /></template>);
await render(
<template><SearchBox @onToggleMenu={{this.noop}} /></template>
);
await fillIn('.search-input', 'berlin');
await click('.search-input'); // Focus
@@ -118,7 +122,9 @@ module('Integration | Component | search-box', function (hooks) {
this.owner.register('service:photon', MockPhotonService);
this.noop = () => {};
await render(<template><SearchBox @onToggleMenu={{this.noop}} /></template>);
await render(
<template><SearchBox @onToggleMenu={{this.noop}} /></template>
);
await fillIn('.search-input', 'cafe');

View File

@@ -32,7 +32,7 @@ module('Unit | Route | place', function (hooks) {
}
class MapUiStub extends Service {
selectPlace(place) {
selectPlace() {
selectPlaceCalled = true;
}
stopSearch() {}