From 5e6698f5e0f57ca65076991e126502eb7f7eeecb Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 09:47:57 +0000 Subject: [PATCH] =?UTF-8?q?Hustler-Pixelwelt=20T13:=20Korrekturen=20Hochha?= =?UTF-8?q?us=20(Fokus,=20Tastatur,=20weniger=20Bewegung,=20Handy-Tippfl?= =?UTF-8?q?=C3=A4chen,=20Schnee=20per=20transform,=20Pause=20=C3=BCberall,?= =?UTF-8?q?=20Seite=20ohne=20Leerlauf-Blitz)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- src/components/workers/hochhaus/Hochhaus.jsx | 53 ++- .../workers/hochhaus/HochhausWelt.jsx | 42 ++- .../workers/hochhaus/HustlerDialog.jsx | 23 +- src/components/workers/hochhaus/useKamera.js | 53 ++- src/lib/hustlerSicht.js | 63 +++- src/pages/WorkersPage.jsx | 23 +- src/styles/hustler/hustler-detail.css | 10 +- src/styles/hustler/hustler-ui.css | 14 +- src/styles/hustler/hustler-welt.css | 42 ++- tests/unit/hustlerDialog.test.mjs | 88 +++++ tests/unit/hustlerHochhausWelt.test.mjs | 306 +++++++++++++++++- tests/unit/hustlerSeite.test.mjs | 181 ++++++++++- tests/unit/hustlerSicht.test.mjs | 143 +++++++- tests/unit/workersRoute.test.mjs | 5 +- 14 files changed, 994 insertions(+), 52 deletions(-) diff --git a/src/components/workers/hochhaus/Hochhaus.jsx b/src/components/workers/hochhaus/Hochhaus.jsx index 82b9ae2..0f3bf01 100644 --- a/src/components/workers/hochhaus/Hochhaus.jsx +++ b/src/components/workers/hochhaus/Hochhaus.jsx @@ -1,21 +1,43 @@ -import { useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import HochhausKopf from './HochhausKopf' import HochhausWelt from './HochhausWelt' import HustlerDialog from './HustlerDialog' import RegieEbene from './RegieEbene' +// Von welchem Bildschirm man zurückkommt -> Etage, über die man gegangen war (Freigabe-Pult über die Etage „freigabe“, Keller über „keller“) +const ETAGE_NACH_TAB = Object.freeze({ freigabe: 'freigabe', botleads: 'keller' }) + +// Überschrift der Seite, nur für Screenreader (Pult und Keller haben ihre eigene sichtbare Überschrift) +function Ueberschrift() { + return

Hustler – Hochhaus

+} + // Bildschirm „Hochhaus“: Spielwelt mit Regie-Ebene (T5, T8), Kopfzeile (T6) und Detailansicht eines Hustlers (T7). Zustände wie bisher im WorkersBoard. // Reine Darstellung ohne Auth-Bezug: isAdmin, der Feed (feed = { state, error, loaded, refresh }), die Hustler-Tabelle (workers), die Regie-Anbindung -// (regieRef, die Seite ruft regieRef.current.push(events) bei neuen Ereignissen) und der Bildschirmwechsel (onOpen(tab)) kommen von der Seite. +// (regieRef, die Seite ruft regieRef.current.push(events) bei neuen Ereignissen), der Bildschirmwechsel (onOpen(tab)) und vonTab (der Bildschirm, von dem man +// zurückkommt: 'freigabe' | 'botleads' | null) kommen von der Seite. Beim Zurückkommen bekommt die Etage, über die man gegangen war, den Fokus (M2). +// Die Kopfzeile steht im DOM vor der Welt, damit die Tab-Reihenfolge oben beginnt (M7); über der Welt liegt sie per z-index. // Kein CSS-Import und keine Bilder: die Stile hängt WorkersPage ein (Regeln dieser Datei in styles/hustler/hustler-ui.css). -export default function Hochhaus({ api, isAdmin, feed, workers, regieRef, onOpen }) { +export default function Hochhaus({ api, isAdmin, feed, workers, regieRef, onOpen, vonTab = null }) { + const wurzelRef = useRef(null) const weltRef = useRef(null) const [auswahl, setAuswahl] = useState(null) // workerId der offenen Detailansicht (null = keine) const ready = feed.loaded && Boolean(feed.state) + + // Rückweg aus Pult oder Keller: Fokus auf die Etage, über die man ging (erst wenn die Welt steht; die Kamera holt sie bei Tastaturfokus ins Bild) + useEffect(() => { + if (!ready || !vonTab || !Object.hasOwn(ETAGE_NACH_TAB, vonTab)) return + const wurzel = wurzelRef.current + if (!wurzel || typeof wurzel.querySelector !== 'function') return + const etage = wurzel.querySelector(`[data-hk-etage="${ETAGE_NACH_TAB[vonTab]}"]`) + if (etage && typeof etage.focus === 'function') etage.focus({ preventScroll: true }) + }, [ready, vonTab]) + // Dienst noch nie erreicht: Pixel-Leerzustand mit Fehlertext und „Neu laden“ if (!ready && feed.error) { return ( -
+
+
Der Hustler-Dienst ist noch nicht erreichbar

