From ea528486aa8a9795d399f61561806f385955d373 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 11:42:45 +0000 Subject: [PATCH] Design P6: chat.css neu (Praefix .woms-chat, Umschaltung bei 860 px), Fehlerzeile ohne Icon Co-Authored-By: Claude Sonnet 5.5 --- src/components/chat/ChatThread.jsx | 5 +- src/styles/chat.css | 134 +++++++++++++++++++++++------ tests/unit/chatCss.test.mjs | 44 ++++++++++ 3 files changed, 152 insertions(+), 31 deletions(-) create mode 100644 tests/unit/chatCss.test.mjs diff --git a/src/components/chat/ChatThread.jsx b/src/components/chat/ChatThread.jsx index 609e32d..2bef943 100644 --- a/src/components/chat/ChatThread.jsx +++ b/src/components/chat/ChatThread.jsx @@ -1,5 +1,4 @@ import { useEffect, useRef, useState } from 'react' -import { FaRotateRight } from 'react-icons/fa6' import { Button } from '../ui' import { PORTAL_URL, chatUploadUrl, getChatMessages, markChatRead, sendChatMessage, teamAuthHeaders } from '../../lib/teamApi' import { loadChatComposer } from '../../lib/loadChatComposer' @@ -119,8 +118,8 @@ export default function ChatThread({ customerId, customerName }) {
{error && (
- {error} - + {error} +
)}
diff --git a/src/styles/chat.css b/src/styles/chat.css index 8a123bc..699f542 100644 --- a/src/styles/chat.css +++ b/src/styles/chat.css @@ -1,5 +1,12 @@ -/* Chat im Ticketsystem (Messenger und Reiter "Chat"). Eingabezeile, Anhaenge und Sprachnachrichten kommen aus - chat-composer.css des Kundenportals; hier Farben (--chat-*), Verlauf, Sprechblasen und die Liste. */ +/* Chat im Ticketsystem (Messenger und Reiter "Chat") im weissen Design. + Eingabezeile, Anhaenge und Sprachnachrichten kommen als Baustein aus dem Kundenportal (chat-composer.css, window.ChatComposer). + Dessen CSS laedt zur Laufzeit zuletzt, bei gleicher Spezifitaet gewinnt es. Darum ist jede Regel fuer Elemente des Bausteins + strikt spezifischer als die Portal-Regel (Praefix .woms-chat, Spezifitaeten in den Kommentaren: Eigen > Fremd) und es gibt kein + !important. Einzige Ausnahme mit Absicht: .chat-tray-item, dessen rote Fehlervariante des Portals (gleiche Spezifitaet, spaeter + geladen) gewinnen soll. Das Seitenlayout (.chat-page, .chat-side, .chat-list*, .chat-main, .chat-back) hat kein Praefix. + Bildansicht und Kamera haengen am body und bleiben dunkel (Medienbetrachter, vom Praefix nicht erreichbar). */ + +/* Variablen des Bausteins: weisses Design statt Portal-Lila (P1), dazu Blasen und Spur der Audioleiste (P6) */ .woms-chat { --chat-accent: var(--accent); --chat-accent-hover: var(--accent-hover); @@ -10,50 +17,121 @@ --chat-surface: var(--bg); --chat-line: var(--line-strong); --chat-muted: var(--text-2); + --chat-bubble-in: var(--bg-hover); + --chat-bubble-out: var(--grey-bg); + --chat-track: rgba(26, 31, 54, 0.2); display: flex; flex-direction: column; gap: var(--sp-2); min-width: 0; } + +/* Verlauf: scrollt in sich, ohne Rand und Hintergrund */ .woms-chat .chat-messages { - display: flex; flex-direction: column; gap: 8px; min-height: 280px; max-height: 60vh; overflow-y: auto; - padding: 12px; border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--bg); + display: flex; flex-direction: column; gap: 8px; min-height: 280px; max-height: 60vh; overflow-y: auto; padding: 4px 0; } + +/* Blasen: fremde hellgrau, eigene etwas dunkler und rechts, kein Rand. .chat-bubble 0,2,0 > 0,1,0 */ .woms-chat .chat-bubble { - align-self: flex-start; max-width: 78%; padding: 8px 12px; border-radius: 14px; - background: var(--surface-2); color: var(--text); overflow-wrap: anywhere; + align-self: flex-start; max-width: 78%; padding: 8px 12px; border: 0; border-radius: var(--radius-lg); + background: var(--chat-bubble-in); color: var(--text); overflow-wrap: anywhere; } -.woms-chat .chat-bubble.mine { align-self: flex-end; background: var(--accent-soft); border: 1px solid var(--line); } -.woms-chat .chat-meta { display: block; margin-top: 4px; font-size: 11.5px; color: var(--text-muted); } -.woms-chat .chat-notice { font-size: 12px; } -/* Meldungen des Eingabebausteins (Stile aus chat-composer.css des Portals): Fehler rot, Info grau, beides lesbar auf Weiss */ +.woms-chat .chat-bubble.mine { align-self: flex-end; background: var(--chat-bubble-out); } +.woms-chat .chat-meta { display: block; margin-top: 4px; font-size: 12px; color: var(--text-2); } +.woms-chat .chat-notice { font-size: 12px; color: var(--text-2); } + +/* Datei-Chip: weiss mit Ring (die Blase ist selbst --bg-hover). .chat-att-file 0,2,0 > 0,1,0, :hover 0,3,0 > 0,2,0 */ +.woms-chat .chat-att-file { border: 0; background: var(--bg); box-shadow: var(--ring); } +.woms-chat .chat-att-file:hover { background: var(--bg-hover); } + +/* Audioleiste und Fokus. .chat-audio-bar 0,2,0 > 0,1,0; :focus-visible 0,3,0 > 0,2,0 (Portal: weisser Ring) */ +.woms-chat .chat-audio-bar { background: var(--chat-track); } +.woms-chat .chat-att-image:focus-visible, +.woms-chat .chat-audio-bar:focus-visible { outline: 2px solid var(--accent); } +/* Abspielen in der eigenen Blase: schwarz mit weisser Schrift statt Portal-Weiss. 0,4,0 > 0,3,0 */ +.woms-chat .chat-bubble.mine .chat-audio-play { background: var(--chat-accent); color: var(--chat-on-accent); } +.woms-chat .chat-audio-dl:hover { background: var(--accent-soft); } + +/* Meldungen des Bausteins: Fehler rot (--red-fg), Info grau. .chat-composer-note 0,2,0 > 0,1,0; die Zeitanzeige 0,4,0 > 0,3,0 */ .woms-chat .chat-composer-note, .woms-chat .chat-tray-item.is-error .chat-file-size, .woms-chat .chat-audio.is-error .chat-audio-time { color: var(--red-fg); } .woms-chat .chat-composer-note.is-info { color: var(--text-2); } -/* Eingabefeld: Grundstil wie im frueheren Admin-Panel (chat-composer.css setzt nur das Layout) */ -.woms-chat .chat-input-row textarea { - resize: none; min-height: 46px; max-height: 130px; padding: 11px 14px; border-radius: 14px; - border: 1px solid var(--border); background: var(--surface-2); color: var(--text); font: inherit; line-height: 1.4; -} -.woms-chat .chat-input-row textarea:focus { outline: none; border-color: var(--accent); } -.chat-page { display: grid; grid-template-columns: 320px minmax(0, 1fr); gap: var(--sp-4); align-items: start; } -.chat-side { display: flex; flex-direction: column; gap: var(--sp-3); min-width: 0; } +/* Eingabefeld: Ring statt Rand, Hoehe und Radius wie bisher. 0,2,1 > 0,1,1; :focus 0,3,1 */ +.woms-chat .chat-input-row textarea { + resize: none; min-height: 46px; max-height: 130px; padding: 11px 14px; border: 0; border-radius: 14px; + background: var(--bg); color: var(--text); box-shadow: var(--ring); font: inherit; line-height: 1.4; +} +.woms-chat .chat-input-row textarea:focus { outline: none; box-shadow: 0 0 0 1px var(--accent), 0 0 0 4px var(--accent-ring); } +/* "font: inherit" hebt die Portal-Regel gegen das iOS-Zoomen auf: am Handy wieder 16 px */ +@media (max-width: 640px) { + .woms-chat .chat-input-row textarea { font-size: 16px; } +} + +/* Icon-Knoepfe (Anhang, Mikrofon) und Senden. .chat-icon-btn 0,2,0 > 0,1,0; :hover:not(:disabled) 0,4,0 > 0,3,0; + [aria-expanded] 0,3,0 > 0,2,0; :disabled 0,3,0 > 0,2,0; .chat-send.is-busy:disabled 0,4,0 > 0,3,0. + Senden bleibt ueber --chat-accent (P1) schwarz mit weisser Schrift. */ +.woms-chat .chat-icon-btn { border: 0; background: var(--bg); color: var(--text); box-shadow: var(--ring); } +.woms-chat .chat-icon-btn:hover:not(:disabled) { background: var(--bg-hover); } +.woms-chat .chat-icon-btn[aria-expanded="true"], +.woms-chat .chat-icon-btn[aria-expanded="true"]:hover:not(:disabled) { background: var(--accent-soft); box-shadow: 0 0 0 1px var(--accent); } +.woms-chat .chat-icon-btn:disabled, +.woms-chat .chat-send:disabled { opacity: 0.55; cursor: not-allowed; } +.woms-chat .chat-send.is-busy:disabled { opacity: 0.8; cursor: progress; } + +/* Sprachaufnahme: Knoepfe ohne Ring und Hintergrund, Rot ueber --danger. .chat-rec .chat-icon-btn 0,3,0 > 0,2,0; :hover 0,4,0 > 0,3,0. + .chat-rec-stop steht hinter der Icon-Knopf-Regel (gleiche Spezifitaet 0,2,0), sonst gewinnt deren Farbe. */ +.woms-chat .chat-rec { border-color: var(--danger-ring); background: var(--red-bg); } +.woms-chat .chat-rec .chat-icon-btn { background: transparent; box-shadow: none; } +.woms-chat .chat-rec .chat-icon-btn:hover { background: var(--accent-soft); } +.woms-chat .chat-rec-stop { color: var(--danger); } +.woms-chat .chat-rec-wave { color: var(--danger); } +.woms-chat .chat-rec-dot { background: var(--danger); } + +/* Anhang-Menue: ohne Rand, mit Schatten; Hover und Tastaturfokus --bg-hover, Fokus zusaetzlich mit Umriss (Portal: outline none). + .chat-menu 0,2,0 > 0,1,0; .chat-menu button 0,2,1 > 0,1,1; :hover/:focus-visible 0,3,1 > 0,2,1 */ +.woms-chat .chat-menu { border: 0; border-radius: var(--radius-lg); box-shadow: var(--shadow-pop); } +.woms-chat .chat-menu button { border-radius: var(--radius); } +.woms-chat .chat-menu button:hover, +.woms-chat .chat-menu button:focus-visible { background: var(--bg-hover); } +.woms-chat .chat-menu button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; } + +/* Vorschau der Anhaenge vor dem Senden. .chat-tray-item 0,2,0 = Portal-Fehlervariante .chat-tray-item.is-error (0,2,0, spaeter + geladen): die rote Variante bleibt absichtlich. */ +.woms-chat .chat-tray-item { border: 1px solid transparent; background: var(--bg-hover); } + +/* Dateien ablegen. .chat-drop-active::after 0,2,1 > 0,1,1 */ +.woms-chat.chat-drop-active::after { background: rgba(255, 255, 255, 0.92); color: var(--text); border-color: var(--text-3); } + +/* ------------------------------------------------------------------ Seite: Liste und Verlauf (ohne Praefix) --- */ +.chat-page { display: grid; grid-template-columns: 320px minmax(0, 1fr); align-items: stretch; } +.chat-side { display: flex; flex-direction: column; gap: var(--sp-3); min-width: 0; padding-right: var(--sp-4); border-right: 1px solid var(--line); } +.chat-main { min-width: 0; padding-left: var(--sp-5); } .chat-list { display: flex; flex-direction: column; max-height: 70vh; overflow-y: auto; } .chat-list-item { - display: grid; gap: 2px; padding: 10px 14px; border: none; border-bottom: 1px solid var(--border); + display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: baseline; gap: 2px 8px; + width: 100%; padding: 10px 8px; border: 0; border-bottom: 1px solid var(--line); border-radius: 0; background: transparent; color: var(--text); font: inherit; text-align: left; cursor: pointer; } -.chat-list-item:hover { background: var(--surface-2); } -.chat-list-item.active { background: var(--accent-soft); } -.chat-list-top { display: flex; align-items: center; gap: 8px; justify-content: space-between; } -.chat-list-last { color: var(--text-muted); font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.chat-list-time { color: var(--text-faint); font-size: 11.5px; } -.chat-main { min-width: 0; } -.chat-back { display: none; } +.chat-list-item:first-child { border-top: 1px solid var(--line); } +.chat-list-item:hover { background: var(--bg-subtle); } +.chat-list-item[aria-current="true"] { background: var(--accent-soft); } +.chat-list-top { grid-column: 1 / -1; display: flex; align-items: baseline; justify-content: space-between; gap: 8px; min-width: 0; } +.chat-list-top strong { min-width: 0; font-weight: 600; overflow-wrap: anywhere; } +.chat-list-last { grid-column: 1; min-width: 0; color: var(--text-2); font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.chat-list-time { flex: none; color: var(--text-2); font-size: 12px; } +.chat-unread { grid-column: 2; color: var(--accent); font-size: 12px; font-weight: 700; } +.chat-page .chat-back { display: none; } -@media (max-width: 720px) { +@media (max-width: 1100px) { + .chat-page { grid-template-columns: 260px minmax(0, 1fr); } +} + +/* Handy: eine Spalte, entweder die Liste oder der Verlauf. .chat-page .chat-back 0,2,0 > .btn 0,1,0 */ +@media (max-width: 860px) { .chat-page { grid-template-columns: minmax(0, 1fr); } .chat-page.has-thread .chat-side { display: none; } .chat-page:not(.has-thread) .chat-main { display: none; } - .chat-back { display: inline-flex; } + .chat-page .chat-side, + .chat-page .chat-main { padding: 0; border: 0; } + .chat-page .chat-back { display: inline-flex; margin-bottom: var(--sp-3); } .woms-chat .chat-bubble { max-width: 90%; } } diff --git a/tests/unit/chatCss.test.mjs b/tests/unit/chatCss.test.mjs new file mode 100644 index 0000000..b750c71 --- /dev/null +++ b/tests/unit/chatCss.test.mjs @@ -0,0 +1,44 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..') +const css = readFileSync(path.join(root, 'src/styles/chat.css'), 'utf8') +const thread = readFileSync(path.join(root, 'src/components/chat/ChatThread.jsx'), 'utf8') + +// Quelltext ohne Kommentare: der Kopfkommentar darf "!important" und "720 px" nennen, ohne die Prüfungen zu reißen +const rules = css.replace(/\/\*[\s\S]*?\*\//g, '') +// Alle Selektoren (ohne @-Regeln), Kommas gesplittet +const selectors = [...rules.matchAll(/([^{}]+)\{/g)] + .map((m) => m[1].trim()) + .filter((s) => s && !s.startsWith('@')) + .flatMap((s) => s.split(',').map((x) => x.trim())) + +// Fremde Klassen des Portal-Bausteins (chat-composer.css): jede Regel darüber braucht das Präfix .woms-chat +const FREMD = /\.chat-(bubble|meta|messages|notice|input-row|icon-btn|send|menu|tray|rec|audio|att|file|composer|drop-active)\b/ + +test('chat.css: jede Regel für Klassen des Portal-Bausteins beginnt mit .woms-chat', () => { + const bad = selectors.filter((s) => FREMD.test(s) && !s.startsWith('.woms-chat')) + assert.deepEqual(bad, []) +}) + +test('chat.css: kein !important (Spezifität statt Gewalt, Portal-CSS lädt zuletzt)', () => { + assert.doesNotMatch(rules, /!important/) +}) + +test('chat.css: Umschaltung Liste/Verlauf bei 860 px, kein 720-px-Wert mehr', () => { + assert.match(rules, /@media \(max-width: 860px\)/) + assert.doesNotMatch(rules, /720px/) +}) + +test('ChatThread: die drei Container des Portal-Bausteins bleiben unverändert', () => { + assert.ok(thread.includes('className="woms-chat" ref={zoneRef}')) + assert.ok(thread.includes('className="chat-messages" ref={boxRef}')) + assert.ok(thread.includes('
')) +}) + +test('chat.css: .chat-page .chat-back ist außerhalb der Handyansicht verborgen (schlägt .btn aus global.css)', () => { + assert.match(rules, /\.chat-page \.chat-back\s*\{[^}]*display:\s*none/) +})