feat(onboarding): polish setup experience

This commit is contained in:
Thomas Camlong authored and ajnart committed 2026-08-14 01:20:27 +02:00
1 parent f4891447d3
commit 4ca02a9133
34 files changed
+1005 -1172

No files matched your search

@@ -0,0 +1,10 @@
"use client";
import { ColorInput, useMantineTheme } from "@mantine/core";
import type { ColorInputProps } from "@mantine/core";
export const BoardColorInput = (props: ColorInputProps) => {
const theme = useMantineTheme();
return <ColorInput {...props} format="hex" swatches={Object.values(theme.colors).map((color) => color[6])} />;
};
@@ -0,0 +1,86 @@
"use client";
import React from "react";
import { Combobox, Group, InputBase, Text, useCombobox } from "@mantine/core";
import { IconCheck, IconDeviceDesktop, IconMoon, IconSun } from "@tabler/icons-react";
import type { ColorScheme } from "@homarr/definitions";
import { colorSchemes } from "@homarr/definitions";
import { useScopedI18n } from "@homarr/translation/client";
const schemeIcons: Record<ColorScheme, React.ElementType> = {
auto: IconDeviceDesktop,
light: IconSun,
dark: IconMoon,
};
interface ColorSchemeComboboxProps {
label?: string;
value: ColorScheme;
onChange: (value: ColorScheme) => void;
width?: string;
}
export const ColorSchemeCombobox = ({ label, value, onChange, width }: ColorSchemeComboboxProps) => {
const tOptions = useScopedI18n("common.colorScheme.options");
const combobox = useCombobox({ onDropdownClose: () => combobox.resetSelectedOption() });
return (
<Combobox
store={combobox}
onOptionSubmit={(nextValue) => {
onChange(nextValue as ColorScheme);
combobox.closeDropdown();
}}
>
<Combobox.Target>
<InputBase
component="button"
type="button"
pointer
label={label}
rightSection={<Combobox.Chevron />}
rightSectionPointerEvents="none"
onClick={() => combobox.toggleDropdown()}
variant="filled"
w={width}
>
<OptionItem scheme={value} currentScheme={value} tOptions={tOptions} />
</InputBase>
</Combobox.Target>
<Combobox.Dropdown>
<Combobox.Options>
{colorSchemes.map((scheme) => (
<Combobox.Option value={scheme} key={scheme}>
<OptionItem scheme={scheme} currentScheme={value} tOptions={tOptions} showCheck />
</Combobox.Option>
))}
</Combobox.Options>
</Combobox.Dropdown>
</Combobox>
);
};
const OptionItem = ({
scheme,
currentScheme,
tOptions,
showCheck,
}: {
scheme: ColorScheme;
currentScheme: ColorScheme;
tOptions: (key: ColorScheme) => string;
showCheck?: boolean;
}) => {
const Icon = schemeIcons[scheme];
return (
<Group wrap="nowrap" justify="space-between">
<Group wrap="nowrap">
<Icon size={16} stroke={1.5} />
<Text>{tOptions(scheme)}</Text>
</Group>
{showCheck && scheme === currentScheme && <IconCheck color="currentColor" size={16} />}
</Group>
);
};
+3
View File
@@ -15,6 +15,9 @@ export { BetaBadge } from "./beta-badge";
export { MaskedImage } from "./masked-image";
export { MaskedOrNormalImage } from "./masked-or-normal-image";
export { LanguageIcon } from "./language-icon";
export { LanguageCombobox } from "./language-combobox";
export { ColorSchemeCombobox } from "./color-scheme-combobox";
export { BoardColorInput } from "./board-color-input";
export { Link } from "./link";
export {
SelectGridLayout,
@@ -0,0 +1,92 @@
"use client";
import React from "react";
import { Combobox, Group, InputBase, Loader, ScrollArea, Text, useCombobox } from "@mantine/core";
import { IconCheck } from "@tabler/icons-react";
import type { SupportedLanguage } from "@homarr/translation";
import { localeConfigurations, supportedLanguages } from "@homarr/translation";
import { LanguageIcon } from "./language-icon";
interface LanguageComboboxProps {
label?: string;
value: SupportedLanguage;
onChange: (value: SupportedLanguage) => void;
isPending?: boolean;
width?: string;
withinPortal?: boolean;
}
export const LanguageCombobox = ({
label,
value,
onChange,
isPending,
width,
withinPortal = false,
}: LanguageComboboxProps) => {
const combobox = useCombobox({ onDropdownClose: () => combobox.resetSelectedOption() });
return (
<Combobox
store={combobox}
withinPortal={withinPortal}
onOptionSubmit={(nextValue) => {
onChange(nextValue as SupportedLanguage);
combobox.closeDropdown();
}}
>
<Combobox.Target>
<InputBase
component="button"
type="button"
pointer
label={label}
leftSection={isPending ? <Loader size={16} /> : null}
rightSection={<Combobox.Chevron />}
rightSectionPointerEvents="none"
onClick={() => combobox.toggleDropdown()}
variant="filled"
w={width}
>
<OptionItem currentLocale={value} localeKey={value} />
</InputBase>
</Combobox.Target>
<Combobox.Dropdown>
<ScrollArea h={300}>
<Combobox.Options>
{supportedLanguages.map((locale) => (
<Combobox.Option value={locale} key={locale}>
<OptionItem currentLocale={value} localeKey={locale} showCheck />
</Combobox.Option>
))}
</Combobox.Options>
</ScrollArea>
</Combobox.Dropdown>
</Combobox>
);
};
const OptionItem = ({
currentLocale,
localeKey,
showCheck,
}: {
currentLocale: SupportedLanguage;
localeKey: SupportedLanguage;
showCheck?: boolean;
}) => (
<Group wrap="nowrap" justify="space-between">
<Group wrap="nowrap">
<LanguageIcon icon={localeConfigurations[localeKey].icon} />
<Group wrap="nowrap" gap="xs">
<Text>{localeConfigurations[localeKey].name}</Text>
<Text size="xs" c="dimmed" inherit>
({localeConfigurations[localeKey].translatedName})
</Text>
</Group>
</Group>
{showCheck && localeKey === currentLocale && <IconCheck color="currentColor" size={16} />}
</Group>
);