Add risks

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Jonathan
2025-05-20 18:49:47 +02:00
committed by Sacha Al Himdani
parent a2d1310780
commit a86181c83d
41 changed files with 4533 additions and 1304 deletions

View File

@@ -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) {

View 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",
},
],
},
};

View 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>;
}

View 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",
},
};

View 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>
);
}

View File

@@ -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}

View File

@@ -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>
);

View 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",
},
};

View 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}
/>
);
}

View 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;
}

View File

@@ -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>
);
}

View 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",
},
};

View 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>
);
}

View File

@@ -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">

View 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>
);
},
};

View 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>
);
}

View File

@@ -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";

View File

@@ -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";