Files
ClaudeandClaude Sonnet 5.5 670ead3091 Kundenansicht neu Chat-Fix: Chat als Vollbild-Ansicht (Verlauf scrollt in sich, Eingabe unten, Spalte zentriert, "Chat" klein im Kopf)
Pilot-Feedback 17.10.: Der Chat wirkte durchsichtig und verrutscht.
- Handy: Inhalt steht fest zwischen Kopf und Leiste (wie das Blatt); die Eingabe sitzt höchstens 6 px über der Leiste bzw. dem Widerrufsstreifen und bei
  offener Tastatur am unteren Rand des sichtbaren Bereichs. "Chat" steht klein im Kopf (die H1 bleibt nur für Screenreader), der Kopf ist deckend mit Haarlinie,
  der Verlauf beginnt direkt darunter und scrollt in sich.
- Desktop: Spalte in Fensterhöhe, Chat höchstens 760 px breit und mittig, Verlauf füllt die Höhe, Eingabe am unteren Spaltenrand.
- Verlauf steht unten (neueste Nachricht sichtbar), kurze Verläufe unten bei der Eingabe; Blasen fremd #f8f9fb, eigen #ebeef1.
- "Senden" bleibt schwarz, auch bei leerem Feld (gesperrt im Ansichtsmodus weiter gedämpft).
- haltFokus: Druck auf Senden, Anhang oder Mikrofon nimmt dem Textfeld den Fokus nicht; sonst kommt die Leiste zurück, schiebt die feste Eingabe hoch und der
  Tipp auf "Senden" geht bei offener Tastatur verloren (im Browser nachgestellt). public/chat-composer.js und die alte Ansicht bleiben unverändert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:17:26 +00:00

82 lines
5.3 KiB
JavaScript

