Offline-capable geomap for storing location bookmarks
0

Configure Feed

Select the types of activity you want to include in your feed.

fix: minor defects

- add icons to precache
- track-on-load should only ask on main route
- list deletion now confirms first
- misrouting imported bookmarks
- fix address clearing

Ben Pevsner (Jul 26, 2026, 4:16 PM +0200) fcae6fdc 605979c1

+36 -10
+3 -1
www/components/m-map.ts
··· 214 214 // target is more specific, so it gets the last word. 215 215 this.#fitToActiveList() 216 216 this.#applyPendingNav() 217 - if (app.trackOnInitialLoad) this.#geolocate?.trigger() 217 + // `hidden` is set by the router: only geolocate when the map is what 218 + // the user opened, not behind a deep link into another route. 219 + if (app.trackOnInitialLoad && !this.hidden) this.#geolocate?.trigger() 218 220 // The compact attribution expands once the first source with 219 221 // attribution loads; minimize it after a beat, the same way 220 222 // MapLibre's own drag handler does.
+17 -2
www/components/m-welcome.ts
··· 28 28 override connectedCallback() { 29 29 super.connectedCallback() 30 30 app.addEventListener(this.#onAppUpdate) 31 + globalThis.addEventListener('hashchange', this.#onAppUpdate) 31 32 } 32 33 33 34 override disconnectedCallback() { 34 35 super.disconnectedCallback() 35 36 app.removeEventListener(this.#onAppUpdate) 37 + globalThis.removeEventListener('hashchange', this.#onAppUpdate) 36 38 } 37 39 38 40 #onAppUpdate = () => this.requestUpdate() 39 41 42 + /** 43 + * Only greet on the map itself. Someone deep-linked into a shared list or 44 + * bookmark came to look at that, not to be asked for their location — the 45 + * welcome waits until they visit the home map. 46 + */ 47 + get #onHome(): boolean { 48 + const path = globalThis.location.hash.replace(/^#!/, '') 49 + return path === '' || path === '/' 50 + } 51 + 40 52 get #open(): boolean { 41 - return app.preferencesLoaded && !app.hasSeenWelcome && !this.#dismissed 53 + return this.#onHome && app.preferencesLoaded && !app.hasSeenWelcome && 54 + !this.#dismissed 42 55 } 43 56 44 57 #requestLocation = () => { ··· 49 62 } 50 63 this.#locationStatus = 'requesting' 51 64 this.requestUpdate() 52 - app.setTrackOnInitialLoad(true) 53 65 54 66 navigator.geolocation.getCurrentPosition( 55 67 async (pos) => { 68 + // Only once permission is actually granted — set eagerly, a denial 69 + // would leave the app re-prompting on every load. 70 + app.setTrackOnInitialLoad(true) 56 71 const { latitude: lat, longitude: lng } = pos.coords 57 72 this.#coords = { lat, lng } 58 73 const manifest = await fetchTileManifest()
+3 -1
www/models/app.ts
··· 306 306 307 307 // ====== LISTS ====== 308 308 309 - async addList(name: string, color?: string): Promise<void> { 309 + /** Returns the new list's id. */ 310 + async addList(name: string, color?: string): Promise<string> { 310 311 const now = new Date().toISOString() 311 312 const id = crypto.randomUUID() 312 313 await listsColl.set( ··· 319 320 updatedAt: now, 320 321 }), 321 322 ) 323 + return id 322 324 } 323 325 324 326 async updateList(
+4 -1
www/routes/bookmark.ts
··· 206 206 ...b.properties, 207 207 name: (fd.get('name') as string).trim() || undefined, 208 208 description: (fd.get('description') as string).trim() || undefined, 209 + // Emptying the field clears the address, matching name/description. 210 + // The structured geocoder fields go with it — they described the old 211 + // address, and nothing displays them without a `displayText`. 209 212 address: addressText 210 213 ? { ...b.properties.address, displayText: addressText } 211 - : b.properties.address, 214 + : undefined, 212 215 }, 213 216 categories: listIds, 214 217 })
+7 -3
www/routes/bookmarks.ts
··· 129 129 this.requestUpdate() 130 130 const listIdMap = new Map<string, string>() 131 131 for (const folder of folders) { 132 - await app.addList(folder.name) 133 - const created = app.bookmarkLists.find((c) => c.name === folder.name) 134 - if (created) listIdMap.set(folder.tempId, created.id) 132 + listIdMap.set(folder.tempId, await app.addList(folder.name)) 135 133 } 136 134 for (const bm of bookmarks) { 137 135 if (bm.isDuplicate) continue ··· 143 141 } 144 142 145 143 async #deleteList(id: string) { 144 + const name = this.lists.find((l) => l.id === id)?.name ?? 'this list' 145 + if ( 146 + !globalThis.confirm( 147 + `Delete "${name}"? Its bookmarks are kept, but any shared link to the list stops working.`, 148 + ) 149 + ) return 146 150 await app.deleteList(id) 147 151 this.expandedLists.delete(id) 148 152 }
+2 -2
www/worker.ts
··· 24 24 '/static/icons/navigation.svg', 25 25 '/static/icons/tool.svg', 26 26 '/static/icons/download.svg', 27 - '/static/icons/cloud.svg', 28 27 '/static/icons/layers.svg', 29 28 '/static/icons/arrow-left.svg', 30 29 '/static/icons/copy.svg', 31 30 '/static/icons/edit.svg', 32 31 '/static/icons/external-link.svg', 33 32 '/static/icons/tag.svg', 34 - '/static/icons/cloud-off.svg', 35 33 '/static/icons/book.svg', 36 34 '/static/icons/bookmark.svg', 37 35 '/static/icons/x.svg', 36 + '/static/icons/check-circle.svg', 37 + '/static/icons/circle.svg', 38 38 '/static/tiles/tiles.json', 39 39 '/dist/index.js', 40 40 '/manifest.json',