diff --git a/apps/api/src/dashboard/dto/create-widget.dto.ts b/apps/api/src/dashboard/dto/create-widget.dto.ts index 36640e8..bed72c2 100644 --- a/apps/api/src/dashboard/dto/create-widget.dto.ts +++ b/apps/api/src/dashboard/dto/create-widget.dto.ts @@ -2,8 +2,8 @@ import { IsIn, IsObject, IsOptional, IsString } from 'class-validator'; /** * DTO for creating a new widget instance on a user's dashboard. - * widgetType must be one of the eight supported types - * ('picture-frame' seit quick-260921-pi9). + * widgetType must be one of the nine supported types + * ('picture-frame' seit quick-260921-pi9, 'xframe' seit quick-260921-qd3). * config is optional and defaults to {} on the model. */ export class CreateWidgetDto { @@ -17,6 +17,7 @@ export class CreateWidgetDto { 'favorites', 'stopwatch', 'picture-frame', + 'xframe', ]) widgetType!: string; diff --git a/apps/web/src/app/(portal)/page.test.tsx b/apps/web/src/app/(portal)/page.test.tsx index 9202772..55ab51e 100644 --- a/apps/web/src/app/(portal)/page.test.tsx +++ b/apps/web/src/app/(portal)/page.test.tsx @@ -59,6 +59,7 @@ vi.mock('@/components/dashboard/widgets/calculator-widget', () => ({ CalculatorW vi.mock('@/components/dashboard/widgets/stopwatch-widget', () => ({ StopwatchWidget: () => null })); vi.mock('@/components/dashboard/widgets/favorites-widget', () => ({ FavoritesWidget: () => null })); vi.mock('@/components/dashboard/widgets/picture-frame-widget', () => ({ PictureFrameWidget: () => null })); +vi.mock('@/components/dashboard/widgets/xframe-widget', () => ({ XframeWidget: () => null })); beforeEach(() => { mockStore.isEditMode = false; diff --git a/apps/web/src/app/(portal)/page.tsx b/apps/web/src/app/(portal)/page.tsx index b75e329..397b01f 100644 --- a/apps/web/src/app/(portal)/page.tsx +++ b/apps/web/src/app/(portal)/page.tsx @@ -5,7 +5,7 @@ import { useTranslations } from 'next-intl'; import { DashboardGrid } from '@/components/dashboard/dashboard-grid'; import { EditModeToggle } from '@/components/dashboard/edit-mode-toggle'; import { WidgetCatalogModal } from '@/components/dashboard/widget-catalog-modal'; -import { wireClockWidget, wireSearchWidget, wireCalendarWidget, wireNoteWidget, wireCalculatorWidget, wireStopwatchWidget, wireFavoritesWidget, wirePictureFrameWidget } from '@/components/dashboard/widget-registry'; +import { wireClockWidget, wireSearchWidget, wireCalendarWidget, wireNoteWidget, wireCalculatorWidget, wireStopwatchWidget, wireFavoritesWidget, wirePictureFrameWidget, wireXframeWidget } from '@/components/dashboard/widget-registry'; import { ClockWidget } from '@/components/dashboard/widgets/clock-widget'; import { SearchWidget } from '@/components/dashboard/widgets/search-widget'; import { CalendarWidget } from '@/components/dashboard/widgets/calendar-widget'; @@ -14,6 +14,7 @@ import { CalculatorWidget } from '@/components/dashboard/widgets/calculator-widg import { StopwatchWidget } from '@/components/dashboard/widgets/stopwatch-widget'; import { FavoritesWidget } from '@/components/dashboard/widgets/favorites-widget'; import { PictureFrameWidget } from '@/components/dashboard/widgets/picture-frame-widget'; +import { XframeWidget } from '@/components/dashboard/widgets/xframe-widget'; import { useDashboardStore } from '@/lib/stores/dashboard-store'; import type { WidgetType } from '@/components/dashboard/widget-registry'; @@ -26,6 +27,7 @@ wireCalculatorWidget(CalculatorWidget); wireStopwatchWidget(StopwatchWidget); wireFavoritesWidget(FavoritesWidget); wirePictureFrameWidget(PictureFrameWidget); +wireXframeWidget(XframeWidget); export default function DashboardPage() { const t = useTranslations('widgets'); diff --git a/apps/web/src/components/dashboard/widget-catalog-modal.test.tsx b/apps/web/src/components/dashboard/widget-catalog-modal.test.tsx index 44d9b86..b669d24 100644 --- a/apps/web/src/components/dashboard/widget-catalog-modal.test.tsx +++ b/apps/web/src/components/dashboard/widget-catalog-modal.test.tsx @@ -25,6 +25,8 @@ vi.mock('next-intl', () => ({ 'stopwatch.description': 'Zeit messen', 'pictureFrame.name': 'Bilderrahmen', 'pictureFrame.description': 'Bilder als Diashow', + 'xframe.name': 'XFrame', + 'xframe.description': 'Webseite einbetten', }, common: { close: 'Schließen', diff --git a/apps/web/src/components/dashboard/widget-catalog-modal.tsx b/apps/web/src/components/dashboard/widget-catalog-modal.tsx index 2e6e02d..c556738 100644 --- a/apps/web/src/components/dashboard/widget-catalog-modal.tsx +++ b/apps/web/src/components/dashboard/widget-catalog-modal.tsx @@ -19,6 +19,7 @@ const WIDGET_TYPES: WidgetType[] = [ 'favorites', 'stopwatch', 'picture-frame', + 'xframe', ]; /** diff --git a/apps/web/src/components/dashboard/widget-registry.test.tsx b/apps/web/src/components/dashboard/widget-registry.test.tsx index fe5822a..8158b05 100644 --- a/apps/web/src/components/dashboard/widget-registry.test.tsx +++ b/apps/web/src/components/dashboard/widget-registry.test.tsx @@ -18,6 +18,8 @@ const ALL_WIDGET_TYPES: WidgetType[] = [ 'stopwatch', // Bilderrahmen (quick-260921-pi9) 'picture-frame', + // XFrame (quick-260921-qd3) + 'xframe', ]; describe('WIDGET_CONSTRAINTS (DASH-11)', () => { @@ -53,7 +55,7 @@ describe('WIDGET_CONSTRAINTS (DASH-11)', () => { expect(Object.keys(WIDGET_CONSTRAINTS)).toContain('stopwatch'); }); - it('Test A (quick-260916-dyv): Minima = kleinste bedienbare Kachel je Typ, Vorgaben unveraendert (quick-260916-iex: Link-Widget entfernt; quick-260921-pi9: Bilderrahmen dazu, acht Typen)', () => { + it('Test A (quick-260916-dyv): Minima = kleinste bedienbare Kachel je Typ, Vorgaben unveraendert (quick-260916-iex: Link-Widget entfernt; quick-260921-pi9: Bilderrahmen dazu; quick-260921-qd3: XFrame dazu, neun Typen)', () => { // Raster 24 Spalten / 20 px (quick-260916-bwo). Die Minima sind seit // quick-260916-dyv inhaltsgetrieben (Rechnung im Plan), die Vorgaben // (defaultW/defaultH) bleiben das Doppelte der alten 12-Spalten-Werte. @@ -67,6 +69,7 @@ describe('WIDGET_CONSTRAINTS (DASH-11)', () => { favorites: { minW: 3, minH: 3, defaultW: 6, defaultH: 10 }, stopwatch: { minW: 4, minH: 3, defaultW: 6, defaultH: 6 }, 'picture-frame': { minW: 4, minH: 4, defaultW: 8, defaultH: 12 }, + xframe: { minW: 4, minH: 4, defaultW: 12, defaultH: 12 }, }); let counted = 0; @@ -76,6 +79,6 @@ describe('WIDGET_CONSTRAINTS (DASH-11)', () => { counted++; } } - expect(counted).toBe(32); + expect(counted).toBe(36); }); }); diff --git a/apps/web/src/components/dashboard/widget-registry.tsx b/apps/web/src/components/dashboard/widget-registry.tsx index 63bc7da..23395d7 100644 --- a/apps/web/src/components/dashboard/widget-registry.tsx +++ b/apps/web/src/components/dashboard/widget-registry.tsx @@ -7,6 +7,7 @@ import type { ComponentType } from 'react'; * Einzel-Schnellzugriffs-Typ wurde in quick-260916-iex entfernt — Favoriten * decken den Fall ab). * picture-frame: Bilderrahmen (quick-260921-pi9). + * xframe: Webseite als Rahmen (quick-260921-qd3). */ export type WidgetType = | 'clock' @@ -16,7 +17,8 @@ export type WidgetType = | 'calculator' | 'favorites' | 'stopwatch' - | 'picture-frame'; + | 'picture-frame' + | 'xframe'; /** * Props contract that every widget component must accept. @@ -60,6 +62,11 @@ export const WIDGET_CONSTRAINTS: Record< // Browser-Rundgang mit 216 px Hoehe zu flach. 4x4 ist die kleinste Kachel, // in der Bild und Unterschrift-Streifen noch erkennbar sind. 'picture-frame': { minW: 4, minH: 4, defaultW: 8, defaultH: 12 }, + // quick-260921-qd3: eine Webseite braucht Breite UND Hoehe — 12x12 = halbe + // Rasterbreite bei rund 330 px Hoehe (8 Zeilen waeren nur 216 px, zu flach + // fuer eine Seite; vgl. Bilderrahmen 8x12). 4x4 kleinste Kachel, in der ein + // Rahmen noch Sinn hat. + xframe: { minW: 4, minH: 4, defaultW: 12, defaultH: 12 }, }; /** @@ -277,6 +284,29 @@ function PictureFrameIcon({ className }: { className?: string }) { ); } +function XframeIcon({ className }: { className?: string }) { + return ( + + ); +} + /** * Registry of all widget types. Used by the widget catalog modal * and the grid renderer to look up components and constraints. @@ -346,6 +376,14 @@ export const WIDGET_REGISTRY: Record = { ...WIDGET_CONSTRAINTS['picture-frame'], component: PlaceholderWidget, // Replaced via wirePictureFrameWidget() }, + xframe: { + type: 'xframe', + nameKey: 'xframe.name', + descriptionKey: 'xframe.description', + icon: XframeIcon, + ...WIDGET_CONSTRAINTS.xframe, + component: PlaceholderWidget, // Replaced via wireXframeWidget() + }, }; // Wire actual widget components lazily to avoid circular deps @@ -415,3 +453,11 @@ export function wirePictureFrameWidget(component: ComponentType) { pictureFrameWired = true; } } + +let xframeWired = false; +export function wireXframeWidget(component: ComponentType) { + if (!xframeWired) { + WIDGET_REGISTRY.xframe.component = component; + xframeWired = true; + } +} diff --git a/apps/web/src/components/dashboard/widgets/xframe-config.test.ts b/apps/web/src/components/dashboard/widgets/xframe-config.test.ts new file mode 100644 index 0000000..69c1985 --- /dev/null +++ b/apps/web/src/components/dashboard/widgets/xframe-config.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, it } from 'vitest'; +import { + XFRAME_RELOAD_MAX, + XFRAME_RELOAD_MIN, + XFRAME_RELOAD_OPTIONS, + XFRAME_SANDBOX, + XFRAME_TITLE_MAX, + isHttpsUrl, + resolveXframeConfig, +} from './xframe-config'; + +/** + * xframe-config.test — NEU (quick-260921-qd3, XFrame). + * + * Reiner Resolver ohne React: die API prueft Config-Felder nicht — allein + * diese Funktion entscheidet, was zum `