fix: preserve widget preview dimensions
This commit is contained in:
1 parent
5188ed89d6
commit
763e1324e8
6 files changed
+86
-60
No files matched your search
@@ -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 && <BoardItemMenu item={menuItem} definition={definition} />}
|
||||
{isEditMode && <BoardItemMenu item={menuItem} definition={definition} previewDimensions={previewDimensions} />}
|
||||
<WidgetContextMenu
|
||||
item={item}
|
||||
definition={definition}
|
||||
previewDimensions={previewDimensions}
|
||||
widgetStateRef={widgetStateRef}
|
||||
widgetRuntimeRef={widgetRuntimeRef}
|
||||
sourceRef={sourceRef}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { LazyWidgetEditModal, preloadWidgetEditModal } from "./lazy-widget-edit-
|
||||
interface BoardItemMenuProps {
|
||||
item: SectionItem;
|
||||
definition: WidgetDefinition;
|
||||
previewDimensions: { width: number; height: number };
|
||||
resetErrorBoundary?: () => void;
|
||||
}
|
||||
|
||||
@@ -31,7 +32,7 @@ export const BoardItemMenu = (props: BoardItemMenuProps) => {
|
||||
|
||||
return <BoardItemMenuInner {...props} />;
|
||||
};
|
||||
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,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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<WidgetEditModalProps<WidgetKind>>
|
||||
defaultTitle(t) {
|
||||
return t("item.edit.title");
|
||||
},
|
||||
size: modalSizeSelect,
|
||||
size: "100rem",
|
||||
transitionProps: {
|
||||
transition: "pop",
|
||||
duration: 180,
|
||||
|
||||
@@ -35,6 +35,7 @@ import { matchesWidgetItemQuery } from "./widget-query-scope";
|
||||
interface WidgetContextMenuProps {
|
||||
item: SectionItem;
|
||||
definition: WidgetDefinition;
|
||||
previewDimensions: { width: number; height: number };
|
||||
widgetStateRef: MutableRefObject<Record<string, unknown> | null>;
|
||||
widgetRuntimeRef: WidgetRuntimeRef;
|
||||
sourceRef: RefObject<HTMLElement | null>;
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<TSort extends WidgetKind> {
|
||||
}
|
||||
|
||||
interface WidgetEditPreviewProps {
|
||||
kind: WidgetKind;
|
||||
Component: ComponentType<WidgetComponentProps<WidgetKind>>;
|
||||
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<HTMLDivElement>();
|
||||
const { ref, width: availableWidth, height: availableHeight } = useElementSize<HTMLDivElement>();
|
||||
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")}
|
||||
</Alert>
|
||||
) : (
|
||||
<Card
|
||||
className={classes.previewWidget}
|
||||
w={previewWidth}
|
||||
h={previewHeight}
|
||||
p={previewHeight >= 96 ? undefined : 4}
|
||||
style={{ borderColor: state.advancedOptions.borderColor || undefined }}
|
||||
>
|
||||
{state.advancedOptions.title && (
|
||||
<Badge className={classes.previewTitle} size="sm" variant="default">
|
||||
{state.advancedOptions.title}
|
||||
</Badge>
|
||||
)}
|
||||
<QueryErrorResetBoundary>
|
||||
{({ reset }) => (
|
||||
<ErrorBoundary
|
||||
onReset={reset}
|
||||
resetKeys={[state.options, state.integrationIds]}
|
||||
fallbackRender={({ resetErrorBoundary, error }) => (
|
||||
<WidgetError definition={definition} error={error} resetErrorBoundary={resetErrorBoundary} />
|
||||
)}
|
||||
>
|
||||
<div className={classes.previewContent} inert>
|
||||
<Component
|
||||
options={state.options as never}
|
||||
integrationIds={state.integrationIds}
|
||||
width={previewWidth}
|
||||
height={previewHeight}
|
||||
isEditMode={false}
|
||||
boardId={boardId}
|
||||
itemId={itemId}
|
||||
setOptions={({ newOptions }) => onChangeOptions(newOptions as Record<string, unknown>)}
|
||||
/>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
<Box className={classes.previewViewport} w={previewWidth} h={previewHeight}>
|
||||
<Card
|
||||
className={combineClasses(
|
||||
classes.previewWidget,
|
||||
`${kind}-wrapper`,
|
||||
"board-grid-item-content",
|
||||
state.advancedOptions.customCssClasses,
|
||||
)}
|
||||
</QueryErrorResetBoundary>
|
||||
</Card>
|
||||
w={sourceWidth}
|
||||
h={sourceHeight}
|
||||
p={0}
|
||||
style={{
|
||||
borderColor: state.advancedOptions.borderColor || undefined,
|
||||
containerType: "size",
|
||||
transform: `scale(${previewScale})`,
|
||||
}}
|
||||
>
|
||||
{state.advancedOptions.title && (
|
||||
<Badge className={classes.previewTitle} size="sm" variant="default">
|
||||
{state.advancedOptions.title}
|
||||
</Badge>
|
||||
)}
|
||||
<QueryErrorResetBoundary>
|
||||
{({ reset }) => (
|
||||
<ErrorBoundary
|
||||
onReset={reset}
|
||||
resetKeys={[state.options, state.integrationIds]}
|
||||
fallbackRender={({ resetErrorBoundary, error }) => (
|
||||
<WidgetError definition={definition} error={error} resetErrorBoundary={resetErrorBoundary} />
|
||||
)}
|
||||
>
|
||||
<div className={classes.previewContent} inert>
|
||||
<Component
|
||||
options={state.options as never}
|
||||
integrationIds={state.integrationIds}
|
||||
width={sourceWidth}
|
||||
height={sourceHeight}
|
||||
isEditMode={false}
|
||||
displayMode="compact"
|
||||
boardId={boardId}
|
||||
itemId={itemId}
|
||||
setOptions={({ newOptions }) => onChangeOptions(newOptions as Record<string, unknown>)}
|
||||
/>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
</QueryErrorResetBoundary>
|
||||
</Card>
|
||||
</Box>
|
||||
)}
|
||||
</Center>
|
||||
</Stack>
|
||||
@@ -356,6 +368,7 @@ export const WidgetEditModal = createModal<WidgetEditModalProps<WidgetKind>>(({
|
||||
<Box className={classes.workspace} data-with-preview={innerProps.previewComponent ? true : undefined}>
|
||||
{innerProps.previewComponent && (
|
||||
<WidgetEditPreview
|
||||
kind={innerProps.kind}
|
||||
Component={innerProps.previewComponent}
|
||||
definition={definition}
|
||||
state={{ ...form.values, advancedOptions }}
|
||||
@@ -562,7 +575,7 @@ export const WidgetEditModal = createModal<WidgetEditModalProps<WidgetKind>>(({
|
||||
defaultTitle(t) {
|
||||
return t("item.edit.title");
|
||||
},
|
||||
size: modalSizeSelect,
|
||||
size: "100rem",
|
||||
transitionProps: {
|
||||
transition: "pop",
|
||||
duration: 180,
|
||||
|
||||
Reference in new issue
Block a user