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:
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { FormEvent, useEffect, useMemo, useState } from 'react';
|
||||
import { FormEvent, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { updateWidgetConfig } from '@/lib/dashboard-api';
|
||||
import {
|
||||
@@ -12,6 +12,9 @@ import {
|
||||
} from '@/lib/favorites-api';
|
||||
import type { WidgetProps } from '@/components/dashboard/widget-registry';
|
||||
|
||||
// quick-260916-iex: wie DEBOUNCE_MS im Notiz-Widget
|
||||
const TITLE_DEBOUNCE_MS = 1500;
|
||||
|
||||
/**
|
||||
* FavoritesWidget — persistent per-widget list of favorite links (DASH-09).
|
||||
*
|
||||
@@ -22,6 +25,9 @@ import type { WidgetProps } from '@/components/dashboard/widget-registry';
|
||||
* - Server-side icon discovery (letter fallback when iconUrl is null)
|
||||
* - Links open in new tab with rel="noreferrer" (T-08-08 open-redirect mitigation)
|
||||
* - No dangerouslySetInnerHTML (T-08-07 XSS mitigation)
|
||||
* - Optionaler Titel (quick-260916-iex): leer -> keine Kopfzeile, sonst
|
||||
* Kopfzeile im Notiz-Look; im Bearbeitungsmodus immer ein Titelfeld,
|
||||
* entprellt gespeichert (Muster note-widget.tsx)
|
||||
*/
|
||||
export function FavoritesWidget({
|
||||
instanceId,
|
||||
@@ -39,6 +45,30 @@ export function FavoritesWidget({
|
||||
(config.viewMode as 'list' | 'grid') ?? 'list',
|
||||
);
|
||||
|
||||
// Optionaler Titel (quick-260916-iex)
|
||||
const [title, setTitle] = useState<string>(
|
||||
typeof config.title === 'string' ? config.title : '',
|
||||
);
|
||||
const titleTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => clearTimeout(titleTimerRef.current);
|
||||
}, []);
|
||||
|
||||
function handleTitleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const value = e.target.value;
|
||||
setTitle(value);
|
||||
clearTimeout(titleTimerRef.current);
|
||||
titleTimerRef.current = setTimeout(() => {
|
||||
void updateWidgetConfig(instanceId, { title: value });
|
||||
}, TITLE_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
const hasTitle = title.trim() !== '';
|
||||
const showHeader = isEditMode || hasTitle;
|
||||
|
||||
// Add-form state
|
||||
const [newTitle, setNewTitle] = useState('');
|
||||
const [newUrl, setNewUrl] = useState('');
|
||||
@@ -171,7 +201,30 @@ export function FavoritesWidget({
|
||||
|
||||
// --- Render ---
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-auto p-1 gap-2">
|
||||
<div className="flex h-full flex-col overflow-hidden">
|
||||
{/* Header: optional title (quick-260916-iex) — mirrors note-widget.tsx look */}
|
||||
{showHeader && (
|
||||
<div className="flex items-center gap-2 border-b border-border px-1.5 py-1.5">
|
||||
{isEditMode ? (
|
||||
<input
|
||||
type="text"
|
||||
className="flex-1 bg-transparent text-sm font-semibold text-foreground outline-none placeholder:text-muted-foreground widgetNoDrag"
|
||||
value={title}
|
||||
onChange={handleTitleChange}
|
||||
placeholder={t('favorites.titlePlaceholder')}
|
||||
aria-label={t('favorites.titleLabel')}
|
||||
/>
|
||||
) : (
|
||||
hasTitle && (
|
||||
<h2 className="truncate text-sm font-semibold text-foreground">
|
||||
{title.trim()}
|
||||
</h2>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-1 flex-col gap-2 overflow-auto p-1">
|
||||
{/* View mode toggle (edit mode only) */}
|
||||
{isEditMode && (
|
||||
<div className="flex gap-1 widgetNoDrag" aria-label={t('favorites.name')}>
|
||||
@@ -295,6 +348,7 @@ export function FavoritesWidget({
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user