Kundenansicht neu P2: Ansicht Chat mit dem Composer und lilafreier CSS-Kopie
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a3831040ba
commit
8c8d29657d
6 files changed
+475
-9
No files matched your search
+143
-4
@@ -1,7 +1,146 @@
|
||||
/* Kundenansicht neu: Platzhalter, den das nächste Paket füllt (Stub aus P1). */
|
||||
/* Kundenansicht neu (P2): Bereich Chat. Verlauf und Eingabezeile des gemeinsamen Composers (window.ChatComposer, chat-composer.js),
|
||||
Poll alle 10 s mit ?since=, Gelesen-Markierung, Vorgabe aus "Postfach anfragen". Nachrichten gehen nur als Text in den Verlauf. */
|
||||
|
||||
import { h } from './dom.js'
|
||||
import { api } from './api.js'
|
||||
import { h, leeren } from './dom.js'
|
||||
import { K, fmtDatumZeit, meldeZaehlerNeu, nimmChatVorgabe } from './ansicht-hilfen.js'
|
||||
|
||||
export async function zeigeChat(wurzel, { route, me, nav }) {
|
||||
wurzel.replaceChildren(h('p', { class: 'kn-leer' }, 'Chat (folgt)'))
|
||||
export const CHAT_TEXTE = {
|
||||
platzhalter: 'Nachricht an das WEBklar-Team…',
|
||||
kurz: 'Nachricht…',
|
||||
gesperrt: 'Im Ansichtsmodus kann nicht als Kunde geschrieben werden.',
|
||||
leer: 'Noch keine Nachrichten.',
|
||||
}
|
||||
|
||||
const POLL_MS = 10000
|
||||
const dateiUrl = (att) => `/api/chat/attachments/${encodeURIComponent(att.id)}`
|
||||
|
||||
/** 'Du · 14.10.26, 10:05' oder '<Name oder Webklar-Team> · …'. */
|
||||
export function nachrichtMeta(msg) {
|
||||
const meine = msg?.sender === 'customer'
|
||||
return `${meine ? 'Du' : msg?.senderName || 'Webklar-Team'} · ${fmtDatumZeit(msg?.createdAt)}`
|
||||
}
|
||||
|
||||
let aktiv = null // der laufende Chat: { timer, composer }
|
||||
|
||||
function raeumeAuf() {
|
||||
if (!aktiv) return
|
||||
if (aktiv.timer) clearInterval(aktiv.timer)
|
||||
try { aktiv.composer?.destroy?.() } catch { /* schon abgebaut */ }
|
||||
aktiv = null
|
||||
}
|
||||
|
||||
export async function zeigeChat(wurzel, { route, me, nav } = {}) {
|
||||
raeumeAuf()
|
||||
const eigener = { timer: null, composer: null }
|
||||
aktiv = eigener
|
||||
const gilt = () => aktiv === eigener
|
||||
leeren(wurzel).append(h('p', { class: K.leer }, 'Wird geladen …'))
|
||||
|
||||
const Composer = globalThis.ChatComposer
|
||||
if (!Composer) {
|
||||
leeren(wurzel).append(h('p', { class: K.fehler }, 'Der Chat konnte nicht gestartet werden. Bitte lade die Seite neu.'))
|
||||
return
|
||||
}
|
||||
const viewAs = me?.viewAs === true
|
||||
|
||||
const verlauf = h('div', { class: 'chat-messages kn-chat-verlauf', role: 'log', 'aria-label': 'Nachrichtenverlauf' })
|
||||
const composerWurzel = h('div', { class: 'kn-chat-composer' })
|
||||
const kasten = h('div', { class: 'chat-box kn-chat-kasten' }, verlauf, composerWurzel)
|
||||
leeren(wurzel).append(h('div', { class: 'kn-chat' }, kasten))
|
||||
|
||||
const gezeigt = new Set()
|
||||
let zuletzt = ''
|
||||
let leerHinweis = null
|
||||
let fehler = null
|
||||
const hinweisLeer = () => {
|
||||
if (gezeigt.size || leerHinweis) return
|
||||
leerHinweis = h('p', { class: K.leer }, CHAT_TEXTE.leer)
|
||||
verlauf.append(leerHinweis)
|
||||
}
|
||||
const raeumeHinweise = () => { leerHinweis?.remove(); leerHinweis = null; fehler?.remove(); fehler = null }
|
||||
|
||||
/** Neue Nachrichten anhängen (nach id entdoppelt); Rückgabe: die wirklich neuen. */
|
||||
function haengeAn(nachrichten) {
|
||||
const neu = []
|
||||
for (const msg of nachrichten) {
|
||||
if (!msg || gezeigt.has(msg.id)) continue
|
||||
gezeigt.add(msg.id)
|
||||
neu.push(msg)
|
||||
}
|
||||
if (!neu.length) return neu
|
||||
raeumeHinweise()
|
||||
for (const msg of neu) {
|
||||
verlauf.append(Composer.renderMessage(msg, { mine: msg.sender === 'customer', meta: nachrichtMeta(msg), fileUrl: dateiUrl }))
|
||||
if (msg.createdAt > zuletzt) zuletzt = msg.createdAt
|
||||
}
|
||||
verlauf.scrollTop = verlauf.scrollHeight
|
||||
return neu
|
||||
}
|
||||
|
||||
// Nachrichten als gelesen markieren; im Ansichtsmodus nie (der Kunde hat sie nicht gelesen)
|
||||
async function markiereGelesen() {
|
||||
if (viewAs) return
|
||||
await api('/api/chat/read', { method: 'POST' }).catch(() => {})
|
||||
if (gilt()) meldeZaehlerNeu('chat')
|
||||
}
|
||||
|
||||
async function hole() {
|
||||
const frage = zuletzt ? `?since=${encodeURIComponent(zuletzt)}` : ''
|
||||
const daten = await api(`/api/chat/messages${frage}`)
|
||||
if (!gilt()) return []
|
||||
return haengeAn(Array.isArray(daten?.messages) ? daten.messages : [])
|
||||
}
|
||||
|
||||
async function poll() {
|
||||
if (!gilt()) return
|
||||
if (!wurzel.isConnected) { clearInterval(eigener.timer); return }
|
||||
if (globalThis.document?.hidden) return
|
||||
if (wurzel.closest?.('[hidden]')) return // der Chat ist gerade nicht zu sehen
|
||||
try {
|
||||
const neu = await hole()
|
||||
if (neu.some((m) => m.sender !== 'customer')) await markiereGelesen()
|
||||
} catch { /* der nächste Poll versucht es erneut */ }
|
||||
}
|
||||
|
||||
Composer.trackScroll(verlauf)
|
||||
eigener.composer = Composer.create({
|
||||
root: composerWurzel,
|
||||
dropZone: kasten,
|
||||
uploadUrl: '/api/chat/attachments',
|
||||
placeholder: CHAT_TEXTE.platzhalter,
|
||||
placeholderShort: CHAT_TEXTE.kurz,
|
||||
disabled: viewAs,
|
||||
disabledText: CHAT_TEXTE.gesperrt,
|
||||
send: async ({ text, attachmentIds }) => {
|
||||
const { message } = await api('/api/chat/messages', { method: 'POST', body: { text, attachmentIds } })
|
||||
return message
|
||||
},
|
||||
onSent: (message) => { haengeAn([message]) },
|
||||
})
|
||||
|
||||
const vorgabe = nimmChatVorgabe()
|
||||
if (vorgabe) {
|
||||
const feld = composerWurzel.querySelector('textarea')
|
||||
if (feld) {
|
||||
feld.value = vorgabe
|
||||
if (typeof Event === 'function') feld.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
feld.focus()
|
||||
if (typeof feld.setSelectionRange === 'function') feld.setSelectionRange(vorgabe.length, vorgabe.length)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await hole()
|
||||
if (!gilt()) return
|
||||
hinweisLeer()
|
||||
} catch (f) {
|
||||
if (!gilt()) return
|
||||
fehler = h('p', { class: K.fehler }, `Nachrichten konnten nicht geladen werden: ${f.message}`)
|
||||
verlauf.append(fehler)
|
||||
}
|
||||
await markiereGelesen()
|
||||
if (!gilt()) return
|
||||
eigener.timer = setInterval(poll, POLL_MS)
|
||||
eigener.timer?.unref?.() // im Browser eine Zahl; in Node hält der Timer den Prozess nicht fest
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
/* Chat: Anhaenge, Kamera und Sprachnachrichten fuer die weisse Kundenansicht (Kopie von chat-composer.css, siehe chat-composer.js).
|
||||
Gegenueber dem Original: helle, neutrale Variablen, kein Lila, kein Gewicht 800 und kein Weichzeichner im Overlay. Das Original bleibt fuer die alte Ansicht. */
|
||||
:root {
|
||||
--chat-accent: #111111;
|
||||
--chat-accent-hover: #333333;
|
||||
--chat-accent-soft: rgba(17, 17, 17, 0.08);
|
||||
--chat-accent-line: rgba(17, 17, 17, 0.5);
|
||||
--chat-accent-text: #111111;
|
||||
--chat-on-accent: #ffffff;
|
||||
--chat-line: #d8dde3;
|
||||
--chat-muted: #5e6678;
|
||||
--chat-surface: #ffffff;
|
||||
}
|
||||
|
||||
.chat-composer [hidden], .chat-overlay [hidden], .chat-bubble [hidden] { display: none !important; }
|
||||
.chat-composer button, .chat-bubble button, .chat-bubble a, .chat-overlay button { -webkit-tap-highlight-color: transparent; }
|
||||
.chat-ico { display: inline-grid; place-items: center; line-height: 0; }
|
||||
.chat-ico svg { width: 20px; height: 20px; }
|
||||
|
||||
/* ------------------------------------------------------------- Verlauf --- */
|
||||
.chat-bubble { white-space: normal; }
|
||||
.chat-bubble .chat-text { white-space: pre-wrap; }
|
||||
.chat-bubble.has-atts { padding: 6px 6px 8px; }
|
||||
.chat-bubble.has-atts .chat-text, .chat-bubble.has-atts .chat-meta { padding: 0 8px; }
|
||||
.chat-atts { display: grid; grid-template-columns: minmax(0, 1fr); gap: 6px; }
|
||||
.chat-atts + .chat-text { margin-top: 8px; }
|
||||
|
||||
.chat-att-images { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px; width: 272px; max-width: 100%; }
|
||||
.chat-att-images.single { display: block; width: auto; }
|
||||
.chat-att-image {
|
||||
position: relative; display: block; max-width: 100%; padding: 0; border: 0; border-radius: 12px; overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.06); color: inherit; font: inherit; cursor: zoom-in;
|
||||
}
|
||||
.chat-att-image img { display: block; }
|
||||
.chat-att-images.single img { max-width: 100%; max-height: 320px; height: auto; }
|
||||
.chat-att-images:not(.single) .chat-att-image { aspect-ratio: 1; }
|
||||
.chat-att-images:not(.single) img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.chat-att-image:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
|
||||
.chat-att-image.is-broken {
|
||||
display: flex; align-items: center; gap: 8px; padding: 12px 14px; aspect-ratio: auto;
|
||||
font-size: 13px; color: var(--chat-muted); cursor: default;
|
||||
}
|
||||
|
||||
.chat-att-file {
|
||||
display: flex; align-items: center; gap: 10px; width: 270px; max-width: 100%; box-sizing: border-box; padding: 8px 12px 8px 8px;
|
||||
border-radius: 12px; background: rgba(0, 0, 0, 0.22); border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
color: inherit !important; text-decoration: none !important;
|
||||
}
|
||||
.chat-att-file:hover { background: rgba(0, 0, 0, 0.34); }
|
||||
.chat-att-file > .chat-ico svg { width: 18px; height: 18px; opacity: 0.75; }
|
||||
.chat-file-icon { position: relative; flex: none; width: 36px; height: 40px; display: grid; place-items: center; }
|
||||
.chat-file-icon .chat-ico svg { width: 34px; height: 34px; stroke-width: 1.4; opacity: 0.9; }
|
||||
.chat-file-ext {
|
||||
position: absolute; left: 50%; bottom: 6px; transform: translateX(-50%); padding: 0 3px; border-radius: 3px;
|
||||
font-size: 8.5px; line-height: 13px; font-weight: 700; letter-spacing: 0.02em; background: var(--chat-accent); color: var(--chat-on-accent);
|
||||
white-space: nowrap; word-break: normal;
|
||||
}
|
||||
.chat-file-ext:empty { display: none; }
|
||||
.chat-file-text { flex: 1; min-width: 0; display: grid; gap: 1px; }
|
||||
.chat-file-name { font-size: 13.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.chat-file-size { font-size: 12px; opacity: 0.7; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
.chat-audio { display: flex; align-items: center; gap: 10px; width: 260px; max-width: 100%; box-sizing: border-box; padding: 2px; }
|
||||
.chat-audio-play {
|
||||
flex: none; width: 38px; height: 38px; padding: 0; border: 0; border-radius: 50%; display: grid; place-items: center;
|
||||
background: var(--chat-accent); color: var(--chat-on-accent); cursor: pointer;
|
||||
}
|
||||
.chat-bubble.mine .chat-audio-play { background: #fff; color: #17102a; }
|
||||
.chat-audio-play .chat-ico svg { width: 16px; height: 16px; }
|
||||
.chat-audio-main { flex: 1; min-width: 0; display: grid; gap: 7px; }
|
||||
.chat-audio-label { font-size: 12px; font-weight: 600; opacity: 0.8; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.chat-audio-bar { position: relative; display: block; height: 6px; border-radius: 999px; background: rgba(255, 255, 255, 0.2); cursor: pointer; }
|
||||
.chat-audio-bar::after { content: ''; position: absolute; inset: -8px 0; }
|
||||
.chat-audio-bar:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }
|
||||
.chat-audio-fill { position: absolute; left: 0; top: 0; bottom: 0; width: 0; border-radius: inherit; background: currentColor; }
|
||||
.chat-audio-time { flex: none; min-width: 32px; text-align: right; font-size: 12px; font-variant-numeric: tabular-nums; opacity: 0.8; }
|
||||
.chat-audio-dl {
|
||||
flex: none; display: grid; place-items: center; width: 28px; height: 28px; border-radius: 50%;
|
||||
color: inherit !important; opacity: 0.7;
|
||||
}
|
||||
.chat-audio-dl:hover { opacity: 1; background: rgba(255, 255, 255, 0.1); }
|
||||
.chat-audio-dl .chat-ico svg { width: 16px; height: 16px; }
|
||||
.chat-audio.is-error .chat-audio-time { opacity: 1; color: #fca5a5; font-size: 11px; }
|
||||
|
||||
/* ------------------------------------------------------- Eingabezeile --- */
|
||||
.chat-composer { position: relative; margin-top: 12px; }
|
||||
.chat-composer .chat-input-row { display: flex; align-items: flex-end; gap: 8px; margin-top: 0; }
|
||||
.chat-composer textarea { flex: 1; min-width: 0; box-sizing: border-box; }
|
||||
.chat-icon-btn {
|
||||
flex: none; width: 46px; height: 46px; padding: 0; border-radius: 50%; display: grid; place-items: center;
|
||||
border: 1px solid var(--chat-line); background: rgba(255, 255, 255, 0.05); color: inherit; cursor: pointer;
|
||||
}
|
||||
.chat-icon-btn:hover:not(:disabled) { background: rgba(255, 255, 255, 0.11); }
|
||||
.chat-icon-btn:disabled { opacity: 0.45; cursor: default; }
|
||||
.chat-icon-btn[aria-expanded="true"] { background: var(--chat-accent-soft); border-color: var(--chat-accent-line); }
|
||||
.chat-icon-btn:focus-visible, .chat-send:focus-visible, .chat-audio-play:focus-visible, .chat-tray-remove:focus-visible {
|
||||
outline: 2px solid var(--chat-accent-text); outline-offset: 2px;
|
||||
}
|
||||
|
||||
.chat-attach { position: relative; flex: none; }
|
||||
.chat-menu {
|
||||
position: absolute; left: 0; bottom: calc(100% + 8px); z-index: 30; min-width: 190px; padding: 6px; display: grid; gap: 2px;
|
||||
border-radius: 14px; background: var(--chat-surface); border: 1px solid var(--chat-line); box-shadow: 0 14px 36px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
.chat-menu button {
|
||||
display: flex; align-items: center; gap: 12px; width: 100%; padding: 10px 12px; border: 0; border-radius: 10px;
|
||||
background: none; color: inherit; font: inherit; font-size: 14px; text-align: left; cursor: pointer;
|
||||
}
|
||||
.chat-menu button:hover, .chat-menu button:focus-visible { background: rgba(255, 255, 255, 0.08); outline: none; }
|
||||
.chat-menu .chat-ico { color: var(--chat-accent-text); }
|
||||
|
||||
.chat-send {
|
||||
flex: none; display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 46px; padding: 0 20px;
|
||||
border: none; border-radius: 999px; background: var(--chat-accent); color: var(--chat-on-accent);
|
||||
font: inherit; font-weight: 700; cursor: pointer;
|
||||
}
|
||||
.chat-send:hover:not(:disabled) { background: var(--chat-accent-hover); }
|
||||
.chat-send:disabled { opacity: 0.5; cursor: default; }
|
||||
.chat-send.is-busy:disabled { opacity: 0.8; cursor: progress; }
|
||||
.chat-send .chat-ico { display: none; }
|
||||
.chat-send .chat-ico svg { width: 19px; height: 19px; }
|
||||
|
||||
.chat-composer-note { margin: 8px 2px 0; font-size: 13px; line-height: 1.45; color: #fca5a5; }
|
||||
.chat-composer-note.is-info { color: var(--chat-muted); }
|
||||
|
||||
/* Vorschau der Anhaenge vor dem Senden */
|
||||
.chat-tray { display: flex; gap: 8px; padding: 2px 2px 10px; overflow-x: auto; scrollbar-width: thin; }
|
||||
.chat-tray-item {
|
||||
position: relative; flex: none; display: flex; align-items: center; gap: 10px; height: 64px; box-sizing: border-box;
|
||||
border-radius: 12px; border: 1px solid var(--chat-line); background: rgba(255, 255, 255, 0.05); overflow: hidden;
|
||||
}
|
||||
.chat-tray-item.is-image { width: 64px; }
|
||||
.chat-tray-item.is-image img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
||||
.chat-tray-item.is-image.is-queued img, .chat-tray-item.is-image.is-uploading img { opacity: 0.55; }
|
||||
.chat-tray-item.is-file { width: 220px; padding: 0 36px 0 8px; }
|
||||
.chat-tray-item.is-file.is-error { padding-right: 64px; }
|
||||
.chat-tray-item.is-voice { width: 280px; padding: 0 36px 0 10px; }
|
||||
.chat-tray-item.is-voice .chat-audio { width: 100%; padding: 0; }
|
||||
.chat-tray-progress { position: absolute; left: 0; bottom: 0; height: 3px; width: 0; background: var(--chat-accent); transition: width 0.2s; }
|
||||
.chat-tray-item.is-done .chat-tray-progress { opacity: 0; transition: opacity 0.6s 0.3s; }
|
||||
.chat-tray-item.is-error { border-color: rgba(248, 113, 113, 0.75); background: rgba(248, 113, 113, 0.08); }
|
||||
.chat-tray-item.is-error .chat-file-size { color: #fca5a5; opacity: 1; }
|
||||
.chat-tray-remove, .chat-tray-retry {
|
||||
position: absolute; top: 5px; width: 24px; height: 24px; padding: 0; border: 0; border-radius: 50%; display: grid; place-items: center;
|
||||
background: rgba(0, 0, 0, 0.72); color: #fff; cursor: pointer;
|
||||
}
|
||||
.chat-tray-remove { right: 5px; }
|
||||
.chat-tray-retry { right: 34px; }
|
||||
.chat-tray-item.is-image .chat-tray-retry { top: 50%; left: 50%; right: auto; width: 30px; height: 30px; transform: translate(-50%, -50%); }
|
||||
.chat-tray-remove .chat-ico svg, .chat-tray-retry .chat-ico svg { width: 14px; height: 14px; }
|
||||
|
||||
/* Sprachaufnahme (ersetzt waehrenddessen Textfeld und Anhang-Knopf) */
|
||||
.chat-rec {
|
||||
display: none; flex: 1; min-width: 0; align-items: center; gap: 10px; height: 46px; box-sizing: border-box; padding: 0 5px;
|
||||
border-radius: 999px; border: 1px solid rgba(248, 113, 113, 0.45); background: rgba(248, 113, 113, 0.08);
|
||||
}
|
||||
.chat-composer.is-recording .chat-rec { display: flex; }
|
||||
.chat-composer.is-recording .chat-attach,
|
||||
.chat-composer.is-recording textarea,
|
||||
.chat-composer.is-recording .chat-mic { display: none; }
|
||||
.chat-rec .chat-icon-btn { width: 36px; height: 36px; border: 0; background: transparent; }
|
||||
.chat-rec .chat-icon-btn:hover { background: rgba(255, 255, 255, 0.1); }
|
||||
.chat-rec-stop { color: #f87171; }
|
||||
.chat-rec-dot { flex: none; width: 10px; height: 10px; border-radius: 50%; background: #f87171; animation: chat-rec-pulse 1.2s ease-in-out infinite; }
|
||||
.chat-rec-label { flex: none; font-size: 13px; color: var(--chat-muted); }
|
||||
.chat-rec-time { flex: none; min-width: 36px; font-weight: 700; font-variant-numeric: tabular-nums; }
|
||||
.chat-rec-wave { flex: 1; min-width: 0; height: 24px; display: flex; align-items: center; justify-content: flex-end; gap: 2px; overflow: hidden; color: #fca5a5; }
|
||||
.chat-rec-wave i { flex: none; width: 3px; height: 100%; border-radius: 2px; background: currentColor; transform: scaleY(0.1); transition: transform 0.09s linear; }
|
||||
@keyframes chat-rec-pulse { 50% { opacity: 0.25; } }
|
||||
|
||||
/* Dateien in den Chat ziehen */
|
||||
.chat-dropzone { position: relative; }
|
||||
.chat-drop-active::after {
|
||||
content: 'Dateien hier ablegen'; position: absolute; inset: 0; z-index: 40; display: grid; place-items: center;
|
||||
border: 2px dashed var(--chat-accent-line); border-radius: inherit; background: rgba(10, 10, 12, 0.85);
|
||||
color: #fff; font-weight: 700; pointer-events: none;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------- Bildansicht und Kamera --- */
|
||||
html.chat-overlay-open, html.chat-overlay-open body { overflow: hidden; }
|
||||
.chat-overlay {
|
||||
position: fixed; inset: 0; z-index: 1000; display: flex; flex-direction: column;
|
||||
background: rgba(4, 4, 6, 0.95); color: #fff; font-family: inherit;
|
||||
}
|
||||
.chat-ov-top { flex: none; display: flex; align-items: center; gap: 10px; padding: 12px 14px; padding-top: max(12px, env(safe-area-inset-top)); }
|
||||
.chat-ov-title { flex: 1; min-width: 0; font-size: 14px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.chat-ov-btn {
|
||||
flex: none; display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-width: 44px; height: 44px; padding: 0 12px;
|
||||
box-sizing: border-box; border-radius: 999px; border: 1px solid rgba(255, 255, 255, 0.25); background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff !important; font: inherit; font-size: 14px; font-weight: 600; text-decoration: none; cursor: pointer;
|
||||
}
|
||||
.chat-ov-btn:hover { background: rgba(255, 255, 255, 0.2); }
|
||||
.chat-ov-btn:focus-visible, .chat-shutter:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }
|
||||
.chat-ov-btn.wide { padding: 0 18px; }
|
||||
.chat-ov-btn.primary { background: var(--chat-accent); border-color: transparent; color: var(--chat-on-accent) !important; }
|
||||
.chat-ov-btn.primary:hover { background: var(--chat-accent-hover); }
|
||||
|
||||
.chat-lightbox-stage { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; padding: 0 12px 16px; }
|
||||
.chat-lightbox-stage img { max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 6px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); }
|
||||
|
||||
.chat-cam-stage { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; padding: 0 12px; }
|
||||
.chat-cam-video, .chat-cam-still { max-width: 100%; max-height: 100%; border-radius: 14px; background: #000; }
|
||||
.chat-cam-video.is-mirrored { transform: scaleX(-1); }
|
||||
.chat-camera:not(.is-review) .chat-cam-still, .chat-camera.is-review .chat-cam-video { display: none; }
|
||||
.chat-cam-msgbox { max-width: 420px; display: grid; gap: 16px; justify-items: center; text-align: center; }
|
||||
.chat-cam-msg { margin: 0; line-height: 1.5; color: rgba(255, 255, 255, 0.88); }
|
||||
.chat-cam-controls {
|
||||
flex: none; display: flex; align-items: center; justify-content: center; gap: 24px;
|
||||
padding: 18px 16px; padding-bottom: max(18px, env(safe-area-inset-bottom));
|
||||
}
|
||||
.chat-camera .chat-cam-review, .chat-camera.is-review .chat-cam-live { display: none; }
|
||||
.chat-camera.is-review .chat-cam-review { display: flex; }
|
||||
.chat-cam-side { flex: 0 1 110px; display: flex; justify-content: center; }
|
||||
.chat-shutter {
|
||||
flex: none; width: 74px; height: 74px; padding: 0; border-radius: 50%; border: 4px solid #fff;
|
||||
background: rgba(255, 255, 255, 0.18); box-shadow: inset 0 0 0 4px rgba(0, 0, 0, 0.35); cursor: pointer;
|
||||
}
|
||||
.chat-shutter:hover:not(:disabled) { background: rgba(255, 255, 255, 0.35); }
|
||||
.chat-shutter:active:not(:disabled) { background: #fff; }
|
||||
.chat-shutter:disabled { opacity: 0.4; cursor: default; }
|
||||
|
||||
/* App-Ansicht (gleiche Bedingung wie in kundenbereich.css: Handy, Tablet hochkant, Handy quer):
|
||||
Senden als runder Knopf mit Symbol */
|
||||
@media (max-width: 880px), (pointer: coarse) and (max-height: 500px) {
|
||||
.chat-send { width: 46px; padding: 0; }
|
||||
.chat-send .chat-ico { display: inline-grid; }
|
||||
.chat-send .chat-send-label { display: none; }
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.chat-composer .chat-input-row { gap: 6px; }
|
||||
.chat-composer textarea { font-size: 16px; } /* iOS zoomt sonst beim Tippen hinein */
|
||||
.chat-icon-btn { width: 42px; height: 42px; }
|
||||
.chat-send { width: 42px; height: 42px; }
|
||||
.chat-rec { height: 42px; }
|
||||
.chat-rec-label { display: none; }
|
||||
.chat-tray-item.is-file { width: 190px; }
|
||||
.chat-tray-item.is-voice { width: 250px; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.chat-rec-dot { animation: none; }
|
||||
.chat-rec-wave i, .chat-tray-progress { transition: none; }
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
<link rel="stylesheet" href="/neu/tokens.css">
|
||||
<link rel="stylesheet" href="/neu/basis.css">
|
||||
<link rel="stylesheet" href="/neu/huelle.css">
|
||||
<link rel="stylesheet" href="/neu/chat-composer.css">
|
||||
<link rel="stylesheet" href="/neu/ansichten.css">
|
||||
<link rel="stylesheet" href="/neu/funktionen.css">
|
||||
<link rel="stylesheet" href="/neu/module.css">
|
||||
@@ -83,6 +84,7 @@
|
||||
<div id="kn-ansage" class="kn-nur-leser" aria-live="polite"></div>
|
||||
|
||||
<script src="/kundenansicht.js"></script>
|
||||
<script src="/chat-composer.js"></script>
|
||||
<script type="module" src="/neu/huelle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user