Add policies version history

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-27 10:12:56 +02:00
committed by Sacha Al Himdani
parent abe0ce1dde
commit 2e6741942b
36 changed files with 1710 additions and 883 deletions

View File

@@ -8,7 +8,7 @@ type Props = {
export function Avatar({ name, src, size = "m" }: Props) {
const className = clsx(
"bg-txt-success text-txt-invert rounded-full font-semibold flex items-center justify-center",
"bg-txt-success text-txt-invert rounded-full font-semibold flex items-center justify-center flex-none",
size === "s" && "size-5 text-xss",
size === "m" && "size-6 text-xxs",
size === "l" && "size-8 text-sm",

View File

@@ -62,6 +62,7 @@ export const Button = (props: Props) => {
iconAfter: IconAfterComponent,
children,
onClick,
variant,
...componentProps
} = props;
return (
@@ -69,7 +70,7 @@ export const Button = (props: Props) => {
<Component
{...componentProps}
onClick={onClick}
className={button({ ...props, empty: !children })}
className={button({ ...props, variant, empty: !children })}
>
{IconComponent && <IconComponent size={16} className="flex-none" />}
{children}

View File

@@ -0,0 +1,8 @@
import type {IconProps} from "./type.ts";
export function IconClock({size = 24, className}: IconProps) {
return <svg width={size} height={size} viewBox="0 0 16 16" fill="currentColor" className={className} xmlns="http://www.w3.org/2000/svg">
<path d="M13.3335 8.00065C13.3335 5.05513 10.9457 2.66732 8.00016 2.66732C5.05465 2.66732 2.66683 5.05513 2.66683 8.00065C2.66683 10.9462 5.05464 13.334 8.00016 13.334C10.9457 13.334 13.3335 10.9462 13.3335 8.00065ZM7.3335 5.33398C7.3335 4.96579 7.63197 4.66732 8.00016 4.66732C8.36835 4.66732 8.66683 4.96579 8.66683 5.33398V7.72461L10.1382 9.19596L10.1838 9.24675C10.3973 9.50859 10.3823 9.8946 10.1382 10.1387C9.89411 10.3827 9.50811 10.3978 9.24626 10.1842L9.19548 10.1387L7.52881 8.47201C7.40378 8.34698 7.3335 8.17746 7.3335 8.00065V5.33398ZM14.6668 8.00065C14.6668 11.6826 11.6821 14.6673 8.00016 14.6673C4.31827 14.6673 1.3335 11.6826 1.3335 8.00065C1.3335 4.31875 4.31827 1.33398 8.00016 1.33398C11.6821 1.33398 14.6668 4.31876 14.6668 8.00065Z" fill="currentColor"/>
</svg>
}

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.3335 8.00065C13.3335 5.05513 10.9457 2.66732 8.00016 2.66732C5.05465 2.66732 2.66683 5.05513 2.66683 8.00065C2.66683 10.9462 5.05464 13.334 8.00016 13.334C10.9457 13.334 13.3335 10.9462 13.3335 8.00065ZM7.3335 5.33398C7.3335 4.96579 7.63197 4.66732 8.00016 4.66732C8.36835 4.66732 8.66683 4.96579 8.66683 5.33398V7.72461L10.1382 9.19596L10.1838 9.24675C10.3973 9.50859 10.3823 9.8946 10.1382 10.1387C9.89411 10.3827 9.50811 10.3978 9.24626 10.1842L9.19548 10.1387L7.52881 8.47201C7.40378 8.34698 7.3335 8.17746 7.3335 8.00065V5.33398ZM14.6668 8.00065C14.6668 11.6826 11.6821 14.6673 8.00016 14.6673C4.31827 14.6673 1.3335 11.6826 1.3335 8.00065C1.3335 4.31875 4.31827 1.33398 8.00016 1.33398C11.6821 1.33398 14.6668 4.31876 14.6668 8.00065Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 873 B

View File

@@ -12,6 +12,7 @@ export { IconCrossLargeX } from "./IconCrossLargeX.tsx";
export { IconUpload } from "./IconUpload.tsx";
export { IconListStack } from "./IconListStack.tsx";
export { IconCircleCheck1 } from "./IconCircleCheck1.tsx";
export { IconClock } from "./IconClock.tsx";
export { IconImport2 } from "./IconImport2.tsx";
export { IconChevronTriangleDownSmall } from "./IconChevronTriangleDownSmall.tsx";
export { IconCheckmark1 } from "./IconCheckmark1.tsx";

View File

@@ -1,8 +1,8 @@
import {
useRef,
type FormEventHandler,
type TextareaHTMLAttributes,
type RefObject,
type RefCallback,
useLayoutEffect,
} from "react";
import { input } from "../Input/Input";
import clsx from "clsx";
@@ -10,26 +10,35 @@ import clsx from "clsx";
type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
variant?: "bordered" | "ghost" | "title";
autogrow?: boolean;
ref?: RefObject<HTMLTextAreaElement>;
ref?: RefCallback<HTMLTextAreaElement>;
};
export function Textarea(props: Props) {
const textbox = useRef<HTMLTextAreaElement>(null);
const ref = useRef<HTMLTextAreaElement>(null);
const { autogrow, variant, ref: propsRef, ...restProps } = props;
const ref = propsRef || textbox;
const adjustHeight: FormEventHandler<HTMLTextAreaElement> = (e) => {
props.onInput?.(e);
const adjustHeight = () => {
if (!autogrow || !ref.current) return;
ref.current.style.height = "inherit";
const paddingY = 2;
ref.current.style.height = `${ref.current.scrollHeight + paddingY * 2}px`;
};
useLayoutEffect(() => {
adjustHeight();
}, []);
return (
<textarea
{...restProps}
ref={ref}
onInput={adjustHeight}
ref={(node) => {
ref.current = node;
propsRef?.(node);
}}
onInput={(e) => {
adjustHeight();
props.onInput?.(e);
}}
className={input({
...props,
className: clsx("min-h-20", props.className),

View File

@@ -19,7 +19,13 @@ export const Default: Story = {
return (
<ConfirmDialog
message="Are you sure you want to delete this risk?"
onConfirm={() => {}}
onConfirm={() => {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 1000);
});
}}
>
<Button variant="danger">Delete this</Button>
</ConfirmDialog>

View File

@@ -1,16 +1,23 @@
import { useTranslate } from "@probo/i18n";
import { Dialog, DialogContent, DialogFooter } from "./Dialog";
import {
Children,
cloneElement,
isValidElement,
Root,
Trigger,
Overlay,
Content,
Title,
Description,
Cancel,
} from "@radix-ui/react-alert-dialog";
import {
useRef,
useState,
type ComponentProps,
type MouseEvent,
type ReactNode,
type RefObject,
} from "react";
import { Button } from "../../Atoms/Button/Button";
import { Root as Portal } from "@radix-ui/react-portal";
import { dialog } from "./Dialog";
type Props = {
children?: ReactNode;
@@ -18,7 +25,12 @@ type Props = {
message: string;
variant?: ComponentProps<typeof Button>["variant"];
label?: string;
onConfirm?: () => void;
onConfirm?: () => Promise<void>;
ref?: RefObject<{ open: () => void } | null>;
};
export const useConfirmDialogRef = () => {
return useRef<{ open: () => void } | null>(null);
};
export function ConfirmDialog(props: Props) {
@@ -27,45 +39,52 @@ export function ConfirmDialog(props: Props) {
const variant = props.variant ?? "danger";
const label = variant === "danger" ? __("Delete") : props.label;
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const {
overlay,
content,
header,
footer,
title: titleClassname,
} = dialog();
const onConfirm = () => {
setOpen(false);
props.onConfirm?.();
document.body.click();
};
const children = Children.map(props.children, (child) => {
if (!isValidElement(child)) {
throw new Error("Cannot use non element children");
}
return cloneElement(child, {
// @ts-expect-error We inject a handler on an unknown element
onClick: (e: MouseEvent) => {
e.preventDefault();
setOpen(true);
},
setLoading(true);
props.onConfirm?.().then(() => {
setLoading(false);
setOpen(false);
});
});
};
if (props.ref) {
props.ref.current = {
open: () => setOpen(true),
};
}
return (
<>
{children}
<Root open={open} onOpenChange={setOpen}>
{props.children && <Trigger asChild children={props.children} />}
<Portal>
<Dialog
open={open}
onOpenChange={setOpen}
title={title}
className="max-w-[500px]"
>
<DialogContent>
<p className="text-sm text-txt-secondary p-4">
{props.message}
</p>
</DialogContent>
<DialogFooter>
<Button variant={variant} onClick={onConfirm}>
<Overlay className={overlay()} />
<Content className={content({ className: "max-w-[500px]" })}>
<header className={header()}>
<Title children={title} className={titleClassname()} />
</header>
<Description className="p-6" children={props.message} />
<footer className={footer()}>
<Cancel asChild>
<Button disabled={loading} variant="tertiary">
{__("Cancel")}
</Button>
</Cancel>
<Button
disabled={loading}
variant={variant}
onClick={onConfirm}
>
{label}
</Button>
</DialogFooter>
</Dialog>
</footer>
</Content>
</Portal>
</>
</Root>
);
}

View File

@@ -8,63 +8,100 @@ import {
Close,
} from "@radix-ui/react-dialog";
import { IconCrossLargeX } from "../../Atoms/Icons";
import { type HTMLAttributes, type ReactNode } from "react";
import {
Children,
cloneElement,
isValidElement,
useRef,
useState,
type ComponentProps,
type CSSProperties,
type HTMLAttributes,
type ReactElement,
type ReactNode,
type RefObject,
} from "react";
import { Button } from "../../Atoms/Button/Button";
import { useTranslate } from "@probo/i18n";
import clsx from "clsx";
import { tv } from "tailwind-variants";
export const dialog = tv({
slots: {
overlay:
"fixed inset-0 z-50 bg-dialog/40 duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
content:
"text-txt-secondary text-sm fixed inset-0 m-auto z-50 w-full h-max bg-level-2 rounded-2xl max-w-5xl w-[95%] duration-200 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-[state=closed]:slide-out-to-top-5 data-[state=open]:slide-in-from-top-5",
header: "flex justify-between items-center p-3 border-b border-b-border-low",
title: "text-sm font-medium text-txt-primary",
footer: "flex justify-end items-center p-3 border-t border-t-border-low gap-2",
},
});
type Props = {
trigger?: ReactNode;
title: ReactNode;
title?: ReactNode;
children?: ReactNode;
onOpenChange?: (open: boolean) => void;
open?: boolean;
defaultOpen?: boolean;
className?: string;
ref?: RefObject<{ open: () => void; close: () => void } | null>;
};
export const useDialogRef = () => {
return useRef<{ open: () => void; close: () => void } | null>(null);
};
export function Dialog({
trigger,
title,
children,
onOpenChange,
open,
className,
ref,
defaultOpen,
}: Props) {
const { overlay, content, header, title: titleClassname } = dialog();
const [open, setOpen] = useState(!!defaultOpen);
if (ref) {
ref.current = {
open() {
setOpen(true);
},
close() {
setOpen(false);
},
};
}
return (
<Root open={open} onOpenChange={onOpenChange}>
<Root open={open} onOpenChange={setOpen}>
{trigger && <Trigger asChild>{trigger}</Trigger>}
<Portal>
<Overlay
className={clsx(
"fixed inset-0 z-50 bg-dialog/40",
`duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0`,
)}
/>
<Overlay className={overlay()} />
<Content
aria-describedby={undefined}
className={clsx(
"fixed inset-0 m-auto z-50 w-full h-max bg-level-2 rounded-2xl max-w-5xl w-[95%]",
className,
`duration-200
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-[state=closed]:slide-out-to-top-5
data-[state=open]:slide-in-from-top-5`,
)}
className={content({ className })}
>
<div className="flex justify-between items-center p-3 border-b border-b-border-low">
<Title className="text-sm font-medium text-txt-primary">
{title}
</Title>
{title ? (
<div className={header()}>
<Title className={titleClassname()}> {title}</Title>
<Close asChild>
<Button
tabIndex={-1}
variant="tertiary"
icon={IconCrossLargeX}
/>
</Close>
</div>
) : (
<Close asChild>
<Button
tabIndex={-1}
variant="tertiary"
className="absolute top-4 right-4"
icon={IconCrossLargeX}
/>
</Close>
</div>
)}
{children}
</Content>
</Portal>
@@ -80,8 +117,9 @@ export function DialogFooter({
exitLabel?: string;
}) {
const { __ } = useTranslate();
const { footer } = dialog();
return (
<footer className="flex justify-end items-center p-3 border-t border-t-border-low gap-2">
<footer className={footer()}>
<Close asChild>
<Button variant="secondary">{exitLabel ?? __("Cancel")}</Button>
</Close>
@@ -91,19 +129,51 @@ export function DialogFooter({
}
export function DialogContent(
props: HTMLAttributes<HTMLDivElement> & { padded?: boolean },
props: HTMLAttributes<HTMLDivElement> & {
padded?: boolean;
scrollableChildren?: boolean;
},
) {
let children = props.children;
if (props.scrollableChildren) {
children = Children.map(props.children, (c) => {
if (
isValidElement<{
className?: string;
style?: CSSProperties;
}>(c)
) {
return cloneElement(c, {
...c.props,
className: clsx(c.props.className, "overflow-y-auto"),
style: {
maxHeight: "var(--maxHeight)",
...c.props.style,
},
});
}
return c;
});
}
return (
<div
{...props}
children={children}
className={clsx(
"overflow-y-auto",
props.className,
props.padded && "p-6",
)}
style={{
maxHeight: "min(640px, calc(100vh - 140px))",
}}
style={
{
"--maxHeight": "min(640px, calc(100vh - 140px))",
maxHeight: "var(--maxHeight)",
} as CSSProperties
}
/>
);
}
export function DialogTitle(props: ComponentProps<typeof Title>) {
return <Title {...props} />;
}

View File

@@ -40,10 +40,19 @@ export {
} from "./Molecules/UserDropdown/UserDropdown";
export { PageHeader } from "./Molecules/PageHeader/PageHeader";
export { Skeleton } from "./Atoms/Skeleton/Skeleton";
export { Dialog, DialogContent, DialogFooter } from "./Molecules/Dialog/Dialog";
export {
Dialog,
DialogContent,
DialogFooter,
DialogTitle,
useDialogRef,
} from "./Molecules/Dialog/Dialog";
export { RiskBadge } from "./Molecules/Badge/RiskBadge";
export { SeverityBadge } from "./Molecules/Badge/SeverityBadge.tsx";
export { ConfirmDialog } from "./Molecules/Dialog/ConfirmDialog.tsx";
export {
ConfirmDialog,
useConfirmDialogRef,
} from "./Molecules/Dialog/ConfirmDialog.tsx";
export { RisksChart } from "./Molecules/Risks/RisksChart";
export { RiskOverview } from "./Molecules/Risks/RiskOverview";
export { Combobox, ComboboxItem } from "./Molecules/Combobox/Combobox";

View File

@@ -244,7 +244,6 @@
*:focus-visible {
box-shadow: var(--shadow-focus);
outline: none;
border-radius: 0.25rem;
}
::-webkit-scrollbar {