Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): dark mode detection #2739

Merged
merged 8 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading