diff --git a/apps/nextjs/src/app/[locale]/boards/_board-switcher-logo.tsx b/apps/nextjs/src/app/[locale]/boards/_board-switcher-logo.tsx
deleted file mode 100644
index f28a04489..000000000
--- a/apps/nextjs/src/app/[locale]/boards/_board-switcher-logo.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-"use client";
-
-import { Group, Menu, ScrollArea, Text, UnstyledButton } from "@mantine/core";
-import { IconCheck, IconChevronDown, IconLayoutBoard } from "@tabler/icons-react";
-
-import { clientApi } from "@homarr/api/client";
-import { useRequiredBoard } from "@homarr/boards/context";
-import { useI18n } from "@homarr/translation/client";
-import { Link } from "@homarr/ui";
-
-import { BoardLogo } from "~/components/layout/logo/board-logo";
-
-export const BoardSwitcherLogo = () => {
- const board = useRequiredBoard();
- const { data: boards = [] } = clientApi.board.getAllBoards.useQuery();
- const t = useI18n();
-
- return (
-
- );
-};
diff --git a/apps/nextjs/src/app/[locale]/boards/_header-actions.tsx b/apps/nextjs/src/app/[locale]/boards/_header-actions.tsx
index 589fdf7b7..c68be29e9 100644
--- a/apps/nextjs/src/app/[locale]/boards/_header-actions.tsx
+++ b/apps/nextjs/src/app/[locale]/boards/_header-actions.tsx
@@ -3,16 +3,14 @@
import { IconLayoutBoard } from "@tabler/icons-react";
import { useRequiredBoard } from "@homarr/boards/context";
-import { useI18n } from "@homarr/translation/client";
import { HeaderButton } from "~/components/layout/header/button";
export const BoardOtherHeaderActions = () => {
const board = useRequiredBoard();
- const t = useI18n();
return (
-
+
);
diff --git a/apps/nextjs/src/app/[locale]/boards/layout.tsx b/apps/nextjs/src/app/[locale]/boards/layout.tsx
deleted file mode 100644
index 04e3a3e4d..000000000
--- a/apps/nextjs/src/app/[locale]/boards/layout.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import type { PropsWithChildren } from "react";
-import type { Viewport } from "next";
-
-export const viewport: Viewport = {
- viewportFit: "cover",
-};
-
-export default function BoardsLayout({ children }: PropsWithChildren) {
- return children;
-}
diff --git a/apps/nextjs/src/components/board/mobile/deferred-mobile-item.tsx b/apps/nextjs/src/components/board/mobile/deferred-mobile-item.tsx
deleted file mode 100644
index ea142a886..000000000
--- a/apps/nextjs/src/components/board/mobile/deferred-mobile-item.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-"use client";
-
-import type { PropsWithChildren } from "react";
-import { useEffect, useRef, useState } from "react";
-import { Skeleton } from "@mantine/core";
-
-import classes from "./mobile-board.module.css";
-
-interface DeferredMobileItemProps extends PropsWithChildren {
- eager: boolean;
-}
-
-export const DeferredMobileItem = ({ eager, children }: DeferredMobileItemProps) => {
- const [hasMounted, setHasMounted] = useState(eager);
- const wrapperRef = useRef(null);
-
- useEffect(() => {
- if (eager || hasMounted) return;
-
- const element = wrapperRef.current;
- if (!element || typeof IntersectionObserver === "undefined") {
- setHasMounted(true);
- return;
- }
-
- const observer = new IntersectionObserver(
- ([entry]) => {
- if (!entry?.isIntersecting) return;
- setHasMounted(true);
- observer.disconnect();
- },
- { rootMargin: "200% 0px" },
- );
- observer.observe(element);
- return () => observer.disconnect();
- }, [eager, hasMounted]);
-
- return (
-
- {hasMounted ? children : }
-
- );
-};
diff --git a/apps/nextjs/src/components/board/mobile/mobile-board.module.css b/apps/nextjs/src/components/board/mobile/mobile-board.module.css
index fae316cc7..902e77372 100644
--- a/apps/nextjs/src/components/board/mobile/mobile-board.module.css
+++ b/apps/nextjs/src/components/board/mobile/mobile-board.module.css
@@ -1,61 +1,20 @@
-.stream {
- display: flex;
- flex-direction: column;
- gap: 10px;
- padding-bottom: 10px;
- overflow-x: clip;
-}
-
.grid {
display: grid;
grid-template-columns: repeat(var(--mobile-column-count), minmax(0, 1fr));
grid-auto-flow: row;
- grid-auto-rows: min(calc((100vw - 42px - env(safe-area-inset-left) - env(safe-area-inset-right)) / 2), 12rem);
+ grid-auto-rows: min(calc((100vw - 42px) / 2), 12rem);
gap: 10px;
+ padding-bottom: 10px;
}
.item {
position: relative;
min-width: 0;
min-height: 0;
- content-visibility: auto;
- contain-intrinsic-size: auto 12rem;
}
-.deferredItem {
+.item > :global(.grid-stack-item-content) {
width: 100%;
height: 100%;
-}
-
-.deferredItem > :global(.grid-stack-item-content) {
- width: 100%;
- height: 100%;
- overflow: hidden;
-}
-
-.sectionHeading {
- scroll-margin-top: calc(var(--app-shell-header-height) + var(--mantine-spacing-md));
- padding-block: var(--mantine-spacing-xs);
- outline-offset: 4px;
-}
-
-.empty {
- min-height: min(60dvh, 32rem);
- padding: var(--mantine-spacing-xl);
-}
-
-.widgetAction {
- position: absolute;
- z-index: 11;
- top: 4px;
- right: 4px;
- border: 1px solid color-mix(in srgb, var(--mantine-color-white) 30%, transparent);
- box-shadow: var(--mantine-shadow-sm);
-}
-
-.detailContent,
-.detailContent > :global(.grid-stack-item-content) {
- width: 100%;
- height: 100%;
- min-height: 24rem;
+ overflow-y: auto;
}
diff --git a/apps/nextjs/src/components/board/mobile/mobile-board.tsx b/apps/nextjs/src/components/board/mobile/mobile-board.tsx
index 918ea6217..aa06a521f 100644
--- a/apps/nextjs/src/components/board/mobile/mobile-board.tsx
+++ b/apps/nextjs/src/components/board/mobile/mobile-board.tsx
@@ -2,98 +2,21 @@
import type { CSSProperties, RefObject } from "react";
import { useMemo, useRef } from "react";
-import { Box, Center, Stack, Text, ThemeIcon, Title } from "@mantine/core";
-import { IconLayoutBoard } from "@tabler/icons-react";
+import { Box } from "@mantine/core";
import { getDesktopLayout, useRequiredBoard } from "@homarr/boards/context";
import { ReadOnlyEditModeProvider } from "@homarr/boards/edit-mode";
import type { GridItemHTMLElement, GridStack as GridStackInstance } from "@homarr/gridstack";
-import { useI18n } from "@homarr/translation/client";
-import { widgetImports } from "@homarr/widgets";
-import type { SectionItem } from "~/app/[locale]/boards/_types";
import { BoardItemContent } from "../items/item-content";
import { SectionProvider } from "../sections/section-context";
-import { DeferredMobileItem } from "./deferred-mobile-item";
import classes from "./mobile-board.module.css";
-import { MobileWidgetActions } from "./mobile-widget-actions";
-import {
- createMobileBoardElements,
- createMobileBoardItems,
- getMobileRootSection,
- mobileColumnCount,
-} from "./mobile-layout";
-import type { MobileBoardElement, MobileBoardSectionHeading } from "./mobile-layout";
-import { resolveMobileItemPresentation } from "./mobile-presentation";
-
-type MobileBoardRenderBlock =
- | MobileBoardSectionHeading
- | {
- type: "items";
- id: string;
- items: SectionItem[];
- };
-
-const createRenderBlocks = (elements: MobileBoardElement[]): MobileBoardRenderBlock[] => {
- const blocks: MobileBoardRenderBlock[] = [];
-
- for (const element of elements) {
- if (element.type === "sectionHeading") {
- blocks.push(element);
- continue;
- }
-
- const previousBlock = blocks.at(-1);
- if (previousBlock?.type === "items") {
- previousBlock.items.push(element);
- } else {
- blocks.push({ type: "items", id: `items-${element.id}`, items: [element] });
- }
- }
-
- return blocks;
-};
-
-const getMobilePresentation = (item: SectionItem) => {
- const definition = widgetImports[item.kind].definition;
- return resolveMobileItemPresentation(item, "mobile" in definition ? definition.mobile : undefined);
-};
-
-const MobileBoardItem = ({ item }: { item: SectionItem }) => {
- const widgetStateRef = useRef | null>(null);
- const presentation = getMobilePresentation(item);
-
- return (
-
-
-
-
-
-
- );
-};
+import { createMobileBoardItems, getMobileRootSection, mobileColumnCount } from "./mobile-layout";
export const MobileBoard = () => {
const board = useRequiredBoard();
- const t = useI18n();
const desktopLayout = getDesktopLayout(board);
const items = useMemo(() => createMobileBoardItems(board, desktopLayout.id), [board, desktopLayout.id]);
- const elements = useMemo(() => createMobileBoardElements(board, desktopLayout.id), [board, desktopLayout.id]);
- const renderBlocks = useMemo(() => createRenderBlocks(elements), [elements]);
const rootSection = getMobileRootSection(board);
const wrapperRef = useRef(null);
const itemRefs = useRef>>({});
@@ -109,47 +32,21 @@ export const MobileBoard = () => {
refs: { wrapper: wrapperRef, items: itemRefs, gridstack: gridstackRef },
}}
>
-
- {elements.length === 0 ? (
-
-
-
-
-
-
- {t("board.mobile.empty.title")}
-
-
- {t("board.mobile.empty.description")}
-
-
-
- ) : (
- renderBlocks.map((block) =>
- block.type === "sectionHeading" ? (
-
- {block.title}
-
- ) : (
-
- {block.items.map((item) => (
-
- ))}
-
- ),
- )
- )}
+
+ {items.map((item) => (
+
+
+
+ ))}
diff --git a/apps/nextjs/src/components/board/mobile/mobile-layout.spec.ts b/apps/nextjs/src/components/board/mobile/mobile-layout.spec.ts
index f617694d5..3e06594d0 100644
--- a/apps/nextjs/src/components/board/mobile/mobile-layout.spec.ts
+++ b/apps/nextjs/src/components/board/mobile/mobile-layout.spec.ts
@@ -5,12 +5,7 @@ import { BoardMockBuilder } from "../items/actions/test/mocks/board-mock";
import { DynamicSectionMockBuilder } from "../items/actions/test/mocks/dynamic-section-mock";
import { EmptySectionMockBuilder } from "../items/actions/test/mocks/empty-section-mock";
import { ItemMockBuilder } from "../items/actions/test/mocks/item-mock";
-import {
- createMobileBoardElements,
- createMobileBoardItems,
- getMobileRootSection,
- getMobileSectionAnchorId,
-} from "./mobile-layout";
+import { createMobileBoardItems, getMobileRootSection } from "./mobile-layout";
describe("createMobileBoardItems", () => {
test("selects the first root section in visual order", () => {
@@ -98,86 +93,4 @@ describe("createMobileBoardItems", () => {
expect(result.map((item) => item.id)).toEqual(["second", "first"]);
expect(result[1]).toMatchObject({ width: 1, height: 1 });
});
-
- test("emits named category and dynamic section headings in visual order", () => {
- const board = new BoardMockBuilder().build();
- const desktopLayoutId = board.layouts.at(0)?.id;
- if (!desktopLayoutId) throw new Error("Expected a desktop layout");
-
- const category = new CategorySectionMockBuilder({ id: "category", name: " Services ", yOffset: 0 }).build();
- const dynamicSection = new DynamicSectionMockBuilder({
- id: "dynamic",
- options: { title: "Monitoring", borderColor: "", customCssClasses: [] },
- })
- .addLayout({ layoutId: desktopLayoutId, parentSectionId: category.id, xOffset: 1, yOffset: 0 })
- .build();
- const nestedDynamicSection = new DynamicSectionMockBuilder({
- id: "nested-dynamic",
- options: { title: "Details", borderColor: "", customCssClasses: [] },
- })
- .addLayout({ layoutId: desktopLayoutId, parentSectionId: dynamicSection.id, xOffset: 0, yOffset: 1 })
- .build();
-
- board.sections.push(category, dynamicSection, nestedDynamicSection);
- board.items.push(
- new ItemMockBuilder({ id: "before-dynamic" })
- .addLayout({ layoutId: desktopLayoutId, sectionId: category.id, xOffset: 0, yOffset: 0 })
- .build(),
- new ItemMockBuilder({ id: "inside-dynamic" })
- .addLayout({ layoutId: desktopLayoutId, sectionId: dynamicSection.id, xOffset: 0, yOffset: 0 })
- .build(),
- new ItemMockBuilder({ id: "inside-nested" })
- .addLayout({ layoutId: desktopLayoutId, sectionId: nestedDynamicSection.id })
- .build(),
- );
-
- const result = createMobileBoardElements(board, desktopLayoutId);
-
- expect(
- result.map((element) => (element.type === "sectionHeading" ? `heading:${element.title}` : `item:${element.id}`)),
- ).toEqual([
- "heading:Services",
- "item:before-dynamic",
- "heading:Monitoring",
- "item:inside-dynamic",
- "heading:Details",
- "item:inside-nested",
- ]);
- expect(result[0]).toMatchObject({
- type: "sectionHeading",
- sectionId: category.id,
- anchorId: getMobileSectionAnchorId(category.id),
- });
- });
-
- test("omits headings for empty and unnamed sections while preserving their items", () => {
- const board = new BoardMockBuilder().build();
- const desktopLayoutId = board.layouts.at(0)?.id;
- if (!desktopLayoutId) throw new Error("Expected a desktop layout");
-
- const emptyNamedCategory = new CategorySectionMockBuilder({
- id: "empty-category",
- name: "Empty",
- yOffset: 0,
- }).build();
- const unnamedCategory = new CategorySectionMockBuilder({ id: "unnamed-category", name: " ", yOffset: 1 }).build();
- const unnamedDynamicSection = new DynamicSectionMockBuilder({ id: "unnamed-dynamic" })
- .addLayout({ layoutId: desktopLayoutId, parentSectionId: unnamedCategory.id })
- .build();
-
- board.sections.push(emptyNamedCategory, unnamedCategory, unnamedDynamicSection);
- board.items.push(
- new ItemMockBuilder({ id: "inside-unnamed" })
- .addLayout({ layoutId: desktopLayoutId, sectionId: unnamedDynamicSection.id })
- .build(),
- new ItemMockBuilder({ id: "orphan" })
- .addLayout({ layoutId: desktopLayoutId, sectionId: "missing-section" })
- .build(),
- );
-
- const result = createMobileBoardElements(board, desktopLayoutId);
-
- expect(result.map((element) => element.type)).toEqual(["item", "item"]);
- expect(result.map((element) => element.id)).toEqual(["inside-unnamed", "orphan"]);
- });
});
diff --git a/apps/nextjs/src/components/board/mobile/mobile-layout.ts b/apps/nextjs/src/components/board/mobile/mobile-layout.ts
index 1017856a4..ab8bd4d65 100644
--- a/apps/nextjs/src/components/board/mobile/mobile-layout.ts
+++ b/apps/nextjs/src/components/board/mobile/mobile-layout.ts
@@ -1,8 +1,7 @@
-import type { Board, CategorySection, DynamicSectionItem, ItemLayout, SectionItem } from "~/app/[locale]/boards/_types";
+import type { Board, DynamicSectionItem, ItemLayout, SectionItem } from "~/app/[locale]/boards/_types";
export const mobileColumnCount = 2;
const mobileMaxHeight = 3;
-const mobileSectionAnchorPrefix = "mobile-board-section-";
type PositionedElement = DynamicSectionItem | SectionItem;
type PositionedLayout = {
@@ -13,16 +12,6 @@ type PositionedLayout = {
height: number;
};
-export type MobileBoardSectionHeading = {
- type: "sectionHeading";
- id: string;
- sectionId: string;
- title: string;
- anchorId: string;
-};
-
-export type MobileBoardElement = SectionItem | MobileBoardSectionHeading;
-
const comparePosition = (elementA: PositionedElement, elementB: PositionedElement) =>
elementA.yOffset - elementB.yOffset || elementA.xOffset - elementB.xOffset;
@@ -39,8 +28,6 @@ export const getMobileRootSection = (board: Board) =>
yOffset: 0,
};
-export const getMobileSectionAnchorId = (sectionId: string) => `${mobileSectionAnchorPrefix}${sectionId}`;
-
const getLayout = (
layouts: TLayout[],
desktopLayoutId: string,
@@ -51,20 +38,7 @@ const getLayout = (
(layoutA, layoutB) => (layoutPriority.get(layoutB.layoutId) ?? -1) - (layoutPriority.get(layoutA.layoutId) ?? -1),
)[0];
-const createSectionHeading = (section: CategorySection | DynamicSectionItem): MobileBoardSectionHeading | null => {
- const title = (section.kind === "category" ? section.name : section.options.title).trim();
- if (title.length === 0) return null;
-
- return {
- type: "sectionHeading",
- id: `section-heading-${section.id}`,
- sectionId: section.id,
- title,
- anchorId: getMobileSectionAnchorId(section.id),
- };
-};
-
-export const createMobileBoardElements = (board: Board, desktopLayoutId: string): MobileBoardElement[] => {
+export const createMobileBoardItems = (board: Board, desktopLayoutId: string): SectionItem[] => {
const rootSections = getMobileRootSections(board);
const defaultSectionId = rootSections[0]?.id ?? "mobile";
const layoutPriority = new Map(
@@ -106,66 +80,38 @@ export const createMobileBoardElements = (board: Board, desktopLayoutId: string)
elementsBySection.set(parentSectionId, elements);
}
- const result: MobileBoardElement[] = [];
+ const result: SectionItem[] = [];
const visitedItems = new Set();
const visitedSections = new Set();
- const createSectionElements = (
- section: CategorySection | DynamicSectionItem | (typeof rootSections)[number],
- ): { elements: MobileBoardElement[]; items: SectionItem[] } => {
- if (visitedSections.has(section.id)) return { elements: [], items: [] };
- const sectionId = section.id;
+ const appendSectionItems = (sectionId: string) => {
+ if (visitedSections.has(sectionId)) return;
visitedSections.add(sectionId);
- const sectionElements: MobileBoardElement[] = [];
- const sectionItems: SectionItem[] = [];
for (const element of (elementsBySection.get(sectionId) ?? []).toSorted(comparePosition)) {
if (element.type === "section") {
- const nestedResult = createSectionElements(element);
- sectionElements.push(...nestedResult.elements);
- sectionItems.push(...nestedResult.items);
+ appendSectionItems(element.id);
continue;
}
if (!visitedItems.has(element.id)) {
visitedItems.add(element.id);
- sectionElements.push(element);
- sectionItems.push(element);
+ result.push(element);
}
}
-
- if (sectionItems.length === 0) return { elements: [], items: [] };
-
- const heading = section.kind === "empty" ? null : createSectionHeading(section);
- return {
- elements: heading ? [heading, ...sectionElements] : sectionElements,
- items: sectionItems,
- };
};
- rootSections.forEach((section) => result.push(...createSectionElements(section).elements));
- dynamicSections
- .toSorted(comparePosition)
- .forEach((section) => result.push(...createSectionElements(section).elements));
+ rootSections.forEach((section) => appendSectionItems(section.id));
+ dynamicSections.toSorted(comparePosition).forEach((section) => appendSectionItems(section.id));
items.toSorted(comparePosition).forEach((item) => {
if (!visitedItems.has(item.id)) result.push(item);
});
- let itemIndex = 0;
- return result.map((element) => {
- if (element.type === "sectionHeading") return element;
-
- return {
- ...element,
- xOffset: 0,
- yOffset: itemIndex++,
- width: Math.max(1, Math.min(element.width, mobileColumnCount)),
- height: Math.max(1, Math.min(element.height, mobileMaxHeight)),
- };
- });
+ return result.map((item, index) => ({
+ ...item,
+ xOffset: 0,
+ yOffset: index,
+ width: Math.max(1, Math.min(item.width, mobileColumnCount)),
+ height: Math.max(1, Math.min(item.height, mobileMaxHeight)),
+ }));
};
-
-export const createMobileBoardItems = (board: Board, desktopLayoutId: string): SectionItem[] =>
- createMobileBoardElements(board, desktopLayoutId).filter(
- (element): element is SectionItem => element.type === "item",
- );
diff --git a/apps/nextjs/src/components/board/mobile/mobile-presentation.spec.ts b/apps/nextjs/src/components/board/mobile/mobile-presentation.spec.ts
deleted file mode 100644
index c2e6c1197..000000000
--- a/apps/nextjs/src/components/board/mobile/mobile-presentation.spec.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { describe, expect, test } from "vitest";
-
-import { resolveMobileItemPresentation } from "./mobile-presentation";
-
-describe("resolveMobileItemPresentation", () => {
- test("renders apps as eager compact tiles", () => {
- expect(resolveMobileItemPresentation({ kind: "app", height: 8 }, undefined)).toMatchObject({
- width: 1,
- height: 1,
- eager: true,
- supportsDetails: false,
- });
- });
-
- test("defaults widgets to full width and clamps desktop height", () => {
- expect(resolveMobileItemPresentation({ kind: "calendar", height: 8 }, undefined)).toMatchObject({
- width: 2,
- height: 3,
- eager: false,
- supportsDetails: true,
- });
- });
-
- test("prefers explicit presentation metadata", () => {
- expect(
- resolveMobileItemPresentation(
- { kind: "clock", height: 3 },
- {
- width: 1,
- height: 1,
- supportsCompactSummary: true,
- supportsDetailView: false,
- eager: true,
- },
- ),
- ).toEqual({
- width: 1,
- height: 1,
- displayMode: "mobileSummary",
- supportsDetails: false,
- eager: true,
- });
- });
-});
diff --git a/apps/nextjs/src/components/board/mobile/mobile-presentation.ts b/apps/nextjs/src/components/board/mobile/mobile-presentation.ts
deleted file mode 100644
index 62dbd65aa..000000000
--- a/apps/nextjs/src/components/board/mobile/mobile-presentation.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import type { WidgetKind } from "@homarr/definitions";
-import type { WidgetMobilePresentation } from "@homarr/widgets";
-
-export const resolveMobileItemPresentation = (
- item: { kind: WidgetKind; height: number },
- mobile: WidgetMobilePresentation | undefined,
-) => ({
- width: mobile?.width ?? (item.kind === "app" ? 1 : 2),
- height: mobile?.height ?? (item.kind === "app" ? 1 : Math.max(1, Math.min(item.height, 3))),
- displayMode: mobile?.supportsCompactSummary ? ("mobileSummary" as const) : ("default" as const),
- supportsDetails: mobile?.supportsDetailView ?? item.kind !== "app",
- eager: mobile?.eager ?? (item.kind === "app" || item.kind === "bookmarks"),
-});
diff --git a/apps/nextjs/src/components/board/mobile/mobile-widget-actions.tsx b/apps/nextjs/src/components/board/mobile/mobile-widget-actions.tsx
deleted file mode 100644
index d05b1ebcf..000000000
--- a/apps/nextjs/src/components/board/mobile/mobile-widget-actions.tsx
+++ /dev/null
@@ -1,166 +0,0 @@
-"use client";
-
-import type { MutableRefObject } from "react";
-import { useMemo } from "react";
-import { ActionIcon, Box, Button, Drawer, Loader, Modal, Stack } from "@mantine/core";
-import { useDisclosure } from "@mantine/hooks";
-import { IconDotsVertical, IconMaximize, IconRefresh } from "@tabler/icons-react";
-import type { QueryKey } from "@tanstack/react-query";
-import { useIsFetching, useQueryClient } from "@tanstack/react-query";
-
-import { useRequiredBoard } from "@homarr/boards/context";
-import { useEditMode } from "@homarr/boards/edit-mode";
-import { useSettings } from "@homarr/settings";
-import { translateIfNecessary } from "@homarr/translation";
-import { useI18n } from "@homarr/translation/client";
-import type { WidgetContextMenuAction } from "@homarr/widgets";
-import { reduceWidgetOptionsWithDefaultValues, widgetImports } from "@homarr/widgets";
-
-import type { SectionItem } from "~/app/[locale]/boards/_types";
-import { BoardItemContent } from "../items/item-content";
-import { useItemActions } from "../items/item-actions";
-import classes from "./mobile-board.module.css";
-
-interface MobileWidgetActionsProps {
- item: SectionItem;
- supportsDetails: boolean;
- widgetStateRef: MutableRefObject | null>;
-}
-
-export const MobileWidgetActions = ({ item, supportsDetails, widgetStateRef }: MobileWidgetActionsProps) => {
- const [actionsOpened, actionsDisclosure] = useDisclosure(false);
- const [detailsOpened, detailsDisclosure] = useDisclosure(false);
- const t = useI18n();
- const settings = useSettings();
- const board = useRequiredBoard();
- const [isEditMode] = useEditMode();
- const { updateItemOptions } = useItemActions();
- const definition = widgetImports[item.kind].definition;
- const queryClient = useQueryClient();
- const queryKey = useMemo(
- () => ("queryKey" in definition ? (definition.queryKey as QueryKey) : [["widget", item.kind]]),
- [definition, item.kind],
- );
- const isFetching = useIsFetching({ queryKey }) > 0;
- const options = useMemo(
- () => reduceWidgetOptionsWithDefaultValues(item.kind, settings, item.options) as Record,
- [item.kind, item.options, settings],
- );
- const contextActions = useMemo(() => {
- if (!("contextActions" in definition) || typeof definition.contextActions !== "function") return [];
-
- return definition.contextActions({
- options,
- setOptions: (partial: Record) =>
- updateItemOptions({
- itemId: item.id,
- newOptions: { ...options, ...partial },
- }),
- integrationIds: item.integrationIds,
- context: { isEditMode, boardId: board.id, itemId: item.id },
- widgetStateRef,
- } as never) as WidgetContextMenuAction[];
- }, [board.id, definition, isEditMode, item.id, item.integrationIds, options, updateItemOptions, widgetStateRef]);
- const visibleContextActions = contextActions.filter((action) => !action.hidden);
- const title = item.advancedOptions.title?.trim() || t(`widget.${item.kind}.name`);
- const hasActions = item.kind !== "app" || supportsDetails || visibleContextActions.length > 0;
-
- if (!hasActions) return null;
-
- const refresh = () => {
- void queryClient.refetchQueries({ queryKey, type: "all" });
- actionsDisclosure.close();
- };
-
- return (
- <>
-
-
-
-
-
-
- {supportsDetails && (
- }
- onClick={() => {
- actionsDisclosure.close();
- detailsDisclosure.open();
- }}
- >
- {t("board.mobile.details")}
-
- )}
- {visibleContextActions.map((action) => {
- const Icon = action.icon;
- return (
- : undefined}
- onClick={() => {
- action.onClick();
- actionsDisclosure.close();
- }}
- >
- {String(translateIfNecessary(t, action.label))}
-
- );
- })}
- : }
- onClick={refresh}
- >
- {t("item.menu.label.refresh")}
-
-
-
-
-
-
-
-
-
- >
- );
-};
diff --git a/apps/nextjs/src/components/board/use-mobile-board.spec.ts b/apps/nextjs/src/components/board/use-mobile-board.spec.ts
deleted file mode 100644
index 35a8a95b6..000000000
--- a/apps/nextjs/src/components/board/use-mobile-board.spec.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { describe, expect, test } from "vitest";
-
-import { resolveMobileBoardViewport } from "./use-mobile-board";
-
-describe("resolveMobileBoardViewport", () => {
- test("keeps phones mobile regardless of their viewport width", () => {
- expect(
- resolveMobileBoardViewport({
- deviceClass: "phone",
- matchesMobileWidth: false,
- hasResolvedClientWidth: true,
- }),
- ).toEqual({
- deviceClass: "phone",
- isMobile: true,
- isResolved: true,
- });
- });
-
- test.each([
- [true, true],
- [false, false],
- ])("uses the 48em query for tablets (matches: %s)", (matchesMobileWidth, expectedIsMobile) => {
- expect(
- resolveMobileBoardViewport({
- deviceClass: "tablet",
- matchesMobileWidth,
- hasResolvedClientWidth: true,
- }).isMobile,
- ).toBe(expectedIsMobile);
- });
-
- test("keeps tablets unresolved until their client width is known", () => {
- expect(
- resolveMobileBoardViewport({
- deviceClass: "tablet",
- matchesMobileWidth: false,
- hasResolvedClientWidth: false,
- }).isResolved,
- ).toBe(false);
- });
-
- test.each([
- [true, true],
- [false, false],
- ])("keeps desktop resize behavior (matches: %s)", (matchesMobileWidth, expectedIsMobile) => {
- expect(
- resolveMobileBoardViewport({
- deviceClass: "desktop",
- matchesMobileWidth,
- hasResolvedClientWidth: true,
- }),
- ).toMatchObject({
- isMobile: expectedIsMobile,
- isResolved: true,
- });
- });
-});
diff --git a/apps/nextjs/src/components/board/use-mobile-board.tsx b/apps/nextjs/src/components/board/use-mobile-board.tsx
index 7197aac22..84eea8508 100644
--- a/apps/nextjs/src/components/board/use-mobile-board.tsx
+++ b/apps/nextjs/src/components/board/use-mobile-board.tsx
@@ -1,97 +1,21 @@
"use client";
import type { PropsWithChildren } from "react";
-import { createContext, useContext, useEffect, useState } from "react";
-import { Box, Group, Skeleton, Stack } from "@mantine/core";
+import { createContext, useContext } from "react";
import { useMediaQuery } from "@mantine/hooks";
export const mobileBoardMediaQuery = "(max-width: 48em)";
-export type MobileBoardDeviceClass = "phone" | "tablet" | "desktop";
-
-interface MobileBoardViewport {
- deviceClass: MobileBoardDeviceClass;
- isMobile: boolean;
- isResolved: boolean;
-}
-
-const MobileBoardViewportContext = createContext({
- deviceClass: "desktop",
- isMobile: false,
- isResolved: true,
-});
-
-export const resolveMobileBoardViewport = ({
- deviceClass,
- matchesMobileWidth,
- hasResolvedClientWidth,
-}: {
- deviceClass: MobileBoardDeviceClass;
- matchesMobileWidth: boolean;
- hasResolvedClientWidth: boolean;
-}): MobileBoardViewport => ({
- deviceClass,
- isMobile: deviceClass === "phone" || matchesMobileWidth,
- isResolved: deviceClass !== "tablet" || hasResolvedClientWidth,
-});
-
-const MobileBoardViewportSkeleton = () => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-);
+const InitialMobileBoardContext = createContext(undefined);
export const MobileBoardViewportProvider = ({
- initialDeviceClass,
+ initialIsMobile,
children,
-}: PropsWithChildren<{ initialDeviceClass: MobileBoardDeviceClass }>) => {
- const [hasResolvedClientWidth, setHasResolvedClientWidth] = useState(false);
- const matchesMobileWidth = useMediaQuery(mobileBoardMediaQuery, initialDeviceClass === "phone", {
- getInitialValueInEffect: true,
- });
+}: PropsWithChildren<{ initialIsMobile: boolean }>) => (
+ {children}
+);
- useEffect(() => {
- setHasResolvedClientWidth(true);
- }, []);
-
- const viewport = resolveMobileBoardViewport({
- deviceClass: initialDeviceClass,
- matchesMobileWidth,
- hasResolvedClientWidth,
- });
-
- return (
-
- {viewport.isResolved ? children : }
-
- );
+export const useIsMobileBoard = () => {
+ const initialIsMobile = useContext(InitialMobileBoardContext);
+ return useMediaQuery(mobileBoardMediaQuery, initialIsMobile, { getInitialValueInEffect: true });
};
-
-export const useMobileBoardViewport = () => useContext(MobileBoardViewportContext);
-
-export const useIsMobileBoard = () => useMobileBoardViewport().isMobile;
diff --git a/apps/nextjs/src/components/layout/header/button.tsx b/apps/nextjs/src/components/layout/header/button.tsx
index c26e3e7fe..49c67d883 100644
--- a/apps/nextjs/src/components/layout/header/button.tsx
+++ b/apps/nextjs/src/components/layout/header/button.tsx
@@ -25,7 +25,7 @@ const headerButtonActionIconProps: ActionIconProps = {
variant: "subtle",
style: { border: "none" },
color: "gray",
- size: 44,
+ size: "lg",
};
export const HeaderButton = forwardRef((props, ref) => {
@@ -34,15 +34,15 @@ export const HeaderButton = forwardRef((pr
}
component={Link}
- {...headerButtonActionIconProps}
{...props}
+ {...headerButtonActionIconProps}
>
{props.children}
);
}
return (
-
+
{props.children}
);
diff --git a/apps/nextjs/src/components/layout/header/responsive-user.tsx b/apps/nextjs/src/components/layout/header/responsive-user.tsx
deleted file mode 100644
index b97c0383e..000000000
--- a/apps/nextjs/src/components/layout/header/responsive-user.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-"use client";
-
-import type { PropsWithChildren } from "react";
-
-import { useIsMobileBoard } from "../../board/use-mobile-board";
-
-export const ResponsiveUserButton = ({
- hideOnMobileBoard,
- children,
-}: PropsWithChildren<{ hideOnMobileBoard: boolean }>) => {
- const isMobileBoard = useIsMobileBoard();
- return hideOnMobileBoard && isMobileBoard ? null : children;
-};
diff --git a/packages/widgets/src/common/mobile-summary.tsx b/packages/widgets/src/common/mobile-summary.tsx
deleted file mode 100644
index d3f6e9bf1..000000000
--- a/packages/widgets/src/common/mobile-summary.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Center, Group, Stack, Text } from "@mantine/core";
-
-interface WidgetMobileSummaryProps {
- value: number | string;
- label: string;
- description?: string;
-}
-
-export function WidgetMobileSummary({ value, label, description }: WidgetMobileSummaryProps) {
- return (
-
-
-
-
- {value}
-
-
- {label}
-
-
- {description !== undefined && (
-
- {description}
-
- )}
-
-
- );
-}
diff --git a/packages/widgets/src/index.tsx b/packages/widgets/src/index.tsx
index 445f465cf..cf131db7b 100644
--- a/packages/widgets/src/index.tsx
+++ b/packages/widgets/src/index.tsx
@@ -69,8 +69,6 @@ import * as wud from "./wud";
export type {
NormalizedWidgetQuery,
WidgetDefinition,
- WidgetDisplayMode,
- WidgetMobilePresentation,
WidgetContextMenuAction,
WidgetContextActionProps,
WidgetOptionsSettings,