fix(ui): Beschriftungszeile mit Diktatknopf richtet die Felder daneben aus

Im Abschnitt "Verlauf" der Opportunity begann "Was wurde besprochen?"
tiefer als "Letzte Interaktion" daneben. Ursache war die
Beschriftungszeile: rechts steht ein Diktatknopf darin, links nur Text -
die Zeile war damit 26,5 statt 16,5 Pixel hoch und schob das Eingabefeld
nach unten.

Der Knopf bekommt eine feste Hoehe (h-7, 28 px) statt sich aus
Zeilenhoehe, Innenabstand und Rahmen zu ergeben; auf 26,5 px laesst sich
keine Zeile ausrichten. Dazu labelRowClass mit min-h-7: damit ist die
Zeile in beiden Spalten gleich hoch, mit Knopf oder ohne. Gemessen sind
es jetzt 0,0 px Abweichung statt 8.

Zweiter Fund derselben Stelle: die drei Beschriftungen im Dialog waren
von Hand abgeschrieben statt labelClass zu nehmen. Damit fehlte ihnen die
Klasse "field-label", die seit den Themes die Schrift der
Feldbeschriftungen traegt - in "Pixel Art" blieben sie ohne
Pixelschrift. Die Auszeichnung steht jetzt einmal in leadUi.tsx
(labelTextClass) und wird nicht mehr kopiert; DictateTextarea benutzt
sie ebenso.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sascha Wolf
2026-09-09 15:30:35 +02:00
co-authored by Claude Opus 5
parent dc701f8522
commit ab6b9f2372
5 changed files with 50 additions and 21 deletions
+2 -1
View File
@@ -227,7 +227,8 @@ Browser-Download.
neues Theme stehen in [05 Frontend](05-frontend.md#themes-ohne-dark-varianten) - neues Theme stehen in [05 Frontend](05-frontend.md#themes-ohne-dark-varianten) -
wichtigste davon: auf `brand-500`/`brand-600` steht weisse Schrift. wichtigste davon: auf `brand-500`/`brand-600` steht weisse Schrift.
* **Gemeinsame Klassen** statt kopierter Utility-Ketten: `inputClass`, * **Gemeinsame Klassen** statt kopierter Utility-Ketten: `inputClass`,
`labelClass` aus `components/leads/leadUi.tsx`, dazu `cardClass`, `labelClass`, `labelTextClass`, `labelRowClass` aus
`components/leads/leadUi.tsx`, dazu `cardClass`,
`primaryButtonClass` aus `components/scanner/scannerUi.tsx`. `primaryButtonClass` aus `components/scanner/scannerUi.tsx`.
* **Meldungen als Toast**, Rückfragen über `ConfirmDialog` - nie `alert()` * **Meldungen als Toast**, Rückfragen über `ConfirmDialog` - nie `alert()`
oder `confirm()`. Rot (`tone="danger"`, Standard) nur, wenn etwas verloren oder `confirm()`. Rot (`tone="danger"`, Standard) nur, wenn etwas verloren
+1 -1
View File
@@ -278,7 +278,7 @@ Warum genau diese Konstanten festgelegt sind, steht in
| `components/acquisition/AcquisitionTrash.tsx` | Papierkorb der Akquise: zurückholen, endgültig löschen, leeren. Die Meldung nach dem Zurückholen nennt das Ziel (Eingang oder Archiv) - das Funddatum bleibt, also landet ein alter Fund im Archiv | | `components/acquisition/AcquisitionTrash.tsx` | Papierkorb der Akquise: zurückholen, endgültig löschen, leeren. Die Meldung nach dem Zurückholen nennt das Ziel (Eingang oder Archiv) - das Funddatum bleibt, also landet ein alter Fund im Archiv |
| `components/acquisition/acquisitionUi.tsx` | `fieldClass` (abgesetzte Felder) und `OptionSelect` - zeigt einen Wert, der nicht mehr in der Liste steht, als solchen; `alwaysOffer` hält den eigenen Namen für *Von* immer wählbar. *Anbieter* nutzt es nicht: Freitext mit `<datalist>`. Dazu `ConsultantSelect` (mehrere Berater: Auswahlfeld hängt an, Plaketten nehmen weg) und `ConsultantList` für die Anzeige in der Liste | | `components/acquisition/acquisitionUi.tsx` | `fieldClass` (abgesetzte Felder) und `OptionSelect` - zeigt einen Wert, der nicht mehr in der Liste steht, als solchen; `alwaysOffer` hält den eigenen Namen für *Von* immer wählbar. *Anbieter* nutzt es nicht: Freitext mit `<datalist>`. Dazu `ConsultantSelect` (mehrere Berater: Auswahlfeld hängt an, Plaketten nehmen weg) und `ConsultantList` für die Anzeige in der Liste |
| `components/settings/AcquisitionSettings.tsx` | Reiter *Einstellungen → Akquise*: die vier Auswahllisten pflegen | | `components/settings/AcquisitionSettings.tsx` | Reiter *Einstellungen → Akquise*: die vier Auswahllisten pflegen |
| `components/leads/leadUi.tsx` | `inputClass`, `labelClass`, `KindBadge`, `Avatar`, Farben der Kontaktart | | `components/leads/leadUi.tsx` | `inputClass`, `labelClass` (Beschriftung als eigene Zeile), `labelTextClass` + `labelRowClass` (Zeile mit Knopf rechts, z. B. Diktat - `min-h-7` haelt sie so hoch wie den Knopf, damit Felder in Nachbarspalten auf einer Linie beginnen), `KindBadge`, `Avatar`, Farben der Kontaktart |
| `components/leads/mailPool.ts` | `findMailPoolMatch()`, `diffFields()`, `isConflict()`, `MAIL_POOL_FIELDS` - Dublettenabgleich des Vorbereichs | | `components/leads/mailPool.ts` | `findMailPoolMatch()`, `diffFields()`, `isConflict()`, `MAIL_POOL_FIELDS` - Dublettenabgleich des Vorbereichs |
| `components/leads/MailPoolLeads.tsx` | Ansicht „Aus Mails": Goldmarkierung, Abweichungen, Übernehmen, Sammelübernahme | | `components/leads/MailPoolLeads.tsx` | Ansicht „Aus Mails": Goldmarkierung, Abweichungen, Übernehmen, Sammelübernahme |
| `components/leads/MailPoolMergeDialog.tsx` | Feldauswahl beim Ergänzen eines vorhandenen Kontakts | | `components/leads/MailPoolMergeDialog.tsx` | Feldauswahl beim Ergänzen eines vorhandenen Kontakts |
+18 -3
View File
@@ -32,10 +32,25 @@ export const inputClass =
/* /*
* `field-label` ist keine Utility-Klasse, sondern ein Anker: darueber gibt ein * `field-label` ist keine Utility-Klasse, sondern ein Anker: darueber gibt ein
* Theme allen Feldbeschriftungen eine andere Schrift (siehe index.css). Ohne * Theme allen Feldbeschriftungen eine andere Schrift (siehe index.css). Ohne
* sie muesste jedes Label im Markup angefasst werden. * sie muesste jedes Label im Markup angefasst werden - genau deshalb steht die
* Auszeichnung hier einmal und wird nicht abgeschrieben.
*/ */
export const labelClass = export const labelTextClass =
'field-label mb-1.5 block text-[11px] font-extrabold uppercase tracking-wider text-ink-500'; 'field-label text-[11px] font-extrabold uppercase tracking-wider text-ink-500';
/** Beschriftung als eigene Zeile - der Normalfall. */
export const labelClass = `${labelTextClass} mb-1.5 block`;
/**
* Beschriftungszeile mit Platz fuer einen Knopf rechts (Diktat).
*
* `min-h-7` ist der Punkt: der Diktatknopf ist 28 px hoch, eine nackte
* Beschriftung nur 16,5 px. Ohne die Mindesthoehe beginnt das Eingabefeld
* daneben ueber 10 px tiefer als das in der Nachbarspalte - im Abschnitt
* "Verlauf" der Opportunity war genau das zu sehen. Die Hoehe des Knopfes ist
* dafuer in DictationField.tsx auf denselben Wert festgelegt.
*/
export const labelRowClass = 'mb-1.5 flex min-h-7 items-center justify-between gap-3';
export const Avatar: React.FC<{ export const Avatar: React.FC<{
src?: string | null; src?: string | null;
@@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useState } from 'react';
import { Modal } from '../ui/Modal'; import { Modal } from '../ui/Modal';
import { LeadModal } from '../leads/LeadModal'; import { LeadModal } from '../leads/LeadModal';
import { DictateTextarea, DictateButton, DictationHint } from '../ui/DictationField'; import { DictateTextarea, DictateButton, DictationHint } from '../ui/DictationField';
import { Avatar, inputClass, labelClass } from '../leads/leadUi'; import { Avatar, inputClass, labelClass, labelRowClass, labelTextClass } from '../leads/leadUi';
import { STAGE_COLORS, STAGE_HINTS, STAGE_LABELS } from './opportunityUi'; import { STAGE_COLORS, STAGE_HINTS, STAGE_LABELS } from './opportunityUi';
import { useToast } from '../Toast'; import { useToast } from '../Toast';
import { acquisitionOptionsApi, opportunitiesApi } from '../../lib/api'; import { acquisitionOptionsApi, opportunitiesApi } from '../../lib/api';
@@ -590,9 +590,16 @@ export const OpportunityModal: React.FC<OpportunityModalProps> = ({
</h3> </h3>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div> <div>
<label className={labelClass} htmlFor="opp-interaction"> {/*
Dieselbe Zeile wie beim Feld daneben (labelRowClass), obwohl hier
kein Knopf steht: sonst beginnt das Datumsfeld 8 px hoeher als
die Notiz rechts - der Diktatknopf macht die Nachbarzeile hoeher.
*/}
<div className={labelRowClass}>
<label className={labelTextClass} htmlFor="opp-interaction">
Letzte Interaktion Letzte Interaktion
</label> </label>
</div>
<input <input
id="opp-interaction" id="opp-interaction"
type="date" type="date"
@@ -602,8 +609,8 @@ export const OpportunityModal: React.FC<OpportunityModalProps> = ({
/> />
</div> </div>
<div className="sm:col-span-2"> <div className="sm:col-span-2">
<div className="mb-1.5 flex items-center justify-between gap-3"> <div className={labelRowClass}>
<label className="block text-[11px] font-extrabold uppercase tracking-wider text-ink-500" htmlFor="opp-interaction-note"> <label className={labelTextClass} htmlFor="opp-interaction-note">
Was wurde besprochen? Was wurde besprochen?
</label> </label>
<DictateButton <DictateButton
@@ -631,8 +638,8 @@ export const OpportunityModal: React.FC<OpportunityModalProps> = ({
</div> </div>
<div className="mt-4"> <div className="mt-4">
<div className="mb-1.5 flex items-center justify-between gap-3"> <div className={labelRowClass}>
<label className="block text-[11px] font-extrabold uppercase tracking-wider text-ink-500" htmlFor="opp-next"> <label className={labelTextClass} htmlFor="opp-next">
Nächster Schritt Nächster Schritt
</label> </label>
<DictateButton <DictateButton
+14 -8
View File
@@ -3,7 +3,7 @@ import { useRecorder } from '../../lib/dictation';
import { useWhisper } from '../../lib/whisper'; import { useWhisper } from '../../lib/whisper';
import { useNavigation } from '../../lib/navigation'; import { useNavigation } from '../../lib/navigation';
import { useToast } from '../Toast'; import { useToast } from '../Toast';
import { inputClass } from '../leads/leadUi'; import { inputClass, labelRowClass, labelTextClass } from '../leads/leadUi';
const MIC_ICON = const MIC_ICON =
'M19 11a7 7 0 01-14 0m7 7v3m0-3a7 7 0 007-7m-7 7a7 7 0 01-7-7m7-9a3 3 0 013 3v6a3 3 0 01-6 0V5a3 3 0 013-3z'; 'M19 11a7 7 0 01-14 0m7 7v3m0-3a7 7 0 007-7m-7 7a7 7 0 01-7-7m7-9a3 3 0 013 3v6a3 3 0 01-6 0V5a3 3 0 013-3z';
@@ -52,7 +52,9 @@ export const DictateButton: React.FC<DictateButtonProps> = ({
goTo('Einstellungen', { tab: 'whisper' }); goTo('Einstellungen', { tab: 'whisper' });
}} }}
title="Das Spracherkennungsmodell ist noch nicht geladen - hier einrichten" title="Das Spracherkennungsmodell ist noch nicht geladen - hier einrichten"
className="flex items-center gap-1.5 rounded-lg border border-dashed border-ink-300 px-2 py-1 text-[11px] font-bold text-ink-400 transition hover:border-brand-400 hover:text-brand-700" // Dieselbe Hoehe wie der Diktatknopf, damit die Beschriftungszeile in
// beiden Zustaenden gleich hoch bleibt.
className="flex h-7 items-center gap-1.5 rounded-lg border border-dashed border-ink-300 px-2 text-[11px] font-bold text-ink-400 transition hover:border-brand-400 hover:text-brand-700"
> >
<svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={MIC_ICON} /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={MIC_ICON} />
@@ -79,7 +81,14 @@ export const DictateButton: React.FC<DictateButtonProps> = ({
: `${label} diktieren` : `${label} diktieren`
} }
aria-label={isRecording ? 'Aufnahme beenden' : `${label} diktieren`} aria-label={isRecording ? 'Aufnahme beenden' : `${label} diktieren`}
className={`flex items-center gap-1.5 rounded-lg border px-2 py-1 text-[11px] font-bold transition disabled:opacity-60 ${ /*
* `h-7` (28 px) statt sich aus Zeilenhoehe, Innenabstand und Rahmen zu
* ergeben: das ergab 26,5 px, und eine Beschriftungszeile laesst sich
* nicht auf einen halben Pixel ausrichten. Mit der runden Hoehe passt
* labelRowClass (min-h-7) genau, und das Feld daneben beginnt auf
* derselben Linie.
*/
className={`flex h-7 items-center gap-1.5 rounded-lg border px-2 text-[11px] font-bold transition disabled:opacity-60 ${
isRecording isRecording
? 'border-red-300 bg-red-50 text-red-700' ? 'border-red-300 bg-red-50 text-red-700'
: 'border-ink-200 text-ink-500 hover:border-brand-300 hover:text-brand-700' : 'border-ink-200 text-ink-500 hover:border-brand-300 hover:text-brand-700'
@@ -218,11 +227,8 @@ export const DictateTextarea: React.FC<DictateTextareaProps> = ({
onBeforeNavigate, onBeforeNavigate,
}) => ( }) => (
<div> <div>
<div className="mb-1.5 flex items-center justify-between gap-3"> <div className={labelRowClass}>
<label <label htmlFor={id} className={labelTextClass}>
htmlFor={id}
className="block text-[11px] font-extrabold uppercase tracking-wider text-ink-500"
>
{label} {label}
</label> </label>
<DictateButton <DictateButton