aboutsummaryrefslogtreecommitdiffstats
path: root/apps/browser-extension/src
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-04-22 18:28:41 +0100
committerMohamedBassem <me@mbassem.com>2024-04-23 09:56:37 +0100
commit7ddcb5fe6b7de9df3e0424598d1836d51c3f80eb (patch)
tree8c1a52f560583a0bc69df48b9fe6bc5f9d39dec8 /apps/browser-extension/src
parent5f599f29e1aed699d313b9f652934ecedef8f3ea (diff)
downloadkarakeep-7ddcb5fe6b7de9df3e0424598d1836d51c3f80eb.tar.zst
ui(extension): Use shadcn and better dark mode support
Diffstat (limited to 'apps/browser-extension/src')
-rw-r--r--apps/browser-extension/src/Layout.tsx13
-rw-r--r--apps/browser-extension/src/Logo.tsx8
-rw-r--r--apps/browser-extension/src/NotConfiguredPage.tsx8
-rw-r--r--apps/browser-extension/src/OptionsPage.tsx9
-rw-r--r--apps/browser-extension/src/SignInPage.tsx14
-rw-r--r--apps/browser-extension/src/components/ui/button.tsx57
-rw-r--r--apps/browser-extension/src/components/ui/dialog.tsx120
-rw-r--r--apps/browser-extension/src/components/ui/input.tsx24
-rw-r--r--apps/browser-extension/src/index.css73
-rw-r--r--apps/browser-extension/src/utils/ThemeProvider.tsx73
-rw-r--r--apps/browser-extension/src/utils/css.ts7
-rw-r--r--apps/browser-extension/src/utils/providers.tsx9
12 files changed, 320 insertions, 95 deletions
diff --git a/apps/browser-extension/src/Layout.tsx b/apps/browser-extension/src/Layout.tsx
index bf760362..0431f550 100644
--- a/apps/browser-extension/src/Layout.tsx
+++ b/apps/browser-extension/src/Layout.tsx
@@ -1,6 +1,7 @@
import { Home, RefreshCw, Settings, X } from "lucide-react";
import { Outlet, useNavigate } from "react-router-dom";
+import { Button } from "./components/ui/button";
import usePluginSettings from "./utils/settings";
export default function Layout() {
@@ -35,16 +36,16 @@ export default function Layout() {
</div>
<div className="flex space-x-3">
{process.env.NODE_ENV == "development" && (
- <button onClick={() => navigate(0)}>
+ <Button onClick={() => navigate(0)}>
<RefreshCw className="w-4" />
- </button>
+ </Button>
)}
- <button onClick={() => navigate("/options")}>
+ <Button onClick={() => navigate("/options")}>
<Settings className="w-4" />
- </button>
- <button onClick={() => window.close()}>
+ </Button>
+ <Button onClick={() => window.close()}>
<X className="w-4" />
- </button>
+ </Button>
</div>
</div>
</div>
diff --git a/apps/browser-extension/src/Logo.tsx b/apps/browser-extension/src/Logo.tsx
index 462bbef0..3e2e6444 100644
--- a/apps/browser-extension/src/Logo.tsx
+++ b/apps/browser-extension/src/Logo.tsx
@@ -1,9 +1,15 @@
+import logoImgWhite from "../public/logo-full-white.png";
import logoImg from "../public/logo-full.png";
export default function Logo() {
return (
<span className="flex items-center justify-center">
- <img src={logoImg} alt="hoarder logo" className="h-10" />
+ <img src={logoImg} alt="hoarder logo" className="h-10 dark:hidden" />
+ <img
+ src={logoImgWhite}
+ alt="hoarder logo"
+ className="hidden h-10 dark:block"
+ />
</span>
);
}
diff --git a/apps/browser-extension/src/NotConfiguredPage.tsx b/apps/browser-extension/src/NotConfiguredPage.tsx
index 298e9f5e..31d45d6a 100644
--- a/apps/browser-extension/src/NotConfiguredPage.tsx
+++ b/apps/browser-extension/src/NotConfiguredPage.tsx
@@ -1,6 +1,8 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
+import { Button } from "./components/ui/button";
+import { Input } from "./components/ui/input";
import Logo from "./Logo";
import usePluginSettings from "./utils/settings";
@@ -33,16 +35,14 @@ export default function NotConfiguredPage() {
<p className="text-red-500">{error}</p>
<div className="flex gap-2">
<label className="my-auto">Server Address</label>
- <input
+ <Input
name="address"
value={serverAddress}
className="h-8 flex-1 rounded-lg border border-gray-300 p-2"
onChange={(e) => setServerAddress(e.target.value)}
/>
</div>
- <button className="bg-black text-white" onClick={onSave}>
- Configure
- </button>
+ <Button onClick={onSave}>Configure</Button>
</div>
);
}
diff --git a/apps/browser-extension/src/OptionsPage.tsx b/apps/browser-extension/src/OptionsPage.tsx
index 24785857..41b72178 100644
--- a/apps/browser-extension/src/OptionsPage.tsx
+++ b/apps/browser-extension/src/OptionsPage.tsx
@@ -1,6 +1,7 @@
import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom";
+import { Button } from "./components/ui/button";
import Logo from "./Logo";
import Spinner from "./Spinner";
import usePluginSettings from "./utils/settings";
@@ -55,12 +56,14 @@ export default function OptionsPage() {
<span className="text-lg">Settings</span>
<hr />
<div className="flex gap-2">
+ <span className="my-auto">Server Address:</span>
+ {settings.address}
+ </div>
+ <div className="flex gap-2">
<span className="my-auto">Logged in as:</span>
{loggedInMessage}
</div>
- <button className="rounded-lg border border-gray-200" onClick={onLogout}>
- Logout
- </button>
+ <Button onClick={onLogout}>Logout</Button>
</div>
);
}
diff --git a/apps/browser-extension/src/SignInPage.tsx b/apps/browser-extension/src/SignInPage.tsx
index aa8699ae..4e846070 100644
--- a/apps/browser-extension/src/SignInPage.tsx
+++ b/apps/browser-extension/src/SignInPage.tsx
@@ -1,6 +1,8 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
+import { Button } from "./components/ui/button";
+import { Input } from "./components/ui/input";
import Logo from "./Logo";
import usePluginSettings from "./utils/settings";
import { api } from "./utils/trpc";
@@ -51,7 +53,7 @@ export default function SignInPage() {
<form className="flex flex-col gap-y-2" onSubmit={onSubmit}>
<div className="flex flex-col gap-y-1">
<label className="my-auto font-bold">Email</label>
- <input
+ <Input
value={formData.email}
onChange={(e) =>
setFormData((f) => ({ ...f, email: e.target.value }))
@@ -63,7 +65,7 @@ export default function SignInPage() {
</div>
<div className="flex flex-col gap-y-1">
<label className="my-auto font-bold">Password</label>
- <input
+ <Input
value={formData.password}
onChange={(e) =>
setFormData((f) => ({
@@ -76,13 +78,9 @@ export default function SignInPage() {
className="h-8 flex-1 rounded-lg border border-gray-300 p-2"
/>
</div>
- <button
- className="bg-black text-white"
- type="submit"
- disabled={isPending}
- >
+ <Button type="submit" disabled={isPending}>
Login
- </button>
+ </Button>
</form>
</div>
);
diff --git a/apps/browser-extension/src/components/ui/button.tsx b/apps/browser-extension/src/components/ui/button.tsx
new file mode 100644
index 00000000..1bd3802d
--- /dev/null
+++ b/apps/browser-extension/src/components/ui/button.tsx
@@ -0,0 +1,57 @@
+import type { VariantProps } from "class-variance-authority";
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva } from "class-variance-authority";
+
+import { cn } from "../../utils/css";
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+);
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
+ VariantProps<typeof buttonVariants> {
+ asChild?: boolean;
+}
+
+const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button";
+ return (
+ <Comp
+ className={cn(buttonVariants({ variant, size, className }))}
+ ref={ref}
+ {...props}
+ />
+ );
+ },
+);
+Button.displayName = "Button";
+
+export { Button, buttonVariants };
diff --git a/apps/browser-extension/src/components/ui/dialog.tsx b/apps/browser-extension/src/components/ui/dialog.tsx
new file mode 100644
index 00000000..997230bb
--- /dev/null
+++ b/apps/browser-extension/src/components/ui/dialog.tsx
@@ -0,0 +1,120 @@
+import * as React from "react";
+import * as DialogPrimitive from "@radix-ui/react-dialog";
+import { X } from "lucide-react";
+
+import { cn } from "../../utils/css";
+
+const Dialog = DialogPrimitive.Root;
+
+const DialogTrigger = DialogPrimitive.Trigger;
+
+const DialogPortal = DialogPrimitive.Portal;
+
+const DialogClose = DialogPrimitive.Close;
+
+const DialogOverlay = React.forwardRef<
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
+>(({ className, ...props }, ref) => (
+ <DialogPrimitive.Overlay
+ ref={ref}
+ className={cn(
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
+ className,
+ )}
+ {...props}
+ />
+));
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
+
+const DialogContent = React.forwardRef<
+ React.ElementRef<typeof DialogPrimitive.Content>,
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
+>(({ className, children, ...props }, ref) => (
+ <DialogPortal>
+ <DialogOverlay />
+ <DialogPrimitive.Content
+ ref={ref}
+ className={cn(
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
+ className,
+ )}
+ {...props}
+ >
+ {children}
+ <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
+ <X className="h-4 w-4" />
+ <span className="sr-only">Close</span>
+ </DialogPrimitive.Close>
+ </DialogPrimitive.Content>
+ </DialogPortal>
+));
+DialogContent.displayName = DialogPrimitive.Content.displayName;
+
+const DialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes<HTMLDivElement>) => (
+ <div
+ className={cn(
+ "flex flex-col space-y-1.5 text-center sm:text-left",
+ className,
+ )}
+ {...props}
+ />
+);
+DialogHeader.displayName = "DialogHeader";
+
+const DialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes<HTMLDivElement>) => (
+ <div
+ className={cn(
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
+ className,
+ )}
+ {...props}
+ />
+);
+DialogFooter.displayName = "DialogFooter";
+
+const DialogTitle = React.forwardRef<
+ React.ElementRef<typeof DialogPrimitive.Title>,
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
+>(({ className, ...props }, ref) => (
+ <DialogPrimitive.Title
+ ref={ref}
+ className={cn(
+ "text-lg font-semibold leading-none tracking-tight",
+ className,
+ )}
+ {...props}
+ />
+));
+DialogTitle.displayName = DialogPrimitive.Title.displayName;
+
+const DialogDescription = React.forwardRef<
+ React.ElementRef<typeof DialogPrimitive.Description>,
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
+>(({ className, ...props }, ref) => (
+ <DialogPrimitive.Description
+ ref={ref}
+ className={cn("text-sm text-muted-foreground", className)}
+ {...props}
+ />
+));
+DialogDescription.displayName = DialogPrimitive.Description.displayName;
+
+export {
+ Dialog,
+ DialogPortal,
+ DialogOverlay,
+ DialogClose,
+ DialogTrigger,
+ DialogContent,
+ DialogHeader,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription,
+};
diff --git a/apps/browser-extension/src/components/ui/input.tsx b/apps/browser-extension/src/components/ui/input.tsx
new file mode 100644
index 00000000..5751b004
--- /dev/null
+++ b/apps/browser-extension/src/components/ui/input.tsx
@@ -0,0 +1,24 @@
+import * as React from "react";
+
+import { cn } from "../../utils/css";
+
+export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
+
+const Input = React.forwardRef<HTMLInputElement, InputProps>(
+ ({ className, type, ...props }, ref) => {
+ return (
+ <input
+ type={type}
+ className={cn(
+ "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
+ className,
+ )}
+ ref={ref}
+ {...props}
+ />
+ );
+ },
+);
+Input.displayName = "Input";
+
+export { Input };
diff --git a/apps/browser-extension/src/index.css b/apps/browser-extension/src/index.css
index e7d4bb2f..d5c92220 100644
--- a/apps/browser-extension/src/index.css
+++ b/apps/browser-extension/src/index.css
@@ -1,72 +1 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-
-:root {
- font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
- line-height: 1.5;
- font-weight: 400;
-
- color-scheme: light dark;
- color: rgba(255, 255, 255, 0.87);
- background-color: #242424;
-
- font-synthesis: none;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-a {
- font-weight: 500;
- color: #646cff;
- text-decoration: inherit;
-}
-a:hover {
- color: #535bf2;
-}
-
-body {
- margin: 0;
- display: flex;
- place-items: center;
- min-width: 320px;
- min-height: 100vh;
-}
-
-h1 {
- font-size: 3.2em;
- line-height: 1.1;
-}
-
-button {
- border-radius: 8px;
- border: 1px solid transparent;
- padding: 0.6em 1.2em;
- font-size: 1em;
- font-weight: 500;
- font-family: inherit;
- background-color: #1a1a1a;
- cursor: pointer;
- transition: border-color 0.25s;
-}
-button:hover {
- border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
- outline: 4px auto -webkit-focus-ring-color;
-}
-
-@media (prefers-color-scheme: light) {
- :root {
- color: #213547;
- background-color: #ffffff;
- }
- a:hover {
- color: #747bff;
- }
- button {
- background-color: #f9f9f9;
- }
-}
+@import "@hoarder/tailwind-config/globals.css";
diff --git a/apps/browser-extension/src/utils/ThemeProvider.tsx b/apps/browser-extension/src/utils/ThemeProvider.tsx
new file mode 100644
index 00000000..79a0b32f
--- /dev/null
+++ b/apps/browser-extension/src/utils/ThemeProvider.tsx
@@ -0,0 +1,73 @@
+import { createContext, useContext, useEffect, useState } from "react";
+
+type Theme = "dark" | "light" | "system";
+
+interface ThemeProviderProps {
+ children: React.ReactNode;
+ defaultTheme?: Theme;
+ storageKey?: string;
+}
+
+interface ThemeProviderState {
+ theme: Theme;
+ setTheme: (theme: Theme) => void;
+}
+
+const initialState: ThemeProviderState = {
+ theme: "system",
+ setTheme: () => null,
+};
+
+const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
+
+export function ThemeProvider({
+ children,
+ defaultTheme = "system",
+ storageKey = "vite-ui-theme",
+ ...props
+}: ThemeProviderProps) {
+ const [theme, setTheme] = useState<Theme>(
+ () => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
+ );
+
+ useEffect(() => {
+ const root = window.document.documentElement;
+
+ root.classList.remove("light", "dark");
+
+ if (theme === "system") {
+ const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
+ .matches
+ ? "dark"
+ : "light";
+
+ root.classList.add(systemTheme);
+ return;
+ }
+
+ root.classList.add(theme);
+ }, [theme]);
+
+ const value = {
+ theme,
+ setTheme: (theme: Theme) => {
+ localStorage.setItem(storageKey, theme);
+ setTheme(theme);
+ },
+ };
+
+ return (
+ <ThemeProviderContext.Provider {...props} value={value}>
+ {children}
+ </ThemeProviderContext.Provider>
+ );
+}
+
+export const useTheme = () => {
+ const context = useContext(ThemeProviderContext);
+
+ if (context === undefined)
+ throw new Error("useTheme must be used within a ThemeProvider");
+
+ return context;
+};
diff --git a/apps/browser-extension/src/utils/css.ts b/apps/browser-extension/src/utils/css.ts
new file mode 100644
index 00000000..88283f01
--- /dev/null
+++ b/apps/browser-extension/src/utils/css.ts
@@ -0,0 +1,7 @@
+import type { ClassValue } from "clsx";
+import { clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/apps/browser-extension/src/utils/providers.tsx b/apps/browser-extension/src/utils/providers.tsx
index 4ca17016..4b571254 100644
--- a/apps/browser-extension/src/utils/providers.tsx
+++ b/apps/browser-extension/src/utils/providers.tsx
@@ -1,9 +1,16 @@
import { TRPCProvider } from "@hoarder/shared-react/providers/trpc-provider";
import usePluginSettings from "./settings";
+import { ThemeProvider } from "./ThemeProvider";
export function Providers({ children }: { children: React.ReactNode }) {
const { settings } = usePluginSettings();
- return <TRPCProvider settings={settings}>{children}</TRPCProvider>;
+ return (
+ <TRPCProvider settings={settings}>
+ <ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
+ {children}
+ </ThemeProvider>
+ </TRPCProvider>
+ );
}