diff --git a/app/components/app-header.gjs b/app/components/app-header.gjs
new file mode 100644
index 0000000..5654380
--- /dev/null
+++ b/app/components/app-header.gjs
@@ -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;
+ }
+
+
+
+
+}
diff --git a/app/components/icon.gjs b/app/components/icon.gjs
index e0ccc11..e65be9f 100644
--- a/app/components/icon.gjs
+++ b/app/components/icon.gjs
@@ -2,31 +2,43 @@ import Component from '@glimmer/component';
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 globe from 'feather-icons/dist/icons/globe.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 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 phone from 'feather-icons/dist/icons/phone.svg?raw';
+import server from 'feather-icons/dist/icons/server.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 = {
'arrow-left': arrowLeft,
+ activity,
bookmark,
clock,
globe,
home,
+ 'log-in': logIn,
+ 'log-out': logOut,
map,
'map-pin': mapPin,
+ menu,
navigation,
phone,
+ server,
settings,
user,
- x
+ x,
+ zap,
};
export default class IconComponent extends Component {
diff --git a/app/components/user-menu.gjs b/app/components/user-menu.gjs
new file mode 100644
index 0000000..3bc94c0
--- /dev/null
+++ b/app/components/user-menu.gjs
@@ -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();
+ }
+
+
+
+
+}
diff --git a/app/services/storage.js b/app/services/storage.js
index 88498f2..d52a929 100644
--- a/app/services/storage.js
+++ b/app/services/storage.js
@@ -1,19 +1,25 @@
import Service from '@ember/service';
import RemoteStorage from 'remotestoragejs';
import Places from '@remotestorage/module-places';
+import Widget from 'remotestorage-widget';
import { tracked } from '@glimmer/tracking';
import { getGeohashPrefixesInBbox } from '../utils/geohash-coverage';
+import { action } from '@ember/object';
import { debounce } from '@ember/runloop';
import Geohash from 'latlon-geohash';
export default class StorageService extends Service {
rs;
+ widget;
@tracked placesInView = [];
@tracked savedPlaces = [];
@tracked loadedPrefixes = [];
@tracked currentBbox = null;
@tracked version = 0; // Shared version tracker for bookmarks
@tracked initialSyncDone = false;
+ @tracked connected = false;
+ @tracked userAddress = null;
+ @tracked isWidgetOpen = false;
constructor() {
super(...arguments);
@@ -29,14 +35,38 @@ export default class StorageService extends Service {
window.remoteStorage = this.rs;
- // const widget = new Widget(this.rs);
- // widget.attach();
+ this.widget = new Widget(this.rs, {
+ leaveOpen: true,
+ skipInitial: true,
+ });
+ // We don't attach immediately; we'll attach when the user clicks Connect
this.rs.on('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);
if (!this.initialSyncDone) {
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
// (because the 'places' array contains the authoritative new state for this prefix)
return !prefixSet.has(hash);
- } catch (e) {
+ } catch {
return true; // Keep malformed/unknown places safe
}
});
@@ -198,6 +228,28 @@ export default class StorageService extends Service {
async removePlace(place) {
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;
}
}
diff --git a/app/styles/app.css b/app/styles/app.css
index 4c86934..3d06397 100644
--- a/app/styles/app.css
+++ b/app/styles/app.css
@@ -22,12 +22,161 @@ body {
outline: none; /* Prevent focus outline on click */
}
-/* Ensure RS widget is above the map */
-#remotestorage-widget {
+/* Ensure RS widget is above the map but potentially hidden initially if needed */
+#rs-widget-container {
position: absolute;
- top: 10px;
+ top: 60px; /* Below header */
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 */
diff --git a/app/templates/application.gjs b/app/templates/application.gjs
index 847acde..e27cbb1 100644
--- a/app/templates/application.gjs
+++ b/app/templates/application.gjs
@@ -2,11 +2,13 @@ import Component from '@glimmer/component';
import { pageTitle } from 'ember-page-title';
import Map from '#components/map';
import PlacesSidebar from '#components/places-sidebar';
+import AppHeader from '#components/app-header';
import { service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { eq } from 'ember-truth-helpers';
import { and } from 'ember-truth-helpers';
+import { on } from '@ember/modifier';
export default class ApplicationComponent extends Component {
@service storage;
@@ -74,6 +76,21 @@ export default class ApplicationComponent extends Component {
{{pageTitle "M/\RCO"}}
+
+
+
+
+ {{#if this.storage.isWidgetOpen}}
+
+ {{/if}}
+