From 5576d2edf145662abe8e6d0b3c8bffa391dcee0a Mon Sep 17 00:00:00 2001 From: Roman Priesol Date: Wed, 9 Sep 2026 13:14:19 +0200 Subject: [PATCH] Note UX polish: pin marker, icon picker, fix Save button color MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The clicked location now stays marked (a plain maplibre-gl Marker, removed on save/cancel/deactivate) while the note form is open — before, nothing on the map showed where the pending note would land. - POI_ICONS gains a dozen more symbols (water, dump station, toilets, playground, viewpoint, swimming, fishing, climbing, hiking, …) via Object.assign after PARK4NIGHT_TAGS' own icon constants are declared (they're below POI_ICONS in the file, so the literal itself can't reference them directly) — reused as real marker-badge glyphs, not just PARK4NIGHT_TAGS' inline description-tag icons. NoteSaveForm gets a picker grid for these, shown only for a Point note (a Measure-drawn line/polygon has no one spot for a badge). - Fixed: .notes-panel__save's blue background was losing to .notes-panel__actions button's gray — same class-vs-class+element specificity trap .gpx-panel__action[hidden] already has a comment about elsewhere in this file. The button was never actually blue. Verified live (properly simulating a logged-in session this time, via Page.addScriptToEvaluateOnNewDocument rather than a post-load DOM poke, since NotesControl's mount decision happens once, synchronously, at page load): marker appears and clears correctly, 19 icons + "none" render and select, Save renders #1a5fb4. Full suite: 115 runs, 0 failures. --- TODO.adoc | 3 ++ public/css/style.css | 48 ++++++++++++++++++++++++++++- public/js/icons.js | 23 ++++++++++++++ public/js/notes.js | 73 +++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 142 insertions(+), 5 deletions(-) diff --git a/TODO.adoc b/TODO.adoc index 61e6ae1..38d58c1 100644 --- a/TODO.adoc +++ b/TODO.adoc @@ -47,6 +47,9 @@ independence from connectivity. ** [x] Fixed: a note saved before `properties.id` existed (the earliest test notes) had no Edit/Delete-able target — the popup no longer offers them without an id, and `rake gpx:backfill_note_ids` assigns one to any pre-existing Note feature missing it ** [x] Title + body split (name in the black header, body in a separate field) with light self-authored markdown (`**bold**`, `*italic*`, auto-linked URLs) in the body — a *new*, separate `formatNoteBody` (not `formatDescription`, which stays plain/untrusted-safe for uploaded GPX/park4night content per docs/nps-resources.adoc's earlier markdown-rendering call) ** [x] Gated behind login: NotesControl isn't added to the map at all when logged out, Measure's "Save as note" button is hidden (Measure itself stays usable either way) +** [x] The clicked point stays marked on the map while the note form is open (a plain `Marker`, removed on save/cancel/deactivate) — it was disappearing back into the flat map with nothing showing where the note would land +** [x] Icon picker for point notes (a grid in the save form) — extends `POI_ICONS` with more of the symbols `PARK4NIGHT_TAGS` already vendors (water, dump station, toilets, playground, viewpoint, swimming, fishing, climbing, hiking, …), reusable as real marker-badge glyphs, not just inline tag icons. Hidden for a Measure-drawn line/polygon note — no single spot for a badge to sit at +** [x] Fixed: the Save button's own blue background was losing to `.notes-panel__actions button`'s gray — same class-vs-class+element specificity trap as `.gpx-panel__action[hidden]` already has a comment about elsewhere in this file, just not yet applied here ** [ ] The Unigrid Matrix category filter only re-slices the *points* source (`_applyCategoryFilter`) — toggling the "Note" category off hides point notes but not polygon/line ones sharing that category * [ ] Offline satellite tile caching (e.g. ESRI) for a selected route area — key for judging surface quality and obstacles when wild camping diff --git a/public/css/style.css b/public/css/style.css index f5a4154..3f3ca3f 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -888,6 +888,48 @@ html, body { resize: vertical; } +.notes-panel__icons { + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 2px; + margin: 2px 0; +} + +/* Same "author display wins over the UA [hidden] rule" gotcha as + .gpx-panel__action[hidden] elsewhere in this file. */ +.notes-panel__icons[hidden] { + display: none; +} + +.notes-panel__icon-option { + width: auto; + height: auto; + display: flex; + align-items: center; + justify-content: center; + padding: 3px; + border: 1px solid transparent; + border-radius: 4px; + background: none; + color: #555; + cursor: pointer; +} + +.notes-panel__icon-option svg { + width: 16px; + height: 16px; +} + +.notes-panel__icon-option:hover { + background: #f0f0f0; +} + +.notes-panel__icon-option.active { + border-color: #1a5fb4; + color: #1a5fb4; + background: #dbe9ff; +} + .notes-panel__target, .notes-panel__new-name { width: 100%; @@ -917,7 +959,11 @@ html, body { cursor: pointer; } -.notes-panel__save { +/* ".notes-panel__actions " prefix (not just ".notes-panel__save") to + outrank ".notes-panel__actions button" — same class-count-vs-class+ + element specificity trap as elsewhere in this file; without it this + lost and the Save button just looked like a second Cancel button. */ +.notes-panel__actions .notes-panel__save { background: #1a5fb4; color: #fff; } diff --git a/public/js/icons.js b/public/js/icons.js index 8b0376c..d3d061d 100644 --- a/public/js/icons.js +++ b/public/js/icons.js @@ -305,6 +305,29 @@ export const PARK4NIGHT_TAGS = { windsurf: { icon: WIND_SURFING_ICON, label: "Windsurfing" }, }; +// Extends POI_ICONS (and so POI_ICON_KEYS, and what a marker badge can +// show — see gpx.js's _registerCategoryGlyphs) with a handful of the same +// symbols PARK4NIGHT_TAGS above already vendors, reused rather than +// duplicated — this app's own hand-drawn notes (notes.js's icon picker) +// need marker-badge glyphs for "water source"/"dump station"/"viewpoint" +// far more than the original OSM-POI-upload set (parking/hotel/fuel) ever +// covered. Object.assign, not more entries in the POI_ICONS literal +// itself, since these constants are declared below it in this file. +Object.assign(POI_ICONS, { + amenity_drinking_water: DRINKING_WATER_ICON, + amenity_sanitary_dump_station: SANITARY_DISPOSAL_ICON, + amenity_toilets: RESTROOMS_ICON, + shop_gas: PROPANE_ICON, + amenity_electricity: ELECTRICAL_HOOKUP_ICON, + shop_laundry: LAUNDRY_ICON, + leisure_playground: PLAYGROUND_ICON, + tourism_viewpoint: SCENIC_VIEWPOINT_ICON, + leisure_swimming: SWIMMING_ICON, + leisure_fishing: FISHING_ICON, + sport_climbing: CLIMBING_ICON, + route_hiking: WALKING_ICON, +}); + export const LIVETRACK_ICON = ` diff --git a/public/js/notes.js b/public/js/notes.js index a4ddc1a..c54dc86 100644 --- a/public/js/notes.js +++ b/public/js/notes.js @@ -1,6 +1,31 @@ -import { NOTE_PIN_ICON } from "/js/icons.js"; +import { Marker } from "/vendor/maplibre-gl/maplibre-gl.mjs"; +import { NOTE_PIN_ICON, POI_ICONS } from "/js/icons.js"; import { notifyGpxStoreChanged } from "/js/panels.js"; +// Human labels for the icon picker's title attribute — POI_ICONS' own keys +// are internal (OSM-tag-shaped) ids, not display text. +const ICON_LABELS = { + amenity_parking: "Parking", + tourism_camp_site: "Camp site", + leisure_picnic_table: "Picnic table", + amenity_fuel: "Fuel", + tourism_hotel: "Lodging", + forest: "Forest", + sport_cycling: "Cycling", + amenity_drinking_water: "Drinking water", + amenity_sanitary_dump_station: "Dump station", + amenity_toilets: "Toilets", + shop_gas: "Gas/LPG refill", + amenity_electricity: "Electricity", + shop_laundry: "Laundry", + leisure_playground: "Playground", + tourism_viewpoint: "Viewpoint", + leisure_swimming: "Swimming", + leisure_fishing: "Fishing", + sport_climbing: "Climbing", + route_hiking: "Hiking", +}; + const NEW_LAYER_VALUE = "__new__"; // Every hand-drawn note (from here or measure.js's "Save as note") gets one @@ -29,6 +54,15 @@ export class NoteSaveForm { this._el.hidden = true; this._el.innerHTML = `
+ + ${Object.keys(POI_ICONS) + .map( + (key) => + ``, + ) + .join("")} +
@@ -40,11 +74,19 @@ export class NoteSaveForm { container.appendChild(this._el); this._title = this._el.querySelector(".notes-panel__title"); + this._iconPicker = this._el.querySelector(".notes-panel__icons"); this._body = this._el.querySelector(".notes-panel__body"); this._target = this._el.querySelector(".notes-panel__target"); this._newName = this._el.querySelector(".notes-panel__new-name"); this._saveButton = this._el.querySelector(".notes-panel__save"); + this._selectedIcon = ""; + this._iconPicker.querySelectorAll(".notes-panel__icon-option").forEach((button) => { + button.addEventListener("click", () => { + this._selectedIcon = button.dataset.icon; + this._iconPicker.querySelectorAll(".notes-panel__icon-option").forEach((b) => b.classList.toggle("active", b === button)); + }); + }); this._target.addEventListener("change", () => { this._newName.hidden = this._target.value !== NEW_LAYER_VALUE; if (!this._newName.hidden) this._newName.focus(); @@ -62,6 +104,12 @@ export class NoteSaveForm { this._el.hidden = false; this._title.value = ""; this._body.value = ""; + this._selectedIcon = ""; + this._iconPicker.querySelectorAll(".notes-panel__icon-option").forEach((b, i) => b.classList.toggle("active", i === 0)); + // An icon only makes sense on a single point — Measure's "Save as + // note" can hand this a LineString/Polygon, which has no one spot for + // a badge glyph to sit at. + this._iconPicker.hidden = geometry.type !== "Point"; this._title.focus(); await this._populateTargets(); } @@ -112,6 +160,7 @@ export class NoteSaveForm { category: "Note", time: new Date().toISOString(), ...(details ? { description: details } : {}), + ...(this._selectedIcon ? { icon: this._selectedIcon } : {}), }, geometry: this._geometry, }; @@ -143,6 +192,7 @@ export class NoteSaveForm { export class NotesControl { constructor() { this._active = false; + this._marker = null; // where the pending note will land — visible until saved/cancelled } onAdd(map) { @@ -161,8 +211,14 @@ export class NotesControl { this._hint = this._container.querySelector(".notes-panel__hint"); this._form = new NoteSaveForm(this._panel, { - onSaved: () => (this._hint.hidden = false), - onCancel: () => (this._hint.hidden = false), + onSaved: () => { + this._hint.hidden = false; + this._removeMarker(); + }, + onCancel: () => { + this._hint.hidden = false; + this._removeMarker(); + }, }); this._button.addEventListener("click", () => this._setActive(!this._active)); @@ -170,6 +226,7 @@ export class NotesControl { this._onClick = (event) => { if (!this._active || this._form.isOpen) return; this._hint.hidden = true; + this._marker = new Marker({ color: "#c0392b" }).setLngLat(event.lngLat).addTo(this._map); this._form.open({ type: "Point", coordinates: [event.lngLat.lng, event.lngLat.lat] }); }; map.on("click", this._onClick); @@ -177,6 +234,11 @@ export class NotesControl { return this._container; } + _removeMarker() { + this._marker?.remove(); + this._marker = null; + } + onRemove() { this._map.off("click", this._onClick); this._container.remove(); @@ -191,7 +253,10 @@ export class NotesControl { this._button.classList.toggle("active", active); this._panel.hidden = !active; this._map.getCanvas().style.cursor = active ? "crosshair" : ""; - if (!active) this._form.close(); + if (!active) { + this._form.close(); + this._removeMarker(); + } } }