fix: refine widget modernization
This commit is contained in:
50 files changed
+292
-386
No files matched your search
@@ -22,6 +22,7 @@
|
||||
overscroll-behavior: contain;
|
||||
animation: focus-surface-in 220ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
transform-origin: top left;
|
||||
padding-top: 60px;
|
||||
}
|
||||
|
||||
.backdropClosing {
|
||||
@@ -57,6 +58,21 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.surfaceControlsAdvanced {
|
||||
right: 0;
|
||||
bottom: auto;
|
||||
height: 60px;
|
||||
background: color-mix(in srgb, var(--mantine-color-body) 92%, transparent);
|
||||
border-bottom: 1px solid var(--mantine-color-default-border);
|
||||
}
|
||||
|
||||
.surfaceTitle {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 120px;
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
@@ -68,7 +84,7 @@
|
||||
.pinButton {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 52px;
|
||||
right: 60px;
|
||||
z-index: 2;
|
||||
pointer-events: auto;
|
||||
}
|
||||
@@ -82,7 +98,7 @@
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
@media (any-pointer: coarse) {
|
||||
.touchButton {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ import { getAdvancedFocusRect } from "./geometry";
|
||||
|
||||
describe("getAdvancedFocusRect", () => {
|
||||
it("expands a widget and keeps it inside the viewport", () => {
|
||||
expect(getAdvancedFocusRect({ left: 10, top: 10, width: 300, height: 200 }, { width: 1200, height: 800 })).toEqual({
|
||||
left: 240,
|
||||
top: 190,
|
||||
width: 720,
|
||||
height: 480,
|
||||
});
|
||||
const rect = getAdvancedFocusRect({ left: 10, top: 10, width: 300, height: 200 }, { width: 1200, height: 800 });
|
||||
|
||||
expect(rect.left).toBe(120);
|
||||
expect(rect.top).toBeCloseTo(118.6);
|
||||
expect(rect.width).toBe(960);
|
||||
expect(rect.height).toBeCloseTo(622.8);
|
||||
});
|
||||
|
||||
it("uses the available viewport on small screens", () => {
|
||||
|
||||
@@ -11,8 +11,10 @@ export const getAdvancedFocusRect = (source: FocusRect, viewport: { width: numbe
|
||||
const margin = viewport.width < 640 ? 8 : 24;
|
||||
const availableWidth = Math.max(0, viewport.width - margin * 2);
|
||||
const availableHeight = Math.max(0, viewport.height - appShellHeaderHeight - margin * 2);
|
||||
const width = Math.min(availableWidth, Math.max(source.width, Math.min(960, source.width * 2.4)));
|
||||
const height = Math.min(availableHeight, Math.max(source.height, Math.min(720, source.height * 2.4)));
|
||||
const preferredWidth = viewport.width < 640 ? availableWidth : Math.min(960, availableWidth * 0.9);
|
||||
const preferredHeight = viewport.width < 640 ? availableHeight : Math.min(720, availableHeight * 0.9);
|
||||
const width = Math.min(availableWidth, Math.max(source.width, preferredWidth));
|
||||
const height = Math.min(availableHeight, Math.max(source.height, preferredHeight));
|
||||
|
||||
return {
|
||||
left: Math.max(margin, (viewport.width - width) / 2),
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useCallback } from "react";
|
||||
import { useUpdateBoard } from "@homarr/boards/updater";
|
||||
import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
|
||||
|
||||
import type { Board } from "~/app/[locale]/boards/_types";
|
||||
import type { CreateItemInput } from "./actions/create-item";
|
||||
import { createItemCallback } from "./actions/create-item";
|
||||
import type { DuplicateItemInput } from "./actions/duplicate-item";
|
||||
@@ -33,17 +32,7 @@ interface UpdateItemIntegrations {
|
||||
export const useItemActions = () => {
|
||||
const { updateBoard } = useUpdateBoard();
|
||||
|
||||
const createItem = useCallback(
|
||||
(input: CreateItemInput) => {
|
||||
let nextBoard: Board | undefined;
|
||||
updateBoard((previous) => {
|
||||
nextBoard = createItemCallback(input)(previous);
|
||||
return nextBoard;
|
||||
});
|
||||
return nextBoard;
|
||||
},
|
||||
[updateBoard],
|
||||
);
|
||||
const createItem = useCallback((input: CreateItemInput) => updateBoard(createItemCallback(input)), [updateBoard]);
|
||||
|
||||
const duplicateItem = useCallback(
|
||||
({ itemId }: DuplicateItemInput) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CSSProperties, KeyboardEvent, MutableRefObject, PointerEvent } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { ActionIcon, Badge, Box, Card, FocusTrap } from "@mantine/core";
|
||||
import { ActionIcon, Badge, Box, Card, FocusTrap, Text } from "@mantine/core";
|
||||
import { useElementSize, useMergedRef, useViewportSize } from "@mantine/hooks";
|
||||
import { IconMaximize, IconPin, IconX } from "@tabler/icons-react";
|
||||
import { QueryErrorResetBoundary, useQueryClient } from "@tanstack/react-query";
|
||||
@@ -43,6 +43,8 @@ export const BoardItemContent = ({ item }: BoardItemContentProps) => {
|
||||
const { width: viewportWidth, height: viewportHeight } = useViewportSize();
|
||||
const board = useRequiredBoard();
|
||||
const t = useI18n();
|
||||
const widgetName = t(`widget.${item.kind}.name`);
|
||||
const advancedDialogLabel = t("item.advancedFocus.dialog", { widget: widgetName });
|
||||
const [isEditMode] = useEditMode();
|
||||
const widgetStateRef = useRef<Record<string, unknown> | null>(null);
|
||||
const { active, open, close, dismiss, pin, hover, leave } = useAdvancedFocus();
|
||||
@@ -86,6 +88,8 @@ export const BoardItemContent = ({ item }: BoardItemContentProps) => {
|
||||
<Box
|
||||
ref={mergedRef}
|
||||
tabIndex={isEditMode ? undefined : 0}
|
||||
role={isEditMode ? undefined : "group"}
|
||||
aria-label={isEditMode ? undefined : `${widgetName}: ${t("item.advancedFocus.open")}`}
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={isAdvanced}
|
||||
aria-keyshortcuts="Shift+Enter"
|
||||
@@ -104,11 +108,9 @@ export const BoardItemContent = ({ item }: BoardItemContentProps) => {
|
||||
<Card
|
||||
role={isAdvanced ? "dialog" : undefined}
|
||||
aria-modal={activeFocus?.pinned || undefined}
|
||||
aria-label={
|
||||
isAdvanced ? t("item.advancedFocus.dialog", { widget: t(`widget.${item.kind}.name`) }) : undefined
|
||||
}
|
||||
aria-label={isAdvanced ? advancedDialogLabel : undefined}
|
||||
radius={board.itemRadius}
|
||||
p={0}
|
||||
p={isAdvanced ? undefined : 0}
|
||||
h={isAdvanced ? undefined : "100%"}
|
||||
w={isAdvanced ? undefined : "100%"}
|
||||
className={combineClasses(
|
||||
@@ -143,13 +145,23 @@ export const BoardItemContent = ({ item }: BoardItemContentProps) => {
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<div className={advancedFocusClasses.surfaceControls}>
|
||||
<div
|
||||
className={combineClasses(
|
||||
advancedFocusClasses.surfaceControls,
|
||||
isAdvanced && advancedFocusClasses.surfaceControlsAdvanced,
|
||||
)}
|
||||
>
|
||||
{isAdvanced && (
|
||||
<Text className={advancedFocusClasses.surfaceTitle} size="sm" fw={600} truncate>
|
||||
{advancedDialogLabel}
|
||||
</Text>
|
||||
)}
|
||||
{isAdvanced && (
|
||||
<ActionIcon
|
||||
data-autofocus={activeFocus.pinned && activeFocus.autofocusClose ? true : undefined}
|
||||
className={advancedFocusClasses.closeButton}
|
||||
variant="default"
|
||||
size="lg"
|
||||
size={44}
|
||||
aria-label={t("item.advancedFocus.close")}
|
||||
onClick={() => close()}
|
||||
>
|
||||
@@ -160,6 +172,7 @@ export const BoardItemContent = ({ item }: BoardItemContentProps) => {
|
||||
<ActionIcon
|
||||
className={advancedFocusClasses.pinButton}
|
||||
variant={activeFocus.pinned ? "filled" : "default"}
|
||||
size={44}
|
||||
aria-label={activeFocus.pinned ? t("item.advancedFocus.pinned") : t("item.advancedFocus.pin")}
|
||||
aria-pressed={activeFocus.pinned}
|
||||
onClick={(event) => pin(event.detail === 0)}
|
||||
@@ -171,7 +184,7 @@ export const BoardItemContent = ({ item }: BoardItemContentProps) => {
|
||||
<ActionIcon
|
||||
className={advancedFocusClasses.touchButton}
|
||||
variant="default"
|
||||
size="lg"
|
||||
size={44}
|
||||
aria-label={t("item.advancedFocus.open")}
|
||||
onClick={openPinned}
|
||||
>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { MutableRefObject, ReactNode, RefObject } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { Group, Loader, Menu, Switch, Text, Tooltip } from "@mantine/core";
|
||||
import { Group, Loader, Menu, Text, Tooltip } from "@mantine/core";
|
||||
import {
|
||||
IconAlertTriangle,
|
||||
IconCircleCheck,
|
||||
@@ -242,13 +242,10 @@ export const WidgetContextMenu = ({ item, widgetStateRef, sourceRef, children }:
|
||||
<>
|
||||
<Menu.Label>{tMenu("options")}</Menu.Label>
|
||||
{toggleOptions.map(([key]) => (
|
||||
<Menu.Item key={key} onClick={() => handleToggle(key)(!options[key])}>
|
||||
<Menu.CheckboxItem key={key} checked={Boolean(options[key])} onChange={handleToggle(key)}>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
||||
<Group wrap="nowrap">
|
||||
{String(translateIfNecessary(t, ((fn: any) => fn(`widget.${item.kind}.option.${key}.label`)) as any))}
|
||||
<Switch size="xs" checked={Boolean(options[key])} readOnly tabIndex={-1} ml="auto" />
|
||||
</Group>
|
||||
</Menu.Item>
|
||||
{String(translateIfNecessary(t, ((fn: any) => fn(`widget.${item.kind}.option.${key}.label`)) as any))}
|
||||
</Menu.CheckboxItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user