Add vendor certifications tab

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Jonathan
2025-06-01 12:08:27 +02:00
committed by Sacha Al Himdani
parent b94b58e2de
commit df23fd635f
37 changed files with 2266 additions and 199 deletions

View File

@@ -0,0 +1,34 @@
type Translator = (s: string) => string;
export const certifications = {
securityStandards: [
"SOC 2",
"ISO 27001",
"HITRUST",
"NIST",
"SOC 2 Type 2",
"SOC 2 Type 1",
],
regulatoryLegal: ["HIPAA", "FERPA", "FISMA", "PIPEDA", "GDPR", "CCPA"],
industrySpecific: ["FinTech", "MPAA", "GSMA"],
internationalGov: ["FedRAMP", "ENS High", "IRAP", "CJIS"],
custom: [] as string[],
} as const;
export const certificationCategoryLabel = (
__: Translator,
category: keyof typeof certifications,
) => {
switch (category) {
case "securityStandards":
return __("Security Standards");
case "regulatoryLegal":
return __("Regulatory & Legal");
case "industrySpecific":
return __("Industry-Specific");
case "internationalGov":
return __("International & Government");
default:
return __("Custom certifications");
}
};

View File

@@ -0,0 +1,7 @@
export function withViewTransition(fn: () => void) {
if (!document.startViewTransition) {
fn();
return;
}
document.startViewTransition(fn);
}

View File

