diff --git a/apps/nextjs/src/components/create/universal-create-modal.module.css b/apps/nextjs/src/components/create/universal-create-modal.module.css deleted file mode 100644 index 38e8cf86a..000000000 --- a/apps/nextjs/src/components/create/universal-create-modal.module.css +++ /dev/null @@ -1,21 +0,0 @@ -.action { - width: 100%; - border-radius: var(--mantine-radius-md); - padding: var(--mantine-spacing-sm); - text-align: start; -} - -@media (hover: hover) { - .action:hover { - background: var(--mantine-color-default-hover); - } -} - -.action:focus-visible { - outline: 2px solid var(--mantine-primary-color-filled); - outline-offset: 2px; -} - -.actionIcon { - flex: 0 0 auto; -} diff --git a/apps/nextjs/src/components/create/universal-create-modal.tsx b/apps/nextjs/src/components/create/universal-create-modal.tsx index a6676a227..ab66845e2 100644 --- a/apps/nextjs/src/components/create/universal-create-modal.tsx +++ b/apps/nextjs/src/components/create/universal-create-modal.tsx @@ -1,11 +1,12 @@ "use client"; -import { useEffect, useRef } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useRouter } from "next/navigation"; -import { Group, Stack, Text, ThemeIcon, UnstyledButton } from "@mantine/core"; +import { Badge, Center, Divider, Input, ScrollArea, SimpleGrid, Stack, Text, ThemeIcon } from "@mantine/core"; import { IconApps, IconBox, + IconBuildingStore, IconCode, IconLayoutDashboard, IconLayoutGridAdd, @@ -15,18 +16,18 @@ import { import { useSession } from "@homarr/auth/client"; import type { GroupPermissionKey } from "@homarr/definitions"; -import { createModal, useModalAction } from "@homarr/modals"; +import { createModal, modalSizeSelect, useModalAction } from "@homarr/modals"; import { AddBoardModal } from "@homarr/modals-collection"; import { useScopedI18n } from "@homarr/translation/client"; +import { SelectableCard } from "@homarr/ui"; import type { TablerIcon } from "@homarr/ui"; -import classes from "./universal-create-modal.module.css"; import type { RankedUniversalCreateAction, UniversalCreateActionGroup, UniversalCreateActionKey, } from "./universal-create-actions"; -import { getUniversalCreateActionDefinitions } from "./universal-create-actions"; +import { filterAndRankUniversalCreateActions, getUniversalCreateActionDefinitions } from "./universal-create-actions"; import type { SetupMetricEntryPoint } from "./setup-analytics"; import { useSetupAnalytics } from "./setup-analytics"; @@ -43,11 +44,20 @@ const actionIcons = { integration: IconPlugConnected, container: IconLayoutGridAdd, board: IconLayoutDashboard, - workshop: IconSearch, + workshop: IconBuildingStore, customWidget: IconCode, } satisfies Record; +const groupColors: Record = { + currentBoard: "primaryColor", + library: "cyan", + boards: "teal", +}; + +const selectGridCols = { base: 1, sm: 2, md: 3 }; + export const UniversalCreateModal = createModal(({ actions, innerProps }) => { + const [search, setSearch] = useState(""); const { data: session } = useSession(); const router = useRouter(); const t = useScopedI18n("universalCreate"); @@ -97,7 +107,7 @@ export const UniversalCreateModal = createModal(({ ac openAddBoardModal(undefined); break; case "workshop": - router.push("/manage/workshop"); + router.push("/manage/custom-widgets/workshop"); break; case "customWidget": router.push("/manage/custom-widgets/new"); @@ -108,55 +118,129 @@ export const UniversalCreateModal = createModal(({ ac } }; - const resolvedActions = definitions.map( - (definition) => - ({ - ...definition, - name: t(`action.${definition.key}.label`), - description: t(`action.${definition.key}.description`), - keywords: t(`action.${definition.key}.keywords`).split(" "), - }) satisfies RankedUniversalCreateAction, + const resolvedActions = useMemo( + () => + definitions.map( + (definition) => + ({ + ...definition, + name: t(`action.${definition.key}.label`), + description: t(`action.${definition.key}.description`), + keywords: t(`action.${definition.key}.keywords`).split(" "), + }) satisfies RankedUniversalCreateAction, + ), + [definitions, t], ); - const orderedActions = resolvedActions.toSorted((left, right) => right.priority - left.priority); + + const filteredActions = useMemo( + () => filterAndRankUniversalCreateActions(resolvedActions, search), + [resolvedActions, search], + ); + + const isSearching = search.trim().length > 0; const groups = ["currentBoard", "library", "boards"] satisfies UniversalCreateActionGroup[]; return ( - {groups.map((group) => { - const groupActions = orderedActions.filter((action) => action.group === group); - if (groupActions.length === 0) return null; + {/* Top Search Input */} + setSearch(event.currentTarget.value)} + leftSection={} + placeholder={t("search.placeholder")} + aria-label={t("search.label")} + data-autofocus + onKeyDown={(event) => { + if (event.key === "Enter" && filteredActions.length === 1 && filteredActions[0]) { + invokeAction(filteredActions[0].key); + } + }} + /> + + {/* Action Cards Grid */} + + + {isSearching ? ( + + {filteredActions.map((action) => { + const Icon = actionIcons[action.key]; + return ( + invokeAction(action.key)} + aria-label={action.name} + icon={ + + + + } + title={action.name} + topRight={ + + {t(`group.${action.group}`)} + + } + description={action.description} + /> + ); + })} + + ) : ( + groups.map((group) => { + const groupActions = resolvedActions + .filter((action) => action.group === group) + .toSorted((left, right) => right.priority - left.priority); + if (groupActions.length === 0) return null; - return ( - - - {t(`group.${group}`)} - - {groupActions.map((action) => { - const Icon = actionIcons[action.key]; return ( - invokeAction(action.key)}> - - - - - - - {action.name} + + + {t(`group.${group}`)} - - {action.description} - - - - + } + labelPosition="left" + /> + + {groupActions.map((action) => { + const Icon = actionIcons[action.key]; + return ( + invokeAction(action.key)} + aria-label={action.name} + icon={ + + + + } + title={action.name} + topRight={ + + {t(`group.${action.group}`)} + + } + description={action.description} + /> + ); + })} + + ); - })} - - ); - })} + }) + )} + + {filteredActions.length === 0 && ( +
+ {t("search.noResults")} +
+ )} +
+ ); }).withOptions({ defaultTitle: (t) => t("universalCreate.title"), - size: "lg", + size: modalSizeSelect, });