diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_client.module.css b/apps/nextjs/src/app/[locale]/boards/(content)/_client.module.css index 00ca097b5..b60ab124a 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_client.module.css +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_client.module.css @@ -19,35 +19,11 @@ align-self: start; box-sizing: border-box; height: var(--board-gutter-viewport-height); - padding-top: 24px; - margin-top: -24px; overflow: clip; } .gutter[data-board-editing="true"] { - outline: 1px dashed var(--mantine-color-default-border); - outline-offset: -1px; + outline: 0.0625rem solid var(--mantine-color-default-border); + outline-offset: -0.0625rem; border-radius: var(--mantine-radius-md); } - -.gutter[data-board-editing="true"]::before { - position: sticky; - top: 0; - inset-inline-start: 0; - z-index: 2; - display: block; - width: fit-content; - height: 24px; - padding-inline: var(--mantine-spacing-xs); - margin-top: -24px; - overflow: hidden; - font-size: var(--mantine-font-size-xs); - line-height: 24px; - color: var(--mantine-color-dimmed); - text-overflow: ellipsis; - white-space: nowrap; - pointer-events: none; - content: attr(aria-label); - background: color-mix(in srgb, var(--mantine-color-body) 88%, transparent); - border-radius: var(--mantine-radius-sm); -} diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx index 7a0f7f40f..7c9ab7c4e 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx @@ -1,5 +1,6 @@ "use client"; +import type { PropsWithChildren } from "react"; import { Box, Paper, Stack, Text } from "@mantine/core"; import { IconBulb } from "@tabler/icons-react"; @@ -22,6 +23,7 @@ import { import { ScaledBoardCanvas } from "~/components/board/layout/scaled-board-canvas"; import { BoardEmptySection } from "~/components/board/sections/empty-section"; import { BoardGridEditorBoundary } from "~/components/board/sections/grid/board-grid-editor-boundary"; +import { GridEditorRegistryProvider } from "~/components/board/sections/grid/grid-editor-registry"; import { BoardGridPortalHost } from "~/components/board/sections/grid/grid-portal-host"; import { BoardSectionCollapseProvider } from "~/components/board/sections/section-collapse"; import { BoardBackgroundVideo } from "~/components/layout/background"; @@ -31,6 +33,12 @@ import classes from "./_client.module.css"; const APP_SHELL_INLINE_PADDING = 32; +const BoardSelectionGridProvider = ({ children }: PropsWithChildren) => ( + + {children} + +); + export const ClientBoard = () => { const board = useRequiredBoard(); const t = useI18n("board.landmark"); @@ -68,7 +76,7 @@ export const ClientBoard = () => { .join(" "); const content = ( - + @@ -144,7 +152,7 @@ export const ClientBoard = () => { - + ); if (representativeWidth === null) return content; diff --git a/apps/nextjs/src/components/assistant/assistant-panel.module.css b/apps/nextjs/src/components/assistant/assistant-panel.module.css index 1abba2121..a7e46f6ce 100644 --- a/apps/nextjs/src/components/assistant/assistant-panel.module.css +++ b/apps/nextjs/src/components/assistant/assistant-panel.module.css @@ -157,30 +157,12 @@ border-bottom: 1px solid var(--mantine-color-default-border); } -.panelIdentity { - min-width: 0; - flex: 1 1 auto; -} - -.panelIdentityIcon { - flex: 0 0 auto; -} - -.panelIdentityTitle, -.panelThreadTitle { - max-width: 14rem; -} - .panelActions { min-width: 0; flex: 0 0 auto; margin-inline-start: auto; } -.newConversationButton { - flex: 0 0 auto; -} - .panelAction { width: 2.625rem; height: 2.625rem; @@ -207,21 +189,6 @@ display: none; } - .newConversationButton { - width: 2.625rem; - max-width: 2.625rem; - height: 2.625rem; - padding-inline: 0.75rem; - } - - .newConversationButton :global(.mantine-Button-section) { - margin-inline: 0; - } - - .newConversationButton :global(.mantine-Button-label) { - display: none; - } - .messages { padding: 0.5rem 0.65rem 1rem; } @@ -277,8 +244,7 @@ @container assistant-widget (max-width: 22rem) { .panelAction, - .historyButton, - .newConversationButton { + .historyButton { width: 2.25rem; max-width: 2.25rem; height: 2.25rem; @@ -301,10 +267,6 @@ .composerContextLabel { display: none; } - - .panelThreadTitle { - display: none; - } } @container assistant-widget (max-width: 18rem) { diff --git a/apps/nextjs/src/components/assistant/assistant-panel.tsx b/apps/nextjs/src/components/assistant/assistant-panel.tsx index bdeb8ed45..a8b8eea9b 100644 --- a/apps/nextjs/src/components/assistant/assistant-panel.tsx +++ b/apps/nextjs/src/components/assistant/assistant-panel.tsx @@ -2368,7 +2368,7 @@ const EmptyThread = () => { - + @@ -2957,7 +2957,6 @@ const Composer = (props: ComposerProps) => { ) : ( void; } -const AssistantPanelIdentity = () => { - const t = useI18n("assistant"); - const threadTitle = useAuiState((state) => state.threadListItem.title); - - return ( - - - - - - - {t("title")} - - - {threadTitle ?? t("newConversation")} - - - - ); -}; - export const AssistantConversationSurface = ({ isRunning, pendingAction, @@ -3212,23 +3190,20 @@ export const AssistantConversationSurface = ({ return ( - - + + {onExpand && ( diff --git a/apps/nextjs/src/components/assistant/assistant-spotlight.tsx b/apps/nextjs/src/components/assistant/assistant-spotlight.tsx index f48888ccc..f82a52d39 100644 --- a/apps/nextjs/src/components/assistant/assistant-spotlight.tsx +++ b/apps/nextjs/src/components/assistant/assistant-spotlight.tsx @@ -16,7 +16,7 @@ const AssistantPromptDetail = () => { const t = useI18n("assistant"); return ( - + diff --git a/apps/nextjs/src/components/assistant/assistant-widget.tsx b/apps/nextjs/src/components/assistant/assistant-widget.tsx index 1b7acee3e..1fee8e930 100644 --- a/apps/nextjs/src/components/assistant/assistant-widget.tsx +++ b/apps/nextjs/src/components/assistant/assistant-widget.tsx @@ -137,7 +137,7 @@ const CompactAssistantWidget = ({ - + @@ -167,13 +167,7 @@ interface AssistantWidgetStateProps { children?: ReactNode; } -const AssistantWidgetState = ({ - icon: Icon, - title, - description, - color = "red", - children, -}: AssistantWidgetStateProps) => ( +const AssistantWidgetState = ({ icon: Icon, title, description, color, children }: AssistantWidgetStateProps) => (
diff --git a/apps/nextjs/src/components/board/items/item-move-modal.tsx b/apps/nextjs/src/components/board/items/item-move-modal.tsx index cded56a68..c0beae1fd 100644 --- a/apps/nextjs/src/components/board/items/item-move-modal.tsx +++ b/apps/nextjs/src/components/board/items/item-move-modal.tsx @@ -11,6 +11,7 @@ import { useI18n } from "@homarr/translation/client"; import type { Board, ContainerSectionItem, SectionItem } from "~/app/[locale]/boards/_types"; import { getLayoutRowCount } from "../layout"; import { resolvePinnedGridCollisions } from "../sections/grid/dnd"; +import { useRegisteredGridEditors } from "../sections/grid/grid-editor-registry"; import { getSectionGridColumnCount, getSectionGridPlacements } from "../sections/grid/section-grid-placements"; import type { CommitSectionGridInput, SectionGridPlacement } from "../sections/grid/use-grid-layout-actions"; import { useGridLayoutActions } from "../sections/grid/use-grid-layout-actions"; @@ -57,26 +58,29 @@ export const ItemMoveModal = createModal(({ actions, innerProps }) = const tContainer = useI18n("section.container"); const tMoveResize = useI18n("item.moveResize"); const { board, commitSectionGrids, currentLayoutId, entry, sourceSectionId } = innerProps; + const registeredGridEditors = useRegisteredGridEditors(); const minimumSize = useMemo( () => getEntryMinimumSize(board, currentLayoutId, entry), [board, currentLayoutId, entry], ); - const targets = useMemo( - () => - getMoveTargets(board, currentLayoutId, entry, { - canvas: tLandmark("canvas"), - container: tContainer("action.create"), - leftRail: tLandmark("leftRail"), - rightRail: tLandmark("rightRail"), - numbered: (name, index) => tMoveResize("target.numbered", { name, index: String(index) }), - located: (name, location, index) => - tMoveResize("target.located", { - name, - location, - index: String(index), - }), - }), - [board, currentLayoutId, entry, tContainer, tLandmark, tMoveResize], + const targets = getMoveTargets( + board, + currentLayoutId, + entry, + { + canvas: tLandmark("canvas"), + container: tContainer("action.create"), + leftRail: tLandmark("leftRail"), + rightRail: tLandmark("rightRail"), + numbered: (name, index) => tMoveResize("target.numbered", { name, index: String(index) }), + located: (name, location, index) => + tMoveResize("target.located", { + name, + location, + index: String(index), + }), + }, + registeredGridEditors, ); const targetById = useMemo(() => new Map(targets.map((target) => [target.id, target])), [targets]); const initialTarget = targetById.get(sourceSectionId) ?? targets[0]; @@ -252,6 +256,7 @@ export const getMoveTargets = ( layoutId: string, entry: MovableEntry, labels: MoveTargetLabels, + registeredGridEditors?: ReadonlyMap, ): MoveTarget[] => { const minimum = getEntryMinimumSize(board, layoutId, entry); @@ -260,13 +265,15 @@ export const getMoveTargets = ( const columnCount = getSectionColumnCount(board, layoutId, section.id); const lane = getRootSectionLane(section.xOffset); if (columnCount === 0) return []; + const registeredEditor = registeredGridEditors?.get(section.id); + if (lane !== "main" && registeredGridEditors && !registeredEditor) return []; return minimum.width <= columnCount ? [ { id: section.id, name: lane === "left" ? labels.leftRail : lane === "right" ? labels.rightRail : labels.canvas, columnCount, - maxRowCount: getSectionMaxRowCount(board, layoutId, section.id), + maxRowCount: registeredEditor?.placementMaxRowCount ?? getSectionMaxRowCount(board, layoutId, section.id), }, ] : []; diff --git a/apps/nextjs/src/components/board/layout/README.md b/apps/nextjs/src/components/board/layout/README.md index 12ac371d0..6eb9c412f 100644 --- a/apps/nextjs/src/components/board/layout/README.md +++ b/apps/nextjs/src/components/board/layout/README.md @@ -3,8 +3,8 @@ The board uses fixed logical units: - one `1 × 1` item is exactly `200 × 200` logical pixels; -- the gap is always `24` logical pixels, giving a `224` pixel track pitch; -- multi-cell items include the gaps between their cells (`2 × 1` is `424 × 200`); +- the gap matches Mantine's `sm` spacing at `12` logical pixels, giving a `212` pixel track pitch; +- multi-cell items include the gaps between their cells (`2 × 1` is `412 × 200`); - the canvas is zoomed once with one uniform scale derived from its available width. Items keep the same logical dimensions at every viewport. The viewport reserves diff --git a/apps/nextjs/src/components/board/layout/constants.ts b/apps/nextjs/src/components/board/layout/constants.ts index 26e3f0765..e4f309700 100644 --- a/apps/nextjs/src/components/board/layout/constants.ts +++ b/apps/nextjs/src/components/board/layout/constants.ts @@ -3,7 +3,7 @@ * individual cards never change their internal layout at different viewports. */ export const LOGICAL_GRID_CELL_SIZE = 200; -export const LOGICAL_GRID_GAP = 24; +export const LOGICAL_GRID_GAP = 12; export const LOGICAL_GRID_PITCH = LOGICAL_GRID_CELL_SIZE + LOGICAL_GRID_GAP; export const COLLAPSED_SECTION_ROW_COUNT = 0.5; diff --git a/apps/nextjs/src/components/board/layout/scaled-board-canvas.module.css b/apps/nextjs/src/components/board/layout/scaled-board-canvas.module.css index 014c503b6..e0f3b6d65 100644 --- a/apps/nextjs/src/components/board/layout/scaled-board-canvas.module.css +++ b/apps/nextjs/src/components/board/layout/scaled-board-canvas.module.css @@ -23,8 +23,8 @@ * zoom and enlarges everything together. */ --mantine-scale: var(--board-canvas-ui-scale, 1); - /* Keep the legacy card inset visually stable while the fixed canvas is zoomed. */ - --board-grid-item-inset: calc(10px * var(--board-canvas-inverse-scale, 1) - 12px); + /* Keep a subtle boundary inset without widening the visual gap too much. */ + --board-grid-item-inset: calc(var(--mantine-spacing-xs) / 2); --mantine-spacing-xs: calc(0.625rem * var(--board-canvas-ui-scale, 1)); --mantine-spacing-sm: calc(0.75rem * var(--board-canvas-ui-scale, 1)); --mantine-spacing-md: calc(1rem * var(--board-canvas-ui-scale, 1)); diff --git a/apps/nextjs/src/components/board/layout/test/geometry.spec.ts b/apps/nextjs/src/components/board/layout/test/geometry.spec.ts index 3d46398bb..0db173bb6 100644 --- a/apps/nextjs/src/components/board/layout/test/geometry.spec.ts +++ b/apps/nextjs/src/components/board/layout/test/geometry.spec.ts @@ -9,18 +9,18 @@ import { } from "../index"; describe("fixed dashboard geometry", () => { - test("keeps a 1x1 item exactly 200px with a fixed 24px gap", () => { + test("keeps a 1x1 item exactly 200px with a fixed 12px gap", () => { expect(getLogicalTrackSize(1)).toBe(200); - expect(getLogicalTrackSize(0.5)).toBe(88); - expect(getLogicalTrackSize(2)).toBe(424); - expect(getLogicalTrackSize(3)).toBe(648); + expect(getLogicalTrackSize(0.5)).toBe(94); + expect(getLogicalTrackSize(2)).toBe(412); + expect(getLogicalTrackSize(3)).toBe(624); expect(getLogicalItemStyle({ x: 1, y: 2, w: 2, h: 3 })).toEqual({ position: "absolute", - left: 224, - top: 448, - width: 424, - height: 648, + left: 212, + top: 424, + width: 412, + height: 624, }); }); @@ -35,7 +35,7 @@ describe("fixed dashboard geometry", () => { expect(getGridRowCountForVisualHeight(1200, 1)).toBe(6); expect(getLogicalTrackSize(getGridRowCountForVisualHeight(1200, 1))).toBeGreaterThanOrEqual(1200); - expect(getGridRowCountForVisualHeight(1200, 0.5)).toBe(11); + expect(getGridRowCountForVisualHeight(1200, 0.5)).toBe(12); expect(getLogicalTrackSize(getGridRowCountForVisualHeight(1200, 0.5)) * 0.5).toBeGreaterThanOrEqual(1200); }); }); diff --git a/apps/nextjs/src/components/board/sections/container-section.tsx b/apps/nextjs/src/components/board/sections/container-section.tsx index b18777dba..1c21a311e 100644 --- a/apps/nextjs/src/components/board/sections/container-section.tsx +++ b/apps/nextjs/src/components/board/sections/container-section.tsx @@ -42,7 +42,18 @@ export const BoardContainerSection = ({ section }: Props) => { const labelRight = isEditMode ? 48 : options.showOpenAll ? 40 : 8; return ( - + { styles={{ root: { overflow: "visible", - "--opacity": board.opacity / 100, - "--border-color": options.borderColor || undefined, }, }} radius={board.itemRadius} @@ -66,7 +75,7 @@ export const BoardContainerSection = ({ section }: Props) => { )} - {!isVisuallyCollapsed && !options.collapsible && options.showLabel && options.title && ( - - {options.title} - - )} {options.showOpenAll && !isEditMode && ( { + {!isVisuallyCollapsed && !options.collapsible && options.showLabel && options.title && ( + + {options.title} + + )} {isEditMode && } ); diff --git a/apps/nextjs/src/components/board/sections/grid/board-grid-editor-boundary.tsx b/apps/nextjs/src/components/board/sections/grid/board-grid-editor-boundary.tsx index ceb64c6de..0e081e0a1 100644 --- a/apps/nextjs/src/components/board/sections/grid/board-grid-editor-boundary.tsx +++ b/apps/nextjs/src/components/board/sections/grid/board-grid-editor-boundary.tsx @@ -7,7 +7,7 @@ import { createPortal } from "react-dom"; import { useEditMode } from "@homarr/boards/edit-mode"; import { getLoadedGridEditorModule, loadGridEditorAsync } from "./grid-editor-loader"; -import { GridEditorRegistryProvider, useRegisteredGridEditors } from "./grid-editor-registry"; +import { useRegisteredGridEditors } from "./grid-editor-registry"; import { GridEditorRuntimeProvider } from "./grid-editor-runtime"; type BoardGridEditorProviderComponent = ComponentType; @@ -54,14 +54,14 @@ export const BoardGridEditorBoundary = ({ children }: PropsWithChildren) => { const GridEditor = resolvedEditorModule?.default ?? null; return ( - + <> {children} {isEditMode && Provider && GridEditor && ( )} - + ); }; diff --git a/apps/nextjs/src/components/board/sections/grid/dnd/coordinates.spec.ts b/apps/nextjs/src/components/board/sections/grid/dnd/coordinates.spec.ts index 5468f2d5d..93b387de7 100644 --- a/apps/nextjs/src/components/board/sections/grid/dnd/coordinates.spec.ts +++ b/apps/nextjs/src/components/board/sections/grid/dnd/coordinates.spec.ts @@ -23,7 +23,7 @@ describe("dnd grid coordinates", () => { test("uses the dragged shape origin so a non-center grab retains its pointer offset", () => { const scale = 0.75; const target = { left: -280, top: 390, width: getLogicalTrackSize(5) * scale }; - const initialShape = { left: target.left, top: target.top, width: 424 * scale, height: 200 * scale }; + const initialShape = { left: target.left, top: target.top, width: 412 * scale, height: 200 * scale }; const pointerDelta = { x: LOGICAL_GRID_PITCH * scale * 2, y: LOGICAL_GRID_PITCH * scale * 3 }; expect( diff --git a/apps/nextjs/src/components/board/sections/grid/grid-editor-registry.tsx b/apps/nextjs/src/components/board/sections/grid/grid-editor-registry.tsx index aa7839e65..09cba728d 100644 --- a/apps/nextjs/src/components/board/sections/grid/grid-editor-registry.tsx +++ b/apps/nextjs/src/components/board/sections/grid/grid-editor-registry.tsx @@ -55,6 +55,7 @@ export interface GridEditorRenderProps { } export interface RegisteredGridEditor extends GridEditorRenderProps { + placementMaxRowCount: number | null; host: HTMLElement; disabled: boolean; } 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 798a2c970..53079451f 100644 --- a/apps/nextjs/src/components/board/sections/grid/grid-editor.css +++ b/apps/nextjs/src/components/board/sections/grid/grid-editor.css @@ -155,17 +155,6 @@ body[data-board-grid-interacting="resize"] .board-grid-entry[data-dnd-active="tr box-shadow: inset 0 0 0 2px var(--mantine-color-red-filled); } -[data-section-kind="empty"][data-rail-placement="main"] > .board-grid-editor[data-dnd-drop-target="true"] { - outline: 2px solid color-mix(in srgb, var(--mantine-primary-color-filled) 60%, transparent); - outline-offset: 4px; - box-shadow: none; -} - -[data-section-kind="empty"][data-rail-placement="main"] - > .board-grid-editor[data-dnd-drop-target="true"][data-dnd-drop-valid="false"] { - outline-color: var(--mantine-color-red-filled); -} - [data-board-container-collapsed] > [data-board-container-body] > [data-section-id] diff --git a/apps/nextjs/src/components/board/sections/grid/section-grid.module.css b/apps/nextjs/src/components/board/sections/grid/section-grid.module.css index 12a8904d9..c08746422 100644 --- a/apps/nextjs/src/components/board/sections/grid/section-grid.module.css +++ b/apps/nextjs/src/components/board/sections/grid/section-grid.module.css @@ -50,6 +50,10 @@ overflow: visible; } +.staticItem:has([data-board-container-label]) { + z-index: 2; +} + .editorEntry { overflow: visible; cursor: grab; diff --git a/apps/nextjs/src/components/board/sections/grid/section-grid.tsx b/apps/nextjs/src/components/board/sections/grid/section-grid.tsx index 959287b17..cec4616c9 100644 --- a/apps/nextjs/src/components/board/sections/grid/section-grid.tsx +++ b/apps/nextjs/src/components/board/sections/grid/section-grid.tsx @@ -130,6 +130,10 @@ export const SectionGrid = ({ const contentRowCount = Math.max(1, getLayoutRowCount(displayPlacements)); const rowCount = Math.max(contentRowCount, requestedRowCount, minimumViewportRowCount); const maxRowCount = section.kind === "container" || railPlacement !== "main" ? rowCount : null; + let placementMaxRowCount = maxRowCount; + if (railPlacement !== "main") { + placementMaxRowCount = minimumViewportRowCount; + } // A scrollable container isn't forced to grow with its content - it scrolls internally instead // of expanding to fit every widget, so its viewport height is capped independently of rowCount. const isScrollableContainer = section.kind === "container" && section.options.scrollable; @@ -175,6 +179,7 @@ export const SectionGrid = ({ columnCount, rowCount, maxRowCount, + placementMaxRowCount, placements: displayPlacements, transactionPlacements: placements, className: editorClassName, @@ -190,6 +195,7 @@ export const SectionGrid = ({ entryElementStore, isInteractionDisabled, maxRowCount, + placementMaxRowCount, placements, rowCount, section, diff --git a/apps/nextjs/src/components/board/sections/item.module.css b/apps/nextjs/src/components/board/sections/item.module.css index 319eb7b07..006e98e25 100644 --- a/apps/nextjs/src/components/board/sections/item.module.css +++ b/apps/nextjs/src/components/board/sections/item.module.css @@ -1,4 +1,4 @@ -.itemCard { +.containerRoot { @mixin dark { --background-color: rgb(from var(--mantine-color-dark-6) r g b / var(--opacity)); --border-color: rgb(from var(--mantine-color-dark-4) r g b / var(--opacity)); @@ -7,6 +7,9 @@ --background-color: rgb(from var(--mantine-color-white) r g b / var(--opacity)); --border-color: rgb(from var(--mantine-color-gray-3) r g b / var(--opacity)); } +} + +.itemCard { background-color: var(--background-color) !important; border-color: var(--border-color) !important; } @@ -42,6 +45,21 @@ white-space: nowrap; } +.containerLabel { + overflow: visible; + pointer-events: none; + background-color: var(--background-color); + border-color: var(--border-color); +} + +.containerLabel :global(.mantine-Badge-label) { + min-width: 0; + overflow: hidden; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + .containerBody { visibility: visible; opacity: 1; diff --git a/apps/nextjs/src/components/board/selection/board-selection-context.tsx b/apps/nextjs/src/components/board/selection/board-selection-context.tsx index fb6f3ec6a..341017309 100644 --- a/apps/nextjs/src/components/board/selection/board-selection-context.tsx +++ b/apps/nextjs/src/components/board/selection/board-selection-context.tsx @@ -31,7 +31,7 @@ interface BoardSelectionContextValue { copySelectedItems: () => Promise; pasteItems: () => Promise; removeSelectedItems: () => void; - moveSelectedItemsToSection: (sectionId: string) => void; + moveSelectedItemsToSection: (sectionId: string, maxRowCount?: number | null) => void; } const BoardSelectionContext = createContext(null); @@ -229,7 +229,7 @@ export const BoardSelectionProvider = ({ children }: PropsWithChildren) => { }, [board.items, clearSelection, persistBoard, selectedItemIds, tCommon, tSelection]); const moveSelectedItemsToSection = useCallback( - (targetSectionId: string) => { + (targetSectionId: string, maxRowCount?: number | null) => { if (selectedItemIds.size === 0) return; const idsToMove = new Set(selectedItemIds); @@ -248,7 +248,7 @@ export const BoardSelectionProvider = ({ children }: PropsWithChildren) => { targetSection.kind === "container" ? (containerLayout?.width ?? 0) : getBoardLaneColumnCount(boardLayout, getRootSectionLane(targetSection.xOffset)); - const rowCount = containerLayout?.height ?? 9999; + const rowCount = containerLayout?.height ?? maxRowCount ?? 9999; if (columnCount === 0 || rowCount === 0) return previous; const existingContainers = previous.sections diff --git a/apps/nextjs/src/components/board/selection/board-selection-toolbar.tsx b/apps/nextjs/src/components/board/selection/board-selection-toolbar.tsx index 4cd1fc118..c377ae166 100644 --- a/apps/nextjs/src/components/board/selection/board-selection-toolbar.tsx +++ b/apps/nextjs/src/components/board/selection/board-selection-toolbar.tsx @@ -25,6 +25,7 @@ import { useI18n } from "@homarr/translation/client"; import { widgetCatalogIcons } from "@homarr/widgets/catalog"; import type { EmptySection } from "~/app/[locale]/boards/_types"; +import { useRegisteredGridEditors } from "~/components/board/sections/grid/grid-editor-registry"; import { useBoardSelection } from "./board-selection-context"; const MAX_AVATARS_DISPLAYED = 5; @@ -40,6 +41,7 @@ export const BoardSelectionToolbar = () => { moveSelectedItemsToSection, } = useBoardSelection(); const board = useRequiredBoard(); + const registeredGridEditors = useRegisteredGridEditors(); const currentLayoutId = useCurrentLayout(); const currentLayout = board.layouts.find((layout) => layout.id === currentLayoutId); const t = useI18n(); @@ -96,7 +98,15 @@ export const BoardSelectionToolbar = () => { .at(0); const placementOptions = [ - ...(mainCanvasSection ? [{ value: mainCanvasSection.id, label: t("item.create.destination.mainCanvas") }] : []), + ...(mainCanvasSection + ? [ + { + value: mainCanvasSection.id, + label: t("item.create.destination.mainCanvas"), + maxRowCount: null, + }, + ] + : []), ...board.sections.flatMap((section) => { if (section.kind !== "empty") return []; const lane = getRootSectionLane(section.xOffset); @@ -105,6 +115,7 @@ export const BoardSelectionToolbar = () => { { value: section.id, label: t(`item.create.destination.${lane === "left" ? "leftRail" : "rightRail"}`), + maxRowCount: registeredGridEditors.get(section.id)?.placementMaxRowCount, }, ]; }), @@ -113,6 +124,7 @@ export const BoardSelectionToolbar = () => { .map((section) => ({ value: section.id, label: section.options.title.trim() || t("section.container.untitled"), + maxRowCount: section.layouts.find((layout) => layout.layoutId === currentLayoutId)?.height, })), ]; @@ -244,7 +256,11 @@ export const BoardSelectionToolbar = () => { {tSelection("moveTo")} {placementOptions.map((opt) => ( - moveSelectedItemsToSection(opt.value)}> + moveSelectedItemsToSection(opt.value, opt.maxRowCount)} + > {opt.label} ))} diff --git a/packages/widgets/src/archive-team-warrior/component.tsx b/packages/widgets/src/archive-team-warrior/component.tsx index d221338ef..81c1c64c4 100644 --- a/packages/widgets/src/archive-team-warrior/component.tsx +++ b/packages/widgets/src/archive-team-warrior/component.tsx @@ -82,7 +82,7 @@ const ArchiveTeamWarriorWidgetContent = ({ {(isAdvanced || options.showBroadcastMessage) && status.broadcastMessage && layout.showBroadcast && ( - + {status.broadcastMessage} @@ -143,7 +143,13 @@ const ArchiveTeamWarriorWidgetContent = ({ {status.items.map((item) => { const itemStatusKey = getStatusKey(item.status); return ( - + @@ -201,6 +207,9 @@ export const getArchiveCompactLayout = (width: number, height: number, isAdvance const formatBandwidth = (value?: number) => formatByteRate(Math.round(value ?? 0)); +const neutralSurfaceBorder = + "rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45))"; + type WarriorStatusKey = "running" | "completed" | "failed" | "canceled" | "stopped" | "idle" | "unknown"; const getStatusKey = (status?: string | null): WarriorStatusKey => { diff --git a/packages/widgets/src/assistant/component.tsx b/packages/widgets/src/assistant/component.tsx index 2f5f61cf3..242532d0d 100644 --- a/packages/widgets/src/assistant/component.tsx +++ b/packages/widgets/src/assistant/component.tsx @@ -17,7 +17,7 @@ export default function AssistantWidget(props: WidgetComponentProps<"assistant"> return (
- + diff --git a/packages/widgets/src/audio-stats/component.module.css b/packages/widgets/src/audio-stats/component.module.css index 84b3a447c..4a16abfb6 100644 --- a/packages/widgets/src/audio-stats/component.module.css +++ b/packages/widgets/src/audio-stats/component.module.css @@ -40,7 +40,7 @@ gap: clamp(2px, 1.5cqi, 4px); padding: clamp(4px, 2cqi, 8px); border-radius: var(--mantine-radius-md); - background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6)); + background-color: transparent; overflow: hidden; } diff --git a/packages/widgets/src/bazarr/component.module.css b/packages/widgets/src/bazarr/component.module.css index f9f44256c..90c5ba4bd 100644 --- a/packages/widgets/src/bazarr/component.module.css +++ b/packages/widgets/src/bazarr/component.module.css @@ -29,7 +29,7 @@ gap: clamp(2px, 1.5cqi, 5px); padding: clamp(4px, 2.5cqi, 10px); border-radius: var(--mantine-radius-md); - background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6)); + background-color: transparent; overflow: hidden; } diff --git a/packages/widgets/src/beszel-system-grid/component.module.css b/packages/widgets/src/beszel-system-grid/component.module.css index b4854aa30..41bc5efed 100644 --- a/packages/widgets/src/beszel-system-grid/component.module.css +++ b/packages/widgets/src/beszel-system-grid/component.module.css @@ -1,7 +1,3 @@ -.progress { - opacity: max(0.6, var(--opacity, 1)); -} - .clickableCard { position: relative; transition: background-color 150ms; diff --git a/packages/widgets/src/beszel-system-grid/component.tsx b/packages/widgets/src/beszel-system-grid/component.tsx index ae04f9ffb..260e33ed7 100644 --- a/packages/widgets/src/beszel-system-grid/component.tsx +++ b/packages/widgets/src/beszel-system-grid/component.tsx @@ -159,7 +159,6 @@ const MetricValue = ({ value, progress, size }: Pick {progress && ( div > div.bookmarkIcon { background-color: var(--mantine-color-iconColor-filled-hover); } @@ -23,3 +25,9 @@ .bookmarkButton:focus-visible .bookmarkIcon { background-color: var(--mantine-color-iconColor-filled-hover); } + +@media (prefers-reduced-motion: reduce) { + .card { + transition: none; + } +} diff --git a/packages/widgets/src/bookmarks/component.tsx b/packages/widgets/src/bookmarks/component.tsx index de8cd67ee..38220d569 100644 --- a/packages/widgets/src/bookmarks/component.tsx +++ b/packages/widgets/src/bookmarks/component.tsx @@ -16,7 +16,6 @@ import { UnstyledButton, } from "@mantine/core"; import { IconLink } from "@tabler/icons-react"; -import combineClasses from "clsx"; import { clientApi } from "@homarr/api/client"; import { useRequiredBoard } from "@homarr/boards/context"; @@ -216,6 +215,7 @@ const FlexLayout = ({ - + ({ withTableBorder={false} borderRadius={0} highlightOnHover - striped="odd" - stripedColor={{ dark: "dark.7", light: "gray.0" }} highlightOnHoverColor={{ dark: "dark.5", light: "gray.1" }} verticalAlign="center" loaderBackgroundBlur={2} textSelectionDisabled height="100%" {...props} + striped={false} columns={fittedColumns} onMouseDownCapture={handleMouseDownCapture} className={mergedClassName} diff --git a/packages/widgets/src/coolify/component.module.css b/packages/widgets/src/coolify/component.module.css new file mode 100644 index 000000000..a31961dff --- /dev/null +++ b/packages/widgets/src/coolify/component.module.css @@ -0,0 +1,29 @@ +.accordion { + --coolify-neutral-fill: rgb( + from var(--mantine-color-default-hover) r g b / calc(var(--opacity, 1) * 0.12) + ); + --coolify-neutral-border: rgb( + from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45) + ); +} + +.accordion :global(.mantine-Accordion-item) { + --item-border-color: var(--coolify-neutral-border); + --item-filled-color: var(--coolify-neutral-fill); +} + +.accordion :global(.mantine-Accordion-control) { + background-color: transparent; +} + +.accordion :global(.mantine-Accordion-control:not(:disabled, [data-disabled]):hover) { + background-color: var(--coolify-neutral-fill); +} + +.neutralDividerBottom { + border-bottom: 1px solid var(--coolify-neutral-border); +} + +.neutralDividerTop { + border-top: 1px solid var(--coolify-neutral-border); +} diff --git a/packages/widgets/src/coolify/instance-card.tsx b/packages/widgets/src/coolify/instance-card.tsx index 56c5e92ed..d12de63e5 100644 --- a/packages/widgets/src/coolify/instance-card.tsx +++ b/packages/widgets/src/coolify/instance-card.tsx @@ -8,6 +8,7 @@ import { useTimeAgo } from "@homarr/common"; import { useI18n } from "@homarr/translation/client"; import { ApplicationsSection } from "./applications-section"; +import classes from "./component.module.css"; import { getSafeApplicationUrl, SAFE_NEW_TAB_REL } from "../common/application-url"; import { buildServerResourceCounts, getBadgeColor, isCoolifyServerOnline, parseStatus } from "./coolify-utils"; import { ServersSection } from "./servers-section"; @@ -71,12 +72,12 @@ export function InstanceCard({ instance, options, isTiny, isAdvanced, widgetKey, .join(" · "); return ( - + Coolify @@ -135,6 +136,7 @@ export function InstanceCard({ instance, options, isTiny, isAdvanced, widgetKey, + v{version} diff --git a/packages/widgets/src/coolify/single-instance-layout.tsx b/packages/widgets/src/coolify/single-instance-layout.tsx index be15023ad..49edbb03b 100644 --- a/packages/widgets/src/coolify/single-instance-layout.tsx +++ b/packages/widgets/src/coolify/single-instance-layout.tsx @@ -8,6 +8,7 @@ import { useTimeAgo } from "@homarr/common"; import { useI18n } from "@homarr/translation/client"; import { ApplicationsSection } from "./applications-section"; +import classes from "./component.module.css"; import { getSafeApplicationUrl, SAFE_NEW_TAB_REL } from "../common/application-url"; import { buildServerResourceCounts } from "./coolify-utils"; import { ServersSection } from "./servers-section"; @@ -82,6 +83,7 @@ export function SingleInstanceLayout({ + Coolify diff --git a/packages/widgets/src/dns-hole/controls/component.module.css b/packages/widgets/src/dns-hole/controls/component.module.css index 1ab18a656..e83cd23eb 100644 --- a/packages/widgets/src/dns-hole/controls/component.module.css +++ b/packages/widgets/src/dns-hole/controls/component.module.css @@ -1,7 +1,3 @@ -[data-mantine-color-scheme="light"] .card { - background-color: var(--mantine-color-gray-1); -} - -[data-mantine-color-scheme="dark"] .card { - background-color: var(--mantine-color-dark-7); +.card { + background-color: transparent; } diff --git a/packages/widgets/src/firewall/component.tsx b/packages/widgets/src/firewall/component.tsx index 6c7ccf542..46f6f671d 100644 --- a/packages/widgets/src/firewall/component.tsx +++ b/packages/widgets/src/firewall/component.tsx @@ -27,6 +27,7 @@ import type { WidgetComponentProps } from "../definition"; import { calculateBandwidth, formatBitsPerSec } from "./bandwidth"; import { FirewallMenu } from "./firewall-menu"; import { FirewallVersion, formatVersion } from "./firewall-version"; +import classes from "./firewall.module.css"; export interface Firewall { label: string; @@ -243,7 +244,7 @@ const FirewallPanel = ({ t, tCommon, }: FirewallPanelProps) => ( - + {isAdvanced && ( @@ -382,6 +383,7 @@ const InterfacesPanel = ({ value={isAdvanced ? "interfaces" : accordionValue} onChange={isAdvanced ? undefined : setAccordionValue} variant={isAdvanced ? "contained" : "default"} + classNames={{ item: classes.accordionItem, control: classes.accordionControl }} > }> diff --git a/packages/widgets/src/firewall/firewall-menu.tsx b/packages/widgets/src/firewall/firewall-menu.tsx index 6b7a26239..f5330a92c 100644 --- a/packages/widgets/src/firewall/firewall-menu.tsx +++ b/packages/widgets/src/firewall/firewall-menu.tsx @@ -1,6 +1,7 @@ import { Box, Select } from "@mantine/core"; import type { Firewall } from "./component"; +import classes from "./firewall.module.css"; interface FirewallMenuProps { onChange: (value: string | null) => void; @@ -18,6 +19,7 @@ export const FirewallMenu = ({ onChange, isTiny, dropdownItems, selectedFirewall w="100%" color="lightgray" data={dropdownItems} + classNames={{ input: classes.selectInput }} styles={{ input: { minHeight: "24px", diff --git a/packages/widgets/src/firewall/firewall.module.css b/packages/widgets/src/firewall/firewall.module.css index 1ab18a656..f0f6ffc95 100644 --- a/packages/widgets/src/firewall/firewall.module.css +++ b/packages/widgets/src/firewall/firewall.module.css @@ -1,7 +1,33 @@ -[data-mantine-color-scheme="light"] .card { - background-color: var(--mantine-color-gray-1); +.card { + background-color: transparent; + border-color: rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45)); } -[data-mantine-color-scheme="dark"] .card { - background-color: var(--mantine-color-dark-7); +.accordionItem { + border-color: rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45)); + background-color: transparent; +} + +.accordionItem[data-active] { + background-color: transparent; +} + +.accordionControl { + background-color: transparent; +} + +.accordionControl:hover, +.accordionControl:focus-visible, +.accordionControl:active { + background-color: rgb(from var(--mantine-color-default-hover) r g b / calc(var(--opacity, 1) * 0.4)); +} + +.selectInput { + background-color: transparent; + border-color: rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45)); +} + +.selectInput:focus, +.selectInput:focus-within { + border-color: var(--mantine-primary-color-filled); } diff --git a/packages/widgets/src/health-monitoring/system-health.module.css b/packages/widgets/src/health-monitoring/system-health.module.css index 71b6ae8f7..1d66203db 100644 --- a/packages/widgets/src/health-monitoring/system-health.module.css +++ b/packages/widgets/src/health-monitoring/system-health.module.css @@ -1,9 +1,5 @@ -[data-mantine-color-scheme="light"] .card { - background-color: var(--mantine-color-gray-1); -} - -[data-mantine-color-scheme="dark"] .card { - background-color: var(--mantine-color-dark-7); +.card { + background-color: transparent; } .systemPanel { diff --git a/packages/widgets/src/health-monitoring/system-health.tsx b/packages/widgets/src/health-monitoring/system-health.tsx index a6c96a7d1..e99ff99da 100644 --- a/packages/widgets/src/health-monitoring/system-health.tsx +++ b/packages/widgets/src/health-monitoring/system-health.tsx @@ -191,7 +191,7 @@ export const SystemHealthMonitoring = ({ ))} {isAdvanced && ( - + )} diff --git a/packages/widgets/src/media-missing/component.module.css b/packages/widgets/src/media-missing/component.module.css index 9068a25b3..5b1175b00 100644 --- a/packages/widgets/src/media-missing/component.module.css +++ b/packages/widgets/src/media-missing/component.module.css @@ -4,23 +4,19 @@ display: flex; text-decoration: none; color: inherit; - border: 1px solid var(--border-color); - background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7)); + border: 1px solid + rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45)); + background-color: transparent; transition: border-color 120ms ease, background-color 120ms ease; - @mixin dark { - --border-color: var(--mantine-color-dark-4); - } - @mixin light { - --border-color: var(--mantine-color-gray-3); - } - &:hover, &:focus-visible { border-color: var(--mantine-primary-color-filled); - background-color: var(--mantine-color-default-hover); + background-color: rgb( + from var(--mantine-color-default-hover) r g b / calc(var(--opacity, 1) * 0.14) + ); } &:focus-visible { @@ -29,6 +25,16 @@ } } +:global(.mediaMissing-wrapper) :global(.mantine-SimpleGrid-root) > :global(.mantine-Paper-root) { + border-color: rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45)); + background-color: transparent; +} + +:global(.mediaMissing-wrapper) :global(.mantine-Tabs-list) { + --tab-border-color: rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45)); + --tab-hover-color: rgb(from var(--mantine-color-default-hover) r g b / calc(var(--opacity, 1) * 0.14)); +} + .content { position: relative; z-index: 1; diff --git a/packages/widgets/src/media-missing/component.tsx b/packages/widgets/src/media-missing/component.tsx index ce09142a6..854c9c303 100644 --- a/packages/widgets/src/media-missing/component.tsx +++ b/packages/widgets/src/media-missing/component.tsx @@ -36,11 +36,13 @@ import classes from "./component.module.css"; import type { MediaMissingTab } from "./tabs"; import { resolveMediaMissingTab } from "./tabs"; +const MIN_CARD_WIDTH = 280; +const MAX_COLUMNS = 4; + export default function MediaMissingWidget({ integrationIds, options, width, - height, displayMode, widgetRuntimeRef, }: WidgetComponentProps<"mediaMissing">) { @@ -88,9 +90,7 @@ export default function MediaMissingWidget({ const enabledPanelCount = Number(showMissing) + Number(showQueued); const panelWidth = isAdvanced && enabledPanelCount > 1 ? width / enabledPanelCount : width; const isThin = !isAdvanced && panelWidth > 0 && panelWidth < 160; - const isShort = !isAdvanced && height > 0 && height < 180; - const targetCardWidth = isShort ? 130 : 200; - const columns = panelWidth > 0 ? Math.max(1, Math.min(Math.floor(panelWidth / targetCardWidth), 4)) : 1; + const columns = panelWidth > 0 ? Math.max(1, Math.min(Math.floor(panelWidth / MIN_CARD_WIDTH), MAX_COLUMNS)) : 1; const density: Density = isThin ? "thin" : panelWidth > 0 && panelWidth / columns < 180 ? "compact" : "comfortable"; const tabLabel = (label: string, shown: number, total: number) => (isThin ? total : `${label} (${shown}/${total})`); @@ -114,7 +114,7 @@ export default function MediaMissingWidget({ {emptyLabel} ) : ( - + {entries.map(({ item, integrationId }) => ( - { - const Icon = viewIcons[value]; - return { - label: ( -
- - {footerLayout.showTabLabels ? ( - - {t(`tab.${value}`)} - - ) : ( - {t(`tab.${value}`)} - )} -
- ), + {width >= 560 ? ( + + {views.map((value) => { + const Icon = viewIcons[value]; + return ( + + ); + })} + + ) : ( +