feat(web): Favoriten-Widget — optionaler Titel (Kopfzeile, FavoritesConfig, i18n)

- Favoriten-Widget: config.title (nur typeof string) steuert eine Kopfzeile
  im Notiz-Look; leer -> keine Kopfzeile; im Bearbeitungsmodus immer ein
  Titelfeld (widgetNoDrag, 1500 ms entprellt, { title })
- Einstellungen -> Dashboard -> Widgets: FavoritesConfig (Muster NoteConfig)
  mit uebersetzter Beschriftung, Instanz-Kopfzeile zeigt "— {title}" jetzt
  fuer note UND favorites
- NoteConfig: hart kodiertes "Title" durch uebersetzten Schluessel ersetzt
- 3 neue Uebersetzungsschluessel (note.titleLabel, favorites.titleLabel,
  favorites.titlePlaceholder) in de/en
- favorites-Tests 7+4, Panel-Tests 7+3, Umlaut-Waechter 3/3 gruen, tsc Exit 0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-16 13:39:48 +02:00
parent 684f063a8e
commit 7f1ee3b1f0
6 changed files with 294 additions and 10 deletions
@@ -182,3 +182,55 @@ describe('WidgetSettingsPanel — Kalender-Einstellungen (quick-260916-htc)', ()
expect((screen.getByLabelText(cal.configLookahead) as HTMLSelectElement).value).toBe('30');
});
});
const fav = (de as { widgets: { favorites: Record<string, string> } }).widgets.favorites;
const note = (de as { widgets: { note: Record<string, string> } }).widgets.note;
describe('WidgetSettingsPanel — Favoriten-Titel (quick-260916-iex)', () => {
function renderFavoritesExpanded(config: Record<string, unknown> = {}) {
const onWidgetUpdate = vi.fn();
const widget = { id: 'f1', widgetType: 'favorites', config };
render(<WidgetSettingsPanel widgets={[widget]} onWidgetUpdate={onWidgetUpdate} />);
fireEvent.click(screen.getByRole('button', { name: /Favoriten #1/ }));
return { onWidgetUpdate };
}
it('Test B1: Instanz-Kopfzeile zeigt "— Titel", Feld vorbelegt, Aenderung ruft updateWidgetConfig/onWidgetUpdate auf', async () => {
const { onWidgetUpdate } = renderFavoritesExpanded({ title: 'Werkzeuge' });
expect(screen.getByRole('button', { name: /Favoriten #1/ })).toHaveTextContent('— Werkzeuge');
const input = screen.getByLabelText(fav.titleLabel) as HTMLInputElement;
expect(input.value).toBe('Werkzeuge');
fireEvent.change(input, { target: { value: 'Werkzeuge 2' } });
await vi.waitFor(() =>
expect(onWidgetUpdate).toHaveBeenCalledWith('f1', { title: 'Werkzeuge 2' }),
);
expect(updateWidgetConfig).toHaveBeenCalledWith('f1', { title: 'Werkzeuge 2' });
});
it('Test B2: ohne Titel bzw. mit Leerraum-Titel keine "—" in der Kopfzeile, Feld leer', () => {
renderFavoritesExpanded({});
expect(screen.getByRole('button', { name: /Favoriten #1/ })).not.toHaveTextContent('—');
expect((screen.getByLabelText(fav.titleLabel) as HTMLInputElement).value).toBe('');
cleanup();
renderFavoritesExpanded({ title: ' ' });
expect(screen.getByRole('button', { name: /Favoriten #1/ })).not.toHaveTextContent('—');
expect((screen.getByLabelText(fav.titleLabel) as HTMLInputElement).value).toBe('');
});
it('Test B3: Notiz-Instanz zeigt uebersetzte Beschriftung "Titel" statt hart kodiertem "Title"', () => {
const onWidgetUpdate = vi.fn();
const widget = { id: 'n1', widgetType: 'note', config: { title: 'Einkauf' } };
render(<WidgetSettingsPanel widgets={[widget]} onWidgetUpdate={onWidgetUpdate} />);
fireEvent.click(screen.getByRole('button', { name: /Notizen #1/ }));
const input = screen.getByLabelText(note.titleLabel) as HTMLInputElement;
expect(input.value).toBe('Einkauf');
expect(screen.queryByText('Title')).not.toBeInTheDocument();
});
});