Workers P2: Auswahl aus der Liste holt die Karte ins Bild und setzt den Fokus (Handy und Tablet)
Endprüfung P2. Unter 1100 px steht die Karte über der Liste mit 19 Einträgen, aus der man wählt (die Liste ist dort die Alternative zur Leinwand). Ein Klick in der Liste änderte nichts Sichtbares: die Karte öffnete sich oberhalb des Fensters, Screenreader hörten nichts, und „Karte schließen“ ließ den Fokus ins Leere fallen. Bei neuer Auswahl nimmt jetzt der Name in der Karte den Fokus (tabIndex -1), und liegt der Kartenanfang außerhalb des Fensters, wird die Karte an den oberen Rand geholt (scroll-margin-top: unter der festen Leiste der Handy-Ansicht 64 px). „Karte schließen“ gibt den Fokus an den Listeneintrag des Workers zurück. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9ebc562c83
commit
84fb37a98e
5 files changed
+63
-5
No files matched your search
@@ -17,6 +17,8 @@ const CHIP = {
|
||||
|
||||
const PORTRAIT_W = 72
|
||||
const PORTRAIT_H = 96
|
||||
// Beginnt die Karte tiefer als so viele Pixel über der Unterkante des Fensters, wird sie nach oben geholt
|
||||
const MIN_VISIBLE = 120
|
||||
|
||||
const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v)
|
||||
|
||||
@@ -52,6 +54,23 @@ function TraceText({ link, children }) {
|
||||
// Karte eines Workers: Porträt, Rolle, Modell, Status, Tageswerte und die letzten Aktionen.
|
||||
// worker = Zustandseintrag (status, task, counters, connected), trace = Antwort von api.trace (oder null).
|
||||
export default function WorkerCard({ workerId, worker, trace, loading, error, onClose }) {
|
||||
const sectionRef = useRef(null)
|
||||
const headingRef = useRef(null)
|
||||
|
||||
// Neue Auswahl: Der Name nimmt den Fokus (Screenreader sagen die Karte an), und liegt der Kartenanfang außerhalb des Fensters,
|
||||
// wird die Karte an den oberen Rand geholt. Auf Handy und Tablet steht die Karte über der langen Liste, aus der man wählt.
|
||||
useEffect(() => {
|
||||
if (!workerId) return
|
||||
const heading = headingRef.current
|
||||
if (heading && typeof heading.focus === 'function') heading.focus({ preventScroll: true })
|
||||
const section = sectionRef.current
|
||||
if (!section || typeof section.scrollIntoView !== 'function') return
|
||||
const top = section.getBoundingClientRect().top
|
||||
const viewport = window.innerHeight || document.documentElement.clientHeight
|
||||
const margin = parseFloat(window.getComputedStyle(section).scrollMarginTop) || 0
|
||||
if (top < margin || top > viewport - MIN_VISIBLE) section.scrollIntoView({ block: 'start' })
|
||||
}, [workerId])
|
||||
|
||||
if (!workerId) {
|
||||
return (
|
||||
<div className="wk-card">
|
||||
@@ -74,10 +93,10 @@ export default function WorkerCard({ workerId, worker, trace, loading, error, on
|
||||
const rows = traceRows(trace && trace.events, Date.now())
|
||||
|
||||
return (
|
||||
<section className="wk-card" aria-label="Worker-Karte">
|
||||
<section className="wk-card" aria-label="Worker-Karte" ref={sectionRef}>
|
||||
{onClose && <button type="button" className="btn btn-quiet" onClick={onClose} aria-label="Karte schließen">×</button>}
|
||||
<Portrait hat={hat} grey={status === 'grau'} />
|
||||
<h2 className="wk-name">{name}</h2>
|
||||
<h2 className="wk-name" ref={headingRef} tabIndex={-1}>{name}</h2>
|
||||
{roleText && <p className="wk-role">{roleText}</p>}
|
||||
<p>
|
||||
{modelId && <span className={`wk-chip ${CHIP[hat]}`}>{modelId}</span>}
|
||||
|
||||
@@ -28,6 +28,7 @@ export default function WorkersBoard({ api, isAdmin, stageRef }) {
|
||||
const ownRef = useRef(null)
|
||||
const ref = stageRef ?? ownRef
|
||||
const aliveRef = useRef(true)
|
||||
const sideRef = useRef(null)
|
||||
const [workers, setWorkers] = useState({})
|
||||
const [selectedId, setSelectedId] = useState(null)
|
||||
const [settingsOpen, setSettingsOpen] = useState(false)
|
||||
@@ -68,6 +69,19 @@ export default function WorkersBoard({ api, isAdmin, stageRef }) {
|
||||
const paused = Boolean(state.paused)
|
||||
const worker = selectedId && Object.hasOwn(workers, selectedId) ? workers[selectedId] : undefined
|
||||
|
||||
// Karte schließen: der Fokus geht zurück auf den Listeneintrag des Workers (sonst fiele er mit dem Knopf „ד ins Leere)
|
||||
const closeCard = () => {
|
||||
const id = selectedId
|
||||
setSelectedId(null)
|
||||
if (!id || !sideRef.current) return
|
||||
for (const button of sideRef.current.querySelectorAll('[data-wk-worker]')) {
|
||||
if (button.getAttribute('data-wk-worker') === id) {
|
||||
button.focus()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pause für alle Mitarbeiter: der Dienst bekommt den Zielwert, danach wird der Zustand sofort neu geholt
|
||||
const togglePause = async () => {
|
||||
if (pausing) return
|
||||
@@ -103,14 +117,14 @@ export default function WorkersBoard({ api, isAdmin, stageRef }) {
|
||||
|
||||
<div className="wk-main">
|
||||
<WorkersStage ref={ref} state={state} selectedId={selectedId} onSelect={setSelectedId} />
|
||||
<div className="wk-side">
|
||||
<div className="wk-side" ref={sideRef}>
|
||||
<WorkerCard
|
||||
workerId={selectedId}
|
||||
worker={worker}
|
||||
trace={trace}
|
||||
loading={traceLoading}
|
||||
error={traceError}
|
||||
onClose={() => setSelectedId(null)}
|
||||
onClose={closeCard}
|
||||
/>
|
||||
<WorkerList workers={workers} selectedId={selectedId} onSelect={setSelectedId} />
|
||||
</div>
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
|
||||
/* ---------- Worker-Karte ---------- */
|
||||
|
||||
.wk-card { position: relative; padding-bottom: 20px; }
|
||||
.wk-card { position: relative; padding-bottom: 20px; scroll-margin-top: 12px; }
|
||||
.wk-card > .btn-quiet { position: absolute; top: 0; right: 0; }
|
||||
.wk-card h3 { margin: 20px 0 8px; font-size: 13px; line-height: 20px; font-weight: 600; color: var(--text-2); }
|
||||
.wk-portrait {
|
||||
@@ -180,6 +180,8 @@
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
/* unter der festen Leiste der Handy-Ansicht (52 px) */
|
||||
.wk-card { scroll-margin-top: 64px; }
|
||||
.wk-kpis.stats { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px 24px; }
|
||||
.wk-zoom { gap: 6px; }
|
||||
.wk-zoom .btn { min-width: 36px; min-height: 36px; }
|
||||
|
||||
Reference in new issue
Block a user