From 2d8efe1a9783a7483a49c365f00ccdd911ffe0fd Mon Sep 17 00:00:00 2001 From: Schalli Date: Wed, 16 Sep 2026 09:12:02 +0200 Subject: [PATCH] feat(quick-260916-bwo): Widget-Inhalt skaliert per Container-Queries (Uhr, Stoppuhr, Rechner), Punktgroesse der Uhrzeit einstellbar - widget-wrapper.tsx: Rumpf ist Groessen-Container (@container-size) fuer cqw/cqh - clock-font-size.ts (neu): Grenzen 8..200, resolveClockTimeFontSizePt nur fuer endliche Zahlen (T-BWO-01) - clock-widget.tsx: Container-Query-Klassen mit clamp(), data-font-mode auto/fixed, Inline-pt nur bei Zahl, keine vw-Formel mehr, p-1 - stopwatch-widget.tsx: Anzeige per Container-Query, p-1.5; calculator-widget.tsx: Anzeige und Tasten skalieren, Tasten h-full min-h-7, p-1 - widget-settings-panel.tsx: Zahlenfeld Schriftgroesse der Uhrzeit (Blur/Enter, leer = automatisch, Fehlermeldung ausserhalb 8..200) - de.json/en.json: widgets.clock.fontSizeLabel/fontSizeAuto/fontSizeHint/fontSizeInvalid (Sie-Form, Waechter gruen) - Tests: Uhr +4, Stoppuhr +1, Rechner +1, widget-settings-panel.test.tsx neu (4) Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY --- .../widgets/calculator-widget.test.tsx | 10 ++ .../dashboard/widgets/calculator-widget.tsx | 21 ++-- .../dashboard/widgets/clock-font-size.ts | 32 ++++++ .../dashboard/widgets/clock-widget.test.tsx | 88 ++++++++++++++ .../dashboard/widgets/clock-widget.tsx | 19 +++- .../widgets/stopwatch-widget.test.tsx | 9 ++ .../dashboard/widgets/stopwatch-widget.tsx | 7 +- .../dashboard/widgets/widget-wrapper.tsx | 9 +- .../settings/widget-settings-panel.test.tsx | 107 ++++++++++++++++++ .../settings/widget-settings-panel.tsx | 70 ++++++++++++ apps/web/src/messages/de.json | 6 +- apps/web/src/messages/en.json | 6 +- 12 files changed, 364 insertions(+), 20 deletions(-) create mode 100644 apps/web/src/components/dashboard/widgets/clock-font-size.ts create mode 100644 apps/web/src/components/settings/widget-settings-panel.test.tsx diff --git a/apps/web/src/components/dashboard/widgets/calculator-widget.test.tsx b/apps/web/src/components/dashboard/widgets/calculator-widget.test.tsx index 5f57548..6514190 100644 --- a/apps/web/src/components/dashboard/widgets/calculator-widget.test.tsx +++ b/apps/web/src/components/dashboard/widgets/calculator-widget.test.tsx @@ -82,4 +82,14 @@ describe('CalculatorWidget', () => { expect(screen.getByLabelText('Anzeige')).toHaveTextContent('3'); }); + + it('quick-260916-bwo: Anzeige und Tasten tragen Container-Query-Klassen, Tasten fuellen ihre Zelle', () => { + render(); + + expect(screen.getByLabelText('Anzeige').className).toMatch(/cqw/); + const seven = screen.getByRole('button', { name: '7' }); + expect(seven.className).toMatch(/cqw/); + expect(seven.className).toContain('h-full'); + expect(screen.getByRole('button', { name: 'CE' }).className).toMatch(/cqw/); + }); }); diff --git a/apps/web/src/components/dashboard/widgets/calculator-widget.tsx b/apps/web/src/components/dashboard/widgets/calculator-widget.tsx index 989a410..fd506f0 100644 --- a/apps/web/src/components/dashboard/widgets/calculator-widget.tsx +++ b/apps/web/src/components/dashboard/widgets/calculator-widget.tsx @@ -84,7 +84,7 @@ function CalcButton({ label, ariaLabel, className = '', onClick, disabled }: But aria-label={ariaLabel ?? label} onClick={onClick} disabled={disabled} - className={`flex items-center justify-center rounded text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${className}`} + className={`flex items-center justify-center rounded font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${className}`} > {label} @@ -307,16 +307,21 @@ export function CalculatorWidget({ isEditMode }: WidgetProps) { } } - const baseBtn = 'h-9 cursor-pointer select-none active:scale-95'; - const numBtn = `${baseBtn} bg-background hover:bg-muted border border-border text-foreground`; - const opBtn = `${baseBtn} bg-muted hover:bg-muted/80 border border-border text-primary font-semibold`; - const utilBtn = `${baseBtn} bg-muted/50 hover:bg-muted border border-border text-muted-foreground text-xs`; - const eqBtn = `${baseBtn} bg-primary hover:bg-primary/90 text-primary-foreground font-semibold`; + // quick-260916-bwo: Tasten fuellen ihre Rasterzelle (h-full) und skalieren + // ihre Schrift per Container-Query mit der Kachel; jede Tastenart traegt genau + // EINE Schriftgroessen-Klasse (die Basis in CalcButton hat keine mehr). Alle + // Klassen stehen woertlich als Literale, damit der Tailwind-Scanner sie findet. + const baseBtn = 'h-full min-h-7 cursor-pointer select-none active:scale-95'; + const numBtn = `${baseBtn} bg-background hover:bg-muted border border-border text-foreground text-[clamp(11px,min(4cqw,4.5cqh),40px)]`; + const opBtn = `${baseBtn} bg-muted hover:bg-muted/80 border border-border text-primary font-semibold text-[clamp(11px,min(4cqw,4.5cqh),40px)]`; + const utilBtn = `${baseBtn} bg-muted/50 hover:bg-muted border border-border text-muted-foreground text-[clamp(10px,min(3.2cqw,3.6cqh),32px)]`; + const eqBtn = `${baseBtn} bg-primary hover:bg-primary/90 text-primary-foreground font-semibold text-[clamp(11px,min(4cqw,4.5cqh),40px)]`; + // Speicherzeile bleibt bewusst klein (h-7 text-xs) — Nebenfunktion, kein Hauptinhalt. const memBtn = 'h-7 cursor-pointer select-none rounded text-xs font-medium transition-colors hover:bg-muted text-muted-foreground disabled:opacity-30 disabled:cursor-default px-1'; return (
{/* Display */} {display} diff --git a/apps/web/src/components/dashboard/widgets/clock-font-size.ts b/apps/web/src/components/dashboard/widgets/clock-font-size.ts new file mode 100644 index 0000000..461359d --- /dev/null +++ b/apps/web/src/components/dashboard/widgets/clock-font-size.ts @@ -0,0 +1,32 @@ +/** + * clock-font-size — Grenzen und Aufloesung der festen Punktgroesse der + * Uhrzeit (quick-260916-bwo). + * + * Das Uhr-Widget skaliert seine Zeit standardmaessig automatisch ueber + * CSS-Container-Queries mit der Kachelgroesse. Optional legt der Benutzer + * unter Einstellungen -> Dashboard eine feste Schriftgroesse in Punkt fest + * (Config-Feld `timeFontSizePt`, Zahl oder null; leer = automatisch). + * + * T-BWO-01: Der Wert landet als Inline-Style im Widget. Die API prueft + * Config-Felder nicht (`@IsObject()`), deshalb entscheidet ALLEIN diese + * Funktion, was zum Style wird: nur ein `typeof number`, endlich und im + * Bereich MIN..MAX. Zeichenketten, NaN, Infinity und Werte ausserhalb fallen + * auf `null` = automatisch zurueck — kein Text aus der Konfiguration erreicht + * jemals den Style. Formular UND Widget benutzen dieselben Grenzen von hier. + */ + +export const CLOCK_FONT_SIZE_MIN_PT = 8; +export const CLOCK_FONT_SIZE_MAX_PT = 200; +/** Das Datum unter der Uhrzeit bekommt diesen Anteil der festen Punktgroesse. */ +export const CLOCK_DATE_FACTOR = 0.4; + +export function resolveClockTimeFontSizePt(config: Record): number | null { + const value = config.timeFontSizePt; + if (typeof value !== 'number' || !Number.isFinite(value)) { + return null; + } + if (value < CLOCK_FONT_SIZE_MIN_PT || value > CLOCK_FONT_SIZE_MAX_PT) { + return null; + } + return value; +} diff --git a/apps/web/src/components/dashboard/widgets/clock-widget.test.tsx b/apps/web/src/components/dashboard/widgets/clock-widget.test.tsx index 3b4f9ed..dbddca6 100644 --- a/apps/web/src/components/dashboard/widgets/clock-widget.test.tsx +++ b/apps/web/src/components/dashboard/widgets/clock-widget.test.tsx @@ -1,5 +1,10 @@ import { cleanup, render, screen } from '@testing-library/react'; import { afterEach, describe, expect, it, vi } from 'vitest'; +import { + CLOCK_FONT_SIZE_MAX_PT, + CLOCK_FONT_SIZE_MIN_PT, + resolveClockTimeFontSizePt, +} from './clock-font-size'; // Mock next-intl vi.mock('next-intl', () => ({ @@ -65,4 +70,87 @@ describe('ClockWidget', () => { vi.useRealTimers(); }); + + // --- quick-260916-bwo: Skalierung per Container-Query, feste Punktgroesse --- + // jsdom verwirft clamp()/min() in style.fontSize (gemessen) — deshalb liegt die + // automatische Groesse in einer Tailwind-KLASSE und nur die feste Punktgroesse + // im Inline-Style. Die Tests lesen fuer 'auto' die Klasse, fuer 'fixed' style. + + it('Test 3 (quick-260916-bwo): resolveClockTimeFontSizePt laesst nur endliche Zahlen 8..200 durch (T-BWO-01)', () => { + expect(resolveClockTimeFontSizePt({ timeFontSizePt: 36 })).toBe(36); + expect(resolveClockTimeFontSizePt({ timeFontSizePt: 8 })).toBe(8); + expect(resolveClockTimeFontSizePt({ timeFontSizePt: 200 })).toBe(200); + expect(resolveClockTimeFontSizePt({ timeFontSizePt: 36.5 })).toBe(36.5); + expect(resolveClockTimeFontSizePt({ timeFontSizePt: 7 })).toBeNull(); + expect(resolveClockTimeFontSizePt({ timeFontSizePt: 201 })).toBeNull(); + expect(resolveClockTimeFontSizePt({ timeFontSizePt: '36' })).toBeNull(); + expect(resolveClockTimeFontSizePt({ timeFontSizePt: Number.NaN })).toBeNull(); + expect(resolveClockTimeFontSizePt({ timeFontSizePt: Number.POSITIVE_INFINITY })).toBeNull(); + expect(resolveClockTimeFontSizePt({ timeFontSizePt: null })).toBeNull(); + expect(resolveClockTimeFontSizePt({})).toBeNull(); + expect(CLOCK_FONT_SIZE_MIN_PT).toBe(8); + expect(CLOCK_FONT_SIZE_MAX_PT).toBe(200); + }); + + it('Test 4 (quick-260916-bwo): feste Punktgroesse -> Inline-Style 36pt, Datum 14.4pt, data-font-mode fixed', async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2026-06-24T12:30:00Z')); + + const { ClockWidget } = await import('./clock-widget'); + render( + , + ); + + const timeEl = screen.getByRole('time'); + expect(timeEl.style.fontSize).toBe('36pt'); + expect(timeEl.getAttribute('data-font-mode')).toBe('fixed'); + expect(screen.getByTestId('clock-date').style.fontSize).toBe('14.4pt'); + }); + + it('Test 5 (quick-260916-bwo): ohne Punktgroesse -> kein Inline-Style, data-font-mode auto, Container-Query-Klassen', async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2026-06-24T12:30:00Z')); + + const { ClockWidget } = await import('./clock-widget'); + render( + , + ); + + const timeEl = screen.getByRole('time'); + expect(timeEl.style.fontSize).toBe(''); + expect(timeEl.getAttribute('data-font-mode')).toBe('auto'); + expect(timeEl.className).toMatch(/cqw/); + expect(timeEl.className).toMatch(/cqh/); + const dateEl = screen.getByTestId('clock-date'); + expect(dateEl.className).toMatch(/cq[wh]/); + expect(dateEl.style.fontSize).toBe(''); + }); + + it('Test 6 (quick-260916-bwo): Zeichenketten-Wert wird ignoriert — auto, kein Inline-Style (T-BWO-01)', async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2026-06-24T12:30:00Z')); + + const { ClockWidget } = await import('./clock-widget'); + render( + , + ); + + const timeEl = screen.getByRole('time'); + expect(timeEl.style.fontSize).toBe(''); + expect(timeEl.getAttribute('data-font-mode')).toBe('auto'); + expect(timeEl.className).toMatch(/cqw/); + expect(screen.getByTestId('clock-date').style.fontSize).toBe(''); + }); }); diff --git a/apps/web/src/components/dashboard/widgets/clock-widget.tsx b/apps/web/src/components/dashboard/widgets/clock-widget.tsx index ce532f0..3282611 100644 --- a/apps/web/src/components/dashboard/widgets/clock-widget.tsx +++ b/apps/web/src/components/dashboard/widgets/clock-widget.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import type { WidgetProps } from '../widget-registry'; +import { CLOCK_DATE_FACTOR, resolveClockTimeFontSizePt } from './clock-font-size'; /** * Digital clock widget displaying time in a configurable timezone. @@ -10,10 +11,14 @@ import type { WidgetProps } from '../widget-registry'; * Config: * - timezone: IANA timezone string (default 'Europe/Berlin') * - showDate: boolean (default false) — show date below time + * - timeFontSizePt: number | null (quick-260916-bwo) — feste Schriftgroesse der + * Uhrzeit in Punkt; leer/null = automatisch ueber Container-Queries mit der + * Kachelgroesse. Grenzen 8..200 in `clock-font-size.ts` (T-BWO-01). */ export function ClockWidget({ config }: WidgetProps) { const timezone = (config.timezone as string) || 'Europe/Berlin'; const showDate = config.showDate === true; + const fixedPt = resolveClockTimeFontSizePt(config); const [now, setNow] = useState(() => new Date()); @@ -39,11 +44,16 @@ export function ClockWidget({ config }: WidgetProps) { }); return ( -
+
+ {/* Automatisch = CSS (Container-Query-Klasse, skaliert mit der Kachel), + fest = Zahl als Inline-Style in pt. Die Trennung ist bewusst: jsdom + verwirft clamp() im Inline-Style, der Browser bekommt die Klasse + ohnehin — und "auto = Klasse, fest = Zahl" ist die einfachere Form. */}