Add risks
Signed-off-by: Bryan Frimin <bryan@getprobo.com> Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
committed by
Sacha Al Himdani
parent
a2d1310780
commit
a86181c83d
@@ -1,18 +1,19 @@
|
||||
import { tv } from "tailwind-variants";
|
||||
|
||||
type Props = {
|
||||
fullName: string;
|
||||
name: string;
|
||||
src?: string | null;
|
||||
size?: "s" | "m" | "l" | "xl";
|
||||
};
|
||||
|
||||
const avatar = tv({
|
||||
base: "bg-txt-success text-sm text-invert rounded-full text-xxs font-semibold flex items-center justify-center",
|
||||
base: "bg-txt-success text-txt-invert rounded-full font-semibold flex items-center justify-center",
|
||||
variants: {
|
||||
size: {
|
||||
s: "size-5",
|
||||
m: "size-6",
|
||||
l: "size-8",
|
||||
xl: "size-16",
|
||||
s: "size-5 text-xss",
|
||||
m: "size-6 text-xxs",
|
||||
l: "size-8 text-sm",
|
||||
xl: "size-16 text-3xl",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -20,8 +21,13 @@ const avatar = tv({
|
||||
},
|
||||
});
|
||||
|
||||
export function Avatar({ fullName, size = "m" }: Props) {
|
||||
return <div className={avatar({ size })}>{extractInitials(fullName)}</div>;
|
||||
export function Avatar({ name, src, size = "m" }: Props) {
|
||||
if (src) {
|
||||
return <img className={avatar({ size })} src={src} alt={name} />;
|
||||
}
|
||||
return (
|
||||
<div className={avatar({ size })}>{extractInitials(name ?? "")}</div>
|
||||
);
|
||||
}
|
||||
|
||||
function extractInitials(name: string) {
|
||||
|
||||
29
packages/ui/src/Atoms/Breadcrumb/Breadcrumb.stories.tsx
Normal file
29
packages/ui/src/Atoms/Breadcrumb/Breadcrumb.stories.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Breadcrumb } from "./Breadcrumb";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
export default {
|
||||
title: "Atoms/Breadcrumb",
|
||||
component: Breadcrumb,
|
||||
argTypes: {},
|
||||
} satisfies Meta<typeof Breadcrumb>;
|
||||
|
||||
type Story = StoryObj<typeof Breadcrumb>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{
|
||||
label: "Home",
|
||||
to: "/",
|
||||
},
|
||||
{
|
||||
label: "Library",
|
||||
to: "/library",
|
||||
},
|
||||
{
|
||||
label: "Data",
|
||||
to: "/data",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
51
packages/ui/src/Atoms/Breadcrumb/Breadcrumb.tsx
Normal file
51
packages/ui/src/Atoms/Breadcrumb/Breadcrumb.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import clsx from "clsx";
|
||||
import { Link } from "react-router";
|
||||
import { IconChevronRight } from "../Icons";
|
||||
|
||||
type Props = {
|
||||
items: (ItemProps | string)[];
|
||||
};
|
||||
|
||||
type ItemProps = {
|
||||
label: string;
|
||||
to?: string;
|
||||
active?: boolean;
|
||||
};
|
||||
|
||||
export function Breadcrumb({ items }: Props) {
|
||||
return (
|
||||
<div className="flex items-center gap-[6px] text-txt-tertiary">
|
||||
{items.map((item, index) => (
|
||||
<>
|
||||
{index > 0 && <IconChevronRight size={12} />}
|
||||
<BreadcrumbItem
|
||||
key={index}
|
||||
{...(typeof item === "string" ? { label: item } : item)}
|
||||
active={index === items.length - 1}
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbItem({ label, to, active }: ItemProps) {
|
||||
const className = clsx(
|
||||
"text-sm px-1 rounded-sm h-5",
|
||||
active && "text-txt-primary font-medium",
|
||||
);
|
||||
if (to) {
|
||||
return (
|
||||
<Link
|
||||
to={to}
|
||||
className={clsx(
|
||||
className,
|
||||
"hover:bg-tertiary-hover active:bg-tertiary-pressed",
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return <span className={className}>{label}</span>;
|
||||
}
|
||||
17
packages/ui/src/Atoms/Card/Card.stories.tsx
Normal file
17
packages/ui/src/Atoms/Card/Card.stories.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Card } from "./Card";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
export default {
|
||||
title: "Atoms/Card",
|
||||
component: Card,
|
||||
argTypes: {},
|
||||
} satisfies Meta<typeof Card>;
|
||||
|
||||
type Story = StoryObj<typeof Card>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
padded: true,
|
||||
children: "lorem ipsum",
|
||||
},
|
||||
};
|
||||
27
packages/ui/src/Atoms/Card/Card.tsx
Normal file
27
packages/ui/src/Atoms/Card/Card.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { tv } from "tailwind-variants";
|
||||
import { Slot } from "../Slot";
|
||||
|
||||
type Props = PropsWithChildren<{
|
||||
padded?: boolean;
|
||||
className?: string;
|
||||
asChild?: boolean;
|
||||
}>;
|
||||
|
||||
const card = tv({
|
||||
base: "border border-border-low surface-1 rounded-2xl",
|
||||
variants: {
|
||||
padded: {
|
||||
true: "p-6",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function Card({ padded = false, children, className, asChild }: Props) {
|
||||
const Component = asChild ? Slot : "div";
|
||||
return (
|
||||
<Component className={card({ padded, className })}>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export function Dropdown({ children, toggle, className }: Props) {
|
||||
<DropdownMenu.Content
|
||||
className={clsx(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
||||
"z-50 p-2 shadow-mid min-w-[8rem] bg-surface-1 overflow-y-auto overflow-x-hidden rounded-2xl border-border-low",
|
||||
"z-50 p-2 shadow-mid min-w-[8rem] bg-level-1 overflow-y-auto overflow-x-hidden rounded-2xl border-border-low",
|
||||
className,
|
||||
)}
|
||||
sideOffset={5}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "react";
|
||||
import { clsx } from "clsx";
|
||||
import { IconCollapse, IconExpand } from "../Icons";
|
||||
import { Button } from "../Button/Button";
|
||||
|
||||
const sidebarContext = createContext({ open: true });
|
||||
|
||||
@@ -15,21 +16,17 @@ export function Sidebar({ children }: PropsWithChildren) {
|
||||
<sidebarContext.Provider value={{ open }}>
|
||||
<aside
|
||||
className={clsx(
|
||||
"border-r border-border-solid relative pt-12 flex-none",
|
||||
"border-r border-border-solid relative pt-16 flex-none",
|
||||
open ? "px-4 w-[260px]" : "px-2",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<button
|
||||
<Button
|
||||
variant="tertiary"
|
||||
icon={open ? IconCollapse : IconExpand}
|
||||
onClick={() => setOpen(!open)}
|
||||
className="cursor-pointer absolute bottom-4 left-4"
|
||||
>
|
||||
{open ? (
|
||||
<IconCollapse size={16} />
|
||||
) : (
|
||||
<IconExpand size={16} />
|
||||
)}
|
||||
</button>
|
||||
className="absolute bottom-4 left-4"
|
||||
/>
|
||||
</aside>
|
||||
</sidebarContext.Provider>
|
||||
);
|
||||
|
||||
16
packages/ui/src/Atoms/Skeleton/Skeleton.stories.tsx
Normal file
16
packages/ui/src/Atoms/Skeleton/Skeleton.stories.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Skeleton } from "./Skeleton";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
export default {
|
||||
title: "Atoms/Skeleton",
|
||||
component: Skeleton,
|
||||
argTypes: {},
|
||||
} satisfies Meta<typeof Skeleton>;
|
||||
|
||||
type Story = StoryObj<typeof Skeleton>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
className: "w-50 h-4",
|
||||
},
|
||||
};
|
||||
16
packages/ui/src/Atoms/Skeleton/Skeleton.tsx
Normal file
16
packages/ui/src/Atoms/Skeleton/Skeleton.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { clsx } from "clsx";
|
||||
|
||||
export function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"animate-pulse rounded-md bg-border-low",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
29
packages/ui/src/Atoms/Slot.tsx
Normal file
29
packages/ui/src/Atoms/Slot.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
import { cloneElement, isValidElement, Children } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export type AsChildProps<DefaultElementProps> =
|
||||
| ({ asChild?: false } & DefaultElementProps)
|
||||
| { asChild: true; children: ReactNode };
|
||||
|
||||
export function Slot({
|
||||
children,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLElement> & {
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
if (isValidElement(children)) {
|
||||
return cloneElement(children, {
|
||||
...props,
|
||||
// @ts-expect-error children.props can be undefined it's ok
|
||||
...children.props,
|
||||
// @ts-expect-error children.props.className can be undefined it's ok
|
||||
className: clsx(props.className, children.props.className),
|
||||
});
|
||||
}
|
||||
if (Children.count(children) > 1) {
|
||||
Children.only(null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
import logo from "../assets/android-chrome-512x512.png";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Outlet } from "react-router";
|
||||
import { Toasts } from "../Atoms/Toasts/Toasts";
|
||||
|
||||
export function AuthLayout() {
|
||||
const { __ } = useTranslate();
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 min-h-screen">
|
||||
<div className="bg-surface-0 flex flex-col items-center justify-center">
|
||||
<div className="bg-level-0 flex flex-col items-center justify-center">
|
||||
<div className="max-w-112">
|
||||
<Outlet />
|
||||
</div>
|
||||
@@ -21,7 +20,6 @@ export function AuthLayout() {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Toasts />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
16
packages/ui/src/Layouts/CenteredLayout.stories.tsx
Normal file
16
packages/ui/src/Layouts/CenteredLayout.stories.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { CenteredLayout } from "./CenteredLayout";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
export default {
|
||||
title: "Layouts/CenteredLayout",
|
||||
component: CenteredLayout,
|
||||
argTypes: {},
|
||||
} satisfies Meta<typeof CenteredLayout>;
|
||||
|
||||
type Story = StoryObj<typeof CenteredLayout>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: "lorem ipsum",
|
||||
},
|
||||
};
|
||||
24
packages/ui/src/Layouts/CenteredLayout.tsx
Normal file
24
packages/ui/src/Layouts/CenteredLayout.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { Outlet } from "react-router";
|
||||
import { Skeleton } from "../Atoms/Skeleton/Skeleton";
|
||||
|
||||
export function CenteredLayout({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<div className="grid place-items-center h-screen">
|
||||
<div className="w-full max-w-2xl flex flex-col items-center">
|
||||
{children ?? <Outlet />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CenteredLayoutSkeleton() {
|
||||
return (
|
||||
<CenteredLayout>
|
||||
<div className="w-full max-w-2xl flex flex-col items-center space-y-6">
|
||||
<Skeleton className="w-77 h-9" />
|
||||
<Skeleton className="w-full h-20" />
|
||||
</div>
|
||||
</CenteredLayout>
|
||||
);
|
||||
}
|
||||
@@ -11,8 +11,18 @@ type Props = PropsWithChildren<{
|
||||
export function Layout({ header, sidebar, children }: Props) {
|
||||
return (
|
||||
<div>
|
||||
<header className="absolute left-0 right-0 px-4 flex items-center border-b border-border-solid h-12">
|
||||
<header className="absolute z-2 left-0 right-0 px-4 flex items-center border-b border-border-solid h-12 bg-level-1">
|
||||
<Logo className="w-12 h-5" />
|
||||
<svg
|
||||
className="mx-3"
|
||||
width="8"
|
||||
height="18"
|
||||
viewBox="0 0 8 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M1 17L7 1" stroke="#C3C8C2" />
|
||||
</svg>
|
||||
{header}
|
||||
</header>
|
||||
<div className="flex h-screen">
|
||||
|
||||
37
packages/ui/src/Molecules/Dialog/Dialog.stories.tsx
Normal file
37
packages/ui/src/Molecules/Dialog/Dialog.stories.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Button } from "../../Atoms/Button/Button";
|
||||
import { Dialog, DialogContent, DialogFooter } from "./Dialog";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
export default {
|
||||
title: "Atoms/Dialog",
|
||||
component: Dialog,
|
||||
argTypes: {},
|
||||
} satisfies Meta<typeof Dialog>;
|
||||
|
||||
type Story = StoryObj<typeof Dialog>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: (args) => {
|
||||
return (
|
||||
<Dialog
|
||||
{...args}
|
||||
onClose={() => {}}
|
||||
trigger={<Button>Open dialog</Button>}
|
||||
title="Edit profile"
|
||||
>
|
||||
<DialogContent>
|
||||
<div>
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit.
|
||||
Voluptate, voluptas. Doloribus incidunt cum laboriosam
|
||||
nulla magni soluta voluptatum omnis, sapiente minus
|
||||
corporis impedit explicabo vero praesentium, fugit
|
||||
possimus facilis rem.
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<Button>Save</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
);
|
||||
},
|
||||
};
|
||||
72
packages/ui/src/Molecules/Dialog/Dialog.tsx
Normal file
72
packages/ui/src/Molecules/Dialog/Dialog.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
Root,
|
||||
Trigger,
|
||||
Portal,
|
||||
Overlay,
|
||||
Content,
|
||||
Title,
|
||||
Close,
|
||||
} from "@radix-ui/react-dialog";
|
||||
import { IconCrossLargeX } from "../../Atoms/Icons";
|
||||
import type { ReactNode } from "react";
|
||||
import { Button } from "../../Atoms/Button/Button";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
trigger: ReactNode;
|
||||
title: ReactNode;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export function Dialog({ onClose, trigger, title, children }: Props) {
|
||||
return (
|
||||
<Root>
|
||||
<Trigger asChild>{trigger}</Trigger>
|
||||
<Portal>
|
||||
<Overlay className="fixed inset-0 z-50 bg-dialog/40 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
|
||||
<div className="fixed grid place-items-center inset-0 z-50">
|
||||
<Content className="bg-level-2 rounded-2xl max-w-5xl w-[95%]">
|
||||
<div className="flex justify-between items-center p-3 border-b border-b-border-low">
|
||||
<Title className="text-sm font-medium">
|
||||
{title}
|
||||
</Title>
|
||||
<Close asChild>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
icon={IconCrossLargeX}
|
||||
/>
|
||||
</Close>
|
||||
</div>
|
||||
{children}
|
||||
</Content>
|
||||
</div>
|
||||
</Portal>
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
|
||||
export function DialogFooter({ children }: { children?: ReactNode }) {
|
||||
const { __ } = useTranslate();
|
||||
return (
|
||||
<footer className="flex justify-end items-center p-3 border-t border-t-border-low gap-2">
|
||||
<Close asChild>
|
||||
<Button variant="secondary">{__("Cancel")}</Button>
|
||||
</Close>
|
||||
{children}
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
export function DialogContent({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
className="overflow-y-auto"
|
||||
style={{
|
||||
maxHeight: "min(640px, calc(100vh - 140px))",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,12 +2,18 @@
|
||||
export { Layout } from "./Layouts/Layout";
|
||||
export { AuthLayout } from "./Layouts/AuthLayout";
|
||||
export { ErrorLayout } from "./Layouts/ErrorLayout";
|
||||
export {
|
||||
CenteredLayout,
|
||||
CenteredLayoutSkeleton,
|
||||
} from "./Layouts/CenteredLayout";
|
||||
|
||||
// Atoms
|
||||
export * from "./Atoms/Icons";
|
||||
export { Logo } from "./Atoms/Logo/Logo";
|
||||
export { SidebarItem } from "./Atoms/Sidebar/SidebarItem";
|
||||
export { Button } from "./Atoms/Button/Button";
|
||||
export { Card } from "./Atoms/Card/Card";
|
||||
export { Breadcrumb } from "./Atoms/Breadcrumb/Breadcrumb";
|
||||
export {
|
||||
Dropdown,
|
||||
DropdownSeparator,
|
||||
@@ -22,6 +28,8 @@ export {
|
||||
UserDropdownItem,
|
||||
} from "./Molecules/UserDropdown/UserDropdown";
|
||||
export { PageHeader } from "./Molecules/PageHeader/PageHeader";
|
||||
export { Skeleton } from "./Atoms/Skeleton/Skeleton";
|
||||
export { Dialog, DialogContent, DialogFooter } from "./Molecules/Dialog/Dialog";
|
||||
|
||||
// Hooks
|
||||
export { useToast } from "./Atoms/Toasts/Toasts";
|
||||
export { useToast, Toasts } from "./Atoms/Toasts/Toasts";
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
/* Text colors */
|
||||
--color-txt-primary: #141e12;
|
||||
--color-invert: #fcfdfc;
|
||||
--color-txt-invert: var(--color-invert);
|
||||
--color-txt-secondary: #6b716a;
|
||||
--color-txt-tertiary: #818780;
|
||||
--color-txt-quaternary: #d8dcd8;
|
||||
@@ -162,5 +163,3 @@
|
||||
0px 2px 4px 0px rgba(0, 0, 0, 0.08),
|
||||
0px 2px 12px 0px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
@import "tw-animate-css";
|
||||
|
||||
Reference in New Issue
Block a user