feat: rework management settings layout

This commit is contained in:
ajnart committed 2026-08-20 11:48:13 +02:00
1 parent a2219b72f6
commit c09f3e7501
6 files changed
+100 -9

No files matched your search

+1 -1
View File
@@ -13,7 +13,7 @@ tags:
The settings page applies options to the whole Homarr application.
Some changes need a full page reload before they take effect.
All changes are collected in a single form. Click the **Save changes** button in the save bar at the bottom of the page to apply them. The save bar only appears when you have unsaved changes, and the **Discard** button reverts your changes.
Choose a section from the settings navigation to edit its options. All sections are collected in a single form, so you can move between them without losing unsaved changes. Click the **Save changes** button in the save bar at the bottom of the page to apply them. The save bar only appears when you have unsaved changes, and the **Discard** button reverts your changes.
## Analytics
@@ -29,6 +29,7 @@ export const AppearanceSettingsForm = ({ form }: AppearanceSettingsFormProps) =>
}))}
{...form.getInputProps("defaultColorScheme")}
SelectOption={AppearanceCustomOption}
withinPortal
/>
</SectionCard>
);
@@ -26,6 +26,7 @@ export const BoardSettingsForm = ({ form }: BoardSettingsFormProps) => {
clearable
boards={selectableBoards}
{...form.getInputProps("homeBoardId")}
withinPortal
/>
<BoardSelect
@@ -34,6 +35,7 @@ export const BoardSettingsForm = ({ form }: BoardSettingsFormProps) => {
clearable
boards={selectableBoards}
{...form.getInputProps("mobileHomeBoardId")}
withinPortal
/>
<Text fw={500}>{tBoard("status.title")}</Text>
@@ -22,6 +22,7 @@ export const CultureSettingsForm = ({ form }: CultureSettingsFormProps) => {
label={tCulture("defaultLocale.label")}
{...localeInputProps}
value={localeInputProps.value as SupportedLanguage}
withinPortal
/>
</SectionCard>
);
@@ -0,0 +1,35 @@
.list {
align-self: flex-start;
flex-wrap: nowrap;
min-width: 14rem;
position: sticky;
top: calc(var(--app-shell-header-height) + var(--mantine-spacing-md));
}
.tab {
justify-content: flex-start;
}
.panel {
flex: 1;
min-width: 0;
padding-inline-start: var(--mantine-spacing-xl);
}
@media (max-width: $mantine-breakpoint-sm) {
.list {
max-width: calc(100vw - var(--mantine-spacing-xl));
min-width: 0;
overflow-x: auto;
position: static;
}
.tab {
flex: 0 0 auto;
}
.panel {
padding-block-start: var(--mantine-spacing-lg);
padding-inline-start: 0;
}
}
@@ -1,7 +1,17 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { Button, Stack } from "@mantine/core";
import { Button, Stack, Tabs, TabsList, TabsPanel, TabsTab } from "@mantine/core";
import { useMediaQuery } from "@mantine/hooks";
import {
IconChartBar,
IconLanguage,
IconLayoutDashboard,
IconPalette,
IconSearch,
IconUsers,
IconWorldSearch,
} from "@tabler/icons-react";
import { z } from "zod/v4";
import { clientApi } from "@homarr/api/client";
@@ -20,6 +30,7 @@ import { BoardSettingsForm } from "./board-settings-form";
import { CrawlingAndIndexingSettings } from "./crawling-and-indexing.settings";
import { CultureSettingsForm } from "./culture-settings-form";
import { SearchSettingsForm } from "./search-settings-form";
import classes from "./settings-form.module.css";
import { UserSettingsForm } from "./user-settings-form";
const settingsFormSchema = z.object({
@@ -63,6 +74,7 @@ interface SettingsFormProps {
export const SettingsForm = ({ initialData }: SettingsFormProps) => {
const t = useI18n();
const tSettings = useScopedI18n("management.page.settings");
const isDesktop = useMediaQuery("(min-width: 48em)");
const initialValues = buildInitialValues(initialData);
const initialValuesRef = useRef(initialValues);
@@ -169,13 +181,53 @@ export const SettingsForm = ({ initialData }: SettingsFormProps) => {
return (
<form onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}>
<Stack gap="xl">
<AnalyticsSettings form={form} />
<CrawlingAndIndexingSettings form={form} />
<BoardSettingsForm form={form} />
<UserSettingsForm form={form} />
<SearchSettingsForm form={form} />
<AppearanceSettingsForm form={form} />
<CultureSettingsForm form={form} />
<Tabs defaultValue="board" orientation={isDesktop ? "vertical" : "horizontal"}>
<TabsList className={classes.list} aria-label={tSettings("title")}>
<TabsTab value="board" className={classes.tab} leftSection={<IconLayoutDashboard size={18} />}>
{tSettings("section.board.title")}
</TabsTab>
<TabsTab value="user" className={classes.tab} leftSection={<IconUsers size={18} />}>
{tSettings("section.user.title")}
</TabsTab>
<TabsTab value="search" className={classes.tab} leftSection={<IconSearch size={18} />}>
{tSettings("section.search.title")}
</TabsTab>
<TabsTab value="appearance" className={classes.tab} leftSection={<IconPalette size={18} />}>
{tSettings("section.appearance.title")}
</TabsTab>
<TabsTab value="culture" className={classes.tab} leftSection={<IconLanguage size={18} />}>
{tSettings("section.culture.title")}
</TabsTab>
<TabsTab value="analytics" className={classes.tab} leftSection={<IconChartBar size={18} />}>
{tSettings("section.analytics.title")}
</TabsTab>
<TabsTab value="crawling" className={classes.tab} leftSection={<IconWorldSearch size={18} />}>
{tSettings("section.crawlingAndIndexing.title")}
</TabsTab>
</TabsList>
<TabsPanel value="board" className={classes.panel}>
<BoardSettingsForm form={form} />
</TabsPanel>
<TabsPanel value="user" className={classes.panel}>
<UserSettingsForm form={form} />
</TabsPanel>
<TabsPanel value="search" className={classes.panel}>
<SearchSettingsForm form={form} />
</TabsPanel>
<TabsPanel value="appearance" className={classes.panel}>
<AppearanceSettingsForm form={form} />
</TabsPanel>
<TabsPanel value="culture" className={classes.panel}>
<CultureSettingsForm form={form} />
</TabsPanel>
<TabsPanel value="analytics" className={classes.panel}>
<AnalyticsSettings form={form} />
</TabsPanel>
<TabsPanel value="crawling" className={classes.panel}>
<CrawlingAndIndexingSettings form={form} />
</TabsPanel>
</Tabs>
{form.isDirty() && (
<UnsavedChangesBar>