mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-08-07 09:22:53 +02:00
Refactor front-end assets into external modules (#245)
* Refactor front-end assets into external modules * Restore chat flag inline script * Declare legend toggle control variable * Remove dynamic background generation * Restore background script with theme-based color * run rufo
This commit is contained in:
+38
@@ -49,6 +49,22 @@ MAX_JSON_BODY_BYTES = begin
|
||||
end
|
||||
# Fallback version string used when Git metadata is unavailable.
|
||||
VERSION_FALLBACK = "v0.3.0"
|
||||
DEFAULT_REFRESH_INTERVAL_SECONDS = 60
|
||||
REFRESH_INTERVAL_SECONDS = begin
|
||||
raw = ENV.fetch("REFRESH_INTERVAL_SECONDS", DEFAULT_REFRESH_INTERVAL_SECONDS.to_s)
|
||||
value = Integer(raw, 10)
|
||||
value.positive? ? value : DEFAULT_REFRESH_INTERVAL_SECONDS
|
||||
rescue ArgumentError
|
||||
DEFAULT_REFRESH_INTERVAL_SECONDS
|
||||
end
|
||||
MAP_TILE_FILTER_LIGHT = ENV.fetch(
|
||||
"MAP_TILE_FILTER_LIGHT",
|
||||
"grayscale(1) saturate(0) brightness(0.92) contrast(1.05)"
|
||||
)
|
||||
MAP_TILE_FILTER_DARK = ENV.fetch(
|
||||
"MAP_TILE_FILTER_DARK",
|
||||
"grayscale(1) invert(1) brightness(0.9) contrast(1.08)"
|
||||
)
|
||||
|
||||
# Fetch a configuration string from environment variables.
|
||||
#
|
||||
@@ -162,6 +178,25 @@ def sanitized_default_frequency
|
||||
sanitized_string(DEFAULT_FREQUENCY)
|
||||
end
|
||||
|
||||
# Assemble configuration exposed to the frontend JavaScript bundle.
|
||||
#
|
||||
# @return [Hash] settings describing refresh cadence and map defaults.
|
||||
def frontend_app_config
|
||||
{
|
||||
refreshIntervalSeconds: REFRESH_INTERVAL_SECONDS,
|
||||
refreshMs: REFRESH_INTERVAL_SECONDS * 1000,
|
||||
chatEnabled: !private_mode?,
|
||||
defaultChannel: sanitized_default_channel,
|
||||
defaultFrequency: sanitized_default_frequency,
|
||||
mapCenter: { lat: MAP_CENTER_LAT, lon: MAP_CENTER_LON },
|
||||
maxNodeDistanceKm: MAX_NODE_DISTANCE_KM,
|
||||
tileFilters: {
|
||||
light: MAP_TILE_FILTER_LIGHT,
|
||||
dark: MAP_TILE_FILTER_DARK,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
# Return the configured Matrix room when present.
|
||||
#
|
||||
# @return [String, nil] Matrix room identifier or nil when blank.
|
||||
@@ -1919,6 +1954,7 @@ end
|
||||
# Renders the main site with configuration-driven defaults for the template.
|
||||
get "/" do
|
||||
meta = meta_configuration
|
||||
config = frontend_app_config
|
||||
|
||||
response.set_cookie("theme", value: "dark", path: "/", max_age: 60 * 60 * 24 * 7, same_site: :lax) unless request.cookies["theme"]
|
||||
|
||||
@@ -1935,5 +1971,7 @@ get "/" do
|
||||
matrix_room: sanitized_matrix_room,
|
||||
version: APP_VERSION,
|
||||
private_mode: private_mode?,
|
||||
refresh_interval_seconds: REFRESH_INTERVAL_SECONDS,
|
||||
app_config_json: JSON.generate(config),
|
||||
}
|
||||
end
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
const CONFIG_SELECTOR = '[data-app-config]';
|
||||
|
||||
export function readAppConfig() {
|
||||
const el = document.querySelector(CONFIG_SELECTOR);
|
||||
if (!el) {
|
||||
return {};
|
||||
}
|
||||
const raw = el.getAttribute('data-app-config') || '';
|
||||
if (!raw) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return typeof parsed === 'object' && parsed !== null ? parsed : {};
|
||||
} catch (err) {
|
||||
console.error('Failed to parse application configuration', err);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { readAppConfig } from './config.js';
|
||||
import { initializeApp } from './main.js';
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
refreshMs: 60_000,
|
||||
refreshIntervalSeconds: 60,
|
||||
chatEnabled: true,
|
||||
defaultChannel: '#MediumFast',
|
||||
defaultFrequency: '868MHz',
|
||||
mapCenter: { lat: 52.502889, lon: 13.404194 },
|
||||
maxNodeDistanceKm: 137,
|
||||
tileFilters: {
|
||||
light: 'grayscale(1) saturate(0) brightness(0.92) contrast(1.05)',
|
||||
dark: 'grayscale(1) invert(1) brightness(0.9) contrast(1.08)'
|
||||
}
|
||||
};
|
||||
|
||||
function mergeConfig(raw) {
|
||||
const config = { ...DEFAULT_CONFIG, ...(raw || {}) };
|
||||
config.mapCenter = {
|
||||
lat: Number(raw?.mapCenter?.lat ?? DEFAULT_CONFIG.mapCenter.lat),
|
||||
lon: Number(raw?.mapCenter?.lon ?? DEFAULT_CONFIG.mapCenter.lon)
|
||||
};
|
||||
config.tileFilters = {
|
||||
light: raw?.tileFilters?.light || DEFAULT_CONFIG.tileFilters.light,
|
||||
dark: raw?.tileFilters?.dark || DEFAULT_CONFIG.tileFilters.dark
|
||||
};
|
||||
const refreshIntervalSeconds = Number(
|
||||
raw?.refreshIntervalSeconds ?? DEFAULT_CONFIG.refreshIntervalSeconds
|
||||
);
|
||||
config.refreshIntervalSeconds = Number.isFinite(refreshIntervalSeconds)
|
||||
? refreshIntervalSeconds
|
||||
: DEFAULT_CONFIG.refreshIntervalSeconds;
|
||||
const refreshMs = Number(raw?.refreshMs ?? config.refreshIntervalSeconds * 1000);
|
||||
config.refreshMs = Number.isFinite(refreshMs) ? refreshMs : DEFAULT_CONFIG.refreshMs;
|
||||
config.chatEnabled = Boolean(raw?.chatEnabled ?? DEFAULT_CONFIG.chatEnabled);
|
||||
config.defaultChannel = raw?.defaultChannel || DEFAULT_CONFIG.defaultChannel;
|
||||
config.defaultFrequency = raw?.defaultFrequency || DEFAULT_CONFIG.defaultFrequency;
|
||||
const maxDistance = Number(raw?.maxNodeDistanceKm ?? DEFAULT_CONFIG.maxNodeDistanceKm);
|
||||
config.maxNodeDistanceKm = Number.isFinite(maxDistance)
|
||||
? maxDistance
|
||||
: DEFAULT_CONFIG.maxNodeDistanceKm;
|
||||
return config;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const rawConfig = readAppConfig();
|
||||
const config = mergeConfig(rawConfig);
|
||||
initializeApp(config);
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function resolveBackgroundColor() {
|
||||
if (!document.body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var color = '';
|
||||
try {
|
||||
var styles = window.getComputedStyle(document.body);
|
||||
if (styles) {
|
||||
color = styles.getPropertyValue('--bg');
|
||||
if (color) {
|
||||
color = color.trim();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
color = '';
|
||||
}
|
||||
|
||||
if (!color) {
|
||||
color = document.body.classList.contains('dark') ? '#0e1418' : '#f6f3ee';
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
function applyBackground() {
|
||||
var color = resolveBackgroundColor();
|
||||
if (!color) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.documentElement.style.backgroundColor = color;
|
||||
document.documentElement.style.backgroundImage = 'none';
|
||||
document.body.style.backgroundColor = color;
|
||||
document.body.style.backgroundImage = 'none';
|
||||
}
|
||||
|
||||
function init() {
|
||||
applyBackground();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
window.addEventListener('themechange', applyBackground);
|
||||
|
||||
window.__potatoBackground = {
|
||||
applyBackground: applyBackground,
|
||||
resolveBackgroundColor: resolveBackgroundColor
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,58 @@
|
||||
(function () {
|
||||
var THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
|
||||
function getCookie(name) {
|
||||
var matcher = new RegExp(
|
||||
'(?:^|; )' + name.replace(/([.$?*|{}()\[\]\\/+^])/g, '\\$1') + '=([^;]*)'
|
||||
);
|
||||
var match = document.cookie.match(matcher);
|
||||
return match ? decodeURIComponent(match[1]) : null;
|
||||
}
|
||||
|
||||
function setCookie(name, value, opts) {
|
||||
var options = Object.assign(
|
||||
{ path: '/', 'max-age': THEME_COOKIE_MAX_AGE, SameSite: 'Lax' },
|
||||
opts || {}
|
||||
);
|
||||
var updated = encodeURIComponent(name) + '=' + encodeURIComponent(value);
|
||||
for (var k in options) {
|
||||
if (!Object.prototype.hasOwnProperty.call(options, k)) continue;
|
||||
updated += '; ' + k + (options[k] === true ? '' : '=' + options[k]);
|
||||
}
|
||||
document.cookie = updated;
|
||||
}
|
||||
|
||||
function persistTheme(value) {
|
||||
setCookie('theme', value, { 'max-age': THEME_COOKIE_MAX_AGE });
|
||||
}
|
||||
|
||||
var theme = getCookie('theme');
|
||||
if (theme !== 'dark' && theme !== 'light') {
|
||||
theme = 'dark';
|
||||
}
|
||||
persistTheme(theme);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
if (theme === 'dark') {
|
||||
document.body.classList.add('dark');
|
||||
} else {
|
||||
document.body.classList.remove('dark');
|
||||
}
|
||||
|
||||
var btn = document.getElementById('themeToggle');
|
||||
if (btn) {
|
||||
btn.textContent = document.body.classList.contains('dark') ? '☀️' : '🌙';
|
||||
}
|
||||
|
||||
if (typeof window.applyFiltersToAllTiles === 'function') {
|
||||
window.applyFiltersToAllTiles();
|
||||
}
|
||||
});
|
||||
|
||||
window.__themeCookie = {
|
||||
getCookie: getCookie,
|
||||
setCookie: setCookie,
|
||||
persistTheme: persistTheme,
|
||||
maxAge: THEME_COOKIE_MAX_AGE
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,931 @@
|
||||
:root {
|
||||
--bg: #f6f3ee;
|
||||
--bg2: #ffffff;
|
||||
--fg: #0c0f12;
|
||||
--muted: #5c6773;
|
||||
--card: rgba(0, 0, 0, 0.03);
|
||||
--line: rgba(12, 15, 18, 0.08);
|
||||
--accent: #2b6cb0;
|
||||
--row-alt: rgba(0, 0, 0, 0.02);
|
||||
--table-head-bg: rgba(0, 0, 0, 0.06);
|
||||
--table-head-fg: var(--fg);
|
||||
--pad: 16px;
|
||||
--map-tile-filter-light: grayscale(1) saturate(0) brightness(0.92) contrast(1.05);
|
||||
--map-tile-filter-dark: grayscale(1) invert(1) brightness(0.9) contrast(1.08);
|
||||
}
|
||||
|
||||
body.dark {
|
||||
--bg: #0e1418;
|
||||
--bg2: #0e141b;
|
||||
--fg: #e6ebf0;
|
||||
--muted: #9aa7b4;
|
||||
--card: rgba(255, 255, 255, 0.04);
|
||||
--line: rgba(255, 255, 255, 0.1);
|
||||
--accent: #5fa8ff;
|
||||
--row-alt: rgba(255, 255, 255, 0.05);
|
||||
--table-head-bg: rgba(255, 255, 255, 0.06);
|
||||
--table-head-fg: var(--fg);
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
background-color: var(--bg);
|
||||
color: var(--fg);
|
||||
transition: background-color 160ms ease, color 160ms ease;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
hr {
|
||||
border-color: var(--line);
|
||||
}
|
||||
|
||||
.card,
|
||||
.panel,
|
||||
.box {
|
||||
background: var(--card);
|
||||
backdrop-filter: blur(2px);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
thead th {
|
||||
background: var(--table-head-bg);
|
||||
color: var(--table-head-fg);
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
tbody td {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
tbody tr:nth-child(even) td {
|
||||
background: var(--row-alt);
|
||||
}
|
||||
|
||||
.leaflet-container {
|
||||
background: transparent !important;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.neighbor-connection-line {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.neighbor-snr {
|
||||
margin-left: 4px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: system-ui, Segoe UI, Roboto, Ubuntu, Arial, sans-serif;
|
||||
margin: var(--pad);
|
||||
padding-bottom: 32px;
|
||||
--map-tiles-filter: var(--map-tile-filter-light);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.site-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.site-title img {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
display: block;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.meta {
|
||||
color: #555;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.pill {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: #eee;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#map {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
height: 60vh;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#map[data-map-status="placeholder"] {
|
||||
background: repeating-linear-gradient(
|
||||
135deg,
|
||||
rgba(0, 0, 0, 0.02),
|
||||
rgba(0, 0, 0, 0.02) 12px,
|
||||
rgba(0, 0, 0, 0.04) 12px,
|
||||
rgba(0, 0, 0, 0.04) 24px
|
||||
);
|
||||
}
|
||||
|
||||
#map .map-placeholder-message {
|
||||
text-align: center;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
#map .map-placeholder-message strong {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#map .map-placeholder-message span {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
body.dark #map .map-placeholder-message {
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
#map .map-status-message {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
backdrop-filter: blur(6px);
|
||||
pointer-events: none;
|
||||
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #333;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||
z-index: 1200;
|
||||
}
|
||||
|
||||
body.dark #map[data-map-status="placeholder"] {
|
||||
background: repeating-linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 255, 255, 0.02),
|
||||
rgba(255, 255, 255, 0.02) 12px,
|
||||
rgba(255, 255, 255, 0.06) 12px,
|
||||
rgba(255, 255, 255, 0.06) 24px
|
||||
);
|
||||
}
|
||||
|
||||
body.dark #map .map-status-message {
|
||||
border-color: rgba(255, 255, 255, 0.16);
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
color: #eee;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 4px 6px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: ui-monospace, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: var(--pad);
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.map-row {
|
||||
display: flex;
|
||||
gap: var(--pad);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
#chat {
|
||||
flex: 0 0 33%;
|
||||
max-width: 33%;
|
||||
height: 60vh;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
overflow-y: auto;
|
||||
padding: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.chat-entry-node {
|
||||
font-family: ui-monospace, Menlo, Consolas, monospace;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.chat-entry-msg {
|
||||
font-family: ui-monospace, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.chat-entry-date {
|
||||
font-family: ui-monospace, Menlo, Consolas, monospace;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.short-name {
|
||||
display: inline-block;
|
||||
border-radius: 4px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.short-name[data-node-info] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.short-info-overlay {
|
||||
position: absolute;
|
||||
background: #fff;
|
||||
color: #111;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
|
||||
padding: 8px 10px 10px;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
min-width: 200px;
|
||||
max-width: 240px;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.short-info-overlay[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.short-info-overlay .short-info-close {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.short-info-overlay .short-info-close:hover {
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.short-info-content {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.meta-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.refresh-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.refresh-info {
|
||||
margin: 0;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.refresh-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.auto-refresh-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.controls label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.controls .filter-input {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: 1 1 260px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.filter-input input[type="text"] {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
padding-right: 28px;
|
||||
}
|
||||
|
||||
.filter-clear {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.filter-clear:hover {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
body.dark .filter-clear:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.filter-clear:focus-visible {
|
||||
outline: 2px solid #4a90e2;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #ccc;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
.sort-button {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.sort-button:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.sort-button:focus-visible {
|
||||
outline: 2px solid #4a90e2;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.sort-indicator {
|
||||
font-size: 0.75em;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
th[aria-sort] .sort-indicator {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.legend {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
color: var(--fg);
|
||||
padding: 8px 10px 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
min-width: 160px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.legend-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 4px;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.legend-title {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.legend-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
padding: 3px 6px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.legend-item:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.legend-item:focus-visible {
|
||||
outline: 2px solid #4a90e2;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.legend-item[aria-pressed="true"] {
|
||||
border-color: rgba(0, 0, 0, 0.2);
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.legend-swatch {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.legend-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.legend-toggle {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.legend-toggle-button {
|
||||
font-size: 12px;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
#map .leaflet-tile-pane,
|
||||
#map .leaflet-layer,
|
||||
#map .leaflet-tile.map-tiles {
|
||||
opacity: 0.75;
|
||||
filter: var(--map-tiles-filter, var(--map-tile-filter-light));
|
||||
-webkit-filter: var(--map-tiles-filter, var(--map-tile-filter-light));
|
||||
}
|
||||
|
||||
.leaflet-popup-content-wrapper,
|
||||
.leaflet-popup-tip {
|
||||
background: #fff;
|
||||
color: #333;
|
||||
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
#nodes {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: var(--pad);
|
||||
width: calc(100% - 2 * var(--pad));
|
||||
background: #fafafa;
|
||||
border-top: 1px solid #ddd;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.info-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--pad);
|
||||
z-index: 4000;
|
||||
}
|
||||
|
||||
.info-overlay[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.info-dialog {
|
||||
background: #fff;
|
||||
color: #111;
|
||||
max-width: 420px;
|
||||
width: min(100%, 420px);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.2);
|
||||
position: relative;
|
||||
padding: 20px 24px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.info-dialog:focus {
|
||||
outline: 2px solid #4a90e2;
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
.info-close {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
padding: 4px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.info-close:hover {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.info-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.info-intro {
|
||||
margin: 0 0 12px;
|
||||
font-size: 14px;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.info-details {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.info-details dt {
|
||||
font-weight: 600;
|
||||
margin-top: 12px;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.info-details dd {
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
.info-details dd a {
|
||||
color: inherit;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
#nodes th:nth-child(15),
|
||||
#nodes td:nth-child(15),
|
||||
#nodes th:nth-child(16),
|
||||
#nodes td:nth-child(16),
|
||||
#nodes th:nth-child(17),
|
||||
#nodes td:nth-child(17),
|
||||
#nodes th:nth-child(18),
|
||||
#nodes td:nth-child(18) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--pad);
|
||||
}
|
||||
|
||||
.site-title img {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.map-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.controls {
|
||||
order: 2;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto auto;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.controls .filter-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.controls button {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.meta-info {
|
||||
order: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.refresh-row {
|
||||
grid-template-columns: 1fr;
|
||||
row-gap: 8px;
|
||||
}
|
||||
|
||||
.refresh-actions {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
justify-self: start;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
#map {
|
||||
order: 1;
|
||||
flex: none;
|
||||
max-width: 100%;
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
#chat {
|
||||
order: 2;
|
||||
flex: none;
|
||||
max-width: 100%;
|
||||
height: 30vh;
|
||||
}
|
||||
|
||||
#nodes th:nth-child(1),
|
||||
#nodes td:nth-child(1),
|
||||
#nodes th:nth-child(5),
|
||||
#nodes td:nth-child(5),
|
||||
#nodes th:nth-child(6),
|
||||
#nodes td:nth-child(6),
|
||||
#nodes th:nth-child(9),
|
||||
#nodes td:nth-child(9),
|
||||
#nodes th:nth-child(11),
|
||||
#nodes td:nth-child(11),
|
||||
#nodes th:nth-child(13),
|
||||
#nodes td:nth-child(13),
|
||||
#nodes th:nth-child(14),
|
||||
#nodes td:nth-child(14),
|
||||
#nodes th:nth-child(15),
|
||||
#nodes td:nth-child(15),
|
||||
#nodes th:nth-child(16),
|
||||
#nodes td:nth-child(16),
|
||||
#nodes th:nth-child(17),
|
||||
#nodes td:nth-child(17),
|
||||
#nodes th:nth-child(18),
|
||||
#nodes td:nth-child(18) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.legend {
|
||||
max-width: min(240px, 80vw);
|
||||
}
|
||||
}
|
||||
|
||||
body.dark {
|
||||
background: #111;
|
||||
color: #eee;
|
||||
--map-tiles-filter: var(--map-tile-filter-dark);
|
||||
}
|
||||
|
||||
body.dark .meta {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
body.dark .refresh-info {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
body.dark .pill {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
body.dark #map {
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
body.dark #chat {
|
||||
border-color: #444;
|
||||
background: #222;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
body.dark th {
|
||||
background: #222;
|
||||
}
|
||||
|
||||
body.dark button {
|
||||
background: #333;
|
||||
border-color: #444;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
body.dark button:hover {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
body.dark .sort-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
body.dark .sort-button:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
body.dark label {
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
body.dark input[type="text"] {
|
||||
background: #222;
|
||||
color: #eee;
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
body.dark .legend {
|
||||
background: #333;
|
||||
border-color: #444;
|
||||
color: #eee;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
body.dark .legend-toggle-button {
|
||||
background: #333;
|
||||
border-color: #444;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
body.dark .legend-toggle-button:hover {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
body.dark .legend-item:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
body.dark .legend-item[aria-pressed="true"] {
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
|
||||
body.dark .leaflet-popup-content-wrapper,
|
||||
body.dark .leaflet-popup-tip {
|
||||
background: #333;
|
||||
color: #eee;
|
||||
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
body.dark footer {
|
||||
background: #222;
|
||||
border-top-color: #444;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
body.dark a {
|
||||
color: #9bd;
|
||||
}
|
||||
|
||||
body.dark .chat-entry-node {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
body.dark .chat-entry-msg {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
body.dark .short-name {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
body.dark .chat-entry-date {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
body.dark .info-overlay {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
body.dark .info-dialog {
|
||||
background: #1c1c1c;
|
||||
color: #eee;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
body.dark .info-intro {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
body.dark .info-details dt {
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
body.dark .info-close:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
body.dark .short-info-overlay {
|
||||
background: #1c1c1c;
|
||||
border-color: #444;
|
||||
color: #eee;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
body.dark .short-info-overlay .short-info-close:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
#map .leaflet-tile-pane,
|
||||
#map .leaflet-layer,
|
||||
#map .leaflet-tile.map-tiles {
|
||||
filter: var(--map-tiles-filter, var(--map-tile-filter-light));
|
||||
-webkit-filter: var(--map-tiles-filter, var(--map-tile-filter-light));
|
||||
}
|
||||
|
||||
body.dark #map .leaflet-tile-pane,
|
||||
body.dark #map .leaflet-layer,
|
||||
body.dark #map .leaflet-tile.map-tiles {
|
||||
filter: var(--map-tiles-filter, var(--map-tile-filter-dark));
|
||||
-webkit-filter: var(--map-tiles-filter, var(--map-tile-filter-dark));
|
||||
}
|
||||
+15
-3018
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user