diff --git a/apps/web/src/components/dashboard/widgets/widget-wrapper.tsx b/apps/web/src/components/dashboard/widgets/widget-wrapper.tsx
index 7adca31..111ca0b 100644
--- a/apps/web/src/components/dashboard/widgets/widget-wrapper.tsx
+++ b/apps/web/src/components/dashboard/widgets/widget-wrapper.tsx
@@ -65,8 +65,13 @@ export function WidgetWrapper({ widget, isEditMode, onRemove }: WidgetWrapperPro
)}
- {/* Widget body */}
-
+ {/* Widget body.
+ quick-260916-bwo: Groessen-Container (@container-size -> container-type: size),
+ damit die Widget-Inhalte per cqw/cqh mit der Kachel skalieren. Braucht eine
+ definite Hoehe — die kommt ueber h-full aus der Karte, die das RGL-Element
+ mit Pixelhoehe fuellt. Steht am Rumpf statt an der Karte, weil die Karte
+ im Bearbeitungsmodus zusaetzlich den Griff traegt. */}
+
{WidgetComponent ? (
{
+ const messages = (await import('@/messages/de.json')).default as Record;
+ const lookup = (path: string): string | undefined =>
+ path.split('.').reduce((o, k) => (o && typeof o === 'object' ? (o as any)[k] : undefined), messages) as
+ | string
+ | undefined;
+ return {
+ useTranslations: (ns?: string) => (key: string) => lookup(ns ? `${ns}.${key}` : key) ?? key,
+ };
+});
+
+vi.mock('@/lib/dashboard-api', () => ({
+ updateWidgetConfig: vi.fn().mockResolvedValue(undefined),
+}));
+
+vi.mock('next/link', () => ({
+ default: ({ href, children }: { href: string; children: React.ReactNode }) => {children},
+}));
+
+vi.mock('@/components/settings/search-provider-form', () => ({
+ SearchProviderForm: () => null,
+}));
+
+const clockTexts = (de as { widgets: { clock: Record } }).widgets.clock;
+
+const WIDGET = { id: 'c1', widgetType: 'clock', config: { timezone: 'Europe/Berlin', timeFontSizePt: 24 } };
+
+function renderExpanded() {
+ const onWidgetUpdate = vi.fn();
+ render();
+ fireEvent.click(screen.getByRole('button', { name: /Uhr #1/ }));
+ const input = screen.getByLabelText(clockTexts.fontSizeLabel) as HTMLInputElement;
+ return { onWidgetUpdate, input };
+}
+
+beforeEach(() => {
+ vi.clearAllMocks();
+});
+
+afterEach(() => {
+ cleanup();
+});
+
+describe('WidgetSettingsPanel — Schriftgroesse der Uhrzeit (quick-260916-bwo)', () => {
+ it('Test 1: das Zahlenfeld ist aus der Konfiguration vorbelegt, mit Grenzen, Platzhalter und Hilfetext', () => {
+ const { input } = renderExpanded();
+
+ expect(input.value).toBe('24');
+ expect(input.type).toBe('number');
+ expect(input.min).toBe('8');
+ expect(input.max).toBe('200');
+ expect(input.placeholder).toBe(clockTexts.fontSizeAuto);
+ expect(screen.getByText(clockTexts.fontSizeHint)).toBeInTheDocument();
+ });
+
+ it('Test 2: eine Zahl im Bereich wird beim Verlassen des Felds uebernommen', async () => {
+ const { input, onWidgetUpdate } = renderExpanded();
+
+ fireEvent.change(input, { target: { value: '36' } });
+ fireEvent.blur(input);
+
+ await vi.waitFor(() => expect(onWidgetUpdate).toHaveBeenCalledWith('c1', { timeFontSizePt: 36 }));
+ expect(updateWidgetConfig).toHaveBeenCalledTimes(1);
+ expect(updateWidgetConfig).toHaveBeenCalledWith('c1', { timeFontSizePt: 36 });
+ });
+
+ it('Test 3: leeres Feld plus Enter -> automatisch (timeFontSizePt null), keine Fehlermeldung', async () => {
+ const { input } = renderExpanded();
+
+ fireEvent.change(input, { target: { value: '' } });
+ fireEvent.keyDown(input, { key: 'Enter' });
+
+ await vi.waitFor(() => expect(updateWidgetConfig).toHaveBeenCalledWith('c1', { timeFontSizePt: null }));
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
+ });
+
+ it('Test 4: ausserhalb 8..200 -> Fehlermeldung, aria-invalid, KEIN Aufruf', () => {
+ const { input } = renderExpanded();
+
+ fireEvent.change(input, { target: { value: '300' } });
+ fireEvent.blur(input);
+
+ expect(updateWidgetConfig).not.toHaveBeenCalled();
+ expect(screen.getByRole('alert')).toHaveTextContent(clockTexts.fontSizeInvalid);
+ expect(input.getAttribute('aria-invalid')).toBe('true');
+
+ fireEvent.change(input, { target: { value: '7' } });
+ fireEvent.blur(input);
+
+ expect(updateWidgetConfig).not.toHaveBeenCalled();
+ expect(screen.getByRole('alert')).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/src/components/settings/widget-settings-panel.tsx b/apps/web/src/components/settings/widget-settings-panel.tsx
index 168cc8a..33fd54e 100644
--- a/apps/web/src/components/settings/widget-settings-panel.tsx
+++ b/apps/web/src/components/settings/widget-settings-panel.tsx
@@ -6,6 +6,11 @@ import Link from 'next/link';
import { WIDGET_REGISTRY, type WidgetType } from '@/components/dashboard/widget-registry';
import { SearchProviderForm } from '@/components/settings/search-provider-form';
import { updateWidgetConfig } from '@/lib/dashboard-api';
+import {
+ CLOCK_FONT_SIZE_MAX_PT,
+ CLOCK_FONT_SIZE_MIN_PT,
+ resolveClockTimeFontSizePt,
+} from '@/components/dashboard/widgets/clock-font-size';
/**
* IANA timezones offered for clock configuration (D-12).
@@ -206,6 +211,35 @@ function ClockConfig({
const timezone = (config.timezone as string) || 'Europe/Berlin';
const showDate = config.showDate === true;
+ // quick-260916-bwo: feste Schriftgroesse der Uhrzeit in Punkt (leer = automatisch).
+ // Entwurf als Text im Feld, Uebernahme bei Blur oder Enter; Grenzen aus
+ // clock-font-size.ts — dieselben, die das Widget beim Rendern anwendet (T-BWO-01).
+ const [fontSizeDraft, setFontSizeDraft] = useState(() => {
+ const pt = resolveClockTimeFontSizePt(config);
+ return pt === null ? '' : String(pt);
+ });
+ const [fontSizeError, setFontSizeError] = useState(false);
+
+ const commitFontSize = () => {
+ const raw = fontSizeDraft.trim();
+ if (raw === '') {
+ setFontSizeError(false);
+ if (resolveClockTimeFontSizePt(config) !== null) {
+ onChange({ timeFontSizePt: null });
+ }
+ return;
+ }
+ const n = Number(raw);
+ if (!Number.isFinite(n) || n < CLOCK_FONT_SIZE_MIN_PT || n > CLOCK_FONT_SIZE_MAX_PT) {
+ setFontSizeError(true);
+ return;
+ }
+ setFontSizeError(false);
+ if (n !== resolveClockTimeFontSizePt(config)) {
+ onChange({ timeFontSizePt: n });
+ }
+ };
+
return (
{/* Timezone select (D-12) */}
@@ -243,6 +277,42 @@ function ClockConfig({
{t('clock.dateHint')}
+
+ {/* Schriftgroesse der Uhrzeit in Punkt (quick-260916-bwo) */}
+
+
+
setFontSizeDraft(e.target.value)}
+ onBlur={commitFontSize}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ commitFontSize();
+ }
+ }}
+ aria-invalid={fontSizeError || undefined}
+ aria-describedby="clock-font-size-hint"
+ className="h-9 w-full max-w-xs rounded border border-border bg-background px-3 text-sm text-foreground"
+ />
+
+ {t('clock.fontSizeHint')}
+
+ {fontSizeError && (
+
+ {t('clock.fontSizeInvalid')}
+
+ )}
+
);
}
diff --git a/apps/web/src/messages/de.json b/apps/web/src/messages/de.json
index f9c59d7..d998996 100644
--- a/apps/web/src/messages/de.json
+++ b/apps/web/src/messages/de.json
@@ -186,7 +186,11 @@
"clock": {
"name": "Uhr",
"description": "Zeigt die aktuelle Uhrzeit an",
- "dateHint": "Datum anzeigen"
+ "dateHint": "Datum anzeigen",
+ "fontSizeLabel": "Schriftgröße der Uhrzeit (Punkt)",
+ "fontSizeAuto": "automatisch",
+ "fontSizeHint": "Leer lassen, dann richtet sich die Uhrzeit nach der Größe der Kachel. Ein Wert zwischen 8 und 200 legt die Schrift fest, unabhängig von der Kachelgröße.",
+ "fontSizeInvalid": "Bitte geben Sie eine Zahl zwischen 8 und 200 ein oder lassen Sie das Feld leer."
},
"search": {
"name": "Suchleiste",
diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json
index 88d4e57..adb0e1e 100644
--- a/apps/web/src/messages/en.json
+++ b/apps/web/src/messages/en.json
@@ -186,7 +186,11 @@
"clock": {
"name": "Clock",
"description": "Shows the current time",
- "dateHint": "Show date"
+ "dateHint": "Show date",
+ "fontSizeLabel": "Font size of the time (pt)",
+ "fontSizeAuto": "automatic",
+ "fontSizeHint": "Leave empty and the time follows the size of the tile. A value between 8 and 200 fixes the font size regardless of the tile size.",
+ "fontSizeInvalid": "Please enter a number between 8 and 200 or leave the field empty."
},
"search": {
"name": "Search",