(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(
-
-
- ,
- 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 5c87f962ea..441a330fc7 100644
--- a/apps/admin/components/common/header/index.tsx
+++ b/apps/admin/components/common/header/index.tsx
@@ -4,13 +4,20 @@
* See the LICENSE file for details.
*/
+import { Fragment } from "react";
import { observer } from "mobx-react";
+import Link from "next/link";
import { usePathname } from "next/navigation";
// icons
import { Menu, Settings } from "lucide-react";
-// components
-import { Breadcrumbs } from "@/components/common/breadcrumbs";
-import { BreadcrumbLink } from "../breadcrumb-link";
+// plane internal packages
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbList,
+ BreadcrumbSeparator,
+} from "@makeplane/propel/components/breadcrumb";
// hooks
import { useTheme } from "@/hooks/store";
// local imports
@@ -62,26 +69,28 @@ export const AdminHeader = observer(function AdminHeader() {
-
-
+
+
}
+ render={}
/>
- }
- />
- {breadcrumbItems.map(
- (item) =>
- item.title && (
- }
- />
- )
- )}
-
+
+ {breadcrumbItems.map(
+ (item) =>
+ item.title && (
+
+
+
+ } />
+
+
+ )
+ )}
+
+
diff --git a/apps/admin/components/common/new-user-popup.tsx b/apps/admin/components/common/new-user-popup.tsx
index 30dda8d2dc..32e2775b43 100644
--- a/apps/admin/components/common/new-user-popup.tsx
+++ b/apps/admin/components/common/new-user-popup.tsx
@@ -8,7 +8,7 @@ import { observer } from "mobx-react";
import Link from "next/link";
import { useTheme as useNextTheme } from "next-themes";
// ui
-import { Button, getButtonStyling } from "@plane/propel/button";
+import { Button } from "@makeplane/propel/components/button";
import { resolveGeneralTheme } from "@plane/utils";
// hooks
import TakeoffIconDark from "@/app/assets/logos/takeoff-icon-dark.svg?url";
@@ -33,12 +33,15 @@ export const NewUserPopup = observer(function NewUserPopup() {
workspace.
-
- Create workspace
-
-
+ }
+ label="Create workspace"
+ />
+
diff --git a/apps/admin/components/common/plane-lockup.tsx b/apps/admin/components/common/plane-lockup.tsx
new file mode 100644
index 0000000000..e2aab44683
--- /dev/null
+++ b/apps/admin/components/common/plane-lockup.tsx
@@ -0,0 +1,58 @@
+/**
+ * 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 logo lockup. Not part of @makeplane/propel's icon set, so it is kept as a local asset.
+
+type PlaneLockupProps = {
+ width?: string | number;
+ height?: string | number;
+ className?: string;
+ color?: string;
+};
+
+export function PlaneLockup({ width = "253", height = "53", className, color = "currentColor" }: PlaneLockupProps) {
+ return (
+
+ );
+}
diff --git a/apps/admin/components/common/skeleton.tsx b/apps/admin/components/common/skeleton.tsx
new file mode 100644
index 0000000000..8652a3fefb
--- /dev/null
+++ b/apps/admin/components/common/skeleton.tsx
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2023-present Plane Software, Inc. and contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * See the LICENSE file for details.
+ */
+
+import React from "react";
+// helpers
+import { cn } from "@plane/utils";
+
+// Loading-placeholder skeleton. @makeplane/propel has no skeleton component yet;
+// this matches the API the workspace @plane/propel skeleton exposed.
+
+type SkeletonProps = {
+ children: React.ReactNode;
+ className?: string;
+ ariaLabel?: string;
+};
+
+function SkeletonRoot({ children, className = "", ariaLabel = "Loading content" }: SkeletonProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+type ItemProps = {
+ height?: string;
+ width?: string;
+ className?: string;
+};
+
+function SkeletonItem({ height = "auto", width = "auto", className = "" }: ItemProps) {
+ return
;
+}
+
+const Skeleton = Object.assign(SkeletonRoot, { Item: SkeletonItem });
+
+SkeletonRoot.displayName = "plane-ui-skeleton";
+SkeletonItem.displayName = "plane-ui-skeleton-item";
+
+export { Skeleton };
diff --git a/apps/admin/components/instance/failure.tsx b/apps/admin/components/instance/failure.tsx
index 89515b8769..8d9d37f3d7 100644
--- a/apps/admin/components/instance/failure.tsx
+++ b/apps/admin/components/instance/failure.tsx
@@ -6,7 +6,7 @@
import { observer } from "mobx-react";
import { useTheme } from "next-themes";
-import { Button } from "@plane/propel/button";
+import { Button } from "@makeplane/propel/components/button";
// assets
import { AuthHeader } from "@/app/(all)/(home)/auth-header";
import InstanceFailureDarkImage from "@/app/assets/instance/instance-failure-dark.svg?url";
@@ -34,9 +34,7 @@ export const InstanceFailureView = observer(function InstanceFailureView() {
-
+
diff --git a/apps/admin/components/instance/setup-form.tsx b/apps/admin/components/instance/setup-form.tsx
index ea68ec913e..175c6e747c 100644
--- a/apps/admin/components/instance/setup-form.tsx
+++ b/apps/admin/components/instance/setup-form.tsx
@@ -10,14 +10,13 @@ import { useSearchParams } from "next/navigation";
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 { Button } from "@makeplane/propel/components/button";
+import { Checkbox } from "@makeplane/propel/components/checkbox";
+import { Input, InputGroup } from "@makeplane/propel/components/input";
import { AuthService } from "@plane/services";
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";
@@ -168,46 +167,48 @@ export function InstanceSetupForm() {
- {
- const validation = validatePersonName(e.target.value);
- if (validation === true || e.target.value === "") {
- handleFormChange("first_name", e.target.value);
- }
- }}
- autoComplete="off"
- autoFocus
- maxLength={50}
- />
+
+ {
+ const validation = validatePersonName(e.target.value);
+ if (validation === true || e.target.value === "") {
+ handleFormChange("first_name", e.target.value);
+ }
+ }}
+ autoComplete="off"
+ autoFocus
+ maxLength={50}
+ />
+
- {
- const validation = validatePersonName(e.target.value);
- if (validation === true || e.target.value === "") {
- handleFormChange("last_name", e.target.value);
- }
- }}
- autoComplete="off"
- maxLength={50}
- />
+
+ {
+ const validation = validatePersonName(e.target.value);
+ if (validation === true || e.target.value === "") {
+ handleFormChange("last_name", e.target.value);
+ }
+ }}
+ autoComplete="off"
+ maxLength={50}
+ />
+
@@ -215,18 +216,19 @@ export function InstanceSetupForm() {
- handleFormChange("email", e.target.value)}
- hasError={errorData.type && errorData.type === EErrorCodes.INVALID_EMAIL ? true : false}
- autoComplete="off"
- />
+
+ handleFormChange("email", e.target.value)}
+ aria-invalid={errorData.type && errorData.type === EErrorCodes.INVALID_EMAIL ? true : false}
+ autoComplete="off"
+ />
+
{errorData.type && errorData.type === EErrorCodes.INVALID_EMAIL && errorData.message && (
{errorData.message}
)}
@@ -236,39 +238,39 @@ export function InstanceSetupForm() {
- {
- const validation = validateCompanyName(e.target.value, false);
- if (validation === true || e.target.value === "") {
- handleFormChange("company_name", e.target.value);
- }
- }}
- maxLength={80}
- />
+
+ {
+ const validation = validateCompanyName(e.target.value, false);
+ if (validation === true || e.target.value === "") {
+ handleFormChange("company_name", e.target.value);
+ }
+ }}
+ maxLength={80}
+ />
+
-
+
handleFormChange("password", e.target.value)}
- hasError={errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD ? true : false}
+ aria-invalid={errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD ? true : false}
onFocus={() => setIsPasswordInputFocused(true)}
onBlur={() => setIsPasswordInputFocused(false)}
autoComplete="new-password"
@@ -277,7 +279,7 @@ export function InstanceSetupForm() {
+
{errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD && errorData.message && (
{errorData.message}
)}
@@ -303,16 +305,15 @@ export function InstanceSetupForm() {
-
+
handleFormChange("confirm_password", e.target.value)}
placeholder="Confirm password"
- className="w-full border border-subtle !bg-surface-1 pr-12 placeholder:text-placeholder"
onFocus={() => setIsRetryPasswordInputFocused(true)}
onBlur={() => setIsRetryPasswordInputFocused(false)}
autoComplete="new-password"
@@ -321,7 +322,7 @@ export function InstanceSetupForm() {
handleShowPassword("retypePassword")}
>
@@ -330,13 +331,13 @@ export function InstanceSetupForm() {
handleShowPassword("retypePassword")}
>
)}
-
+
{!!formData.confirm_password &&
formData.password !== formData.confirm_password &&
renderPasswordMatchError && (
@@ -347,10 +348,9 @@ export function InstanceSetupForm() {
handleFormChange("is_telemetry_enabled", !formData.is_telemetry_enabled)}
+ aria-label="Allow Plane to anonymously collect usage events"
+ onCheckedChange={(checked) => handleFormChange("is_telemetry_enabled", checked)}
checked={formData.is_telemetry_enabled}
/>
@@ -368,9 +368,15 @@ export function InstanceSetupForm() {
-
- {isSubmitting ? : "Continue"}
-
+
diff --git a/apps/admin/components/workspace/list-item.tsx b/apps/admin/components/workspace/list-item.tsx
index 9594d7f219..58716a93c3 100644
--- a/apps/admin/components/workspace/list-item.tsx
+++ b/apps/admin/components/workspace/list-item.tsx
@@ -7,9 +7,9 @@
import { observer } from "mobx-react";
// plane internal packages
+import { Tooltip } from "@makeplane/propel/components/tooltip";
+import { NewTabOutline } from "@makeplane/propel/icons";
import { WEB_BASE_URL } from "@plane/constants";
-import { NewTabIcon } from "@plane/propel/icons";
-import { Tooltip } from "@plane/propel/tooltip";
import { getFileURL } from "@plane/utils";
// hooks
import { useWorkspace } from "@/hooks/store";
@@ -52,7 +52,7 @@ export const WorkspaceListItem = observer(function WorkspaceListItem({ workspace
{workspace.name}
/
-
+
[{workspace.slug}]
@@ -82,7 +82,7 @@ export const WorkspaceListItem = observer(function WorkspaceListItem({ workspace
-
+
);
diff --git a/apps/admin/hooks/use-sidebar-menu/core.ts b/apps/admin/hooks/use-sidebar-menu/core.ts
index 9d4e449b3a..e0a961f29b 100644
--- a/apps/admin/hooks/use-sidebar-menu/core.ts
+++ b/apps/admin/hooks/use-sidebar-menu/core.ts
@@ -6,7 +6,7 @@
import { Image, BrainCog, Cog, Mail } from "lucide-react";
// plane imports
-import { LockIcon, WorkspaceIcon } from "@plane/propel/icons";
+import { LockOutline, WorkspaceOutline } from "@makeplane/propel/icons";
// types
import type { TSidebarMenuItem } from "./types";
@@ -26,13 +26,13 @@ export const coreSidebarMenuLinks: Record
href: `/email/`,
},
workspace: {
- Icon: WorkspaceIcon,
+ Icon: WorkspaceOutline,
name: "Workspaces",
description: "Manage all workspaces on this instance.",
href: `/workspace/`,
},
authentication: {
- Icon: LockIcon,
+ Icon: LockOutline,
name: "Authentication",
description: "Configure authentication modes.",
href: `/authentication/`,
diff --git a/apps/admin/package.json b/apps/admin/package.json
index b44bcfdb79..c83062f072 100644
--- a/apps/admin/package.json
+++ b/apps/admin/package.json
@@ -23,9 +23,9 @@
"@fontsource/ibm-plex-mono": "catalog:",
"@fontsource/material-symbols-rounded": "catalog:",
"@headlessui/react": "catalog:",
+ "@makeplane/propel": "0.2.0",
"@plane/constants": "workspace:*",
"@plane/hooks": "workspace:*",
- "@plane/propel": "workspace:*",
"@plane/services": "workspace:*",
"@plane/types": "workspace:*",
"@plane/utils": "workspace:*",
@@ -39,7 +39,6 @@
"react": "catalog:",
"react-dom": "catalog:",
"react-hook-form": "catalog:",
- "react-popper": "catalog:",
"react-router": "catalog:",
"serve": "catalog:",
"swr": "catalog:",
diff --git a/apps/admin/providers/toast.tsx b/apps/admin/providers/toast.tsx
index 541678efa5..1042dd90bd 100644
--- a/apps/admin/providers/toast.tsx
+++ b/apps/admin/providers/toast.tsx
@@ -4,11 +4,80 @@
* See the LICENSE file for details.
*/
-import { useTheme } from "next-themes";
-import { Toast } from "@plane/propel/toast";
-import { resolveGeneralTheme } from "@plane/utils";
+import { createToastManager, ToastProvider } from "@makeplane/propel/components/toast";
+import type { ToastData, ToastVariant } from "@makeplane/propel/components/toast";
+import { IconButton } from "@makeplane/propel/components/icon-button";
+import { Icon } from "@makeplane/propel/components/icon";
+import { CloseOutline } from "@makeplane/propel/icons";
+
+// Keep the call-site API the workspace @plane/propel toast exposed, backed by
+// @makeplane/propel's toast manager, so consumers only change their import path.
+export enum TOAST_TYPE {
+ SUCCESS = "success",
+ ERROR = "error",
+ INFO = "info",
+ WARNING = "warning",
+}
+
+const TYPE_TO_VARIANT: Record = {
+ [TOAST_TYPE.SUCCESS]: "success",
+ [TOAST_TYPE.ERROR]: "danger",
+ [TOAST_TYPE.INFO]: "info",
+ [TOAST_TYPE.WARNING]: "warning",
+};
+
+const toastManager = createToastManager();
+
+type SetToastProps = {
+ type: TOAST_TYPE;
+ title: string;
+ message?: string;
+};
+
+export const setToast = (props: SetToastProps) =>
+ toastManager.add({
+ title: props.title,
+ description: props.message,
+ data: { variant: TYPE_TO_VARIANT[props.type] },
+ });
+
+type PromiseToastData = {
+ title: string;
+ message?: (data: TValue) => string;
+};
+
+type PromiseToastOptions = {
+ loading?: string;
+ success: PromiseToastData;
+ error: PromiseToastData;
+};
+
+export const setPromiseToast = (promise: Promise, options: PromiseToastOptions): void => {
+ void toastManager.promise(promise, {
+ loading: {
+ title: options.loading ?? "Loading...",
+ data: { variant: "neutral" },
+ },
+ success: (data) => ({
+ title: options.success.title,
+ description: options.success.message?.(data),
+ data: { variant: "success" },
+ }),
+ error: (data) => ({
+ title: options.error.title,
+ description: options.error.message?.(data),
+ data: { variant: "danger" },
+ }),
+ });
+};
export function ToastWithTheme() {
- const { resolvedTheme } = useTheme();
- return ;
+ return (
+ } />
+ }
+ />
+ );
}
diff --git a/apps/admin/styles/globals.css b/apps/admin/styles/globals.css
index 7f7f2483a5..df9c6dbc25 100644
--- a/apps/admin/styles/globals.css
+++ b/apps/admin/styles/globals.css
@@ -1,4 +1,9 @@
@import "@plane/tailwind-config/index.css";
+/* Design tokens already arrive via @plane/tailwind-config. The @makeplane/propel styles barrel
+ re-imports the same token files (identical content) and, crucially, registers propel's dist as a
+ Tailwind @source so the utility classes used by its components are emitted, plus the animation
+ tokens (animate-spinner-reveal, animate-progress-indeterminate) its components rely on. */
+@import "@makeplane/propel/styles";
.shadow-custom {
box-shadow: 2px 2px 8px 2px rgba(234, 231, 250, 0.3); /* Convert #EAE7FA4D to rgba */
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 137d1e4ac6..2419a82c76 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -610,15 +610,15 @@ importers:
'@headlessui/react':
specifier: 'catalog:'
version: 2.2.10(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@makeplane/propel':
+ specifier: 0.2.0
+ version: 0.2.0(@date-fns/tz@1.4.1)(@types/react@19.2.17)(date-fns@4.1.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.1.17)
'@plane/constants':
specifier: workspace:*
version: link:../../packages/constants
'@plane/hooks':
specifier: workspace:*
version: link:../../packages/hooks
- '@plane/propel':
- specifier: workspace:*
- version: link:../../packages/propel
'@plane/services':
specifier: workspace:*
version: link:../../packages/services
@@ -658,9 +658,6 @@ 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)