Compare commits

..
50 changed files with 156 additions and 217 deletions

No files matched your search

-2
View File
@@ -31,8 +31,6 @@ const nextConfig: NextConfig = {
experimental: { experimental: {
optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"], optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
turbopackFileSystemCacheForDev: true, turbopackFileSystemCacheForDev: true,
preloadEntriesOnStart: false,
webpackMemoryOptimizations: true,
}, },
transpilePackages: ["@homarr/ui", "@homarr/notifications", "@homarr/modals", "@homarr/spotlight", "@homarr/widgets"], transpilePackages: ["@homarr/ui", "@homarr/notifications", "@homarr/modals", "@homarr/spotlight", "@homarr/widgets"],
images: { images: {
@@ -2,7 +2,7 @@
import type { PropsWithChildren } from "react"; import type { PropsWithChildren } from "react";
import type { MantineColorScheme, MantineColorSchemeManager } from "@mantine/core"; import type { MantineColorScheme, MantineColorSchemeManager } from "@mantine/core";
import { DirectionProvider, MantineProvider, v8CssVariablesResolver } from "@mantine/core"; import { DirectionProvider, MantineProvider } from "@mantine/core";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
@@ -18,12 +18,7 @@ export const CustomMantineProvider = ({
const manager = useColorSchemeManager(); const manager = useColorSchemeManager();
return ( return (
<DirectionProvider> <DirectionProvider>
<MantineProvider <MantineProvider defaultColorScheme={defaultColorScheme} colorSchemeManager={manager} theme={theme}>
defaultColorScheme={defaultColorScheme}
colorSchemeManager={manager}
theme={theme}
cssVariablesResolver={v8CssVariablesResolver}
>
{children} {children}
</MantineProvider> </MantineProvider>
</DirectionProvider> </DirectionProvider>
@@ -2,7 +2,7 @@
import type { PropsWithChildren } from "react"; import type { PropsWithChildren } from "react";
import type { MantineColorsTuple } from "@mantine/core"; import type { MantineColorsTuple } from "@mantine/core";
import { colorsTuple, createTheme, darken, lighten, MantineProvider, rem, v8CssVariablesResolver } from "@mantine/core"; import { colorsTuple, createTheme, darken, lighten, MantineProvider, rem } from "@mantine/core";
import { useRequiredBoard } from "@homarr/boards/context"; import { useRequiredBoard } from "@homarr/boards/context";
import type { ColorScheme } from "@homarr/definitions"; import type { ColorScheme } from "@homarr/definitions";
@@ -33,12 +33,7 @@ export const BoardMantineProvider = ({
}); });
return ( return (
<MantineProvider <MantineProvider defaultColorScheme={defaultColorScheme} theme={theme} colorSchemeManager={colorSchemeManager}>
defaultColorScheme={defaultColorScheme}
theme={theme}
colorSchemeManager={colorSchemeManager}
cssVariablesResolver={v8CssVariablesResolver}
>
{children} {children}
</MantineProvider> </MantineProvider>
); );
@@ -1,15 +1,15 @@
"use client"; "use client";
import { Button, Stack, TextInput } from "@mantine/core"; import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
import type { z } from "zod/v4"; import type { z } from "zod/v4";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { signIn } from "@homarr/auth/client"; import { signIn } from "@homarr/auth/client";
import { revalidatePathActionAsync } from "@homarr/common/client"; import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { UserCreatePasswordFields } from "@homarr/forms-collection";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import { CustomPasswordInput } from "@homarr/ui";
import { userInitSchema } from "@homarr/validation/user"; import { userInitSchema } from "@homarr/validation/user";
export const InitUserForm = () => { export const InitUserForm = () => {
@@ -59,10 +59,12 @@ export const InitUserForm = () => {
> >
<Stack gap="lg"> <Stack gap="lg">
<TextInput label={t("field.username.label")} {...form.getInputProps("username")} /> <TextInput label={t("field.username.label")} {...form.getInputProps("username")} />
<UserCreatePasswordFields <CustomPasswordInput
passwordInputProps={form.getInputProps("password")} withPasswordRequirements
confirmPasswordInputProps={form.getInputProps("confirmPassword")} label={t("field.password.label")}
{...form.getInputProps("password")}
/> />
<PasswordInput label={t("field.passwordConfirm.label")} {...form.getInputProps("confirmPassword")} />
<Button type="submit" fullWidth loading={isPending}> <Button type="submit" fullWidth loading={isPending}>
{t("action.create")} {t("action.create")}
</Button> </Button>
@@ -107,23 +107,6 @@ export function DockerTable(initialData: RouterOutputs["docker"]["getContainers"
refetchOnReconnect: false, refetchOnReconnect: false,
}); });
const relativeTime = useTimeAgo(data.timestamp); const relativeTime = useTimeAgo(data.timestamp);
const utils = clientApi.useUtils();
const { mutate, isPending } = clientApi.docker.invalidate.useMutation({
async onSuccess() {
await utils.docker.getContainers.invalidate();
showSuccessNotification({
title: tDocker("action.refresh.notification.success.title"),
message: tDocker("action.refresh.notification.success.message"),
});
},
onError() {
showErrorNotification({
title: tDocker("action.refresh.notification.error.title"),
message: tDocker("action.refresh.notification.error.message"),
});
},
});
const table = useTranslatedMantineReactTable({ const table = useTranslatedMantineReactTable({
data: data.containers, data: data.containers,
enableDensityToggle: false, enableDensityToggle: false,
@@ -142,11 +125,35 @@ export function DockerTable(initialData: RouterOutputs["docker"]["getContainers"
}, },
initialState: { density: "xs", showGlobalFilter: true }, initialState: { density: "xs", showGlobalFilter: true },
renderTopToolbarCustomActions: () => ( renderTopToolbarCustomActions: () => {
<Button variant="default" rightSection={<IconRefresh size="1rem" />} onClick={() => mutate()} loading={isPending}> const utils = clientApi.useUtils();
{tDocker("action.refresh.label")} const { mutate, isPending } = clientApi.docker.invalidate.useMutation({
</Button> async onSuccess() {
), await utils.docker.getContainers.invalidate();
showSuccessNotification({
title: tDocker("action.refresh.notification.success.title"),
message: tDocker("action.refresh.notification.success.message"),
});
},
onError() {
showErrorNotification({
title: tDocker("action.refresh.notification.error.title"),
message: tDocker("action.refresh.notification.error.message"),
});
},
});
return (
<Button
variant="default"
rightSection={<IconRefresh size="1rem" />}
onClick={() => mutate()}
loading={isPending}
>
{tDocker("action.refresh.label")}
</Button>
);
},
renderToolbarAlertBannerContent: ({ groupedAlert, table }) => { renderToolbarAlertBannerContent: ({ groupedAlert, table }) => {
const dockerContainers = table.getSelectedRowModel().rows.map((row) => row.original); const dockerContainers = table.getSelectedRowModel().rows.map((row) => row.original);
return ( return (
@@ -1,7 +1,20 @@
"use client"; "use client";
import { startTransition, useCallback, useState } from "react"; import { startTransition, useCallback, useState } from "react";
import { Badge, Button, Card, Group, Stack, Stepper, Table, Text, TextInput, Title, Tooltip } from "@mantine/core"; import {
Badge,
Button,
Card,
Group,
PasswordInput,
Stack,
Stepper,
Table,
Text,
TextInput,
Title,
Tooltip,
} from "@mantine/core";
import { useListState } from "@mantine/hooks"; import { useListState } from "@mantine/hooks";
import { IconPlus, IconUserCheck } from "@tabler/icons-react"; import { IconPlus, IconUserCheck } from "@tabler/icons-react";
import { z } from "zod/v4"; import { z } from "zod/v4";
@@ -11,11 +24,10 @@ import type { GroupPermissionKey } from "@homarr/definitions";
import { everyoneGroup, groupPermissions } from "@homarr/definitions"; import { everyoneGroup, groupPermissions } from "@homarr/definitions";
import type { IsValid } from "@homarr/form"; import type { IsValid } from "@homarr/form";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { UserCreatePasswordFields } from "@homarr/forms-collection";
import { useModalAction } from "@homarr/modals"; import { useModalAction } from "@homarr/modals";
import { showErrorNotification } from "@homarr/notifications"; import { showErrorNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client"; import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { UserAvatar } from "@homarr/ui"; import { CustomPasswordInput, UserAvatar } from "@homarr/ui";
import { createCustomErrorParams } from "@homarr/validation/form/i18n"; import { createCustomErrorParams } from "@homarr/validation/form/i18n";
import { userPasswordSchema } from "@homarr/validation/user"; import { userPasswordSchema } from "@homarr/validation/user";
@@ -155,10 +167,18 @@ export const UserCreateStepperComponent = ({ initialGroups }: UserCreateStepperC
<form> <form>
<Card p="xl"> <Card p="xl">
<Stack gap="md"> <Stack gap="md">
<UserCreatePasswordFields <CustomPasswordInput
withPasswordRequirements
label={tUserField("password.label")}
variant="filled" variant="filled"
passwordInputProps={securityForm.getInputProps("password")} withAsterisk
confirmPasswordInputProps={securityForm.getInputProps("confirmPassword")} {...securityForm.getInputProps("password")}
/>
<PasswordInput
label={tUserField("passwordConfirm.label")}
variant="filled"
withAsterisk
{...securityForm.getInputProps("confirmPassword")}
/> />
</Stack> </Stack>
</Card> </Card>
+8 -50
View File
@@ -81,7 +81,13 @@ describe("initUser should initialize the first user", () => {
await expect(actAsync()).rejects.toThrow("passwordsDoNotMatch"); await expect(actAsync()).rejects.toThrow("passwordsDoNotMatch");
}); });
it.each([["aB2%"], ["short"]])("should reject passwords shorter than 8 characters for '%s'", async (password) => { it.each([
["aB2%"], // too short
["abc123DEF"], // does not contain special characters
["abcDEFghi+"], // does not contain numbers
["ABC123+/-"], // does not contain lowercase
["abc123+/-"], // does not contain uppercase
])("should throw error that password requirements do not match for '%s' as password", async (password) => {
const db = createDb(); const db = createDb();
await createOnboardingStepAsync(db, "user"); await createOnboardingStepAsync(db, "user");
const caller = userRouter.createCaller({ const caller = userRouter.createCaller({
@@ -97,55 +103,7 @@ describe("initUser should initialize the first user", () => {
confirmPassword: password, confirmPassword: password,
}); });
await expect(actAsync()).rejects.toThrow(); await expect(actAsync()).rejects.toThrow("passwordRequirements");
});
it("should accept passwords without complexity requirements", async () => {
const db = createDb();
await createOnboardingStepAsync(db, "user");
const caller = userRouter.createCaller({
db,
deviceType: undefined,
session: null,
});
await caller.initUser({
username: "test",
password: "abc123DEF",
confirmPassword: "abc123DEF",
});
const user = await db.query.users.findFirst({
columns: {
id: true,
},
});
expect(user).toBeDefined();
});
it("should accept passwords with special characters like LoveHomarr<3", async () => {
const db = createDb();
await createOnboardingStepAsync(db, "user");
const caller = userRouter.createCaller({
db,
deviceType: undefined,
session: null,
});
await caller.initUser({
username: "test",
password: "LoveHomarr<3",
confirmPassword: "LoveHomarr<3",
});
const user = await db.query.users.findFirst({
columns: {
id: true,
},
});
expect(user).toBeDefined();
}); });
}); });
-2
View File
@@ -5,5 +5,3 @@ export * from "./icon-picker/icon-picker";
export * from "./new-app/icon-matcher"; export * from "./new-app/icon-matcher";
export * from "./upload-media/upload-media"; export * from "./upload-media/upload-media";
export * from "./user-create/user-create-password-fields";
@@ -1,33 +0,0 @@
"use client";
import { PasswordInput, Stack } from "@mantine/core";
import type { PasswordInputProps } from "@mantine/core";
import { useScopedI18n } from "@homarr/translation/client";
import { CustomPasswordInput } from "@homarr/ui";
interface UserCreatePasswordFieldsProps {
passwordInputProps: PasswordInputProps;
confirmPasswordInputProps: PasswordInputProps;
variant?: PasswordInputProps["variant"];
}
export const UserCreatePasswordFields = ({
passwordInputProps,
confirmPasswordInputProps,
variant,
}: UserCreatePasswordFieldsProps) => {
const t = useScopedI18n("user.field");
return (
<Stack gap="md">
<CustomPasswordInput
withPasswordRequirements
label={t("password.label")}
variant={variant}
{...passwordInputProps}
/>
<PasswordInput label={t("passwordConfirm.label")} variant={variant} {...confirmPasswordInputProps} />
</Stack>
);
};
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Contrasenya", "label": "Contrasenya",
"suggestion": { "requirement": {
"length": "Inclou com a mínim 8 caràcters", "length": "Inclou com a mínim 8 caràcters",
"lowercase": "Inclou minúscules", "lowercase": "Inclou minúscules",
"uppercase": "Inclou majúscules", "uppercase": "Inclou majúscules",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "密码", "label": "密码",
"suggestion": { "requirement": {
"length": "至少包含 8 个字符", "length": "至少包含 8 个字符",
"lowercase": "包含小写字母", "lowercase": "包含小写字母",
"uppercase": "包含大写字母", "uppercase": "包含大写字母",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "crwdns26:0crwdne26:0", "label": "crwdns26:0crwdne26:0",
"suggestion": { "requirement": {
"length": "crwdns28:0crwdne28:0", "length": "crwdns28:0crwdne28:0",
"lowercase": "crwdns30:0crwdne30:0", "lowercase": "crwdns30:0crwdne30:0",
"uppercase": "crwdns32:0crwdne32:0", "uppercase": "crwdns32:0crwdne32:0",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Heslo", "label": "Heslo",
"suggestion": { "requirement": {
"length": "Obsahuje alespoň 8 znaků", "length": "Obsahuje alespoň 8 znaků",
"lowercase": "Obsahuje malé písmeno", "lowercase": "Obsahuje malé písmeno",
"uppercase": "Obsahuje velké písmeno", "uppercase": "Obsahuje velké písmeno",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Adgangskode", "label": "Adgangskode",
"suggestion": { "requirement": {
"length": "Inkluderer mindst 8 tegn", "length": "Inkluderer mindst 8 tegn",
"lowercase": "Inkluderer små bogstaver", "lowercase": "Inkluderer små bogstaver",
"uppercase": "Inkluderer store bogstaver", "uppercase": "Inkluderer store bogstaver",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Passwort", "label": "Passwort",
"suggestion": { "requirement": {
"length": "Enthält mindestens 8 Zeichen", "length": "Enthält mindestens 8 Zeichen",
"lowercase": "Enthält Kleinbuchstaben", "lowercase": "Enthält Kleinbuchstaben",
"uppercase": "Enthält Großbuchstaben", "uppercase": "Enthält Großbuchstaben",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Passwort", "label": "Passwort",
"suggestion": { "requirement": {
"length": "Enthält mindestens 8 Zeichen", "length": "Enthält mindestens 8 Zeichen",
"lowercase": "Enthält Kleinbuchstaben", "lowercase": "Enthält Kleinbuchstaben",
"uppercase": "Enthält Großbuchstaben", "uppercase": "Enthält Großbuchstaben",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Κωδικός", "label": "Κωδικός",
"suggestion": { "requirement": {
"length": "", "length": "",
"lowercase": "Περιλαμβάνει πεζό γράμμα", "lowercase": "Περιλαμβάνει πεζό γράμμα",
"uppercase": "Περιλαμβάνει κεφαλαίο γράμμα", "uppercase": "Περιλαμβάνει κεφαλαίο γράμμα",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Password", "label": "Password",
"suggestion": { "requirement": {
"length": "Includes a minimum of 8 characters", "length": "Includes a minimum of 8 characters",
"lowercase": "Includes a lower-case letter", "lowercase": "Includes a lower-case letter",
"uppercase": "Includes an upper-case letter", "uppercase": "Includes an upper-case letter",
+2 -3
View File
@@ -132,9 +132,8 @@
}, },
"password": { "password": {
"label": "Password", "label": "Password",
"suggestionsTitle": "Password suggestions", "requirement": {
"suggestion": { "length": "Includes at least 8 characters",
"length": "At least 8 characters",
"lowercase": "Includes lowercase letter", "lowercase": "Includes lowercase letter",
"uppercase": "Includes uppercase letter", "uppercase": "Includes uppercase letter",
"number": "Includes number", "number": "Includes number",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Contraseña", "label": "Contraseña",
"suggestion": { "requirement": {
"length": "Incluye al menos 8 caracteres", "length": "Incluye al menos 8 caracteres",
"lowercase": "Incluye al menos una letra minúscula", "lowercase": "Incluye al menos una letra minúscula",
"uppercase": "Incluye al menos una letra mayúscula", "uppercase": "Incluye al menos una letra mayúscula",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "", "label": "",
"suggestion": { "requirement": {
"length": "", "length": "",
"lowercase": "", "lowercase": "",
"uppercase": "", "uppercase": "",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Mot de passe", "label": "Mot de passe",
"suggestion": { "requirement": {
"length": "Contient au moins 8 caractères", "length": "Contient au moins 8 caractères",
"lowercase": "Inclut une lettre minuscule", "lowercase": "Inclut une lettre minuscule",
"uppercase": "Inclut une lettre majuscule", "uppercase": "Inclut une lettre majuscule",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "סיסמה", "label": "סיסמה",
"suggestion": { "requirement": {
"length": "כולל לפחות 8 תווים", "length": "כולל לפחות 8 תווים",
"lowercase": "אפשר אותיות קטנות", "lowercase": "אפשר אותיות קטנות",
"uppercase": "אפשר אותיות גדולות", "uppercase": "אפשר אותיות גדולות",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Lozinka", "label": "Lozinka",
"suggestion": { "requirement": {
"length": "", "length": "",
"lowercase": "Uključuje mala slova", "lowercase": "Uključuje mala slova",
"uppercase": "Uključuje veliko slovo", "uppercase": "Uključuje veliko slovo",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Jelszó", "label": "Jelszó",
"suggestion": { "requirement": {
"length": "Tartalmazzon legalább 8 karaktert", "length": "Tartalmazzon legalább 8 karaktert",
"lowercase": "Tartalmazzon kisbetűt", "lowercase": "Tartalmazzon kisbetűt",
"uppercase": "Tartalmazzon nagybetűt", "uppercase": "Tartalmazzon nagybetűt",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Password", "label": "Password",
"suggestion": { "requirement": {
"length": "Include almeno 8 caratteri", "length": "Include almeno 8 caratteri",
"lowercase": "Include lettera minuscola", "lowercase": "Include lettera minuscola",
"uppercase": "Include lettera maiuscola", "uppercase": "Include lettera maiuscola",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "パスワード", "label": "パスワード",
"suggestion": { "requirement": {
"length": "少なくとも 8 文字以上を含む", "length": "少なくとも 8 文字以上を含む",
"lowercase": "小文字を含む", "lowercase": "小文字を含む",
"uppercase": "大文字を含む", "uppercase": "大文字を含む",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "비밀번호", "label": "비밀번호",
"suggestion": { "requirement": {
"length": "", "length": "",
"lowercase": "소문자 포함", "lowercase": "소문자 포함",
"uppercase": "대문자 포함", "uppercase": "대문자 포함",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Slaptažodis", "label": "Slaptažodis",
"suggestion": { "requirement": {
"length": "", "length": "",
"lowercase": "", "lowercase": "",
"uppercase": "", "uppercase": "",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Parole", "label": "Parole",
"suggestion": { "requirement": {
"length": "", "length": "",
"lowercase": "Ietver mazo burtu", "lowercase": "Ietver mazo burtu",
"uppercase": "Ietver lielo burtu", "uppercase": "Ietver lielo burtu",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Wachtwoord", "label": "Wachtwoord",
"suggestion": { "requirement": {
"length": "Bevat ten minste 8 tekens", "length": "Bevat ten minste 8 tekens",
"lowercase": "Inclusief kleine letter", "lowercase": "Inclusief kleine letter",
"uppercase": "Inclusief hoofdletter", "uppercase": "Inclusief hoofdletter",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Passord", "label": "Passord",
"suggestion": { "requirement": {
"length": "Inkluderer minst 8 tegn", "length": "Inkluderer minst 8 tegn",
"lowercase": "Inkluderer liten bokstav", "lowercase": "Inkluderer liten bokstav",
"uppercase": "Inkluderer stor bokstav", "uppercase": "Inkluderer stor bokstav",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Hasło", "label": "Hasło",
"suggestion": { "requirement": {
"length": "Zawiera co najmniej 8 znaków", "length": "Zawiera co najmniej 8 znaków",
"lowercase": "Zawiera małą literę", "lowercase": "Zawiera małą literę",
"uppercase": "Zawiera wielką literę", "uppercase": "Zawiera wielką literę",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Senha", "label": "Senha",
"suggestion": { "requirement": {
"length": "Inclua pelo menos 8 caracteres", "length": "Inclua pelo menos 8 caracteres",
"lowercase": "Inclui letras minúsculas", "lowercase": "Inclui letras minúsculas",
"uppercase": "Inclui letras maiúsculas", "uppercase": "Inclui letras maiúsculas",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "", "label": "",
"suggestion": { "requirement": {
"length": "", "length": "",
"lowercase": "", "lowercase": "",
"uppercase": "", "uppercase": "",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Parola", "label": "Parola",
"suggestion": { "requirement": {
"length": "", "length": "",
"lowercase": "Să includă litere mici", "lowercase": "Să includă litere mici",
"uppercase": "Să includă litere mari", "uppercase": "Să includă litere mari",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Пароль", "label": "Пароль",
"suggestion": { "requirement": {
"length": "Содержит не менее 8 символов", "length": "Содержит не менее 8 символов",
"lowercase": "Содержит строчную букву", "lowercase": "Содержит строчную букву",
"uppercase": "Содержит заглавную букву", "uppercase": "Содержит заглавную букву",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Heslo", "label": "Heslo",
"suggestion": { "requirement": {
"length": "Obsahuje aspoň 8 znakov", "length": "Obsahuje aspoň 8 znakov",
"lowercase": "Zahŕňa malé písmeno", "lowercase": "Zahŕňa malé písmeno",
"uppercase": "Zahŕňa veľké písmená", "uppercase": "Zahŕňa veľké písmená",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Geslo", "label": "Geslo",
"suggestion": { "requirement": {
"length": "", "length": "",
"lowercase": "Vključuje male črke", "lowercase": "Vključuje male črke",
"uppercase": "Vključuje velike tiskane črke", "uppercase": "Vključuje velike tiskane črke",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Lösenord", "label": "Lösenord",
"suggestion": { "requirement": {
"length": "Använd minst 8 tecken", "length": "Använd minst 8 tecken",
"lowercase": "Använd liten bokstav", "lowercase": "Använd liten bokstav",
"uppercase": "Använd stor bokstav", "uppercase": "Använd stor bokstav",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Şifre", "label": "Şifre",
"suggestion": { "requirement": {
"length": "En az 8 karakter içermeli", "length": "En az 8 karakter içermeli",
"lowercase": "Küçük harf içermeli", "lowercase": "Küçük harf içermeli",
"uppercase": "Büyük harf içermeli", "uppercase": "Büyük harf içermeli",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Пароль", "label": "Пароль",
"suggestion": { "requirement": {
"length": "Включає принаймні 8 символів", "length": "Включає принаймні 8 символів",
"lowercase": "Включає малу літеру", "lowercase": "Включає малу літеру",
"uppercase": "Включає велику літеру", "uppercase": "Включає велику літеру",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "Mật khẩu", "label": "Mật khẩu",
"suggestion": { "requirement": {
"length": "Bao gồm ít nhất 8 kí tự", "length": "Bao gồm ít nhất 8 kí tự",
"lowercase": "Bao gồm chữ thường", "lowercase": "Bao gồm chữ thường",
"uppercase": "Bao gồm chữ in hoa", "uppercase": "Bao gồm chữ in hoa",
+1 -1
View File
@@ -132,7 +132,7 @@
}, },
"password": { "password": {
"label": "密碼", "label": "密碼",
"suggestion": { "requirement": {
"length": "至少包含 8 個字符", "length": "至少包含 8 個字符",
"lowercase": "包含小寫字母", "lowercase": "包含小寫字母",
"uppercase": "包含大寫字母", "uppercase": "包含大寫字母",
@@ -20,15 +20,15 @@ export const CustomPasswordInput = ({ withPasswordRequirements, ...props }: Cust
}; };
const WithPasswordRequirements = (props: PasswordInputProps) => { const WithPasswordRequirements = (props: PasswordInputProps) => {
const [password, setPassword] = useState(""); const [value, setValue] = useState("");
const onChange = (event: ChangeEvent<HTMLInputElement>) => { const onChange = (event: ChangeEvent<HTMLInputElement>) => {
setPassword(event.currentTarget.value); setValue(event.currentTarget.value);
props.onChange?.(event); props.onChange?.(event);
}; };
return ( return (
<PasswordRequirementsPopover password={password}> <PasswordRequirementsPopover password={value}>
<PasswordInput {...props} onChange={onChange} /> <PasswordInput {...props} onChange={onChange} />
</PasswordRequirementsPopover> </PasswordRequirementsPopover>
); );
@@ -1,19 +1,14 @@
import { rem, Text } from "@mantine/core"; import { rem, Text } from "@mantine/core";
import { IconCheck, IconPoint } from "@tabler/icons-react"; import { IconCheck, IconX } from "@tabler/icons-react";
const getRequirementDisplay = (meets: boolean) =>
({
true: { color: "teal", Icon: IconCheck },
false: { color: "dimmed", Icon: IconPoint },
})[String(meets) as "true" | "false"];
export function PasswordRequirement({ meets, label }: { meets: boolean; label: string }) { export function PasswordRequirement({ meets, label }: { meets: boolean; label: string }) {
const display = getRequirementDisplay(meets);
const Icon = display.Icon;
return ( return (
<Text c={display.color} display="flex" style={{ alignItems: "center" }} size="sm"> <Text c={meets ? "teal" : "red"} display="flex" style={{ alignItems: "center" }} size="sm">
<Icon style={{ width: rem(14), height: rem(14) }} /> {meets ? (
<IconCheck style={{ width: rem(14), height: rem(14) }} />
) : (
<IconX style={{ width: rem(14), height: rem(14) }} />
)}
<Text span ml={10}> <Text span ml={10}>
{label} {label}
</Text> </Text>
@@ -1,24 +1,16 @@
import type { PropsWithChildren } from "react"; import type { PropsWithChildren } from "react";
import { useState } from "react"; import { useState } from "react";
import { Popover, Progress, Text } from "@mantine/core"; import { Popover, Progress } from "@mantine/core";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import { passwordRequirements } from "@homarr/validation/user"; import { passwordRequirements } from "@homarr/validation/user";
import { PasswordRequirement } from "./password-requirement"; import { PasswordRequirement } from "./password-requirement";
const strengthColorMap = [
{ min: 100, color: "teal" },
{ min: 51, color: "yellow" },
{ min: 0, color: "dimmed" },
] as const;
export const PasswordRequirementsPopover = ({ password, children }: PropsWithChildren<{ password: string }>) => { export const PasswordRequirementsPopover = ({ password, children }: PropsWithChildren<{ password: string }>) => {
const tPassword = useScopedI18n("user.field.password");
const requirements = useRequirements(); const requirements = useRequirements();
const strength = useStrength(password); const strength = useStrength(password);
const [popoverOpened, setPopoverOpened] = useState(false); const [popoverOpened, setPopoverOpened] = useState(false);
const strengthColor = strengthColorMap.find(({ min }) => strength >= min)?.color ?? "dimmed";
const checks = ( const checks = (
<> <>
{requirements.map((requirement) => ( {requirements.map((requirement) => (
@@ -27,6 +19,8 @@ export const PasswordRequirementsPopover = ({ password, children }: PropsWithChi
</> </>
); );
const color = strength === 100 ? "teal" : strength > 50 ? "yellow" : "red";
return ( return (
<Popover opened={popoverOpened} position="bottom" width="target" transitionProps={{ transition: "pop" }}> <Popover opened={popoverOpened} position="bottom" width="target" transitionProps={{ transition: "pop" }}>
<Popover.Target> <Popover.Target>
@@ -35,10 +29,7 @@ export const PasswordRequirementsPopover = ({ password, children }: PropsWithChi
</div> </div>
</Popover.Target> </Popover.Target>
<Popover.Dropdown> <Popover.Dropdown>
<Progress color={strengthColor} value={strength} size={5} mb="xs" /> <Progress color={color} value={strength} size={5} mb="xs" />
<Text size="sm" c="dimmed" mb="xs">
{tPassword("suggestionsTitle")}
</Text>
{checks} {checks}
</Popover.Dropdown> </Popover.Dropdown>
</Popover> </Popover>
@@ -46,7 +37,7 @@ export const PasswordRequirementsPopover = ({ password, children }: PropsWithChi
}; };
const useRequirements = () => { const useRequirements = () => {
const t = useScopedI18n("user.field.password.suggestion"); const t = useScopedI18n("user.field.password.requirement");
return passwordRequirements.map(({ check, value }) => ({ check, label: t(value) })); return passwordRequirements.map(({ check, value }) => ({ check, label: t(value) }));
}; };
+16 -2
View File
@@ -19,10 +19,24 @@ export const passwordRequirements = [
{ check: regexCheck(/[$&+,:;=?@#|'<>.^*()%!\-~`"_/\\[\]{}]/), value: "special" }, { check: regexCheck(/[$&+,:;=?@#|'<>.^*()%!\-~`"_/\\[\]{}]/), value: "special" },
] satisfies { ] satisfies {
check: (value: string) => boolean; check: (value: string) => boolean;
value: keyof TranslationObject["user"]["field"]["password"]["suggestion"]; value: keyof TranslationObject["user"]["field"]["password"]["requirement"];
}[]; }[];
export const userPasswordSchema = z.string().min(8).max(255); export const userPasswordSchema = z
.string()
.min(8)
.max(255)
.refine(
(value) => {
return passwordRequirements.every((requirement) => requirement.check(value));
},
{
params: createCustomErrorParams({
key: "passwordRequirements",
params: {},
}),
},
);
const addConfirmPasswordRefinement = < const addConfirmPasswordRefinement = <
TSchema extends z.ZodObject<{ password: z.core.$ZodString; confirmPassword: z.core.$ZodString }, z.core.$strip>, TSchema extends z.ZodObject<{ password: z.core.$ZodString; confirmPassword: z.core.$ZodString }, z.core.$strip>,
+14 -14
View File
@@ -388,8 +388,8 @@ catalogs:
specifier: ^4.5.0 specifier: ^4.5.0
version: 4.5.0 version: 4.5.0
mantine-react-table: mantine-react-table:
specifier: npm:mantine-react-table-open@9.0.3 specifier: 2.0.0-beta.9
version: 9.0.3 version: 2.0.0-beta.9
maria2: maria2:
specifier: ^0.4.1 specifier: ^0.4.1
version: 0.4.1 version: 0.4.1
@@ -819,7 +819,7 @@ importers:
version: 2.20.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.15)(react@19.2.6) version: 2.20.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.15)(react@19.2.6)
mantine-react-table: mantine-react-table:
specifier: 'catalog:' specifier: 'catalog:'
version: mantine-react-table-open@9.0.3(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/dates@9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(@tabler/icons-react@3.44.0(react@19.2.6))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) version: 2.0.0-beta.9(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/dates@9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(@tabler/icons-react@3.44.0(react@19.2.6))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
next: next:
specifier: 'catalog:' specifier: 'catalog:'
version: 16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0) version: 16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0)
@@ -2204,7 +2204,7 @@ importers:
version: 4.3.1 version: 4.3.1
mantine-react-table: mantine-react-table:
specifier: 'catalog:' specifier: 'catalog:'
version: mantine-react-table-open@9.0.3(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/dates@9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(@tabler/icons-react@3.44.0(react@19.2.6))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) version: 2.0.0-beta.9(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/dates@9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(@tabler/icons-react@3.44.0(react@19.2.6))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
next: next:
specifier: 'catalog:' specifier: 'catalog:'
version: 16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0) version: 16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0)
@@ -2259,7 +2259,7 @@ importers:
version: 4.2.0 version: 4.2.0
mantine-react-table: mantine-react-table:
specifier: 'catalog:' specifier: 'catalog:'
version: mantine-react-table-open@9.0.3(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/dates@9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(@tabler/icons-react@3.44.0(react@19.2.6))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) version: 2.0.0-beta.9(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/dates@9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(@tabler/icons-react@3.44.0(react@19.2.6))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
next: next:
specifier: 'catalog:' specifier: 'catalog:'
version: 16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0) version: 16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0)
@@ -2450,7 +2450,7 @@ importers:
version: 1.11.21 version: 1.11.21
mantine-react-table: mantine-react-table:
specifier: 'catalog:' specifier: 'catalog:'
version: mantine-react-table-open@9.0.3(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/dates@9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(@tabler/icons-react@3.44.0(react@19.2.6))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) version: 2.0.0-beta.9(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/dates@9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(@tabler/icons-react@3.44.0(react@19.2.6))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
next: next:
specifier: 'catalog:' specifier: 'catalog:'
version: 16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0) version: 16.2.6(@babel/core@7.29.0)(@playwright/test@1.60.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.100.0)
@@ -7442,18 +7442,18 @@ packages:
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
engines: {node: '>=10'} engines: {node: '>=10'}
mantine-react-table-open@9.0.3: mantine-react-table@2.0.0-beta.9:
resolution: {integrity: sha512-HsrBU4KZluj96FPvvH0wcLzog0m5LIMdwB/59oj6BpftLGEibiLxX00G+tDM06PL2jXuiRxiGKPJ4oQOV+VK8w==} resolution: {integrity: sha512-ZdfcwebWaPERoDvAuk43VYcBCzamohARVclnbuepT0PHZ0wRcDPMBR+zgaocL+pFy8EXUGwvWTOKNh25ITpjNQ==}
engines: {node: '>=16'} engines: {node: '>=16'}
peerDependencies: peerDependencies:
'@mantine/core': ^9.0.0 '@mantine/core': ^7.9
'@mantine/dates': ^9.0.0 '@mantine/dates': ^7.9
'@mantine/hooks': ^9.0.0 '@mantine/hooks': ^7.9
'@tabler/icons-react': '>=2.23.0' '@tabler/icons-react': '>=2.23.0'
clsx: '>=2' clsx: '>=2'
dayjs: '>=1.11' dayjs: '>=1.11'
react: '>=19.2' react: '>=18.0'
react-dom: '>=19.2' react-dom: '>=18.0'
maria2@0.4.1: maria2@0.4.1:
resolution: {integrity: sha512-3lr8UeB+R+BN9c7eOBkeH1GlHf7+0Kzt0pcEgsT9xoXIA5gfXAzjd1niYiYDsvb62v5rLPqCqDiyYxtmmhGJ3g==} resolution: {integrity: sha512-3lr8UeB+R+BN9c7eOBkeH1GlHf7+0Kzt0pcEgsT9xoXIA5gfXAzjd1niYiYDsvb62v5rLPqCqDiyYxtmmhGJ3g==}
@@ -14795,7 +14795,7 @@ snapshots:
dependencies: dependencies:
semver: 7.7.4 semver: 7.7.4
mantine-react-table-open@9.0.3(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/dates@9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(@tabler/icons-react@3.44.0(react@19.2.6))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): mantine-react-table@2.0.0-beta.9(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/dates@9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(@tabler/icons-react@3.44.0(react@19.2.6))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
dependencies: dependencies:
'@mantine/core': 9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@mantine/core': 9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@mantine/dates': 9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@mantine/dates': 9.2.2(@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@9.2.2(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+1 -1
View File
@@ -131,7 +131,7 @@ catalog:
json5: ^2.2.3 json5: ^2.2.3
ldapts: 8.1.8 ldapts: 8.1.8
lodash.clonedeep: ^4.5.0 lodash.clonedeep: ^4.5.0
mantine-react-table: npm:mantine-react-table-open@9.0.3 mantine-react-table: 2.0.0-beta.9
maria2: ^0.4.1 maria2: ^0.4.1
mysql2: 3.22.4 mysql2: 3.22.4
next: 16.2.6 next: 16.2.6