Offline-capable geomap, meant for storing location bookmarks
0
fork

Configure Feed

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

feat: use new civility ui components

+178 -388
+4 -4
deno.json
··· 1 1 { 2 - "version": "0.4.1", 2 + "version": "0.4.2", 3 3 "workspace": ["./data"], 4 4 "tasks": { 5 5 "data": "deno run -A ./data/cli/main.ts", ··· 25 25 "@civility/store": "jsr:@civility/store@^1.0.0-beta.8", 26 26 "@civility/store/idb": "jsr:@civility/store@^1.0.0-beta.8/idb", 27 27 "@civility/sync": "jsr:@civility/sync@^1.0.0-beta.10", 28 - "@civility/ui": "jsr:@civility/ui@^1.0.0-beta.2", 28 + "@civility/ui": "jsr:@civility/ui@^1.0.0-beta.3", 29 29 "@civility/workers": "jsr:@civility/workers@^0.2.5", 30 30 "@std/async": "jsr:@std/async@^1.2.0", 31 31 "@zod/zod": "jsr:@zod/zod@^4.3.6", 32 32 "fflate": "npm:fflate@^0.8.2", 33 33 "lit": "npm:lit@^3.3.2", 34 - "maplibre-gl": "npm:maplibre-gl@^5.22.0", 35 - "pmtiles": "npm:pmtiles@^4.4.0", 34 + "maplibre-gl": "npm:maplibre-gl@^5.23.0", 35 + "pmtiles": "npm:pmtiles@^4.4.1", 36 36 "pmtiles-offline": "npm:pmtiles-offline@^1.0.0" 37 37 } 38 38 }
+19 -19
deno.lock
··· 4 4 "jsr:@civility/blobs@^1.0.0-beta.4": "1.0.0-beta.4", 5 5 "jsr:@civility/store@^1.0.0-beta.8": "1.0.0-beta.8", 6 6 "jsr:@civility/sync@^1.0.0-beta.10": "1.0.0-beta.10", 7 - "jsr:@civility/ui@^1.0.0-beta.2": "1.0.0-beta.2", 7 + "jsr:@civility/ui@^1.0.0-beta.3": "1.0.0-beta.3", 8 8 "jsr:@civility/workers@~0.2.5": "0.2.5", 9 9 "jsr:@cliffy/command@1": "1.0.0", 10 10 "jsr:@cliffy/flags@1.0.0": "1.0.0", ··· 26 26 "npm:fast-json-patch@^3.1.1": "3.1.1", 27 27 "npm:fflate@~0.8.2": "0.8.2", 28 28 "npm:lit@^3.3.2": "3.3.2", 29 - "npm:maplibre-gl@^5.22.0": "5.22.0", 29 + "npm:maplibre-gl@^5.23.0": "5.23.0", 30 30 "npm:pmtiles-offline@1": "1.0.0", 31 - "npm:pmtiles@^4.4.0": "4.4.0" 31 + "npm:pmtiles@^4.4.1": "4.4.1" 32 32 }, 33 33 "jsr": { 34 34 "@civility/blobs@1.0.0-beta.4": { ··· 52 52 "jsr:@paulmillr/qr" 53 53 ] 54 54 }, 55 - "@civility/ui@1.0.0-beta.2": { 56 - "integrity": "4cdef14beafa95ca418cdd14f5f9d54551bd5c8f9f84517438fd848133be340e", 55 + "@civility/ui@1.0.0-beta.3": { 56 + "integrity": "4c35d660ff511ef45d824ac1941928b1fc099930a9764f81ce7cb9c416df9588", 57 57 "dependencies": [ 58 58 "jsr:@std/html", 59 59 "npm:lit" ··· 149 149 "@mapbox/point-geometry@1.1.0": { 150 150 "integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==" 151 151 }, 152 - "@mapbox/tiny-sdf@2.0.7": { 153 - "integrity": "sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==" 152 + "@mapbox/tiny-sdf@2.1.0": { 153 + "integrity": "sha512-uFJhNh36BR4OCuWIEiWaEix9CA2WzT6CAIcqVjWYpnx8+QDtS+oC4QehRrx5cX4mgWs37MmKnwUejeHxVymzNg==" 154 154 }, 155 155 "@mapbox/unitbezier@0.0.1": { 156 156 "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==" ··· 169 169 "@maplibre/geojson-vt@5.0.4": { 170 170 "integrity": "sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ==" 171 171 }, 172 - "@maplibre/geojson-vt@6.0.4": { 173 - "integrity": "sha512-HYv3POhMRCdhP3UPPATM/hfcy6/WuVIf5FKboH8u/ZuFMTnAIcSVlq5nfOqroLokd925w2QtE7YwquFOIacwVQ==", 172 + "@maplibre/geojson-vt@6.1.0": { 173 + "integrity": "sha512-2eIY4gZxeKIVOZVNkAMb+5NgXhgsMQpOveTQAvnp53LYqHGJZDidk7Ew0Tged9PThidpbS+NFTh0g4zivhPDzQ==", 174 174 "dependencies": [ 175 175 "kdbush" 176 176 ] ··· 357 357 "lit-html" 358 358 ] 359 359 }, 360 - "maplibre-gl@5.22.0": { 361 - "integrity": "sha512-nc8YA+YSEioMZg5W0cb6Cf3wQ8aJge66dsttyBgpOArOnlmFJO1Kc5G32kYVPeUYhLpBja83T99uanmJvYAIyQ==", 360 + "maplibre-gl@5.23.0": { 361 + "integrity": "sha512-aou8YBNFS8uVtDWFWt0W/6oorfl18wt+oIA8fnXk1kivjkbtXi9gGrQvflTpwrR3hG13aWdIdbYWeN0NFMV7ag==", 362 362 "dependencies": [ 363 363 "@mapbox/jsonlint-lines-primitives", 364 364 "@mapbox/point-geometry", ··· 366 366 "@mapbox/unitbezier", 367 367 "@mapbox/vector-tile", 368 368 "@mapbox/whoots-js", 369 - "@maplibre/geojson-vt@6.0.4", 369 + "@maplibre/geojson-vt@6.1.0", 370 370 "@maplibre/maplibre-gl-style-spec", 371 371 "@maplibre/mlt", 372 372 "@maplibre/vt-pbf", ··· 422 422 "pmtiles-offline@1.0.0": { 423 423 "integrity": "sha512-bagqJVRs5VDb6LkpNMYqk5/18wnV9LQcjLFYQTU4Y8c2u2QONIuV7QVlXZ+0JKvb/xdrEzrR9jgQv0qyfoN0eg==" 424 424 }, 425 - "pmtiles@4.4.0": { 426 - "integrity": "sha512-tCLI1C5134MR54i8izUWhse0QUtO/EC33n9yWp1N5dYLLvyc197U0fkF5gAJhq1TdWO9Tvl+9hgvFvM0fR27Zg==", 425 + "pmtiles@4.4.1": { 426 + "integrity": "sha512-5oTeQc/yX/ft1evbpIlnoCZugQuug/iYIAj/ZTqIqzdGek4uZEho99En890EE6NOSI3JTI3IG8R7r8+SltphxA==", 427 427 "dependencies": [ 428 428 "fflate" 429 429 ] ··· 431 431 "potpack@2.1.0": { 432 432 "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==" 433 433 }, 434 - "protocol-buffers-schema@3.6.0": { 435 - "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" 434 + "protocol-buffers-schema@3.6.1": { 435 + "integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==" 436 436 }, 437 437 "quickselect@3.0.0": { 438 438 "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==" ··· 477 477 "jsr:@civility/blobs@^1.0.0-beta.4", 478 478 "jsr:@civility/store@^1.0.0-beta.8", 479 479 "jsr:@civility/sync@^1.0.0-beta.10", 480 - "jsr:@civility/ui@^1.0.0-beta.2", 480 + "jsr:@civility/ui@^1.0.0-beta.3", 481 481 "jsr:@civility/workers@~0.2.5", 482 482 "jsr:@std/async@^1.2.0", 483 483 "jsr:@zod/zod@^4.3.6", 484 484 "npm:fflate@~0.8.2", 485 485 "npm:lit@^3.3.2", 486 - "npm:maplibre-gl@^5.22.0", 486 + "npm:maplibre-gl@^5.23.0", 487 487 "npm:pmtiles-offline@1", 488 - "npm:pmtiles@^4.4.0" 488 + "npm:pmtiles@^4.4.1" 489 489 ], 490 490 "members": { 491 491 "data": {
-217
www/components/ui-data-actions.ts
··· 1 - import { html, LitElement, type TemplateResult } from 'lit' 2 - 3 - export interface Methods { 4 - exportData(filename?: string): Promise<{ 5 - success: boolean 6 - path?: string 7 - error?: string 8 - }> 9 - importData(): Promise<{ 10 - success: boolean 11 - path?: string 12 - error?: string 13 - }> 14 - deleteAllData(): Promise<{ success: boolean; error?: string }> 15 - } 16 - 17 - export class UiDataActions extends LitElement { 18 - static override properties = { 19 - methods: { type: Object }, 20 - _isImporting: { state: true }, 21 - _showDeleteConfirm: { state: true }, 22 - _status: { state: true }, 23 - _deleteStatus: { state: true }, 24 - } 25 - 26 - declare methods: Methods | null 27 - declare _isImporting: boolean 28 - declare _showDeleteConfirm: boolean 29 - declare _status: string 30 - declare _deleteStatus: string 31 - 32 - constructor() { 33 - super() 34 - this.methods = null 35 - this._isImporting = false 36 - this._showDeleteConfirm = false 37 - this._status = '' 38 - this._deleteStatus = '' 39 - } 40 - 41 - override createRenderRoot() { 42 - return this 43 - } 44 - 45 - #setStatus(msg: string, isError = false): void { 46 - this._status = isError ? `Error: ${msg}` : msg 47 - } 48 - 49 - #setDeleteStatus(msg: string): void { 50 - this._deleteStatus = msg 51 - } 52 - 53 - async #handleExport(): Promise<void> { 54 - if (!this.methods) return 55 - const btn = this.querySelector<HTMLButtonElement>('[data-export-btn]') 56 - if (btn) btn.disabled = true 57 - const result = await this.methods.exportData() 58 - if (btn) btn.disabled = false 59 - if (result.success) { 60 - this.#setStatus('Data exported successfully.') 61 - this.dispatchEvent( 62 - new CustomEvent('on-exported', { bubbles: true, composed: true }), 63 - ) 64 - } else { 65 - this.#setStatus(result.error ?? 'Export failed.', true) 66 - } 67 - } 68 - 69 - #handleDeleteAllData(): void { 70 - this._showDeleteConfirm = true 71 - this._deleteStatus = '' 72 - this.requestUpdate() 73 - } 74 - 75 - #handleDeleteCancel = (): void => { 76 - this._showDeleteConfirm = false 77 - this._deleteStatus = '' 78 - this.requestUpdate() 79 - } 80 - 81 - async #handleDeleteConfirm(): Promise<void> { 82 - if (!this.methods) return 83 - const input = this.querySelector<HTMLInputElement>( 84 - '[data-delete-confirm-input]', 85 - ) 86 - if (input?.value !== 'Delete my data') { 87 - this.#setDeleteStatus('Please type "Delete my data" exactly to confirm.') 88 - return 89 - } 90 - const btn = this.querySelector<HTMLButtonElement>( 91 - '[data-delete-confirm-btn]', 92 - ) 93 - if (btn) btn.disabled = true 94 - const result = await this.methods.deleteAllData() 95 - if (result.success) { 96 - this._showDeleteConfirm = false 97 - this.#setStatus('All data deleted.') 98 - this.dispatchEvent( 99 - new CustomEvent('on-deleted', { bubbles: true, composed: true }), 100 - ) 101 - globalThis.location.reload() 102 - } else { 103 - if (btn) btn.disabled = false 104 - this.#setDeleteStatus(result.error ?? 'Delete failed.') 105 - } 106 - } 107 - 108 - async #handleImport(): Promise<void> { 109 - if (!this.methods) return 110 - this._isImporting = true 111 - this.requestUpdate() 112 - const result = await this.methods.importData() 113 - this._isImporting = false 114 - this.requestUpdate() 115 - if (result.success) { 116 - this.#setStatus('Data imported successfully.') 117 - this.dispatchEvent( 118 - new CustomEvent('on-imported', { bubbles: true, composed: true }), 119 - ) 120 - } else { 121 - this.#setStatus(result.error ?? 'Import failed.', true) 122 - } 123 - } 124 - 125 - override render(): TemplateResult { 126 - return html` 127 - <p>Export your data to a file, or import a previously exported file.</p> 128 - <div> 129 - <button 130 - class="action" 131 - data-export-btn 132 - @click="${this.#handleExport}" 133 - > 134 - Export 135 - </button> 136 - <button 137 - class="action" 138 - ?disabled="${this._isImporting}" 139 - @click="${this.#handleImport}" 140 - > 141 - ${this._isImporting ? 'Importing...' : 'Import'} 142 - </button> 143 - </div> 144 - 145 - <button class="action action--danger" @click="${this 146 - .#handleDeleteAllData}"> 147 - Delete All Data 148 - </button> 149 - 150 - ${this._status 151 - ? html` 152 - <p data-status ${this._status.startsWith('Error') 153 - ? 'data-error' 154 - : ''}> 155 - ${this._status} 156 - </p> 157 - ` 158 - : ''} 159 - 160 - <ui-dialog 161 - ?open="${this._showDeleteConfirm}" 162 - @dismiss="${this.#handleDeleteCancel}" 163 - > 164 - <dialog> 165 - <article class="bm-dialog"> 166 - <h2 class="bm-dialog-title">Delete All Data</h2> 167 - <p> 168 - This will permanently delete all your data. This cannot be undone. 169 - </p> 170 - <p>Type <strong>Delete my data</strong> to confirm:</p> 171 - <input 172 - data-delete-confirm-input 173 - type="text" 174 - placeholder="Delete my data" 175 - autocomplete="off" 176 - > 177 - ${this._deleteStatus 178 - ? html` 179 - <p data-status data-error> 180 - ${this._deleteStatus} 181 - </p> 182 - ` 183 - : ''} 184 - <div class="bm-form-actions"> 185 - <button 186 - class="bm-btn-danger" 187 - data-delete-confirm-btn 188 - @click="${this.#handleDeleteConfirm}" 189 - > 190 - Delete 191 - </button> 192 - <button type="button" @click="${this.#handleDeleteCancel}"> 193 - Cancel 194 - </button> 195 - </div> 196 - </article> 197 - </dialog> 198 - </ui-dialog> 199 - 200 - <ui-dialog ?open="${this._isImporting}"> 201 - <dialog> 202 - <article class="bm-dialog"> 203 - <h2 class="bm-dialog-title">Importing Data</h2> 204 - <p>Please wait while your data is being imported...</p> 205 - <div style="display:flex;justify-content:center;padding:16px 0"> 206 - <ui-spinner size="lg"></ui-spinner> 207 - </div> 208 - </article> 209 - </dialog> 210 - </ui-dialog> 211 - ` 212 - } 213 - } 214 - 215 - if (!customElements.get('ui-data-actions')) { 216 - customElements.define('ui-data-actions', UiDataActions) 217 - }
+48 -64
www/index.html
··· 1 1 <!DOCTYPE html> 2 2 <html lang="en"> 3 - <head> 4 - <meta charset="utf-8" /> 5 - <meta 6 - name="viewport" 7 - content="width=device-width, initial-scale=1, viewport-fit=cover" 8 - > 9 - <meta name="mobile-web-app-capable" content="yes"> 10 - <meta name="apple-mobile-web-app-capable" content="yes"> 11 - <meta name="description" content="An offline-capable world map"> 12 3 13 - <link rel="manifest" href="manifest.json" /> 14 - <link rel="icon" type="image/x-icon" href="/dist/icons/icon.ico" /> 15 - <link rel="apple-touch-icon" href="/dist/icons/icon.png"> 4 + <head> 5 + <meta charset="utf-8" /> 6 + <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> 7 + <meta name="mobile-web-app-capable" content="yes"> 8 + <meta name="apple-mobile-web-app-capable" content="yes"> 9 + <meta name="description" content="An offline-capable world map"> 16 10 17 - <link rel="canonical" href="https://maps.bpev.me" /> 18 - <title>MapsApp</title> 11 + <link rel="manifest" href="manifest.json" /> 12 + <link rel="icon" type="image/x-icon" href="/dist/icons/icon.ico" /> 13 + <link rel="apple-touch-icon" href="/dist/icons/icon.png"> 19 14 20 - <link 21 - rel="stylesheet" 22 - type="text/css" 23 - href="/static/styles/maplibre-gl.css" 24 - > 25 - <link 26 - rel="stylesheet" 27 - type="text/css" 28 - href="/static/styles/civility.min.css" 29 - > 30 - <link rel="stylesheet" type="text/css" href="/static/styles/theme.css"> 15 + <link rel="canonical" href="https://maps.bpev.me" /> 16 + <title>MapsApp</title> 31 17 32 - <script src="/dist/index.js" type="module"></script> 33 - </head> 18 + <link rel="stylesheet" type="text/css" href="/static/styles/maplibre-gl.css"> 19 + <link rel="stylesheet" type="text/css" href="https://bpev.me/civility.min.css"> 20 + <link rel="stylesheet" type="text/css" href="/static/styles/theme.css"> 34 21 35 - <body> 36 - <a href="#main" class="skip-to-main">Skip to main content</a> 22 + <script src="/dist/index.js" type="module"></script> 23 + </head> 37 24 38 - <header hidden> 39 - <a id="header-back" href="#!/" hidden aria-label="Back"> 40 - ← Back 41 - </a> 42 - <strong id="page-title">MapsApp</strong> 43 - </header> 25 + <body> 26 + <a href="#main" class="skip-to-main">Skip to main content</a> 44 27 45 - <main id="main"> 46 - <div 47 - style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%)" 48 - > 49 - <ui-spinner size="lg"></ui-spinner> 50 - </div> 51 - </main> 28 + <header></header> 52 29 53 - <m-map></m-map> 30 + <main id="main"> 31 + <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%)"> 32 + <ui-spinner size="lg"></ui-spinner> 33 + </div> 34 + </main> 54 35 55 - <footer fixed> 56 - <ui-bottom-bar role="navigation" aria-label="Bottom navigation"> 57 - <a href="/" data-route aria-current="page"> 58 - <img src="/static/icons/home.svg" alt="" aria-hidden="true"> 59 - <span>Map</span> 60 - </a> 61 - <a href="/search" data-route> 62 - <img src="/static/icons/navigation.svg" alt="" aria-hidden="true"> 63 - <span>Search</span> 64 - </a> 65 - <a href="/bookmarks" data-route> 66 - <img src="/static/icons/bookmark.svg" alt="" aria-hidden="true"> 67 - <span>Bookmarks</span> 68 - </a> 69 - <a href="/settings" data-route> 70 - <img src="/static/icons/tool.svg" alt="" aria-hidden="true"> 71 - <span>Settings</span> 72 - </a> 73 - </ui-bottom-bar> 74 - </footer> 75 - </body> 36 + <m-map></m-map> 37 + 38 + <footer fixed> 39 + <ui-bottom-bar role="navigation" aria-label="Bottom navigation"> 40 + <a href="/" data-route aria-current="page"> 41 + <img src="/static/icons/home.svg" alt="" aria-hidden="true"> 42 + <span>Map</span> 43 + </a> 44 + <a href="/search" data-route> 45 + <img src="/static/icons/navigation.svg" alt="" aria-hidden="true"> 46 + <span>Search</span> 47 + </a> 48 + <a href="/bookmarks" data-route> 49 + <img src="/static/icons/bookmark.svg" alt="" aria-hidden="true"> 50 + <span>Bookmarks</span> 51 + </a> 52 + <a href="/settings" data-route> 53 + <img src="/static/icons/tool.svg" alt="" aria-hidden="true"> 54 + <span>Settings</span> 55 + </a> 56 + </ui-bottom-bar> 57 + </footer> 58 + </body> 59 + 76 60 </html>
+40 -32
www/index.ts
··· 2 2 import { createLayoutRouter } from '@civility/ui' 3 3 import './routes/map.ts' 4 4 import './components/m-map.ts' 5 - import './components/ui-data-actions.ts' 6 5 import './routes/search.ts' 7 6 import './routes/settings.ts' 8 7 import './routes/settings-downloads.ts' ··· 10 9 import './routes/bookmarks.ts' 11 10 12 11 client.init() 13 - // client.watchForUpdates() 12 + client.watchForUpdates() 14 13 15 14 interface NavMeta { 16 - title?: string 17 15 navActive?: string 18 - backRoute?: string 19 16 } 20 17 21 18 const { ready } = createLayoutRouter<NavMeta>({ 22 19 router: { selectorAttrib: 'data-route', useHash: true }, 23 20 defaultRoute: '/', 24 21 landmarks: { 22 + header: 'body > header', 25 23 main: 'main', 26 24 }, 27 25 afterMount: (ctx, view) => { ··· 30 28 mapEl.hidden = (ctx as { path: string }).path !== '/' 31 29 } 32 30 33 - const titleEl = document.querySelector('#page-title') 34 - const header = document.querySelector<HTMLDivElement>('header') 35 - 36 - if (titleEl && view.meta?.title != undefined) { 37 - titleEl.textContent = view.meta.title 38 - } 39 - if (header) header.hidden = view.meta?.title == undefined 40 - 41 31 document.querySelectorAll('ui-bottom-bar a').forEach((link) => { 42 32 if ( 43 33 view.meta?.navActive && ··· 48 38 link.removeAttribute('aria-current') 49 39 } 50 40 }) 51 - 52 - const backEl = document.querySelector<HTMLAnchorElement>('#header-back') 53 - if (backEl) { 54 - if (view.meta?.backRoute) { 55 - backEl.setAttribute('href', '#!' + view.meta.backRoute) 56 - backEl.hidden = false 57 - } else { 58 - backEl.hidden = true 59 - } 60 - } 61 41 }, 62 42 routes: { 63 43 '/': { ··· 65 45 meta: { navActive: '/' }, 66 46 }, 67 47 '/search': { 68 - landmarks: { main: 'r-search' }, 69 - meta: { title: 'Search', navActive: '/search' }, 48 + landmarks: { 49 + main: 'r-search', 50 + header: () => ({ 51 + tag: 'ui-header-content', 52 + attrs: { title: 'Search' }, 53 + }), 54 + }, 55 + meta: { navActive: '/search' }, 70 56 }, 71 57 '/settings': { 72 - landmarks: { main: 'r-settings' }, 73 - meta: { title: 'Settings', navActive: '/settings' }, 58 + landmarks: { 59 + main: 'r-settings', 60 + header: () => ({ 61 + tag: 'ui-header-content', 62 + attrs: { title: 'Settings' }, 63 + }), 64 + }, 65 + meta: { navActive: '/settings' }, 74 66 }, 75 67 '/settings/downloads': { 76 - landmarks: { main: 'r-settings-downloads' }, 77 - meta: { title: 'Offline Maps', backRoute: '/settings' }, 68 + landmarks: { 69 + main: 'r-settings-downloads', 70 + header: () => ({ 71 + tag: 'ui-header-content', 72 + attrs: { title: 'Offline Maps', back: '', 'fallback-href': '/settings' }, 73 + }), 74 + }, 78 75 }, 79 76 '/settings/about': { 80 - landmarks: { main: 'r-settings-about' }, 81 - meta: { title: 'About', backRoute: '/settings' }, 77 + landmarks: { 78 + main: 'r-settings-about', 79 + header: () => ({ 80 + tag: 'ui-header-content', 81 + attrs: { title: 'About', back: '', 'fallback-href': '/settings' }, 82 + }), 83 + }, 82 84 }, 83 85 '/bookmarks': { 84 - landmarks: { main: 'r-bookmarks' }, 85 - meta: { title: 'Bookmarks', navActive: '/bookmarks' }, 86 + landmarks: { 87 + main: 'r-bookmarks', 88 + header: () => ({ 89 + tag: 'ui-header-content', 90 + attrs: { title: 'Bookmarks' }, 91 + }), 92 + }, 93 + meta: { navActive: '/bookmarks' }, 86 94 }, 87 95 }, 88 96 })
+35 -3
www/manifest.json
··· 1 1 { 2 - "short_name": "maps", 3 - "name": "maps", 4 - "description": "MapsApp", 2 + "short_name": "MapsApp", 3 + "name": "Maps App", 4 + "description": "An offline-capable map application", 5 5 "id": "https://maps.bpev.me", 6 6 "categories": [], 7 + "icons": [ 8 + { 9 + "src": "/dist/icons/128x128.png", 10 + "sizes": "128x128", 11 + "type": "image/png", 12 + "purpose": "any" 13 + }, 14 + { 15 + "src": "/dist/icons/192x192.png", 16 + "sizes": "192x192", 17 + "type": "image/png", 18 + "purpose": "any maskable" 19 + }, 20 + { 21 + "src": "/dist/icons/256x256.png", 22 + "sizes": "256x256", 23 + "type": "image/png", 24 + "purpose": "any" 25 + }, 26 + { 27 + "src": "/dist/icons/512x512.png", 28 + "sizes": "512x512", 29 + "type": "image/png", 30 + "purpose": "any maskable" 31 + }, 32 + { 33 + "src": "/dist/icons/icon.png", 34 + "sizes": "1024x1024", 35 + "type": "image/png", 36 + "purpose": "any" 37 + } 38 + ], 7 39 "orientation": "any", 8 40 "start_url": "/", 9 41 "display": "standalone",
+2 -2
www/routes/settings.ts
··· 1 1 import { html, LitElement, type TemplateResult } from 'lit' 2 2 import app from '../models/app.ts' 3 - import type { Methods } from '../components/ui-data-actions.ts' 3 + import type { UiDataActionMethods } from '@civility/ui' 4 4 5 5 export class SettingsPage extends LitElement { 6 6 protected override createRenderRoot() { ··· 91 91 exportData: (filename?: string) => app.exportStore(filename), 92 92 importData: () => app.importStore(), 93 93 deleteAllData: () => app.deleteAllData(), 94 - } as Methods}" 94 + } as UiDataActionMethods}" 95 95 @on-imported="${() => { 96 96 globalThis.dispatchEvent(new CustomEvent('pmtiles-updated')) 97 97 }}"
+16
www/static/icons/arrow-left.svg
··· 1 + <svg 2 + xmlns="http://www.w3.org/2000/svg" 3 + width="24" 4 + height="24" 5 + viewBox="0 0 24 24" 6 + fill="none" 7 + stroke="currentColor" 8 + stroke-width="2" 9 + stroke-linecap="round" 10 + stroke-linejoin="round" 11 + class="feather feather-arrow-left" 12 + > 13 + <line x1="19" y1="12" x2="5" y2="12"></line><polyline 14 + points="12 19 5 12 12 5" 15 + ></polyline> 16 + </svg>
+14 -47
www/static/styles/theme.css
··· 150 150 /* ── Header ───────────────────────────────────────────────────────────── */ 151 151 152 152 header { 153 + border-bottom: 1px solid currentColor; 154 + } 155 + 156 + ui-header-content { 153 157 display: flex; 154 158 align-items: center; 155 159 gap: var(--s2); 156 160 height: var(--header-height); 157 161 padding: 0 var(--container-padding); 158 - border-bottom: 1px solid currentColor; 159 162 } 160 163 161 - header strong { 164 + ui-header-content h1 { 162 165 flex: 1; 166 + margin: 0; 163 167 font-size: var(--f4); 164 168 font-weight: var(--fw-semibold); 165 169 overflow: hidden; ··· 167 171 white-space: nowrap; 168 172 } 169 173 170 - #header-back[hidden] { 171 - display: none; 174 + ui-header-content .header-back { 175 + background: transparent; 176 + border: none; 177 + padding: var(--s1); 178 + color: var(--primary); 179 + cursor: pointer; 172 180 } 173 181 174 - #header-back { 182 + ui-header-content .header-actions { 175 183 display: flex; 176 184 align-items: center; 177 - gap: var(--s1); 178 - font-size: var(--f6); 179 - font-weight: var(--fw-medium); 180 - color: var(--primary); 181 - text-decoration: none; 182 - padding: var(--s1) var(--s2); 183 - border-radius: var(--br-sm); 184 - white-space: nowrap; 185 - flex-shrink: 0; 186 - transition: opacity var(--transition-fast); 187 - } 188 - 189 - #header-back:hover { 190 - opacity: 0.7; 185 + gap: var(--s2); 191 186 } 192 187 193 188 /* ── Fixed bottom tab bar ──────────────────────────────────────────────── */ ··· 748 743 margin: 0; 749 744 } 750 745 751 - .bm-import-preview { 752 - max-height: 50vh; 753 - overflow-y: auto; 754 - border: 1px solid currentColor; 755 - border-radius: var(--br-base); 756 - } 757 - 758 746 .bm-import-group+.bm-import-group { 759 747 border-top: 1px solid currentColor; 760 748 } ··· 817 805 font-size: 12px; 818 806 z-index: 1; 819 807 } 820 - 821 - 822 - /* UI Data Actions */ 823 - ui-data-actions { 824 - display: block; 825 - } 826 - 827 - ui-data-actions>div { 828 - display: flex; 829 - gap: var(--s2); 830 - margin-bottom: var(--s2); 831 - } 832 - 833 - ui-data-actions>p[data-status] { 834 - margin-top: var(--s2); 835 - font-size: var(--f5); 836 - } 837 - 838 - ui-data-actions>p[data-status][data-error] { 839 - color: var(--error); 840 - }