Filipe: "integriere die zugangskachel links nach rechts und passe sie
perfekt an damit sie in dem neuen bild rechts perfekt und die
vorgemachte kachel passt."
Das neue Motiv (Spicy Media x DogFather) hat rechts eine leere Tafel mit
Neonrahmen. Die Anmeldekarte sitzt jetzt DARIN -- und zwar wirklich
darin, nicht ungefaehr in der Gegend.
DAS PROBLEM, DAS MAN LEICHT UEBERSIEHT: Das Bild liegt mit
`object-fit: cover` unter der Seite; je nach Fensterform schneidet der
Browser oben/unten oder links/rechts etwas ab. Ein `left: 58%` bezieht
sich aber auf das FENSTER. Auf genau einer Bildschirmgroesse saehe es
richtig aus und ueberall sonst falsch. Deshalb rechnet `.tafel-anker`
dieselbe Cover-Formel noch einmal nach und ist damit deckungsgleich mit
dem Bild -- Prozentwerte darin sind Prozent DES BILDES.
Die vier Zahlen sind GEMESSEN (tools/gate-bauen.mjs), nicht geschaetzt.
Zwei Anlaeufe standen daneben und haben sich selbst verraten:
1. "Suche den leuchtenden Rahmen" fand den Mond, die roten Blueten und
jede Spiegelung -- Ergebnis: die ganze rechte Bildhaelfte. Eine
Messung, die das Offensichtliche zurueckgibt, hat nichts gemessen.
2. "Suche die groesste dunkle Flaeche" fand die Breite richtig, aber
94 % Hoehe: Ueber und unter der Tafel ist die Szene genauso
schwarz.
Richtig ist der dritte Weg: vom Mittelpunkt der Tafel nach aussen
laufen, bis es hell ODER farbig wird -- auf diesem Weg liegt nichts
anderes, denn die Tafel ist leer. Danach eine Plausibilitaetspruefung,
die abbricht statt vier geratene Zahlen auszugeben.
DREI FEHLER IM EIGENEN ENTWURF, alle gemessen statt angesehen:
* Die Karte hing 300 px unter dem Bildschirmrand. Auf `.tafel` liegt
die Einblend-Animation, deren Endbild `transform: none` ist -- mein
`translateY(-50%)` war damit wirkungslos. Zwei Wege, dasselbe
Element zu verschieben, vertragen sich nicht.
* Der Anker lag 3 % neben dem Bild: Das Bild traegt `scale(1.03)` als
Reserve fuer die Parallaxe. Jetzt tragen beide dieselbe Verwandlung,
und die Parallaxe laeuft ueber zwei CSS-Groessen am <body> -- so
wandert die Karte mit, statt dass der Rahmen unter ihr wegrutscht.
* Der Hochkant-Ausschnitt zeigte die LEERE Tafel: ein schwarzes
Rechteck mit ein paar Saeulen. Auf dem Handy liegt die Karte ohnehin
davor; dort gehoert das Logo hin. Jetzt faellt der Schriftzug
SPICY MEDIA in den sichtbaren Streifen -- nachgerechnet, nicht
probiert.
STARTSEITE: das Tresorbild als neuer Hintergrund. Die uebernommene
Abdunklung von 0,62 war zu viel (der Tresor ist von Haus aus dunkel) --
uebrig blieb ein Schemen. Und der "Leseweg" verdunkelte ausgerechnet die
MITTE: Bei den alten Szenen stand dort nichts, beim Tresor steht dort
die Tuer. Beides korrigiert und mit pruef-buehne an echten Bildpunkten
nachgemessen.
DABEI GEFUNDEN, ohne Zusammenhang mit dem Bild: Der abgeschaltete
"Ueberfaellig"-Filter kam auf 4,05:1 statt 4,5:1. Die Deckkraft zu
erhoehen half nichts -- die Pruefung rechnet mit der CSS-FARBE und dem
gemessenen Bildpunkt dahinter, und ein `opacity` am Elternknopf aendert
die Farbe nicht. Die Zahl blieb dreimal exakt gleich; genau das war der
Hinweis. Jetzt ein eigener, hellerer Ton. Der Kommentar daneben sagte
ohnehin, was gewollt ist: Man SOLL sehen, dass es null sind.
Und die vier Regeln von heute stehen in CLAUDE.md.
Co-Authored-By: Claude Opus 5 <[email protected]>
164 lines
6.0 KiB
JavaScript
164 lines
6.0 KiB
JavaScript
/* ===================================================================
|
||
Creator Workspace - Anmeldeseite
|
||
Kein Framework, keine externen Abhängigkeiten.
|
||
=================================================================== */
|
||
(() => {
|
||
'use strict';
|
||
|
||
const $ = (id) => document.getElementById(id);
|
||
const rollen = Array.from(document.querySelectorAll('.rolle'));
|
||
const formular = $('formular');
|
||
const code = $('code');
|
||
const auge = $('auge');
|
||
const fehler = $('fehler');
|
||
const hinweis = $('hinweis');
|
||
const knopf = $('knopf');
|
||
const fuer = $('fuer');
|
||
|
||
const NAMEN = { admin: 'DogFather', manager: 'Manager', scout: 'Scout', creator: 'Creator' };
|
||
const SPEICHER = 'dfw.rolle';
|
||
let gewaehlt = null;
|
||
|
||
/* ---------- Rolle wählen ------------------------------------------ */
|
||
|
||
function waehle(rolle, fokus = false) {
|
||
gewaehlt = rolle;
|
||
rollen.forEach((b) => b.setAttribute('aria-checked', String(b.dataset.rolle === rolle)));
|
||
fuer.textContent = `· ${NAMEN[rolle]}`;
|
||
fuer.hidden = false;
|
||
melde('');
|
||
try { localStorage.setItem(SPEICHER, rolle); } catch { /* Privater Modus */ }
|
||
if (fokus) code.focus();
|
||
}
|
||
|
||
rollen.forEach((b) => {
|
||
b.addEventListener('click', () => waehle(b.dataset.rolle, true));
|
||
// Pfeiltasten wie bei echten Radio-Feldern
|
||
b.addEventListener('keydown', (e) => {
|
||
const i = rollen.indexOf(b);
|
||
let ziel = null;
|
||
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') ziel = rollen[(i + 1) % rollen.length];
|
||
if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') ziel = rollen[(i - 1 + rollen.length) % rollen.length];
|
||
if (!ziel) return;
|
||
e.preventDefault();
|
||
ziel.focus();
|
||
waehle(ziel.dataset.rolle);
|
||
});
|
||
});
|
||
|
||
// Zuletzt genutzte Rolle vorauswählen - spart bei täglicher Nutzung einen Klick.
|
||
try {
|
||
const zuletzt = localStorage.getItem(SPEICHER);
|
||
if (zuletzt && NAMEN[zuletzt]) waehle(zuletzt);
|
||
} catch { /* egal */ }
|
||
|
||
/* ---------- Code anzeigen / verbergen ------------------------------ */
|
||
|
||
auge.addEventListener('click', () => {
|
||
const sichtbar = code.type === 'text';
|
||
code.type = sichtbar ? 'password' : 'text';
|
||
auge.setAttribute('aria-pressed', String(!sichtbar));
|
||
auge.setAttribute('aria-label', sichtbar ? 'Code anzeigen' : 'Code verbergen');
|
||
code.focus();
|
||
});
|
||
|
||
/* ---------- Feststelltaste ----------------------------------------- */
|
||
|
||
const pruefeFeststell = (e) => {
|
||
if (typeof e.getModifierState !== 'function') return;
|
||
hinweis.hidden = !e.getModifierState('CapsLock');
|
||
};
|
||
code.addEventListener('keyup', pruefeFeststell);
|
||
code.addEventListener('keydown', pruefeFeststell);
|
||
code.addEventListener('blur', () => { hinweis.hidden = true; });
|
||
|
||
/* ---------- Rückmeldung -------------------------------------------- */
|
||
|
||
function melde(text) { fehler.textContent = text; }
|
||
|
||
function laden(an) {
|
||
knopf.disabled = an;
|
||
knopf.querySelector('.knopf__text').textContent = an ? 'Prüfe …' : 'Anmelden';
|
||
}
|
||
|
||
/* ---------- Absenden ------------------------------------------------ */
|
||
|
||
formular.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const wert = code.value.trim();
|
||
|
||
if (!gewaehlt) { melde('Bitte zuerst auswählen, womit du dich anmeldest.'); rollen[0].focus(); return; }
|
||
if (!wert) { melde('Bitte den Zugangscode eingeben.'); code.focus(); return; }
|
||
|
||
melde('');
|
||
laden(true);
|
||
|
||
try {
|
||
const antwort = await fetch('/workspace/api/anmelden', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ rolle: gewaehlt, code: wert }),
|
||
credentials: 'same-origin',
|
||
});
|
||
|
||
if (antwort.ok) {
|
||
const ziel = (await antwort.json().catch(() => ({}))).weiter || '/workspace';
|
||
document.body.classList.add('oeffnet');
|
||
setTimeout(() => { location.assign(ziel); }, 260);
|
||
return;
|
||
}
|
||
|
||
// 404 heißt: Es gibt noch gar keine Gegenstelle. Das ist derzeit der
|
||
// Normalfall - die Seite steht schon, der Login-Server wird noch gebaut.
|
||
// Eine ehrliche Meldung ist hier besser als "Code stimmt nicht".
|
||
if (antwort.status === 404) {
|
||
melde('Der Zugang wird gerade eingerichtet – Anmelden ist noch nicht möglich.');
|
||
return;
|
||
}
|
||
|
||
// Bewusst keine Auskunft darüber, ob Rolle oder Code falsch war.
|
||
melde(antwort.status === 429
|
||
? 'Zu viele Versuche. Bitte kurz warten.'
|
||
: 'Zugangscode stimmt nicht.');
|
||
code.select();
|
||
} catch {
|
||
melde('Keine Verbindung zum Server. Bitte erneut versuchen.');
|
||
} finally {
|
||
laden(false);
|
||
}
|
||
});
|
||
|
||
/* ---------- Sehr dezente Parallaxe ---------------------------------- */
|
||
|
||
const ruhig = matchMedia('(prefers-reduced-motion: reduce)');
|
||
const bild = document.querySelector('.buehne__bild');
|
||
const grob = matchMedia('(pointer: coarse)');
|
||
|
||
if (bild && !ruhig.matches && !grob.matches) {
|
||
let ziel = { x: 0, y: 0 }, ist = { x: 0, y: 0 }, laeuft = false;
|
||
|
||
addEventListener('pointermove', (e) => {
|
||
ziel.x = (e.clientX / innerWidth - 0.5) * -10; // max. 10 px
|
||
ziel.y = (e.clientY / innerHeight - 0.5) * -6;
|
||
if (!laeuft) { laeuft = true; requestAnimationFrame(schritt); }
|
||
}, { passive: true });
|
||
|
||
function schritt() {
|
||
ist.x += (ziel.x - ist.x) * 0.06;
|
||
ist.y += (ziel.y - ist.y) * 0.06;
|
||
/* NICHT MEHR DIREKT AUF DAS BILD (06.09.2026), sondern als zwei
|
||
Groessen am <body>. Der Grund: Seit die Anmeldekarte in der
|
||
GEMALTEN Tafel des Motivs sitzt, muss sie jede Bewegung des
|
||
Bildes mitmachen -- sonst rutscht der Neonrahmen unter ihr
|
||
weg. Gemessen waren es bis zu 10 px, und das sieht man.
|
||
Ueber die Variablen bewegen sich Bild UND Anker mit
|
||
derselben Zahl; die Karte bleibt geklebt. */
|
||
document.body.style.setProperty('--parallax-x', ist.x.toFixed(2) + 'px');
|
||
document.body.style.setProperty('--parallax-y', ist.y.toFixed(2) + 'px');
|
||
if (Math.abs(ziel.x - ist.x) > 0.05 || Math.abs(ziel.y - ist.y) > 0.05) {
|
||
requestAnimationFrame(schritt);
|
||
} else { laeuft = false; }
|
||
}
|
||
}
|
||
})();
|