Skip to content

Commit

Permalink
feat(ui): dark mode detection (keephq#2739)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kiryous authored Dec 4, 2024
1 parent 40d3733 commit 90f89ab
Show file tree
Hide file tree
Showing 10 changed files with 212 additions and 93 deletions.
5 changes: 5 additions & 0 deletions keep-ui/app/(keep)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import { PHProvider } from "../posthog-provider";
import dynamic from "next/dynamic";
import ReadOnlyBanner from "../read-only-banner";
import { auth } from "@/auth";
import { ThemeScript } from "@/shared/ui/theme/ThemeScript";
import "@/app/globals.css";
import "react-toastify/dist/ReactToastify.css";
import { WatchUpdateTheme } from "@/shared/ui/theme/WatchUpdateTheme";

const PostHogPageView = dynamic(() => import("@/shared/ui/PostHogPageView"), {
ssr: false,
Expand All @@ -35,6 +37,8 @@ export default async function RootLayout({ children }: RootLayoutProps) {
return (
<html lang="en" className={`bg-gray-50 ${mulish.className}`}>
<body className="h-screen flex flex-col lg:grid lg:grid-cols-[fit-content(250px)_30px_auto] lg:grid-rows-1 lg:has-[aside[data-minimized='true']]:grid-cols-[0px_30px_auto]">
{/* ThemeScript must be the first thing to avoid flickering */}
<ThemeScript />
<ConfigProvider config={config}>
<PHProvider>
<NextAuthProvider session={session}>
Expand All @@ -55,6 +59,7 @@ export default async function RootLayout({ children }: RootLayoutProps) {
</NextAuthProvider>
</PHProvider>
</ConfigProvider>
<WatchUpdateTheme />

{/** footer */}
{process.env.GIT_COMMIT_HASH && (
Expand Down
59 changes: 0 additions & 59 deletions keep-ui/app/dark-mode-toggle.tsx

This file was deleted.

20 changes: 20 additions & 0 deletions keep-ui/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@
@tailwind components;
@tailwind utilities;

/* TODO: use proper tailwind/css-variables solution */
/**
* Taken from https://dev.to/jochemstoel/re-add-dark-mode-to-any-website-with-just-a-few-lines-of-code-phl
*/
html.workaround-dark {
filter: invert(100%) hue-rotate(180deg) contrast(80%) !important;
background: #fff;

& .line-content {
background-color: #fefefe;
}

& .workaround-dark-hidden {
display: none;
}

& .workaround-dark-visible {
display: block !important;
}
}
/* https://github.com/vercel/next.js/discussions/13387
nextjs-portal {
display: none;
Expand Down
14 changes: 10 additions & 4 deletions keep-ui/components/LinkWithIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type LinkWithIconProps = {
className?: string;
testId?: string;
isExact?: boolean;
iconClassName?: string;
} & LinkProps &
AnchorHTMLAttributes<HTMLAnchorElement>;

Expand All @@ -30,6 +31,7 @@ export const LinkWithIcon = ({
className,
testId,
isExact = false,
iconClassName,
...restOfLinkProps
}: LinkWithIconProps) => {
const pathname = usePathname();
Expand All @@ -40,10 +42,14 @@ export const LinkWithIcon = ({
restOfLinkProps.href?.toString() || ""
);

const iconClasses = clsx("group-hover:text-orange-400", {
"text-orange-400": isActive,
"text-black": !isActive,
});
const iconClasses = clsx(
"group-hover:text-orange-400",
{
"text-orange-400": isActive,
"text-black": !isActive,
},
iconClassName
);

const textClasses = clsx("truncate", {
"text-orange-400": isActive,
Expand Down
60 changes: 32 additions & 28 deletions keep-ui/components/navbar/UserInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,18 @@ import { Session } from "next-auth";
import { useConfig } from "utils/hooks/useConfig";
import { AuthType } from "@/utils/authenticationType";
import Link from "next/link";
import { LuSlack } from "react-icons/lu";
import { AiOutlineRight } from "react-icons/ai";
import { VscDebugDisconnect } from "react-icons/vsc";
import DarkModeToggle from "app/dark-mode-toggle";
import { useFloating } from "@floating-ui/react";
import { Icon, Subtitle } from "@tremor/react";
import UserAvatar from "./UserAvatar";
import * as Frigade from "@frigade/react";
import { useState } from "react";
import Onboarding from "./Onboarding";
import { useSignOut } from "@/shared/lib/hooks/useSignOut";
import { FaSlack } from "react-icons/fa";
import { ThemeControl } from "@/shared/ui/theme/ThemeControl";
import { HiOutlineDocumentText } from "react-icons/hi2";

const ONBOARDING_FLOW_ID = "flow_FHDz1hit";

Expand All @@ -37,18 +38,12 @@ const UserDropdown = ({ session }: UserDropdownProps) => {

const isNoAuth = configData?.AUTH_TYPE === AuthType.NOAUTH;
return (
<Menu as="li" ref={refs.setReference}>
<Menu as="li" ref={refs.setReference} className="w-full">
<Menu.Button className="flex items-center justify-between w-full text-sm pl-2.5 pr-2 py-1 text-gray-700 hover:bg-stone-200/50 font-medium rounded-lg hover:text-orange-400 focus:ring focus:ring-orange-300 group capitalize">
<span className="space-x-3 flex items-center w-full">
<UserAvatar image={image} name={name ?? email} />{" "}
<Subtitle className="truncate">{name ?? email}</Subtitle>
</span>

<Icon
className="text-gray-700 font-medium px-0"
size="xs"
icon={AiOutlineRight}
/>
</Menu.Button>

<Menu.Items
Expand Down Expand Up @@ -97,24 +92,6 @@ export const UserInfo = ({ session }: UserInfoProps) => {
return (
<>
<ul className="space-y-2 p-2">
<li>
<LinkWithIcon href="/providers" icon={VscDebugDisconnect}>
Providers
</LinkWithIcon>
</li>
<li>
{/* TODO: slows everything down. needs to be replaced */}
<DarkModeToggle />
</li>
<li>
<LinkWithIcon
icon={LuSlack}
href="https://slack.keephq.dev/"
target="_blank"
>
Join our Slack
</LinkWithIcon>
</li>
{flow?.isCompleted === false && (
<li>
<Frigade.ProgressBadge
Expand All @@ -130,7 +107,34 @@ export const UserInfo = ({ session }: UserInfoProps) => {
/>
</li>
)}
{session && <UserDropdown session={session} />}
<li>
<LinkWithIcon href="/providers" icon={VscDebugDisconnect}>
Providers
</LinkWithIcon>
</li>
<li className="flex items-center gap-2">
<LinkWithIcon
icon={FaSlack}
href="https://slack.keephq.dev/"
className="w-auto pr-3.5"
target="_blank"
>
Join Slack
</LinkWithIcon>
<LinkWithIcon
icon={HiOutlineDocumentText}
iconClassName="w-4"
href="https://docs.keephq.dev/"
className="w-auto px-3.5"
target="_blank"
>
Docs
</LinkWithIcon>
</li>
<div className="flex items-center justify-between">
{session && <UserDropdown session={session} />}
<ThemeControl className="text-sm size-10 flex items-center justify-center font-medium rounded-lg focus:ring focus:ring-orange-300 hover:!bg-stone-200/50" />
</div>
</ul>
</>
);
Expand Down
8 changes: 6 additions & 2 deletions keep-ui/components/ui/DropdownMenu/DropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,10 @@ const MenuComponent = React.forwardRef<
data-open={isOpen ? "" : undefined}
data-nested={isNested ? "" : undefined}
data-focus-inside={hasFocusInside ? "" : undefined}
className={isNested ? "DropdownMenuItem" : "DropdownMenuButton"}
className={clsx(
isNested ? "DropdownMenuItem" : "DropdownMenuButton",
props.className
)}
{...getReferenceProps(
parent.getItemProps({
...props,
Expand Down Expand Up @@ -237,7 +240,8 @@ const DropdownDropdownMenuItem = React.forwardRef<
role="DropdownMenuItem"
className={clsx(
"DropdownMenuItem",
props.variant === "destructive" && "text-red-500"
props.variant === "destructive" && "text-red-500",
props.className
)}
tabIndex={isActive ? 0 : -1}
disabled={disabled}
Expand Down
1 change: 1 addition & 0 deletions keep-ui/shared/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const LOCALSTORAGE_THEME_KEY = "theme";
61 changes: 61 additions & 0 deletions keep-ui/shared/ui/theme/ThemeControl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { LOCALSTORAGE_THEME_KEY } from "@/shared/constants";
import {
ComputerDesktopIcon,
MoonIcon,
SunIcon,
} from "@heroicons/react/20/solid";
import { useLocalStorage } from "utils/hooks/useLocalStorage";
import { DropdownMenu } from "@/components/ui/DropdownMenu/DropdownMenu";
import clsx from "clsx";

const THEMES = {
light: { id: "light", icon: SunIcon, title: "Light" },
dark: { id: "dark", icon: MoonIcon, title: "Dark" },
system: { id: "system", icon: ComputerDesktopIcon, title: "System" },
};

export function ThemeControl({ className }: { className?: string }) {
const [theme, setTheme] = useLocalStorage<string | null>(
LOCALSTORAGE_THEME_KEY,
null
);

const updateTheme = (theme: string) => {
setTheme(theme === "system" ? null : theme);
if (theme !== "system") {
document.documentElement.classList[theme === "dark" ? "add" : "remove"](
"workaround-dark"
);
// If system theme is selected, <WatchUpdateTheme /> will handle the rest
}
};

const value = theme === null ? "system" : theme;

return (
<DropdownMenu.Menu
icon={() => (
<>
<span className="workaround-dark-hidden">
<SunIcon className="w-4 h-4" />
</span>
<span className="hidden workaround-dark-visible">
<MoonIcon className="w-4 h-4" />
</span>
</>
)}
label=""
className={clsx(value !== "system" && "text-tremor-brand", className)}
>
{Object.values(THEMES).map(({ id, icon: Icon, title }) => (
<DropdownMenu.Item
key={id}
icon={Icon}
label={title}
onClick={() => updateTheme(id)}
className={clsx(id === value && "text-tremor-brand")}
/>
))}
</DropdownMenu.Menu>
);
}
30 changes: 30 additions & 0 deletions keep-ui/shared/ui/theme/ThemeScript.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"use client";

import { LOCALSTORAGE_THEME_KEY } from "../../constants";

export const ThemeScript = () => {
return (
<script
dangerouslySetInnerHTML={{
__html: `
try {
let theme = localStorage.getItem('keephq-${LOCALSTORAGE_THEME_KEY}');
if (theme) {
theme = JSON.parse(theme);
}
if (!theme) {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light'
}
document.documentElement.classList[theme === "dark" ? "add" : "remove"](
"workaround-dark"
);
} catch (e) {}
`,
}}
/>
);
};
Loading

0 comments on commit 90f89ab

Please sign in to comment.