@@ -1,4 +1,4 @@
export { objectKeys } from "./object";
export { objectKeys, objectEntries } from "./object";
export { sprintf, faviconUrl } from "./string";
export {
getTreatment,
@@ -6,7 +6,9 @@ export {
getRiskLikelihoods,
getSeverity,
} from "./risk";
export { withViewTransition } from "./dom";
export { times, groupBy, isEmpty } from "./array";
export { randomInt } from "./number";
export { getMeasureStateLabel, measureStates } from "./measure";
export { getRole, getRoles, peopleRoles } from "./people";
export { certificationCategoryLabel, certifications } from "./certifications";

View File

@@ -4,3 +4,7 @@
export function objectKeys<T extends Record<string, unknown>>(object: T) {
return Object.keys(object) as (keyof T)[];
}
export function objectEntries<T extends Record<string, unknown>>(object: T) {
return Object.entries(object) as [keyof T, T[keyof T]][];
}

View File

@@ -23,6 +23,7 @@
"@radix-ui/react-tabs": "^1.1.12",
"@tailwindcss/vite": "^4.1.7",
"clsx": "^2.1.1",
"react-dropzone": "^14.3.8",
"react-markdown": "^10.1.0",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",

View File

@@ -1,7 +1,9 @@
import type { HTMLAttributes } from "react";
import { tv } from "tailwind-variants";
import { Slot } from "../Slot";
type Props = {
asChild?: boolean;
variant?:
| "success"
| "warning"
@@ -11,10 +13,10 @@ type Props = {
| "outline"
| "highlight";
size?: "sm" | "md";
} & HTMLAttributes<HTMLDivElement>;
} & HTMLAttributes<HTMLElement>;
const badge = tv({
base: "font-medium rounded-lg w-max flex gap-1 items-center",
base: "font-medium rounded-lg w-max flex gap-1 items-center group whitespace-nowrap",
variants: {
variant: {
success: "bg-success text-txt-success",
@@ -37,5 +39,7 @@ const badge = tv({
});
export function Badge(props: Props) {
return <div {...props} className={badge(props)} />;
const Component = props.asChild ? Slot : "div";
const { asChild, size, variant, ...restProps } = props;
return <Component {...restProps} className={badge(props)} />;
}

View File

@@ -0,0 +1,12 @@
import { Dropzone } from "./Dropzone";
import type { Meta, StoryObj } from "@storybook/react";
export default {
title: "Atoms/Dropzone",
component: Dropzone,
argTypes: {},
} satisfies Meta<typeof Dropzone>;
type Story = StoryObj<typeof Dropzone>;
export const Default: Story = {};

View File

@@ -0,0 +1,24 @@
import { useCallback } from "react";
import { useDropzone } from "react-dropzone";
type Props = {};
export function Dropzone({}: Props) {
const onDrop = useCallback((acceptedFiles) => {
// Do something with the files
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
});
return (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isDragActive ? (
<p>Drop the files here ...</p>
) : (
<p>Drag 'n' drop some files here, or click to select files</p>
)}
</div>
);
}

View File

@@ -83,7 +83,10 @@ export function Select({
return (
<Root onValueChange={onValueChange} value={value}>
<Trigger {...props} className={trigger()}>
<Trigger
{...props}
className={trigger({ className: props.className })}
>
<Value placeholder={placeholder} />
<Icon className={icon()}>
<IconChevronGrabberVertical size={16} />

View File

@@ -18,13 +18,34 @@ type Props = {
loading?: boolean;
onSearch: (query: string) => void;
placeholder?: string;
autoSelect?: boolean;
onSelect?: (value: string) => void;
resetValueOnHide?: boolean;
value?: string;
};
export function Combobox({ children, onSearch, placeholder }: Props) {
export function Combobox({
children,
onSearch,
placeholder,
autoSelect,
onSelect,
resetValueOnHide,
value,
}: Props) {
const showDropdown = !isEmpty(children);
return (
<ComboboxProvider setValue={onSearch}>
<AriaKitCombobox placeholder={placeholder} className={input()} />
<ComboboxProvider
value={value}
setValue={onSearch}
setSelectedValue={(v) => onSelect?.(v as string)}
resetValueOnHide={resetValueOnHide}
>
<AriaKitCombobox
autoSelect={autoSelect}
placeholder={placeholder}
className={input()}
/>
{showDropdown && (
<ComboboxPopover gutter={4} sameWidth className={dropdown()}>
{children}
@@ -39,7 +60,7 @@ export function ComboboxItem({
...props
}: PropsWithChildren<ComponentProps<typeof AriaKitComboboxItem>>) {
return (
<AriaKitComboboxItem className={dropdownItem()} {...props}>
<AriaKitComboboxItem hideOnClick className={dropdownItem()} {...props}>
{children}
</AriaKitComboboxItem>
);

View File

@@ -2,7 +2,7 @@ import { tv } from "tailwind-variants";
import { Label } from "../../Atoms/Label/Label";
import { Input } from "../../Atoms/Input/Input";
import { Textarea } from "../../Atoms/Textarea/Textarea";
import { type ComponentProps } from "react";
import { type ComponentProps, type ReactNode } from "react";
import { Select } from "../../Atoms/Select/Select";
type BaseProps<T extends string, P> = {
@@ -10,7 +10,8 @@ type BaseProps<T extends string, P> = {
help?: string;
error?: string;
onValueChange?: (s: string) => void;
type: T;
type?: T;
children?: ReactNode;
} & P;
type Props =
@@ -33,6 +34,7 @@ const { base: baseClass, label: labelClass, help: helpClass } = field();
export function Field(props: Props) {
const showHelp = props.help && !props.error;
const childrenAsInput = !props.type && props.children;
return (
<div className={baseClass()}>
{props.label && (
@@ -40,7 +42,7 @@ export function Field(props: Props) {
{props.label}
</Label>
)}
{getInput(props)}
{childrenAsInput ? props.children : getInput(props)}
{showHelp && <span className={helpClass()}>{props.help}</span>}
{props.error && (
<span className="text-txt-danger text-sm mt-1">

View File

@@ -249,6 +249,11 @@ button:focus-visible {
}
}
/* Allow interpolation : https://developer.chrome.com/docs/css-ui/animate-to-height-auto */
:root {
interpolate-size: allow-keywords;
}
::-webkit-scrollbar {
width: 7px;
height: 7px;