design overhaul and default route
Sonarqube Scanner / Build and analyze (push) Failing after 10s

This commit is contained in:
Florian Zumpe
2026-09-27 20:21:29 +02:00
parent 98a7293efd
commit 8fb5b3a397
9 changed files with 615 additions and 97 deletions
+273 -83
View File
@@ -1,14 +1,16 @@
:root {
--bootstrap-bg: #f3f7ee;
--bootstrap-card-bg: #ffffff;
--bootstrap-text: #1c2b20;
--bootstrap-muted: #53605a;
--bootstrap-accent: #315d3a;
--bootstrap-accent-strong: #25462c;
--bootstrap-border: #d6dfd3;
--bootstrap-error: #8b1e1e;
--bootstrap-shadow: 0 1rem 3rem rgb(29 52 36 / 18%);
--bootstrap-text: #10291b;
--bootstrap-muted: #67726c;
--bootstrap-accent: #286438;
--bootstrap-accent-strong: #194d2b;
--bootstrap-border: #dce6dc;
--bootstrap-error: #a32020;
--bootstrap-shadow: 0 1.2rem 3.6rem rgb(29 52 36 / 16%);
--bootstrap-fade-duration: 180ms;
}
#bootstrap-screen {
position: fixed;
inset: 0;
@@ -18,7 +20,7 @@
justify-items: center;
min-height: 100svh;
box-sizing: border-box;
padding: max(1rem, env(safe-area-inset-top)) max(1rem, env(safe-area-inset-right)) max(1rem, env(safe-area-inset-bottom)) max(1rem, env(safe-area-inset-left));
padding: max(5.25rem, env(safe-area-inset-top)) max(1rem, env(safe-area-inset-right)) max(1rem, env(safe-area-inset-bottom)) max(1rem, env(safe-area-inset-left));
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior-y: contain;
@@ -26,127 +28,315 @@
-webkit-overflow-scrolling: touch;
color: var(--bootstrap-text);
background:
radial-gradient(circle at 20% 10%, rgb(135 185 112 / 32%), transparent 34rem),
radial-gradient(circle at 85% 90%, rgb(230 198 116 / 25%), transparent 28rem),
var(--bootstrap-bg);
radial-gradient(circle at 12% 6%, rgb(126 183 101 / 24%), transparent 27rem),
radial-gradient(circle at 88% 94%, rgb(235 196 104 / 18%), transparent 25rem),
linear-gradient(180deg, #f7faf4 0%, var(--bootstrap-bg) 100%);
}
#bootstrap-screen[hidden] {
display: none;
}
#bootstrap-screen > section {
width: min(100%, 34rem);
width: min(100%, 42rem);
box-sizing: border-box;
padding: clamp(1.25rem, 5vw, 2rem);
border: 1px solid var(--bootstrap-border);
border-radius: 1.5rem;
padding: clamp(1.1rem, 4vw, 1.8rem);
border: 1px solid rgb(214 224 214 / 80%);
border-radius: clamp(1.4rem, 5vw, 2rem);
box-shadow: var(--bootstrap-shadow);
background: var(--bootstrap-card-bg);
background: rgb(255 255 255 / 96%);
}
#bootstrap-screen [aria-hidden="true"] {
display: inline-block;
margin-bottom: .25rem;
font-size: 2rem;
.bootstrap-brand {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.bootstrap-logo {
width: clamp(4.25rem, 15vw, 5.75rem);
height: auto;
filter: drop-shadow(0 .35rem .6rem rgb(49 93 58 / 12%));
}
#bootstrap-screen h1 {
margin: 0 0 .5rem;
font-size: clamp(1.8rem, 8vw, 2.5rem);
line-height: 1.05;
margin: 0 0 .3rem;
color: #0d2b1b;
font-size: clamp(2rem, 8vw, 3rem);
line-height: 1;
letter-spacing: -.035em;
}
#bootstrap-screen p {
margin: .65rem 0;
#bootstrap-screen .bootstrap-brand p {
margin: 0;
color: var(--bootstrap-muted);
font-size: clamp(.98rem, 3.8vw, 1.18rem);
line-height: 1.35;
}
.bootstrap-hero {
overflow: hidden;
margin: .35rem 0 1rem;
border-radius: 1.5rem;
background: #eef7f1;
}
.bootstrap-hero svg {
display: block;
width: 100%;
height: auto;
max-height: 16rem;
}
.bootstrap-action-slot {
display: grid;
margin: 1rem 0;
}
.bootstrap-action-slot > * {
grid-area: 1 / 1;
}
.bootstrap-action-status,
#bootstrap-enter.ui-button,
.bootstrap-enter {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 4rem;
box-sizing: border-box;
margin: 0;
padding: .8rem 1.25rem;
border: 0;
border-radius: 999px;
font: inherit;
font-size: clamp(1.05rem, 4.4vw, 1.28rem);
font-weight: 750;
letter-spacing: .005em;
}
.bootstrap-action-status {
gap: .75rem;
color: #eff8f0;
background: linear-gradient(135deg, #387347, var(--bootstrap-accent));
box-shadow: 0 .45rem 1.1rem rgb(40 100 56 / 18%);
opacity: 1;
transform: translateY(0);
transition: opacity var(--bootstrap-fade-duration) ease, transform var(--bootstrap-fade-duration) ease;
}
.bootstrap-action-status.is-leaving {
opacity: 0;
transform: translateY(-.2rem);
pointer-events: none;
}
.bootstrap-loading-dot {
flex: 0 0 auto;
width: .65rem;
height: .65rem;
border-radius: 50%;
background: #d9f0de;
box-shadow: 0 0 0 0 rgb(217 240 222 / 50%);
animation: bootstrap-pulse 1.35s ease-out infinite;
}
.bootstrap-status-text {
opacity: 1;
transition: opacity var(--bootstrap-fade-duration) ease;
}
.bootstrap-status-text.is-fading {
opacity: 0;
}
#bootstrap-enter.ui-button,
.bootstrap-enter {
gap: .75rem;
color: #fff;
background: linear-gradient(135deg, #2d743d, var(--bootstrap-accent-strong));
box-shadow: 0 .55rem 1.25rem rgb(25 77 43 / 24%);
cursor: pointer;
opacity: 0;
transform: translateY(.2rem);
transition: opacity var(--bootstrap-fade-duration) ease, transform var(--bootstrap-fade-duration) ease, background-color .15s ease;
}
#bootstrap-enter.is-visible {
opacity: 1;
transform: translateY(0);
}
#bootstrap-enter svg {
width: 1.5rem;
height: 1.5rem;
margin-left: auto;
fill: none;
stroke: currentColor;
stroke-width: 2.5;
stroke-linecap: round;
stroke-linejoin: round;
}
#bootstrap-enter:hover,
#bootstrap-enter:focus-visible {
background: #173f24;
}
.bootstrap-features {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0;
margin: 1rem 0;
padding: .8rem .4rem;
border: 1px solid #e2ebe2;
border-radius: 1.35rem;
background: linear-gradient(135deg, #f4f9f4, #fbfcf8);
}
.bootstrap-features > div {
display: grid;
place-items: center;
gap: .4rem;
min-width: 0;
padding: .3rem .45rem;
text-align: center;
}
.bootstrap-features > div + div {
border-left: 1px solid #dce6dc;
}
.bootstrap-features svg {
width: 2rem;
height: 2rem;
fill: none;
stroke: var(--bootstrap-accent);
stroke-width: 2.2;
stroke-linecap: round;
stroke-linejoin: round;
}
.bootstrap-features > div:nth-child(2) svg {
stroke: #7351b4;
}
.bootstrap-features > div:nth-child(3) svg {
fill: #e96c2a;
stroke: #e96c2a;
}
.bootstrap-features span {
color: #24402c;
font-size: clamp(.78rem, 3vw, .95rem);
font-weight: 650;
line-height: 1.2;
}
#bootstrap-screen .privacy-summary {
display: grid;
gap: .75rem;
margin: 1rem 0;
gap: .65rem;
margin: .85rem 0 0;
padding: .85rem;
border: 1px solid var(--bootstrap-border);
border-radius: 1rem;
border-radius: 1.15rem;
background: #f9fbf6;
}
#bootstrap-screen .privacy-summary > p {
margin: 0;
color: var(--bootstrap-muted);
font-size: .92rem;
line-height: 1.4;
}
#bootstrap-screen .privacy-summary > .switch-row.switch-disabled {
display: none;
}
#bootstrap-screen progress {
width: 100%;
height: .9rem;
overflow: hidden;
border: 0;
border-radius: 999px;
background: #dce7d8;
}
#bootstrap-screen progress::-webkit-progress-bar {
border-radius: 999px;
background: #dce7d8;
}
#bootstrap-screen progress::-webkit-progress-value {
border-radius: 999px;
background: var(--bootstrap-accent);
}
#bootstrap-screen progress::-moz-progress-bar {
border-radius: 999px;
background: var(--bootstrap-accent);
}
#bootstrap-steps {
display: grid;
gap: .35rem;
margin: 1rem 0;
padding-left: 1.4rem;
.storage-cleanup-status {
min-height: 1.2rem;
margin: .75rem 0 0;
color: var(--bootstrap-muted);
font-size: .86rem;
text-align: center;
}
#bootstrap-steps li[data-state="done"] {
color: var(--bootstrap-accent-strong);
}
#bootstrap-steps li[data-state="active"] {
font-weight: 700;
color: var(--bootstrap-text);
}
#bootstrap-error {
.bootstrap-error {
margin: .65rem 0 0;
padding: .75rem .9rem;
border-radius: .9rem;
color: var(--bootstrap-error);
background: #fff0f0;
font-weight: 700;
}
#bootstrap-retry,
#bootstrap-enter {
.bootstrap-retry,
#bootstrap-retry.ui-button {
width: 100%;
min-height: 3rem;
margin-top: .75rem;
border: 0;
box-sizing: border-box;
margin-top: .65rem;
border: 1px solid #d8abab;
border-radius: 999px;
color: #ffffff;
background: var(--bootstrap-accent);
color: #861d1d;
background: #fff;
font: inherit;
font-weight: 700;
cursor: pointer;
}
#bootstrap-retry:hover,
#bootstrap-enter:hover,
#bootstrap-retry:focus-visible,
#bootstrap-enter:focus-visible {
background: var(--bootstrap-accent-strong);
.bootstrap-ready,
.bootstrap-state-model {
display: none !important;
}
.bootstrap-ready {
font-weight: 700;
color: var(--bootstrap-accent-strong);
@keyframes bootstrap-pulse {
0% { box-shadow: 0 0 0 0 rgb(217 240 222 / 55%); }
65%, 100% { box-shadow: 0 0 0 .7rem rgb(217 240 222 / 0%); }
}
.storage-cleanup-status {
min-height: 1.25rem;
font-size: .95rem;
@media (max-width: 34rem) {
#bootstrap-screen {
padding-right: max(.75rem, env(safe-area-inset-right));
padding-left: max(.75rem, env(safe-area-inset-left));
}
#bootstrap-screen > section {
padding: 1rem;
}
.bootstrap-brand {
gap: .75rem;
}
.bootstrap-hero svg {
max-height: 12.5rem;
}
.bootstrap-features {
padding: .65rem .25rem;
}
}
@media (max-height: 700px) {
.bootstrap-hero svg {
max-height: 10rem;
}
}
@media (prefers-reduced-motion: reduce) {
.bootstrap-loading-dot {
animation: none;
}
.bootstrap-status-text,
.bootstrap-action-status,
#bootstrap-enter.ui-button,
.bootstrap-enter {
transition: none;
}
}
+74 -12
View File
@@ -22,12 +22,77 @@
</nav>
</div>
<div id="bootstrap-screen" aria-live="polite" aria-busy="true">
<section>
<span aria-hidden="true">🧭</span>
<h1>Wegwichtel</h1>
<p>Der Wegwichtel bereitet Routen und Medien vor.</p>
<section class="bootstrap-card">
<header class="bootstrap-brand">
<svg class="bootstrap-logo" viewBox="0 0 96 96" aria-hidden="true" focusable="false">
<circle cx="48" cy="48" r="39" fill="#fff7d7" stroke="#f4b400" stroke-width="6"/>
<path d="M48 15 57 39 81 48 57 57 48 81 39 57 15 48 39 39Z" fill="#d9ddd8"/>
<path d="m48 48 24-23-13 31Z" fill="#e53935"/>
<path d="m48 48-24 23 13-31Z" fill="#2d9cdb"/>
<circle cx="48" cy="48" r="6" fill="#f8fbf7" stroke="#315d3a" stroke-width="3"/>
<circle cx="80" cy="16" r="8" fill="#fff" stroke="#f4b400" stroke-width="5"/>
</svg>
<div>
<h1>Wegwichtel</h1>
<p>Der Wegwichtel bereitet Routen und Medien vor.</p>
</div>
</header>
<div class="bootstrap-hero" aria-hidden="true">
<svg viewBox="0 0 720 300" preserveAspectRatio="xMidYMid slice" focusable="false">
<rect width="720" height="300" rx="32" fill="#eef7f1"/>
<circle cx="255" cy="68" r="27" fill="#ffd66b"/>
<path d="M0 150C92 84 172 113 250 156c81 44 139-14 228-36 82-20 149 2 242 62v118H0Z" fill="#a9d4ca"/>
<path d="M0 191c111-67 217-16 306 25 89 40 179-50 283-49 53 0 92 15 131 43v90H0Z" fill="#74aa62"/>
<path d="M0 224c93-51 194-19 283 17 85 34 189-35 279-31 60 3 110 18 158 51v39H0Z" fill="#5e914e"/>
<g fill="#477d49">
<circle cx="75" cy="200" r="34"/><circle cx="118" cy="210" r="26"/><circle cx="610" cy="210" r="31"/><circle cx="654" cy="218" r="25"/>
</g>
<g fill="#607d8b" opacity=".9">
<path d="M305 174h28v36h-28zM339 161h35v49h-35zM381 180h34v30h-34z"/>
<path d="m350 160 8-24 8 24Z"/>
</g>
<path d="M250 300c34-52 88-69 137-74 53-5 113 5 153 22-65 9-127 29-155 52Z" fill="#f6dfb7"/>
<g transform="translate(538 117)">
<rect x="36" y="24" width="13" height="121" rx="6" fill="#7b5231"/>
<path d="M0 17h105l25 21-25 21H0Z" fill="#f5a332"/>
<path d="M14 65h91l22 19-22 19H14Z" fill="#4f914b"/>
</g>
</svg>
</div>
<div class="bootstrap-action-slot">
<output id="bootstrap-action-status" class="bootstrap-action-status" aria-live="polite">
<span class="bootstrap-loading-dot" aria-hidden="true"></span>
<span id="bootstrap-status-text" class="bootstrap-status-text">Wegwichtel wird vorbereitet …</span>
</output>
<button id="bootstrap-enter" class="bootstrap-enter" type="button" hidden>
<span>Los geht’s</span>
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m9 5 7 7-7 7"/></svg>
</button>
</div>
<p id="bootstrap-error" class="bootstrap-error" hidden></p>
<button id="bootstrap-retry" class="bootstrap-retry" type="button" hidden>Erneut versuchen</button>
<p id="bootstrap-ready" class="bootstrap-ready" hidden>Alles ist bereit.</p>
<div class="bootstrap-features" aria-label="Wegwichtel Funktionen">
<div>
<svg viewBox="0 0 32 32" aria-hidden="true" focusable="false"><path d="m3 7 8-3 10 4 8-3v20l-8 3-10-4-8 3Zm8-3v20m10-16v20"/></svg>
<span>Routen<br>entdecken</span>
</div>
<div>
<svg viewBox="0 0 32 32" aria-hidden="true" focusable="false"><path d="M7 18v-4a9 9 0 0 1 18 0v4M7 18H4v7h5v-7Zm18 0h3v7h-5v-7Z"/></svg>
<span>Spannende<br>Geschichten</span>
</div>
<div>
<svg viewBox="0 0 32 32" aria-hidden="true" focusable="false"><path d="m16 3 3.9 8 8.8 1.3-6.4 6.2 1.5 8.8-7.8-4.1-7.8 4.1 1.5-8.8-6.4-6.2 8.8-1.3Z"/></svg>
<span>Kindgerechte<br>Erlebnisse</span>
</div>
</div>
<div class="privacy-summary">
<p><strong>Cookies:</strong> Diese Anwendung setzt derzeit keine Cookies und verwendet weder Werbung noch Tracking.</p>
<p><strong>Datenschutzfreundlich:</strong> Keine Werbung, kein Tracking und derzeit keine Cookies.</p>
<label class="switch-row switch-disabled">
<span>
<strong>Optionale Cookies</strong>
@@ -39,15 +104,16 @@
<label class="switch-row">
<span>
<strong>Streckenfortschritt lokal speichern</strong>
<small>Speichert ausgewählte Routen, besuchte und angesehene Stationen sowie abgespielte Audiodateien ausschließlich in diesem Browser.</small>
<small>Speichert ausgewählte Routen und besuchte Stationen ausschließlich in diesem Browser.</small>
</span>
<input class="progress-storage-toggle" type="checkbox" role="switch" aria-checked="false">
<span class="switch-track" aria-hidden="true"></span>
</label>
</div>
<p id="storage-cleanup-status" class="storage-cleanup-status" hidden></p>
<progress id="bootstrap-progress" max="6" value="0"></progress>
<ol id="bootstrap-steps">
<progress id="bootstrap-progress" class="bootstrap-state-model" max="6" value="0" hidden></progress>
<ol id="bootstrap-steps" class="bootstrap-state-model" hidden>
<li data-step="jquery">jQuery laden</li>
<li data-step="jquery-ui">jQuery UI laden</li>
<li data-step="modules">Anwendungsmodule laden</li>
@@ -55,10 +121,6 @@
<li data-step="routes">Routen laden</li>
<li data-step="location">Standort ermitteln</li>
</ol>
<p id="bootstrap-ready" class="bootstrap-ready" hidden>Alles ist bereit. Tippe oder klicke auf diese Startseite, um die Anwendung zu öffnen.</p>
<p id="bootstrap-error" hidden></p>
<button id="bootstrap-retry" type="button" hidden>Erneut versuchen</button>
<button id="bootstrap-enter" type="button" hidden>Anwendung öffnen</button>
</section>
</div>
<main id="app-shell" hidden>
+61 -1
View File
@@ -3,6 +3,14 @@
const versions = Object.freeze({ app: '0.12.14', jquery: '4.0.0', jqueryUi: '1.14.2' });
const steps = ['jquery', 'jquery-ui', 'modules', 'server', 'routes', 'location'];
const stepLabels = Object.freeze({
jquery: 'jQuery laden …',
'jquery-ui': 'jQuery UI laden …',
modules: 'Anwendungsmodule laden …',
server: 'Server prüfen …',
routes: 'Routen laden …',
location: 'Standort ermitteln …'
});
const screen = document.getElementById('bootstrap-screen');
const progress = document.getElementById('bootstrap-progress');
const errorBox = document.getElementById('bootstrap-error');
@@ -10,16 +18,54 @@
const enter = document.getElementById('bootstrap-enter');
const readyText = document.getElementById('bootstrap-ready');
const cleanupStatus = document.getElementById('storage-cleanup-status');
const actionStatus = document.getElementById('bootstrap-action-status');
const statusText = document.getElementById('bootstrap-status-text');
let ready = false;
let statusTransition = 0;
const versioned = path => `${path}${path.includes('?') ? '&' : '?'}v=${encodeURIComponent(versions.app)}`;
function setStatusText(text, immediate = false) {
if (!statusText) return;
statusTransition += 1;
const transition = statusTransition;
if (immediate) {
statusText.textContent = text;
statusText.classList.remove('is-fading');
return;
}
statusText.classList.add('is-fading');
globalThis.setTimeout(() => {
if (transition !== statusTransition) return;
statusText.textContent = text;
statusText.classList.remove('is-fading');
}, 180);
}
function nextPendingStep() {
return steps.find(step => !document.querySelector(`[data-step="${step}"]`)?.classList.contains('done'));
}
function announceStep(step) {
if (stepLabels[step]) setStatusText(stepLabels[step]);
}
function mark(step, state) {
const item = document.querySelector(`[data-step="${step}"]`);
if (!item) return;
item.classList.remove('done', 'failed');
item.classList.add(state);
item.dataset.state = state;
progress.value = document.querySelectorAll('#bootstrap-steps .done').length;
if (state === 'failed') {
setStatusText(`Fehler bei: ${stepLabels[step] || step}`);
return;
}
if (state === 'done') {
const next = nextPendingStep();
if (next) announceStep(next);
}
}
function loadScript(src, test) {
@@ -78,7 +124,14 @@
screen.setAttribute('aria-busy', 'false');
screen.classList.add('ready');
readyText.hidden = false;
setStatusText('Vorbereitung abgeschlossen.');
actionStatus?.classList.add('is-leaving');
enter.hidden = false;
globalThis.requestAnimationFrame(() => enter.classList.add('is-visible'));
globalThis.setTimeout(() => {
if (actionStatus) actionStatus.hidden = true;
}, 220);
enter.focus();
}
@@ -88,11 +141,18 @@
screen.classList.remove('ready');
readyText.hidden = true;
enter.hidden = true;
enter.classList.remove('is-visible');
errorBox.hidden = true;
retry.hidden = true;
if (actionStatus) {
actionStatus.hidden = false;
actionStatus.classList.remove('is-leaving');
}
try {
setStatusText('Lokalen Speicher prüfen …', true);
await cleanupOldProgress();
announceStep('jquery');
await loadScript(
`vendor/jquery/jquery-${versions.jquery}.min.js`,
@@ -116,7 +176,7 @@
showReadyState();
} catch (error) {
console.error(error);
const current = steps.find(step => !document.querySelector(`[data-step="${step}"]`)?.classList.contains('done'));
const current = nextPendingStep();
if (current) mark(current, 'failed');
errorBox.textContent = error.message;
errorBox.hidden = false;