diff --git a/apps/nextjs/src/components/board/items/item-content.module.css b/apps/nextjs/src/components/board/items/item-content.module.css index 7100f3197..7ec4b3a16 100644 --- a/apps/nextjs/src/components/board/items/item-content.module.css +++ b/apps/nextjs/src/components/board/items/item-content.module.css @@ -11,6 +11,19 @@ border-color: var(--border-color) !important; } +.settingsButton { + opacity: 0; + pointer-events: none; + transition: opacity 120ms ease; +} + +:global([data-type="item"]:hover) .settingsButton, +:global([data-type="item"]:focus-within) .settingsButton, +.settingsButton[data-menu-open="true"] { + opacity: 1; + pointer-events: auto; +} + .editModeInertContent { display: contents; } @@ -39,3 +52,9 @@ opacity: 1; pointer-events: auto; } + +@media (prefers-reduced-motion: reduce) { + .settingsButton { + transition: none; + } +} diff --git a/apps/nextjs/src/components/board/items/item-content.tsx b/apps/nextjs/src/components/board/items/item-content.tsx index 0a28730b0..15d64af7a 100644 --- a/apps/nextjs/src/components/board/items/item-content.tsx +++ b/apps/nextjs/src/components/board/items/item-content.tsx @@ -25,7 +25,6 @@ import type { WidgetComponentProps, WidgetDefinition, WidgetRuntimeRef } from "@ import { loadWidgetResources, reduceWidgetOptionsWithDefinition } from "@homarr/widgets/manifest"; import type { SectionItem } from "~/app/[locale]/boards/_types"; -import { useBoardSelection } from "../selection/board-selection-context"; import advancedFocusClasses from "../advanced-focus/advanced-focus.module.css"; import { useAdvancedFocus } from "../advanced-focus/context"; import { startAdvancedFocusEntrance } from "../advanced-focus/entrance"; @@ -60,18 +59,6 @@ type BoardItemCardProps = CardProps & { const BoardItemCard = ({ item, innerRef, children, ...cardProps }: BoardItemCardProps) => { const board = useRequiredBoard(); - const [isEditMode] = useEditMode(); - const { isSelected, toggleSelectItem, selectedItemIds } = useBoardSelection(); - const selected = isSelected(item.id); - - const handleClickCapture = (event: React.MouseEvent) => { - if (!isEditMode) return; - if (event.metaKey || event.ctrlKey || (selectedItemIds.size > 0 && !event.shiftKey)) { - event.preventDefault(); - event.stopPropagation(); - toggleSelectItem(item.id); - } - }; return ( 0), "--border-color": item.advancedOptions.borderColor !== "" ? item.advancedOptions.borderColor : undefined, }, }} p={0} > - {selected && ( - - - ✓ - - - )} {children} ); diff --git a/apps/nextjs/src/components/board/items/item-menu.tsx b/apps/nextjs/src/components/board/items/item-menu.tsx index 0679edde3..55e998125 100644 --- a/apps/nextjs/src/components/board/items/item-menu.tsx +++ b/apps/nextjs/src/components/board/items/item-menu.tsx @@ -14,6 +14,7 @@ import { BoardRemoveConfirmationMenuItem } from "../remove-confirmation-menu-ite import { useSectionContext } from "../sections/section-context"; import { useItemActions } from "./item-actions"; import { useOpenItemMoveModal } from "./item-move-modal"; +import itemContentClasses from "./item-content.module.css"; import { LazyWidgetEditModal, preloadWidgetEditModal } from "./lazy-widget-edit-modal"; interface BoardItemMenuProps { @@ -105,9 +106,11 @@ const BoardItemMenuInner = ({ item, definition, resetErrorBoundary }: BoardItemM size={24} radius="sm" pos="absolute" - top={12} - right={8} + top={4} + right={4} style={{ zIndex: 30 }} + className={itemContentClasses.settingsButton} + data-menu-open={isMenuOpen || undefined} data-board-widget-settings aria-label={tItem("menu.label.settingsFor", { name: label })} > diff --git a/apps/nextjs/src/components/board/items/item-select-modal.spec.tsx b/apps/nextjs/src/components/board/items/item-select-modal.spec.tsx index 9c52f61a5..9b429bcaf 100644 --- a/apps/nextjs/src/components/board/items/item-select-modal.spec.tsx +++ b/apps/nextjs/src/components/board/items/item-select-modal.spec.tsx @@ -118,7 +118,7 @@ describe("Advanced Add App - Modal Subcomponents", () => { }); it("renders overflow indicator when 6 or more integrations are supported", async () => { - const integrations: IntegrationKind[] = ["sonarr", "radarr", "plex", "jellyfin", "emby", "kavita"]; + const integrations: IntegrationKind[] = ["sonarr", "radarr", "plex", "jellyfin", "emby", "deluge"]; await act(async () => root.render( diff --git a/apps/nextjs/src/components/board/sections/grid/fixed-grid-item.tsx b/apps/nextjs/src/components/board/sections/grid/fixed-grid-item.tsx index dd9d8100b..07dadd7b6 100644 --- a/apps/nextjs/src/components/board/sections/grid/fixed-grid-item.tsx +++ b/apps/nextjs/src/components/board/sections/grid/fixed-grid-item.tsx @@ -49,16 +49,15 @@ export const FixedGridItem = ({ const { section, items, innerSections, columnCount, maxRowCount, announce } = useSectionContext(); const { commitSectionGrid } = useGridLayoutActions(); const [isKeyboardEditing, setIsKeyboardEditing] = useState(false); - const { isSelected, toggleSelectItem, selectedItemIds } = useBoardSelection(); + const { isSelected, toggleSelectItem } = useBoardSelection(); const selected = item.type === "item" && isSelected(item.id); const handleClickCapture = (event: React.MouseEvent) => { - if (!isEditorActive || item.type !== "item") return; - if (event.metaKey || event.ctrlKey || (selectedItemIds.size > 0 && !event.shiftKey)) { - event.preventDefault(); - event.stopPropagation(); - toggleSelectItem(item.id); - } + if (!isEditorActive || item.type !== "item" || (!event.metaKey && !event.ctrlKey)) return; + + event.preventDefault(); + event.stopPropagation(); + toggleSelectItem(item.id); }; const instructionsId = useId(); const isEditorActive = isEditMode && runtimeStatus === "ready"; diff --git a/apps/nextjs/src/components/board/sections/grid/grid-editor.css b/apps/nextjs/src/components/board/sections/grid/grid-editor.css index 2cace7d36..d55282b3b 100644 --- a/apps/nextjs/src/components/board/sections/grid/grid-editor.css +++ b/apps/nextjs/src/components/board/sections/grid/grid-editor.css @@ -32,6 +32,27 @@ opacity: 0.42; } +.board-grid-entry[data-board-item-selected="true"] { + z-index: 15; + opacity: 0.56; + filter: saturate(0.72) brightness(0.82); + transition: + opacity 140ms ease, + filter 140ms ease; +} + +.board-grid-entry[data-board-item-selected="true"]::after { + position: absolute; + inset: 0; + z-index: 100; + content: ""; + background: color-mix(in srgb, var(--mantine-primary-color-filled) 12%, transparent); + border: 0.125rem dashed var(--mantine-primary-color-filled); + border-radius: var(--board-item-radius, var(--mantine-radius-md)); + box-shadow: inset 0 0 1.125rem color-mix(in srgb, var(--mantine-primary-color-filled) 20%, transparent); + pointer-events: none; +} + .board-grid-drag-overlay[data-dnd-dragging] { position: fixed !important; z-index: 10000; @@ -177,138 +198,115 @@ body[data-board-grid-interacting="resize"] .board-grid-entry[data-dnd-active="tr .board-grid-resize-handle { position: absolute; - z-index: 20; - display: block; + z-index: 25; + display: grid; padding: 0; + color: var(--mantine-color-dimmed); appearance: none; - background: transparent; - border: 0; + background: var(--mantine-color-body); + border: 0.0625rem solid var(--mantine-color-default-border); + box-shadow: var(--mantine-shadow-xs); + opacity: 0; + scale: 0.88; + place-items: center; touch-action: none; + transition: + opacity 120ms ease, + scale 160ms cubic-bezier(0.22, 1, 0.36, 1), + border-color 120ms ease; } -body[data-board-grid-interacting] .board-grid-resize-handle { +.board-grid-resize-handle::before { + position: absolute; + inset: -0.375rem; + content: ""; +} + +.board-grid-resize-handle > svg { pointer-events: none; } -body[data-board-grid-interacting] .board-grid-resize-handle::after { - opacity: 0; -} - -.board-grid-resize-handle::after { - position: absolute; - inset: 50% auto auto 50%; - width: 12px; - height: 3px; - content: ""; - background: color-mix(in srgb, var(--mantine-color-text) 70%, transparent); - border-radius: var(--mantine-radius-xl); - opacity: 0; - transform: translate(-50%, -50%); - transition: opacity 100ms ease; -} - -.board-grid-entry:hover > .board-grid-resize-handle::after, -.board-grid-resize-handle:focus-visible::after { +.board-grid-entry:has(> .board-grid-resize-handle:hover) > .board-grid-resize-handle, +.board-grid-entry:has(> .board-grid-resize-handle:active) > .board-grid-resize-handle { + border-color: var(--mantine-color-dimmed); opacity: 1; -} - -.board-grid-resize-handle:focus-visible { - outline: 2px solid var(--mantine-primary-color-filled); - outline-offset: -8px; + scale: 1; } .board-grid-resize-handle[data-grid-resize-handle="n"], .board-grid-resize-handle[data-grid-resize-handle="s"] { - right: 44px; - left: 44px; - height: max(32px, calc(25px * var(--board-canvas-inverse-scale, 1))); + left: 50%; + width: 1.75rem; + height: 1rem; + border-radius: var(--mantine-radius-xl); + translate: -50% 0; cursor: ns-resize; } .board-grid-resize-handle[data-grid-resize-handle="n"] { - top: 0; + top: -0.25rem; } .board-grid-resize-handle[data-grid-resize-handle="s"] { - bottom: 0; + bottom: -0.25rem; } .board-grid-resize-handle[data-grid-resize-handle="e"], .board-grid-resize-handle[data-grid-resize-handle="w"] { - top: 44px; - bottom: 44px; - width: max(32px, calc(25px * var(--board-canvas-inverse-scale, 1))); + top: 50%; + width: 1rem; + height: 1.75rem; + border-radius: var(--mantine-radius-xl); + translate: 0 -50%; cursor: ew-resize; } .board-grid-resize-handle[data-grid-resize-handle="e"] { - right: 0; + right: -0.25rem; } .board-grid-resize-handle[data-grid-resize-handle="w"] { - left: 0; -} - -.board-grid-resize-handle[data-grid-resize-handle="e"]::after, -.board-grid-resize-handle[data-grid-resize-handle="w"]::after { - width: 3px; - height: 12px; + left: -0.25rem; } .board-grid-resize-handle[data-grid-resize-handle="ne"], .board-grid-resize-handle[data-grid-resize-handle="se"], .board-grid-resize-handle[data-grid-resize-handle="sw"], .board-grid-resize-handle[data-grid-resize-handle="nw"] { - z-index: 9; - width: max(44px, calc(25px * var(--board-canvas-inverse-scale, 1))); - height: max(44px, calc(25px * var(--board-canvas-inverse-scale, 1))); + width: 1.875rem; + height: 1.875rem; + border-radius: var(--board-item-radius, var(--mantine-radius-md)); } .board-grid-resize-handle[data-grid-resize-handle="ne"] { - top: 0; - right: 0; + top: -0.625rem; + right: -0.625rem; cursor: nesw-resize; } .board-grid-resize-handle[data-grid-resize-handle="se"] { - right: 0; - bottom: 0; + right: -0.625rem; + bottom: -0.625rem; cursor: nwse-resize; } .board-grid-resize-handle[data-grid-resize-handle="sw"] { - bottom: 0; - left: 0; + bottom: -0.625rem; + left: -0.625rem; cursor: nesw-resize; } .board-grid-resize-handle[data-grid-resize-handle="nw"] { - top: 0; - left: 0; + top: -0.625rem; + left: -0.625rem; cursor: nwse-resize; } -.board-grid-resize-handle[data-grid-resize-handle="ne"]::after, -.board-grid-resize-handle[data-grid-resize-handle="sw"]::after { - transform: translate(-50%, -50%) rotate(-45deg); -} - -.board-grid-resize-handle[data-grid-resize-handle="se"]::after, -.board-grid-resize-handle[data-grid-resize-handle="nw"]::after { - transform: translate(-50%, -50%) rotate(45deg); -} - -@media (hover: none), (pointer: coarse) { - .board-grid-resize-handle::after { - opacity: 1; - } -} - @media (prefers-reduced-motion: reduce) { .board-grid-entry, .board-grid-placeholder, .board-grid-resize-outline, - .board-grid-resize-handle::after { + .board-grid-resize-handle { transition: none; } } diff --git a/apps/nextjs/src/components/board/sections/grid/grid-editor.tsx b/apps/nextjs/src/components/board/sections/grid/grid-editor.tsx index af6474a58..428e2a939 100644 --- a/apps/nextjs/src/components/board/sections/grid/grid-editor.tsx +++ b/apps/nextjs/src/components/board/sections/grid/grid-editor.tsx @@ -32,6 +32,7 @@ import type { DragStartEvent, } from "@dnd-kit/react"; import { DragDropProvider, DragOverlay, useDragDropManager, useDraggable, useDroppable } from "@dnd-kit/react"; +import { IconGripHorizontal, IconGripVertical } from "@tabler/icons-react"; import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context"; import { useI18n } from "@homarr/translation/client"; @@ -61,6 +62,7 @@ import { useBoardGridPortalHost } from "./grid-portal-host"; import type { GridInteraction } from "./grid-preview-layer"; import { GridPreviewLayer } from "./grid-preview-layer"; import { createGridResizeOutlineController } from "./grid-resize-outline"; +import { useBoardSelection } from "~/components/board/selection/board-selection-context"; import type { CommitSectionGridInput, SectionGridPlacement } from "./use-grid-layout-actions"; import { useGridLayoutActions } from "./use-grid-layout-actions"; @@ -846,6 +848,9 @@ const DndGridEntryComponent = ({ }: DndGridEntryProps) => { const { acquireContainer, structureRevision } = useBoardGridPortalHost(); const { beginResize, previewResize, completeResize, cancelResize } = useBoardGridEditorActions(); + const { isSelected } = useBoardSelection(); + const board = useRequiredBoard(); + const isSelectedEntry = placement.type === "item" && isSelected(placement.id); const mountRef = useRef(null); const shellRef = useRef(null); const placementRef = useRef(placement); @@ -921,7 +926,12 @@ const DndGridEntryComponent = ({
( -