diff options
| author | MohamedBassem <me@mbassem.com> | 2024-04-22 18:28:41 +0100 |
|---|---|---|
| committer | MohamedBassem <me@mbassem.com> | 2024-04-23 09:56:37 +0100 |
| commit | 7ddcb5fe6b7de9df3e0424598d1836d51c3f80eb (patch) | |
| tree | 8c1a52f560583a0bc69df48b9fe6bc5f9d39dec8 /apps/browser-extension/src | |
| parent | 5f599f29e1aed699d313b9f652934ecedef8f3ea (diff) | |
| download | karakeep-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.tsx | 13 | ||||
| -rw-r--r-- | apps/browser-extension/src/Logo.tsx | 8 | ||||
| -rw-r--r-- | apps/browser-extension/src/NotConfiguredPage.tsx | 8 | ||||
| -rw-r--r-- | apps/browser-extension/src/OptionsPage.tsx | 9 | ||||
| -rw-r--r-- | apps/browser-extension/src/SignInPage.tsx | 14 | ||||
| -rw-r--r-- | apps/browser-extension/src/components/ui/button.tsx | 57 | ||||
| -rw-r--r-- | apps/browser-extension/src/components/ui/dialog.tsx | 120 | ||||
| -rw-r--r-- | apps/browser-extension/src/components/ui/input.tsx | 24 | ||||
| -rw-r--r-- | apps/browser-extension/src/index.css | 73 | ||||
| -rw-r--r-- | apps/browser-extension/src/utils/ThemeProvider.tsx | 73 | ||||
| -rw-r--r-- | apps/browser-extension/src/utils/css.ts | 7 | ||||
| -rw-r--r-- | apps/browser-extension/src/utils/providers.tsx | 9 |
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> + ); } |
