Design P6: chat.css neu (Praefix .woms-chat, Umschaltung bei 860 px), Fehlerzeile ohne Icon
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
308bf545da
commit
ea528486aa
3 files changed
+152
-31
No files matched your search
@@ -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 }) {
|
||||
<div className="woms-chat" ref={zoneRef}>
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 wrap">
|
||||
<span className="muted">{error}</span>
|
||||
<Button variant="ghost" onClick={() => setReloadKey((k) => k + 1)}><FaRotateRight /> Erneut versuchen</Button>
|
||||
<span className="msg err" role="alert">{error}</span>
|
||||
<Button onClick={() => setReloadKey((k) => k + 1)}>Erneut versuchen</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="chat-messages" ref={boxRef} aria-live="polite" />
|
||||
|
||||
+106
-28
@@ -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%; }
|
||||
}
|
||||
@@ -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('<div ref={composerRef} />'))
|
||||
})
|
||||
|
||||
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/)
|
||||
})
|
||||
Reference in new issue
Block a user