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:
committed by
Sacha Al Himdani
parent
abe0ce1dde
commit
2e6741942b
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
8
packages/ui/src/Atoms/Icons/IconClock.tsx
Normal file
8
packages/ui/src/Atoms/Icons/IconClock.tsx
Normal 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>
|
||||
|
||||
}
|
||||
3
packages/ui/src/Atoms/Icons/clock.svg
Normal file
3
packages/ui/src/Atoms/Icons/clock.svg
Normal 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 |
@@ -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";
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -244,7 +244,6 @@
|
||||
*:focus-visible {
|
||||
box-shadow: var(--shadow-focus);
|
||||
outline: none;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
|
||||
Reference in New Issue
Block a user