{feed.error}

@@ -26,7 +48,8 @@ export default function Hochhaus({ api, isAdmin, feed, workers, regieRef, onOpen } if (!ready) { return ( -
+
+
Wird geladen …
) @@ -35,7 +58,16 @@ export default function Hochhaus({ api, isAdmin, feed, workers, regieRef, onOpen // Die Regie-Ebene liegt in der skalierten Bühne (Weltkoordinaten); ihr Handle ist regieRef der Seite, die neue Ereignisse hineinreicht. // Solange die Detailansicht offen ist, ruhen Rad, Ziehen und Tasten der Kamera (gesperrt). return ( -
+
+ + { if (weltRef.current) weltRef.current.ganz() }} + /> - { if (weltRef.current) weltRef.current.ganz() }} - /> {auswahl !== null && ( setAuswahl(null)} onWechsel={setAuswahl} /> diff --git a/src/components/workers/hochhaus/HochhausWelt.jsx b/src/components/workers/hochhaus/HochhausWelt.jsx index 48583b6..984e6af 100644 --- a/src/components/workers/hochhaus/HochhausWelt.jsx +++ b/src/components/workers/hochhaus/HochhausWelt.jsx @@ -3,9 +3,12 @@ // Raumschilder, Bildschirme, Radar, Briefkasten-Zähler, Tresor-/Keller-Text, den Etagen (Knöpfe, Freigabe und Keller sind Links) und den 19 Hustlern; davor der Schnee. // Alles hängt am Dienst-Zustand: Figur und Zusätze nach Status (figurZustand), graue Raum-Ebene samt Papierschild nur für nicht angeschlossene Such-Räume. // Die Kamera (Rad, Ziehen, Pinch, Tasten) steckt in useKamera. `children` (die Regie-Ebene) liegt in der Bühne über den Figuren, in Weltkoordinaten. +// Bedienung: Tastaturfokus (:focus-visible) auf Etage oder Figur außerhalb des Bildes holt sie per Kamera herein (beiFokus, kein Schwenk bei Mausklick); das Sichtfenster ist +// per tabindex -1 fokussierbar (ein Klick in die Welt gibt ihm den Fokus, damit + - 0 Esc wirken); die Tippflächen der Figuren wachsen mit dem Zoom (--hk-tipp-*, tippRand); +// bei offener Detailansicht (gesperrt) trägt das Sichtfenster hk-verdeckt (Schnee aus). // Kein CSS-Import (die Stile hängt WorkersPage ein: styles/hustler/hochhaus.css, hustler-ui.css, hustler-welt.css), keine Bilder, kein innerHTML. import { forwardRef, useImperativeHandle } from 'react' -import { anzeigeName, figurZustand, schilder } from '../../../lib/hustlerSicht.js' +import { anzeigeName, figurZustand, schilder, tippRand } from '../../../lib/hustlerSicht.js' import { AUFZUG, ETAGEN, ETAGEN_SPALTE, HAUS_BILD, HUSTLER, RAEUME, SCHILDER } from '../../../lib/hustlerWelt.js' import { FLOOR_TITLES, ROLES, ROOM_TITLES } from '../../../lib/workersRoster.js' import useKamera from './useKamera.js' @@ -20,6 +23,11 @@ const HAUS_RECHTECK = Object.freeze({ left: HAUS_BILD.x, top: HAUS_BILD.y, width const PX = (n) => `${n}px` +// Elemente, die Fokus per Tastatur bekommen können und beim Fokus ins Bild geholt werden (M1) +const FOKUS_ZIEL = '.hk-etage, .hk-hustler' +// Namensschild einer Figur: so viele Bildschirm-Pixel über dem Knopf sollen beim Heranholen mit sichtbar sein +const SCHILD_HOEHE = 34 + // Feste, je Figur verschiedene Startverschiebung der Animation (deterministisch aus dem Index: 7 und 19 sind teilerfremd, 19 Figuren -> 19 Werte) const versatz = (index, schritt = 7, anzahl = 19) => `-${(((index * schritt) % anzahl) / 10).toFixed(1)}s` @@ -49,6 +57,7 @@ const HochhausWelt = forwardRef(function HochhausWelt({ state, workers, onZeige, const grau = new Set(figuren.filter((x) => x.f.raumGrau).map((x) => x.f.raum)) const { s, x, y } = kam + const tipp = tippRand(s) const etikett = Math.min(18, Math.max(8, 15 / s)).toFixed(2) const namePx = Math.min(14, Math.max(4, 12 / s)).toFixed(2) const buehneStil = { @@ -56,6 +65,10 @@ const HochhausWelt = forwardRef(function HochhausWelt({ state, workers, onZeige, imageRendering: s * geraeteDichte() < 1 ? 'auto' : 'pixelated', '--hk-etikett': `${etikett}px`, '--hk-name': `${namePx}px`, + // Tippfläche der Figuren: wächst mit dem Zoom auf 40 Bildschirm-Pixel (M8, hustler-welt.css) + '--hk-tipp-x': `${tipp.x.toFixed(2)}px`, + '--hk-tipp-oben': `${tipp.oben.toFixed(2)}px`, + '--hk-tipp-unten': `${tipp.unten.toFixed(2)}px`, } const zeige = (id) => () => { @@ -70,10 +83,29 @@ const HochhausWelt = forwardRef(function HochhausWelt({ state, workers, onZeige, if (typeof onOpen === 'function') onOpen(tab) } + // Fokus per Tastatur (:focus-visible) auf Etage oder Figur außerhalb des sichtbaren Bereichs: Kamera holt sie ins Bild (M1). Kein Schwenk bei Mausklick. + const beiFokus = (e) => { + if (gesperrt) return + const knoten = e && e.target + if (!knoten || typeof knoten.closest !== 'function') return + const ziel = knoten.closest(FOKUS_ZIEL) + if (!ziel) return + let tastatur = false + try { tastatur = ziel.matches(':focus-visible') } catch { tastatur = false } + if (!tastatur) return + // Der Browser darf das Sichtfenster nicht selbst verschieben (alte Browser ohne overflow: clip scrollen beim Fokus) + const sicht = e.currentTarget + if (sicht && (sicht.scrollLeft || sicht.scrollTop)) { sicht.scrollLeft = 0; sicht.scrollTop = 0 } + const figur = typeof ziel.matches === 'function' && ziel.matches('.hk-hustler') + kam.zeige({ left: ziel.offsetLeft, top: ziel.offsetTop, width: ziel.offsetWidth, height: ziel.offsetHeight }, figur ? SCHILD_HOEHE : 0) + } + return (