NPMap4 has infoboxControl: a generic "click anything, show every raw property" debug view, kept separate from a dataset's own curated popup. Not portable code (Leaflet), but the concept is: Alt+click anywhere now opens a popup listing every rendered feature at that point via map.queryRenderedFeatures — base map OSM tags included, not just this app's own GPX/FIT/radar layers — with a raw key/value table per feature. No dedicated UI for it on purpose: a keyboard-modified click needs no button, toggle state, or cursor change of its own, and it's fine for it to coexist with a layer's own click handler firing on the same click (e.g. a GPX waypoint's normal popup) since this is a power-user/debugging affordance, not a replacement for those. Feature properties can come from an uploaded GPX/FIT file (untrusted input) via this app's own layers, so every value is escaped the same way gpx.js's popups already are.
813 lines
13 KiB
CSS
813 lines
13 KiB
CSS
html, body {
|
|
height: 100%;
|
|
margin: 0;
|
|
font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
}
|
|
|
|
#app {
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.topbar {
|
|
flex: 0 0 auto;
|
|
display: flex;
|
|
align-items: center;
|
|
height: 48px;
|
|
padding: 0 16px;
|
|
background: #fff;
|
|
border-bottom: 1px solid #ddd;
|
|
}
|
|
|
|
.topbar__brand {
|
|
font-size: 15px;
|
|
font-weight: 700;
|
|
color: #333;
|
|
}
|
|
|
|
.topbar__auth {
|
|
margin-left: auto;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 13px;
|
|
color: #555;
|
|
}
|
|
|
|
.app-body {
|
|
flex: 1;
|
|
display: flex;
|
|
min-height: 0;
|
|
}
|
|
|
|
.sidebar {
|
|
flex: 0 0 52px;
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 8px 0;
|
|
background: #fff;
|
|
border-right: 1px solid #ddd;
|
|
/* No overflow-y:auto here: setting only one axis to a clipping value
|
|
forces the browser to compute the other axis as "auto" too (CSS
|
|
Overflow spec), which would clip the flyouts — they intentionally
|
|
poke out past the sidebar's own width via left:44px. */
|
|
}
|
|
|
|
.sidebar-item {
|
|
position: relative;
|
|
}
|
|
|
|
.sidebar-icon {
|
|
width: 36px;
|
|
height: 36px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: none;
|
|
border-radius: 6px;
|
|
background: none;
|
|
color: #444;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.sidebar-icon svg {
|
|
width: 20px;
|
|
height: 20px;
|
|
}
|
|
|
|
.sidebar-icon:hover {
|
|
background: #f0f0f0;
|
|
}
|
|
|
|
.sidebar-icon.active {
|
|
background: #dbe9ff;
|
|
color: #1a5fb4;
|
|
}
|
|
|
|
.sidebar-flyout {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 44px;
|
|
z-index: 20;
|
|
min-width: max-content;
|
|
background: #fff;
|
|
border: 1px solid #ddd;
|
|
border-radius: 6px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
padding: 6px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.sidebar-flyout[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
.sidebar-flyout__option {
|
|
display: block;
|
|
width: 100%;
|
|
padding: 6px 10px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
background: none;
|
|
font: inherit;
|
|
font-size: 13px;
|
|
text-align: left;
|
|
white-space: nowrap;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.sidebar-flyout__option:hover {
|
|
background: #f0f0f0;
|
|
}
|
|
|
|
.sidebar-flyout__option.active {
|
|
background: #dbe9ff;
|
|
font-weight: 600;
|
|
}
|
|
|
|
#map {
|
|
flex: 1;
|
|
min-width: 0;
|
|
position: relative;
|
|
}
|
|
|
|
.radar-control__playback {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.radar-control__play {
|
|
border: none;
|
|
background: none;
|
|
padding: 2px 4px;
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.radar-control__scrub {
|
|
width: 110px;
|
|
}
|
|
|
|
.radar-control__time {
|
|
font-variant-numeric: tabular-nums;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.basemap-panel {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 52px;
|
|
width: 260px;
|
|
max-width: 90vw;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: #fff;
|
|
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
|
|
font-size: 13px;
|
|
z-index: 10;
|
|
}
|
|
|
|
.basemap-panel[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
.basemap-panel__header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 10px 12px;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
|
|
.basemap-panel__close {
|
|
border: none;
|
|
background: none;
|
|
font-size: 18px;
|
|
line-height: 1;
|
|
padding: 0 4px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.basemap-panel__list {
|
|
padding: 6px;
|
|
}
|
|
|
|
.basemap-panel__option {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
width: 100%;
|
|
padding: 10px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
background: none;
|
|
font: inherit;
|
|
font-size: 14px;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.basemap-panel__option:hover {
|
|
background: #f0f0f0;
|
|
}
|
|
|
|
.basemap-panel__option.active {
|
|
background: #dbe9ff;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.basemap-panel__check {
|
|
color: #1a5fb4;
|
|
visibility: hidden;
|
|
}
|
|
|
|
.basemap-panel__option.active .basemap-panel__check {
|
|
visibility: visible;
|
|
}
|
|
|
|
.gpx-panel {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 52px;
|
|
width: 300px;
|
|
max-width: 90vw;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: #fff;
|
|
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
|
|
font-size: 13px;
|
|
z-index: 10;
|
|
}
|
|
|
|
.gpx-panel[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
.gpx-panel__header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 10px 12px;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
|
|
.gpx-panel__actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 2px;
|
|
}
|
|
|
|
.gpx-panel__action {
|
|
width: 30px;
|
|
height: 30px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: none;
|
|
border-radius: 6px;
|
|
background: none;
|
|
color: #444;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.gpx-panel__action svg {
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
|
|
.gpx-panel__action:hover {
|
|
background: #f0f0f0;
|
|
}
|
|
|
|
.gpx-panel__close {
|
|
border: none;
|
|
background: none;
|
|
font-size: 18px;
|
|
line-height: 1;
|
|
padding: 0 4px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.gpx-upload__status {
|
|
margin: 0 12px 10px;
|
|
color: #666;
|
|
}
|
|
|
|
.gpx-tree {
|
|
/* flex:1 (not just overflow-y) so the tree fills the rest of the panel
|
|
even when short/empty — otherwise the blank space below it wouldn't
|
|
be part of the drop target and dropping a file there would silently
|
|
do nothing. */
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 8px 12px;
|
|
}
|
|
|
|
.gpx-tree ul {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding-left: 16px;
|
|
}
|
|
|
|
.gpx-tree > ul {
|
|
padding-left: 0;
|
|
}
|
|
|
|
.gpx-tree summary {
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.gpx-tree__folder-row {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 4px;
|
|
width: calc(100% - 18px);
|
|
}
|
|
|
|
.gpx-tree summary.gpx-drop-hover {
|
|
background: #eef5fd;
|
|
outline: 1px dashed #4a90d9;
|
|
}
|
|
|
|
.gpx-tree__file {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 2px 0;
|
|
cursor: grab;
|
|
}
|
|
|
|
.gpx-tree__file label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
flex: 1;
|
|
min-width: 0;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.gpx-tree__file-name {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.gpx-tree__kind-icon {
|
|
flex-shrink: 0;
|
|
display: flex;
|
|
color: #999;
|
|
}
|
|
|
|
.gpx-tree__kind-icon svg {
|
|
width: 14px;
|
|
height: 14px;
|
|
}
|
|
|
|
.gpx-tree__delete {
|
|
flex-shrink: 0;
|
|
border: none;
|
|
background: none;
|
|
color: #999;
|
|
font-size: 14px;
|
|
line-height: 1;
|
|
padding: 0 4px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.gpx-tree__delete:hover {
|
|
color: #e63946;
|
|
}
|
|
|
|
.gpx-tree__empty {
|
|
color: #888;
|
|
font-style: italic;
|
|
}
|
|
|
|
.gpx-tree.gpx-drop-hover {
|
|
outline: 1px dashed #4a90d9;
|
|
outline-offset: -2px;
|
|
}
|
|
|
|
.gpx-legend {
|
|
flex-shrink: 0;
|
|
max-height: 140px;
|
|
overflow-y: auto;
|
|
padding: 8px 12px;
|
|
border-top: 1px solid #eee;
|
|
}
|
|
|
|
.gpx-legend[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
.gpx-legend__item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 2px 0;
|
|
}
|
|
|
|
.gpx-legend__swatch {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
#map.gpx-drop-hover::after {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0;
|
|
outline: 3px dashed #4a90d9;
|
|
outline-offset: -3px;
|
|
background: rgba(74, 144, 217, 0.08);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.maplibregl-popup-content p {
|
|
margin: 4px 0 0;
|
|
}
|
|
|
|
.debug-popup .maplibregl-popup-content {
|
|
max-height: 320px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.debug-popup__feature {
|
|
font-size: 11px;
|
|
font-family: monospace;
|
|
}
|
|
|
|
.debug-popup__feature + .debug-popup__feature {
|
|
margin-top: 8px;
|
|
padding-top: 8px;
|
|
border-top: 1px solid #eee;
|
|
}
|
|
|
|
.debug-popup__feature table {
|
|
border-collapse: collapse;
|
|
width: 100%;
|
|
}
|
|
|
|
.debug-popup__feature th {
|
|
text-align: left;
|
|
color: #888;
|
|
font-weight: normal;
|
|
padding: 1px 6px 1px 0;
|
|
vertical-align: top;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.debug-popup__feature td {
|
|
word-break: break-word;
|
|
}
|
|
|
|
.gpx-popup__time {
|
|
color: #888;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.gpx-popup__category {
|
|
color: #888;
|
|
font-size: 12px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
|
|
.gpx-popup__description {
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.gpx-popup__meta {
|
|
margin-top: 8px;
|
|
padding-top: 6px;
|
|
border-top: 1px solid #eee;
|
|
font-size: 12px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.gpx-popup__meta-row + .gpx-popup__meta-row {
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.gpx-popup__meta-label {
|
|
color: #888;
|
|
margin-right: 4px;
|
|
}
|
|
|
|
.gpx-popup__tags {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 4px 10px;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.gpx-popup__tag {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 3px;
|
|
}
|
|
|
|
.gpx-popup__tag-icon {
|
|
display: flex;
|
|
width: 13px;
|
|
height: 13px;
|
|
color: #555;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.gpx-popup__tag-icon svg {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.gpx-popup__link {
|
|
margin-top: 6px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.gnss-control__status {
|
|
color: #888;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.gnss-marker {
|
|
width: 14px;
|
|
height: 14px;
|
|
border: 2px solid #fff;
|
|
border-radius: 50%;
|
|
background: #2a9d8f;
|
|
box-shadow: 0 0 0 2px rgba(42, 157, 143, 0.4);
|
|
}
|
|
|
|
.livetrack-panel__list {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 8px 12px;
|
|
}
|
|
|
|
.livetrack-panel__item {
|
|
border-bottom: 1px solid #f0f0f0;
|
|
}
|
|
|
|
.livetrack-panel__row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
width: 100%;
|
|
padding: 6px 4px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
background: none;
|
|
font: inherit;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.livetrack-panel__row:hover {
|
|
background: #f0f0f0;
|
|
}
|
|
|
|
.livetrack-panel__row.active {
|
|
background: #f1e6fb;
|
|
}
|
|
|
|
.livetrack-panel__row.loading {
|
|
opacity: 0.6;
|
|
cursor: wait;
|
|
}
|
|
|
|
.livetrack-panel__label {
|
|
flex: 1;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.livetrack-panel__label--id {
|
|
font-family: monospace;
|
|
}
|
|
|
|
.livetrack-badge {
|
|
flex-shrink: 0;
|
|
padding: 2px 6px;
|
|
border-radius: 3px;
|
|
background: #e5e5e5;
|
|
color: #666;
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
|
|
.livetrack-badge--live {
|
|
background: #d7f3e3;
|
|
color: #1b7a43;
|
|
}
|
|
|
|
.livetrack-panel__time {
|
|
flex-shrink: 0;
|
|
color: #888;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.profile-panel {
|
|
position: fixed;
|
|
left: 52px;
|
|
right: 0;
|
|
bottom: 0;
|
|
max-height: 45vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: #fcfcfb;
|
|
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.15);
|
|
z-index: 10;
|
|
}
|
|
|
|
.profile-panel[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
.profile-panel__header {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 10px;
|
|
padding: 10px 14px;
|
|
border-bottom: 1px solid #e1e0d9;
|
|
}
|
|
|
|
.profile-panel__title {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.profile-panel__stats {
|
|
flex: 1;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
color: #52514e;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.profile-panel__close {
|
|
border: none;
|
|
background: none;
|
|
font-size: 18px;
|
|
line-height: 1;
|
|
padding: 0 4px;
|
|
cursor: pointer;
|
|
color: #898781;
|
|
}
|
|
|
|
.profile-panel__charts {
|
|
position: relative;
|
|
overflow-y: auto;
|
|
padding: 8px 14px 14px;
|
|
}
|
|
|
|
.profile-chart {
|
|
padding-top: 8px;
|
|
}
|
|
|
|
.profile-chart + .profile-chart {
|
|
border-top: 1px solid #e1e0d9;
|
|
}
|
|
|
|
.profile-chart__label {
|
|
font-size: 11px;
|
|
color: #898781;
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.profile-chart__unit {
|
|
font-weight: normal;
|
|
}
|
|
|
|
.profile-chart__plot {
|
|
position: relative;
|
|
}
|
|
|
|
.profile-chart__svg {
|
|
display: block;
|
|
width: 100%;
|
|
height: 100%;
|
|
cursor: crosshair;
|
|
}
|
|
|
|
.profile-chart__crosshair {
|
|
stroke: #898781;
|
|
stroke-width: 1;
|
|
vector-effect: non-scaling-stroke;
|
|
}
|
|
|
|
/* Overlaid as plain HTML, not SVG text: the chart's viewBox scales width
|
|
and height independently (preserveAspectRatio="none", so the line fills
|
|
a responsive width at a fixed pixel height) — text drawn inside that
|
|
coordinate space would get stretched by the same mismatched factor. */
|
|
.profile-chart__axis-value {
|
|
position: absolute;
|
|
left: 4px;
|
|
color: #898781;
|
|
font-size: 10px;
|
|
line-height: 1;
|
|
background: rgba(252, 252, 251, 0.75);
|
|
padding: 1px 3px;
|
|
border-radius: 2px;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.profile-chart__axis-value--max {
|
|
top: 2px;
|
|
}
|
|
|
|
.profile-chart__axis-value--min {
|
|
bottom: 2px;
|
|
}
|
|
|
|
.profile-panel__xaxis-toggle {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 4px;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.profile-panel__xaxis-toggle-btn {
|
|
border: 1px solid #e1e0d9;
|
|
background: #fcfcfb;
|
|
color: #52514e;
|
|
font-size: 11px;
|
|
padding: 2px 8px;
|
|
border-radius: 3px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.profile-panel__xaxis-toggle-btn.active {
|
|
background: #0b0b0b;
|
|
border-color: #0b0b0b;
|
|
color: #fff;
|
|
}
|
|
|
|
.profile-panel__xaxis {
|
|
position: relative;
|
|
height: 14px;
|
|
margin-top: 6px;
|
|
}
|
|
|
|
.profile-panel__xaxis-tick {
|
|
position: absolute;
|
|
top: 0;
|
|
transform: translateX(-50%);
|
|
font-size: 10px;
|
|
color: #898781;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.profile-panel__xaxis-tick:first-child {
|
|
transform: translateX(0);
|
|
}
|
|
|
|
.profile-panel__xaxis-tick:last-child {
|
|
transform: translateX(-100%);
|
|
}
|
|
|
|
.profile-panel__tooltip {
|
|
position: absolute;
|
|
top: 0;
|
|
z-index: 1;
|
|
pointer-events: none;
|
|
background: #0b0b0b;
|
|
color: #fff;
|
|
border-radius: 4px;
|
|
padding: 6px 8px;
|
|
font-size: 12px;
|
|
white-space: nowrap;
|
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
|
|
}
|
|
|
|
.profile-panel__tooltip[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
.profile-panel__tooltip-distance {
|
|
font-weight: 600;
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.profile-panel__tooltip-label {
|
|
color: #c3c2b7;
|
|
}
|