diff --git a/apps/admin/app/(all)/(dashboard)/ai/page.tsx b/apps/admin/app/(all)/(dashboard)/ai/page.tsx index dec3200982..149d488c13 100644 --- a/apps/admin/app/(all)/(dashboard)/ai/page.tsx +++ b/apps/admin/app/(all)/(dashboard)/ai/page.tsx @@ -6,7 +6,7 @@ import { observer } from "mobx-react"; import useSWR from "swr"; -import { Loader } from "@plane/ui"; +import { Skeleton } from "@plane/propel/skeleton"; // components import { PageWrapper } from "@/components/common/page-wrapper"; // hooks @@ -32,14 +32,14 @@ const InstanceAIPage = observer(function InstanceAIPage(_props: Route.ComponentP {formattedConfig ? ( ) : ( - - + +
- - + +
- -
+ + )} ); diff --git a/apps/admin/app/(all)/(dashboard)/authentication/gitea/page.tsx b/apps/admin/app/(all)/(dashboard)/authentication/gitea/page.tsx index fe8eae4c65..5b713bdc59 100644 --- a/apps/admin/app/(all)/(dashboard)/authentication/gitea/page.tsx +++ b/apps/admin/app/(all)/(dashboard)/authentication/gitea/page.tsx @@ -8,8 +8,9 @@ import { useState } from "react"; import { observer } from "mobx-react"; import useSWR from "swr"; // plane internal packages +import { Skeleton } from "@plane/propel/skeleton"; +import { Switch } from "@plane/propel/switch"; import { setPromiseToast } from "@plane/propel/toast"; -import { Loader, ToggleSwitch } from "@plane/ui"; // assets import giteaLogo from "@/app/assets/logos/gitea-logo.svg?url"; // components @@ -72,7 +73,7 @@ const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthentic description="Allow members to login or sign up to plane with their Gitea accounts." icon={Gitea Logo} config={ - { updateConfig("IS_GITEA_ENABLED", isGiteaEnabled ? "0" : "1"); @@ -89,13 +90,13 @@ const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthentic {formattedConfig ? ( ) : ( - - - - - - - + + + + + + + )} ); diff --git a/apps/admin/app/(all)/(dashboard)/authentication/github/page.tsx b/apps/admin/app/(all)/(dashboard)/authentication/github/page.tsx index a7a29cf9d8..3cb5244ca5 100644 --- a/apps/admin/app/(all)/(dashboard)/authentication/github/page.tsx +++ b/apps/admin/app/(all)/(dashboard)/authentication/github/page.tsx @@ -9,8 +9,9 @@ import { observer } from "mobx-react"; import { useTheme } from "next-themes"; import useSWR from "swr"; // plane internal packages +import { Skeleton } from "@plane/propel/skeleton"; +import { Switch } from "@plane/propel/switch"; import { setPromiseToast } from "@plane/propel/toast"; -import { Loader, ToggleSwitch } from "@plane/ui"; import { resolveGeneralTheme } from "@plane/utils"; // assets import githubLightModeImage from "@/app/assets/logos/github-black.png?url"; @@ -87,7 +88,7 @@ const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthent /> } config={ - { updateConfig("IS_GITHUB_ENABLED", isGithubEnabled ? "0" : "1"); @@ -104,13 +105,13 @@ const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthent {formattedConfig ? ( ) : ( - - - - - - - + + + + + + + )} ); diff --git a/apps/admin/app/(all)/(dashboard)/authentication/gitlab/page.tsx b/apps/admin/app/(all)/(dashboard)/authentication/gitlab/page.tsx index 5bcaef7268..cd350e4383 100644 --- a/apps/admin/app/(all)/(dashboard)/authentication/gitlab/page.tsx +++ b/apps/admin/app/(all)/(dashboard)/authentication/gitlab/page.tsx @@ -7,8 +7,9 @@ import { useState } from "react"; import { observer } from "mobx-react"; import useSWR from "swr"; +import { Skeleton } from "@plane/propel/skeleton"; +import { Switch } from "@plane/propel/switch"; import { setPromiseToast } from "@plane/propel/toast"; -import { Loader, ToggleSwitch } from "@plane/ui"; // assets import GitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url"; // components @@ -71,7 +72,7 @@ const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthent description="Allow members to login or sign up to plane with their GitLab accounts." icon={GitLab Logo} config={ - { if (Boolean(parseInt(enableGitlabConfig)) === true) { @@ -92,13 +93,13 @@ const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthent {formattedConfig ? ( ) : ( - - - - - - - + + + + + + + )} ); diff --git a/apps/admin/app/(all)/(dashboard)/authentication/google/page.tsx b/apps/admin/app/(all)/(dashboard)/authentication/google/page.tsx index 93a61497d2..79b1aaa031 100644 --- a/apps/admin/app/(all)/(dashboard)/authentication/google/page.tsx +++ b/apps/admin/app/(all)/(dashboard)/authentication/google/page.tsx @@ -7,8 +7,9 @@ import { useState } from "react"; import { observer } from "mobx-react"; import useSWR from "swr"; +import { Skeleton } from "@plane/propel/skeleton"; +import { Switch } from "@plane/propel/switch"; import { setPromiseToast } from "@plane/propel/toast"; -import { Loader, ToggleSwitch } from "@plane/ui"; // assets import GoogleLogo from "@/app/assets/logos/google-logo.svg?url"; // components @@ -72,7 +73,7 @@ const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthent accounts." icon={Google Logo} config={ - { if (Boolean(parseInt(enableGoogleConfig)) === true) { @@ -93,13 +94,13 @@ const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthent {formattedConfig ? ( ) : ( - - - - - - - + + + + + + + )} ); diff --git a/apps/admin/app/(all)/(dashboard)/authentication/page.tsx b/apps/admin/app/(all)/(dashboard)/authentication/page.tsx index d96630f2a2..048c352491 100644 --- a/apps/admin/app/(all)/(dashboard)/authentication/page.tsx +++ b/apps/admin/app/(all)/(dashboard)/authentication/page.tsx @@ -9,9 +9,10 @@ import { observer } from "mobx-react"; import { useTheme } from "next-themes"; import useSWR from "swr"; // plane internal packages +import { Skeleton } from "@plane/propel/skeleton"; +import { Switch } from "@plane/propel/switch"; import { setPromiseToast, setToast, TOAST_TYPE } from "@plane/propel/toast"; import type { TInstanceConfigurationKeys, TInstanceAuthenticationModes } from "@plane/types"; -import { Loader, ToggleSwitch } from "@plane/ui"; import { cn, resolveGeneralTheme } from "@plane/utils"; // components import { PageWrapper } from "@/components/common/page-wrapper"; @@ -130,7 +131,7 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
- { if (Boolean(parseInt(enableSignUpConfig)) === true) { @@ -159,13 +160,13 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage( ))}
) : ( - - - - - - - + + + + + + + )} ); diff --git a/apps/admin/app/(all)/(dashboard)/email/email-config-form.tsx b/apps/admin/app/(all)/(dashboard)/email/email-config-form.tsx index 3c33d76961..a3899bc806 100644 --- a/apps/admin/app/(all)/(dashboard)/email/email-config-form.tsx +++ b/apps/admin/app/(all)/(dashboard)/email/email-config-form.tsx @@ -10,11 +10,10 @@ import { useForm } from "react-hook-form"; import { Button } from "@plane/propel/button"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import type { IFormattedInstanceConfiguration, TInstanceEmailConfigurationKeys } from "@plane/types"; -// ui -import { CustomSelect } from "@plane/ui"; // components import type { TControllerInputFormField } from "@/components/common/controller-input"; import { ControllerInput } from "@/components/common/controller-input"; +import { CustomSelect } from "@/components/common/custom-select"; // hooks import { useInstance } from "@/hooks/store"; // local components diff --git a/apps/admin/app/(all)/(dashboard)/email/page.tsx b/apps/admin/app/(all)/(dashboard)/email/page.tsx index e74b34ba8d..262cbab41e 100644 --- a/apps/admin/app/(all)/(dashboard)/email/page.tsx +++ b/apps/admin/app/(all)/(dashboard)/email/page.tsx @@ -7,8 +7,9 @@ import { useEffect, useState } from "react"; import { observer } from "mobx-react"; import useSWR from "swr"; +import { Skeleton } from "@plane/propel/skeleton"; +import { Switch } from "@plane/propel/switch"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; -import { Loader, ToggleSwitch } from "@plane/ui"; // components import { PageWrapper } from "@/components/common/page-wrapper"; // hooks @@ -71,11 +72,11 @@ const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.Comp ), actions: isLoading ? ( - - - + + + ) : ( - + ), }} > @@ -84,13 +85,13 @@ const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.Comp {formattedConfig ? ( ) : ( - - - - - - - + + + + + + + )} )} diff --git a/apps/admin/app/(all)/(dashboard)/email/test-email-modal.tsx b/apps/admin/app/(all)/(dashboard)/email/test-email-modal.tsx index c059b02f29..b5330d9de9 100644 --- a/apps/admin/app/(all)/(dashboard)/email/test-email-modal.tsx +++ b/apps/admin/app/(all)/(dashboard)/email/test-email-modal.tsx @@ -8,9 +8,8 @@ import { useEffect, useState, Fragment } from "react"; import { Dialog, Transition } from "@headlessui/react"; // plane imports import { Button } from "@plane/propel/button"; +import { Input } from "@plane/propel/input"; import { InstanceService } from "@plane/services"; -// ui -import { Input } from "@plane/ui"; type Props = { isOpen: boolean; diff --git a/apps/admin/app/(all)/(dashboard)/general/form.tsx b/apps/admin/app/(all)/(dashboard)/general/form.tsx index e38d7848eb..3d0499ddaf 100644 --- a/apps/admin/app/(all)/(dashboard)/general/form.tsx +++ b/apps/admin/app/(all)/(dashboard)/general/form.tsx @@ -9,9 +9,10 @@ import { Controller, useForm } from "react-hook-form"; import { Telescope } from "lucide-react"; // plane imports import { Button } from "@plane/propel/button"; +import { Input } from "@plane/propel/input"; +import { Switch } from "@plane/propel/switch"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import type { IInstance, IInstanceAdmin } from "@plane/types"; -import { Input, ToggleSwitch } from "@plane/ui"; // components import { ControllerInput } from "@/components/common/controller-input"; // hooks @@ -127,7 +128,7 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo control={control} name="is_telemetry_enabled" render={({ field: { value, onChange } }) => ( - + )} />
diff --git a/apps/admin/app/(all)/(dashboard)/image/page.tsx b/apps/admin/app/(all)/(dashboard)/image/page.tsx index e410e87eb8..bbc9d0a7bd 100644 --- a/apps/admin/app/(all)/(dashboard)/image/page.tsx +++ b/apps/admin/app/(all)/(dashboard)/image/page.tsx @@ -6,7 +6,7 @@ import { observer } from "mobx-react"; import useSWR from "swr"; -import { Loader } from "@plane/ui"; +import { Skeleton } from "@plane/propel/skeleton"; // components import { PageWrapper } from "@/components/common/page-wrapper"; // hooks @@ -32,10 +32,10 @@ const InstanceImagePage = observer(function InstanceImagePage(_props: Route.Comp {formattedConfig ? ( ) : ( - - - - + + + + )} ); diff --git a/apps/admin/app/(all)/(dashboard)/sidebar-dropdown.tsx b/apps/admin/app/(all)/(dashboard)/sidebar-dropdown.tsx index d1ddb1f5a3..b408efdc12 100644 --- a/apps/admin/app/(all)/(dashboard)/sidebar-dropdown.tsx +++ b/apps/admin/app/(all)/(dashboard)/sidebar-dropdown.tsx @@ -11,8 +11,8 @@ import { LogOut, UserCog2, Palette } from "lucide-react"; import { Menu, Transition } from "@headlessui/react"; // plane internal packages import { API_BASE_URL } from "@plane/constants"; +import { Avatar } from "@plane/propel/avatar"; import { AuthService } from "@plane/services"; -import { Avatar } from "@plane/ui"; import { getFileURL, cn } from "@plane/utils"; // hooks import { useTheme, useUser } from "@/hooks/store"; diff --git a/apps/admin/app/(all)/(dashboard)/workspace/create/form.tsx b/apps/admin/app/(all)/(dashboard)/workspace/create/form.tsx index 953940c37a..39a0b232e2 100644 --- a/apps/admin/app/(all)/(dashboard)/workspace/create/form.tsx +++ b/apps/admin/app/(all)/(dashboard)/workspace/create/form.tsx @@ -11,12 +11,13 @@ import { Controller, useForm } from "react-hook-form"; // plane imports import { WEB_BASE_URL, ORGANIZATION_SIZE, RESTRICTED_URLS } from "@plane/constants"; import { Button, getButtonStyling } from "@plane/propel/button"; +import { Input } from "@plane/propel/input"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { InstanceWorkspaceService } from "@plane/services"; import type { IWorkspace } from "@plane/types"; import { validateSlug, validateWorkspaceName } from "@plane/utils"; // components -import { CustomSelect, Input } from "@plane/ui"; +import { CustomSelect } from "@/components/common/custom-select"; // hooks import { useWorkspace } from "@/hooks/store"; diff --git a/apps/admin/app/(all)/(dashboard)/workspace/page.tsx b/apps/admin/app/(all)/(dashboard)/workspace/page.tsx index 5816ac9599..8b8718502b 100644 --- a/apps/admin/app/(all)/(dashboard)/workspace/page.tsx +++ b/apps/admin/app/(all)/(dashboard)/workspace/page.tsx @@ -11,9 +11,10 @@ import useSWR from "swr"; import { Loader as LoaderIcon } from "lucide-react"; // types import { Button, getButtonStyling } from "@plane/propel/button"; +import { Skeleton } from "@plane/propel/skeleton"; +import { Switch } from "@plane/propel/switch"; import { setPromiseToast } from "@plane/propel/toast"; import type { TInstanceConfigurationKeys } from "@plane/types"; -import { Loader, ToggleSwitch } from "@plane/ui"; import { cn } from "@plane/utils"; // components import { PageWrapper } from "@/components/common/page-wrapper"; @@ -94,7 +95,7 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
- { if (Boolean(parseInt(disableWorkspaceCreation)) === true) { @@ -110,9 +111,9 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
) : ( - - - + + + )} {workspaceLoader !== "init-loader" ? ( <> @@ -155,12 +156,12 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props )} ) : ( - - - - - - + + + + + + )} diff --git a/apps/admin/app/(all)/(home)/sign-in-form.tsx b/apps/admin/app/(all)/(home)/sign-in-form.tsx index 7b03b8c1cd..1dec226c89 100644 --- a/apps/admin/app/(all)/(home)/sign-in-form.tsx +++ b/apps/admin/app/(all)/(home)/sign-in-form.tsx @@ -11,8 +11,9 @@ import { Eye, EyeOff } from "lucide-react"; import type { EAdminAuthErrorCodes, TAdminAuthErrorInfo } from "@plane/constants"; import { API_BASE_URL } from "@plane/constants"; import { Button } from "@plane/propel/button"; +import { Input } from "@plane/propel/input"; +import { Spinner } from "@plane/propel/spinners"; import { AuthService } from "@plane/services"; -import { Input, Spinner } from "@plane/ui"; // components import { Banner } from "@/components/common/banner"; // local components diff --git a/apps/admin/components/authentication/email-config-switch.tsx b/apps/admin/components/authentication/email-config-switch.tsx index 0f304335b2..9e43e9e41d 100644 --- a/apps/admin/components/authentication/email-config-switch.tsx +++ b/apps/admin/components/authentication/email-config-switch.tsx @@ -7,8 +7,8 @@ import React from "react"; import { observer } from "mobx-react"; // hooks +import { Switch } from "@plane/propel/switch"; import type { TInstanceAuthenticationMethodKeys } from "@plane/types"; -import { ToggleSwitch } from "@plane/ui"; import { useInstance } from "@/hooks/store"; // ui // types @@ -26,7 +26,7 @@ export const EmailCodesConfiguration = observer(function EmailCodesConfiguration const enableMagicLogin = formattedConfig?.ENABLE_MAGIC_LINK_LOGIN ?? ""; return ( - { const newEnableMagicLogin = Boolean(parseInt(enableMagicLogin)) === true ? "0" : "1"; diff --git a/apps/admin/components/authentication/gitea-config.tsx b/apps/admin/components/authentication/gitea-config.tsx index ef9d6db15a..86a9ccd750 100644 --- a/apps/admin/components/authentication/gitea-config.tsx +++ b/apps/admin/components/authentication/gitea-config.tsx @@ -10,8 +10,8 @@ import Link from "next/link"; import { Settings2 } from "lucide-react"; // plane internal packages import { getButtonStyling } from "@plane/propel/button"; +import { Switch } from "@plane/propel/switch"; import type { TInstanceAuthenticationMethodKeys } from "@plane/types"; -import { ToggleSwitch } from "@plane/ui"; import { cn } from "@plane/utils"; // hooks import { useInstance } from "@/hooks/store"; @@ -37,7 +37,7 @@ export const GiteaConfiguration = observer(function GiteaConfiguration(props: Pr Edit - { Boolean(parseInt(GiteaConfig)) === true diff --git a/apps/admin/components/authentication/github-config.tsx b/apps/admin/components/authentication/github-config.tsx index 06443bf004..1f183f9411 100644 --- a/apps/admin/components/authentication/github-config.tsx +++ b/apps/admin/components/authentication/github-config.tsx @@ -10,8 +10,8 @@ import Link from "next/link"; import { Settings2 } from "lucide-react"; // plane internal packages import { getButtonStyling } from "@plane/propel/button"; +import { Switch } from "@plane/propel/switch"; import type { TInstanceAuthenticationMethodKeys } from "@plane/types"; -import { ToggleSwitch } from "@plane/ui"; import { cn } from "@plane/utils"; // hooks import { useInstance } from "@/hooks/store"; @@ -36,7 +36,7 @@ export const GithubConfiguration = observer(function GithubConfiguration(props: Edit - { const newEnableGithubConfig = Boolean(parseInt(enableGithubConfig)) === true ? "0" : "1"; diff --git a/apps/admin/components/authentication/gitlab-config.tsx b/apps/admin/components/authentication/gitlab-config.tsx index dcd3bed2e6..3e4552bad8 100644 --- a/apps/admin/components/authentication/gitlab-config.tsx +++ b/apps/admin/components/authentication/gitlab-config.tsx @@ -10,8 +10,8 @@ import Link from "next/link"; import { Settings2 } from "lucide-react"; // plane internal packages import { getButtonStyling } from "@plane/propel/button"; +import { Switch } from "@plane/propel/switch"; import type { TInstanceAuthenticationMethodKeys } from "@plane/types"; -import { ToggleSwitch } from "@plane/ui"; import { cn } from "@plane/utils"; // hooks import { useInstance } from "@/hooks/store"; @@ -36,7 +36,7 @@ export const GitlabConfiguration = observer(function GitlabConfiguration(props: Edit - { const newEnableGitlabConfig = Boolean(parseInt(enableGitlabConfig)) === true ? "0" : "1"; diff --git a/apps/admin/components/authentication/google-config.tsx b/apps/admin/components/authentication/google-config.tsx index 556dd5aa60..4a3b5dfd71 100644 --- a/apps/admin/components/authentication/google-config.tsx +++ b/apps/admin/components/authentication/google-config.tsx @@ -10,8 +10,8 @@ import Link from "next/link"; import { Settings2 } from "lucide-react"; // plane internal packages import { getButtonStyling } from "@plane/propel/button"; +import { Switch } from "@plane/propel/switch"; import type { TInstanceAuthenticationMethodKeys } from "@plane/types"; -import { ToggleSwitch } from "@plane/ui"; import { cn } from "@plane/utils"; // hooks import { useInstance } from "@/hooks/store"; @@ -36,7 +36,7 @@ export const GoogleConfiguration = observer(function GoogleConfiguration(props: Edit - { const newEnableGoogleConfig = Boolean(parseInt(enableGoogleConfig)) === true ? "0" : "1"; diff --git a/apps/admin/components/authentication/password-config-switch.tsx b/apps/admin/components/authentication/password-config-switch.tsx index 1b603980d0..b5ff2f5051 100644 --- a/apps/admin/components/authentication/password-config-switch.tsx +++ b/apps/admin/components/authentication/password-config-switch.tsx @@ -7,8 +7,8 @@ import React from "react"; import { observer } from "mobx-react"; // hooks +import { Switch } from "@plane/propel/switch"; import type { TInstanceAuthenticationMethodKeys } from "@plane/types"; -import { ToggleSwitch } from "@plane/ui"; import { useInstance } from "@/hooks/store"; // ui // types @@ -26,7 +26,7 @@ export const PasswordLoginConfiguration = observer(function PasswordLoginConfigu const enableEmailPassword = formattedConfig?.ENABLE_EMAIL_PASSWORD ?? ""; return ( - { const newEnableEmailPassword = Boolean(parseInt(enableEmailPassword)) === true ? "0" : "1"; diff --git a/apps/admin/components/common/breadcrumbs.tsx b/apps/admin/components/common/breadcrumbs.tsx new file mode 100644 index 0000000000..997508a933 --- /dev/null +++ b/apps/admin/components/common/breadcrumbs.tsx @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import * as React from "react"; +// plane internal packages +import { ChevronRightIcon } from "@plane/propel/icons"; +// helpers +import { cn } from "@plane/utils"; + +type BreadcrumbsProps = { + className?: string; + children: React.ReactNode; +}; + +function BreadcrumbsRoot({ className, children }: BreadcrumbsProps) { + const [isSmallScreen, setIsSmallScreen] = React.useState(false); + + React.useEffect(() => { + const handleResize = () => { + setIsSmallScreen(window.innerWidth <= 640); // Adjust this value as per your requirement + }; + + window.addEventListener("resize", handleResize); + handleResize(); // Call it initially to set the correct state + return () => window.removeEventListener("resize", handleResize); + }, []); + + const childrenArray = React.Children.toArray(children); + + return ( +
+ {!isSmallScreen && ( + <> + {childrenArray.map((child, index) => { + if (React.isValidElement(child)) { + return React.cloneElement(child, { + isLast: index === childrenArray.length - 1, + }); + } + return child; + })} + + )} + + {isSmallScreen && childrenArray.length > 1 && ( + <> +
+
+
+ {React.isValidElement(childrenArray[childrenArray.length - 1]) + ? React.cloneElement(childrenArray[childrenArray.length - 1] as React.ReactElement, { + isLast: true, + }) + : childrenArray[childrenArray.length - 1]} +
+ + )} + {isSmallScreen && childrenArray.length === 1 && childrenArray} +
+ ); +} + +// breadcrumb item +type BreadcrumbItemProps = { + component?: React.ReactNode; + showSeparator?: boolean; + isLast?: boolean; +}; + +function BreadcrumbItem(props: BreadcrumbItemProps) { + const { component, showSeparator = true, isLast = false } = props; + return ( +
+ {component} + {showSeparator && !isLast && } +
+ ); +} + +// breadcrumb separator +type BreadcrumbSeparatorProps = { + className?: string; + containerClassName?: string; + iconClassName?: string; +}; + +function BreadcrumbSeparator(props: BreadcrumbSeparatorProps) { + const { className, containerClassName, iconClassName } = props; + return ( +
+
+ +
+
+ ); +} + +const Breadcrumbs = Object.assign(BreadcrumbsRoot, { + Item: BreadcrumbItem, + Separator: BreadcrumbSeparator, +}); + +export { Breadcrumbs }; diff --git a/apps/admin/components/common/checkbox.tsx b/apps/admin/components/common/checkbox.tsx new file mode 100644 index 0000000000..e59d5ffed7 --- /dev/null +++ b/apps/admin/components/common/checkbox.tsx @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import * as React from "react"; +// helpers +import { cn } from "@plane/utils"; + +export interface CheckboxProps extends React.InputHTMLAttributes { + containerClassName?: string; + iconClassName?: string; + indeterminate?: boolean; +} + +const Checkbox = React.forwardRef(function Checkbox(props: CheckboxProps, ref: React.ForwardedRef) { + const { + id, + name, + checked, + indeterminate = false, + disabled, + containerClassName, + iconClassName, + className, + ...rest + } = props; + + return ( +
+ + + +
+ ); +}); +Checkbox.displayName = "form-checkbox-field"; + +export { Checkbox }; diff --git a/apps/admin/components/common/controller-input.tsx b/apps/admin/components/common/controller-input.tsx index eafb9de28b..f3f804c953 100644 --- a/apps/admin/components/common/controller-input.tsx +++ b/apps/admin/components/common/controller-input.tsx @@ -10,7 +10,7 @@ import { Controller } from "react-hook-form"; // icons import { Eye, EyeOff } from "lucide-react"; // plane internal packages -import { Input } from "@plane/ui"; +import { Input } from "@plane/propel/input"; import { cn } from "@plane/utils"; // Generic over the form's values because react-hook-form's Control is invariant: its diff --git a/apps/admin/components/common/controller-switch.tsx b/apps/admin/components/common/controller-switch.tsx index 58681949d2..af156385e1 100644 --- a/apps/admin/components/common/controller-switch.tsx +++ b/apps/admin/components/common/controller-switch.tsx @@ -7,7 +7,7 @@ import type { Control, FieldPath, FieldValues } from "react-hook-form"; import { Controller } from "react-hook-form"; // plane internal packages -import { ToggleSwitch } from "@plane/ui"; +import { Switch } from "@plane/propel/switch"; type Props = { control: Control; @@ -35,7 +35,7 @@ export function ControllerSwitch(props: Props) { render={({ field: { value, onChange } }) => { const parsedValue = Number.parseInt(typeof value === "string" ? value : String(value ?? "0"), 10); const isOn = !Number.isNaN(parsedValue) && parsedValue !== 0; - return onChange(isOn ? "0" : "1")} size="sm" />; + return onChange(isOn ? "0" : "1")} size="sm" />; }} /> diff --git a/apps/admin/components/common/custom-select.tsx b/apps/admin/components/common/custom-select.tsx new file mode 100644 index 0000000000..48e2b54c9b --- /dev/null +++ b/apps/admin/components/common/custom-select.tsx @@ -0,0 +1,247 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import { Combobox } from "@headlessui/react"; +import React, { createContext, useCallback, useContext, useRef, useState } from "react"; +import { createPortal } from "react-dom"; +import { usePopper } from "react-popper"; +// plane internal packages +import { useOutsideClickDetector } from "@plane/hooks"; +import { CheckIcon, ChevronDownIcon } from "@plane/propel/icons"; +// helpers +import { cn } from "@plane/utils"; + +type Placement = + | "top" + | "top-start" + | "top-end" + | "bottom" + | "bottom-start" + | "bottom-end" + | "left" + | "left-start" + | "left-end" + | "right" + | "right-start" + | "right-end"; + +export interface CustomSelectProps { + children: React.ReactNode; + value: any; + onChange: any; + customButtonClassName?: string; + buttonClassName?: string; + className?: string; + customButton?: React.ReactNode; + disabled?: boolean; + input?: boolean; + label?: string | React.ReactNode; + maxHeight?: "sm" | "rg" | "md" | "lg"; + noChevron?: boolean; + optionsClassName?: string; + placement?: Placement; + tabIndex?: number; +} + +export interface CustomSelectItemProps { + children: React.ReactNode; + value: any; + className?: string; +} + +const useDropdownKeyDown = ( + onOpen: () => void, + onClose: () => void, + isOpen: boolean, + selectActiveItem?: () => void +): ((event: React.KeyboardEvent) => void) => + useCallback( + (event: React.KeyboardEvent) => { + if (event.key === "Enter" && !event.nativeEvent.isComposing) { + if (!isOpen) { + event.stopPropagation(); + onOpen(); + } else { + selectActiveItem && selectActiveItem(); + } + } else if (event.key === "Escape" && isOpen) { + event.stopPropagation(); + onClose(); + } + }, + [isOpen, onOpen, onClose] + ); + +// Context to share the close handler with option components +const DropdownContext = createContext<() => void>(() => {}); + +function CustomSelect(props: CustomSelectProps) { + const { + customButtonClassName = "", + buttonClassName = "", + placement, + children, + className = "", + customButton, + disabled = false, + input = false, + label, + maxHeight = "md", + noChevron = false, + onChange, + optionsClassName = "", + value, + tabIndex, + } = props; + // states + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + const [isOpen, setIsOpen] = useState(false); + // refs + const dropdownRef = useRef(null); + + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + }); + + const openDropdown = useCallback(() => { + setIsOpen(true); + if (referenceElement) referenceElement.focus(); + }, [referenceElement]); + + const closeDropdown = useCallback(() => setIsOpen(false), []); + const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen); + useOutsideClickDetector(dropdownRef, closeDropdown); + + const toggleDropdown = useCallback(() => { + if (isOpen) closeDropdown(); + else openDropdown(); + }, [closeDropdown, isOpen, openDropdown]); + + return ( + + { + onChange?.(val); + closeDropdown(); + }} + className={cn("relative flex-shrink-0 text-left", className)} + onKeyDown={handleKeyDown} + disabled={disabled} + > + <> + {customButton ? ( + + + + ) : ( + + + + )} + + {isOpen && + createPortal( + +
+
+ {children} +
+
+
, + document.body + )} +
+
+ ); +} + +function Option(props: CustomSelectItemProps) { + const { children, value, className } = props; + const closeDropdown = useContext(DropdownContext); + + const handleClick = useCallback(() => { + // Close dropdown for both new and already-selected options. + // Use setTimeout to ensure HeadlessUI's onChange handler fires first for new selections. + // For already-selected options, this ensures the dropdown closes since onChange won't fire. + setTimeout(() => { + closeDropdown(); + }, 0); + }, [closeDropdown]); + + return ( + + cn( + "flex cursor-pointer items-center justify-between gap-2 truncate rounded-sm px-1 py-1.5 text-secondary select-none", + { + "bg-layer-transparent-hover": active, + }, + className + ) + } + onClick={handleClick} + > + {({ selected }) => ( +
+ {children} + {selected && } +
+ )} +
+ ); +} + +CustomSelect.Option = Option; + +export { CustomSelect }; diff --git a/apps/admin/components/common/header/index.tsx b/apps/admin/components/common/header/index.tsx index 8be7107048..5c87f962ea 100644 --- a/apps/admin/components/common/header/index.tsx +++ b/apps/admin/components/common/header/index.tsx @@ -6,10 +6,10 @@ import { observer } from "mobx-react"; import { usePathname } from "next/navigation"; -import { Menu, Settings } from "lucide-react"; // icons -import { Breadcrumbs } from "@plane/ui"; +import { Menu, Settings } from "lucide-react"; // components +import { Breadcrumbs } from "@/components/common/breadcrumbs"; import { BreadcrumbLink } from "../breadcrumb-link"; // hooks import { useTheme } from "@/hooks/store"; diff --git a/apps/admin/components/common/password-strength-indicator.tsx b/apps/admin/components/common/password-strength-indicator.tsx new file mode 100644 index 0000000000..5bdc4a8935 --- /dev/null +++ b/apps/admin/components/common/password-strength-indicator.tsx @@ -0,0 +1,144 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import { CircleCheck } from "lucide-react"; +// plane internal packages +import { E_PASSWORD_STRENGTH } from "@plane/constants"; +import { cn, getPasswordCriteria, getPasswordStrength } from "@plane/utils"; + +interface StrengthInfo { + message: string; + textColor: string; + activeFragments: number; +} + +/** + * Get strength information including message, color, and active fragments + */ +const getStrengthInfo = (strength: E_PASSWORD_STRENGTH): StrengthInfo => { + switch (strength) { + case E_PASSWORD_STRENGTH.EMPTY: + return { + message: "Please enter your password", + textColor: "text-primary", + activeFragments: 0, + }; + case E_PASSWORD_STRENGTH.LENGTH_NOT_VALID: + return { + message: "Password is too short", + textColor: "text-danger-primary", + activeFragments: 1, + }; + case E_PASSWORD_STRENGTH.STRENGTH_NOT_VALID: + return { + message: "Password is weak", + textColor: "text-orange-500", + activeFragments: 2, + }; + case E_PASSWORD_STRENGTH.STRENGTH_VALID: + return { + message: "Password is strong", + textColor: "text-success-primary", + activeFragments: 3, + }; + default: + return { + message: "Please enter your password", + textColor: "text-primary", + activeFragments: 0, + }; + } +}; + +/** + * Get fragment color based on position and active state + */ +const getFragmentColor = (fragmentIndex: number, activeFragments: number): string => { + if (fragmentIndex >= activeFragments) { + return "bg-layer-1"; + } + + switch (activeFragments) { + case 1: + return "bg-danger-primary"; + case 2: + return "bg-orange-500"; + case 3: + return "bg-success-primary"; + default: + return "bg-layer-1"; + } +}; + +export interface PasswordStrengthIndicatorProps { + password: string; + showCriteria?: boolean; + isFocused?: boolean; +} + +export function PasswordStrengthIndicator({ + password, + showCriteria = true, + isFocused = false, +}: PasswordStrengthIndicatorProps) { + const strength = getPasswordStrength(password); + const criteria = getPasswordCriteria(password); + const strengthInfo = getStrengthInfo(strength); + + const isPasswordMeterVisible = isFocused ? true : strength === E_PASSWORD_STRENGTH.STRENGTH_VALID ? false : true; + + if ((!password && !showCriteria) || !isPasswordMeterVisible) { + return null; + } + + return ( +
+ {/* Strength Indicator */} +
+
+ {[0, 1, 2].map((fragmentIndex) => ( +
+ ))} +
+ + {/* Strength Message */} + {password &&

{strengthInfo.message}

} +
+ + {/* Criteria list */} + {showCriteria && ( +
+ {criteria.map((criterion) => ( +
+
+ +
+ + {criterion.label} + +
+ ))} +
+ )} +
+ ); +} diff --git a/apps/admin/components/instance/setup-form.tsx b/apps/admin/components/instance/setup-form.tsx index fdb7c80917..ea68ec913e 100644 --- a/apps/admin/components/instance/setup-form.tsx +++ b/apps/admin/components/instance/setup-form.tsx @@ -11,11 +11,14 @@ import { Eye, EyeOff } from "lucide-react"; // plane internal packages import { API_BASE_URL, E_PASSWORD_STRENGTH } from "@plane/constants"; import { Button } from "@plane/propel/button"; +import { Input } from "@plane/propel/input"; +import { Spinner } from "@plane/propel/spinners"; import { AuthService } from "@plane/services"; -import { Checkbox, Input, PasswordStrengthIndicator, Spinner } from "@plane/ui"; import { getPasswordStrength, validatePersonName, validateCompanyName } from "@plane/utils"; // components import { AuthHeader } from "@/app/(all)/(home)/auth-header"; +import { Checkbox } from "@/components/common/checkbox"; +import { PasswordStrengthIndicator } from "@/components/common/password-strength-indicator"; import { Banner } from "../common/banner"; import { FormHeader } from "./form-header"; diff --git a/apps/admin/package.json b/apps/admin/package.json index d889a1c6ac..b44bcfdb79 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -28,7 +28,6 @@ "@plane/propel": "workspace:*", "@plane/services": "workspace:*", "@plane/types": "workspace:*", - "@plane/ui": "workspace:*", "@plane/utils": "workspace:*", "@react-router/node": "catalog:", "isbot": "catalog:", @@ -40,6 +39,7 @@ "react": "catalog:", "react-dom": "catalog:", "react-hook-form": "catalog:", + "react-popper": "catalog:", "react-router": "catalog:", "serve": "catalog:", "swr": "catalog:", diff --git a/packages/propel/package.json b/packages/propel/package.json index f92a5579bc..f98add2773 100644 --- a/packages/propel/package.json +++ b/packages/propel/package.json @@ -37,6 +37,7 @@ "./portal": "./dist/portal/index.js", "./scrollarea": "./dist/scrollarea/index.js", "./skeleton": "./dist/skeleton/index.js", + "./spinners": "./dist/spinners/index.js", "./switch": "./dist/switch/index.js", "./tab-navigation": "./dist/tab-navigation/index.js", "./table": "./dist/table/index.js", diff --git a/packages/propel/src/avatar/avatar.tsx b/packages/propel/src/avatar/avatar.tsx index 9fc4f53e83..3f59a692ca 100644 --- a/packages/propel/src/avatar/avatar.tsx +++ b/packages/propel/src/avatar/avatar.tsx @@ -106,6 +106,14 @@ export function Avatar(props: Props) { className={cn("grid place-items-center overflow-hidden", getBorderRadius(shape), { [sizeInfo.avatarSize]: !isAValidNumber(size), })} + style={ + isAValidNumber(size) + ? { + height: `${size}px`, + width: `${size}px`, + } + : {} + } tabIndex={-1} > diff --git a/packages/propel/tsdown.config.ts b/packages/propel/tsdown.config.ts index 8c3c3d8e7c..5e97cf6f93 100644 --- a/packages/propel/tsdown.config.ts +++ b/packages/propel/tsdown.config.ts @@ -29,6 +29,7 @@ export default defineConfig({ "src/portal/index.ts", "src/scrollarea/index.ts", "src/skeleton/index.ts", + "src/spinners/index.ts", "src/switch/index.ts", "src/table/index.ts", "src/tab-navigation/index.ts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 715e85e912..137d1e4ac6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -625,9 +625,6 @@ importers: '@plane/types': specifier: workspace:* version: link:../../packages/types - '@plane/ui': - specifier: workspace:* - version: link:../../packages/ui '@plane/utils': specifier: workspace:* version: link:../../packages/utils @@ -661,6 +658,9 @@ importers: react-hook-form: specifier: 'catalog:' version: 7.84.0(react@19.2.8) + react-popper: + specifier: 'catalog:' + version: 2.3.0(@popperjs/core@2.11.8)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react-router: specifier: 'catalog:' version: 8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)