snapshot of https://github.com/makeplane/plane.git @ d868349a55de79979eb8b98701bf1702c67ad634 (2026-09-15)
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-15 15:14:58 +05:00
commit 23059a1de3
5252 changed files with 573834 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { TickCircleOutline, WarningCircleOutline } from "@makeplane/propel/icons";
type TBanner = {
type: "success" | "error";
message: string;
};
export function Banner(props: TBanner) {
const { type, message } = props;
return (
<div
className={`w-full rounded-md border p-2 ${type === "error" ? "border-danger-strong bg-danger-subtle" : "border-success-strong bg-success-subtle"}`}
>
<div className="flex items-center justify-center">
<div className="flex-shrink-0">
{type === "error" ? (
<span className="flex h-6 w-6 items-center justify-center rounded-full">
<WarningCircleOutline className="h-5 w-5 text-danger-primary" aria-hidden="true" />
</span>
) : (
<TickCircleOutline className="h-5 w-5 text-success-primary" aria-hidden="true" />
)}
</div>
<div className="ml-1">
<p className={`text-13 font-medium ${type === "error" ? "text-danger-primary" : "text-success-primary"}`}>
{message}
</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { cn } from "@plane/utils";
type TProps = {
children: React.ReactNode;
className?: string;
darkerShade?: boolean;
};
export function CodeBlock({ children, className, darkerShade }: TProps) {
return (
<span
className={cn(
"rounded-md border border-subtle bg-surface-2 px-0.5 text-11 font-semibold text-tertiary",
{
"border-subtle bg-layer-1 text-secondary": darkerShade,
},
className
)}
>
{children}
</span>
);
}

View File

@@ -0,0 +1,81 @@
/**
* 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";
import Link from "next/link";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// ui
import { Button } from "@makeplane/propel/components/button";
type Props = {
isOpen: boolean;
handleClose: () => void;
onDiscardHref: string;
};
export function ConfirmDiscardModal(props: Props) {
const { isOpen, handleClose, onDiscardHref } = props;
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-50" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-backdrop transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-32">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-surface-1 text-left shadow-raised-200 transition-all sm:my-8 sm:w-[30rem]">
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:text-left">
<Dialog.Title as="h3" className="text-16 leading-6 font-medium text-tertiary">
You have unsaved changes
</Dialog.Title>
<div className="mt-2">
<p className="text-13 text-placeholder">
Changes you made will be lost if you go back. Do you wish to go back?
</p>
</div>
</div>
</div>
</div>
<div className="flex items-center justify-end gap-2 p-4 sm:px-6">
<Button variant="secondary" size="md" stretch="auto" onClick={handleClose} label="Keep editing" />
<Button
variant="primary"
size="sm"
stretch="auto"
nativeButton={false}
render={<Link href={onDiscardHref} />}
label="Go back"
/>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
}

View File

@@ -0,0 +1,90 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import React, { useState } from "react";
import type { Control, FieldPath, FieldValues } from "react-hook-form";
import { Controller } from "react-hook-form";
// icons
import { HideOutline, ShowOutline } from "@makeplane/propel/icons";
// plane internal packages
import { Input, InputGroup } from "@makeplane/propel/components/input";
// Generic over the form's values because react-hook-form's Control is invariant: its
// `_options.validate` narrows `name` to a keyof union, so `Control<any>` no longer
// accepts a typed form's control. Inferring from `control` keeps call sites unchanged.
type Props<TFieldValues extends FieldValues = FieldValues> = {
control: Control<TFieldValues>;
type: "text" | "password";
name: FieldPath<TFieldValues>;
label: string;
description?: string | React.ReactNode;
placeholder: string;
error: boolean;
required: boolean;
};
export type TControllerInputFormField<TFieldValues extends FieldValues = FieldValues> = {
key: FieldPath<TFieldValues>;
type: "text" | "password";
label: string;
description?: string | React.ReactNode;
placeholder: string;
error: boolean;
required: boolean;
};
export function ControllerInput<TFieldValues extends FieldValues = FieldValues>(props: Props<TFieldValues>) {
const { name, control, type, label, description, placeholder, error, required } = props;
// states
const [showPassword, setShowPassword] = useState(false);
return (
<div className="flex flex-col gap-1">
<h4 className="text-13 text-tertiary">{label}</h4>
<InputGroup size="lg">
<Controller
control={control}
name={name}
rules={{ required: required ? `${label} is required.` : false }}
render={({ field: { value, onChange, ref } }) => (
<Input
size="lg"
id={name}
name={name}
type={type === "password" && showPassword ? "text" : type}
value={value}
onChange={onChange}
ref={ref}
aria-invalid={error}
placeholder={placeholder}
/>
)}
/>
{type === "password" &&
(showPassword ? (
<button
type="button"
aria-label="Hide password"
className="flex items-center justify-center text-placeholder"
onClick={() => setShowPassword(false)}
>
<HideOutline className="h-4 w-4" />
</button>
) : (
<button
type="button"
aria-label="Show password"
className="flex items-center justify-center text-placeholder"
onClick={() => setShowPassword(true)}
>
<ShowOutline className="h-4 w-4" />
</button>
))}
</InputGroup>
{description && <p className="pt-0.5 text-11 text-tertiary">{description}</p>}
</div>
);
}

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import type { Control, FieldPath, FieldValues } from "react-hook-form";
import { Controller } from "react-hook-form";
// plane internal packages
import { Switch } from "@makeplane/propel/components/switch";
type Props<T extends FieldValues = FieldValues> = {
control: Control<T>;
field: TControllerSwitchFormField<T>;
};
export type TControllerSwitchFormField<T extends FieldValues = FieldValues> = {
name: FieldPath<T>;
label: string;
};
export function ControllerSwitch<T extends FieldValues>(props: Props<T>) {
const {
control,
field: { name, label },
} = props;
return (
<div className="flex items-center justify-between gap-1">
<h4 className="text-sm text-custom-text-300">Refresh user attributes from {label} during sign in</h4>
<div className="relative">
<Controller
control={control}
name={name as FieldPath<T>}
render={({ field: { value, onChange } }) => {
const parsedValue = Number.parseInt(typeof value === "string" ? value : String(value ?? "0"), 10);
const isOn = !Number.isNaN(parsedValue) && parsedValue !== 0;
return <Switch checked={isOn} onCheckedChange={() => onChange(isOn ? "0" : "1")} size="sm" />;
}}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,52 @@
/**
* 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";
// ui
import { Button } from "@makeplane/propel/components/button";
import { CopyOutline } from "@makeplane/propel/icons";
// components
import { TOAST_TYPE, setToast } from "@/providers/toast";
type Props = {
label: string;
url: string;
description: string | React.ReactNode;
};
export type TCopyField = {
key: string;
label: string;
url: string;
description: string | React.ReactNode;
};
export function CopyField(props: Props) {
const { label, url, description } = props;
return (
<div className="flex flex-col gap-1">
<h4 className="text-13 text-secondary">{label}</h4>
<Button
variant="secondary"
size="md"
stretch="auto"
icon={<CopyOutline width={18} height={18} color="#B9B9B9" />}
iconPosition="end"
label={url}
onClick={() => {
navigator.clipboard.writeText(url);
setToast({
type: TOAST_TYPE.INFO,
title: "Copied to clipboard",
message: `The ${label} has been successfully copied to your clipboard`,
});
}}
/>
<div className="text-11 text-tertiary">{description}</div>
</div>
);
}

View File

@@ -0,0 +1,19 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
export const CORE_HEADER_SEGMENT_LABELS: Record<string, string> = {
general: "General",
ai: "Artificial Intelligence",
email: "Email",
authentication: "Authentication",
image: "Image",
google: "Google",
github: "GitHub",
gitlab: "GitLab",
gitea: "Gitea",
workspace: "Workspace",
create: "Create",
};

View File

@@ -0,0 +1,7 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
export const EXTENDED_HEADER_SEGMENT_LABELS: Record<string, string> = {};

View File

@@ -0,0 +1,99 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* 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 } from "lucide-react";
import { SettingsOutline } from "@makeplane/propel/icons";
// plane internal packages
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbSeparator,
} from "@makeplane/propel/components/breadcrumb";
// hooks
import { useTheme } from "@/hooks/store";
// local imports
import { CORE_HEADER_SEGMENT_LABELS } from "./core";
import { EXTENDED_HEADER_SEGMENT_LABELS } from "./extended";
export const HamburgerToggle = observer(function HamburgerToggle() {
const { isSidebarCollapsed, toggleSidebar } = useTheme();
return (
<button
type="button"
aria-label="Toggle sidebar"
className="group flex size-7 cursor-pointer items-center justify-center rounded-sm bg-layer-1 transition-all hover:bg-layer-1-hover md:hidden"
onClick={() => toggleSidebar(!isSidebarCollapsed)}
>
<Menu size={14} className="text-secondary transition-all group-hover:text-primary" />
</button>
);
});
const HEADER_SEGMENT_LABELS = {
...CORE_HEADER_SEGMENT_LABELS,
...EXTENDED_HEADER_SEGMENT_LABELS,
};
// Function to dynamically generate breadcrumb items based on pathname
const generateBreadcrumbItems = (pathname: string) => {
const pathSegments = pathname.split("/").slice(1); // removing the first empty string.
pathSegments.pop();
let currentUrl = "";
const breadcrumbItems = pathSegments.map((segment) => {
currentUrl += "/" + segment;
return {
title: HEADER_SEGMENT_LABELS[segment] ?? segment.toUpperCase(),
href: currentUrl,
};
});
return breadcrumbItems;
};
export const AdminHeader = observer(function AdminHeader() {
const pathName = usePathname();
const breadcrumbItems = generateBreadcrumbItems(pathName || "");
return (
<div className="relative z-10 flex h-header w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-subtle bg-surface-1 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<HamburgerToggle />
<div>
<Breadcrumb aria-label="Breadcrumb">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink
label="Settings"
icon={<SettingsOutline className="h-4 w-4 text-tertiary" />}
render={<Link href="/general/" />}
/>
</BreadcrumbItem>
{breadcrumbItems.map(
(item) =>
item.title && (
<Fragment key={item.title}>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink label={item.title} render={<Link href={item.href} />} />
</BreadcrumbItem>
</Fragment>
)
)}
</BreadcrumbList>
</Breadcrumb>
</div>
</div>
</div>
);
});

View File

@@ -0,0 +1,21 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useTheme } from "next-themes";
import LogoSpinnerDark from "@/app/assets/images/logo-spinner-dark.gif?url";
import LogoSpinnerLight from "@/app/assets/images/logo-spinner-light.gif?url";
export function LogoSpinner() {
const { resolvedTheme } = useTheme();
const logoSrc = resolvedTheme === "dark" ? LogoSpinnerLight : LogoSpinnerDark;
return (
<div className="flex items-center justify-center">
<img src={logoSrc} alt="logo" className="h-6 w-auto sm:h-11" />
</div>
);
}

View File

@@ -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 { observer } from "mobx-react";
import Link from "next/link";
import { useTheme as useNextTheme } from "next-themes";
// ui
import { Button } from "@makeplane/propel/components/button";
import { resolveGeneralTheme } from "@plane/utils";
// hooks
import TakeoffIconDark from "@/app/assets/logos/takeoff-icon-dark.svg?url";
import TakeoffIconLight from "@/app/assets/logos/takeoff-icon-light.svg?url";
import { useTheme } from "@/hooks/store";
// icons
export const NewUserPopup = observer(function NewUserPopup() {
// hooks
const { isNewUserPopup, toggleNewUserPopup } = useTheme();
// theme
const { resolvedTheme } = useNextTheme();
if (!isNewUserPopup) return <></>;
return (
<div className="shadow-md absolute right-8 bottom-8 w-96 rounded-lg border border-subtle bg-surface-1 p-6">
<div className="flex gap-4">
<div className="grow">
<div className="text-14 font-semibold">Create workspace</div>
<div className="py-2 text-13 font-medium text-tertiary">
Instance setup done! Welcome to Plane instance portal. Start your journey with by creating your first
workspace.
</div>
<div className="flex items-center gap-4 pt-2">
<Button
variant="primary"
size="md"
stretch="auto"
nativeButton={false}
render={<Link href="/workspace/create" />}
label="Create workspace"
/>
<Button variant="secondary" size="md" stretch="auto" onClick={toggleNewUserPopup} label="Close" />
</div>
</div>
<div className="flex shrink-0 items-center justify-center">
<img
src={resolveGeneralTheme(resolvedTheme) === "dark" ? TakeoffIconDark : TakeoffIconLight}
height={80}
width={80}
alt="Plane icon"
/>
</div>
</div>
</div>
);
});

View File

@@ -0,0 +1,50 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import type { ReactNode } from "react";
// plane imports
import { cn } from "@plane/utils";
type TPageWrapperProps = {
children: ReactNode;
header?: {
title: string;
description: string | ReactNode;
actions?: ReactNode;
};
customHeader?: ReactNode;
size?: "lg" | "md";
};
export const PageWrapper = (props: TPageWrapperProps) => {
const { children, header, customHeader, size = "md" } = props;
return (
<div
className={cn("mx-auto h-full w-full space-y-6 py-4", {
"max-w-[1000px] md:px-4 2xl:max-w-[1200px]": size === "md",
"px-4 lg:px-12": size === "lg",
})}
>
{customHeader ? (
<div className="mx-4 shrink-0 space-y-1 border-b border-subtle py-4">{customHeader}</div>
) : (
header && (
<div className="mx-4 flex shrink-0 items-center justify-between gap-4 space-y-1 border-b border-subtle py-4">
<div className={header.actions ? "flex flex-col gap-1" : "space-y-1"}>
<div className="text-h5-semibold text-primary">{header.title}</div>
<div className="text-body-sm-regular text-secondary">{header.description}</div>
</div>
{header.actions && <div className="shrink-0">{header.actions}</div>}
</div>
)
)}
<div className="vertical-scrollbar scrollbar-sm flex-grow overflow-hidden overflow-y-scroll px-4 pb-4">
{children}
</div>
</div>
);
};

View File

@@ -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 { TickCircleOutline } from "@makeplane/propel/icons";
// 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 (
<div className={cn("space-y-3")}>
{/* Strength Indicator */}
<div className="space-y-2">
<div className="flex w-full gap-1 transition-all duration-300 ease-linear">
{[0, 1, 2].map((fragmentIndex) => (
<div
key={fragmentIndex}
className={cn(
"h-1 flex-1 rounded-xs transition-all duration-300 ease-in-out",
getFragmentColor(fragmentIndex, strengthInfo.activeFragments)
)}
/>
))}
</div>
{/* Strength Message */}
{password && <p className={cn("!text-13 font-medium", strengthInfo.textColor)}>{strengthInfo.message}</p>}
</div>
{/* Criteria list */}
{showCriteria && (
<div className="flex flex-wrap gap-2">
{criteria.map((criterion) => (
<div key={criterion.key} className="flex items-center gap-1.5">
<div className="flex items-center justify-center p-0.5">
<TickCircleOutline
className={cn("h-3 w-3 flex-shrink-0", {
"text-success-primary": criterion.isValid,
"text-primary": !criterion.isValid,
})}
/>
</div>
<span
className={cn("!text-11", {
"text-success-primary": criterion.isValid,
"text-primary": !criterion.isValid,
})}
>
{criterion.label}
</span>
</div>
))}
</div>
)}
</div>
);
}

