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}
name={name}
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"
placeholder={__("Select an owner")}
onValueChange={field.onChange}
key={people?.length ?? 0}
{...field}
>
{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} />
{p.fullName}
</Option>

View File

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

View File

@@ -4,3 +4,9 @@
@import "tw-animate-css";
@source "../../../packages/ui/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 [isOpen, setOpen] = useState(open);
const [isOpen, setOpen] = useState(!!open);
return (
<Dialog
@@ -362,6 +362,7 @@ function TemplateSelector({
))}
</ControlledSelect>
<Select
key={selectedCategory}
variant={templates?.length === 0 ? "dashed" : "default"}
placeholder={__("Select template")}
onValueChange={onTemplateChange}

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ export function Logo({ className }: Props) {
viewBox="0 0 48 20"
>
<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"
/>
</svg>

View File

@@ -13,13 +13,7 @@ import * as ScrollArea from "@radix-ui/react-scroll-area";
import { input } from "../Input/Input.tsx";
import { IconChevronGrabberVertical } from "../Icons/IconChevronGrabberVertical.tsx";
import { tv } from "tailwind-variants";
import {
Children,
isValidElement,
type ComponentProps,
type PropsWithChildren,
type ReactNode,
} from "react";
import { Children, type ComponentProps, type PropsWithChildren } from "react";
type Props = PropsWithChildren<
{
@@ -36,10 +30,10 @@ type Props = PropsWithChildren<
const select = tv({
slots: {
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:
"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",
},
variants: {
@@ -79,23 +73,6 @@ const select = tv({
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({
placeholder,
children,
@@ -103,26 +80,10 @@ export function Select({
value,
...props
}: Props) {
const childrenArr = Children.toArray(children);
const valueNode = value
? findSelectedOption(
childrenArr,
(c) => c.props.value?.toString() === value?.toString(),
)
: undefined;
return (
<Root onValueChange={onValueChange} value={value}>
<Trigger {...props} className={trigger({ ...props })}>
<div className="text-ellipsis overflow-hidden">
<Value placeholder={placeholder}>
{valueNode ? (
<span className="flex items-center gap-2 overflow-hidden text-ellipsis w-full">
{valueNode}
</span>
) : null}
</Value>
</div>
<Value placeholder={placeholder} />
<Icon className={icon()}>
<IconChevronGrabberVertical size={16} />
</Icon>
@@ -152,10 +113,19 @@ export function Select({
}
export function Option({ children, ...props }: ComponentProps<typeof Item>) {
const hasSingleChildren = Children.count(children) <= 1;
return (
<Item {...props} className={option(props)}>
<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>
</Item>
);

View File

@@ -58,13 +58,23 @@ export function Tbody({ children }: PropsWithChildren) {
export function Td({
children,
noLink,
}: PropsWithChildren<{ noLink?: boolean }>) {
className,
}: PropsWithChildren<{ noLink?: boolean; className?: string }>) {
const { to } = useContext(TrContext);
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 (
<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>
</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">
<Logo className="w-12 h-5" />
<svg
className="mx-3"
className="mx-3 text-txt-tertiary"
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" />
<path d="M1 17L7 1" stroke="currentColor" />
</svg>
{header}
</header>

View File

@@ -31,7 +31,6 @@
--color-active: #e4f7c7;
--color-accent: #141e12;
--color-danger: #ffefef;
--color-danger-plain: #dc3d43;
--color-danger-dark: #dc3d43;
--color-success: #eefadc;
--color-info: #edf6ff;
@@ -163,3 +162,76 @@
0px 2px 4px 0px rgba(0, 0, 0, 0.08),
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);
}
}