diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json
index 3f40e32f5..3531dcd6f 100644
--- a/apps/nextjs/package.json
+++ b/apps/nextjs/package.json
@@ -16,14 +16,15 @@
"@homarr/api": "workspace:^0.1.0",
"@homarr/auth": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
+ "@homarr/form": "workspace:^0.1.0",
+ "@homarr/notifications": "workspace:^0.1.0",
+ "@homarr/spotlight": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
- "@homarr/notifications": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
- "@homarr/spotlight": "workspace:^0.1.0",
- "@homarr/form": "workspace:^0.1.0",
- "@mantine/hooks": "^7.3.1",
- "@mantine/tiptap": "^7.3.1",
+ "@homarr/widgets": "workspace:^0.1.0",
+ "@mantine/hooks": "^7.3.2",
+ "@mantine/tiptap": "^7.3.2",
"@t3-oss/env-nextjs": "^0.7.1",
"@tanstack/react-query": "^5.8.7",
"@tanstack/react-query-devtools": "^5.8.7",
@@ -36,6 +37,8 @@
"@trpc/react-query": "next",
"@trpc/server": "next",
"dayjs": "^1.11.10",
+ "jotai": "^2.6.0",
+ "mantine-modal-manager": "^7.3.2",
"next": "^14.0.3",
"postcss-preset-mantine": "^1.11.1",
"react": "18.2.0",
diff --git a/apps/nextjs/src/app/[locale]/(main)/layout.tsx b/apps/nextjs/src/app/[locale]/(main)/layout.tsx
new file mode 100644
index 000000000..06e6f91fb
--- /dev/null
+++ b/apps/nextjs/src/app/[locale]/(main)/layout.tsx
@@ -0,0 +1,15 @@
+import type { PropsWithChildren } from "react";
+
+import { AppShellMain } from "@homarr/ui";
+
+import { MainHeader } from "~/components/layout/header";
+import { ClientShell } from "~/components/layout/shell";
+
+export default function MainLayout({ children }: PropsWithChildren) {
+ return (
+
+
+ {children}
+
+ );
+}
diff --git a/apps/nextjs/src/app/[locale]/(main)/page.tsx b/apps/nextjs/src/app/[locale]/(main)/page.tsx
new file mode 100644
index 000000000..884e79fd8
--- /dev/null
+++ b/apps/nextjs/src/app/[locale]/(main)/page.tsx
@@ -0,0 +1,9 @@
+import { Stack, Title } from "@homarr/ui";
+
+export default function HomePage() {
+ return (
+
+ Home
+
+ );
+}
diff --git a/apps/nextjs/src/app/[locale]/_client-providers/modals.tsx b/apps/nextjs/src/app/[locale]/_client-providers/modals.tsx
new file mode 100644
index 000000000..1287f756a
--- /dev/null
+++ b/apps/nextjs/src/app/[locale]/_client-providers/modals.tsx
@@ -0,0 +1,9 @@
+"use client";
+
+import type { PropsWithChildren } from "react";
+
+import { ModalsManager } from "../modals";
+
+export const ModalsProvider = ({ children }: PropsWithChildren) => {
+ return {children};
+};
diff --git a/apps/nextjs/src/app/[locale]/auth/login/page.tsx b/apps/nextjs/src/app/[locale]/auth/login/page.tsx
index a585fc698..ba4d5dd00 100644
--- a/apps/nextjs/src/app/[locale]/auth/login/page.tsx
+++ b/apps/nextjs/src/app/[locale]/auth/login/page.tsx
@@ -10,7 +10,7 @@ export default async function Login() {
return (
-
+
{t("title")}
diff --git a/apps/nextjs/src/app/[locale]/init/user/page.tsx b/apps/nextjs/src/app/[locale]/init/user/page.tsx
index fa27e29da..6c234827a 100644
--- a/apps/nextjs/src/app/[locale]/init/user/page.tsx
+++ b/apps/nextjs/src/app/[locale]/init/user/page.tsx
@@ -23,7 +23,7 @@ export default async function InitUser() {
return (
-
+
{t("title")}
diff --git a/apps/nextjs/src/app/[locale]/layout.tsx b/apps/nextjs/src/app/[locale]/layout.tsx
index 26fc4a6f5..d74b5e917 100644
--- a/apps/nextjs/src/app/[locale]/layout.tsx
+++ b/apps/nextjs/src/app/[locale]/layout.tsx
@@ -3,6 +3,7 @@ import { Inter } from "next/font/google";
import "@homarr/ui/styles.css";
import "@homarr/notifications/styles.css";
+import "@homarr/spotlight/styles.css";
import { headers } from "next/headers";
@@ -13,6 +14,7 @@ import {
uiConfiguration,
} from "@homarr/ui";
+import { ModalsProvider } from "./_client-providers/modals";
import { NextInternationalProvider } from "./_client-providers/next-international";
import { TRPCReactProvider } from "./_client-providers/trpc";
@@ -51,8 +53,10 @@ export default function Layout(props: {
defaultColorScheme={colorScheme}
{...uiConfiguration}
>
-
- {props.children}
+
+
+ {props.children}
+
diff --git a/apps/nextjs/src/app/[locale]/modals.tsx b/apps/nextjs/src/app/[locale]/modals.tsx
new file mode 100644
index 000000000..331305f27
--- /dev/null
+++ b/apps/nextjs/src/app/[locale]/modals.tsx
@@ -0,0 +1,9 @@
+"use client";
+
+import { createModalManager } from "mantine-modal-manager";
+
+import { WidgetEditModal } from "@homarr/widgets";
+
+export const [ModalsManager, modalEvents] = createModalManager({
+ widgetEditModal: WidgetEditModal,
+});
diff --git a/apps/nextjs/src/app/[locale]/page.tsx b/apps/nextjs/src/app/[locale]/page.tsx
deleted file mode 100644
index f818aa1a9..000000000
--- a/apps/nextjs/src/app/[locale]/page.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { auth } from "@homarr/auth";
-import { db } from "@homarr/db";
-import { Button, Stack, Title } from "@homarr/ui";
-
-export default async function HomePage() {
- const currentSession = await auth();
- const users = await db.query.users.findMany();
-
- return (
-
- Home
-
- {JSON.stringify(users)}
- {currentSession && (
-
- Currently logged in as {currentSession.user.name}
-
- )}
-
- );
-}
diff --git a/apps/nextjs/src/app/[locale]/widgets/[sort]/layout.tsx b/apps/nextjs/src/app/[locale]/widgets/[sort]/layout.tsx
new file mode 100644
index 000000000..beae35636
--- /dev/null
+++ b/apps/nextjs/src/app/[locale]/widgets/[sort]/layout.tsx
@@ -0,0 +1,27 @@
+import type { PropsWithChildren } from "react";
+
+import { widgetImports } from "@homarr/widgets";
+
+import { MainNavigation } from "~/components/layout/navigation";
+import { ClientShell } from "~/components/layout/shell";
+
+const getLinks = () => {
+ return Object.entries(widgetImports).map(([key, value]) => {
+ return {
+ href: `/widgets/${key}`,
+ icon: value.definition.icon,
+ label: value.definition.sort,
+ };
+ });
+};
+
+export default function WidgetPreviewLayout({ children }: PropsWithChildren) {
+ const links = getLinks();
+
+ return (
+
+
+ {children}
+
+ );
+}
diff --git a/apps/nextjs/src/app/[locale]/widgets/[sort]/page.tsx b/apps/nextjs/src/app/[locale]/widgets/[sort]/page.tsx
new file mode 100644
index 000000000..95adfab81
--- /dev/null
+++ b/apps/nextjs/src/app/[locale]/widgets/[sort]/page.tsx
@@ -0,0 +1,48 @@
+"use client";
+
+import type { PropsWithChildren } from "react";
+import { useState } from "react";
+import { notFound } from "next/navigation";
+
+import { ActionIcon, Affix, Center, IconPencil } from "@homarr/ui";
+import type { WidgetSort } from "@homarr/widgets";
+import { loadWidgetDynamic, widgetImports } from "@homarr/widgets";
+
+import { modalEvents } from "../../modals";
+
+type Props = PropsWithChildren<{ params: { sort: string } }>;
+
+export default function WidgetPreview(props: Props) {
+ const [options, setOptions] = useState>({});
+ if (!(props.params.sort in widgetImports)) {
+ notFound();
+ }
+
+ const sort = props.params.sort as WidgetSort;
+ const Comp = loadWidgetDynamic(sort);
+
+ return (
+
+
+
+ {
+ return modalEvents.openManagedModal({
+ modal: "widgetEditModal",
+ innerProps: {
+ sort,
+ definition: widgetImports[sort].definition.options,
+ state: [options, setOptions],
+ },
+ });
+ }}
+ >
+
+
+
+
+ );
+}
diff --git a/apps/nextjs/src/components/layout/header.tsx b/apps/nextjs/src/components/layout/header.tsx
new file mode 100644
index 000000000..6a36bcd9f
--- /dev/null
+++ b/apps/nextjs/src/components/layout/header.tsx
@@ -0,0 +1,30 @@
+import Link from "next/link";
+
+import { AppShellHeader, Group, UnstyledButton } from "@homarr/ui";
+
+import { ClientBurger } from "./header/burger";
+import { DesktopSearchInput, MobileSearchButton } from "./header/search";
+import { ClientSpotlight } from "./header/spotlight";
+import { UserButton } from "./header/user";
+import { LogoWithTitle } from "./logo";
+
+export const MainHeader = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/nextjs/src/components/layout/header/burger.tsx b/apps/nextjs/src/components/layout/header/burger.tsx
new file mode 100644
index 000000000..02f3def9d
--- /dev/null
+++ b/apps/nextjs/src/components/layout/header/burger.tsx
@@ -0,0 +1,18 @@
+"use client";
+
+import { useCallback } from "react";
+import { atom, useAtom } from "jotai";
+
+import { Burger } from "@homarr/ui";
+
+export const navigationCollapsedAtom = atom(true);
+
+export const ClientBurger = () => {
+ const [collapsed, setCollapsed] = useAtom(navigationCollapsedAtom);
+
+ const toggle = useCallback(() => setCollapsed((c) => !c), [setCollapsed]);
+
+ return (
+
+ );
+};
diff --git a/apps/nextjs/src/components/layout/header/search.module.css b/apps/nextjs/src/components/layout/header/search.module.css
new file mode 100644
index 000000000..3f9899b23
--- /dev/null
+++ b/apps/nextjs/src/components/layout/header/search.module.css
@@ -0,0 +1,26 @@
+.desktopSearch {
+ @mixin smaller-than $mantine-breakpoint-sm {
+ display: none;
+ }
+
+ > div {
+ --_input-bd-override: var(--_input-bd);
+
+ button:focus-within {
+ border-color: var(--_input-bd-override);
+ }
+
+ button {
+ cursor: pointer;
+ color: var(--mantine-color-placeholder);
+ }
+ }
+
+ cursor: pointer;
+}
+
+.mobileSearch {
+ @mixin larger-than $mantine-breakpoint-sm {
+ display: none;
+ }
+}
diff --git a/apps/nextjs/src/components/layout/header/search.tsx b/apps/nextjs/src/components/layout/header/search.tsx
new file mode 100644
index 000000000..1bb742640
--- /dev/null
+++ b/apps/nextjs/src/components/layout/header/search.tsx
@@ -0,0 +1,37 @@
+"use client";
+
+import { spotlight } from "@homarr/spotlight";
+import { useScopedI18n } from "@homarr/translation/client";
+import { ActionIcon, IconSearch, TextInput, UnstyledButton } from "@homarr/ui";
+
+import classes from "./search.module.css";
+
+export const DesktopSearchInput = () => {
+ const t = useScopedI18n("common.search");
+
+ return (
+ }
+ onClick={spotlight.open}
+ >
+ {t("placeholder")}
+
+ );
+};
+
+export const MobileSearchButton = () => {
+ return (
+
+
+
+ );
+};
diff --git a/apps/nextjs/src/components/layout/header/spotlight.tsx b/apps/nextjs/src/components/layout/header/spotlight.tsx
new file mode 100644
index 000000000..61d2f70b4
--- /dev/null
+++ b/apps/nextjs/src/components/layout/header/spotlight.tsx
@@ -0,0 +1,22 @@
+"use client";
+
+import { Spotlight } from "@homarr/spotlight";
+import { useScopedI18n } from "@homarr/translation/client";
+import { IconSearch } from "@homarr/ui";
+
+export const ClientSpotlight = () => {
+ const t = useScopedI18n("common.search");
+
+ return (
+ ,
+ placeholder: `${t("placeholder")}`,
+ }}
+ yOffset={12}
+ />
+ );
+};
diff --git a/apps/nextjs/src/components/layout/header/user.tsx b/apps/nextjs/src/components/layout/header/user.tsx
new file mode 100644
index 000000000..376b31e3d
--- /dev/null
+++ b/apps/nextjs/src/components/layout/header/user.tsx
@@ -0,0 +1,11 @@
+import { UnstyledButton } from "@homarr/ui";
+
+import { UserAvatar } from "~/components/user-avatar";
+
+export const UserButton = () => {
+ return (
+
+
+
+ );
+};
diff --git a/apps/nextjs/src/components/layout/logo.tsx b/apps/nextjs/src/components/layout/logo.tsx
index 8b6010c99..6a5fe592e 100644
--- a/apps/nextjs/src/components/layout/logo.tsx
+++ b/apps/nextjs/src/components/layout/logo.tsx
@@ -1,5 +1,6 @@
import Image from "next/image";
+import type { TitleOrder } from "@homarr/ui";
import { Group, Title } from "@homarr/ui";
interface LogoProps {
@@ -7,12 +8,26 @@ interface LogoProps {
}
export const Logo = ({ size = 60 }: LogoProps) => (
-
+
);
-export const LogoWithTitle = () => (
-
-
- lparr
-
-);
+const logoWithTitleSizes = {
+ lg: { logoSize: 48, titleOrder: 1 },
+ md: { logoSize: 32, titleOrder: 2 },
+ sm: { logoSize: 24, titleOrder: 3 },
+} satisfies Record;
+
+interface LogoWithTitleProps {
+ size: keyof typeof logoWithTitleSizes;
+}
+
+export const LogoWithTitle = ({ size }: LogoWithTitleProps) => {
+ const { logoSize, titleOrder } = logoWithTitleSizes[size];
+
+ return (
+
+
+ lparr
+
+ );
+};
diff --git a/apps/nextjs/src/components/layout/navigation.tsx b/apps/nextjs/src/components/layout/navigation.tsx
new file mode 100644
index 000000000..95dcc5ebe
--- /dev/null
+++ b/apps/nextjs/src/components/layout/navigation.tsx
@@ -0,0 +1,88 @@
+import Link from "next/link";
+
+import {
+ AppShellNavbar,
+ AppShellSection,
+ NavLink,
+ ScrollArea,
+} from "@homarr/ui";
+import type { TablerIconsProps } from "@homarr/ui";
+
+interface MainNavigationProps {
+ headerSection?: JSX.Element;
+ footerSection?: JSX.Element;
+ links: NavigationLink[];
+}
+
+export const MainNavigation = ({
+ headerSection,
+ footerSection,
+ links,
+}: MainNavigationProps) => {
+ return (
+
+ {headerSection && {headerSection}}
+
+ {links.map((link) => (
+
+ ))}
+
+ {footerSection && {footerSection}}
+
+ );
+};
+
+const CommonNavLink = (props: NavigationLink) =>
+ "href" in props ? (
+
+ ) : (
+
+ );
+
+const NavLinkHref = (props: NavigationLinkHref) =>
+ props.external ? (
+ }
+ href={props.href}
+ target="_blank"
+ />
+ ) : (
+ }
+ href={props.href}
+ />
+ );
+
+const NavLinkWithItems = (props: NavigationLinkWithItems) => (
+ }
+ >
+ {props.items.map((item) => (
+
+ ))}
+
+);
+
+interface CommonNavigationLinkProps {
+ label: string;
+ icon: (props: TablerIconsProps) => JSX.Element;
+}
+
+interface NavigationLinkHref extends CommonNavigationLinkProps {
+ href: string;
+ external?: boolean;
+}
+interface NavigationLinkWithItems extends CommonNavigationLinkProps {
+ items: NavigationLinkHref[];
+}
+export type NavigationLink = NavigationLinkHref | NavigationLinkWithItems;
diff --git a/apps/nextjs/src/components/layout/shell.tsx b/apps/nextjs/src/components/layout/shell.tsx
new file mode 100644
index 000000000..ec9d94cc9
--- /dev/null
+++ b/apps/nextjs/src/components/layout/shell.tsx
@@ -0,0 +1,39 @@
+"use client";
+
+import type { PropsWithChildren } from "react";
+import { useAtomValue } from "jotai";
+
+import { AppShell } from "@homarr/ui";
+
+import { navigationCollapsedAtom } from "./header/burger";
+
+interface ClientShellProps {
+ hasHeader?: boolean;
+ hasNavigation?: boolean;
+}
+
+export const ClientShell = ({
+ hasHeader = true,
+ hasNavigation = true,
+ children,
+}: PropsWithChildren) => {
+ const collapsed = useAtomValue(navigationCollapsedAtom);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/apps/nextjs/src/components/user-avatar.tsx b/apps/nextjs/src/components/user-avatar.tsx
new file mode 100644
index 000000000..0c3e5bd39
--- /dev/null
+++ b/apps/nextjs/src/components/user-avatar.tsx
@@ -0,0 +1,32 @@
+import { auth } from "@homarr/auth";
+import type { AvatarProps, MantineSize } from "@homarr/ui";
+import { Avatar } from "@homarr/ui";
+
+interface UserAvatarProps {
+ size: MantineSize;
+}
+
+export const UserAvatar = async ({ size }: UserAvatarProps) => {
+ const currentSession = await auth();
+
+ const commonProps = {
+ size,
+ color: "primaryColor",
+ } satisfies Partial;
+
+ if (!currentSession) return ;
+ if (currentSession.user.image)
+ return (
+
+ );
+
+ return (
+
+ {currentSession.user.name!.substring(0, 2).toUpperCase()}
+
+ );
+};
diff --git a/packages/spotlight/index.ts b/packages/spotlight/index.ts
index 3bd16e178..542a4b2c7 100644
--- a/packages/spotlight/index.ts
+++ b/packages/spotlight/index.ts
@@ -1 +1,2 @@
export * from "./src";
+export { spotlight, Spotlight } from "@mantine/spotlight";
diff --git a/packages/spotlight/package.json b/packages/spotlight/package.json
index 555eac83e..ddbbc5721 100644
--- a/packages/spotlight/package.json
+++ b/packages/spotlight/package.json
@@ -3,7 +3,8 @@
"private": true,
"version": "0.1.0",
"exports": {
- ".": "./index.ts"
+ ".": "./index.ts",
+ "./styles.css": "./src/styles.css"
},
"typesVersions": {
"*": {
diff --git a/packages/spotlight/src/styles.css b/packages/spotlight/src/styles.css
new file mode 100644
index 000000000..351a1f625
--- /dev/null
+++ b/packages/spotlight/src/styles.css
@@ -0,0 +1 @@
+@import "@mantine/spotlight/styles.css";
diff --git a/packages/translation/src/lang/de.ts b/packages/translation/src/lang/de.ts
index e85c056ce..768899288 100644
--- a/packages/translation/src/lang/de.ts
+++ b/packages/translation/src/lang/de.ts
@@ -26,4 +26,36 @@ export default {
create: "Benutzer erstellen",
},
},
+ widget: {
+ clock: {
+ option: {
+ is24HourFormat: {
+ label: "24-Stunden Format",
+ description: "Use 24-hour format instead of 12-hour format",
+ },
+ isLocaleTime: {
+ label: "Use locale time",
+ },
+ timezone: {
+ label: "Timezone",
+ },
+ },
+ },
+ weather: {
+ option: {
+ location: {
+ label: "Standort",
+ },
+ showCity: {
+ label: "Stadt anzeigen",
+ },
+ },
+ },
+ },
+ common: {
+ search: {
+ placeholder: "Suche nach etwas...",
+ nothingFound: "Nichts gefunden",
+ },
+ },
} as const;
diff --git a/packages/translation/src/lang/en.ts b/packages/translation/src/lang/en.ts
index 6a5b6c3c5..3915282f3 100644
--- a/packages/translation/src/lang/en.ts
+++ b/packages/translation/src/lang/en.ts
@@ -26,4 +26,36 @@ export default {
create: "Create user",
},
},
+ widget: {
+ clock: {
+ option: {
+ is24HourFormat: {
+ label: "24-hour format",
+ description: "Use 24-hour format instead of 12-hour format",
+ },
+ isLocaleTime: {
+ label: "Use locale time",
+ },
+ timezone: {
+ label: "Timezone",
+ },
+ },
+ },
+ weather: {
+ option: {
+ location: {
+ label: "Location",
+ },
+ showCity: {
+ label: "Show city",
+ },
+ },
+ },
+ },
+ common: {
+ search: {
+ placeholder: "Search for anything...",
+ nothingFound: "Nothing found",
+ },
+ },
} as const;
diff --git a/packages/widgets/index.ts b/packages/widgets/index.ts
new file mode 100644
index 000000000..3bd16e178
--- /dev/null
+++ b/packages/widgets/index.ts
@@ -0,0 +1 @@
+export * from "./src";
diff --git a/packages/widgets/package.json b/packages/widgets/package.json
new file mode 100644
index 000000000..f8cf624de
--- /dev/null
+++ b/packages/widgets/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "@homarr/widgets",
+ "private": true,
+ "version": "0.1.0",
+ "exports": {
+ ".": "./index.ts"
+ },
+ "typesVersions": {
+ "*": {
+ "*": [
+ "src/*"
+ ]
+ }
+ },
+ "license": "MIT",
+ "scripts": {
+ "clean": "rm -rf .turbo node_modules",
+ "lint": "eslint .",
+ "format": "prettier --check . --ignore-path ../../.gitignore",
+ "typecheck": "tsc --noEmit"
+ },
+ "devDependencies": {
+ "@homarr/eslint-config": "workspace:^0.2.0",
+ "@homarr/prettier-config": "workspace:^0.1.0",
+ "@homarr/tsconfig": "workspace:^0.1.0",
+ "eslint": "^8.53.0",
+ "typescript": "^5.3.3"
+ },
+ "eslintConfig": {
+ "extends": [
+ "@homarr/eslint-config/base"
+ ]
+ },
+ "prettier": "@homarr/prettier-config",
+ "dependencies": {
+ "@homarr/ui": "workspace:^0.1.0",
+ "@homarr/form": "workspace:^0.1.0",
+ "@homarr/notifications": "workspace:^0.1.0",
+ "@homarr/translation": "workspace:^0.1.0",
+ "@homarr/validation": "workspace:^0.1.0"
+ }
+}
diff --git a/packages/widgets/src/WidgetEditModal.tsx b/packages/widgets/src/WidgetEditModal.tsx
new file mode 100644
index 000000000..faf44a308
--- /dev/null
+++ b/packages/widgets/src/WidgetEditModal.tsx
@@ -0,0 +1,71 @@
+"use client";
+
+import type { Dispatch, SetStateAction } from "react";
+import type { ManagedModal } from "mantine-modal-manager";
+
+import { Button, Group, Stack } from "@homarr/ui";
+
+import type { WidgetSort } from ".";
+import { getInputForType } from "./_inputs";
+import { FormProvider, useForm } from "./_inputs/form";
+import type { WidgetOptionsRecordOf } from "./definition";
+import type { WidgetOptionDefinition } from "./options";
+
+interface ModalProps {
+ sort: TSort;
+ state: [
+ Record,
+ Dispatch>>,
+ ];
+ definition: WidgetOptionsRecordOf;
+}
+
+export const WidgetEditModal: ManagedModal> = ({
+ actions,
+ innerProps,
+}) => {
+ const [value, setValue] = innerProps.state;
+ const form = useForm({
+ initialValues: value,
+ });
+
+ return (
+
+ );
+};
diff --git a/packages/widgets/src/_inputs/common.tsx b/packages/widgets/src/_inputs/common.tsx
new file mode 100644
index 000000000..5027e13d4
--- /dev/null
+++ b/packages/widgets/src/_inputs/common.tsx
@@ -0,0 +1,33 @@
+import { useScopedI18n } from "@homarr/translation/client";
+
+import type { WidgetSort } from "..";
+import type { WidgetOptionOfType, WidgetOptionType } from "../options";
+
+export interface CommonWidgetInputProps {
+ sort: WidgetSort;
+ property: string;
+ options: Omit, "defaultValue" | "type">;
+}
+
+type UseWidgetInputTranslationReturnType = (
+ key: "label" | "description",
+) => string;
+
+/**
+ * Short description why as and unknown convertions are used below:
+ * Typescript was not smart enought to work with the generic of the WidgetSort to only allow properties that are relying within that specified sort.
+ * This does not mean, that the function useWidgetInputTranslation can be called with invalid arguments without type errors and rather means that the above widget..option. string
+ * is not recognized as valid argument for the scoped i18n hook. Because the typesafety should remain outside the usage of those methods I (Meierschlumpf) decided to provide this fully typesafe useWidgetInputTranslation method.
+ *
+ * Some notes about it:
+ * - The label translation can be used for every input, especially considering that all options should have defined a label for themself. The description translation should only be used when withDescription
+ * is defined for the option. The method does sadly not reconize issues with those definitions. So it does not yell at you when you somewhere show the label without having it defined in the translations.
+ */
+export const useWidgetInputTranslation = (
+ sort: WidgetSort,
+ property: string,
+): UseWidgetInputTranslationReturnType => {
+ return useScopedI18n(
+ `widget.${sort}.option.${property}` as never, // Because the type is complex and not recognized by typescript, we need to cast it to never to make it work.
+ ) as unknown as UseWidgetInputTranslationReturnType;
+};
diff --git a/packages/widgets/src/_inputs/form.ts b/packages/widgets/src/_inputs/form.ts
new file mode 100644
index 000000000..7fac3a611
--- /dev/null
+++ b/packages/widgets/src/_inputs/form.ts
@@ -0,0 +1,6 @@
+"use client";
+
+import { createFormContext } from "@homarr/form";
+
+export const [FormProvider, useFormContext, useForm] =
+ createFormContext>();
diff --git a/packages/widgets/src/_inputs/index.ts b/packages/widgets/src/_inputs/index.ts
new file mode 100644
index 000000000..ec9c86958
--- /dev/null
+++ b/packages/widgets/src/_inputs/index.ts
@@ -0,0 +1,24 @@
+import type { WidgetOptionType } from "../options";
+import { WidgetMultiSelectInput } from "./widget-multiselect-input";
+import { WidgetNumberInput } from "./widget-number-input";
+import { WidgetSelectInput } from "./widget-select-input";
+import { WidgetSliderInput } from "./widget-slider-input";
+import { WidgetSwitchInput } from "./widget-switch-input";
+import { WidgetTextInput } from "./widget-text-input";
+
+const mapping = {
+ text: WidgetTextInput,
+ location: () => null,
+ multiSelect: WidgetMultiSelectInput,
+ multiText: () => null,
+ number: WidgetNumberInput,
+ select: WidgetSelectInput,
+ slider: WidgetSliderInput,
+ switch: WidgetSwitchInput,
+} satisfies Record;
+
+export const getInputForType = (
+ type: TType,
+) => {
+ return mapping[type];
+};
diff --git a/packages/widgets/src/_inputs/widget-multiselect-input.tsx b/packages/widgets/src/_inputs/widget-multiselect-input.tsx
new file mode 100644
index 000000000..c26c2782b
--- /dev/null
+++ b/packages/widgets/src/_inputs/widget-multiselect-input.tsx
@@ -0,0 +1,25 @@
+"use client";
+
+import { MultiSelect } from "@homarr/ui";
+
+import type { CommonWidgetInputProps } from "./common";
+import { useWidgetInputTranslation } from "./common";
+import { useFormContext } from "./form";
+
+export const WidgetMultiSelectInput = ({
+ property,
+ sort,
+ options,
+}: CommonWidgetInputProps<"multiSelect">) => {
+ const t = useWidgetInputTranslation(sort, property);
+ const form = useFormContext();
+
+ return (
+
+ );
+};
diff --git a/packages/widgets/src/_inputs/widget-number-input.tsx b/packages/widgets/src/_inputs/widget-number-input.tsx
new file mode 100644
index 000000000..6a09c00a3
--- /dev/null
+++ b/packages/widgets/src/_inputs/widget-number-input.tsx
@@ -0,0 +1,27 @@
+"use client";
+
+import { NumberInput } from "@homarr/ui";
+
+import type { CommonWidgetInputProps } from "./common";
+import { useWidgetInputTranslation } from "./common";
+import { useFormContext } from "./form";
+
+export const WidgetNumberInput = ({
+ property,
+ sort,
+ options,
+}: CommonWidgetInputProps<"number">) => {
+ const t = useWidgetInputTranslation(sort, property);
+ const form = useFormContext();
+
+ return (
+
+ );
+};
diff --git a/packages/widgets/src/_inputs/widget-select-input.tsx b/packages/widgets/src/_inputs/widget-select-input.tsx
new file mode 100644
index 000000000..f1cbfb390
--- /dev/null
+++ b/packages/widgets/src/_inputs/widget-select-input.tsx
@@ -0,0 +1,25 @@
+"use client";
+
+import { Select } from "@homarr/ui";
+
+import type { CommonWidgetInputProps } from "./common";
+import { useWidgetInputTranslation } from "./common";
+import { useFormContext } from "./form";
+
+export const WidgetSelectInput = ({
+ property,
+ sort,
+ options,
+}: CommonWidgetInputProps<"select">) => {
+ const t = useWidgetInputTranslation(sort, property);
+ const form = useFormContext();
+
+ return (
+
+ );
+};
diff --git a/packages/widgets/src/_inputs/widget-slider-input.tsx b/packages/widgets/src/_inputs/widget-slider-input.tsx
new file mode 100644
index 000000000..db82db75a
--- /dev/null
+++ b/packages/widgets/src/_inputs/widget-slider-input.tsx
@@ -0,0 +1,30 @@
+"use client";
+
+import { InputWrapper, Slider } from "@homarr/ui";
+
+import type { CommonWidgetInputProps } from "./common";
+import { useWidgetInputTranslation } from "./common";
+import { useFormContext } from "./form";
+
+export const WidgetSliderInput = ({
+ property,
+ sort,
+ options,
+}: CommonWidgetInputProps<"slider">) => {
+ const t = useWidgetInputTranslation(sort, property);
+ const form = useFormContext();
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/widgets/src/_inputs/widget-switch-input.tsx b/packages/widgets/src/_inputs/widget-switch-input.tsx
new file mode 100644
index 000000000..7f76d4f10
--- /dev/null
+++ b/packages/widgets/src/_inputs/widget-switch-input.tsx
@@ -0,0 +1,24 @@
+"use client";
+
+import { Switch } from "@homarr/ui";
+
+import type { CommonWidgetInputProps } from "./common";
+import { useWidgetInputTranslation } from "./common";
+import { useFormContext } from "./form";
+
+export const WidgetSwitchInput = ({
+ property,
+ sort,
+ options,
+}: CommonWidgetInputProps<"switch">) => {
+ const t = useWidgetInputTranslation(sort, property);
+ const form = useFormContext();
+
+ return (
+
+ );
+};
diff --git a/packages/widgets/src/_inputs/widget-text-input.tsx b/packages/widgets/src/_inputs/widget-text-input.tsx
new file mode 100644
index 000000000..c8deab749
--- /dev/null
+++ b/packages/widgets/src/_inputs/widget-text-input.tsx
@@ -0,0 +1,24 @@
+"use client";
+
+import { TextInput } from "@homarr/ui";
+
+import type { CommonWidgetInputProps } from "./common";
+import { useWidgetInputTranslation } from "./common";
+import { useFormContext } from "./form";
+
+export const WidgetTextInput = ({
+ property,
+ sort: widgetSort,
+ options,
+}: CommonWidgetInputProps<"text">) => {
+ const t = useWidgetInputTranslation(widgetSort, property);
+ const form = useFormContext();
+
+ return (
+
+ );
+};
diff --git a/packages/widgets/src/clock/component.tsx b/packages/widgets/src/clock/component.tsx
new file mode 100644
index 000000000..ff4ae4d79
--- /dev/null
+++ b/packages/widgets/src/clock/component.tsx
@@ -0,0 +1,7 @@
+import type { WidgetComponentProps } from "../definition";
+
+export default function ClockWidget({
+ options,
+}: WidgetComponentProps<"clock">) {
+ return {JSON.stringify(options)};
+}
diff --git a/packages/widgets/src/clock/index.ts b/packages/widgets/src/clock/index.ts
new file mode 100644
index 000000000..428cd492b
--- /dev/null
+++ b/packages/widgets/src/clock/index.ts
@@ -0,0 +1,26 @@
+import { IconClock } from "@homarr/ui";
+
+import { createWidgetDefinition } from "../definition";
+import { opt } from "../options";
+
+export const { definition, componentLoader } = createWidgetDefinition("clock", {
+ icon: IconClock,
+ options: opt.from(
+ (fac) => ({
+ is24HourFormat: fac.switch({
+ defaultValue: true,
+ withDescription: true,
+ }),
+ isLocaleTime: fac.switch({ defaultValue: true }),
+ timezone: fac.select({
+ options: ["Europe/Berlin", "Europe/London", "Europe/Moscow"] as const,
+ defaultValue: "Europe/Berlin",
+ }),
+ }),
+ {
+ timezone: {
+ shouldHide: (options) => options.isLocaleTime,
+ },
+ },
+ ),
+}).withDynamicImport(() => import("./component"));
diff --git a/packages/widgets/src/definition.ts b/packages/widgets/src/definition.ts
new file mode 100644
index 000000000..d0b1eaa0d
--- /dev/null
+++ b/packages/widgets/src/definition.ts
@@ -0,0 +1,40 @@
+import type { LoaderComponent } from "next/dynamic";
+
+import type { TablerIconsProps } from "@homarr/ui";
+
+import type { WidgetImports, WidgetSort } from ".";
+import type {
+ inferOptionsFromDefinition,
+ WidgetOptionsRecord,
+} from "./options";
+
+export const createWidgetDefinition = <
+ TSort extends WidgetSort,
+ TDefinition extends Definition,
+>(
+ sort: TSort,
+ definition: TDefinition,
+) => ({
+ withDynamicImport: (
+ componentLoader: () => LoaderComponent>,
+ ) => ({
+ definition: {
+ sort,
+ ...definition,
+ },
+ componentLoader,
+ }),
+});
+
+interface Definition {
+ icon: (props: TablerIconsProps) => JSX.Element;
+ options: WidgetOptionsRecord;
+}
+
+export interface WidgetComponentProps {
+ options: inferOptionsFromDefinition>;
+ integrations: unknown[];
+}
+
+export type WidgetOptionsRecordOf =
+ WidgetImports[TSort]["definition"]["options"];
diff --git a/packages/widgets/src/import.ts b/packages/widgets/src/import.ts
new file mode 100644
index 000000000..4805784d3
--- /dev/null
+++ b/packages/widgets/src/import.ts
@@ -0,0 +1,5 @@
+import type { WidgetSort } from ".";
+
+export type WidgetImportRecord = {
+ [K in WidgetSort]: unknown;
+};
diff --git a/packages/widgets/src/index.tsx b/packages/widgets/src/index.tsx
new file mode 100644
index 000000000..882ea960f
--- /dev/null
+++ b/packages/widgets/src/index.tsx
@@ -0,0 +1,30 @@
+import dynamic from "next/dynamic";
+import type { Loader } from "next/dynamic";
+
+import { Loader as UiLoader } from "@homarr/ui";
+
+import * as clock from "./clock";
+import type { WidgetComponentProps } from "./definition";
+import type { WidgetImportRecord } from "./import";
+import * as weather from "./weather";
+
+export { WidgetEditModal } from "./WidgetEditModal";
+
+export const widgetSorts = ["clock", "weather"] as const;
+
+export const widgetImports = {
+ clock,
+ weather,
+} satisfies WidgetImportRecord;
+
+export type WidgetSort = (typeof widgetSorts)[number];
+export type WidgetImports = typeof widgetImports;
+export type WidgetImportKey = keyof WidgetImports;
+
+export const loadWidgetDynamic = (sort: TSort) =>
+ dynamic>(
+ widgetImports[sort].componentLoader as Loader>,
+ {
+ loading: () => ,
+ },
+ );
diff --git a/packages/widgets/src/options.ts b/packages/widgets/src/options.ts
new file mode 100644
index 000000000..85c6c7479
--- /dev/null
+++ b/packages/widgets/src/options.ts
@@ -0,0 +1,142 @@
+import type { z } from "@homarr/validation";
+
+interface CommonInput {
+ defaultValue?: TType;
+ withDescription?: boolean;
+}
+
+interface TextInput extends CommonInput {
+ validate: z.ZodType;
+}
+
+interface MultiSelectInput
+ extends CommonInput {
+ options: TOptions;
+}
+
+interface SelectInput
+ extends CommonInput {
+ options: TOptions;
+}
+
+interface NumberInput extends CommonInput {
+ validate: z.ZodNumber;
+ step?: number;
+}
+
+interface SliderInput extends CommonInput {
+ validate: z.ZodNumber;
+ step?: number;
+}
+
+interface OptLocation {
+ name: string;
+ latitude: number;
+ longitude: number;
+}
+
+const optionsFactory = {
+ switch: (input?: CommonInput) => ({
+ type: "switch" as const,
+ defaultValue: input?.defaultValue ?? false,
+ withDescription: input?.withDescription ?? false,
+ }),
+ text: (input?: TextInput) => ({
+ type: "text" as const,
+ defaultValue: input?.defaultValue ?? "",
+ withDescription: input?.withDescription ?? false,
+ validate: input?.validate,
+ }),
+ multiSelect: (
+ input: MultiSelectInput,
+ ) => ({
+ type: "multiSelect" as const,
+ defaultValue: input.defaultValue ?? [],
+ options: input.options,
+ withDescription: input.withDescription ?? false,
+ }),
+ select: (
+ input: SelectInput,
+ ) => ({
+ type: "select" as const,
+ defaultValue: input.defaultValue ?? input.options[0],
+ options: input.options,
+ withDescription: input.withDescription ?? false,
+ }),
+ number: (input: NumberInput) => ({
+ type: "number" as const,
+ defaultValue: input.defaultValue ?? ("" as const),
+ step: input.step,
+ withDescription: input.withDescription ?? false,
+ validate: input.validate,
+ }),
+ slider: (input: SliderInput) => ({
+ type: "slider" as const,
+ defaultValue: input.defaultValue ?? input.validate.minValue ?? 0,
+ step: input.step,
+ withDescription: input.withDescription ?? false,
+ validate: input.validate,
+ }),
+ location: (input?: CommonInput) => ({
+ type: "location" as const,
+ defaultValue: input?.defaultValue ?? {
+ name: "",
+ latitude: 0,
+ longitude: 0,
+ },
+ withDescription: input?.withDescription ?? false,
+ }),
+ multiText: (input?: CommonInput) => ({
+ type: "multiText" as const,
+ defaultValue: input?.defaultValue ?? [],
+ withDescription: input?.withDescription ?? false,
+ }),
+};
+
+type WidgetOptionFactory = typeof optionsFactory;
+export type WidgetOptionDefinition = ReturnType<
+ WidgetOptionFactory[keyof WidgetOptionFactory]
+>;
+export type WidgetOptionsRecord = Record;
+export type WidgetOptionType = WidgetOptionDefinition["type"];
+export type WidgetOptionOfType = Extract<
+ WidgetOptionDefinition,
+ { type: TType }
+>;
+
+type inferOptionFromDefinition =
+ TDefinition["defaultValue"];
+export type inferOptionsFromDefinition = {
+ [key in keyof TOptions]: inferOptionFromDefinition;
+};
+
+interface FieldConfiguration {
+ shouldHide: (options: inferOptionsFromDefinition) => boolean;
+}
+
+type ConfigurationInput = Partial<
+ Record>
+>;
+
+const createOptions = (
+ optionsCallback: (factory: WidgetOptionFactory) => TOptions,
+ configuration?: ConfigurationInput,
+) => {
+ const obj = {} as Record;
+ const options = optionsCallback(optionsFactory);
+
+ for (const key in options) {
+ obj[key] = {
+ ...configuration?.[key],
+ ...options[key],
+ };
+ }
+
+ return obj as {
+ [key in keyof TOptions]: TOptions[key] & FieldConfiguration;
+ };
+};
+
+export const opt = {
+ from: createOptions,
+};
diff --git a/packages/widgets/src/weather/component.tsx b/packages/widgets/src/weather/component.tsx
new file mode 100644
index 000000000..999805d85
--- /dev/null
+++ b/packages/widgets/src/weather/component.tsx
@@ -0,0 +1,7 @@
+import type { WidgetComponentProps } from "../definition";
+
+export default function WeatherWidget({
+ options,
+}: WidgetComponentProps<"weather">) {
+ return {JSON.stringify(options)};
+}
diff --git a/packages/widgets/src/weather/index.ts b/packages/widgets/src/weather/index.ts
new file mode 100644
index 000000000..39fbfcdb7
--- /dev/null
+++ b/packages/widgets/src/weather/index.ts
@@ -0,0 +1,15 @@
+import { IconCloud } from "@homarr/ui";
+
+import { createWidgetDefinition } from "../definition";
+import { opt } from "../options";
+
+export const { definition, componentLoader } = createWidgetDefinition(
+ "weather",
+ {
+ icon: IconCloud,
+ options: opt.from((fac) => ({
+ location: fac.location(),
+ showCity: fac.switch(),
+ })),
+ },
+).withDynamicImport(() => import("./component"));
diff --git a/packages/widgets/tsconfig.json b/packages/widgets/tsconfig.json
new file mode 100644
index 000000000..cbe8483d9
--- /dev/null
+++ b/packages/widgets/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "@homarr/tsconfig/base.json",
+ "compilerOptions": {
+ "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
+ },
+ "include": ["*.ts", "src"],
+ "exclude": ["node_modules"]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c4a4a4d62..5fa4c4189 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -56,12 +56,15 @@ importers:
'@homarr/validation':
specifier: workspace:^0.1.0
version: link:../../packages/validation
+ '@homarr/widgets':
+ specifier: workspace:^0.1.0
+ version: link:../../packages/widgets
'@mantine/hooks':
- specifier: ^7.3.1
- version: 7.3.1(react@18.2.0)
+ specifier: ^7.3.2
+ version: 7.3.2(react@18.2.0)
'@mantine/tiptap':
- specifier: ^7.3.1
- version: 7.3.1(@mantine/core@7.3.1)(@mantine/hooks@7.3.1)(@tabler/icons-react@2.42.0)(@tiptap/extension-link@2.1.13)(react-dom@18.2.0)(react@18.2.0)
+ specifier: ^7.3.2
+ version: 7.3.2(@mantine/core@7.3.2)(@mantine/hooks@7.3.2)(@tabler/icons-react@2.42.0)(@tiptap/extension-link@2.1.13)(react-dom@18.2.0)(react@18.2.0)
'@t3-oss/env-nextjs':
specifier: ^0.7.1
version: 0.7.1(typescript@5.3.3)(zod@3.22.4)
@@ -98,6 +101,12 @@ importers:
dayjs:
specifier: ^1.11.10
version: 1.11.10
+ jotai:
+ specifier: ^2.6.0
+ version: 2.6.0(@types/react@18.2.42)(react@18.2.0)
+ mantine-modal-manager:
+ specifier: ^7.3.2
+ version: 7.3.2(@mantine/core@7.3.2)(@mantine/hooks@7.3.2)(react-dom@18.2.0)(react@18.2.0)
next:
specifier: ^14.0.3
version: 14.0.3(react-dom@18.2.0)(react@18.2.0)
@@ -381,7 +390,7 @@ importers:
dependencies:
'@mantine/core':
specifier: ^7.3.2
- version: 7.3.2(@mantine/hooks@7.3.2)(react-dom@18.2.0)(react@18.2.0)
+ version: 7.3.2(@mantine/hooks@7.3.2)(@types/react@18.2.42)(react-dom@18.2.0)(react@18.2.0)
'@mantine/dates':
specifier: 7.3.2
version: 7.3.2(@mantine/core@7.3.2)(@mantine/hooks@7.3.2)(dayjs@1.11.10)(react-dom@18.2.0)(react@18.2.0)
@@ -427,6 +436,40 @@ importers:
specifier: ^5.3.3
version: 5.3.3
+ packages/widgets:
+ dependencies:
+ '@homarr/form':
+ specifier: workspace:^0.1.0
+ version: link:../form
+ '@homarr/notifications':
+ specifier: workspace:^0.1.0
+ version: link:../notifications
+ '@homarr/translation':
+ specifier: workspace:^0.1.0
+ version: link:../translation
+ '@homarr/ui':
+ specifier: workspace:^0.1.0
+ version: link:../ui
+ '@homarr/validation':
+ specifier: workspace:^0.1.0
+ version: link:../validation
+ devDependencies:
+ '@homarr/eslint-config':
+ specifier: workspace:^0.2.0
+ version: link:../../tooling/eslint
+ '@homarr/prettier-config':
+ specifier: workspace:^0.1.0
+ version: link:../../tooling/prettier
+ '@homarr/tsconfig':
+ specifier: workspace:^0.1.0
+ version: link:../../tooling/typescript
+ eslint:
+ specifier: ^8.53.0
+ version: 8.53.0
+ typescript:
+ specifier: ^5.3.3
+ version: 5.3.3
+
tooling/eslint:
dependencies:
'@next/eslint-plugin-next':
@@ -1383,27 +1426,7 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.15
dev: true
- /@mantine/core@7.3.1(@mantine/hooks@7.3.1)(@types/react@18.2.42)(react-dom@18.2.0)(react@18.2.0):
- resolution: {integrity: sha512-WIesapVzK1ERFcALuosaEPuODN/k/FGkryf2d12R7vsDmxmWqX6yNzPUoJDy6j20ueAkyyg4beJZ4PuZfCRW9Q==}
- peerDependencies:
- '@mantine/hooks': 7.3.1
- react: ^18.2.0
- react-dom: ^18.2.0
- dependencies:
- '@floating-ui/react': 0.24.8(react-dom@18.2.0)(react@18.2.0)
- '@mantine/hooks': 7.3.1(react@18.2.0)
- clsx: 2.0.0
- react: 18.2.0
- react-dom: 18.2.0(react@18.2.0)
- react-number-format: 5.3.1(react-dom@18.2.0)(react@18.2.0)
- react-remove-scroll: 2.5.7(@types/react@18.2.42)(react@18.2.0)
- react-textarea-autosize: 8.5.3(@types/react@18.2.42)(react@18.2.0)
- type-fest: 3.13.1
- transitivePeerDependencies:
- - '@types/react'
- dev: false
-
- /@mantine/core@7.3.2(@mantine/hooks@7.3.2)(react-dom@18.2.0)(react@18.2.0):
+ /@mantine/core@7.3.2(@mantine/hooks@7.3.2)(@types/react@18.2.42)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-CwAuQogVLcLR7O9e1eOgi3gtk4XX6cnaqevAxzJJpIOIyCnHiQ3cEGINVXyUUjUUipBlvK3sqz3NPGJ2ekLFDQ==}
peerDependencies:
'@mantine/hooks': 7.3.2
@@ -1432,7 +1455,7 @@ packages:
react: ^18.2.0
react-dom: ^18.2.0
dependencies:
- '@mantine/core': 7.3.2(@mantine/hooks@7.3.2)(react-dom@18.2.0)(react@18.2.0)
+ '@mantine/core': 7.3.2(@mantine/hooks@7.3.2)(@types/react@18.2.42)(react-dom@18.2.0)(react@18.2.0)
'@mantine/hooks': 7.3.2(react@18.2.0)
clsx: 2.0.0
dayjs: 1.11.10
@@ -1450,14 +1473,6 @@ packages:
react: 18.2.0
dev: false
- /@mantine/hooks@7.3.1(react@18.2.0):
- resolution: {integrity: sha512-pbbqPpVou/13xbt/dYYNphPpbDE2XfPN9mUHBoGZgv9FM8IkziNMIOo4PtNlqqqYsyp1lfQIQVKKT+DLZt1C8Q==}
- peerDependencies:
- react: ^18.2.0
- dependencies:
- react: 18.2.0
- dev: false
-
/@mantine/hooks@7.3.2(react@18.2.0):
resolution: {integrity: sha512-xgumuuI3PBWXff5N02HCI7PEy25mDEdyXDQklUYK93J6FKwpcosyZnGVitoUrV1gLtYYa9ZudeAWdhHuh/CpOg==}
peerDependencies:
@@ -1474,7 +1489,7 @@ packages:
react: ^18.2.0
react-dom: ^18.2.0
dependencies:
- '@mantine/core': 7.3.2(@mantine/hooks@7.3.2)(react-dom@18.2.0)(react@18.2.0)
+ '@mantine/core': 7.3.2(@mantine/hooks@7.3.2)(@types/react@18.2.42)(react-dom@18.2.0)(react@18.2.0)
'@mantine/hooks': 7.3.2(react@18.2.0)
'@mantine/store': 7.3.2(react@18.2.0)
react: 18.2.0
@@ -1490,7 +1505,7 @@ packages:
react: ^18.2.0
react-dom: ^18.2.0
dependencies:
- '@mantine/core': 7.3.2(@mantine/hooks@7.3.2)(react-dom@18.2.0)(react@18.2.0)
+ '@mantine/core': 7.3.2(@mantine/hooks@7.3.2)(@types/react@18.2.42)(react-dom@18.2.0)(react@18.2.0)
'@mantine/hooks': 7.3.2(react@18.2.0)
'@mantine/store': 7.3.2(react@18.2.0)
react: 18.2.0
@@ -1505,18 +1520,18 @@ packages:
react: 18.2.0
dev: false
- /@mantine/tiptap@7.3.1(@mantine/core@7.3.1)(@mantine/hooks@7.3.1)(@tabler/icons-react@2.42.0)(@tiptap/extension-link@2.1.13)(react-dom@18.2.0)(react@18.2.0):
- resolution: {integrity: sha512-4ISRkyyo3IcK+G/Fwm+rpX0KaYAF2b0u/HRbx9K0SQkDekhTrJvFqMIbd1wBfeSrEFCxcy42gYCGZ+3tihNxew==}
+ /@mantine/tiptap@7.3.2(@mantine/core@7.3.2)(@mantine/hooks@7.3.2)(@tabler/icons-react@2.42.0)(@tiptap/extension-link@2.1.13)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-bJF924LhFL0z4bqUSGRTBd9ZR4fJFJObTPwlzJQaY9s8cB9rJaWIBSHp6OmdSvqS5KNpDFfB4tardSKOQleBcw==}
peerDependencies:
- '@mantine/core': 7.3.1
- '@mantine/hooks': 7.3.1
+ '@mantine/core': 7.3.2
+ '@mantine/hooks': 7.3.2
'@tabler/icons-react': '>=2.0.0'
'@tiptap/extension-link': ^2.1.12
react: ^18.2.0
react-dom: ^18.2.0
dependencies:
- '@mantine/core': 7.3.1(@mantine/hooks@7.3.1)(@types/react@18.2.42)(react-dom@18.2.0)(react@18.2.0)
- '@mantine/hooks': 7.3.1(react@18.2.0)
+ '@mantine/core': 7.3.2(@mantine/hooks@7.3.2)(@types/react@18.2.42)(react-dom@18.2.0)(react@18.2.0)
+ '@mantine/hooks': 7.3.2(react@18.2.0)
'@tabler/icons-react': 2.42.0(react@18.2.0)
'@tiptap/extension-link': 2.1.13(@tiptap/core@2.1.13)(@tiptap/pm@2.1.13)
react: 18.2.0
@@ -4788,6 +4803,22 @@ packages:
resolution: {integrity: sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==}
dev: false
+ /jotai@2.6.0(@types/react@18.2.42)(react@18.2.0):
+ resolution: {integrity: sha512-Vt6hsc04Km4j03l+Ax+Sc+FVft5cRJhqgxt6GTz6GM2eM3DyX3CdBdzcG0z2FrlZToL1/0OAkqDghIyARWnSuQ==}
+ engines: {node: '>=12.20.0'}
+ peerDependencies:
+ '@types/react': '>=17.0.0'
+ react: '>=17.0.0'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ react:
+ optional: true
+ dependencies:
+ '@types/react': 18.2.42
+ react: 18.2.0
+ dev: false
+
/js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
dev: false
@@ -4990,6 +5021,20 @@ packages:
/make-error@1.3.6:
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
+ /mantine-modal-manager@7.3.2(@mantine/core@7.3.2)(@mantine/hooks@7.3.2)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-rHt53WBYPB1b+ZlhoYYH3iwG4cZ/UuQd/nVgo+k2f/dbNQskNcRW6ZJiJevcKqndXj/ut2qkgxtppKpUD9eWgg==}
+ peerDependencies:
+ '@mantine/core': 7.3.2
+ '@mantine/hooks': 7.3.2
+ react: ^18.2.0
+ react-dom: ^18.2.0
+ dependencies:
+ '@mantine/core': 7.3.2(@mantine/hooks@7.3.2)(@types/react@18.2.42)(react-dom@18.2.0)(react@18.2.0)
+ '@mantine/hooks': 7.3.2(react@18.2.0)
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ dev: false
+
/markdown-it@13.0.2:
resolution: {integrity: sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==}
hasBin: true