diff --git a/src/__tests__/_/utils/index.ts b/src/__tests__/_/utils/index.ts index 599e2fa7..271a275a 100644 --- a/src/__tests__/_/utils/index.ts +++ b/src/__tests__/_/utils/index.ts @@ -14,7 +14,7 @@ export const waitForSkeletonsToVanish = async () => { }; export const getToastMessage = async () => { - return (await within(screen.getByRole("region")).findByRole("status")) + return (await within(await screen.findByRole("region")).findByRole("status")) .textContent; }; @@ -48,3 +48,9 @@ export const selectCombobox = async (label: string, value: string) => { pointerEventsCheck: PointerEventsCheckLevel.Never, }); }; + +export const waitPointerEvents = async () => { + await waitFor(() => { + expect(document.body).toHaveStyle("pointer-events: auto"); + }); +}; diff --git a/src/__tests__/integrations/variables__constants.spec.tsx b/src/__tests__/integrations/variables__constants.spec.tsx index 52250fed..e5fae722 100644 --- a/src/__tests__/integrations/variables__constants.spec.tsx +++ b/src/__tests__/integrations/variables__constants.spec.tsx @@ -10,6 +10,7 @@ import { confirmDelete, getTableRows, getToastMessage, + waitPointerEvents, } from "@/tests/utils"; setupApiHandlers(); @@ -63,13 +64,17 @@ describe("pages/integrations/variables => constants", () => { const dialog = await screen.findByRole("dialog"); + await waitPointerEvents(); + expect(within(dialog).getByText("Create Constant")).toBeInTheDocument(); await userEvent.type(within(dialog).getByLabelText("Key"), "NEW_KEY"); await userEvent.type(within(dialog).getByLabelText("Value"), "new value"); await userEvent.click( - within(dialog).getByRole("button", { name: "Create Constant" }) + within(dialog).getByRole("button", { + name: "Create Constant", + }) ); expect(await getToastMessage()).toBe("Constant Saved Successfully"); @@ -119,6 +124,8 @@ describe("pages/integrations/variables => constants", () => { }) ); + await waitPointerEvents(); + const dialog = screen.getByRole("dialog"); expect(within(dialog).getByText("Update Constant")).toBeInTheDocument(); diff --git a/src/__tests__/integrations/variables__credentials.spec.tsx b/src/__tests__/integrations/variables__credentials.spec.tsx index 4a6ed29e..efcef99f 100644 --- a/src/__tests__/integrations/variables__credentials.spec.tsx +++ b/src/__tests__/integrations/variables__credentials.spec.tsx @@ -301,13 +301,7 @@ describe("pages/integrations/variables => credentials", () => { ) ); - const dialog = await screen.findByRole( - "dialog", - {}, - { - timeout: 20000, - } - ); + const dialog = await screen.findByRole("dialog"); expect(within(dialog).getByText("Create Secret")).toBeInTheDocument(); diff --git a/src/frontend/_layouts/useAppTheme.ts b/src/frontend/_layouts/useAppTheme.ts index 74668a10..c6a2726b 100644 --- a/src/frontend/_layouts/useAppTheme.ts +++ b/src/frontend/_layouts/useAppTheme.ts @@ -1,78 +1,17 @@ -/* eslint-disable no-param-reassign */ -/* eslint-disable no-bitwise */ import { useAppConfiguration } from "@/frontend/hooks/configuration/configuration.store"; +import { hexToOklch } from "../lib/colors/conversion"; import { usePortalThemes } from "./portal"; -type ThreeNumbers = readonly [number, number, number]; - -function hexToRgb(hex$1: string): ThreeNumbers { - let hex = hex$1.replace("#", ""); - if (hex.length === 3) { - hex = hex - .split("") - .map((c) => c + c) - .join(""); - } - const bigint = parseInt(hex, 16); - const r = (bigint >> 16) & 255; - const g = (bigint >> 8) & 255; - const b = bigint & 255; - return [r, g, b]; -} - -function rgbToXyz([r, g, b]: ThreeNumbers) { - r /= 255; - g /= 255; - b /= 255; - - r = r > 0.04045 ? ((r + 0.055) / 1.055) ** 2.4 : r / 12.92; - g = g > 0.04045 ? ((g + 0.055) / 1.055) ** 2.4 : g / 12.92; - b = b > 0.04045 ? ((b + 0.055) / 1.055) ** 2.4 : b / 12.92; - - const x = (r * 0.4124564 + g * 0.3575761 + b * 0.1804375) / 0.95047; - const y = (r * 0.2126729 + g * 0.7151522 + b * 0.072175) / 1.0; - const z = (r * 0.0193339 + g * 0.119192 + b * 0.9503041) / 1.08883; - - return [x, y, z] as const; -} - -function xyzToLab([x, y, z]: ThreeNumbers) { - x = x > 0.008856 ? Math.cbrt(x) : 7.787 * x + 16 / 116; - y = y > 0.008856 ? Math.cbrt(y) : 7.787 * y + 16 / 116; - z = z > 0.008856 ? Math.cbrt(z) : 7.787 * z + 16 / 116; - - const l = 116 * y - 16; - const a = 500 * (x - y); - const b = 200 * (y - z); - - return [l, a, b] as const; -} - -function labToOklch([l, a, b]: ThreeNumbers) { - const c = Math.sqrt(a * a + b * b); - const h = Math.atan2(b, a); - const hDegrees = (h * 180) / Math.PI; - const hPositive = hDegrees < 0 ? hDegrees + 360 : hDegrees; - - return [l / 100, c / 100, hPositive] as const; -} - -function hexToOklch(hex: string) { - const rgb = hexToRgb(hex); - const xyz = rgbToXyz(rgb); - const lab = xyzToLab(xyz); - const [l, c, h] = labToOklch(lab); - return `${l.toFixed(3)}% ${c.toFixed(3)} ${h.toFixed(3)}`; -} - export const useAppTheme = () => { const themeColor = useAppConfiguration("theme_color"); usePortalThemes(); + const { l, c, h } = hexToOklch(themeColor.data.primary); + document.documentElement.style.setProperty( "--app-primary", - hexToOklch(themeColor.data.primary) + `${l}% ${c} ${h}` ); }; diff --git a/src/frontend/components/app/app-blur.tsx b/src/frontend/components/app/app-blur.tsx new file mode 100644 index 00000000..6f7cfab0 --- /dev/null +++ b/src/frontend/components/app/app-blur.tsx @@ -0,0 +1,28 @@ +import type { ReactNode } from "react"; +import { useEffect } from "react"; + +interface IProps { + children: ReactNode; + isOn: boolean; +} + +const gaussianPortals = ["gaussian-portal-0", "gaussian-portal-1"]; + +export function AppBlur({ children, isOn }: IProps): JSX.Element { + useEffect(() => { + if (isOn) { + gaussianPortals.forEach((portal) => { + document.getElementById(portal)?.classList.add("gaussian-blur"); + }); + setTimeout(() => { + document.body.style.pointerEvents = "auto"; + }, 500); + } else { + gaussianPortals.forEach((portal) => { + document.getElementById(portal)?.classList.remove("gaussian-blur"); + }); + } + }, [isOn]); + + return children as JSX.Element; +} diff --git a/src/frontend/components/app/confirm-alert.tsx b/src/frontend/components/app/confirm-alert.tsx index 4c9392b3..9619ec73 100644 --- a/src/frontend/components/app/confirm-alert.tsx +++ b/src/frontend/components/app/confirm-alert.tsx @@ -14,7 +14,7 @@ import { } from "@/components/ui/alert-dialog"; import { createStore } from "@/frontend/lib/store"; -import { NextPortal } from "./next-portal"; +import { AppBlur } from "./app-blur"; interface IConfirmAlertDetails { title: MessageDescriptor; @@ -51,16 +51,12 @@ export function ConfirmAlert() { store.onClose, ]); - if (!title) { - return null; - } - return ( - + - {_(title)} + {title ? _(title) : null} {t`Are you sure you want to do this?`} @@ -76,6 +72,6 @@ export function ConfirmAlert() { - + ); } diff --git a/src/frontend/components/app/next-portal.tsx b/src/frontend/components/app/next-portal.tsx deleted file mode 100644 index 51d3c82e..00000000 --- a/src/frontend/components/app/next-portal.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type { ReactNode } from "react"; -import { useEffect, useRef } from "react"; -import { createPortal } from "react-dom"; - -import { useToggle } from "@/frontend/hooks/state/useToggleState"; - -interface IProps { - children: ReactNode; -} - -const gaussianPortals = ["gaussian-portal-0", "gaussian-portal-1"]; - -export function NextPortal({ children }: IProps) { - const ref = useRef(null); - const isMounted = useToggle(); - - useEffect(() => { - ref.current = document.body; - isMounted.on(); - gaussianPortals.forEach((portal) => { - document.getElementById(portal)?.classList.add("gaussian-blur"); - }); - - return () => { - gaussianPortals.forEach((portal) => { - document.getElementById(portal)?.classList.remove("gaussian-blur"); - }); - - isMounted.off(); - }; - }, []); - - return isMounted.isOn && ref.current - ? createPortal(children, document.body) - : null; -} diff --git a/src/frontend/components/app/off-canvas.tsx b/src/frontend/components/app/off-canvas.tsx index ef621add..12f63961 100644 --- a/src/frontend/components/app/off-canvas.tsx +++ b/src/frontend/components/app/off-canvas.tsx @@ -7,7 +7,7 @@ import { cn } from "@/components/utils"; import { ScrollArea } from "../ui/scroll-area"; import { Separator } from "../ui/separator"; import { Sheet, SheetContent, SheetHeader, SheetTitle } from "../ui/sheet"; -import { NextPortal } from "./next-portal"; +import { AppBlur } from "./app-blur"; export interface IProps { show: boolean; @@ -20,12 +20,8 @@ export interface IProps { export function OffCanvas({ show, onClose, title, children, size }: IProps) { const { _ } = useLingui(); - if (!show) { - return null; - } - return ( - + - + ); } diff --git a/src/frontend/lib/colors/conversion.ts b/src/frontend/lib/colors/conversion.ts new file mode 100644 index 00000000..9da37587 --- /dev/null +++ b/src/frontend/lib/colors/conversion.ts @@ -0,0 +1,61 @@ +type TRGB = { r: number; g: number; b: number }; +type TLCH = { l: number; c: number; h: number }; + +export const hexToOklch = (hex: string) => { + const hexToRGB = (h: string): TRGB => { + const r: number = parseInt(h.slice(1, 3), 16); + const g: number = parseInt(h.slice(3, 5), 16); + const b: number = parseInt(h.slice(5, 7), 16); + return { r, g, b }; + }; + + const rgbToOklch = (rgb: TRGB): TLCH => { + const r = rgb.r / 255; + const g = rgb.g / 255; + const b = rgb.b / 255; + + const [linearR, linearG, linearB] = [r, g, b].map((c: number) => + c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4 + ); + + // Convert Linear RGB to CIE XYZ + let x: number = + linearR * 0.4124564 + linearG * 0.3575761 + linearB * 0.1804375; + let y: number = + linearR * 0.2126729 + linearG * 0.7151522 + linearB * 0.072175; + let z: number = + linearR * 0.0193339 + linearG * 0.119192 + linearB * 0.9503041; + + // Convert CIE XYZ to CIELAB + [x, y, z] = [x, y, z].map((c: number) => + c > 0.008856 ? c ** (1 / 3) : (903.3 * c + 16) / 116 + ); + let l: number = 116 * y - 16; + const a: number = 500 * (x - y); + const bStar: number = 200 * (y - z); + + // Convert CIELAB to Oklch + // let c: number = Math.sqrt(a * a + bStar * bStar); + let h: number = Math.atan2(bStar, a) * (180 / Math.PI); + if (h < 0) h += 360; + + // Assume c_max is the maximum chroma value observed or expected in your conversions + const c_max = 100; /* your determined or observed maximum chroma value */ + // Adjusted part of the rgbToOklch function for calculating 'c' + let c: number = Math.sqrt(a * a + bStar * bStar); + c = (c / c_max) * 0.37; // Scale c to be within 0 and 0.37 + + // Scale and round values to match the specified ranges + l = Math.round(((l + 16) / 116) * 1000) / 10; // Scale l to be between 0 and 1 + c = Number(c.toFixed(2)); // Ensure c is correctly scaled, adjust if necessary based on your color space calculations + h = Number(h.toFixed(1)); // h is already within 0 to 360 + + return { + l, + c, + h, + }; + }; + + return rgbToOklch(hexToRGB(hex)); +};