This commit is contained in:
Manuel
2024-02-17 16:22:14 +01:00
43 changed files with 830 additions and 309 deletions

View File

@@ -19,6 +19,7 @@
"@homarr/db": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/form": "workspace:^0.1.0",
"@homarr/gridstack": "^1.0.0",
"@homarr/notifications": "workspace:^0.1.0",
"@homarr/spotlight": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
@@ -29,9 +30,9 @@
"@mantine/modals": "^7.5.3",
"@mantine/tiptap": "^7.5.3",
"@t3-oss/env-nextjs": "^0.9.2",
"@tanstack/react-query": "^5.20.5",
"@tanstack/react-query-devtools": "^5.21.0",
"@tanstack/react-query-next-experimental": "5.20.5",
"@tanstack/react-query": "^5.21.2",
"@tanstack/react-query-devtools": "^5.21.3",
"@tanstack/react-query-next-experimental": "5.21.2",
"@tiptap/extension-link": "^2.2.3",
"@tiptap/react": "^2.2.3",
"@tiptap/starter-kit": "^2.2.3",
@@ -40,22 +41,22 @@
"@trpc/react-query": "next",
"@trpc/server": "next",
"dayjs": "^1.11.10",
"@homarr/gridstack": "^1.0.0",
"jotai": "^2.6.4",
"mantine-modal-manager": "^7.5.2",
"next": "^14.1.0",
"next": "^14.1.1-canary.58",
"postcss-preset-mantine": "^1.13.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"sass": "^1.71.0",
"superjson": "2.2.1"
"superjson": "2.2.1",
"use-deep-compare-effect": "^1.8.1",
"sass": "^1.71.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^20.11.19",
"@types/react": "^18.2.55",
"@types/react": "^18.2.56",
"@types/react-dom": "^18.2.19",
"dotenv-cli": "^7.3.0",
"eslint": "^8.56.0",

View File

