Merge commit '55bc3353b' into HEAD

This commit is contained in:
Thomas Camlong committed 2026-08-10 19:14:07 +02:00
commit b4bbaa2e12
13 files changed
+53 -83

No files matched your search

@@ -4,7 +4,7 @@
background: color-mix(in srgb, var(--mantine-color-body) 82%, var(--mantine-primary-color-light));
padding: var(--mantine-spacing-xs);
min-height: 7rem;
max-height: min(24rem, 48dvh);
max-height: min(18rem, 38dvh);
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-gutter: stable;
@@ -45,7 +45,7 @@ export const LayoutPreview = ({ board, layout, layouts, sourceLayout, apps }: Pr
{layout.columnCount} {t("board.setting.section.layout.preview.columns")}
</Text>
</Group>
<Box w={{ base: "100%", md: previewWidth }} maw="100%" className={classes.canvas}>
<Box w={{ base: "100%", md: `max(${previewWidth}, 14rem)` }} maw="100%" className={classes.canvas}>
{elements.length === 0 ? (
<Text size="xs" c="dimmed" ta="center" py="xl">
{t("board.setting.section.layout.preview.empty")}
@@ -6,7 +6,7 @@
display: grid;
gap: 6px;
width: 100%;
min-height: 112px;
min-height: 88px;
padding: 8px;
overflow: hidden;
background: var(--mantine-color-body);
@@ -168,7 +168,7 @@ export const LayoutSettingsContent = ({ board, form, isSaving, saveSettingsAsync
bg="transparent"
>
<Grid gap={{ base: "lg", xl: "xl" }} align="flex-start">
<Grid.Col span={{ base: 12, md: 4 }}>
<Grid.Col span={{ base: 12, md: 6, lg: 4 }}>
<Stack gap="md">
<TextInput {...form.getInputProps(`layouts.${index}.name`)} label={t("layout.field.name.label")} />
<Input.Wrapper label={t("layout.field.columnCount.label")}>
@@ -209,16 +209,9 @@ export const LayoutSettingsContent = ({ board, form, isSaving, saveSettingsAsync
}
max={layout.role === "custom" ? baseBreakpoint - 1 : 32767}
/>
<GutterSettings
left={layout.leftGutterColumnCount}
right={layout.rightGutterColumnCount}
columnCount={layout.columnCount}
onLeftChange={(value) => form.setFieldValue(`layouts.${index}.leftGutterColumnCount`, value)}
onRightChange={(value) => form.setFieldValue(`layouts.${index}.rightGutterColumnCount`, value)}
/>
</Stack>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 8 }}>
<Grid.Col span={{ base: 12, md: 6, lg: 4 }}>
{sourceLayout && (
<LayoutPreview
board={board}
@@ -229,6 +222,15 @@ export const LayoutSettingsContent = ({ board, form, isSaving, saveSettingsAsync
/>
)}
</Grid.Col>
<Grid.Col span={{ base: 12, lg: 4 }}>
<GutterSettings
left={layout.leftGutterColumnCount}
right={layout.rightGutterColumnCount}
columnCount={layout.columnCount}
onLeftChange={(value) => form.setFieldValue(`layouts.${index}.leftGutterColumnCount`, value)}
onRightChange={(value) => form.setFieldValue(`layouts.${index}.rightGutterColumnCount`, value)}
/>
</Grid.Col>
</Grid>
<Group justify="space-between" mt="lg" gap="sm" wrap="wrap">
@@ -334,7 +336,7 @@ const GutterSettings = ({ left, right, columnCount, onLeftChange, onRightChange
<GutterPreview left={left} right={right} columnCount={columnCount} />
<Grid>
<Grid.Col span={{ base: 12, md: 6 }}>
<Grid.Col span={{ base: 12, sm: 6 }}>
<GutterControl
side="left"
value={left}
@@ -342,7 +344,7 @@ const GutterSettings = ({ left, right, columnCount, onLeftChange, onRightChange
onChange={onLeftChange}
/>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 6 }}>
<Grid.Col span={{ base: 12, sm: 6 }}>
<GutterControl
side="right"
value={right}
@@ -377,7 +379,6 @@ const GutterControl = ({
disabled={max < 1}
onChange={(event) => onChange(event.currentTarget.checked ? Math.min(2, max) : 0)}
label={t(`layout.field.gutters.${side}.label`)}
description={t(`layout.field.gutters.${side}.description`)}
/>
{enabled && (
<Input.Wrapper label={t("layout.field.gutters.width.label")}>
@@ -53,7 +53,7 @@ export default async function BoardSettingsPage(props: Props) {
}
return (
<Container fluid px={{ base: "md", md: "xl" }}>
<Container size="xl" px={{ base: "md", md: "xl" }}>
<Stack>
<Title>{t("title", { boardName: capitalize(board.name) })}</Title>
<BoardSettingsForm
@@ -1,5 +1,9 @@
"use client";
import { Anchor } from "@mantine/core";
import { IconExternalLink } from "@tabler/icons-react";
import { createDocumentationLink } from "@homarr/definitions";
import type { UseFormReturnType } from "@homarr/form";
import { useScopedI18n } from "@homarr/translation/client";
@@ -12,11 +16,25 @@ interface AnalyticsSettingsProps {
}
export const AnalyticsSettings = ({ form }: AnalyticsSettingsProps) => {
const t = useScopedI18n("management.page.settings.section.analytics");
const t = useScopedI18n("management.page.settings.section");
return (
<SectionCard title={t("title")}>
<SwitchSetting form={form} formKey="enableGeneral" title={t("general.title")} text={t("general.text")} />
<SectionCard title={t("analytics.title")}>
<SwitchSetting
form={form}
formKey="enableGeneral"
title={t("analytics.general.title")}
text={t("analytics.general.text")}
/>
<Anchor
href={createDocumentationLink("/docs/management/settings", "#analytics")}
target="_blank"
rel="noopener noreferrer"
size="sm"
>
{t("analyticsDocumentation")}
<IconExternalLink size={14} style={{ marginInlineStart: 4, verticalAlign: "middle" }} />
</Anchor>
</SectionCard>
);
};
@@ -1,11 +1,10 @@
import { notFound } from "next/navigation";
import { Stack, Title } from "@mantine/core";
import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { getScopedI18n } from "@homarr/translation/server";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { ManagePageLayout } from "~/components/manage/manage-page-layout";
import { SettingsForm } from "./_components/settings-form";
export async function generateMetadata() {
@@ -28,12 +27,8 @@ export default async function SettingsPage() {
const tSettings = await getScopedI18n("management.page.settings");
return (
<>
<DynamicBreadcrumb />
<Stack>
<Title order={1}>{tSettings("title")}</Title>
<SettingsForm initialData={serverSettings} />
</Stack>
</>
<ManagePageLayout title={tSettings("title")}>
<SettingsForm initialData={serverSettings} />
</ManagePageLayout>
);
}
@@ -22,7 +22,6 @@ export default async function WorkshopPage() {
return (
<ManagePageLayout
title={t("title")}
size="xl"
primaryAction={
<Button
component="a"
@@ -1,38 +1,14 @@
"use client";
import { useEffect, useState } from "react";
import dynamic from "next/dynamic";
import { Center, Loader, Paper } from "@mantine/core";
import { useHotkeys } from "@mantine/hooks";
import { hotkeys } from "@homarr/definitions";
import { Spotlight } from "@homarr/spotlight/component";
import { hasPendingSpotlightOpen, openSpotlight, spotlightOpenEvent } from "@homarr/spotlight/open";
const loadSpotlight = () => import("@homarr/spotlight/component");
const SpotlightLoading = () => (
<Center
data-homarr-dev-benchmark-spotlight-feedback
pos="fixed"
inset={0}
role="status"
aria-label="Loading search"
style={{ zIndex: 1000, background: "rgb(0 0 0 / 20%)", backdropFilter: "blur(2px)" }}
>
<Paper withBorder radius="md" p="xl" shadow="xl">
<Loader />
</Paper>
</Center>
);
const Spotlight = dynamic(() => loadSpotlight().then(({ Spotlight: Component }) => Component), {
ssr: false,
loading: SpotlightLoading,
});
export const preloadSpotlight = () => void loadSpotlight().catch(() => undefined);
export const LazySpotlight = () => {
const [isMounted, setIsMounted] = useState(false);
const [isPreloaded, setIsPreloaded] = useState(false);
useHotkeys(
isMounted
@@ -49,41 +25,27 @@ export const LazySpotlight = () => {
);
useEffect(() => {
let isActive = true;
const mount = () => setIsMounted(true);
window.addEventListener(spotlightOpenEvent, mount);
if (hasPendingSpotlightOpen()) mount();
const preload = () => {
void loadSpotlight().then(
() => {
if (isActive) setIsPreloaded(true);
},
() => undefined,
);
};
let idleId: number | undefined;
const delayId = window.setTimeout(() => {
idleId = window.requestIdleCallback?.(preload, { timeout: 1_000 });
if (idleId === undefined) preload();
idleId = window.requestIdleCallback?.(mount, { timeout: 1_000 });
if (idleId === undefined) mount();
}, 1_000);
return () => {
isActive = false;
window.removeEventListener(spotlightOpenEvent, mount);
window.clearTimeout(delayId);
if (idleId !== undefined) window.cancelIdleCallback?.(idleId);
};
}, []);
return isMounted || isPreloaded ? (
return isMounted ? (
<>
{isPreloaded && <span hidden data-homarr-dev-benchmark-spotlight-preloaded />}
{isMounted && (
<>
<span hidden data-homarr-dev-benchmark-spotlight-mounted />
<Spotlight />
</>
)}
<span hidden data-homarr-dev-benchmark-spotlight-preloaded />
<span hidden data-homarr-dev-benchmark-spotlight-mounted />
<Spotlight />
</>
) : null;
};
@@ -8,7 +8,6 @@ import { useI18n } from "@homarr/translation/client";
import { HeaderButton } from "./button";
import classes from "./search.module.css";
import { preloadSpotlight } from "./lazy-spotlight";
export const DesktopSearchInput = () => {
const t = useI18n();
@@ -22,9 +21,6 @@ export const DesktopSearchInput = () => {
size="sm"
leftSection={<IconSearch size={20} stroke={1.5} />}
onClick={openSpotlight}
onFocus={preloadSpotlight}
onPointerDown={preloadSpotlight}
onPointerEnter={preloadSpotlight}
radius="xl"
>
{`${t("search.placeholder")}...`}
@@ -37,9 +33,6 @@ export const MobileSearchButton = () => {
<HeaderButton
data-homarr-dev-benchmark-interaction="search"
onClick={openSpotlight}
onFocus={preloadSpotlight}
onPointerDown={preloadSpotlight}
onPointerEnter={preloadSpotlight}
className={classes.mobileSearch}
>
<IconSearch size={20} stroke={1.5} />
@@ -13,8 +13,8 @@ export const UnsavedChangesBar = ({ children }: UnsavedChangesBarProps) => {
const t = useI18n();
return (
<div style={{ position: "sticky", bottom: 20 }}>
<Card withBorder>
<div style={{ position: "sticky", bottom: 20, zIndex: "var(--mantine-z-index-app)" }}>
<Card withBorder bg="var(--mantine-color-body)">
<Group justify="space-between" wrap="wrap">
<Text fw={500}>{t("common.unsavedChanges")}</Text>
<Group>{children}</Group>
+1
View File
@@ -92,6 +92,7 @@ describe("lazy widget application graph", () => {
timeout: 15_000,
});
await page.keyboard.press("Control+K");
await expect(page.locator("[data-homarr-dev-benchmark-spotlight-feedback]")).toHaveCount(0);
await expect(page.locator("[data-homarr-dev-benchmark-spotlight]")).toBeVisible({ timeout: 15_000 });
await page.keyboard.press("Escape");
await expect(page.locator("[data-homarr-dev-benchmark-spotlight]")).not.toBeVisible();
+1
View File
@@ -6060,6 +6060,7 @@
}
},
"section": {
"analyticsDocumentation": "See exactly what analytics data Homarr collects",
"analytics": {
"title": "Analytics",
"general": {