8 Commits

Author SHA1 Message Date
f60dacac80 1.5.0 2026-01-24 14:16:40 +07:00
9a02363515 Move all map controls to bottom right corner 2026-01-24 14:16:08 +07:00
f28be0c994 Add user/accounts menu, RS connect 2026-01-24 13:51:29 +07:00
721fe5f01d Fix linting/formatting 2026-01-24 12:52:19 +07:00
518685b7dc Improve secondary btn style 2026-01-24 11:17:07 +07:00
262e5b61a8 1.4.3 2026-01-23 16:52:21 +07:00
f87d8bdda9 Improve save button styles 2026-01-23 16:51:53 +07:00
f17f8ca17b Use feather icons in sidebar header 2026-01-23 15:49:59 +07:00
18 changed files with 545 additions and 93 deletions

View File

@@ -0,0 +1,59 @@
import Component from '@glimmer/component';
import { service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { on } from '@ember/modifier';
import Icon from '#components/icon';
import UserMenu from '#components/user-menu';
export default class AppHeaderComponent extends Component {
@service storage;
@tracked isUserMenuOpen = false;
@action
toggleUserMenu() {
this.isUserMenuOpen = !this.isUserMenuOpen;
}
@action
closeUserMenu() {
this.isUserMenuOpen = false;
}
<template>
<header class="app-header">
<div class="header-left">
<button class="icon-btn" type="button" aria-label="Menu">
<Icon @name="menu" @size={{24}} @color="#333" />
</button>
</div>
<div class="header-right">
<div class="user-menu-container">
<button
class="user-btn"
type="button"
aria-label="User Menu"
{{on "click" this.toggleUserMenu}}
>
<div class="user-avatar-placeholder">
<Icon @name="user" @size={{20}} @color="white" />
</div>
</button>
{{#if this.isUserMenuOpen}}
<UserMenu
@storage={{this.storage}}
@onClose={{this.closeUserMenu}}
/>
<div
class="menu-backdrop"
{{on "click" this.closeUserMenu}}
role="button"
></div>
{{/if}}
</div>
</div>
</header>
</template>
}

View File

@@ -1,26 +1,44 @@
import Component from '@glimmer/component'; import Component from '@glimmer/component';
import { htmlSafe } from '@ember/template'; import { htmlSafe } from '@ember/template';
import arrowLeft from 'feather-icons/dist/icons/arrow-left.svg?raw';
import activity from 'feather-icons/dist/icons/activity.svg?raw';
import bookmark from 'feather-icons/dist/icons/bookmark.svg?raw';
import clock from 'feather-icons/dist/icons/clock.svg?raw'; import clock from 'feather-icons/dist/icons/clock.svg?raw';
import globe from 'feather-icons/dist/icons/globe.svg?raw'; import globe from 'feather-icons/dist/icons/globe.svg?raw';
import home from 'feather-icons/dist/icons/home.svg?raw'; import home from 'feather-icons/dist/icons/home.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 map from 'feather-icons/dist/icons/map.svg?raw'; import map from 'feather-icons/dist/icons/map.svg?raw';
import mapPin from 'feather-icons/dist/icons/map-pin.svg?raw'; import mapPin from 'feather-icons/dist/icons/map-pin.svg?raw';
import menu from 'feather-icons/dist/icons/menu.svg?raw';
import navigation from 'feather-icons/dist/icons/navigation.svg?raw'; import navigation from 'feather-icons/dist/icons/navigation.svg?raw';
import phone from 'feather-icons/dist/icons/phone.svg?raw'; import phone from 'feather-icons/dist/icons/phone.svg?raw';
import user from 'feather-icons/dist/icons/user.svg?raw'; import server from 'feather-icons/dist/icons/server.svg?raw';
import settings from 'feather-icons/dist/icons/settings.svg?raw'; import settings from 'feather-icons/dist/icons/settings.svg?raw';
import user from 'feather-icons/dist/icons/user.svg?raw';
import x from 'feather-icons/dist/icons/x.svg?raw';
import zap from 'feather-icons/dist/icons/zap.svg?raw';
const ICONS = { const ICONS = {
'arrow-left': arrowLeft,
activity,
bookmark,
clock, clock,
globe, globe,
home, home,
'log-in': logIn,
'log-out': logOut,
map, map,
mapPin, 'map-pin': mapPin,
menu,
navigation, navigation,
phone, phone,
server,
settings,
user, user,
settings x,
zap,
}; };
export default class IconComponent extends Component { export default class IconComponent extends Component {

View File

@@ -61,7 +61,6 @@ export default class MapComponent extends Component {
zIndex: 10, // Ensure it sits above the map tiles zIndex: 10, // Ensure it sits above the map tiles
}); });
// Default view settings // Default view settings
let center = [99.05738, 7.55087]; let center = [99.05738, 7.55087];
let zoom = 13.0; let zoom = 13.0;
@@ -95,7 +94,11 @@ export default class MapComponent extends Component {
target: element, target: element,
layers: [openfreemap, bookmarkLayer], layers: [openfreemap, bookmarkLayer],
view: view, view: view,
controls: defaultControls({ zoom: false, rotate: true, attribution: true }), controls: defaultControls({
zoom: false,
rotate: true,
attribution: true,
}),
}); });
apply(this.mapInstance, 'https://tiles.openfreemap.org/styles/liberty'); apply(this.mapInstance, 'https://tiles.openfreemap.org/styles/liberty');
@@ -210,7 +213,9 @@ export default class MapComponent extends Component {
geolocation.un('change:position', zoomToLocation); geolocation.un('change:position', zoomToLocation);
locateListenerKey = null; locateListenerKey = null;
} }
} catch (e) { /* ignore */ } } catch (e) {
/* ignore */
}
// Hide pulse // Hide pulse
if (this.locationOverlayElement) { if (this.locationOverlayElement) {
@@ -261,7 +266,8 @@ export default class MapComponent extends Component {
else if (accuracy) { else if (accuracy) {
const viewportWidthMeters = 6.325 * accuracy; const viewportWidthMeters = 6.325 * accuracy;
const minDimensionPixels = Math.min(size[0], size[1]); const minDimensionPixels = Math.min(size[0], size[1]);
const requiredResolutionMeters = viewportWidthMeters / minDimensionPixels; const requiredResolutionMeters =
viewportWidthMeters / minDimensionPixels;
const metersPerMapUnit = getPointResolution( const metersPerMapUnit = getPointResolution(
view.getProjection(), view.getProjection(),
1, 1,
@@ -443,23 +449,23 @@ export default class MapComponent extends Component {
// If the pin is in the bottom half (y > splitPoint), it is obscured // If the pin is in the bottom half (y > splitPoint), it is obscured
if (pixel[1] > splitPoint) { if (pixel[1] > splitPoint) {
// Target position: Center of top half = height * 0.25 // Target position: Center of top half = height * 0.25
const targetY = height * 0.25; const targetY = height * 0.25;
const deltaY = pixel[1] - targetY; const deltaY = pixel[1] - targetY;
const view = this.mapInstance.getView(); const view = this.mapInstance.getView();
const center = view.getCenter(); const center = view.getCenter();
const resolution = view.getResolution(); const resolution = view.getResolution();
// Move the map center SOUTH (decrease Y) to move the pin UP (decrease pixel Y) // Move the map center SOUTH (decrease Y) to move the pin UP (decrease pixel Y)
const deltaMapUnits = deltaY * resolution; const deltaMapUnits = deltaY * resolution;
const newCenter = [center[0], center[1] - deltaMapUnits]; const newCenter = [center[0], center[1] - deltaMapUnits];
view.animate({ view.animate({
center: newCenter, center: newCenter,
duration: 500, duration: 500,
easing: (t) => t * (2 - t) // Ease-out easing: (t) => t * (2 - t), // Ease-out
}); });
} }
} }
@@ -525,7 +531,7 @@ export default class MapComponent extends Component {
const viewState = { const viewState = {
center: currentCenter, center: currentCenter,
zoom: currentZoom zoom: currentZoom,
}; };
localStorage.setItem('marco:map-view', JSON.stringify(viewState)); localStorage.setItem('marco:map-view', JSON.stringify(viewState));
@@ -545,7 +551,9 @@ export default class MapComponent extends Component {
if (features && features.length > 0) { if (features && features.length > 0) {
console.debug(`Found ${features.length} features in map layer:`); console.debug(`Found ${features.length} features in map layer:`);
for (const f of features) { console.debug(f) } for (const f of features) {
console.debug(f);
}
const bookmarkFeature = features.find((f) => f.get('isBookmark')); const bookmarkFeature = features.find((f) => f.get('isBookmark'));
if (bookmarkFeature) { if (bookmarkFeature) {
clickedBookmark = bookmarkFeature.get('originalPlace'); clickedBookmark = bookmarkFeature.get('originalPlace');

View File

@@ -79,10 +79,10 @@ export default class PlaceDetails extends Component {
get cuisine() { get cuisine() {
if (!this.tags.cuisine) return null; if (!this.tags.cuisine) return null;
return this.tags.cuisine return this.tags.cuisine
.split(';') .split(';')
.map(c => capitalize.compute([c])) .map((c) => capitalize.compute([c]))
.map(c => c.replace('_', ' ')) .map((c) => c.replace('_', ' '))
.join(', '); .join(', ');
} }
get wikipedia() { get wikipedia() {
@@ -123,17 +123,25 @@ export default class PlaceDetails extends Component {
</p> </p>
{{#if this.place.description}} {{#if this.place.description}}
<p class="place-description"> <p class="place-description">
{{this.place.description}} {{this.place.description}}
</p> </p>
{{/if}} {{/if}}
<div class="actions"> <div class="actions">
<button <button
type="button" type="button"
class={{if this.place.createdAt "btn-secondary" "btn-primary"}} class={{if
this.place.createdAt
"btn btn-secondary"
"btn btn-outline"
}}
{{on "click" (fn @onToggleSave this.place)}} {{on "click" (fn @onToggleSave this.place)}}
> >
{{if this.place.createdAt "Saved ✓" "Save"}} <Icon
@name="bookmark"
@color={{if this.place.createdAt "currentColor" "#007bff"}}
/>
{{if this.place.createdAt "Saved" "Save"}}
</button> </button>
</div> </div>
@@ -163,14 +171,22 @@ export default class PlaceDetails extends Component {
{{#if this.website}} {{#if this.website}}
<p class="content-with-icon"> <p class="content-with-icon">
<Icon @name="globe" @title="Website" /> <Icon @name="globe" @title="Website" />
<span><a href={{this.website}} target="_blank" rel="noopener noreferrer">{{this.websiteDomain}}</a></span> <span><a
href={{this.website}}
target="_blank"
rel="noopener noreferrer"
>{{this.websiteDomain}}</a></span>
</p> </p>
{{/if}} {{/if}}
{{#if this.wikipedia}} {{#if this.wikipedia}}
<p> <p>
<strong>Wikipedia:</strong> <strong>Wikipedia:</strong>
<a href="https://wikipedia.org/wiki/{{this.wikipedia}}" target="_blank" rel="noopener noreferrer">Article</a> <a
href="https://wikipedia.org/wiki/{{this.wikipedia}}"
target="_blank"
rel="noopener noreferrer"
>Article</a>
</p> </p>
{{/if}} {{/if}}
@@ -185,7 +201,7 @@ export default class PlaceDetails extends Component {
{{/if}} {{/if}}
<p class="content-with-icon"> <p class="content-with-icon">
<Icon @name="mapPin" @title="Geo link" /> <Icon @name="map-pin" @title="Geo link" />
<span> <span>
<a href={{this.geoLink}} target="_blank" rel="noopener noreferrer"> <a href={{this.geoLink}} target="_blank" rel="noopener noreferrer">
{{this.visibleGeoLink}} {{this.visibleGeoLink}}

View File

@@ -5,6 +5,7 @@ import { on } from '@ember/modifier';
import { fn } from '@ember/helper'; import { fn } from '@ember/helper';
import or from 'ember-truth-helpers/helpers/or'; import or from 'ember-truth-helpers/helpers/or';
import PlaceDetails from './place-details'; import PlaceDetails from './place-details';
import Icon from './icon';
export default class PlacesSidebar extends Component { export default class PlacesSidebar extends Component {
@service storage; @service storage;
@@ -49,23 +50,23 @@ export default class PlacesSidebar extends Component {
// Update selection to the new saved place object // Update selection to the new saved place object
// This updates the local UI state immediately without a route refresh // This updates the local UI state immediately without a route refresh
if (this.args.onUpdate) { if (this.args.onUpdate) {
// When deleting, we revert to a "fresh" object or just close. // When deleting, we revert to a "fresh" object or just close.
// Since we close the sidebar below, we might not strictly need to update local state, // Since we close the sidebar below, we might not strictly need to update local state,
// but it's good practice. // but it's good practice.
// Reconstruct the "original" place without ID/Geohash/CreatedAt // Reconstruct the "original" place without ID/Geohash/CreatedAt
const freshPlace = { const freshPlace = {
...place, ...place,
id: undefined, id: undefined,
geohash: undefined, geohash: undefined,
createdAt: undefined createdAt: undefined,
}; };
this.args.onUpdate(freshPlace); this.args.onUpdate(freshPlace);
} }
// Also fire onSelect if it exists (for list view) // Also fire onSelect if it exists (for list view)
if (this.args.onSelect) { if (this.args.onSelect) {
// Similar logic for select if needed, but we usually close. // Similar logic for select if needed, but we usually close.
this.args.onSelect(null); this.args.onSelect(null);
} }
// Close sidebar after delete // Close sidebar after delete
@@ -80,7 +81,8 @@ export default class PlacesSidebar extends Component {
} else { } else {
// It's a fresh POI -> Save it // It's a fresh POI -> Save it
const placeData = { const placeData = {
title: place.osmTags.name || place.osmTags['name:en'] || 'Untitled Place', title:
place.osmTags.name || place.osmTags['name:en'] || 'Untitled Place',
lat: place.lat, lat: place.lat,
lon: place.lon, lon: place.lon,
tags: [], tags: [],
@@ -123,15 +125,15 @@ export default class PlacesSidebar extends Component {
type="button" type="button"
class="back-btn" class="back-btn"
{{on "click" this.clearSelection}} {{on "click" this.clearSelection}}
></button> ><Icon @name="arrow-left" @size={{20}} @color="#333" /></button>
{{else}} {{else}}
<h2>Nearby Places</h2> <h2>Nearby Places</h2>
{{/if}} {{/if}}
<button <button type="button" class="close-btn" {{on "click" @onClose}}><Icon
type="button" @name="x"
class="close-btn" @size={{20}}
{{on "click" @onClose}} @color="#333"
>×</button> /></button>
</div> </div>
<div class="sidebar-content"> <div class="sidebar-content">

View File

@@ -0,0 +1,66 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import Icon from '#components/icon';
import { on } from '@ember/modifier';
export default class UserMenuComponent extends Component {
@action
connectRS() {
this.args.onClose();
this.args.storage.connect();
}
@action
disconnectRS() {
this.args.storage.disconnect();
}
<template>
<div class="user-menu-popover">
<div class="user-status">
{{#if @storage.connected}}
Connected as
<strong>{{@storage.userAddress}}</strong>
{{else}}
Not connected
{{/if}}
</div>
<ul class="account-list">
<li class="account-item">
<div class="account-info">
<Icon @name="server" @size={{18}} />
<span>RemoteStorage</span>
</div>
{{#if @storage.connected}}
<button
class="btn-text text-danger"
type="button"
{{on "click" this.disconnectRS}}
>Disconnect</button>
{{else}}
<button
class="btn-text text-primary"
type="button"
{{on "click" this.connectRS}}
>Connect</button>
{{/if}}
</li>
<li class="account-item disabled">
<div class="account-info">
<Icon @name="globe" @size={{18}} />
<span>OpenStreetMap</span>
</div>
</li>
<li class="account-item disabled">
<div class="account-info">
<Icon @name="zap" @size={{18}} />
<span>Nostr</span>
</div>
</li>
</ul>
</div>
</template>
}

View File

@@ -24,7 +24,7 @@ out center;
`.trim(); `.trim();
const url = `https://overpass.bke.ro/api/interpreter?data=${encodeURIComponent( const url = `https://overpass.bke.ro/api/interpreter?data=${encodeURIComponent(
// const url = `https://overpass-api.de/api/interpreter?data=${encodeURIComponent( // const url = `https://overpass-api.de/api/interpreter?data=${encodeURIComponent(
query query
)}`; )}`;
@@ -100,7 +100,8 @@ out center;
`.trim(); `.trim();
} }
const url = `https://overpass-api.de/api/interpreter?data=${encodeURIComponent( const url = `https://overpass.bke.ro/api/interpreter?data=${encodeURIComponent(
// const url = `https://overpass-api.de/api/interpreter?data=${encodeURIComponent(
query query
)}`; )}`;
const res = await this.fetchWithRetry(url); const res = await this.fetchWithRetry(url);

View File

@@ -1,19 +1,25 @@
import Service from '@ember/service'; import Service from '@ember/service';
import RemoteStorage from 'remotestoragejs'; import RemoteStorage from 'remotestoragejs';
import Places from '@remotestorage/module-places'; import Places from '@remotestorage/module-places';
import Widget from 'remotestorage-widget';
import { tracked } from '@glimmer/tracking'; import { tracked } from '@glimmer/tracking';
import { getGeohashPrefixesInBbox } from '../utils/geohash-coverage'; import { getGeohashPrefixesInBbox } from '../utils/geohash-coverage';
import { action } from '@ember/object';
import { debounce } from '@ember/runloop'; import { debounce } from '@ember/runloop';
import Geohash from 'latlon-geohash'; import Geohash from 'latlon-geohash';
export default class StorageService extends Service { export default class StorageService extends Service {
rs; rs;
widget;
@tracked placesInView = []; @tracked placesInView = [];
@tracked savedPlaces = []; @tracked savedPlaces = [];
@tracked loadedPrefixes = []; @tracked loadedPrefixes = [];
@tracked currentBbox = null; @tracked currentBbox = null;
@tracked version = 0; // Shared version tracker for bookmarks @tracked version = 0; // Shared version tracker for bookmarks
@tracked initialSyncDone = false; @tracked initialSyncDone = false;
@tracked connected = false;
@tracked userAddress = null;
@tracked isWidgetOpen = false;
constructor() { constructor() {
super(...arguments); super(...arguments);
@@ -29,14 +35,38 @@ export default class StorageService extends Service {
window.remoteStorage = this.rs; window.remoteStorage = this.rs;
// const widget = new Widget(this.rs); this.widget = new Widget(this.rs, {
// widget.attach(); leaveOpen: true,
skipInitial: true,
});
// We don't attach immediately; we'll attach when the user clicks Connect
this.rs.on('ready', () => { this.rs.on('ready', () => {
// console.debug('[rs] client ready'); // console.debug('[rs] client ready');
}); });
this.rs.on('sync-done', (result) => { this.rs.on('connected', () => {
console.debug('Remote storage connected');
this.connected = true;
this.userAddress = this.rs.remote.userAddress;
// Close widget after successful connection (respecting autoCloseAfter)
setTimeout(() => {
this.isWidgetOpen = false;
}, 1500);
});
this.rs.on('disconnected', () => {
console.debug('Remote storage disconnected');
this.connected = false;
this.userAddress = null;
this.placesInView = [];
this.savedPlaces = [];
this.loadedPrefixes = [];
this.initialSyncDone = false;
});
this.rs.on('sync-done', () => {
// console.debug('[rs] sync done:', result); // console.debug('[rs] sync done:', result);
if (!this.initialSyncDone) { if (!this.initialSyncDone) {
this.initialSyncDone = true; this.initialSyncDone = true;
@@ -157,7 +187,7 @@ export default class StorageService extends Service {
// If the hash is in the set of reloaded prefixes, we discard the old version // If the hash is in the set of reloaded prefixes, we discard the old version
// (because the 'places' array contains the authoritative new state for this prefix) // (because the 'places' array contains the authoritative new state for this prefix)
return !prefixSet.has(hash); return !prefixSet.has(hash);
} catch (e) { } catch {
return true; // Keep malformed/unknown places safe return true; // Keep malformed/unknown places safe
} }
}); });
@@ -198,6 +228,28 @@ export default class StorageService extends Service {
async removePlace(place) { async removePlace(place) {
await this.places.remove(place.id, place.geohash); await this.places.remove(place.id, place.geohash);
this.savedPlaces = this.savedPlaces.filter(p => p.id !== place.id); this.savedPlaces = this.savedPlaces.filter((p) => p.id !== place.id);
}
@action
connect() {
this.isWidgetOpen = true;
// Check if widget is already attached
if (!document.querySelector('.rs-widget')) {
// Attach to our specific container
this.widget.attach('rs-widget-container');
}
}
@action
closeWidget() {
this.isWidgetOpen = false;
}
@action
disconnect() {
this.rs.disconnect();
this.isWidgetOpen = false;
} }
} }

View File

@@ -22,12 +22,161 @@ body {
outline: none; /* Prevent focus outline on click */ outline: none; /* Prevent focus outline on click */
} }
/* Ensure RS widget is above the map */ /* Ensure RS widget is above the map but potentially hidden initially if needed */
#remotestorage-widget { #rs-widget-container {
position: absolute; position: absolute;
top: 10px; top: 60px; /* Below header */
right: 10px; right: 10px;
z-index: 1000; z-index: 4000;
display: none; /* Hidden by default */
}
#rs-widget-container.visible {
display: block;
}
.rs-backdrop {
position: fixed;
inset: 0;
z-index: 3999; /* Below widget container but above everything else */
/* background: rgba(0,0,0,0.2); Optional: dim background */
}
/* App Header */
.app-header {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 60px;
padding: 0 1rem;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 3000; /* Above sidebar (2000) and map */
pointer-events: none; /* Let clicks pass through to map where transparent */
}
.header-left,
.header-right {
pointer-events: auto; /* Re-enable clicks for buttons */
}
.icon-btn {
background: white;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
cursor: pointer;
transition: transform 0.1s;
}
.icon-btn:active {
transform: scale(0.95);
}
.user-btn {
background: none;
border: none;
cursor: pointer;
padding: 0;
}
.user-avatar-placeholder {
width: 40px;
height: 40px;
background: #333;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
/* User Menu Popover */
.user-menu-container {
position: relative;
}
.user-menu-popover {
position: absolute;
top: 100%;
right: 0;
margin-top: 10px;
width: 280px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 1rem;
z-index: 3001;
}
.menu-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 3000; /* Below popover but above everything else */
/* background: rgba(0,0,0,0.1); Optional dimming */
}
.user-status {
padding-bottom: 1rem;
margin-bottom: 1rem;
border-bottom: 1px solid #eee;
color: #666;
font-size: 0.9rem;
}
.account-list {
list-style: none;
padding: 0;
margin: 0;
}
.account-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0;
}
.account-item.disabled {
opacity: 0.5;
pointer-events: none;
}
.account-info {
display: flex;
align-items: center;
gap: 0.75rem;
font-weight: 500;
}
.btn-text {
background: none;
border: none;
padding: 0;
font-weight: 600;
cursor: pointer;
font-size: 0.9rem;
}
.text-primary {
color: #007bff;
}
.text-danger {
color: #dc3545;
}
.btn-text:hover {
text-decoration: underline;
} }
/* Sidebar Styles */ /* Sidebar Styles */
@@ -62,20 +211,26 @@ body {
padding: 1rem; padding: 1rem;
} }
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
padding: 0 0.5rem;
}
.back-btn { .back-btn {
background: none; background: none;
border: none; border: none;
font-size: 1.2rem;
cursor: pointer; cursor: pointer;
margin-right: 0.5rem; padding: 0 0.5rem;
margin-left: -0.5rem;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn {
background: none;
border: none;
cursor: pointer;
padding: 0 0.5rem;
margin-right: -0.5rem;
display: flex;
align-items: center;
justify-content: center;
} }
.place-details { .place-details {
@@ -99,23 +254,63 @@ body {
.place-details .actions { .place-details .actions {
padding-bottom: 0.3rem; padding-bottom: 0.3rem;
/* display: flex; */
/* flex-direction: row; */
/* gap: 1rem; */
} }
.btn-primary { .btn {
background: #007bff;
color: white;
border: none;
padding: 0.75rem 1.5rem; padding: 0.75rem 1.5rem;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
width: 100%; /* width: 50%; */
font-size: 1rem; font-size: 1rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
} }
.btn-primary:hover { .btn-outline {
background: transparent;
color: #333;
border: 1px solid #ccc;
}
.btn-outline:hover {
border: 1px solid #898989;
}
.btn-secondary {
color: #333;
border: 1px solid rgba(255, 204, 51, 0.2);
background: rgba(255, 204, 51, 0.3);
}
.btn-secondary:hover {
background: rgba(255, 204, 51, 0.4);
}
.btn-blue {
background: #007bff;
color: white;
border: none;
}
.btn-blue:hover {
background: #0056b3; background: #0056b3;
} }
.btn-green {
background: #198754;
color: white;
border: none;
}
.btn-green:hover {
background: #157347;
}
.places-list { .places-list {
list-style: none; list-style: none;
padding: 0; padding: 0;
@@ -218,13 +413,28 @@ body {
/* Locate Control */ /* Locate Control */
.ol-control.ol-locate { .ol-control.ol-locate {
top: 5em; /* Position below zoom controls (usually at .5em or similar) */ top: auto;
bottom: 2.5em;
right: 0.5em; right: 0.5em;
left: auto; left: auto;
} }
.ol-touch .ol-control.ol-locate { .ol-touch .ol-control.ol-locate {
top: 5.5em; /* Adjust for touch devices where controls might be larger */ top: auto;
bottom: 3.5em;
}
/* Rotate Control */
.ol-rotate {
top: auto;
bottom: 5em;
right: 0.5em;
left: auto;
}
.ol-touch .ol-rotate {
top: auto;
bottom: 6em;
} }
span.icon { span.icon {

View File

@@ -2,11 +2,13 @@ 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 PlacesSidebar from '#components/places-sidebar'; import PlacesSidebar from '#components/places-sidebar';
import AppHeader from '#components/app-header';
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';
import { eq } from 'ember-truth-helpers'; import { eq } from 'ember-truth-helpers';
import { and } from 'ember-truth-helpers'; import { and } from 'ember-truth-helpers';
import { on } from '@ember/modifier';
export default class ApplicationComponent extends Component { export default class ApplicationComponent extends Component {
@service storage; @service storage;
@@ -74,6 +76,21 @@ export default class ApplicationComponent extends Component {
<template> <template>
{{pageTitle "M/\RCO"}} {{pageTitle "M/\RCO"}}
<AppHeader />
<div
id="rs-widget-container"
class={{if this.storage.isWidgetOpen "visible"}}
></div>
{{#if this.storage.isWidgetOpen}}
<div
class="rs-backdrop"
role="button"
{{on "click" this.storage.closeWidget}}
></div>
{{/if}}
<Map <Map
@onPlacesFound={{this.showPlaces}} @onPlacesFound={{this.showPlaces}}
@isSidebarOpen={{this.isSidebarOpen}} @isSidebarOpen={{this.isSidebarOpen}}

View File

@@ -38,8 +38,11 @@ export default class PlaceTemplate extends Component {
// 3. If not saved, check our local "optimistic" state (from handleUpdate) // 3. If not saved, check our local "optimistic" state (from handleUpdate)
// This handles the "unsaved" state immediately after deletion before any other sync // This handles the "unsaved" state immediately after deletion before any other sync
if (this.localPlace && (this.localPlace.osmId === id || this.localPlace.id === id)) { if (
return this.localPlace; this.localPlace &&
(this.localPlace.osmId === id || this.localPlace.id === id)
) {
return this.localPlace;
} }
// 4. Fallback to the route model (which might be the stale "saved" object from when the route loaded) // 4. Fallback to the route model (which might be the stale "saved" object from when the route loaded)
@@ -50,7 +53,7 @@ export default class PlaceTemplate extends Component {
...model, ...model,
id: undefined, id: undefined,
createdAt: undefined, createdAt: undefined,
geohash: undefined geohash: undefined,
}; };
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "marco", "name": "marco",
"version": "1.4.2", "version": "1.5.0",
"private": true, "private": true,
"description": "Small description for marco goes here", "description": "Small description for marco goes here",
"repository": "", "repository": "",

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

@@ -6,8 +6,8 @@
<meta name="description" content=""> <meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<script type="module" crossorigin src="/assets/main-C6x36ClG.js"></script> <script type="module" crossorigin src="/assets/main-DPNrocGB.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-_X0dk-zm.css"> <link rel="stylesheet" crossorigin href="/assets/main-h3_9Qqi3.css">
</head> </head>
<body> <body>
</body> </body>