diff --git a/apps/docs/docs/management/api/index.mdx b/apps/docs/docs/management/api/index.mdx index 451586e1a..fda610981 100644 --- a/apps/docs/docs/management/api/index.mdx +++ b/apps/docs/docs/management/api/index.mdx @@ -81,8 +81,8 @@ The OpenAPI and MCP APIs include board management endpoints for common automatio - Update board visual and behavior settings - Set desktop and mobile home boards for the current user - Configure global board defaults such as public home boards, automatic mobile layout mode, and status behavior +- Configure saved responsive layouts through Homarr's board settings UI when a server uses legacy mobile layout mode Use the interactive API reference to inspect the exact request body for OpenAPI endpoints under the `boards` and `settings` tags. Saved responsive layouts are intentionally not exposed as an OpenAPI or MCP replacement operation -because omitting a layout would remove it; configure them through Homarr's board settings UI when a server uses -legacy mobile layout mode. +because omitting a layout would remove it. diff --git a/apps/docs/docs/management/boards/index.mdx b/apps/docs/docs/management/boards/index.mdx index 990d58f9c..e004d75bd 100644 --- a/apps/docs/docs/management/boards/index.mdx +++ b/apps/docs/docs/management/boards/index.mdx @@ -34,7 +34,7 @@ Underscores and hyphen-minus are allowed. ### Column count -Homarr has a complex drag-and-drop system that can be used from the browser. +Homarr has a complex drag and drop system that can be used from the browser. The column count defines how many horizontal columns are available to drag and drop elements to. ### Public @@ -270,7 +270,7 @@ Alternatively, you can also define it via your user profile: ### For whole server -You can also set a global home board that will be used for all users who have not set a home board themselves. +You can also set a global home board that will be used for all users that have not set a home board themselves. It will also be used for anonymous users. 1. Navigate to the settings: `Management` > `Settings` > `Boards` diff --git a/apps/docs/tsconfig.json b/apps/docs/tsconfig.json index 308e7cd26..1142ed3fb 100644 --- a/apps/docs/tsconfig.json +++ b/apps/docs/tsconfig.json @@ -26,5 +26,5 @@ "**/*.md", "**/*.mdx", ], - "exclude": ["node_modules", "build", ".docusaurus"], + "exclude": ["node_modules"], } diff --git a/apps/nextjs/next.config.ts b/apps/nextjs/next.config.ts index 491cfa4c5..4ff31b9aa 100644 --- a/apps/nextjs/next.config.ts +++ b/apps/nextjs/next.config.ts @@ -5,7 +5,6 @@ import "@homarr/common/env"; import "@homarr/core/infrastructure/logs/env"; import "@homarr/docker/env"; -import path from "node:path"; import type { NextConfig } from "next"; import createNextIntlPlugin from "next-intl/plugin"; @@ -35,10 +34,6 @@ const nextConfig: NextConfig = { webpackMemoryOptimizations: true, }, turbopack: { - // Keep Turbopack and output-file tracing inside this monorepo. Without an - // explicit root, an unrelated parent lockfile can make Next.js scan the - // entire parent directory and stall production builds. - root: path.join(import.meta.dirname, "../.."), // ponytail: known Turbopack NFT warning from path.join(process.cwd(), …) in // src/app/api/backup/{route,shared}.ts. No working placement in 16.2.x // (see vercel/next.js#95125). Suppress until upstream fix lands. diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_guarded-history.spec.ts b/apps/nextjs/src/app/[locale]/boards/(content)/_guarded-history.spec.ts deleted file mode 100644 index d96d80f09..000000000 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_guarded-history.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { beforeEach, describe, expect, test, vi } from "vitest"; - -import { classifyGuardedNavigation, removeGuardHistoryEntry, replaceGuardedSameDocumentUrl } from "./_guarded-history"; - -describe("guarded board edit history", () => { - beforeEach(() => { - window.history.replaceState({ page: "board" }, "", "/boards/demo"); - }); - - test("replaces the guard entry for same-document fragment navigation", () => { - const section = document.createElement("section"); - section.id = "Media requests"; - document.body.appendChild(section); - const scrollIntoView = vi.fn(); - Object.defineProperty(section, "scrollIntoView", { value: scrollIntoView }); - - replaceGuardedSameDocumentUrl(new URL("/boards/demo#Media%20requests", window.location.href), { - key: "__guard", - id: "guard-id", - }); - - expect(window.location.hash).toBe("#Media%20requests"); - expect(window.history.state).toStrictEqual({ - page: "board", - __guard: "guard-id", - }); - expect(scrollIntoView).toHaveBeenCalledOnce(); - section.remove(); - }); - - test("treats an explicit empty fragment as same-document navigation", () => { - const current = new URL(window.location.href); - const destination = new URL("#", current); - - expect(classifyGuardedNavigation(destination, current)).toBe("same-document"); - }); - - test("removes the guard while preserving the fragment when editing ends", () => { - window.history.replaceState({ page: "board", __guard: "guard-id" }, "", "/boards/demo#section"); - const back = vi.spyOn(window.history, "back").mockImplementation(() => { - window.history.replaceState({ page: "board" }, "", "/boards/demo"); - window.dispatchEvent(new PopStateEvent("popstate")); - }); - - removeGuardHistoryEntry("http://localhost:3000/boards/demo#section", "__guard"); - - expect(back).toHaveBeenCalledOnce(); - expect(window.location.href).toBe("http://localhost:3000/boards/demo#section"); - expect(window.history.state).toStrictEqual({ page: "board" }); - }); -}); diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_guarded-history.ts b/apps/nextjs/src/app/[locale]/boards/(content)/_guarded-history.ts deleted file mode 100644 index 29677f8e1..000000000 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_guarded-history.ts +++ /dev/null @@ -1,58 +0,0 @@ -const getHistoryState = () => - window.history.state && typeof window.history.state === "object" - ? (window.history.state as Record) - : {}; - -interface GuardState { - key: string; - id: string; -} - -export const classifyGuardedNavigation = (destination: URL, current: URL) => { - if (destination.href === current.href) return "same-url"; - if ( - destination.origin === current.origin && - destination.pathname === current.pathname && - destination.search === current.search - ) { - return "same-document"; - } - return "other-document"; -}; - -export const replaceGuardedSameDocumentUrl = (destination: URL, guard: GuardState) => { - window.history.replaceState( - { - ...getHistoryState(), - [guard.key]: guard.id, - }, - document.title, - destination.href, - ); - - if (!destination.hash) { - window.scrollTo({ top: 0, left: 0 }); - return; - } - - let targetId = destination.hash.slice(1); - try { - targetId = decodeURIComponent(targetId); - } catch { - // Keep the encoded value when the fragment contains malformed escape sequences. - } - document.getElementById(targetId)?.scrollIntoView(); -}; - -export const removeGuardHistoryEntry = (guardedUrl: string, guardStateKey: string) => { - window.addEventListener( - "popstate", - () => { - const state = { ...getHistoryState() }; - delete state[guardStateKey]; - window.history.replaceState(state, document.title, guardedUrl); - }, - { once: true }, - ); - window.history.back(); -}; diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx index c3851055d..56cf5d6e7 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx @@ -55,8 +55,6 @@ import { CurrentColorSchemeCombobox } from "~/components/color-scheme/current-co import { CurrentLanguageCombobox } from "~/components/language/current-language-combobox"; import { HeaderButton } from "~/components/layout/header/button"; -import { classifyGuardedNavigation, removeGuardHistoryEntry, replaceGuardedSameDocumentUrl } from "./_guarded-history"; - export const BoardContentHeaderActions = ({ demoReadOnly }: { demoReadOnly: boolean }) => { const [isEditMode] = useEditMode(); const board = useRequiredBoard(); @@ -76,9 +74,9 @@ export const BoardContentHeaderActions = ({ demoReadOnly }: { demoReadOnly: bool return ( <> - {isEditMode && } + {isEditMode && !isMobile && } - +