import { test } from 'node:test'
import assert from 'node:assert/strict'
import { haltFokus } from '../public/neu/ansicht-chat.js'
import { lies, handyTeil } from './support/knHuelle.js'
// Chat als Vollbild-Ansicht (Pilot-Feedback 17.10.: „durchsichtig und verrutscht“). Die Lage prüfen die Ansichten kn-p2-chat-* des Werkzeugs im Browser;
// diese Pins lesen nur den Quelltext und halten die Regeln fest, an denen die Lage hängt.
const ANSICHTEN = lies('ansichten.css')
const HUELLE = lies('huelle.css')
const regel = (css, selektor) => {
const m = css.match(new RegExp(`(?:^|\\n)${selektor.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\{([^}]*)\\}`))
return m ? m[1] : ''
}
const chatBlock = () => HUELLE.slice(HUELLE.indexOf('Chat: Vollbild-Ansicht'))
test('Chat: der Verlauf füllt die Höhe und scrollt in sich, ohne feste Höhengrenze (kein 60vh/55vh mehr)', () => {
const verlauf = regel(ANSICHTEN, '.kn .kn-chat .chat-messages')
assert.match(verlauf, /flex:\s*1 1 0/)
assert.match(verlauf, /min-height:\s*0/)
assert.match(verlauf, /overflow-y:\s*auto/)
assert.doesNotMatch(verlauf, /max-height/)
assert.doesNotMatch(ANSICHTEN, /\.chat-messages\s*\{[^}]*(?:min-height:\s*(?:240|280)px|max-height:\s*\d+vh)/)
assert.match(ANSICHTEN, /\.kn \.kn-chat \.chat-messages > :first-child\s*\{\s*margin-top:\s*auto/) // kurze Verläufe stehen unten bei der Eingabe
})
test('Chat: Spalte höchstens 760 px, mittig; Eingabe fest unten', () => {
const spalte = ANSICHTEN.match(/\n\.kn-chat \{[^}]*\}/)[0]
assert.match(spalte, /max-width:\s*760px/)
assert.match(spalte, /margin-inline:\s*auto/)
assert.match(regel(ANSICHTEN, '.kn .kn-chat .kn-chat-composer'), /flex:\s*none/)
assert.match(regel(HUELLE, '.kn-haupt > #bereich-chat:not([hidden])'), /max-width:\s*760px[^}]*margin:\s*0 auto/)
})
test('Chat: Blasen ruhig (Fläche #f8f9fb für fremde, #ebeef1 für eigene), über Tokens', () => {
assert.match(ANSICHTEN, /--chat-bubble-in:\s*var\(--kn-bg-subtle\)/)
assert.match(ANSICHTEN, /--chat-bubble-out:\s*var\(--kn-grau-bg\)/)
const tokens = lies('tokens.css')
assert.match(tokens, /--kn-bg-subtle:\s*#f8f9fb/)
assert.match(tokens, /--kn-grau-bg:\s*#ebeef1/)
})
test('Chat: „Senden“ bleibt der schwarze Hauptknopf, auch bei leerem Feld; gesperrt (Ansichtsmodus) und beim Senden bleibt er gedämpft', () => {
assert.match(regel(ANSICHTEN, '.kn .kn-chat .chat-composer:not(.is-disabled) .chat-send:disabled:not(.is-busy)'), /opacity:\s*1\b/)
assert.match(regel(ANSICHTEN, '.kn .kn-chat .chat-icon-btn:disabled, .kn .kn-chat .chat-send:disabled'), /opacity:\s*\.55/)
assert.match(regel(ANSICHTEN, '.kn .kn-chat .chat-send.is-busy:disabled'), /opacity:\s*\.8/)
})
test('Chat in der Hülle: Fensterhöhe am Desktop, am Handy fest zwischen Kopf und Leiste, „Chat“ klein im Kopf statt große Überschrift', () => {
const block = chatBlock()
assert.match(regel(block, '.kn-spalte:has(#bereich-chat:not([hidden]))'), /display:\s*flex[^}]*height:\s*calc\(100dvh - var\(--kn-banner-h\)\)[^}]*min-height:\s*0/)
assert.match(regel(block, '#chat-body'), /flex:\s*1 1 auto[^}]*min-height:\s*0/)
const handy = handyTeil(block)
assert.match(handy, /\.kn-haupt:has\(#bereich-chat:not\(\[hidden\]\)\)\s*\{[^}]*position:\s*fixed[^}]*inset:\s*calc\(var\(--kn-banner-h\) \+ var\(--kn-kopf-h\) \+ env\(safe-area-inset-top\)\) 0 var\(--kn-unten, var\(--kn-zeile-h\)\)/)
assert.match(handy, /\.kn-haupt:has\(#bereich-chat:not\(\[hidden\]\)\)\s*\{[^}]*padding:\s*0 max\(16px, env\(safe-area-inset-right\)\) 6px max\(16px, env\(safe-area-inset-left\)\)/) // höchstens 8 px Luft über der Leiste
assert.match(handy, /#bereich-chat > \.kn-titel\s*\{[^}]*position:\s*absolute[^}]*width:\s*1px[^}]*height:\s*1px[^}]*clip:\s*rect\(0 0 0 0\)/) // nur noch für Screenreader
assert.match(handy, /\.kn:has\(#bereich-chat:not\(\[hidden\]\)\) \.kn-kopf\s*\{[^}]*height:[^}]*border-bottom-color:\s*var\(--kn-line\)/) // deckender Kopf mit Haarlinie
assert.match(handy, /\.kn:has\(#bereich-chat:not\(\[hidden\]\)\) \.kn-kopf-titel\s*\{[^}]*opacity:\s*1[^}]*transform:\s*none/)
assert.doesNotMatch(block, /backdrop-filter|gradient|rgba\(/) // deckend, kein Glas
})
test('haltFokus: Druck auf einen Knopf im Composer nimmt dem Eingabefeld den Fokus nicht (sonst springt die Eingabe, wenn die Leiste zurückkommt, und der Tipp auf „Senden“ geht verloren)', () => {
const hoerer = new Map()
const knopf = { matches: (s) => s === 'button' }
const wurzel = {
addEventListener: (n, f) => hoerer.set(n, f),
removeEventListener: (n) => hoerer.delete(n),
contains: (el) => el === knopf || el === symbol,
}
const symbol = { closest: (s) => (s === 'button' ? knopf : null) } // das Symbol im Knopf ist das Ziel des Drucks
const feld = { closest: () => null }
const draussen = { closest: () => ({}) } // ein Knopf, der nicht im Composer liegt
const h = haltFokus(wurzel)
assert.deepEqual([...hoerer.keys()], ['mousedown'])
const druck = (target) => { let verhindert = false; hoerer.get('mousedown')({ target, preventDefault: () => { verhindert = true } }); return verhindert }
assert.equal(druck(symbol), true, 'Knopf im Composer: Fokus bleibt')
assert.equal(druck(feld), false, 'Textfeld: normal')
assert.equal(druck(draussen), false, 'Knopf außerhalb: normal')
assert.equal(druck(undefined), false)
h.abbau()
assert.equal(hoerer.size, 0)
})