@@ -14,7 +14,7 @@ export default async function EditIntegrationPage({
params,
}: EditIntegrationPageProps) {
const t = await getScopedI18n("integration.page.edit");
const integration = await api.integration.byId.query({ id: params.id });
const integration = await api.integration.byId({ id: params.id });
return (
<Container>

View File

@@ -47,7 +47,7 @@ interface IntegrationsPageProps {
export default async function IntegrationsPage({
searchParams,
}: IntegrationsPageProps) {
const integrations = await api.integration.all.query();
const integrations = await api.integration.all();
const t = await getScopedI18n("integration");
return (

View File

@@ -0,0 +1,8 @@
"use client";
import type { PropsWithChildren } from "react";
import { Provider } from "jotai";
export const JotaiProvider = ({ children }: PropsWithChildren) => {
return <Provider>{children}</Provider>;
};

View File

@@ -1,5 +1,6 @@
"use client";
import type { PropsWithChildren } from "react";
import { useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
@@ -9,19 +10,7 @@ import superjson from "superjson";
import { clientApi } from "@homarr/api/client";
import { env } from "~/env.mjs";
const getBaseUrl = () => {
if (typeof window !== "undefined") return ""; // browser should use relative url
if (env.VERCEL_URL) return env.VERCEL_URL; // SSR should use vercel url
return `http://localhost:${env.PORT}`; // dev SSR should use localhost
};
export function TRPCReactProvider(props: {
children: React.ReactNode;
headers?: Headers;
}) {
export function TRPCReactProvider(props: PropsWithChildren) {
const [queryClient] = useState(
() =>
new QueryClient({
@@ -35,7 +24,6 @@ export function TRPCReactProvider(props: {
const [trpcClient] = useState(() =>
clientApi.createClient({
transformer: superjson,
links: [
loggerLink({
enabled: (opts) =>
@@ -43,11 +31,12 @@ export function TRPCReactProvider(props: {
(opts.direction === "down" && opts.result instanceof Error),
}),
unstable_httpBatchStreamLink({
url: `${getBaseUrl()}/api/trpc`,
transformer: superjson,
url: getBaseUrl() + "/api/trpc",
headers() {
const headers = new Map(props.headers);
const headers = new Headers();
headers.set("x-trpc-source", "nextjs-react");
return Object.fromEntries(headers);
return headers;
},
}),
],
@@ -65,3 +54,9 @@ export function TRPCReactProvider(props: {
</clientApi.Provider>
);
}
function getBaseUrl() {
if (typeof window !== "undefined") return window.location.origin;
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
return `http://localhost:${process.env.PORT ?? 3000}`;
}

View File

@@ -3,6 +3,6 @@ import { createBoardPage } from "../_creator";
export default createBoardPage<{ locale: string }>({
async getInitialBoard() {
return await api.board.default.query();
return await api.board.default();
},
});

View File

@@ -3,6 +3,6 @@ import { createBoardPage } from "../_creator";
export default createBoardPage<{ locale: string; name: string }>({
async getInitialBoard({ name }) {
return await api.board.byName.query({ name });
return await api.board.byName({ name });
},
});

View File

@@ -28,7 +28,7 @@ interface Props {
}
export default async function BoardSettingsPage({ params }: Props) {
const board = await api.board.byName.query({ name: params.name });
const board = await api.board.byName({ name: params.name });
const t = await getScopedI18n("board.setting");
return (

View File

@@ -0,0 +1,18 @@
import React from "react";
type PropsWithChildren = Required<React.PropsWithChildren>;
export const composeWrappers = (
wrappers: React.FunctionComponent<PropsWithChildren>[],
): React.FunctionComponent<PropsWithChildren> => {
return wrappers
.reverse()
.reduce((Acc, Current): React.FunctionComponent<PropsWithChildren> => {
// eslint-disable-next-line react/display-name
return (props) => (
<Current>
<Acc {...props} />
</Current>
);
});
};

View File

@@ -30,7 +30,6 @@ export const InitUserForm = () => {
});
const handleSubmit = async (values: FormType) => {
console.log(values);
await mutateAsync(values, {
onSuccess: () => {
showSuccessNotification({

View File

@@ -1,11 +1,9 @@
import type { Metadata } from "next";
import type { Metadata, Viewport } from "next";
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";
import "@homarr/ui/styles.css";
import { Notifications } from "@homarr/notifications";
import {
@@ -14,25 +12,39 @@ import {
uiConfiguration,
} from "@homarr/ui";
import { JotaiProvider } from "./_client-providers/jotai";
import { ModalsProvider } from "./_client-providers/modals";
import { NextInternationalProvider } from "./_client-providers/next-international";
import { TRPCReactProvider } from "./_client-providers/trpc";
import { composeWrappers } from "./compose";
const fontSans = Inter({
subsets: ["latin"],
variable: "--font-sans",
});
/**
* Since we're passing `headers()` to the `TRPCReactProvider` we need to
* make the entire app dynamic. You can move the `TRPCReactProvider` further
* down the tree (e.g. /dashboard and onwards) to make part of the app statically rendered.
*/
export const dynamic = "force-dynamic";
export const metadata: Metadata = {
metadataBase: new URL("http://localhost:3000"),
title: "Create T3 Turbo",
description: "Simple monorepo with shared backend for web & mobile apps",
openGraph: {
title: "Create T3 Turbo",
description: "Simple monorepo with shared backend for web & mobile apps",
url: "https://create-t3-turbo.vercel.app",
siteName: "Create T3 Turbo",
},
twitter: {
card: "summary_large_image",
site: "@jullerino",
creator: "@jullerino",
},
};
export const viewport: Viewport = {
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "white" },
{ media: "(prefers-color-scheme: dark)", color: "black" },
],
};
export default function Layout(props: {
@@ -41,25 +53,32 @@ export default function Layout(props: {
}) {
const colorScheme = "dark";
const StackedProvider = composeWrappers([
(innerProps) => <JotaiProvider {...innerProps} />,
(innerProps) => <TRPCReactProvider {...innerProps} />,
(innerProps) => (
<NextInternationalProvider {...innerProps} locale={props.params.locale} />
),
(innerProps) => (
<MantineProvider
{...innerProps}
defaultColorScheme={colorScheme}
{...uiConfiguration}
/>
),
(innerProps) => <ModalsProvider {...innerProps} />,
]);
return (
<html lang="en">
<html lang="en" suppressHydrationWarning>
<head>
<ColorSchemeScript defaultColorScheme={colorScheme} />
</head>
<body className={["font-sans", fontSans.variable].join(" ")}>
<TRPCReactProvider headers={headers()}>
<NextInternationalProvider locale={props.params.locale}>
<MantineProvider
defaultColorScheme={colorScheme}
{...uiConfiguration}
>
<ModalsProvider>
<Notifications />
{props.children}
</ModalsProvider>
</MantineProvider>
</NextInternationalProvider>
</TRPCReactProvider>
<StackedProvider>
<Notifications />
{props.children}
</StackedProvider>
</body>
</html>
);

View File

@@ -10,7 +10,7 @@ import { DeleteBoardButton } from "./_components/delete-board-button";
export default async function ManageBoardsPage() {
const t = await getScopedI18n("management.page.board");
const boards = await api.board.getAll.query();
const boards = await api.board.getAll();
return (
<>

View File

@@ -28,7 +28,7 @@ const handler = auth(async (req) => {
router: appRouter,
req,
createContext: () =>
createTRPCContext({ auth: req.auth, headers: req.headers }),
createTRPCContext({ session: req.auth, headers: req.headers }),
onError({ error, path }) {
console.error(`>>> tRPC Error on '${path}'`, error);
},

View File

@@ -34,26 +34,28 @@ interface Props {
export const SectionContent = ({ items, refs }: Props) => {
return (
<>
{items.map((item) => (
<div
key={item.id}
className="grid-stack-item"
data-id={item.id}
gs-x={item.xOffset}
gs-y={item.yOffset}
gs-w={item.width}
gs-h={item.height}
gs-min-w={1}
gs-min-h={1}
gs-max-w={4}
gs-max-h={4}
ref={refs.items.current[item.id] as RefObject<HTMLDivElement>}
>
<Card className="grid-stack-item-content" withBorder>
<BoardItem item={item} />
</Card>
</div>
))}
{items.map((item) => {
return (
<div
key={item.id}
className="grid-stack-item"
data-id={item.id}
gs-x={item.xOffset}
gs-y={item.yOffset}
gs-w={item.width}
gs-h={item.height}
gs-min-w={1}
gs-min-h={1}
gs-max-w={4}
gs-max-h={4}
ref={refs.items.current[item.id] as RefObject<HTMLDivElement>}
>
<Card className="grid-stack-item-content" withBorder>
<BoardItem item={item} />
</Card>
</div>
);
})}
</>
);
};

View File

@@ -107,7 +107,7 @@ export const useGridstack = ({
// Add listener for moving items in config from one wrapper to another
currentGrid?.on("added", (_, nodes) => {
nodes.forEach((node) => onAdd(node));
nodes.forEach(onAdd);
});
return () => {
@@ -192,8 +192,6 @@ const useCssVariableConfiguration = ({
const widgetWidth = mainRef.current.clientWidth / sectionColumnCount;
// widget width is used to define sizes of gridstack items within global.scss
root?.style.setProperty("--gridstack-widget-width", widgetWidth.toString());
console.log("widgetWidth", widgetWidth);
console.log(gridRef.current);
gridRef.current?.cellHeight(widgetWidth);
// gridRef.current is required otherwise the cellheight is run on production as undefined
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@@ -1,11 +1,11 @@
import type { ReactNode } from "react";
import Link from "next/link";
import { Spotlight } from "@homarr/spotlight";
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 { HomarrLogoWithTitle } from "./logo/homarr-logo";
@@ -38,7 +38,7 @@ export const MainHeader = ({ logo, actions, hasNavigation = true }: Props) => {
<UserButton />
</Group>
</Group>
<ClientSpotlight />
<Spotlight />
</AppShellHeader>
);
};

View File

@@ -1,6 +1,6 @@
"use client";
import { spotlight } from "@homarr/spotlight";
import { openSpotlight } from "@homarr/spotlight";
import { useScopedI18n } from "@homarr/translation/client";
import { IconSearch, TextInput, UnstyledButton } from "@homarr/ui";
@@ -17,7 +17,7 @@ export const DesktopSearchInput = () => {
w={400}
size="sm"
leftSection={<IconSearch size={20} stroke={1.5} />}
onClick={spotlight.open}
onClick={openSpotlight}
>
{t("placeholder")}
</TextInput>
@@ -26,7 +26,7 @@ export const DesktopSearchInput = () => {
export const MobileSearchButton = () => {
return (
<HeaderButton onClick={spotlight.open} className={classes.mobileSearch}>
<HeaderButton onClick={openSpotlight} className={classes.mobileSearch}>
<IconSearch size={20} stroke={1.5} />
</HeaderButton>
);

View File

@@ -1,22 +0,0 @@
"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 (
<Spotlight
actions={[]}
nothingFound={t("nothingFound")}
highlightQuery
searchProps={{
leftSection: <IconSearch size={20} stroke={1.5} />,
placeholder: `${t("placeholder")}`,
}}
yOffset={12}
/>
);
};

View File

@@ -1,12 +1,7 @@
import { cache } from "react";
import { headers } from "next/headers";
import { createTRPCClient, loggerLink, TRPCClientError } from "@trpc/client";
import { callProcedure } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import type { TRPCErrorResponse } from "@trpc/server/rpc";
import SuperJSON from "superjson";
import { appRouter, createTRPCContext } from "@homarr/api";
import { createCaller, createTRPCContext } from "@homarr/api";
import { auth } from "@homarr/auth";
/**
@@ -18,44 +13,9 @@ const createContext = cache(async () => {
heads.set("x-trpc-source", "rsc");
return createTRPCContext({
auth: await auth(),
session: await auth(),
headers: heads,
});
});
export const api = createTRPCClient<typeof appRouter>({
transformer: SuperJSON,
links: [
loggerLink({
enabled: (op) =>
process.env.NODE_ENV === "development" ||
(op.direction === "down" && op.result instanceof Error),
}),
/**
* Custom RSC link that invokes procedures directly in the server component Don't be too afraid
* about the complexity here, it's just wrapping `callProcedure` with an observable to make it a
* valid ending link for tRPC.
*/
() =>
({ op }) =>
observable((observer) => {
createContext()
.then((ctx) => {
return callProcedure({
procedures: appRouter._def.procedures,
path: op.path,
getRawInput: () => Promise.resolve(op.input),
ctx,
type: op.type,
});
})
.then((data) => {
observer.next({ result: { data } });
observer.complete();
})
.catch((cause: TRPCErrorResponse) => {
observer.error(TRPCClientError.from(cause));
});
}),
],
});
export const api = createCaller(createContext);

View File

@@ -1,17 +0,0 @@
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
import type { AppRouter } from "./src/root";
export { appRouter, type AppRouter } from "./src/root";
export { createTRPCContext } from "./src/trpc";
/**
* Inference helpers for input types
* @example type HelloInput = RouterInputs['example']['hello']
**/
export type RouterInputs = inferRouterInputs<AppRouter>;
/**
* Inference helpers for output types
* @example type HelloOutput = RouterOutputs['example']['hello']
**/
export type RouterOutputs = inferRouterOutputs<AppRouter>;

View File

@@ -2,7 +2,7 @@
"name": "@homarr/api",
"version": "0.1.0",
"exports": {
".": "./index.ts",
".": "./src/index.ts",
"./client": "./src/client.ts"
},
"private": true,

View File

@@ -1,5 +1,5 @@
import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "..";
import type { AppRouter } from ".";
export const clientApi = createTRPCReact<AppRouter>();

33
packages/api/src/index.ts Normal file
View File

@@ -0,0 +1,33 @@
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
import type { AppRouter } from "./root";
import { appRouter } from "./root";
import { createCallerFactory, createTRPCContext } from "./trpc";
/**
* Create a server-side caller for the tRPC API
* @example
* const trpc = createCaller(createContext);
* const res = await trpc.post.all();
* ^? Post[]
*/
const createCaller = createCallerFactory(appRouter);
/**
* Inference helpers for input types
* @example
* type PostByIdInput = RouterInputs['post']['byId']
* ^? { id: number }
**/
type RouterInputs = inferRouterInputs<AppRouter>;
/**
* Inference helpers for output types
* @example
* type AllPostsOutput = RouterOutputs['post']['all']
* ^? Post[]
**/
type RouterOutputs = inferRouterOutputs<AppRouter>;
export { createTRPCContext, appRouter, createCaller };
export type { AppRouter, RouterInputs, RouterOutputs };

View File

@@ -13,7 +13,7 @@ import {
} from "@homarr/db/schema/sqlite";
import { createDb } from "@homarr/db/test";
import type { RouterOutputs } from "../../..";
import type { RouterOutputs } from "../..";
import { boardRouter } from "../board";
// Mock the auth module to return an empty session

View File

@@ -5,7 +5,7 @@ import { createId } from "@homarr/db";
import { integrations, integrationSecrets } from "@homarr/db/schema/sqlite";
import { createDb } from "@homarr/db/test";
import type { RouterInputs } from "../../..";
import type { RouterInputs } from "../..";
import { encryptSecret, integrationRouter } from "../integration";
import { expectToBeDefined } from "./board.spec";

View File

@@ -17,49 +17,28 @@ import { ZodError } from "@homarr/validation";
/**
* 1. CONTEXT
*
* This section defines the "contexts" that are available in the backend API
* This section defines the "contexts" that are available in the backend API.
*
* These allow you to access things like the database, the session, etc, when
* processing a request
* These allow you to access things when processing a request, like the database, the session, etc.
*
*/
interface CreateContextOptions {
session: Session | null;
}
/**
* This helper generates the "internals" for a tRPC context. If you need to use
* it, you can export it from here
* This helper generates the "internals" for a tRPC context. The API handler and RSC clients each
* wrap this and provides the required context.
*
* Examples of things you may need it for:
* - testing, so we dont have to mock Next.js' req/res
* - trpc's `createSSGHelpers` where we don't have req/res
* @see https://create.t3.gg/en/usage/trpc#-servertrpccontextts
*/
const createInnerTRPCContext = (opts: CreateContextOptions) => {
return {
session: opts.session,
db,
};
};
/**
* This is the actual context you'll use in your router. It will be used to
* process every request that goes through your tRPC endpoint
* @link https://trpc.io/docs/context
* @see https://trpc.io/docs/server/context
*/
export const createTRPCContext = async (opts: {
headers?: Headers;
auth: Session | null;
headers: Headers;
session: Session | null;
}) => {
const session = opts.auth ?? (await auth());
const source = opts.headers?.get("x-trpc-source") ?? "unknown";
const session = opts.session ?? (await auth());
const source = opts.headers.get("x-trpc-source") ?? "unknown";
console.log(">>> tRPC Request from", source, "by", session?.user);
return createInnerTRPCContext({
return {
session,
});
db,
};
};
/**
@@ -70,18 +49,21 @@ export const createTRPCContext = async (opts: {
*/
const t = initTRPC.context<typeof createTRPCContext>().create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
errorFormatter: ({ shape, error }) => ({
...shape,
data: {
...shape.data,
zodError: error.cause instanceof ZodError ? error.cause.flatten() : null,
},
}),
});
/**
* Create a server-side caller
* @see https://trpc.io/docs/server/server-side-calls
*/
export const createCallerFactory = t.createCallerFactory;
/**
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
*

View File

@@ -7,18 +7,10 @@ export const env = createEnv({
process.env.NODE_ENV === "production"
? z.string().min(1)
: z.string().min(1).optional(),
AUTH_URL: z.preprocess(
// This makes Vercel deployments not fail if you don't set NEXTAUTH_URL
// Since NextAuth.js automatically uses the VERCEL_URL if present.
(str) => process.env.VERCEL_URL ?? str,
// VERCEL_URL doesn't include `https` so it cant be validated as a URL
process.env.VERCEL ? z.string() : z.string().url(),
),
},
client: {},
runtimeEnv: {
AUTH_SECRET: process.env.AUTH_SECRET,
AUTH_URL: process.env.AUTH_URL,
},
skipValidation:
Boolean(process.env.CI) || Boolean(process.env.SKIP_ENV_VALIDATION),

View File

@@ -24,7 +24,7 @@
"@t3-oss/env-nextjs": "^0.9.2",
"bcrypt": "^5.1.1",
"cookies": "^0.9.1",
"next": "^14.1.0",
"next": "^14.1.1-canary.58",
"next-auth": "5.0.0-beta.11",
"react": "18.2.0",
"react-dom": "18.2.0"

View File

@@ -0,0 +1,146 @@
# Spotlight
Spotlight is the search functionality of Homarr. It can be opened by pressing `Ctrl + K` or `Cmd + K` on Mac. It is a quick way to search for anything in Homarr.
## API
### SpotlightActionData
The [SpotlightActionData](./src/type.ts) is the data structure that is used to define the actions that are shown in the spotlight.
#### Common properties
| Name | Type | Description |
| ------------------------------ | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
| id | `string` | The id of the action. |
| title | `string \| (t: TranslationFunction) => string` | The title of the action. Either static or generated with translation function |
| description | `string \| (t: TranslationFunction) => string` | The description of the action. Either static or generated with translation function |
| icon | `string \| (props: TablerIconProps) => JSX.Element` | The icon of the action. Either a url to an image or a TablerIcon |
| group | `string` | The group of the action. By default the groups all, web and action exist. |
| ignoreSearchAndOnlyShowInGroup | `boolean` | If true, the action will only be shown in the group and not in the search results. |
| type | `'link' \| 'button'` | The type of the action. Either link or button |
#### Properties for links
| Name | Type | Description |
| ---- | -------- | ---------------------------------------------------------------------------------------------------------- |
| href | `string` | The url the link should navigate to. If %s is contained it will be replaced with the current search query. |
#### Properties for buttons
| Name | Type | Description |
| ------- | -------------------------- | ----------------------------------------------------------------------------------------- |
| onClick | `() => MaybePromise<void>` | The function that should be called when the button is clicked. It can be async if needed. |
### useRegisterSpotlightActions
The [useRegisterSpotlightActions](./src/data-store.ts) hook is used to register actions to the spotlight. It takes an unique key and the array of [SpotlightActionData](#SpotlightActionData).
#### Usage
The following example shows how to use the `useRegisterSpotlightActions` hook to register an action to the spotlight.
```tsx
"use client";
import { useRegisterSpotlightActions } from "@homarr/spotlight";
const MyComponent = () => {
useRegisterSpotlightActions("my-component", [
{
id: "my-action",
title: "My Action",
description: "This is my action",
icon: "https://example.com/icon.png",
group: "web",
type: "link",
href: "https://example.com",
},
]);
return <div>My Component</div>;
};
```
##### Using translation function
```tsx
"use client";
import { useRegisterSpotlightActions } from "@homarr/spotlight";
const MyComponent = () => {
useRegisterSpotlightActions("my-component", [
{
id: "my-action",
title: (t) => t("some.path.to.translation.key"),
description: (t) => t("some.other.path.to.translation.key"),
icon: "https://example.com/icon.png",
group: "web",
type: "link",
href: "https://example.com",
},
]);
return <div>Component implementation</div>;
};
```
##### Using TablerIcon
```tsx
"use client";
import { IconUserCog } from "tabler-react";
import { useRegisterSpotlightActions } from "@homarr/spotlight";
const UserMenu = () => {
useRegisterSpotlightActions("header-user-menu", [
{
id: "user-preferences",
title: (t) => t("user.preferences.title"),
description: (t) => t("user.preferences.description"),
icon: IconUserCog,
group: "action",
type: "link",
href: "/user/preferences",
},
]);
return <div>Component implementation</div>;
};
```
##### Using dependency array
```tsx
"use client";
import { IconUserCog } from "tabler-react";
import { useRegisterSpotlightActions } from "@homarr/spotlight";
const ColorSchemeButton = () => {
const { colorScheme, toggleColorScheme } = useColorScheme();
useRegisterSpotlightActions(
"toggle-color-scheme",
[
{
id: "toggle-color-scheme",
title: (t) => t("common.colorScheme.toggle.title"),
description: (t) =>
t(`common.colorScheme.toggle.${colorScheme}.description`),
icon: colorScheme === "light" ? IconSun : IconMoon,
group: "action",
type: "button",
onClick: toggleColorScheme,
},
],
[colorScheme],
);
return <div>Component implementation</div>;
};
```

View File

@@ -1,2 +1 @@
export * from "./src";
export { spotlight, Spotlight } from "@mantine/spotlight";

View File

@@ -34,6 +34,8 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@mantine/spotlight": "^7.5.3"
"@mantine/spotlight": "^7.5.3",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0"
}
}

View File

@@ -0,0 +1,54 @@
import { useScopedI18n } from "@homarr/translation/client";
import { Chip } from "@homarr/ui";
import {
selectNextAction,
selectPreviousAction,
spotlightStore,
triggerSelectedAction,
} from "./spotlight-store";
import type { SpotlightActionGroup } from "./type";
const disableArrowUpAndDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "ArrowDown") {
selectNextAction(spotlightStore);
e.preventDefault();
} else if (e.key === "ArrowUp") {
selectPreviousAction(spotlightStore);
e.preventDefault();
} else if (e.key === "Enter") {
triggerSelectedAction(spotlightStore);
}
};
const focusActiveByDefault = (e: React.FocusEvent<HTMLInputElement>) => {
const relatedTarget = e.relatedTarget;
const isPreviousTargetRadio =
relatedTarget && "type" in relatedTarget && relatedTarget.type === "radio";
if (isPreviousTargetRadio) return;
const group = e.currentTarget.parentElement?.parentElement;
if (!group) return;
const label = group.querySelector<HTMLLabelElement>("label[data-checked]");
if (!label) return;
label.focus();
};
interface Props {
group: SpotlightActionGroup;
}
export const GroupChip = ({ group }: Props) => {
const t = useScopedI18n("common.search.group");
return (
<Chip
key={group}
value={group}
onFocus={focusActiveByDefault}
onKeyDown={disableArrowUpAndDown}
>
{t(group)}
</Chip>
);
};

View File

@@ -0,0 +1,7 @@
.spotlightAction:hover {
background-color: alpha(var(--mantine-primary-color-filled), 0.1);
}
.spotlightAction[data-selected="true"] {
background-color: alpha(var(--mantine-primary-color-filled), 0.3);
}

View File

@@ -0,0 +1,154 @@
"use client";
import { useCallback, useState } from "react";
import Link from "next/link";
import {
Spotlight as MantineSpotlight,
SpotlightAction,
} from "@mantine/spotlight";
import { useAtomValue } from "jotai";
import type { TranslationFunction } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
import {
Center,
Chip,
Divider,
Flex,
Group,
IconSearch,
Text,
} from "@homarr/ui";
import { GroupChip } from "./chip-group";
import classes from "./component.module.css";
import { actionsAtomRead, groupsAtomRead } from "./data-store";
import { setSelectedAction, spotlightStore } from "./spotlight-store";
import type { SpotlightActionData } from "./type";
export const Spotlight = () => {
const [query, setQuery] = useState("");
const [group, setGroup] = useState("all");
const groups = useAtomValue(groupsAtomRead);
const actions = useAtomValue(actionsAtomRead);
const t = useI18n();
const preparedActions = actions.map((action) => prepareAction(action, t));
const items = preparedActions
.filter(
(item) =>
(item.ignoreSearchAndOnlyShowInGroup
? item.group === group
: item.title.toLowerCase().includes(query.toLowerCase().trim())) &&
(group === "all" || item.group === group),
)
.map((item) => {
const renderRoot =
item.type === "link"
? (props: Record<string, unknown>) => (
<Link href={prepareHref(item.href, query)} {...props} />
)
: undefined;
return (
<SpotlightAction
key={item.id}
renderRoot={renderRoot}
onClick={item.type === "button" ? item.onClick : undefined}
className={classes.spotlightAction}
>
<Group wrap="nowrap" w="100%">
{item.icon && (
<Center w={50} h={50}>
{typeof item.icon !== "string" && <item.icon size={24} />}
{typeof item.icon === "string" && (
<img
src={item.icon}
alt={item.title}
width={24}
height={24}
/>
)}
</Center>
)}
<Flex direction="column">
<Text>{item.title}</Text>
{item.description && (
<Text opacity={0.6} size="xs">
{item.description}
</Text>
)}
</Flex>
</Group>
</SpotlightAction>
);
});
const onGroupChange = useCallback(
(group: string) => {
setSelectedAction(-1, spotlightStore);
setGroup(group);
},
[setGroup, setSelectedAction],
);
return (
<MantineSpotlight.Root
query={query}
onQueryChange={setQuery}
store={spotlightStore}
>
<MantineSpotlight.Search
placeholder={t("common.search.placeholder")}
leftSection={<IconSearch stroke={1.5} />}
/>
<Divider />
<Group wrap="nowrap" p="sm">
<Chip.Group multiple={false} value={group} onChange={onGroupChange}>
<Group justify="start">
{groups.map((group) => (
<GroupChip key={group} group={group} />
))}
</Group>
</Chip.Group>
</Group>
<MantineSpotlight.ActionsList>
{items.length > 0 ? (
items
) : (
<MantineSpotlight.Empty>
{t("common.search.nothingFound")}
</MantineSpotlight.Empty>
)}
</MantineSpotlight.ActionsList>
</MantineSpotlight.Root>
);
};
const prepareHref = (href: string, query: string) => {
return href.replace("%s", query);
};
const translateIfNecessary = (
value: string | ((t: TranslationFunction) => string),
t: TranslationFunction,
) => {
if (typeof value === "function") {
return value(t);
}
return value;
};
const prepareAction = (
action: SpotlightActionData,
t: TranslationFunction,
) => ({
...action,
title: translateIfNecessary(action.title, t),
description: translateIfNecessary(action.description, t),
});

View File

@@ -0,0 +1,72 @@
import { useEffect } from "react";
import { atom, useSetAtom } from "jotai";
import useDeepCompareEffect from "use-deep-compare-effect";
import type { SpotlightActionData, SpotlightActionGroup } from "./type";
const defaultGroups = ["all", "web", "action"] as const;
const reversedDefaultGroups = [...defaultGroups].reverse();
const actionsAtom = atom<Record<string, readonly SpotlightActionData[]>>({});
export const actionsAtomRead = atom((get) =>
Object.values(get(actionsAtom)).flatMap((item) => item),
);
export const groupsAtomRead = atom((get) =>
Array.from(
new Set(
get(actionsAtomRead)
.map((item) => item.group as SpotlightActionGroup) // Allow "all" group to be included in the list of groups
.concat(...defaultGroups),
),
)
.sort((groupA, groupB) => {
const groupAIndex = reversedDefaultGroups.indexOf(groupA);
const groupBIndex = reversedDefaultGroups.indexOf(groupB);
// if both groups are not in the default groups, sort them by name (here reversed because we reverse the array afterwards)
if (groupAIndex === -1 && groupBIndex === -1) {
return groupB.localeCompare(groupA);
}
return groupAIndex - groupBIndex;
})
.reverse(),
);
const registrations = new Map<string, number>();
export const useRegisterSpotlightActions = (
key: string,
actions: SpotlightActionData[],
dependencies: readonly unknown[] = [],
) => {
const setActions = useSetAtom(actionsAtom);
// Use deep compare effect if there are dependencies for the actions, this supports deep compare of the action dependencies
const useSpecificEffect =
dependencies.length >= 1 ? useDeepCompareEffect : useEffect;
useSpecificEffect(() => {
if (!registrations.has(key) || dependencies.length >= 1) {
setActions((prev) => ({
...prev,
[key]: actions,
}));
}
registrations.set(key, (registrations.get(key) ?? 0) + 1);
return () => {
if (registrations.get(key) === 1) {
setActions((prev) => {
const { [key]: _, ...rest } = prev;
return rest;
});
}
registrations.set(key, (registrations.get(key) ?? 0) - 1);
if (registrations.get(key) === 0) {
registrations.delete(key);
}
};
}, [key, dependencies.length >= 1 ? dependencies : undefined]);
};

View File

@@ -1 +1,8 @@
export const name = "spotlight";
"use client";
import { spotlightActions } from "./spotlight-store";
export { Spotlight } from "./component";
const openSpotlight = spotlightActions.open;
export { openSpotlight };

View File

@@ -0,0 +1,45 @@
"use client";
import { clamp } from "@mantine/hooks";
import type { SpotlightStore } from "@mantine/spotlight";
import { createSpotlight } from "@mantine/spotlight";
export const [spotlightStore, spotlightActions] = createSpotlight();
export const setSelectedAction = (index: number, store: SpotlightStore) => {
store.updateState((state) => ({ ...state, selected: index }));
};
export const selectAction = (index: number, store: SpotlightStore): number => {
const state = store.getState();
const actionsList = document.getElementById(state.listId);
const selected =
actionsList?.querySelector<HTMLButtonElement>("[data-selected]");
const actions =
actionsList?.querySelectorAll<HTMLButtonElement>("[data-action]") ?? [];
const nextIndex =
index === -1 ? actions.length - 1 : index === actions.length ? 0 : index;
const selectedIndex = clamp(nextIndex, 0, actions.length - 1);
selected?.removeAttribute("data-selected");
actions[selectedIndex]?.scrollIntoView({ block: "nearest" });
actions[selectedIndex]?.setAttribute("data-selected", "true");
setSelectedAction(selectedIndex, store);
return selectedIndex;
};
export const selectNextAction = (store: SpotlightStore) => {
return selectAction(store.getState().selected + 1, store);
};
export const selectPreviousAction = (store: SpotlightStore) => {
return selectAction(store.getState().selected - 1, store);
};
export const triggerSelectedAction = (store: SpotlightStore) => {
const state = store.getState();
const selected = document.querySelector<HTMLButtonElement>(
`#${state.listId} [data-selected]`,
);
selected?.click();
};

View File

@@ -0,0 +1,31 @@
import type {
TranslationFunction,
TranslationObject,
} from "@homarr/translation";
import type { TablerIconsProps } from "@homarr/ui";
export type SpotlightActionGroup =
keyof TranslationObject["common"]["search"]["group"];
interface BaseSpotlightAction {
id: string;
title: string | ((t: TranslationFunction) => string);
description: string | ((t: TranslationFunction) => string);
group: Exclude<SpotlightActionGroup, "all">; // actions can not be assigned to the "all" group
icon: ((props: TablerIconsProps) => JSX.Element) | string;
ignoreSearchAndOnlyShowInGroup?: boolean;
}
interface SpotlightActionLink extends BaseSpotlightAction {
type: "link";
href: string;
}
type MaybePromise<T> = T | Promise<T>;
interface SpotlightActionButton extends BaseSpotlightAction {
type: "button";
onClick: () => MaybePromise<void>;
}
export type SpotlightActionData = SpotlightActionLink | SpotlightActionButton;

View File

@@ -1,3 +1,5 @@
export * from "./type";
export const supportedLanguages = ["en", "de"] as const;
export type SupportedLanguage = (typeof supportedLanguages)[number];

View File

@@ -154,6 +154,11 @@ export default {
search: {
placeholder: "Search for anything...",
nothingFound: "Nothing found",
group: {
all: "All",
web: "Web",
action: "Actions",
},
},
userAvatar: {
menu: {

View File

@@ -0,0 +1,5 @@
import type { useI18n } from "./client";
import type enTranslation from "./lang/en";
export type TranslationFunction = ReturnType<typeof useI18n>;
export type TranslationObject = typeof enTranslation;

View File

@@ -55,7 +55,6 @@ export const WidgetEditModal: ManagedModal<ModalProps<WidgetKind>> = ({
)}
{Object.entries(definition.options).map(
([key, value]: [string, OptionsBuilderResult[string]]) => {
console.log(value);
const Input = getInputForType(value.type);
if (!Input || value.shouldHide?.(form.values.options as never)) {

187
pnpm-lock.yaml generated
View File

@@ -106,14 +106,14 @@ importers:
specifier: ^0.9.2
version: 0.9.2(typescript@5.3.3)(zod@3.22.4)
'@tanstack/react-query':
specifier: ^5.20.5
version: 5.20.5(react@18.2.0)
specifier: ^5.21.2
version: 5.21.2(react@18.2.0)
'@tanstack/react-query-devtools':
specifier: ^5.21.0
version: 5.21.0(@tanstack/react-query@5.20.5)(react@18.2.0)
specifier: ^5.21.3
version: 5.21.3(@tanstack/react-query@5.21.2)(react@18.2.0)
'@tanstack/react-query-next-experimental':
specifier: 5.20.5
version: 5.20.5(@tanstack/react-query@5.20.5)(next@14.1.0)(react@18.2.0)
specifier: 5.21.2
version: 5.21.2(@tanstack/react-query@5.21.2)(next@14.1.1-canary.58)(react@18.2.0)
'@tiptap/extension-link':
specifier: ^2.2.3
version: 2.2.3(@tiptap/core@2.2.3)(@tiptap/pm@2.2.3)
@@ -128,10 +128,10 @@ importers:
version: 11.0.0-next-beta.289(@trpc/server@11.0.0-next-beta.289)
'@trpc/next':
specifier: next
version: 11.0.0-next-beta.289(@tanstack/react-query@5.20.5)(@trpc/client@11.0.0-next-beta.289)(@trpc/react-query@11.0.0-next-beta.289)(@trpc/server@11.0.0-next-beta.289)(next@14.1.0)(react-dom@18.2.0)(react@18.2.0)
version: 11.0.0-next-beta.289(@tanstack/react-query@5.21.2)(@trpc/client@11.0.0-next-beta.289)(@trpc/react-query@11.0.0-next-beta.289)(@trpc/server@11.0.0-next-beta.289)(next@14.1.1-canary.58)(react-dom@18.2.0)(react@18.2.0)
'@trpc/react-query':
specifier: next
version: 11.0.0-next-beta.289(@tanstack/react-query@5.20.5)(@trpc/client@11.0.0-next-beta.289)(@trpc/server@11.0.0-next-beta.289)(react-dom@18.2.0)(react@18.2.0)
version: 11.0.0-next-beta.289(@tanstack/react-query@5.21.2)(@trpc/client@11.0.0-next-beta.289)(@trpc/server@11.0.0-next-beta.289)(react-dom@18.2.0)(react@18.2.0)
'@trpc/server':
specifier: next
version: 11.0.0-next-beta.289
@@ -140,13 +140,13 @@ importers:
version: 1.11.10
jotai:
specifier: ^2.6.4
version: 2.6.4(@types/react@18.2.55)(react@18.2.0)
version: 2.6.4(@types/react@18.2.56)(react@18.2.0)
mantine-modal-manager:
specifier: ^7.5.2
version: 7.5.2(@mantine/hooks@7.5.3)(react-dom@18.2.0)(react@18.2.0)
next:
specifier: ^14.1.0
version: 14.1.0(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0)
specifier: ^14.1.1-canary.58
version: 14.1.1-canary.58(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0)
postcss-preset-mantine:
specifier: ^1.13.0
version: 1.13.0(postcss@8.4.35)
@@ -162,6 +162,9 @@ importers:
superjson:
specifier: 2.2.1
version: 2.2.1
use-deep-compare-effect:
specifier: ^1.8.1
version: 1.8.1(react@18.2.0)
devDependencies:
'@homarr/eslint-config':
specifier: workspace:^0.2.0
@@ -176,8 +179,8 @@ importers:
specifier: ^20.11.19
version: 20.11.19
'@types/react':
specifier: ^18.2.55
version: 18.2.55
specifier: ^18.2.56
version: 18.2.56
'@types/react-dom':
specifier: ^18.2.19
version: 18.2.19
@@ -258,11 +261,11 @@ importers:
specifier: ^0.9.1
version: 0.9.1
next:
specifier: ^14.1.0
version: 14.1.0(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0)
specifier: ^14.1.1-canary.58
version: 14.1.1-canary.58(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0)
next-auth:
specifier: 5.0.0-beta.11
version: 5.0.0-beta.11(next@14.1.0)(react@18.2.0)
version: 5.0.0-beta.11(next@14.1.1-canary.58)(react@18.2.0)
react:
specifier: 18.2.0
version: 18.2.0
@@ -433,6 +436,12 @@ importers:
packages/spotlight:
dependencies:
'@homarr/translation':
specifier: workspace:^0.1.0
version: link:../translation
'@homarr/ui':
specifier: workspace:^0.1.0
version: link:../ui
'@mantine/spotlight':
specifier: ^7.5.3
version: 7.5.3(@mantine/core@7.5.3)(@mantine/hooks@7.5.3)(react-dom@18.2.0)(react@18.2.0)
@@ -1659,8 +1668,8 @@ packages:
- supports-color
dev: false
/@next/env@14.1.0:
resolution: {integrity: sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw==}
/@next/env@14.1.1-canary.58:
resolution: {integrity: sha512-xMoPPiWVEIyIPoWaLhswlMq2Tnfkcv+XRmUAEhKkJgWdHIMFh45tiniWrZQLfrn4DAnF8gEAoejJaniNShFm7w==}
dev: false
/@next/eslint-plugin-next@14.1.0:
@@ -1669,8 +1678,8 @@ packages:
glob: 10.3.10
dev: false
/@next/swc-darwin-arm64@14.1.0:
resolution: {integrity: sha512-nUDn7TOGcIeyQni6lZHfzNoo9S0euXnu0jhsbMOmMJUBfgsnESdjN97kM7cBqQxZa8L/bM9om/S5/1dzCrW6wQ==}
/@next/swc-darwin-arm64@14.1.1-canary.58:
resolution: {integrity: sha512-exDpEe9ptK0NJEEyA+sThnR/6Oif/XaPK1y10fkcTNoQJkUCENL46nR1bCEYDAQrS5w03C1BHs3LSIOuoQcIag==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
@@ -1678,8 +1687,8 @@ packages:
dev: false
optional: true
/@next/swc-darwin-x64@14.1.0:
resolution: {integrity: sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g==}
/@next/swc-darwin-x64@14.1.1-canary.58:
resolution: {integrity: sha512-Q+38djBg+yaZjB8R6kst8kqHLIpTIM2RzuIvFGOflb8WaVI1ApXERTJKd345BOp42s0rIJ1UqynRplNCqYebeA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
@@ -1687,8 +1696,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-arm64-gnu@14.1.0:
resolution: {integrity: sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ==}
/@next/swc-linux-arm64-gnu@14.1.1-canary.58:
resolution: {integrity: sha512-wv2457qPqh7ENqQ3t8sCWCsNStEW/o8qejZ6Ywdwl3eKP1jnFNtn4OWfOuP2FUl1XmVEzbABozuOyiCGAtD63Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -1696,8 +1705,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-arm64-musl@14.1.0:
resolution: {integrity: sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g==}
/@next/swc-linux-arm64-musl@14.1.1-canary.58:
resolution: {integrity: sha512-xvfU/+mZfLpW/tRy743JtkNPlNI2t1dSkIzvDHHn/V28k1c+4YmsNxKMZRqqcxi73hwnuWYgor/96Sm4r9pmQQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -1705,8 +1714,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-x64-gnu@14.1.0:
resolution: {integrity: sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ==}
/@next/swc-linux-x64-gnu@14.1.1-canary.58:
resolution: {integrity: sha512-OgayNv+ChIjsPYMkBit2LTI0tMJ72or4w5DfQe2i1M2Ccy09p/LSRCiOptkV3XKyO+Tz8+fAqN+PdLRxfxQiuQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -1714,8 +1723,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-x64-musl@14.1.0:
resolution: {integrity: sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg==}
/@next/swc-linux-x64-musl@14.1.1-canary.58:
resolution: {integrity: sha512-6epwXWxSUyNKzOt0FSRW0QioPaWA9xlu8LM5N0c9eg8vaI3FvZmQegtHIzuzNc7RyWE1a5ZY4uDBVA1JaUJRow==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -1723,8 +1732,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-arm64-msvc@14.1.0:
resolution: {integrity: sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ==}
/@next/swc-win32-arm64-msvc@14.1.1-canary.58:
resolution: {integrity: sha512-Qa79WuVRcrkmbxzTXr/tmWvFHVMjUu9KpxNrKn+HO/DmrVEnR4NxmuzOOxc6QkCb+Rb86rfV7tttPQqw7YigBA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
@@ -1732,8 +1741,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-ia32-msvc@14.1.0:
resolution: {integrity: sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw==}
/@next/swc-win32-ia32-msvc@14.1.1-canary.58:
resolution: {integrity: sha512-fuqdt6XKc4kvUpgOMh/LL2eqJ/eltxknClpaAg9Nt0dGTQIF0rjKEVm8NRwv832URZKKBbz9AtBp5TEmOquG9g==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
@@ -1741,8 +1750,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-x64-msvc@14.1.0:
resolution: {integrity: sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg==}
/@next/swc-win32-x64-msvc@14.1.1-canary.58:
resolution: {integrity: sha512-P/DkKFiZo1mILb2UGt1YiurmuKo9LLvgC0l77YQ5g2b8jrTbYS9JeJJcdhLXnsHPPQrR/QgnEDJ2zWRtIhUJZA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -1934,9 +1943,14 @@ packages:
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
dev: true
/@swc/helpers@0.5.2:
resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==}
/@swc/counter@0.1.3:
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
dev: false
/@swc/helpers@0.5.5:
resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==}
dependencies:
'@swc/counter': 0.1.3
tslib: 2.6.2
dev: false
@@ -1981,43 +1995,43 @@ packages:
resolution: {integrity: sha512-4w5evLh+7FUUiA1GucvGj2ReX2TvOjEr4ejXdwL/bsjoSkof6r1gQmzqI+VHrE2CpJpB3al7bCTulOkFa/RcyA==}
dev: false
/@tanstack/query-core@5.20.5:
resolution: {integrity: sha512-T1W28gGgWn0A++tH3lxj3ZuUVZZorsiKcv+R50RwmPYz62YoDEkG4/aXHZELGkRp4DfrW07dyq2K5dvJ4Wl1aA==}
/@tanstack/query-core@5.21.2:
resolution: {integrity: sha512-jg7OcDG44oLT3uuGQQ9BM65ZBIdAq9xNZXPEk7Gr6w1oM1wo2/95H3dPDjLVs0yZKwrmE/ORUOC2Pyi1sp2fDA==}
dev: false
/@tanstack/query-devtools@5.21.0:
resolution: {integrity: sha512-hbfuh9xredeehLhlJY38sey7Ezlr3KduDHRkrnvbwuTQCgkvixFdcJGMpUx0khh74q3Ay8QGa2uO+fV/kSNlww==}
/@tanstack/query-devtools@5.21.3:
resolution: {integrity: sha512-tRJ3uYpF8mLqb+Na25DqPsqdsK38Ff1HIse3znQIEoGMzf7rJKTwV9HCn/9cPQTyTcAuSUZOajyCVz7hnbdCdQ==}
dev: false
/@tanstack/react-query-devtools@5.21.0(@tanstack/react-query@5.20.5)(react@18.2.0):
resolution: {integrity: sha512-79Rq5gtf9iuOANtrAIvSukC2ZT5d8iPXrJtCnkpJWKM2i+pIl6aBJR56sLJiFOvtlrJ1Cy8dkWZzWSi1s4tBbg==}
/@tanstack/react-query-devtools@5.21.3(@tanstack/react-query@5.21.2)(react@18.2.0):
resolution: {integrity: sha512-xU6nSshTneHppN9xQnjLgIAy36GHsQsHp/aCa515dRzcgEqC99Ix4TdwbduPSRTk5ZATsUEO0kKMZzSbubO0Pg==}
peerDependencies:
'@tanstack/react-query': ^5.20.5
'@tanstack/react-query': ^5.21.2
react: ^18.0.0
dependencies:
'@tanstack/query-devtools': 5.21.0
'@tanstack/react-query': 5.20.5(react@18.2.0)
'@tanstack/query-devtools': 5.21.3
'@tanstack/react-query': 5.21.2(react@18.2.0)
react: 18.2.0
dev: false
/@tanstack/react-query-next-experimental@5.20.5(@tanstack/react-query@5.20.5)(next@14.1.0)(react@18.2.0):
resolution: {integrity: sha512-P4r357MckowLGUAeQJ9UWTBK4i/JS/G4alBuXkNNyDp8md/pzk/VXG4y+c6/kJWOoi/Qtawz122l4oMJFp3MHA==}
/@tanstack/react-query-next-experimental@5.21.2(@tanstack/react-query@5.21.2)(next@14.1.1-canary.58)(react@18.2.0):
resolution: {integrity: sha512-IKx6MvXxUWe2uAONZJZAsYBGhDXNpVoN5B81+JsS7AdOADO4+EiASk+3VPPg8vtkdYioiqbJ6sNp9zRn7pOGTA==}
peerDependencies:
'@tanstack/react-query': ^5.20.5
'@tanstack/react-query': ^5.21.2
next: ^13 || ^14
react: ^18.0.0
dependencies:
'@tanstack/react-query': 5.20.5(react@18.2.0)
next: 14.1.0(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0)
'@tanstack/react-query': 5.21.2(react@18.2.0)
next: 14.1.1-canary.58(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0)
react: 18.2.0
dev: false
/@tanstack/react-query@5.20.5(react@18.2.0):
resolution: {integrity: sha512-6MHwJ8G9cnOC/XKrwt56QMc91vN7hLlAQNUA0ubP7h9Jj3a/CmkUwT6ALdFbnVP+PsYdhW3WONa8WQ4VcTaSLQ==}
/@tanstack/react-query@5.21.2(react@18.2.0):
resolution: {integrity: sha512-/Vv1qTumNDDVA5EYk40kivHZ2kICs1w38GBLRvV6A/lrixUJR5bfqZMKqHA1S6ND3gR9hvSyAAYejBbjLrQnSA==}
peerDependencies:
react: ^18.0.0
dependencies:
'@tanstack/query-core': 5.20.5
'@tanstack/query-core': 5.21.2
react: 18.2.0
dev: false
@@ -2314,7 +2328,7 @@ packages:
'@trpc/server': 11.0.0-next-beta.289
dev: false
/@trpc/next@11.0.0-next-beta.289(@tanstack/react-query@5.20.5)(@trpc/client@11.0.0-next-beta.289)(@trpc/react-query@11.0.0-next-beta.289)(@trpc/server@11.0.0-next-beta.289)(next@14.1.0)(react-dom@18.2.0)(react@18.2.0):
/@trpc/next@11.0.0-next-beta.289(@tanstack/react-query@5.21.2)(@trpc/client@11.0.0-next-beta.289)(@trpc/react-query@11.0.0-next-beta.289)(@trpc/server@11.0.0-next-beta.289)(next@14.1.1-canary.58)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-AKCrcbtHh/zFrld6lMG0RC37d/aac4ZisLDjJcViMnEmJXCo0J5nhoZa6f+G9N683NdMWZVmY2rmJidw9IX3QQ==}
peerDependencies:
'@tanstack/react-query': ^5.0.0
@@ -2330,16 +2344,16 @@ packages:
'@trpc/react-query':
optional: true
dependencies:
'@tanstack/react-query': 5.20.5(react@18.2.0)
'@tanstack/react-query': 5.21.2(react@18.2.0)
'@trpc/client': 11.0.0-next-beta.289(@trpc/server@11.0.0-next-beta.289)
'@trpc/react-query': 11.0.0-next-beta.289(@tanstack/react-query@5.20.5)(@trpc/client@11.0.0-next-beta.289)(@trpc/server@11.0.0-next-beta.289)(react-dom@18.2.0)(react@18.2.0)
'@trpc/react-query': 11.0.0-next-beta.289(@tanstack/react-query@5.21.2)(@trpc/client@11.0.0-next-beta.289)(@trpc/server@11.0.0-next-beta.289)(react-dom@18.2.0)(react@18.2.0)
'@trpc/server': 11.0.0-next-beta.289
next: 14.1.0(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0)
next: 14.1.1-canary.58(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@trpc/react-query@11.0.0-next-beta.289(@tanstack/react-query@5.20.5)(@trpc/client@11.0.0-next-beta.289)(@trpc/server@11.0.0-next-beta.289)(react-dom@18.2.0)(react@18.2.0):
/@trpc/react-query@11.0.0-next-beta.289(@tanstack/react-query@5.21.2)(@trpc/client@11.0.0-next-beta.289)(@trpc/server@11.0.0-next-beta.289)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-SAn09DmZ4eFYLS0cCHOVNvRHJhHZ2ssUj4LUTj56wym0MieaCSrcxTqiolnaMfF+mWc1SJlLOzebrxaTHPwJSw==}
peerDependencies:
'@tanstack/react-query': ^5.0.0
@@ -2348,7 +2362,7 @@ packages:
react: '>=18.2.0'
react-dom: '>=18.2.0'
dependencies:
'@tanstack/react-query': 5.20.5(react@18.2.0)
'@tanstack/react-query': 5.21.2(react@18.2.0)
'@trpc/client': 11.0.0-next-beta.289(@trpc/server@11.0.0-next-beta.289)
'@trpc/server': 11.0.0-next-beta.289
react: 18.2.0
@@ -2587,11 +2601,11 @@ packages:
/@types/react-dom@18.2.19:
resolution: {integrity: sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==}
dependencies:
'@types/react': 18.2.55
'@types/react': 18.2.56
dev: true
/@types/react@18.2.55:
resolution: {integrity: sha512-Y2Tz5P4yz23brwm2d7jNon39qoAtMMmalOQv6+fEFt1mT+FcM3D841wDpoUvFXhaYenuROCy3FZYqdTjM7qVyA==}
/@types/react@18.2.56:
resolution: {integrity: sha512-NpwHDMkS/EFZF2dONFQHgkPRwhvgq/OAvIaGQzxGSBmaeR++kTg6njr15Vatz0/2VcCEwJQFi6Jf4Q0qBu0rLA==}
dependencies:
'@types/prop-types': 15.7.11
'@types/scheduler': 0.16.8
@@ -5378,7 +5392,7 @@ packages:
resolution: {integrity: sha512-/WByRr4jDcsKlvMd1dRJnPfS1GVO3WuKyaurJ/vvXcOaUQO8rnNObCQMlv/5uCceVQIq5Q4WLF44ohsdiTohdg==}
dev: false
/jotai@2.6.4(@types/react@18.2.55)(react@18.2.0):
/jotai@2.6.4(@types/react@18.2.56)(react@18.2.0):
resolution: {integrity: sha512-RniwQPX4893YlNR1muOtyUGHYaTD1fhEN4qnOuZJSrDHj6xdEMrqlRSN/hCm2fshwk78ruecB/P2l+NCVWe6TQ==}
engines: {node: '>=12.20.0'}
peerDependencies:
@@ -5390,7 +5404,7 @@ packages:
react:
optional: true
dependencies:
'@types/react': 18.2.55
'@types/react': 18.2.56
react: 18.2.0
dev: false
@@ -5890,7 +5904,7 @@ packages:
engines: {node: '>= 0.4.0'}
dev: true
/next-auth@5.0.0-beta.11(next@14.1.0)(react@18.2.0):
/next-auth@5.0.0-beta.11(next@14.1.1-canary.58)(react@18.2.0):
resolution: {integrity: sha512-OrfOVyXBGC69O8lEe81ZwFzQuWnY3Bsqee7kawr1iBycgSD64oNCtoFvRXiOWe7R0jOaafqQQlpsOS0mARhT3Q==}
peerDependencies:
'@simplewebauthn/browser': ^9.0.1
@@ -5907,7 +5921,7 @@ packages:
optional: true
dependencies:
'@auth/core': 0.27.0
next: 14.1.0(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0)
next: 14.1.1-canary.58(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0)
react: 18.2.0
dev: false
@@ -5923,8 +5937,8 @@ packages:
resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==}
dev: true
/next@14.1.0(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0):
resolution: {integrity: sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==}
/next@14.1.1-canary.58(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0):
resolution: {integrity: sha512-rL4Tmm5ldL4VA+lTz0sQfJcDOe3vXB7LItcWuILb8eDs/6X1wrXNF9KDtviJH0tq/jrrTOihN7aBCkkge85m6g==}
engines: {node: '>=18.17.0'}
hasBin: true
peerDependencies:
@@ -5938,8 +5952,8 @@ packages:
sass:
optional: true
dependencies:
'@next/env': 14.1.0
'@swc/helpers': 0.5.2
'@next/env': 14.1.1-canary.58
'@swc/helpers': 0.5.5
busboy: 1.6.0
caniuse-lite: 1.0.30001587
graceful-fs: 4.2.11
@@ -5949,15 +5963,15 @@ packages:
sass: 1.71.0
styled-jsx: 5.1.1(@babel/core@7.23.9)(react@18.2.0)
optionalDependencies:
'@next/swc-darwin-arm64': 14.1.0
'@next/swc-darwin-x64': 14.1.0
'@next/swc-linux-arm64-gnu': 14.1.0
'@next/swc-linux-arm64-musl': 14.1.0
'@next/swc-linux-x64-gnu': 14.1.0
'@next/swc-linux-x64-musl': 14.1.0
'@next/swc-win32-arm64-msvc': 14.1.0
'@next/swc-win32-ia32-msvc': 14.1.0
'@next/swc-win32-x64-msvc': 14.1.0
'@next/swc-darwin-arm64': 14.1.1-canary.58
'@next/swc-darwin-x64': 14.1.1-canary.58
'@next/swc-linux-arm64-gnu': 14.1.1-canary.58
'@next/swc-linux-arm64-musl': 14.1.1-canary.58
'@next/swc-linux-x64-gnu': 14.1.1-canary.58
'@next/swc-linux-x64-musl': 14.1.1-canary.58
'@next/swc-win32-arm64-msvc': 14.1.1-canary.58
'@next/swc-win32-ia32-msvc': 14.1.1-canary.58
'@next/swc-win32-x64-msvc': 14.1.1-canary.58
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
@@ -7847,6 +7861,17 @@ packages:
react: 18.2.0
dev: false
/use-deep-compare-effect@1.8.1(react@18.2.0):
resolution: {integrity: sha512-kbeNVZ9Zkc0RFGpfMN3MNfaKNvcLNyxOAAd9O4CBZ+kCBXXscn9s/4I+8ytUER4RDpEYs5+O6Rs4PqiZ+rHr5Q==}
engines: {node: '>=10', npm: '>=6'}
peerDependencies:
react: '>=16.13'
dependencies:
'@babel/runtime': 7.23.9
dequal: 2.0.3
react: 18.2.0
dev: false
/use-isomorphic-layout-effect@1.1.2(react@18.2.0):
resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==}
peerDependencies: