Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions packages/propel/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,29 @@ module.exports = {
root: true,
extends: ["@plane/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
rules: {
"import/order": [
"warn",
{
groups: ["builtin", "external", "internal", "parent", "sibling"],
pathGroups: [
{
pattern: "react",
group: "external",
position: "before",
},
{
pattern: "@plane/**",
group: "external",
position: "after",
},
],
pathGroupsExcludedImportTypes: ["builtin", "internal", "react"],
alphabetize: {
order: "asc",
caseInsensitive: true,
},
},
],
},
};
8 changes: 6 additions & 2 deletions packages/propel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@
"clean": "rm -rf .turbo && rm -rf .next && rm -rf node_modules && rm -rf dist"
},
"exports": {
"./ui/*": "./src/ui/*.tsx",
"./avatar": "./src/avatar/index.ts",
"./charts/*": "./src/charts/*/index.ts",
"./dialog": "./src/dialog/index.ts",
"./menu": "./src/menu/index.ts",
"./table": "./src/table/index.ts",
"./tabs": "./src/tabs/index.ts",
"./styles/fonts": "./src/styles/fonts/index.css"
},
"dependencies": {
"@radix-ui/react-slot": "^1.1.1",
"@base-ui-components/react": "^1.0.0-beta.2",
"@plane/utils": "*",
"@tanstack/react-table": "^8.21.3",
"class-variance-authority": "^0.7.1",
"lucide-react": "^0.469.0",
Expand Down
121 changes: 121 additions & 0 deletions packages/propel/src/avatar/avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import React from "react";
import { Avatar as AvatarPrimitive } from "@base-ui-components/react/avatar";
// utils
import { cn } from "@plane/utils";

export type TAvatarSize = "sm" | "md" | "base" | "lg" | number;

type Props = {
name?: string; //The name of the avatar which will be displayed on the tooltip
fallbackBackgroundColor?: string; //The background color if the avatar image fails to load
fallbackText?: string;
fallbackTextColor?: string; //The text color if the avatar image fails to load
showTooltip?: boolean;
size?: TAvatarSize; //The size of the avatars
shape?: "circle" | "square";
src?: string; //The source of the avatar image
className?: string;
};

/**
* Get the size details based on the size prop
* @param size The size of the avatar
* @returns The size details
*/
export const getSizeInfo = (size: TAvatarSize) => {
switch (size) {
case "sm":
return {
avatarSize: "h-4 w-4",
fontSize: "text-xs",
spacing: "-space-x-1",
};
case "md":
return {
avatarSize: "h-5 w-5",
fontSize: "text-xs",
spacing: "-space-x-1",
};
case "base":
return {
avatarSize: "h-6 w-6",
fontSize: "text-sm",
spacing: "-space-x-1.5",
};
case "lg":
return {
avatarSize: "h-7 w-7",
fontSize: "text-sm",
spacing: "-space-x-1.5",
};
default:
return {
avatarSize: "h-5 w-5",
fontSize: "text-xs",
spacing: "-space-x-1",
};
}
};

/**
* Get the border radius based on the shape prop
* @param shape The shape of the avatar
* @returns The border radius
*/
export const getBorderRadius = (shape: "circle" | "square") => {
switch (shape) {
case "circle":
return "rounded-full";
case "square":
return "rounded";
default:
return "rounded-full";
}
};

/**
* Check if the value is a valid number
* @param value The value to check
* @returns Whether the value is a valid number or not
*/
export const isAValidNumber = (value: any) => typeof value === "number" && !isNaN(value);

export const Avatar: React.FC<Props> = (props) => {
const {
name,
fallbackBackgroundColor,
fallbackText,
fallbackTextColor,
showTooltip = true,
size = "md",
shape = "circle",
src,
className = "",
} = props;

// get size details based on the size prop
const sizeInfo = getSizeInfo(size);

const fallbackLetter = name?.[0]?.toUpperCase() ?? fallbackText ?? "?";
return (
<div
className={cn("grid place-items-center overflow-hidden", getBorderRadius(shape), {
[sizeInfo.avatarSize]: !isAValidNumber(size),
})}
tabIndex={-1}
>
<AvatarPrimitive.Root className={cn("h-full w-full", getBorderRadius(shape), className)}>
<AvatarPrimitive.Image src={src} width="48" height="48" />
<AvatarPrimitive.Fallback
className={cn(sizeInfo.fontSize, "grid h-full w-full place-items-center", getBorderRadius(shape), className)}
style={{
backgroundColor: fallbackBackgroundColor ?? "rgba(var(--color-primary-500))",
color: fallbackTextColor ?? "#ffffff",
}}
>
{fallbackLetter}
</AvatarPrimitive.Fallback>
</AvatarPrimitive.Root>
</div>
);
};
1 change: 1 addition & 0 deletions packages/propel/src/avatar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./avatar";
17 changes: 17 additions & 0 deletions packages/propel/src/dialog/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export enum EDialogPosition {
TOP = "flex items-center justify-center text-center mx-4 my-10 md:my-20",
CENTER = "flex items-end sm:items-center justify-center p-4 min-h-full",
}

export enum EDialogWidth {
SM = "sm:max-w-sm",
MD = "sm:max-w-md",
LG = "sm:max-w-lg",
XL = "sm:max-w-xl",
XXL = "sm:max-w-2xl",
XXXL = "sm:max-w-3xl",
XXXXL = "sm:max-w-4xl",
VXL = "sm:max-w-5xl",
VIXL = "sm:max-w-6xl",
VIIXL = "sm:max-w-7xl",
}
1 change: 1 addition & 0 deletions packages/propel/src/dialog/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./root";
77 changes: 77 additions & 0 deletions packages/propel/src/dialog/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"use client";

import * as React from "react";
import { Dialog as BaseDialog } from "@base-ui-components/react";
import { cn } from "@plane/utils";
import { EDialogWidth } from "./constants";

function DialogPortal({ ...props }: React.ComponentProps<typeof BaseDialog.Portal>) {
return <BaseDialog.Portal data-slot="dialog-portal" {...props} />;
}

function DialogOverlay({ className, ...props }: React.ComponentProps<typeof BaseDialog.Backdrop>) {
return (
<BaseDialog.Backdrop
data-slot="dialog-overlay"
className={cn(
"fixed inset-0 z-30 bg-custom-backdrop transition-all duration-200 [&[data-ending-style]]:opacity-0 [&[data-starting-style]]:opacity-0",
className
)}
{...props}
/>
);
}

function Dialog({ ...props }: React.ComponentProps<typeof BaseDialog.Root>) {
return <BaseDialog.Root data-slot="dialog" {...props} />;
}

function DialogTrigger({ ...props }: React.ComponentProps<typeof BaseDialog.Trigger>) {
return <BaseDialog.Trigger data-slot="dialog-trigger" {...props} />;
}

function DialogPanel({
className,
width = EDialogWidth.XXL,
children,
...props
}: React.ComponentProps<typeof BaseDialog.Popup> & { width?: EDialogWidth }) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<BaseDialog.Popup
data-slot="dialog-content"
className={cn(
"fixed flex justify-center top-0 left-0 w-full z-30 px-4 sm:py-20 overflow-y-auto overflow-hidden outline-none"
)}
{...props}
>
<div
className={cn(
"rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all w-full",
width,
className
)}
>
{children}
</div>
</BaseDialog.Popup>
</DialogPortal>
);
}

function DialogTitle({ className, ...props }: React.ComponentProps<typeof BaseDialog.Title>) {
return (
<BaseDialog.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
);
}
// compound components
Dialog.Trigger = DialogTrigger;
Dialog.Panel = DialogPanel;
Dialog.Title = DialogTitle;

export { Dialog, DialogTitle, DialogTrigger, DialogPanel };
2 changes: 2 additions & 0 deletions packages/propel/src/menu/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./menu";
export * from "./types";
Loading
Loading