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:
committed by
Sacha Al Himdani
parent
faf1ce0eb9
commit
f9d1f033e0
@@ -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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -74,6 +74,7 @@ export function ActionDropdown(
|
||||
{...props}
|
||||
toggle={
|
||||
<Button
|
||||
className="inline-flex"
|
||||
variant={props.variant ?? "tertiary"}
|
||||
icon={IconDotGrid1x3Horizontal}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user