Add dark mode

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-23 22:52:05 +02:00
committed by Sacha Al Himdani
parent faf1ce0eb9
commit f9d1f033e0
14 changed files with 144 additions and 80 deletions

View File

@@ -19,7 +19,15 @@ export function ControlledField({
control={control} control={control}
name={name} name={name}
render={({ field }) => ( render={({ field }) => (
<Field {...props} {...field} onValueChange={field.onChange} /> <>
<Field
{...props}
{...field}
// TODO : Find a better way to handle this case (comparing number and string for select create issues)
value={field.value ? field.value.toString() : null}
onValueChange={field.onChange}
/>
</>
)} )}
/> />
); );

View File

@@ -72,10 +72,11 @@ function PeopleSelectWithQuery({
variant="editor" variant="editor"
placeholder={__("Select an owner")} placeholder={__("Select an owner")}
onValueChange={field.onChange} onValueChange={field.onChange}
key={people?.length ?? 0}
{...field} {...field}
> >
{people?.map((p) => ( {people?.map((p) => (
<Option key={p.id} value={p.id}> <Option key={p.id} value={p.id} className="flex gap-2">
<Avatar name={p.fullName} /> <Avatar name={p.fullName} />
{p.fullName} {p.fullName}
</Option> </Option>

View File

@@ -144,7 +144,7 @@ function MeasureLinkDialogContent(props: Omit<Props, "trigger">) {
))} ))}
</Select> </Select>
</div> </div>
<div className="space-y-2 divide-y divide-border-low"> <div className="divide-y divide-border-low">
{filteredMeasures.map((measure) => ( {filteredMeasures.map((measure) => (
<MeasureRow key={measure.id} measure={measure} {...props} /> <MeasureRow key={measure.id} measure={measure} {...props} />
))} ))}
@@ -168,6 +168,9 @@ function MeasureRow(props: RowProps) {
const isFetching = isFetchingAttach || isFetchingDetach; const isFetching = isFetchingAttach || isFetchingDetach;
const onClick = () => { const onClick = () => {
if (isFetching) {
return;
}
const action = isLinked ? detachMeasure : attachMeasure; const action = isLinked ? detachMeasure : attachMeasure;
action({ action({
variables: { variables: {
@@ -181,7 +184,10 @@ function MeasureRow(props: RowProps) {
}; };
return ( return (
<div className="py-4 flex items-center gap-4 hover:bg-subtle"> <div
className="py-4 flex items-center gap-4 hover:bg-subtle cursor-pointer"
onClick={onClick}
>
{props.measure.name} {props.measure.name}
<Badge variant="neutral">{props.measure.category}</Badge> <Badge variant="neutral">{props.measure.category}</Badge>
<Button <Button
@@ -189,7 +195,6 @@ function MeasureRow(props: RowProps) {
icon={isLinked ? IconTrashCan : IconPlusLarge} icon={isLinked ? IconTrashCan : IconPlusLarge}
className="ml-auto" className="ml-auto"
variant={isLinked ? "secondary" : "primary"} variant={isLinked ? "secondary" : "primary"}
onClick={onClick}
> >
{isLinked ? __("Unlink") : __("Link")} {isLinked ? __("Unlink") : __("Link")}
</Button> </Button>

View File

@@ -4,3 +4,9 @@
@import "tw-animate-css"; @import "tw-animate-css";
@source "../../../packages/ui/src"; @source "../../../packages/ui/src";
@source "../../../packages/helpers/src"; @source "../../../packages/helpers/src";
*:focus-visible {
box-shadow: var(--shadow-focus);
outline: none;
border-radius: 0.25rem;
}

View File

@@ -138,7 +138,7 @@ export default function FormRiskDialog({
}); });
const [showNote, toggleNote] = useToggle(false); const [showNote, toggleNote] = useToggle(false);
const [isOpen, setOpen] = useState(open); const [isOpen, setOpen] = useState(!!open);
return ( return (
<Dialog <Dialog
@@ -362,6 +362,7 @@ function TemplateSelector({
))} ))}
</ControlledSelect> </ControlledSelect>
<Select <Select
key={selectedCategory}
variant={templates?.length === 0 ? "dashed" : "default"} variant={templates?.length === 0 ? "dashed" : "default"}
placeholder={__("Select template")} placeholder={__("Select template")}
onValueChange={onTemplateChange} onValueChange={onTemplateChange}

View File

@@ -111,7 +111,7 @@ export default function RisksPage(props: Props) {
<Td> <Td>
<SeverityBadge score={risk.residualRiskScore} /> <SeverityBadge score={risk.residualRiskScore} />
</Td> </Td>
<Td noLink> <Td noLink className="text-end">
<ActionDropdown> <ActionDropdown>
<DropdownItem <DropdownItem
icon={IconPencil} icon={IconPencil}

View File

@@ -1,4 +1,4 @@
import { tv } from "tailwind-variants"; import clsx from "clsx";
type Props = { type Props = {
name: string; name: string;
@@ -6,28 +6,18 @@ type Props = {
size?: "s" | "m" | "l" | "xl"; size?: "s" | "m" | "l" | "xl";
}; };
const avatar = tv({
base: "bg-txt-success text-txt-invert rounded-full font-semibold flex items-center justify-center",
variants: {
size: {
s: "size-5 text-xss",
m: "size-6 text-xxs",
l: "size-8 text-sm",
xl: "size-16 text-3xl",
},
},
defaultVariants: {
size: "m",
},
});
export function Avatar({ name, src, size = "m" }: Props) { export function Avatar({ name, src, size = "m" }: Props) {
if (src) { const className = clsx(
return <img className={avatar({ size })} src={src} alt={name} />; "bg-txt-success text-txt-invert rounded-full font-semibold flex items-center justify-center",
} size === "s" && "size-5 text-xss",
return ( size === "m" && "size-6 text-xxs",
<div className={avatar({ size })}>{extractInitials(name ?? "")}</div> size === "l" && "size-8 text-sm",
size === "xl" && "size-16 text-3xl",
); );
if (src) {
return <img className={className} src={src} alt={name} />;
}
return <div className={className}>{extractInitials(name ?? "")}</div>;
} }
function extractInitials(name: string) { function extractInitials(name: string) {

View File

@@ -9,7 +9,7 @@ type Props = PropsWithChildren<{
}>; }>;
const card = tv({ const card = tv({
base: "border border-border-low surface-1 rounded-2xl", base: "border border-border-low bg-level-1 rounded-2xl",
variants: { variants: {
padded: { padded: {
true: "p-6", true: "p-6",

View File

@@ -74,6 +74,7 @@ export function ActionDropdown(
{...props} {...props}
toggle={ toggle={
<Button <Button
className="inline-flex"
variant={props.variant ?? "tertiary"} variant={props.variant ?? "tertiary"}
icon={IconDotGrid1x3Horizontal} icon={IconDotGrid1x3Horizontal}
/> />

View File

@@ -13,7 +13,7 @@ export function Logo({ className }: Props) {
viewBox="0 0 48 20" viewBox="0 0 48 20"
> >
<path <path
fill="#141E12" fill="currentColor"
d="M29.3 0v6.1c.06-.06.17-.13.3-.22.16-.1.46-.24.93-.4A4.7 4.7 0 0 1 32 5.26c1.36 0 2.5.46 3.42 1.4a4.59 4.59 0 0 1 1.38 3.38c0 1.34-.46 2.44-1.4 3.38a4.57 4.57 0 0 1-3.4 1.41 4.83 4.83 0 0 1-3.39-1.3 4.57 4.57 0 0 1-1.42-3.55V0h2.1ZM32 7.27a2.67 2.67 0 0 0-2.7 2.76c0 1.6 1.13 2.77 2.7 2.77 1.55 0 2.7-1.19 2.7-2.77A2.69 2.69 0 0 0 32 7.27ZM17.54 6.65a4.6 4.6 0 0 1 3.4-1.4c1.34 0 2.44.46 3.38 1.4a4.6 4.6 0 0 1 1.4 3.38c0 1.32-.46 2.46-1.4 3.4a4.66 4.66 0 0 1-3.38 1.4 4.7 4.7 0 0 1-3.4-1.4 4.6 4.6 0 0 1-1.41-3.4c0-1.34.46-2.43 1.4-3.38Zm.7 3.38c0 .82.26 1.48.77 2 .53.5 1.16.77 1.93.77.76 0 1.4-.26 1.9-.77a2.7 2.7 0 0 0 .8-2 2.6 2.6 0 0 0-.8-1.97 2.53 2.53 0 0 0-1.9-.8 2.68 2.68 0 0 0-2.7 2.77ZM39.67 6.65a4.6 4.6 0 0 1 3.4-1.4c1.34 0 2.44.46 3.38 1.4a4.6 4.6 0 0 1 1.4 3.38c0 1.32-.45 2.46-1.4 3.4a4.66 4.66 0 0 1-3.38 1.4 4.7 4.7 0 0 1-3.4-1.4 4.6 4.6 0 0 1-1.4-3.4c0-1.34.45-2.43 1.4-3.38Zm.7 3.38c0 .82.26 1.48.77 2 .53.5 1.16.77 1.93.77s1.4-.26 1.91-.77a2.7 2.7 0 0 0 .8-2 2.6 2.6 0 0 0-.8-1.97 2.53 2.53 0 0 0-1.9-.8 2.68 2.68 0 0 0-2.7 2.77ZM2.1 13.96V20H0v-9.9c0-1.52.48-2.7 1.43-3.56a4.9 4.9 0 0 1 3.38-1.3c1.3 0 2.48.47 3.4 1.41a4.6 4.6 0 0 1 1.4 3.39 4.71 4.71 0 0 1-4.8 4.78 4.53 4.53 0 0 1-2.39-.63l-.3-.22H2.1Zm2.7-6.7a2.66 2.66 0 0 0-2.7 2.77 2.69 2.69 0 0 0 2.7 2.77c1.52 0 2.7-1.17 2.7-2.77S6.33 7.27 4.8 7.27ZM13.08 14.7h-2.09V9.14c0-2.7 1.52-3.78 3.56-3.78h.5v2.03h-.4c-1.04 0-1.57.59-1.57 1.75v5.58Z" d="M29.3 0v6.1c.06-.06.17-.13.3-.22.16-.1.46-.24.93-.4A4.7 4.7 0 0 1 32 5.26c1.36 0 2.5.46 3.42 1.4a4.59 4.59 0 0 1 1.38 3.38c0 1.34-.46 2.44-1.4 3.38a4.57 4.57 0 0 1-3.4 1.41 4.83 4.83 0 0 1-3.39-1.3 4.57 4.57 0 0 1-1.42-3.55V0h2.1ZM32 7.27a2.67 2.67 0 0 0-2.7 2.76c0 1.6 1.13 2.77 2.7 2.77 1.55 0 2.7-1.19 2.7-2.77A2.69 2.69 0 0 0 32 7.27ZM17.54 6.65a4.6 4.6 0 0 1 3.4-1.4c1.34 0 2.44.46 3.38 1.4a4.6 4.6 0 0 1 1.4 3.38c0 1.32-.46 2.46-1.4 3.4a4.66 4.66 0 0 1-3.38 1.4 4.7 4.7 0 0 1-3.4-1.4 4.6 4.6 0 0 1-1.41-3.4c0-1.34.46-2.43 1.4-3.38Zm.7 3.38c0 .82.26 1.48.77 2 .53.5 1.16.77 1.93.77.76 0 1.4-.26 1.9-.77a2.7 2.7 0 0 0 .8-2 2.6 2.6 0 0 0-.8-1.97 2.53 2.53 0 0 0-1.9-.8 2.68 2.68 0 0 0-2.7 2.77ZM39.67 6.65a4.6 4.6 0 0 1 3.4-1.4c1.34 0 2.44.46 3.38 1.4a4.6 4.6 0 0 1 1.4 3.38c0 1.32-.45 2.46-1.4 3.4a4.66 4.66 0 0 1-3.38 1.4 4.7 4.7 0 0 1-3.4-1.4 4.6 4.6 0 0 1-1.4-3.4c0-1.34.45-2.43 1.4-3.38Zm.7 3.38c0 .82.26 1.48.77 2 .53.5 1.16.77 1.93.77s1.4-.26 1.91-.77a2.7 2.7 0 0 0 .8-2 2.6 2.6 0 0 0-.8-1.97 2.53 2.53 0 0 0-1.9-.8 2.68 2.68 0 0 0-2.7 2.77ZM2.1 13.96V20H0v-9.9c0-1.52.48-2.7 1.43-3.56a4.9 4.9 0 0 1 3.38-1.3c1.3 0 2.48.47 3.4 1.41a4.6 4.6 0 0 1 1.4 3.39 4.71 4.71 0 0 1-4.8 4.78 4.53 4.53 0 0 1-2.39-.63l-.3-.22H2.1Zm2.7-6.7a2.66 2.66 0 0 0-2.7 2.77 2.69 2.69 0 0 0 2.7 2.77c1.52 0 2.7-1.17 2.7-2.77S6.33 7.27 4.8 7.27ZM13.08 14.7h-2.09V9.14c0-2.7 1.52-3.78 3.56-3.78h.5v2.03h-.4c-1.04 0-1.57.59-1.57 1.75v5.58Z"
/> />
</svg> </svg>

View File

@@ -13,13 +13,7 @@ import * as ScrollArea from "@radix-ui/react-scroll-area";
import { input } from "../Input/Input.tsx"; import { input } from "../Input/Input.tsx";
import { IconChevronGrabberVertical } from "../Icons/IconChevronGrabberVertical.tsx"; import { IconChevronGrabberVertical } from "../Icons/IconChevronGrabberVertical.tsx";
import { tv } from "tailwind-variants"; import { tv } from "tailwind-variants";
import { import { Children, type ComponentProps, type PropsWithChildren } from "react";
Children,
isValidElement,
type ComponentProps,
type PropsWithChildren,
type ReactNode,
} from "react";
type Props = PropsWithChildren< type Props = PropsWithChildren<
{ {
@@ -36,10 +30,10 @@ type Props = PropsWithChildren<
const select = tv({ const select = tv({
slots: { slots: {
trigger: trigger:
"flex justify-between items-center data-placeholder:text-txt-tertiary whitespace-nowrap cursor-pointer", "flex justify-between items-center data-placeholder:text-txt-tertiary whitespace-nowrap cursor-pointer *:first:contents",
content: content:
"z-100 shadow-mid rounded-[10px] bg-level-1 p-1 animate-in fade-in slide-in-from-top-2 overflow-y-auto overflow-y-auto", "z-100 shadow-mid rounded-[10px] bg-level-1 p-1 animate-in fade-in slide-in-from-top-2 overflow-y-auto overflow-y-auto",
option: "flex items-center h-8 text-sm font-medium text-txt-primary hover:bg-tertiary-hover active:bg-tertiary-pressed cursor-pointer px-[10px]", option: "flex gap-2 items-center h-8 text-sm font-medium text-txt-primary hover:bg-tertiary-hover active:bg-tertiary-pressed cursor-pointer px-[10px]",
icon: "-mr-1", icon: "-mr-1",
}, },
variants: { variants: {
@@ -79,23 +73,6 @@ const select = tv({
const { trigger, option, content, icon } = select(); const { trigger, option, content, icon } = select();
/**
* To display the selected value we need to find the selected option among children
*/
const findSelectedOption = (
children: unknown[],
condition: (c: { props: Record<string, unknown> }) => boolean,
): ReactNode => {
const selectedOptions = children.find(
// @ts-expect-error We know that the children are ReactElements with props
(c) => isValidElement(c) && condition(c),
);
if (!isValidElement(selectedOptions)) {
return null;
}
return (selectedOptions.props as { children: ReactNode }).children;
};
export function Select({ export function Select({
placeholder, placeholder,
children, children,
@@ -103,26 +80,10 @@ export function Select({
value, value,
...props ...props
}: Props) { }: Props) {
const childrenArr = Children.toArray(children);
const valueNode = value
? findSelectedOption(
childrenArr,
(c) => c.props.value?.toString() === value?.toString(),
)
: undefined;
return ( return (
<Root onValueChange={onValueChange} value={value}> <Root onValueChange={onValueChange} value={value}>
<Trigger {...props} className={trigger({ ...props })}> <Trigger {...props} className={trigger({ ...props })}>
<div className="text-ellipsis overflow-hidden"> <Value placeholder={placeholder} />
<Value placeholder={placeholder}>
{valueNode ? (
<span className="flex items-center gap-2 overflow-hidden text-ellipsis w-full">
{valueNode}
</span>
) : null}
</Value>
</div>
<Icon className={icon()}> <Icon className={icon()}>
<IconChevronGrabberVertical size={16} /> <IconChevronGrabberVertical size={16} />
</Icon> </Icon>
@@ -152,10 +113,19 @@ export function Select({
} }
export function Option({ children, ...props }: ComponentProps<typeof Item>) { export function Option({ children, ...props }: ComponentProps<typeof Item>) {
const hasSingleChildren = Children.count(children) <= 1;
return ( return (
<Item {...props} className={option(props)}> <Item {...props} className={option(props)}>
<ItemText asChild> <ItemText asChild>
<span className="flex items-center gap-2">{children}</span> <span
className={
hasSingleChildren
? "text-ellipsis overflow-hidden"
: "flex gap-2 items-center"
}
>
{children}
</span>
</ItemText> </ItemText>
</Item> </Item>
); );

View File

@@ -58,13 +58,23 @@ export function Tbody({ children }: PropsWithChildren) {
export function Td({ export function Td({
children, children,
noLink, noLink,
}: PropsWithChildren<{ noLink?: boolean }>) { className,
}: PropsWithChildren<{ noLink?: boolean; className?: string }>) {
const { to } = useContext(TrContext); const { to } = useContext(TrContext);
if (!to || noLink) { if (!to || noLink) {
return <td className="first:pl-6 last:pr-6 py-3">{children}</td>; return (
<td className={clsx("first:pl-6 last:pr-6 py-3", className)}>
{children}
</td>
);
} }
return ( return (
<td className="first:*:pl-6 *:block last:*:pr-6 py-3"> <td
className={clsx(
"first:*:pl-6 *:block last:*:pr-6 *:py-3",
className,
)}
>
<Link to={to}>{children}</Link> <Link to={to}>{children}</Link>
</td> </td>
); );

View File

@@ -37,14 +37,14 @@ export function Layout({ header, sidebar, children }: Props) {
<header className="absolute z-2 left-0 right-0 px-4 flex items-center border-b border-border-solid h-12 bg-level-1"> <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" /> <Logo className="w-12 h-5" />
<svg <svg
className="mx-3" className="mx-3 text-txt-tertiary"
width="8" width="8"
height="18" height="18"
viewBox="0 0 8 18" viewBox="0 0 8 18"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path d="M1 17L7 1" stroke="#C3C8C2" /> <path d="M1 17L7 1" stroke="currentColor" />
</svg> </svg>
{header} {header}
</header> </header>

View File

@@ -31,7 +31,6 @@
--color-active: #e4f7c7; --color-active: #e4f7c7;
--color-accent: #141e12; --color-accent: #141e12;
--color-danger: #ffefef; --color-danger: #ffefef;
--color-danger-plain: #dc3d43;
--color-danger-dark: #dc3d43; --color-danger-dark: #dc3d43;
--color-success: #eefadc; --color-success: #eefadc;
--color-info: #edf6ff; --color-info: #edf6ff;
@@ -163,3 +162,76 @@
0px 2px 4px 0px rgba(0, 0, 0, 0.08), 0px 2px 4px 0px rgba(0, 0, 0, 0.08),
0px 2px 12px 0px rgba(0, 0, 0, 0.06); 0px 2px 12px 0px rgba(0, 0, 0, 0.06);
} }
@media (prefers-color-scheme: dark) {
:root {
--color-level-0: #151715;
--color-level-1: #1a1d19;
--color-level-2: #20241f;
--color-level-3: #3b3f3a;
/* Text colors */
--color-txt-primary: #eceeec;
--color-invert: #151715;
--color-txt-invert: var(--color-invert);
--color-txt-secondary: #9aa299;
--color-txt-tertiary: #778175;
--color-txt-quaternary: #4c514b;
--color-txt-disabled: #3b3f3a;
--color-txt-accent: #c4f042;
--color-txt-danger: #ff6369;
--color-txt-success: rgba(181, 255, 44, 0.72);
--color-txt-info: #52a9ff;
--color-txt-warning: #f1a10d;
/* Backgrounds */
--color-primary: #c4f042;
--color-invert: #151715;
--color-secondary: rgba(214, 251, 196, 0.06);
--color-tertiary: rgba(255, 255, 255, 0);
--color-subtle: rgba(213, 254, 175, 0.03);
--color-highlight: rgba(214, 251, 196, 0.06);
--color-active: rgba(239, 254, 226, 0.08);
--color-accent: #c4f042;
--color-danger: rgba(254, 58, 61, 0.13);
--color-danger-dark: rgba(255, 89, 95, 0.94);
--color-success: rgba(184, 253, 106, 0.06);
--color-info: rgba(22, 119, 254, 0.14);
--color-warning: rgba(254, 115, 0, 0.09);
--color-dialog: #151715;
/* Borders */
--color-border-solid: #2b2f2a;
--color-border-low: rgba(239, 254, 226, 0.08);
--color-border-mid: rgba(233, 254, 223, 0.1);
--color-border-strong: rgba(241, 255, 237, 0.25);
--color-border-active: rgba(253, 255, 253, 0.93);
--color-border-accent: #c4f042;
--color-border-danger: #f2555a;
--color-border-success: #c4f042;
--color-border-warning: #ffcb47;
--color-border-info: #369eff;
/* Hover */
--color-primary-hover: rgba(196, 240, 66, 0.88);
--color-invert-hover: #1a1d19;
--color-secondary-hover: rgba(214, 251, 196, 0.06);
--color-tertiary-hover: rgba(213, 254, 175, 0.03);
--color-subtle-hover: rgba(214, 251, 196, 0.06);
--color-highlight-hover: rgba(233, 254, 223, 0.1);
--color-accent-hover: rgba(183, 255, 50, 0.82);
--color-active-hover: rgba(233, 254, 223, 0.1);
--color-danger-hover: rgba(254, 58, 61, 0.7);
/* Pressed */
--color-primary-pressed: rgba(196, 240, 66, 0.84);
--color-invert-pressed: #20241f;
--color-secondary-pressed: rgba(239, 254, 226, 0.08);
--color-tertiary-pressed: rgba(214, 251, 196, 0.06);
--color-subtle-pressed: rgba(239, 254, 226, 0.08);
--color-highlight-pressed: rgba(236, 254, 229, 0.13);
--color-accent-pressed: rgba(209, 255, 70, 0.94);
--color-active-pressed: rgba(236, 254, 229, 0.13);
--color-danger-pressed: rgba(254, 58, 61, 0.5);
}
}