View File

@@ -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 (
<svg
width={width}
height={height}
viewBox="0 0 253 53"
fill={color}
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<g clipPath="url(#clip0_27_76)">
<path
d="M217.077 51.4468H210.972L210.98 31.4317C210.717 27.7545 208.786 25.1545 205.03 24.6199C198.587 23.7006 194.663 27.8828 194.21 34.0258L194.218 51.4468H188.041V20.2543H192.158L194.255 26.8846H194.6C195.825 22.8701 198.684 20.4831 202.898 19.5757C210.126 18.0231 216.643 22.2724 217.079 29.8239V51.4468H217.077Z"
fill={color}
/>
<path
d="M163.812 19.7315C158.03 20.9566 153.67 25.2354 153.222 31.264H159.395C159.669 28.3484 161.561 25.9969 164.267 24.9553C167.418 23.7401 172.805 23.8881 175.178 26.6005C176.774 28.4253 177.305 31.5816 175.212 33.2249C172.675 35.2173 166.39 34.6117 163.214 34.7636C158.631 34.9826 154.141 36.8429 153.183 41.7727C151.1 52.5042 165.035 54.9267 171.902 49.8963C174.095 48.2885 175.672 45.8443 176.261 43.1792H176.577L178.788 51.4508H182.61V29.964C182.517 20.3214 171.462 18.108 163.81 19.7315H163.812ZM174.847 41.2637C172.484 45.649 166.958 49.0441 162.218 47.3318C158.44 45.9686 158.268 40.4095 161.973 38.9181C165.613 37.4524 173.732 38.9694 176.368 34.949C176.275 37.0855 175.906 39.2989 174.849 41.2637H174.847Z"
fill={color}
/>
<path
d="M113.082 35.5652V51.4475H106.611V9.00063H124.863C125.007 8.99274 125.141 9.06178 125.283 9.06967C133.732 9.53129 139.297 13.601 139.104 22.6025C138.928 30.7991 132.663 35.2199 124.934 35.5671H113.082V35.5652ZM113.082 14.4828V30.2941H124.088C129.595 29.8956 133.071 27.1022 132.491 21.265C131.967 15.9781 127.802 14.6544 123.107 14.4788C119.795 14.3546 116.402 14.5795 113.082 14.4808V14.4828Z"
fill={color}
/>
<path d="M148.647 9H142.529V51.4468H148.647V9Z" fill={color} />
<path
d="M245.872 42.418C244.911 44.4953 243.138 46.1603 240.934 46.8981C238.368 47.7562 234.479 47.5431 232.083 46.2569C228.67 44.4263 227.145 40.6485 227.137 36.8984H251.891C252.194 33.6908 251.902 30.3648 250.496 27.4333C247.083 20.3177 238.859 17.7236 231.513 19.9429C222.868 22.5568 220.165 31.4162 221.462 39.6543C222.86 48.5611 230.197 52.976 238.958 52.3585C244.884 51.9423 250.271 48.1192 251.788 42.4161H245.874L245.872 42.418ZM227.208 32.4381C227.778 26.2457 233.347 23.2018 239.163 24.4269C243.361 25.3107 245.75 28.9523 245.842 33.1068H227.145C227.145 33.1068 227.19 32.6255 227.208 32.4401V32.4381Z"
fill={color}
/>
<path
d="M44.3223 2.9264C44.3223 0.754665 46.6083 -0.65811 48.5508 0.313121L80.4551 16.2653C82.9294 17.5024 84.4922 20.0321 84.4922 22.7985V48.2487C84.4922 50.4204 82.2071 51.833 80.2646 50.8619L62.3281 41.8932V22.7975C62.3281 20.0311 60.7653 17.5015 58.291 16.2643L44.3223 9.27992V2.9264ZM0 2.92543C8.01645e-05 0.753753 2.28609 -0.659069 4.22852 0.312144L22.1582 9.27699V28.3766C22.1582 31.1428 23.7213 33.6716 26.1953 34.9088L40.1699 41.8952V48.2487C40.1697 50.4202 37.8847 51.832 35.9424 50.861L4.03711 34.9088C1.56305 33.6716 0 31.1428 0 28.3766V2.92543ZM22.1582 2.92543C22.1583 0.753753 24.4443 -0.659069 26.3867 0.312144L44.3223 9.27992V28.3776C44.3223 31.1439 45.8861 33.6727 48.3604 34.9098L62.3281 41.8932V48.2487C62.3279 50.4202 60.0429 51.832 58.1006 50.861L40.1699 41.8952V22.7975C40.1699 20.0311 38.6071 17.5015 36.1328 16.2643L22.1582 9.27699V2.92543Z"
fill={color}
/>
</g>
<defs>
<clipPath id="clip0_27_76">
<rect width="252" height="53" fill="white" />
</clipPath>
</defs>
</svg>
);
}

View File

@@ -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 (
<div data-slot="skeleton" className={cn("animate-pulse", className)} role="status" aria-label={ariaLabel}>
{children}
</div>
);
}
type ItemProps = {
height?: string;
width?: string;
className?: string;
};
function SkeletonItem({ height = "auto", width = "auto", className = "" }: ItemProps) {
return <div data-slot="skeleton-item" className={cn("rounded-md bg-layer-1", className)} style={{ height, width }} />;
}
const Skeleton = Object.assign(SkeletonRoot, { Item: SkeletonItem });
SkeletonRoot.displayName = "plane-ui-skeleton";
SkeletonItem.displayName = "plane-ui-skeleton-item";
export { Skeleton };