diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ffa646..86bc2d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ Diese Liste beschreibt in einfachen Worten, was sich von Version zu Version an T ### Geändert - Dashboard, Favoriten: In der Kachelansicht stehen die Symbole enger beieinander; der Abstand zwischen ihnen ist etwa halb so groß, in eine Zeile passen mehr Favoriten. Passt ein Name nicht in eine Zeile, wird er kleiner geschrieben und auf zwei Zeilen umbrochen. -- Dashboard: Es sieht jetzt auf jedem Bildschirm gleich aus. Tessera merkt sich die Fläche des Bildschirms, an dem Sie ein Dashboard zuerst öffnen, und zeigt es auf anderen Bildschirmen maßstäblich verkleinert oder vergrößert – samt Schrift und ohne Scrollen. Auf dem Handy bleibt es bei der bisherigen Anordnung untereinander. +- Dashboard: Es sieht jetzt auf jedem Bildschirm gleich aus. Tessera merkt sich die Fläche des Bildschirms, an dem Sie ein Dashboard zuerst öffnen, und zeigt es auf anderen Bildschirmen maßstäblich verkleinert oder vergrößert – samt Schrift und ohne Scrollen. Ist ein Dashboard länger als der Bildschirm, wird es so weit verkleinert, dass es ganz hineinpasst. Auf dem Handy bleibt es bei der bisherigen Anordnung untereinander. - Eigene Module: Neue Einträge sind mit der Kategorie „Eigene Module“ vorbelegt. - Dashboard: Der Kalender lässt sich nicht mehr so schmal ziehen, dass seine Überschrift abgeschnitten wird. - Eigene Module: Die Seite füllt jetzt den ganzen Inhaltsbereich. Name und Hinweiszeile darüber sind weggefallen – der Name steht ohnehin oben in der Leiste, und „In neuem Tab öffnen“ sitzt jetzt dort rechts. diff --git a/apps/web/src/components/dashboard/dashboard-grid.test.tsx b/apps/web/src/components/dashboard/dashboard-grid.test.tsx index 509e2e2..d9e8e42 100644 --- a/apps/web/src/components/dashboard/dashboard-grid.test.tsx +++ b/apps/web/src/components/dashboard/dashboard-grid.test.tsx @@ -5,6 +5,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { stubResizeObserver } from '@/test/fake-resize-observer'; import { computeCanvasScale, + fitCanvasToContent, DASHBOARD_BOTTOM_GAP_PX, RESIZE_AXIS_FALLBACK, scaledPositionStrategy, @@ -692,6 +693,22 @@ describe('Leinwand: Dashboard wie ein Bild mitskalieren', () => { return { outer, inner }; } + describe('fitCanvasToContent', () => { + it('Inhalt passt in die Leinwand: Leinwand bleibt (1:1 am Original-PC)', () => { + expect(fitCanvasToContent({ w: 1641, h: 849 }, 700)).toEqual({ w: 1641, h: 849 }); + }); + it('Inhalt hoeher als die Leinwand: Inhaltshoehe zaehlt, damit nie gescrollt wird', () => { + expect(fitCanvasToContent({ w: 1641, h: 849 }, 940)).toEqual({ w: 1641, h: 940 }); + // am Original-PC selbst: 849 / 940 -> alles passt hinein + expect( + computeCanvasScale({ w: 1641, h: 849 }, fitCanvasToContent({ w: 1641, h: 849 }, 940)), + ).toBeCloseTo(849 / 940); + }); + it('unbrauchbare Hoehe: Leinwand bleibt', () => { + expect(fitCanvasToContent({ w: 1600, h: 800 }, Number.NaN)).toEqual({ w: 1600, h: 800 }); + }); + }); + describe('computeCanvasScale', () => { it('breitenbegrenzt: freier Platz unten', () => { expect(computeCanvasScale({ w: 1280, h: 900 }, { w: 1600, h: 800 })).toBe(0.8); @@ -758,12 +775,23 @@ describe('Leinwand: Dashboard wie ein Bild mitskalieren', () => { cleanup(); setInnerHeight(768); + // Rasterinhalt niedriger als die Leinwand (300 < 374) — sonst zaehlte die + // Inhaltshoehe (fitCanvasToContent). + stubResizeObserver({ width: 1000, height: 300 }); const second = await renderGrid({ canvas: { w: 800, h: 374 } }); // min(1000 / 800, 748 / 374) = 1.25 expect(second.inner.style.transform).toBe('scale(1.25)'); expect(captured.props?.width).toBe(800); }); + it('Inhalt hoeher als die Leinwand: eingepasst, damit nie gescrollt wird', async () => { + stubResizeObserver({ width: 1000, height: 1600 }); + setInnerHeight(820); // verfuegbar 800 + const { inner } = await renderGrid({ canvas: { w: 1000, h: 800 } }); + // min(1000 / 1000, 800 / 1600) = 0.5 + expect(inner.style.transform).toBe('scale(0.5)'); + }); + it('schmal (< 768): wie bisher — Containerbreite, kein transform, keine Erfassung', async () => { stubResizeObserver({ width: 600, height: 800 }); const onCaptureCanvas = vi.fn(); diff --git a/apps/web/src/components/dashboard/dashboard-grid.tsx b/apps/web/src/components/dashboard/dashboard-grid.tsx index f43e3b7..e865d72 100644 --- a/apps/web/src/components/dashboard/dashboard-grid.tsx +++ b/apps/web/src/components/dashboard/dashboard-grid.tsx @@ -147,6 +147,16 @@ export function computeCanvasScale(available: GridCanvas, canvas: GridCanvas): n return Number.isFinite(s) && s > 0 ? s : 1; } +/** + * Leinwand fuer die Einpassung: ist der Inhalt (unskalierte Rasterhoehe) + * hoeher als die Leinwand, gilt die Inhaltshoehe, sonst die Leinwand selbst — + * so bleibt es am Original-PC bei 1:1, solange alles draufpasst. + */ +export function fitCanvasToContent(canvas: GridCanvas, contentHeight: number): GridCanvas { + if (!Number.isFinite(contentHeight) || contentHeight <= canvas.h) return canvas; + return { w: canvas.w, h: contentHeight }; +} + /** * Positionsstrategie fuer ein per `transform: scale(s)` skaliertes Raster. * @@ -409,7 +419,19 @@ export function DashboardGrid({ // Desktop = verfuegbare Breite ab CANVAS_MIN_WIDTH; nur dort Leinwand. const isDesktop = width >= CANVAS_MIN_WIDTH; const scaled = isDesktop && canvas !== null; - const scale = scaled ? computeCanvasScale({ w: width, h: availableHeight }, canvas) : 1; + // "Nie scrollen" (Nutzerentscheidung): ist der Inhalt hoeher als die + // Leinwand (schon am Original-PC laenger als der Bildschirm), zaehlt die + // Inhaltshoehe — dann passt auch dort alles hinein, und jeder andere + // Bildschirm zeigt dasselbe Bild. Im Bearbeitungsmodus bleibt die Hoehe vom + // Beginn des Bearbeitens stehen, sonst wuerde das Raster beim Ziehen nach + // unten laufend kleiner und die Kachel liefe dem Mauszeiger davon. + const editHeightRef = useRef(null); + if (isEditMode && editHeightRef.current === null) editHeightRef.current = innerHeight; + if (!isEditMode) editHeightRef.current = null; + const contentHeight = isEditMode ? (editHeightRef.current ?? innerHeight) : innerHeight; + const scale = scaled + ? computeCanvasScale({ w: width, h: availableHeight }, fitCanvasToContent(canvas, contentHeight)) + : 1; const positionStrategy = useMemo(() => scaledPositionStrategy(scale), [scale]); // Leinwand erfassen: nur mit eingehaengtem Raster (also mit Kacheln), am