diff --git a/apps/nextjs/src/components/board/items/item-content.tsx b/apps/nextjs/src/components/board/items/item-content.tsx index a8a394b92..49ed7ffab 100644 --- a/apps/nextjs/src/components/board/items/item-content.tsx +++ b/apps/nextjs/src/components/board/items/item-content.tsx @@ -180,6 +180,7 @@ const LoadedBoardItemContent = ({ const { width: viewportWidth, height: viewportHeight } = viewportSize; const supportsAdvancedFocus = definitionSupportsAdvancedFocus(definition); const widgetName = getWidgetName(item.kind, t); + const previewDimensions = useMemo(() => ({ width, height }), [height, width]); const advancedViewLabel = t("item.advancedFocus.label", { widget: widgetName }); const advancedViewId = `advanced-focus-${item.id}`; const activeFocus = supportsAdvancedFocus && active?.itemId === item.id ? active : null; @@ -350,10 +351,11 @@ const LoadedBoardItemContent = ({ return ( <> - {isEditMode && } + {isEditMode && } void; } @@ -31,7 +32,7 @@ export const BoardItemMenu = (props: BoardItemMenuProps) => { return ; }; -const BoardItemMenuInner = ({ item, definition, resetErrorBoundary }: BoardItemMenuProps) => { +const BoardItemMenuInner = ({ item, definition, previewDimensions, resetErrorBoundary }: BoardItemMenuProps) => { const { data: session } = useSession(); const canDuplicate = item.kind !== "customApi" || (session?.user.permissions.includes("admin") ?? false); const refResetErrorBoundaryOnNextRender = useRef(false); @@ -89,7 +90,7 @@ const BoardItemMenuInner = ({ item, definition, resetErrorBoundary }: BoardItemM integrationSupport: "supportedIntegrations" in definition, settings, itemId: item.id, - previewDimensions: { width: item.width * 200, height: item.height * 200 }, + previewDimensions, appId: item.kind === "app" ? (item.options.appId as string | undefined) : undefined, }, { diff --git a/apps/nextjs/src/components/board/items/lazy-widget-edit-modal.tsx b/apps/nextjs/src/components/board/items/lazy-widget-edit-modal.tsx index 48af5be5f..0e869a544 100644 --- a/apps/nextjs/src/components/board/items/lazy-widget-edit-modal.tsx +++ b/apps/nextjs/src/components/board/items/lazy-widget-edit-modal.tsx @@ -7,7 +7,7 @@ import type { FallbackProps } from "react-error-boundary"; import { clientApi } from "@homarr/api/client"; import type { WidgetKind } from "@homarr/definitions"; -import { createModal, modalSizeSelect, useModalAction } from "@homarr/modals"; +import { createModal, useModalAction } from "@homarr/modals"; import { WidgetError } from "@homarr/widgets/errors"; import type * as WidgetModalsModule from "@homarr/widgets/modals"; import type { WidgetEditModalProps } from "@homarr/widgets/modals"; @@ -160,7 +160,7 @@ export const LazyWidgetEditModal = createModal> defaultTitle(t) { return t("item.edit.title"); }, - size: modalSizeSelect, + size: "100rem", transitionProps: { transition: "pop", duration: 180, diff --git a/apps/nextjs/src/components/board/items/widget-context-menu.tsx b/apps/nextjs/src/components/board/items/widget-context-menu.tsx index 52dfbb3a1..bf9688b8e 100644 --- a/apps/nextjs/src/components/board/items/widget-context-menu.tsx +++ b/apps/nextjs/src/components/board/items/widget-context-menu.tsx @@ -35,6 +35,7 @@ import { matchesWidgetItemQuery } from "./widget-query-scope"; interface WidgetContextMenuProps { item: SectionItem; definition: WidgetDefinition; + previewDimensions: { width: number; height: number }; widgetStateRef: MutableRefObject | null>; widgetRuntimeRef: WidgetRuntimeRef; sourceRef: RefObject; @@ -44,6 +45,7 @@ interface WidgetContextMenuProps { export const WidgetContextMenu = ({ item, definition, + previewDimensions, widgetRuntimeRef, sourceRef, children, @@ -191,7 +193,7 @@ export const WidgetContextMenu = ({ settings, itemId: item.id, boardId: board.id, - previewDimensions: { width: item.width * 200, height: item.height * 200 }, + previewDimensions, appId: item.kind === "app" ? (item.options.appId as string | undefined) : undefined, }, // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -207,6 +209,7 @@ export const WidgetContextMenu = ({ handleRefetch, openModal, persistBoard, + previewDimensions, settings, updateItemAdvancedOptions, updateItemIntegrations, diff --git a/packages/widgets/src/modals/widget-edit-modal.module.css b/packages/widgets/src/modals/widget-edit-modal.module.css index eb9559b10..222cece6c 100644 --- a/packages/widgets/src/modals/widget-edit-modal.module.css +++ b/packages/widgets/src/modals/widget-edit-modal.module.css @@ -6,7 +6,7 @@ } .workspace[data-with-preview] { - grid-template-columns: minmax(0, 0.95fr) minmax(24rem, 1.05fr); + grid-template-columns: minmax(0, 3fr) minmax(34rem, 2fr); } .previewPanel { @@ -28,7 +28,7 @@ } .previewCanvas { - min-height: 23rem; + height: clamp(24rem, calc(100dvh - 17rem), 44rem); padding: var(--mantine-spacing-lg); overflow: hidden; border: 1px solid var(--mantine-color-default-border); @@ -40,12 +40,19 @@ background-size: 2rem 2rem; } -.previewWidget { +.previewViewport { position: relative; flex: 0 0 auto; + animation: dock-widget 260ms cubic-bezier(0.22, 1, 0.36, 1) both; +} + +.previewWidget { + position: absolute; + top: 0; + left: 0; overflow: hidden; box-shadow: var(--mantine-shadow-lg); - animation: dock-widget 260ms cubic-bezier(0.22, 1, 0.36, 1) both; + transform-origin: top left; } .previewTitle { @@ -134,7 +141,7 @@ } } -@media (max-width: 62em) { +@media (max-width: 75em) { .workspace[data-with-preview] { grid-template-columns: 1fr; } @@ -144,7 +151,7 @@ } .previewCanvas { - min-height: 18rem; + height: 24rem; } .settingsPanel { @@ -154,7 +161,7 @@ } @media (prefers-reduced-motion: reduce) { - .previewWidget { + .previewViewport { animation: none; } } diff --git a/packages/widgets/src/modals/widget-edit-modal.tsx b/packages/widgets/src/modals/widget-edit-modal.tsx index 4c9ccaa90..5fe3690ae 100644 --- a/packages/widgets/src/modals/widget-edit-modal.tsx +++ b/packages/widgets/src/modals/widget-edit-modal.tsx @@ -9,6 +9,7 @@ import { schemaResolver } from "@mantine/form"; import { useElementSize } from "@mantine/hooks"; import { IconArrowLeft, IconEye, IconPencil, IconSettings } from "@tabler/icons-react"; import { QueryErrorResetBoundary } from "@tanstack/react-query"; +import combineClasses from "clsx"; import { ErrorBoundary } from "react-error-boundary"; import { z } from "zod/v4"; @@ -16,7 +17,7 @@ import { objectEntries } from "@homarr/common"; import { useSession } from "@homarr/auth/client"; import { useOptionalBoard } from "@homarr/boards/context"; import type { WidgetKind } from "@homarr/definitions"; -import { createModal, modalSizeSelect, useModalAction } from "@homarr/modals"; +import { createModal, useModalAction } from "@homarr/modals"; import type { SettingsContextProps } from "@homarr/settings/creator"; import { useI18n } from "@homarr/translation/client"; import { IntegrationAvatar } from "@homarr/ui"; @@ -71,6 +72,7 @@ export interface WidgetEditModalProps { } interface WidgetEditPreviewProps { + kind: WidgetKind; Component: ComponentType>; definition: WidgetDefinition; state: WidgetEditModalState; @@ -81,6 +83,7 @@ interface WidgetEditPreviewProps { } const WidgetEditPreview = ({ + kind, Component, definition, state, @@ -90,18 +93,15 @@ const WidgetEditPreview = ({ onChangeOptions, }: WidgetEditPreviewProps) => { const tItem = useI18n("item.edit"); - const { ref, width: availableWidth } = useElementSize(); + const { ref, width: availableWidth, height: availableHeight } = useElementSize(); const sourceWidth = Math.max(dimensions.width, 1); const sourceHeight = Math.max(dimensions.height, 1); - const aspectRatio = sourceWidth / sourceHeight; - const maximumWidth = Math.max(availableWidth, 280); - let previewWidth = maximumWidth; - let previewHeight = previewWidth / aspectRatio; - - if (previewHeight > 320) { - previewHeight = 320; - previewWidth = previewHeight * aspectRatio; + let previewScale = 1; + if (availableWidth > 0 && availableHeight > 0) { + previewScale = Math.min(availableWidth / sourceWidth, availableHeight / sourceHeight, 1); } + const previewWidth = sourceWidth * previewScale; + const previewHeight = sourceHeight * previewScale; const hasIntegrationSupport = "supportedIntegrations" in definition; const integrationRequired = hasIntegrationSupport && definition.integrationsRequired !== false; @@ -118,43 +118,55 @@ const WidgetEditPreview = ({ {tItem("preview.integrationRequired")} ) : ( - = 96 ? undefined : 4} - style={{ borderColor: state.advancedOptions.borderColor || undefined }} - > - {state.advancedOptions.title && ( - - {state.advancedOptions.title} - - )} - - {({ reset }) => ( - ( - - )} - > -
- onChangeOptions(newOptions as Record)} - /> -
-
+ + - + w={sourceWidth} + h={sourceHeight} + p={0} + style={{ + borderColor: state.advancedOptions.borderColor || undefined, + containerType: "size", + transform: `scale(${previewScale})`, + }} + > + {state.advancedOptions.title && ( + + {state.advancedOptions.title} + + )} + + {({ reset }) => ( + ( + + )} + > +
+ onChangeOptions(newOptions as Record)} + /> +
+
+ )} +
+
+ )} @@ -356,6 +368,7 @@ export const WidgetEditModal = createModal>(({ {innerProps.previewComponent && ( >(({ defaultTitle(t) { return t("item.edit.title"); }, - size: modalSizeSelect, + size: "100rem", transitionProps: { transition: "pop", duration: 180,