Hustler-Pixelwelt T13: Korrekturen Pult und Keller (Sperre laufender Aktionen, Fokus, Kontrast, Handy-Tippflächen, Pause)
Pult: laufende Anfragen je Eintrags-Id in PultList (laufRef, Brief meldet Beginn/Ende): Brief schließen und denselben Eintrag wieder öffnen sperrt alle Aktionsknöpfe, kein zweites reply/paste/start-build (P1); spätes Ergebnis nennt den Titel (P1b); Original und Entwurf pre-wrap (P3); Überschrift nimmt beim Einhängen den Fokus (M2); „Pult zuklappen“ mit aria-expanded (K4); paused -> hk-steht an der Szene (R3); Brief z-index 9 (R4); Textlinks am Handy 40 px (K7). Keller: Überschrift-Fokus (M2), Reiter-Fokusring innen (M3), Seitenzeile/Punkte/Reiterzahl in Pixelify Sans 13 px (K5), Suchfeld und Akte-Link 40 px am Handy (K7), Ortszeile mit title (W2), paused -> hk-steht (R3), Akte z-index 9 (R4); Arne ohne Eintrag neutral (R5). Tests: Sperre laufender Aktionen, Titel im späten Ergebnis, Fokus, aria-expanded, paused, pre-wrap, z-index, Tippflächen, Reiter-Fokus, Schrift, Kontrast (WCAG) von .hk-zeile-alt/.hk-kl-tag auf Papier und Hover, Arne neutral (alte „Arne schläft ohne Eintrag“-Prüfung ersetzt). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ea1485c27c
commit
0924a8d2ae
13 files changed
+597
-70
No files matched your search
@@ -14,14 +14,15 @@ const browserDocument = () => (typeof document === 'undefined' ? null : document
|
||||
|
||||
// Bildschirm „Keller“ (Hustler-Pixelwelt T10, Spec E12/E13; Dateiname bleibt BotLeadList): die Archiv-Szene mit Arne (keller/KellerSzene), davor Arnes Register-Buch
|
||||
// (keller/RegisterBuch) mit den Betrieben, die nicht angeschrieben werden, samt Grund, und die Akte zu einem Eintrag (keller/Akte). Reine Liste, nichts wird gesendet
|
||||
// oder angerufen. Props: api, bot, onClearBot, kpis (botLeads für die Zahl am Schild), workers (Hustler-Tabelle der Seite: Arnes Status), onBack (◂ Hochhaus).
|
||||
// oder angerufen. Props: api, bot, onClearBot, kpis (botLeads für die Zahl am Schild), workers (Hustler-Tabelle der Seite: Arnes Status), paused (Hustler angehalten:
|
||||
// die Szene steht still, Klasse hk-steht), onBack (◂ Hochhaus). Beim Einhängen nimmt die Überschrift „Bot-Leads“ (tabIndex -1) den Fokus, ohne dass die Seite springt (T13 M2).
|
||||
// Die Liste holt sich ihre Daten bei jeder Änderung von Raum, Suche, Seite oder bot selbst; es gibt kein Polling. Nur die jüngste Anfrage zählt: eine späte
|
||||
// Antwort (auch ein später Fehler) einer älteren Anfrage oder nach dem Abbau wird verworfen. Scheitert eine Anfrage, steht der Satz des Dienstes mit „Erneut versuchen“,
|
||||
// das genau diese Anfrage (Raum, Suche, Seite oder Eintrag) noch einmal stellt; Raum und Suche gehen weiterhin.
|
||||
// bot (aus ?bot= der Adresse, Link aus dem Ereignisstrom): zeigt genau diesen Eintrag, hervorgehoben, mit „Alle anzeigen“ (onClearBot nimmt bot aus der Adresse).
|
||||
// Alle Texte der Zeilen sind Textknoten; Links entstehen nur aus row.sourceHref (botleadRow prüft die Adresse mit safeHref: nur http und https, keine Zugangsdaten), und zwar
|
||||
// nur in der Akte. Kein CSS-Import (WorkersPage hängt die Stile ein), keine Bilder, kein HTML-Einschub.
|
||||
export default function BotLeadList({ api, bot, onClearBot, kpis, workers, onBack }) {
|
||||
export default function BotLeadList({ api, bot, onClearBot, kpis, workers, paused = false, onBack }) {
|
||||
const botId = typeof bot === 'string' ? bot : ''
|
||||
const single = botId !== ''
|
||||
const [view, setView] = useState(START_VIEW)
|
||||
@@ -32,6 +33,7 @@ export default function BotLeadList({ api, bot, onClearBot, kpis, workers, onBac
|
||||
const [akte, setAkte] = useState(null) // Nr. des Eintrags in der gezeigten Seite, dessen Akte offen ist
|
||||
const timerRef = useRef(null)
|
||||
const seqRef = useRef(0)
|
||||
const h1Ref = useRef(null) // Überschrift „Bot-Leads“: nimmt beim Einhängen den Fokus (Bildschirmwechsel)
|
||||
// Tastaturfokus: Wer „Alle anzeigen“ oder ◂/▸ drückt, verliert den Knopf mit dem Fokus (Einzelansicht und Seitenzeile werden ausgetauscht oder
|
||||
// hängen beim Laden aus); ohne Zutun fiele der Fokus auf <body>, an den Seitenanfang. Gemerkt wird nur, was der Nutzer als Letztes bedient hat.
|
||||
const reiterRef = useRef(null) // aktiver Raum-Reiter: Ziel nach „Alle anzeigen“
|
||||
@@ -72,6 +74,12 @@ export default function BotLeadList({ api, bot, onClearBot, kpis, workers, onBac
|
||||
|
||||
useEffect(() => () => window.clearTimeout(timerRef.current), [])
|
||||
|
||||
// Bildschirmwechsel (M2): Tastatur und Screenreader beginnen in diesem Bildschirm, nicht auf <body> oder dem ausgehängten „Hochhaus“-Knopf des alten
|
||||
useEffect(() => {
|
||||
const h1 = h1Ref.current
|
||||
if (h1 && typeof h1.focus === 'function') h1.focus({ preventScroll: true })
|
||||
}, [])
|
||||
|
||||
const onRetry = () => setAttempt((n) => n + 1)
|
||||
const onClear = () => {
|
||||
clearedRef.current = true
|
||||
@@ -126,7 +134,7 @@ export default function BotLeadList({ api, bot, onClearBot, kpis, workers, onBac
|
||||
|
||||
return (
|
||||
<div className="hk-keller">
|
||||
<KellerSzene workers={workers} />
|
||||
<KellerSzene workers={workers} steht={paused === true} />
|
||||
<div className="hk-kl-vignette" aria-hidden="true" />
|
||||
<div className="hk-kl-dunkel" aria-hidden="true" />
|
||||
<div className="hk-kl-ui">
|
||||
@@ -136,7 +144,7 @@ export default function BotLeadList({ api, bot, onClearBot, kpis, workers, onBac
|
||||
</button>
|
||||
<div className="hk-messing hk-kl-schild">
|
||||
<span className="hk-ic hk-ic-robo" aria-hidden="true" />
|
||||
<h1 className="hk-kl-titel">Bot-Leads</h1>
|
||||
<h1 className="hk-kl-titel" tabIndex={-1} ref={h1Ref}>Bot-Leads</h1>
|
||||
{plakette ? <span className="hk-kl-zahl">{plakette}</span> : null}
|
||||
</div>
|
||||
<button type="button" className="hk-knopf hk-kl-knopf-klein hk-kl-zuklappen" aria-expanded={!zu} aria-label={zu ? 'Register aufklappen' : 'Register zuklappen'} onClick={() => setZu((z) => !z)}>
|
||||
|
||||
@@ -6,7 +6,12 @@
|
||||
// Schließen des Briefs, Filterwechsel und Neuladen), die Meldung nach einem Abschluss samt Fokus, den offenen Brief, das Interessenten-Panel, „Pult zuklappen“.
|
||||
//
|
||||
// Props: api (Dienst-API), me = { id, name } des Angemeldeten, kpis (Kennzahlen aus dem Zustand: pultOpen steuert die Zahl am Reiter „Wartet“, im Kopf und die Boten-
|
||||
// Schleife), onBack() („◂ Hochhaus“). workers (Hustler-Tabelle) reicht die Seite durch, das Pult braucht sie nicht: ein Pult-Eintrag nennt keinen Hustler.
|
||||
// Schleife), paused (Hustler angehalten: die Szene steht still, Klasse hk-steht), onBack() („◂ Hochhaus“). workers (Hustler-Tabelle) reicht die Seite durch, das Pult
|
||||
// braucht sie nicht: ein Pult-Eintrag nennt keinen Hustler.
|
||||
// Laufende Aktionen (T13 P1): welche Einträge gerade eine Anfrage an den Dienst laufen haben, merkt PultList (laufRef, nach Eintrags-Id), nicht der Brief: wer den Brief
|
||||
// während der Anfrage schließt und denselben Eintrag wieder öffnet, findet alle Aktionsknöpfe gesperrt und kann keine zweite Anfrage auslösen (sonst fügte „Antwort
|
||||
// einfügen“ doppelt ein); sobald die Anfrage endet, sind sie frei.
|
||||
// Fokus (T13 M2): beim Einhängen nimmt die Überschrift „Freigabe“ (tabIndex -1) den Fokus, ohne dass die Seite springt.
|
||||
// Kein CSS-Import und keine Bilder (die Stile hängt WorkersPage ein), Fremdtext nur als Textknoten.
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useEmployees } from '../../hooks/useEmployees'
|
||||
@@ -24,7 +29,7 @@ const NO_FIELDS = Object.freeze({}) // Eintrag ohne bearbeitete Felder
|
||||
// Das document des Browsers; unter Node (Tests, Server) gibt es keins, dann bleibt der Fokus unangetastet
|
||||
const browserDocument = () => (typeof document === 'undefined' ? null : document)
|
||||
|
||||
export default function PultList({ api, me, kpis, onBack }) {
|
||||
export default function PultList({ api, me, kpis, paused = false, onBack }) {
|
||||
const [filterId, setFilterId] = useState('offen')
|
||||
const [drafts, setDrafts] = useState({}) // bearbeitete Texte je Eintrag; was fehlt, zeigt den Entwurf des Dienstes
|
||||
const [fields, setFields] = useState({}) // Texte der Felder „Als Mensch antworten“ (reply) und „Antwort einfügen“ (paste) je Eintrag
|
||||
@@ -38,11 +43,21 @@ export default function PultList({ api, me, kpis, onBack }) {
|
||||
const [detailId, setDetailId] = useState(null) // Interessent, dessen Panel offen ist (null = keins)
|
||||
const [zu, setZu] = useState(false) // „Pult zuklappen“: nur die Szene bleibt
|
||||
const listeRef = useRef(null)
|
||||
const h1Ref = useRef(null) // Überschrift „Freigabe“: nimmt beim Einhängen den Fokus (Bildschirmwechsel)
|
||||
const laufRef = useRef(new Set()) // Ids der Einträge, für die gerade eine Anfrage an den Dienst läuft (überlebt das Schließen des Briefs)
|
||||
const [, setLaufTakt] = useState(0) // zählt das Ende einer Anfrage: ein offener Brief dieses Eintrags zeichnet sich neu (laufRef allein löst kein Neuzeichnen aus)
|
||||
const filter = PULT_FILTERS.find((f) => f.id === filterId) ?? PULT_FILTERS[0]
|
||||
const { items, loading, error, refresh } = usePultFeed({ api, statuses: filter.statuses })
|
||||
const { employees } = useEmployees()
|
||||
const nowMs = Date.now()
|
||||
|
||||
// Bildschirmwechsel (M2): die Überschrift nimmt den Fokus, damit Tastatur und Screenreader im neuen Bildschirm beginnen und nicht auf <body> oder dem
|
||||
// ausgehängten „Hochhaus“-Knopf des alten stehen. preventScroll: die Szene soll nicht springen.
|
||||
useEffect(() => {
|
||||
const h1 = h1Ref.current
|
||||
if (h1 && typeof h1.focus === 'function') h1.focus({ preventScroll: true })
|
||||
}, [])
|
||||
|
||||
// Verlässt ein Eintrag die Liste, schließt sein Brief und die Knöpfe mit dem Fokus hängen ab. Die Meldung nimmt ihn (tabIndex -1), wenn er sonst nirgends liegt:
|
||||
// Tastatur- und Screenreader-Nutzer bleiben in der Nähe der Liste und hören die Bestätigung (Endprüfung W3).
|
||||
useEffect(() => {
|
||||
@@ -94,6 +109,19 @@ export default function PultList({ api, me, kpis, onBack }) {
|
||||
})
|
||||
}, [])
|
||||
const onSpaet = useCallback((note) => setSpaet(note), [])
|
||||
// Anfrage zu einem Eintrag beginnt (an = true) oder endet. Beginn: true, wenn sie beginnen darf, false, wenn für diesen Eintrag schon eine läuft (die zweite wird
|
||||
// abgewiesen). Das Ende zeichnet neu: ein inzwischen wieder geöffneter Brief des Eintrags gibt seine Knöpfe frei. Der Beginn braucht kein Neuzeichnen: der Brief, der die
|
||||
// Anfrage startet, sperrt sich selbst, und ein später geöffneter liest laufRef beim Zeichnen.
|
||||
const onLauf = useCallback((id, an) => {
|
||||
const laufend = laufRef.current
|
||||
if (an) {
|
||||
if (laufend.has(id)) return false
|
||||
laufend.add(id)
|
||||
return true
|
||||
}
|
||||
if (laufend.delete(id)) setLaufTakt((n) => n + 1)
|
||||
return true
|
||||
}, [])
|
||||
|
||||
const rows = items.map((item) => ({ item, row: rowModel(item, { me, nowMs, employees }) }))
|
||||
const offen = briefId === null ? null : rows.find(({ row }) => row.id === briefId) ?? null
|
||||
@@ -112,7 +140,7 @@ export default function PultList({ api, me, kpis, onBack }) {
|
||||
|
||||
return (
|
||||
<div className="hk-pult">
|
||||
<PultSzene laeuft={botenLaufen(kpis)} />
|
||||
<PultSzene laeuft={botenLaufen(kpis)} steht={paused === true} />
|
||||
|
||||
<div className="hk-pult-ui">
|
||||
<div className="hk-pult-kopf">
|
||||
@@ -121,12 +149,12 @@ export default function PultList({ api, me, kpis, onBack }) {
|
||||
</button>
|
||||
<div className="hk-messing hk-pult-schild">
|
||||
<span className="hk-ic hk-ic-briefkasten" aria-hidden="true" />
|
||||
<h1 className="hk-pult-h1">Freigabe</h1>
|
||||
<h1 className="hk-pult-h1" tabIndex={-1} ref={h1Ref}>Freigabe</h1>
|
||||
{kopfZahl !== null && (
|
||||
<span className="hk-pult-zahl"><span className="hk-nur-sr">Wartet auf dich:</span>{kopfZahl}</span>
|
||||
)}
|
||||
</div>
|
||||
<button type="button" className="hk-knopf hk-pult-knopf hk-pult-zuklappen" onClick={() => setZu((z) => !z)}>
|
||||
<button type="button" className="hk-knopf hk-pult-knopf hk-pult-zuklappen" aria-expanded={!zu} onClick={() => setZu((z) => !z)}>
|
||||
<span className="hk-ic hk-ic-art-mail" aria-hidden="true" />{zu ? 'Pult aufklappen' : 'Pult zuklappen'}
|
||||
</button>
|
||||
</div>
|
||||
@@ -164,6 +192,8 @@ export default function PultList({ api, me, kpis, onBack }) {
|
||||
onClose={schliesse}
|
||||
onDetails={setDetailId}
|
||||
onSpaet={onSpaet}
|
||||
laeuft={laufRef.current.has(offen.row.id)}
|
||||
onLauf={onLauf}
|
||||
/>
|
||||
)}
|
||||
{detailId && (
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { ARNE_RUHE, BUCH_STEMPEL, DAMPF, FENSTER, LEDS, MAUS_STREIFEN, ROHRPOST, SCHUBLADE, arneAnsicht, kellerEinpassen } from '../../../lib/hustlerKeller'
|
||||
|
||||
// Props: workers = Hustler-Tabelle der Seite (Zustandseinträge nach workerId, aus indexWorkers/reduceEvents)
|
||||
export default function KellerSzene({ workers }) {
|
||||
// Props: workers = Hustler-Tabelle der Seite (Zustandseinträge nach workerId, aus indexWorkers/reduceEvents), steht (Hustler angehalten: alles in der Szene steht still,
|
||||
// Klasse hk-steht aus hustler-ui.css, auch Arne und die Animationen der Dinge)
|
||||
export default function KellerSzene({ workers, steht = false }) {
|
||||
const rahmenRef = useRef(null)
|
||||
const [fit, setFit] = useState(() => kellerEinpassen(0, 0))
|
||||
const arne = arneAnsicht(workers)
|
||||
@@ -39,7 +40,7 @@ export default function KellerSzene({ workers }) {
|
||||
const ruht = arne.arbeitet ? '' : ' hk-kl-ruht'
|
||||
|
||||
return (
|
||||
<div className="hk-kl-szene" aria-hidden="true" ref={rahmenRef} data-hk-arne={arne.status}>
|
||||
<div className={steht === true ? 'hk-kl-szene hk-steht' : 'hk-kl-szene'} aria-hidden="true" ref={rahmenRef} data-hk-arne={arne.status}>
|
||||
<div className="hk-kl-buehne" style={{ transform: `translate(${fit.x.toFixed(1)}px, ${fit.y.toFixed(1)}px) scale(${fit.s.toFixed(4)})` }}>
|
||||
<div className="hk-sz-ebene hk-sz-bild" />
|
||||
<div style={FENSTER}>
|
||||
|
||||
@@ -10,6 +10,7 @@ const SEARCH_MAX = 100 // der Dienst kürzt die Suche auf 100 Zeichen
|
||||
|
||||
// Eine Zeile des Buchs: ein Knopf, der die Akte öffnet. Spalten: Raum-Symbol, Name (+ „gekürzt“) mit Ort und Funddatum, Punkte, Grund mit Symbol, Telefon.
|
||||
function Zeile({ z, nr, aktuell, onOpen }) {
|
||||
const ort = [z.place, z.found].filter((t) => t !== '').join(' · ') // die Zeile kürzt ihn mit „…“: title nennt den vollen Ort samt Datum (W2)
|
||||
return (
|
||||
<li>
|
||||
<button
|
||||
@@ -26,7 +27,7 @@ function Zeile({ z, nr, aktuell, onOpen }) {
|
||||
<span className="hk-zelle-text hk-kl-name" title={z.title}>{z.title}</span>
|
||||
{z.gekuerzt ? <span className="hk-kl-tag"><span className="hk-ic hk-ic-schere hk-kl-tag-icon" aria-hidden="true" />gekürzt</span> : null}
|
||||
</span>
|
||||
<span className="hk-klein">{[z.place, z.found].filter((t) => t !== '').join(' · ')}</span>
|
||||
<span className="hk-klein" title={ort === '' ? undefined : ort}>{ort}</span>
|
||||
</span>
|
||||
<span className="hk-zelle hk-kl-spalte-punkte">
|
||||
<span className="hk-kl-balken" aria-hidden="true"><span className={`hk-kl-balken-fuell hk-kl-farbe-${z.punkte.farbe}`} style={{ width: z.punkte.breite }} /></span>
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
// Dialog: role="dialog" aria-modal, Fokus beim Öffnen auf den ersten Knopf („Schließen“), Tab bleibt im Brief, Esc schließt erst ein offenes Feld, dann den Brief
|
||||
// (während einer Anfrage bleibt ein offenes Feld stehen). Schließt der Brief nach einem Abschluss (onFinished), steht die Meldung in der Liste und nimmt den Fokus;
|
||||
// schließt die Person ihn selbst (onClose), gibt PultList den Fokus an den Umschlag zurück. Eine Aktion, die nach dem Schließen noch antwortet, meldet ihr Ergebnis
|
||||
// über onSpaet an die Liste (sonst ginge ein Fehler wie „Kann gesendet sein – Pult neu laden.“ verloren).
|
||||
// über onSpaet an die Liste (sonst ginge ein Fehler wie „Kann gesendet sein – Pult neu laden.“ verloren); der Satz nennt dann den Eintrag („„Titel“: …“), denn der Brief
|
||||
// ist ja zu. Laufende Anfragen (T13 P1): der Brief meldet Beginn und Ende je Eintrags-Id an PultList (onLauf) und bekommt von dort zurück, ob für seinen Eintrag gerade
|
||||
// eine läuft (laeuft): auch ein später wieder geöffneter Brief sperrt dann alle Aktionsknöpfe und löst keine zweite Anfrage aus.
|
||||
// Kein CSS-Import (die Stile hängt WorkersPage ein), keine Bilder, Fremdtext nur als Textknoten.
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { copyTextChecked } from '../../CopyableCredential'
|
||||
@@ -108,8 +110,9 @@ function TextPanel({ className, caption, label, hint, value, onChange, submitLab
|
||||
|
||||
// Props: api (pultAction), item (Eintrag des Dienstes: Original, Entwurf), row (rowModel), text (Entwurf oder bearbeiteter Text), fields ({ reply, paste }),
|
||||
// onText(id, value), onField(id, which, value), refresh() (Liste neu holen), onFinished(id, message, hadFocus) (der Eintrag verlässt die Liste: PultList schließt den Brief,
|
||||
// zeigt die Meldung und nimmt den Fokus), onClose() (die Person schließt), onDetails(prospectId), onSpaet(note) (Ergebnis, das nach dem Schließen eintrifft)
|
||||
export default function Brief({ api, item, row, text, fields, onText, onField, refresh, onFinished, onClose, onDetails, onSpaet }) {
|
||||
// zeigt die Meldung und nimmt den Fokus), onClose() (die Person schließt), onDetails(prospectId), onSpaet(note) (Ergebnis, das nach dem Schließen eintrifft),
|
||||
// laeuft (für diesen Eintrag läuft gerade eine Anfrage, auch eine aus einem früher geschlossenen Brief), onLauf(id, an) (meldet Beginn und Ende; false = läuft schon)
|
||||
export default function Brief({ api, item, row, text, fields, onText, onField, refresh, onFinished, onClose, onDetails, onSpaet, laeuft = false, onLauf }) {
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [confirming, setConfirming] = useState(null) // null | 'discard' | 'approve' | 'build': welche Rückfrage gerade gilt
|
||||
const [open, setOpen] = useState(null) // null | 'reply' | 'paste': welches Feld aufgeklappt ist
|
||||
@@ -121,6 +124,7 @@ export default function Brief({ api, item, row, text, fields, onText, onField, r
|
||||
const ersterRef = useRef(null) // „Schließen“: Fokus beim Öffnen
|
||||
const returnTo = useRef(null) // { control, panel }: wer beim Start der Aktion den Fokus hatte und welches Feld damals offen war
|
||||
const wasBusy = useRef(false)
|
||||
const gesperrt = busy || laeuft === true // alle Aktionsknöpfe: eigene Anfrage oder eine, die der vorige Brief dieses Eintrags noch laufen hat
|
||||
|
||||
useEffect(() => {
|
||||
aliveRef.current = true
|
||||
@@ -165,6 +169,7 @@ export default function Brief({ api, item, row, text, fields, onText, onField, r
|
||||
|
||||
const begin = () => {
|
||||
if (busyRef.current) return false
|
||||
if (typeof onLauf === 'function' && onLauf(row.id, true) === false) return false // für diesen Eintrag läuft schon eine Anfrage (aus einem früher geschlossenen Brief)
|
||||
busyRef.current = true
|
||||
returnTo.current = { control: activeControl(browserDocument()), panel: open } // ohne Fokus beim Start (Safari fokussiert Knöpfe nicht) gibt es nichts zurückzugeben
|
||||
setBusy(true)
|
||||
@@ -174,12 +179,18 @@ export default function Brief({ api, item, row, text, fields, onText, onField, r
|
||||
}
|
||||
const end = () => {
|
||||
busyRef.current = false
|
||||
if (typeof onLauf === 'function') onLauf(row.id, false) // auch bei geschlossenem Brief: erst jetzt ist der Eintrag wieder frei
|
||||
if (aliveRef.current) setBusy(false)
|
||||
}
|
||||
// Meldung des Briefs; ist er schon zu, geht sie an die Liste
|
||||
// Meldung des Briefs; ist er schon zu, geht sie an die Liste und nennt den Eintrag (der Leser sieht nicht mehr, zu welchem Brief sie gehört)
|
||||
const report = (next) => {
|
||||
if (aliveRef.current) setNote(next)
|
||||
else if (typeof onSpaet === 'function') onSpaet(next)
|
||||
if (aliveRef.current) {
|
||||
setNote(next)
|
||||
return
|
||||
}
|
||||
if (typeof onSpaet !== 'function') return
|
||||
const titel = typeof row.title === 'string' ? row.title.trim() : ''
|
||||
onSpaet(titel === '' ? next : { ...next, text: `„${titel}“: ${next.text}` })
|
||||
}
|
||||
|
||||
// Aktion am Dienst. finished: der Eintrag verlässt die Liste, die Erfolgsmeldung steht deshalb bei der Liste und nicht im Brief.
|
||||
@@ -198,8 +209,11 @@ export default function Brief({ api, item, row, text, fields, onText, onField, r
|
||||
} catch (err) {
|
||||
report({ kind: 'err', text: actionErrorText(err, action) })
|
||||
}
|
||||
await refresh()
|
||||
end()
|
||||
try {
|
||||
await refresh()
|
||||
} finally {
|
||||
end() // auch wenn das Neuladen scheitert: die Sperre des Eintrags darf nicht hängen bleiben
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
@@ -209,22 +223,26 @@ export default function Brief({ api, item, row, text, fields, onText, onField, r
|
||||
// Danach wird der Eintrag best-effort übernommen; ein Kollege, der schon dran ist, erscheint als Hinweis.
|
||||
const copyOpen = async () => {
|
||||
if (!begin()) return
|
||||
const result = await copyAndOpen({ text, url: row.sourceHref, copy: copyTextChecked, open: openTab })
|
||||
let next = copyNote(result, row.sourceHref !== null)
|
||||
if (!row.claimedByMe) {
|
||||
try {
|
||||
await api.pultAction(row.id, 'claim')
|
||||
} catch (err) {
|
||||
const hint = err?.status === 409 && row.claimedByOther ? `Wird schon von ${row.claimedByLabel} bearbeitet.` : err?.message || ACTION_FAILED
|
||||
next = { kind: 'err', text: `${next.text} ${hint}` }
|
||||
try {
|
||||
const result = await copyAndOpen({ text, url: row.sourceHref, copy: copyTextChecked, open: openTab })
|
||||
let next = copyNote(result, row.sourceHref !== null)
|
||||
if (!row.claimedByMe) {
|
||||
try {
|
||||
await api.pultAction(row.id, 'claim')
|
||||
} catch (err) {
|
||||
const hint = err?.status === 409 && row.claimedByOther ? `Wird schon von ${row.claimedByLabel} bearbeitet.` : err?.message || ACTION_FAILED
|
||||
next = { kind: 'err', text: `${next.text} ${hint}` }
|
||||
}
|
||||
await refresh()
|
||||
}
|
||||
await refresh()
|
||||
report(next)
|
||||
} finally {
|
||||
end() // die Sperre des Eintrags darf nie hängen bleiben
|
||||
}
|
||||
report(next)
|
||||
end()
|
||||
}
|
||||
|
||||
const discard = () => {
|
||||
if (gesperrt) return
|
||||
if (confirming !== 'discard') {
|
||||
setConfirming('discard')
|
||||
return
|
||||
@@ -235,6 +253,7 @@ export default function Brief({ api, item, row, text, fields, onText, onField, r
|
||||
// Vorschau freigeben: der erste Klick fragt nach, der zweite binnen 4 s reiht den Link an den Interessenten ein (Max sendet im Sendefenster).
|
||||
// Nur mit gültiger Vorschau-Adresse (https, Vorschau-Server): was der Mensch nicht ansehen kann, gibt er nicht frei (Endprüfung W5).
|
||||
const approve = () => {
|
||||
if (gesperrt) return
|
||||
if (!row.previewHref) return // ohne gültige Adresse sieht der Mensch die Vorschau nicht: der Knopf ist gesperrt, das hier fängt jeden anderen Weg ab
|
||||
if (confirming !== 'approve') {
|
||||
setConfirming('approve')
|
||||
@@ -246,6 +265,7 @@ export default function Brief({ api, item, row, text, fields, onText, onField, r
|
||||
// Bau starten wie bei einem „Ja“ des Interessenten (für Einträge „unsicher“): ein Opus-Lauf, der sich nicht zurücknehmen lässt. Wie beim Freigeben
|
||||
// fragt der erste Klick nach, der zweite binnen 4 s startet den Bau.
|
||||
const startBuild = () => {
|
||||
if (gesperrt) return
|
||||
if (confirming !== 'build') {
|
||||
setConfirming('build')
|
||||
return
|
||||
@@ -257,7 +277,9 @@ export default function Brief({ api, item, row, text, fields, onText, onField, r
|
||||
// vorbelegt, das Einfüge-Feld beginnt leer. Beide liegen in der Liste, damit der Text Schließen und einen Wechsel des Filters überlebt.
|
||||
const replyValue = fields.reply ?? text
|
||||
const pasteValue = fields.paste ?? ''
|
||||
const toggle = (which) => setOpen((current) => (current === which ? null : which))
|
||||
const toggle = (which) => {
|
||||
if (!gesperrt) setOpen((current) => (current === which ? null : which))
|
||||
}
|
||||
// Feld schließen (Esc, „Abbrechen“) und den Fokus an den Knopf zurückgeben, der es geöffnet hat: der bleibt eingehängt, der Fokus kann sofort dorthin.
|
||||
// Der Knopf trägt seine Kennung in data-opens.
|
||||
const close = (which) => {
|
||||
@@ -307,7 +329,7 @@ export default function Brief({ api, item, row, text, fields, onText, onField, r
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (open !== null) {
|
||||
if (!busy) close(open)
|
||||
if (!gesperrt) close(open)
|
||||
return
|
||||
}
|
||||
onClose()
|
||||
@@ -390,6 +412,10 @@ export default function Brief({ api, item, row, text, fields, onText, onField, r
|
||||
)
|
||||
)}
|
||||
|
||||
{laeuft === true && !busy && (
|
||||
<p className="hk-brief-hinweis hk-brief-laeuft" role="status">Für diesen Eintrag läuft noch eine Anfrage. Die Aktionen sind frei, sobald sie fertig ist.</p>
|
||||
)}
|
||||
|
||||
{hasActions ? (
|
||||
<>
|
||||
<h3 className="hk-brief-h hk-brief-h-aktionen">Aktionen</h3>
|
||||
@@ -400,7 +426,7 @@ export default function Brief({ api, item, row, text, fields, onText, onField, r
|
||||
: <span className="hk-brief-hinweis">Keine gültige E-Mail-Adresse hinterlegt.</span>
|
||||
)}
|
||||
{row.canSend && row.kind !== 'mail_antwort' && (
|
||||
<Stempel art="haupt" icon="kopieren" onClick={copyOpen} disabled={busy}>
|
||||
<Stempel art="haupt" icon="kopieren" onClick={copyOpen} disabled={gesperrt}>
|
||||
{row.sourceHref ? 'Kopieren & öffnen' : 'Kopieren'}
|
||||
</Stempel>
|
||||
)}
|
||||
@@ -410,7 +436,7 @@ export default function Brief({ api, item, row, text, fields, onText, onField, r
|
||||
: <span className="hk-brief-hinweis">Keine gültige Vorschau-Adresse hinterlegt. Freigabe nicht möglich – bitte den Entwurf prüfen oder verwerfen.</span>
|
||||
)}
|
||||
{row.canClaim && (
|
||||
<Stempel art="normal" icon="uebernehmen" onClick={() => run(() => api.pultAction(row.id, 'claim'), 'Du kümmerst dich darum.', false)} disabled={busy}>
|
||||
<Stempel art="normal" icon="uebernehmen" onClick={() => run(() => api.pultAction(row.id, 'claim'), 'Du kümmerst dich darum.', false)} disabled={gesperrt}>
|
||||
Ich kümmere mich
|
||||
</Stempel>
|
||||
)}
|
||||
@@ -420,42 +446,42 @@ export default function Brief({ api, item, row, text, fields, onText, onField, r
|
||||
icon="gesendet"
|
||||
aus={text.trim() === ''}
|
||||
onClick={() => run(() => api.pultAction(row.id, 'sent', { text }), 'als gesendet markiert.', true)}
|
||||
disabled={busy || text.trim() === ''}
|
||||
disabled={gesperrt || text.trim() === ''}
|
||||
>
|
||||
Als gesendet markieren
|
||||
</Stempel>
|
||||
)}
|
||||
{row.canReply && (
|
||||
<Stempel art="normal" icon="antworten" onClick={() => toggle('reply')} aria-expanded={open === 'reply'} data-opens="reply" disabled={busy}>
|
||||
<Stempel art="normal" icon="antworten" onClick={() => toggle('reply')} aria-expanded={open === 'reply'} data-opens="reply" disabled={gesperrt}>
|
||||
Als Mensch antworten
|
||||
</Stempel>
|
||||
)}
|
||||
{row.canApprovePreview && (
|
||||
<Stempel art="gruen" icon="gesendet" frage={confirming === 'approve'} onClick={approve} disabled={busy || !row.previewHref}>
|
||||
<Stempel art="gruen" icon="gesendet" frage={confirming === 'approve'} onClick={approve} disabled={gesperrt || !row.previewHref}>
|
||||
{confirming === 'approve' ? 'Wirklich freigeben?' : 'Freigeben & senden'}
|
||||
</Stempel>
|
||||
)}
|
||||
{row.canStartBuild && (
|
||||
<>
|
||||
<Stempel art="normal" icon="bauen" frage={confirming === 'build'} onClick={startBuild} disabled={busy} title={BUILD_HINT}>
|
||||
<Stempel art="normal" icon="bauen" frage={confirming === 'build'} onClick={startBuild} disabled={gesperrt} title={BUILD_HINT}>
|
||||
{confirming === 'build' ? 'Wirklich bauen?' : 'Vorschau bauen'}
|
||||
</Stempel>
|
||||
{confirming === 'build' && <span className="hk-brief-hinweis hk-brief-hinweis-bau">{BUILD_HINT}</span>}
|
||||
</>
|
||||
)}
|
||||
{row.canPaste && (
|
||||
<Stempel art="normal" icon="einfuegen" onClick={() => toggle('paste')} aria-expanded={open === 'paste'} data-opens="paste" disabled={busy}>
|
||||
<Stempel art="normal" icon="einfuegen" onClick={() => toggle('paste')} aria-expanded={open === 'paste'} data-opens="paste" disabled={gesperrt}>
|
||||
Antwort des Kunden einfügen
|
||||
</Stempel>
|
||||
)}
|
||||
{row.canDone && (
|
||||
<Stempel art="normal" icon="erledigt" onClick={() => run(() => api.pultAction(row.id, 'done'), 'erledigt.', true)} disabled={busy}>
|
||||
<Stempel art="normal" icon="erledigt" onClick={() => run(() => api.pultAction(row.id, 'done'), 'erledigt.', true)} disabled={gesperrt}>
|
||||
Erledigt
|
||||
</Stempel>
|
||||
)}
|
||||
{row.canDiscard && (
|
||||
<>
|
||||
<Stempel art="rot" icon="verwerfen" frage={confirming === 'discard'} onClick={discard} disabled={busy}>
|
||||
<Stempel art="rot" icon="verwerfen" frage={confirming === 'discard'} onClick={discard} disabled={gesperrt}>
|
||||
{confirming === 'discard' ? 'Wirklich verwerfen?' : 'Verwerfen'}
|
||||
</Stempel>
|
||||
{/* Eine Vorschau-Freigabe wird nur durch Freigeben gesendet; Verwerfen schließt sie, ohne dass etwas hinausgeht (D20) */}
|
||||
@@ -478,7 +504,7 @@ export default function Brief({ api, item, row, text, fields, onText, onField, r
|
||||
submitLabel="Jetzt senden"
|
||||
onSubmit={sendReply}
|
||||
onClose={() => close('reply')}
|
||||
busy={busy}
|
||||
busy={gesperrt}
|
||||
/>
|
||||
)}
|
||||
{open === 'paste' && row.canPaste && (
|
||||
@@ -492,7 +518,7 @@ export default function Brief({ api, item, row, text, fields, onText, onField, r
|
||||
submitLabel="Einfügen"
|
||||
onSubmit={sendPaste}
|
||||
onClose={() => close('paste')}
|
||||
busy={busy}
|
||||
busy={gesperrt}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import { SZENE_START, SZENE_TEILE as T, szenePassung } from '../../../lib/hustle
|
||||
const lage = (teil) => ({ left: teil.left, top: teil.top })
|
||||
const flaeche = (teil) => ({ left: teil.left, top: teil.top, width: teil.width, height: teil.height })
|
||||
|
||||
// Props: laeuft (Boten-Schleife an, Standard aus)
|
||||
export default function PultSzene({ laeuft = false }) {
|
||||
// Props: laeuft (Boten-Schleife an, Standard aus), steht (Hustler angehalten: alles in der Szene steht still, Klasse hk-steht aus hustler-ui.css, auch Boten und Fähnchen)
|
||||
export default function PultSzene({ laeuft = false, steht = false }) {
|
||||
const rahmen = useRef(null)
|
||||
const [pass, setPass] = useState(SZENE_START)
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function PultSzene({ laeuft = false }) {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={laeuft ? 'hk-pult-szene' : 'hk-pult-szene hk-pult-ruhe'} ref={rahmen} aria-hidden="true">
|
||||
<div className={`hk-pult-szene${laeuft ? '' : ' hk-pult-ruhe'}${steht === true ? ' hk-steht' : ''}`} ref={rahmen} aria-hidden="true">
|
||||
<div className="hk-pult-buehne" style={{ transform: pass.transform }}>
|
||||
<div className="hk-sz-ebene hk-sz-bild" />
|
||||
<div className="hk-pult-teil hk-pult-fenster" style={flaeche(T.fenster1)}>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Kein React, kein DOM, kein Appwrite: unter Node testbar (loadModule). Beispieldaten gibt es hier nicht; alle Werte kommen aus dem Dienst.
|
||||
import { ROOM_FILTERS, botleadRow } from './botleadsView.js'
|
||||
import { grundInfo } from './botleadGruende.js'
|
||||
import { figurZustand } from './hustlerSicht.js'
|
||||
import { anzeigeName, figurZustand, modellKurz } from './hustlerSicht.js'
|
||||
import { ROLES } from './workersRoster.js'
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
@@ -61,9 +61,16 @@ export const LEDS = Object.freeze([
|
||||
/**
|
||||
* Was die Szene von Arne zeigt (Spec E12): bei „arbeitet“ die Ablauf-Schleife mit Rohrpost, Stempel und Schublade, sonst steht oder sitzt er als Haus-Figur der
|
||||
* passenden Pose (warten, schlafen, nicht) am Pult und die drei ruhen; bei „fehler“ trägt er den roten Marker. workers = Hustler-Tabelle der Seite.
|
||||
* Gibt es für Arne noch keinen Eintrag (Tabelle leer, Seite hat den ersten Zustand noch nicht): er steht neutral am Pult (status 'unbekannt', Pose „warten“ ohne
|
||||
* Kaffeepunkte), ohne Zzz, ohne Marker und ohne „nicht angeschlossen“; wir wissen nichts über ihn und behaupten nichts (T13 R5). Ein Eintrag mit unbrauchbarem Status
|
||||
* gilt weiter als Leerlauf (figurZustand).
|
||||
*/
|
||||
export function arneAnsicht(workers) {
|
||||
const f = figurZustand('arne', workers && typeof workers === 'object' ? workers.arne : undefined)
|
||||
const eintrag = workers && typeof workers === 'object' ? workers.arne : undefined
|
||||
if (eintrag === null || typeof eintrag !== 'object' || Array.isArray(eintrag)) {
|
||||
return { status: 'unbekannt', arbeitet: false, pose: 'warten', alarm: false, punkte: false, zzz: false, label: `${anzeigeName('arne')}, ${modellKurz('arne')}` }
|
||||
}
|
||||
const f = figurZustand('arne', eintrag)
|
||||
return {
|
||||
status: f.status,
|
||||
arbeitet: f.status === 'arbeitet',
|
||||
|
||||
@@ -43,11 +43,21 @@
|
||||
.hk-keller .hk-knopf:disabled { opacity: 0.45; cursor: default; }
|
||||
.hk-keller .hk-knopf.hk-kl-zuklappen { margin-left: auto; }
|
||||
.hk-keller .hk-kl-schild { display: flex; align-items: center; gap: 10px; padding: 0 6px; color: #3a2410; }
|
||||
.hk-keller .hk-kl-titel:focus-visible { outline: 2px solid #1a1630; outline-offset: 4px; }
|
||||
.hk-keller .hk-kl-titel, .hk-keller .hk-kl-zahl { margin: 0; font-family: 'Press Start 2P', 'Pixelify Sans', monospace; font-size: 16px; line-height: 16px; font-weight: 400; color: #3a2410; }
|
||||
|
||||
/* Raum-Reiter: sitzen oberhalb des Buchs, die inaktiven ragen 8 px hinter das Buch */
|
||||
.hk-keller .hk-kl-reiter { position: relative; z-index: 1; flex: none; display: flex; flex-wrap: nowrap; gap: 6px; padding: 0 0 0 22px; min-width: 0; }
|
||||
.hk-keller .hk-kl-reiter-zahl { font-family: 'Press Start 2P', 'Pixelify Sans', monospace; font-size: 8px; line-height: 8px; color: #5a4a3a; }
|
||||
.hk-keller .hk-kl-reiter-zahl { font-family: 'Pixelify Sans', ui-monospace, monospace; font-size: 13px; line-height: 16px; color: #5a4a3a; }
|
||||
/* Fokus am Raum-Reiter innen (T13 M3): außen klebte der Ring am Buch und wurde am Handy vom wischbaren Reiterstreifen abgeschnitten. Der Kasten-Schatten des Reiters
|
||||
(sein „Rand“) bleibt, der Ring liegt als gelbe Innenkante darüber. */
|
||||
.hk-keller .hk-kl-reiter .hk-reiter:focus-visible {
|
||||
outline: 2px solid #1a1630; outline-offset: -2px;
|
||||
box-shadow: 0 -2px 0 0 #2b2230, -2px 0 0 0 #2b2230, 2px 0 0 0 #2b2230, inset 0 0 0 4px #ffd25a;
|
||||
}
|
||||
.hk-keller .hk-kl-reiter .hk-reiter-an:focus-visible {
|
||||
box-shadow: 0 -2px 0 0 #7a2e3a, -2px 0 0 0 #7a2e3a, 2px 0 0 0 #7a2e3a, inset 0 0 0 4px #ffd25a;
|
||||
}
|
||||
|
||||
/* Buch: Kopf mit Titel, Hinweis und Suche, Kopfzeile, Liste, Fuß mit Seitenzeile */
|
||||
.hk-keller .hk-kl-buch { position: relative; z-index: 2; flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; gap: 6px; }
|
||||
@@ -74,7 +84,7 @@
|
||||
.hk-keller .hk-kl-farbe-mittel { background: #c9962a; }
|
||||
.hk-keller .hk-kl-farbe-schlecht { background: #b8452f; }
|
||||
.hk-keller .hk-kl-farbe-leer { background: #9a8f84; }
|
||||
.hk-keller .hk-kl-punkte-text { font-family: 'Press Start 2P', 'Pixelify Sans', monospace; font-size: 8px; line-height: 8px; }
|
||||
.hk-keller .hk-kl-punkte-text { font-family: 'Pixelify Sans', ui-monospace, monospace; font-size: 13px; line-height: 16px; }
|
||||
|
||||
/* Laden, Leer, Fehler */
|
||||
.hk-keller .hk-kl-laden, .hk-keller .hk-kl-leer, .hk-keller .hk-kl-fehlerbox { margin: 8px; color: #5a4a3a; }
|
||||
@@ -85,13 +95,14 @@
|
||||
|
||||
/* Fuß: ◂ Seitenzeile ▸ */
|
||||
.hk-keller .hk-kl-fuss { margin-top: auto; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 2px; }
|
||||
.hk-keller .hk-kl-seiten { flex: 1 1 auto; font-family: 'Press Start 2P', 'Pixelify Sans', monospace; font-size: 8px; line-height: 12px; color: #5a4a3a; text-align: center; }
|
||||
.hk-keller .hk-kl-seiten { flex: 1 1 auto; font-family: 'Pixelify Sans', ui-monospace, monospace; font-size: 13px; line-height: 16px; color: #5a4a3a; text-align: center; }
|
||||
|
||||
/* Nur für Screenreader */
|
||||
.hk-keller .hk-kl-sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
|
||||
|
||||
/* ---------- Akte: Dialog auf der Manila-Karte ---------- */
|
||||
.hk-keller .hk-kl-akte-ueber { position: absolute; inset: 0; z-index: 20; display: flex; align-items: center; justify-content: center; box-sizing: border-box; padding: 16px; }
|
||||
/* z-index 9: über Szene und Buch (bis 2), aber unter der Handy-Seitenleiste (20) und ihrem Hintergrund (15) der Seite (T13 R4) */
|
||||
.hk-keller .hk-kl-akte-ueber { position: absolute; inset: 0; z-index: 9; display: flex; align-items: center; justify-content: center; box-sizing: border-box; padding: 16px; }
|
||||
.hk-keller .hk-kl-akte-hinten {
|
||||
position: absolute; inset: 0; margin: 0; padding: 0; border: 0; border-radius: 0;
|
||||
background: rgba(6, 7, 18, 0.62); cursor: default;
|
||||
@@ -132,6 +143,10 @@
|
||||
.hk-keller .hk-kl-buch .hk-zeile, .hk-keller .hk-kl-buch .hk-kopfzeile { grid-template-columns: 32px minmax(0, 1fr) minmax(0, 38%); }
|
||||
.hk-keller .hk-kl-buch .hk-kl-spalte-punkte, .hk-keller .hk-kl-buch .hk-kl-spalte-telefon { display: none; }
|
||||
.hk-keller .hk-kl-akte-name { font-size: 12px; line-height: 18px; }
|
||||
/* Tippflächen (T13 K7): Suchfeld und Textlink der Akte mindestens 40 px hoch; die Lupe bleibt im Feld mittig */
|
||||
.hk-keller input.hk-suche { min-height: 40px; }
|
||||
.hk-keller .hk-kl-lupe { top: 4px; }
|
||||
.hk-keller .hk-akte-link { min-height: 40px; }
|
||||
.hk-keller .hk-kl-akte-ueber { padding: 0; }
|
||||
.hk-keller .hk-akte.hk-kl-akte { width: 100%; height: 100%; max-height: none; }
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
.hk-pult .hk-pult-zuklappen { margin-left: auto; }
|
||||
.hk-pult .hk-pult-schild { display: flex; align-items: center; gap: 10px; padding: 0 6px; color: #3a2410; }
|
||||
.hk-pult .hk-pult-h1 { margin: 0; font-family: 'Press Start 2P', 'Pixelify Sans', monospace; font-size: 16px; line-height: 16px; font-weight: 400; color: #3a2410; }
|
||||
.hk-pult .hk-pult-h1:focus-visible { outline: 2px solid #1a1630; outline-offset: 4px; }
|
||||
.hk-pult .hk-pult-zahl { font-family: 'Press Start 2P', 'Pixelify Sans', monospace; font-size: 16px; line-height: 16px; }
|
||||
/* Nur für Screenreader (Beschriftung vor der Zahl) */
|
||||
.hk-pult .hk-nur-sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
|
||||
@@ -71,7 +72,8 @@
|
||||
.hk-pult .hk-marke-veraltet { color: #8a4a20; }
|
||||
|
||||
/* ---------- Brief: Dunkelfläche mit Hintergrund-Knopf, Papier in der Mitte, Inhalt scrollt ---------- */
|
||||
.hk-pult .hk-brief-ueber { position: absolute; inset: 0; z-index: 20; display: flex; align-items: center; justify-content: center; box-sizing: border-box; padding: 16px; }
|
||||
/* z-index 9: über der Szene und dem Klemmbrett (bis 2), aber unter der Handy-Seitenleiste (20) und ihrem Hintergrund (15) der Seite (T13 R4) */
|
||||
.hk-pult .hk-brief-ueber { position: absolute; inset: 0; z-index: 9; display: flex; align-items: center; justify-content: center; box-sizing: border-box; padding: 16px; }
|
||||
.hk-pult .hk-brief-hinten {
|
||||
position: absolute; inset: 0; margin: 0; padding: 0; border: 0; border-radius: 0; background: rgba(6, 7, 18, 0.6); cursor: default;
|
||||
-webkit-appearance: none; appearance: none; -webkit-tap-highlight-color: transparent;
|
||||
@@ -94,18 +96,19 @@
|
||||
.hk-pult .hk-brief-h-aktionen { margin: 16px 0 6px; }
|
||||
.hk-pult .hk-brief-zitat {
|
||||
margin: 0; padding: 8px 10px; background: #f1ead6; box-shadow: inset 4px 0 0 0 #c9a25c;
|
||||
white-space: pre-line; font-size: 14px; line-height: 18px; overflow-wrap: anywhere;
|
||||
white-space: pre-wrap; font-size: 14px; line-height: 18px; overflow-wrap: anywhere;
|
||||
}
|
||||
.hk-pult .hk-feld-label { display: block; margin-top: 14px; }
|
||||
.hk-pult .hk-feld-titel { display: block; margin-bottom: 4px; font-size: 13px; line-height: 16px; color: #4a2fa0; }
|
||||
.hk-pult .hk-brief-entwurf { min-height: 96px; background: repeating-linear-gradient(#fffdf6 0 17px, rgba(90, 60, 176, 0.16) 17px 18px); }
|
||||
.hk-pult .hk-brief-lesen {
|
||||
margin: 0; padding: 2px 10px 4px; white-space: pre-line; font-size: 15px; line-height: 20px; overflow-wrap: anywhere;
|
||||
margin: 0; padding: 2px 10px 4px; white-space: pre-wrap; font-size: 15px; line-height: 20px; overflow-wrap: anywhere;
|
||||
background: repeating-linear-gradient(transparent 0 19px, rgba(90, 60, 176, 0.18) 19px 20px);
|
||||
}
|
||||
.hk-pult .hk-aktionen { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; }
|
||||
.hk-pult .hk-brief-hinweis { margin: 0; font-size: 13px; color: #5a4a3a; }
|
||||
.hk-pult .hk-brief-hinweis-bau { color: #7a2e3a; }
|
||||
.hk-pult .hk-brief-laeuft { margin-top: 12px; }
|
||||
.hk-pult .hk-stempel:disabled { opacity: 0.5; cursor: default; }
|
||||
.hk-pult a.hk-stempel { text-decoration: none; }
|
||||
.hk-pult .hk-feld-panel { margin-top: 14px; }
|
||||
@@ -131,6 +134,8 @@
|
||||
.hk-pult .hk-pult-schild { order: 2; flex: 1 1 100%; }
|
||||
.hk-pult .hk-klammer { display: none; }
|
||||
.hk-pult .hk-brief-ueber { padding: 0; }
|
||||
/* Textlinks im Kopf des Briefs („Anzeige öffnen“, „Interessent“): mindestens 40 px hoch zum Antippen (T13 K7) */
|
||||
.hk-pult .hk-link { min-height: 40px; }
|
||||
.hk-pult .hk-brief { width: 100%; height: 100%; max-height: 100%; }
|
||||
.hk-pult .hk-feld-fuss { flex-direction: column; align-items: stretch; }
|
||||
.hk-pult .hk-feld-knoepfe { flex-direction: column-reverse; }
|
||||
|
||||
Reference in new issue
Block a user