remove next-themes, convert header to custom button

This commit is contained in:
Max Leiter 2022-11-15 20:50:54 -08:00
parent bff7c90e5f
commit dfe0d39fa0
21 changed files with 281 additions and 160 deletions

View file

@ -36,6 +36,11 @@
color: var(--fg);
}
/* when data-theme on html is light, change font color */
[data-theme="light"] .warning {
color: var(--bg);
}
.error {
background-color: red;
}

View file

@ -79,7 +79,7 @@ const ButtonDropdown: React.FC<
return (
<div
className={`${styles.main} ${className}`}
className={`${styles.main} ${className || ""}`}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onMouseLeave={onMouseLeave}

View file

@ -46,8 +46,13 @@
margin-left: var(--gap-half);
}
.iconLeft {
margin-right: var(--gap-half);
}
.icon svg {
display: block;
width: 100%;
height: 100%;
transform: scale(1.2) translateY(-0.05em);
}

View file

@ -7,6 +7,7 @@ type Props = React.HTMLProps<HTMLButtonElement> & {
className?: string
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
iconRight?: React.ReactNode
iconLeft?: React.ReactNode
}
// eslint-disable-next-line react/display-name
@ -20,6 +21,7 @@ const Button = forwardRef<HTMLButtonElement, Props>(
type = "button",
disabled = false,
iconRight,
iconLeft,
...props
},
ref
@ -27,11 +29,16 @@ const Button = forwardRef<HTMLButtonElement, Props>(
return (
<button
ref={ref}
className={`${styles.button} ${styles[type]} ${className}`}
className={`${styles.button} ${styles[type]} ${className || ""}`}
disabled={disabled}
onClick={onClick}
{...props}
>
{iconLeft && (
<span className={`${styles.icon} ${styles.iconLeft}`}>
{iconLeft}
</span>
)}
{children}
{iconRight && (
<span className={`${styles.icon} ${styles.iconRight}`}>

View file

@ -9,7 +9,7 @@ export default function Card({
className?: string
} & React.ComponentProps<"div">) {
return (
<div className={`${styles.card} ${className}`} {...props}>
<div className={`${styles.card} ${className || ""}`} {...props}>
<div className={styles.content}>{children}</div>
</div>
)

View file

@ -1,18 +1,14 @@
import React, { useEffect, useState } from "react"
import MoonIcon from "@geist-ui/icons/moon"
import SunIcon from "@geist-ui/icons/sun"
// import { useAllThemes, useTheme } from '@geist-ui/core'
import styles from "./header.module.css"
import { Select } from "@geist-ui/core/dist"
import { useTheme } from "next-themes"
import { useTheme } from "@components/theme/ThemeClientContextProvider"
const Controls = () => {
const [mounted, setMounted] = useState(false)
const { resolvedTheme, setTheme } = useTheme()
useEffect(() => setMounted(true), [])
if (!mounted) return null
const { theme, setTheme } = useTheme()
const switchThemes = () => {
if (resolvedTheme === "dark") {
if (theme === "dark") {
setTheme("light")
} else {
setTheme("dark")
@ -26,7 +22,7 @@ const Controls = () => {
h="28px"
pure
onChange={switchThemes}
value={resolvedTheme}
value={theme}
>
<Select.Option value="light">
<span className={styles.selectContent}>

View file

@ -11,20 +11,37 @@
align-items: center;
}
.tabs .buttons > button,
.tabs .buttons > a > button {
.tabs .buttons button {
border: none;
border-radius: 0;
cursor: pointer;
background: var(--bg);
}
.tabs .active {
border-bottom: 1px solid var(--darker-gray) !important;
.tabs .buttons > a:hover,
.tabs .buttons > button:hover {
color: var(--fg);
box-shadow: inset 0 -1px 0 var(--fg);
transition: box-shadow 0.2s ease-in-out;
}
.tabs a:active,
.tabs a:focus,
.tabs button:active,
.tabs button:focus {
color: var(--darker-gray);
}
.mobile {
position: absolute;
z-index: 1000;
display: flex;
flex-direction: column;
}
.mobile button {
z-index: 1000;
width: 100%;
}
.controls {

View file

@ -1,15 +1,12 @@
"use client"
import {
ButtonGroup,
Button,
Page,
Spacer,
useBodyScroll,
useMediaQuery
} from "@geist-ui/core/dist"
import { useCallback, useEffect, useMemo, useState } from "react"
import { useEffect, useState } from "react"
import styles from "./header.module.css"
import HomeIcon from "@geist-ui/icons/home"
@ -23,11 +20,12 @@ import YourIcon from "@geist-ui/icons/list"
import MoonIcon from "@geist-ui/icons/moon"
import SettingsIcon from "@geist-ui/icons/settings"
import SunIcon from "@geist-ui/icons/sun"
import { useTheme } from "next-themes"
// import useUserData from "@lib/hooks/use-user-data"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { signOut } from "next-auth/react"
import { useTheme } from "@components/theme/ThemeClientContextProvider"
import Button from "@components/button"
type Tab = {
name: string
@ -44,8 +42,7 @@ const Header = ({ signedIn = false, isAdmin = false }) => {
const isMobile = useMediaQuery("xs", { match: "down" })
// const { status } = useSession()
// const signedIn = status === "authenticated"
const { setTheme, resolvedTheme } = useTheme()
const { setTheme, theme } = useTheme()
useEffect(() => {
setBodyHidden(expanded)
}, [expanded, setBodyHidden])
@ -68,16 +65,16 @@ const Header = ({ signedIn = false, isAdmin = false }) => {
name: isMobile ? "Change theme" : "",
onClick: function () {
if (typeof window !== "undefined")
setTheme(resolvedTheme === "light" ? "dark" : "light")
setTheme(theme === "light" ? "dark" : "light")
},
icon: resolvedTheme === "light" ? <MoonIcon /> : <SunIcon />,
icon: theme === "light" ? <MoonIcon /> : <SunIcon />,
value: "theme"
}
]
if (isAdmin) {
defaultPages.push({
name: "admin",
name: "Admin",
icon: <SettingsIcon />,
value: "admin",
href: "/admin"
@ -87,25 +84,25 @@ const Header = ({ signedIn = false, isAdmin = false }) => {
if (signedIn)
return [
{
name: "new",
name: "New",
icon: <NewIcon />,
value: "new",
href: "/new"
},
{
name: "yours",
name: "Yours",
icon: <YourIcon />,
value: "yours",
href: "/mine"
},
{
name: "settings",
name: "Settings",
icon: <SettingsIcon />,
value: "settings",
href: "/settings"
},
{
name: "sign out",
name: "Sign Out",
icon: <SignOutIcon />,
value: "signout",
onClick: () => signOut()
@ -115,7 +112,7 @@ const Header = ({ signedIn = false, isAdmin = false }) => {
else
return [
{
name: "home",
name: "Home",
icon: <HomeIcon />,
value: "home",
href: "/"
@ -147,26 +144,29 @@ const Header = ({ signedIn = false, isAdmin = false }) => {
}
const getButton = (tab: Tab) => {
const activeStyle = pathname === tab.href ? styles.active : ""
const isActive = pathname === tab.href
const activeStyle = isActive ? styles.active : ""
if (tab.onClick) {
return (
<Button
auto={isMobile ? false : true}
key={tab.value}
icon={tab.icon}
iconLeft={tab.icon}
onClick={() => onTabChange(tab.value)}
className={`${styles.tab} ${activeStyle}`}
shadow={false}
aria-label={tab.name}
aria-current={isActive ? "page" : undefined}
>
{tab.name ? tab.name : undefined}
</Button>
)
} else if (tab.href) {
return (
<Link key={tab.value} href={tab.href} className={styles.tab}>
<Button auto={isMobile ? false : true} icon={tab.icon} shadow={false}>
{tab.name ? tab.name : undefined}
</Button>
<Link
key={tab.value}
href={tab.href}
className={`${styles.tab} ${activeStyle}`}
>
<Button iconLeft={tab.icon}>{tab.name ? tab.name : undefined}</Button>
</Link>
)
}
@ -180,28 +180,14 @@ const Header = ({ signedIn = false, isAdmin = false }) => {
<div className={styles.buttons}>{buttons}</div>
</div>
<div className={styles.controls}>
<Button
effect={false}
auto
type="abort"
onClick={() => setExpanded(!expanded)}
aria-label="Menu"
>
<Spacer height={5 / 6} width={0} />
<Button onClick={() => setExpanded(!expanded)} aria-label="Menu">
<MenuIcon />
</Button>
</div>
{/* setExpanded should occur elsewhere; we don't want to close if they change themes */}
{isMobile && expanded && (
<div className={styles.mobile} onClick={() => setExpanded(!expanded)}>
<ButtonGroup
vertical
style={{
background: "var(--bg)"
}}
>
{buttons}
</ButtonGroup>
{buttons}
</div>
)}
</Page.Header>

View file

@ -0,0 +1,5 @@
import styles from "./page.module.css"
export default function Page({ children }: { children: React.ReactNode }) {
return <div className={styles.page}>{children}</div>
}

View file

@ -0,0 +1,10 @@
.page {
max-width: 100vw;
min-height: 100vh;
box-sizing: border-box;
position: relative;
width: 100%;
height: auto;
padding: 0 calc(1.34 * 16px) 0 calc(1.34 * 16px);
margin: 0 auto 0 auto;
}

View file

@ -1,5 +1,5 @@
'use client';
import { Fieldset, Text, Divider } from "@geist-ui/core/dist"
"use client"
import Card from "@components/card"
import styles from "./settings-group.module.css"
type Props = {
@ -9,13 +9,11 @@ type Props = {
const SettingsGroup = ({ title, children }: Props) => {
return (
<Fieldset width={'100%'}>
<Fieldset.Content>
<Text h4>{title}</Text>
</Fieldset.Content>
<Divider />
<Fieldset.Content className={styles.content}>{children}</Fieldset.Content>
</Fieldset>
<Card>
<h4>{title}</h4>
<hr />
<div className={styles.content}>{children}</div>
</Card>
)
}

View file

@ -0,0 +1,62 @@
"use client"
import {
FunctionComponent,
PropsWithChildren,
useCallback,
useMemo
} from "react"
import React, { useContext, useState, createContext } from "react"
import { DEFAULT_THEME, Theme, THEME_COOKIE_NAME } from "./theme"
import { setCookie } from "cookies-next"
interface UseThemeProps {
theme: Theme
setTheme: (theme: Theme) => void
}
const ThemeContext = createContext<UseThemeProps | null>(null)
export function useTheme(): {
theme: Theme
setTheme: (theme: Theme) => void
} {
return (
useContext(ThemeContext) || {
theme: DEFAULT_THEME,
setTheme: () => {}
}
)
}
interface Props extends PropsWithChildren<{}> {
defaultTheme: Theme
}
const ThemeClientContextProvider: FunctionComponent<Props> = ({
defaultTheme,
children
}) => {
const [theme, setThemeState] = useState<Theme>(defaultTheme)
const setCookieAndDocument = useCallback(
(theme: Theme) => {
setThemeState(theme)
setCookie(THEME_COOKIE_NAME, theme)
document.documentElement.setAttribute("data-theme", theme)
},
[setThemeState]
)
const setTheme = useCallback(
(theme: Theme) => {
setCookieAndDocument(theme)
},
[setCookieAndDocument]
)
const value = useMemo(() => ({ theme, setTheme }), [theme, setTheme])
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
}
export default ThemeClientContextProvider

View file

@ -0,0 +1,26 @@
import type { FunctionComponent, PropsWithChildren } from "react";
import ThemeClientContextProvider from "./ThemeClientContextProvider";
import ThemeServerContextProvider, {
useServerTheme,
} from "./ThemeServerContextProvider";
const ThemeProviderWrapper: FunctionComponent<PropsWithChildren<{}>> = ({
children,
}) => {
const theme = useServerTheme();
return (
<ThemeClientContextProvider defaultTheme={theme}>
{children}
</ThemeClientContextProvider>
);
};
const ThemeProvider: FunctionComponent<PropsWithChildren<{}>> = ({ children }) => {
return (
<ThemeServerContextProvider>
<ThemeProviderWrapper>{children}</ThemeProviderWrapper>
</ThemeServerContextProvider>
);
};
export default ThemeProvider;

View file

@ -0,0 +1,24 @@
import type { FunctionComponent, PropsWithChildren } from "react";
// @ts-ignore -- createServerContext is not in @types/react atm
import { useContext, createServerContext } from "react";
import { cookies } from "next/headers";
import { Theme, THEME_COOKIE_NAME } from "./theme";
import { DEFAULT_THEME } from "./theme";
const ThemeContext = createServerContext<Theme | null>(null);
export function useServerTheme(): Theme {
return useContext(ThemeContext);
}
const ThemeServerContextProvider: FunctionComponent<PropsWithChildren<{}>> = ({
children,
}) => {
const cookiesList = cookies();
const theme = cookiesList.get(THEME_COOKIE_NAME)?.value ?? DEFAULT_THEME;
return (
<ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>
);
};
export default ThemeServerContextProvider;

View file

@ -0,0 +1,5 @@
export type Theme = "light" | "dark";
export const DEFAULT_THEME: Theme = "light";
export const THEME_COOKIE_NAME = "drift-theme";

View file

@ -1,6 +1,4 @@
"use client"
import { Card } from "@geist-ui/core/dist"
import Card from "@components/card"
import * as RadixTooltip from "@radix-ui/react-tooltip"
import "./tooltip.css"
@ -16,14 +14,12 @@ const Tooltip = ({
} & RadixTooltip.TooltipProps) => {
return (
<RadixTooltip.Root {...props}>
<RadixTooltip.Trigger asChild className={className}>{children}</RadixTooltip.Trigger>
<RadixTooltip.Trigger asChild className={className}>
{children}
</RadixTooltip.Trigger>
<RadixTooltip.Content>
<Card className="tooltip">
<Card.Body margin={0} padding={1 / 2}>
{content}
</Card.Body>
</Card>
<Card className="tooltip">{content}</Card>
</RadixTooltip.Content>
</RadixTooltip.Root>
)

View file

@ -1,31 +1,46 @@
import "@styles/globals.css"
import { ServerThemeProvider } from "next-themes"
import { LayoutWrapper } from "./root-layout-wrapper"
import styles from '@styles/Home.module.css';
import { cookies } from "next/headers";
import { getSession } from "@lib/server/session";
import styles from "@styles/Home.module.css"
import { getSession } from "@lib/server/session"
import ThemeProvider from "@components/theme/ThemeProvider"
import { THEME_COOKIE_NAME } from "@components/theme/theme"
import { useServerTheme } from "@components/theme/ThemeServerContextProvider"
interface RootLayoutProps {
children: React.ReactNode
}
export default async function RootLayout({ children }: RootLayoutProps) {
export default async function RootLayout({ children }: RootLayoutProps) {
// TODO: this opts out of SSG
const session = await getSession()
return (
<ServerThemeProvider
disableTransitionOnChange
attribute="data-theme"
enableColorScheme
>
<html lang="en">
<head>
</head>
<html lang="en">
<head>
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
var theme = document.cookie
.split('; ')
.find(row => row.startsWith('${THEME_COOKIE_NAME}='))
.split('=')[1];
document.documentElement.setAttribute('data-theme', theme);
console.log("theme on load", theme)
})();
`,
}}
/>
</head>
<ThemeProvider>
<body className={styles.main}>
<LayoutWrapper signedIn={Boolean(session?.user)} isAdmin={session?.user.role === "admin"}>{children}</LayoutWrapper>
<LayoutWrapper
signedIn={Boolean(session?.user)}
isAdmin={session?.user.role === "admin"}
>
{children}
</LayoutWrapper>
</body>
</html>
</ServerThemeProvider>
</ThemeProvider>
</html>
)
}

View file

@ -1,73 +1,24 @@
"use client"
import Header from "@components/header"
import { CssBaseline, GeistProvider, Page, Themes } from "@geist-ui/core/dist"
import { ThemeProvider } from "next-themes"
import Page from "@components/page"
import * as RadixTooltip from "@radix-ui/react-tooltip"
export function LayoutWrapper({
children,
signedIn,
isAdmin,
isAdmin
}: {
children: React.ReactNode
signedIn?: boolean
isAdmin?: boolean
}) {
const customTheme = Themes.createFromLight({
type: "custom",
palette: {
background: "var(--bg)",
foreground: "var(--fg)",
accents_1: "var(--lightest-gray)",
accents_2: "var(--lighter-gray)",
accents_3: "var(--light-gray)",
accents_4: "var(--gray)",
accents_5: "var(--darker-gray)",
accents_6: "var(--darker-gray)",
accents_7: "var(--darkest-gray)",
accents_8: "var(--darkest-gray)",
border: "var(--light-gray)",
warning: "var(--warning)"
},
expressiveness: {
dropdownBoxShadow: "0 0 0 1px var(--lighter-gray)",
shadowSmall: "0 0 0 1px var(--lighter-gray)",
shadowLarge: "0 0 0 1px var(--lighter-gray)",
shadowMedium: "0 0 0 1px var(--lighter-gray)"
},
layout: {
gap: "var(--gap)",
gapHalf: "var(--gap-half)",
gapQuarter: "var(--gap-quarter)",
gapNegative: "var(--gap-negative)",
gapHalfNegative: "var(--gap-half-negative)",
gapQuarterNegative: "var(--gap-quarter-negative)",
radius: "var(--radius)"
},
font: {
mono: "var(--font-mono)",
sans: "var(--font-sans)"
}
})
return (
<RadixTooltip.Provider delayDuration={200}>
<GeistProvider themes={[customTheme]} themeType={"custom"}>
<ThemeProvider
disableTransitionOnChange
cookieName="drift-theme"
attribute="data-theme"
>
<CssBaseline />
<Page width={"100%"}>
<Page.Header>
<Header isAdmin={isAdmin} signedIn={signedIn} />
</Page.Header>
{children}
</Page>
</ThemeProvider>
</GeistProvider>
<Page>
<Header isAdmin={isAdmin} signedIn={signedIn} />
{children}
</Page>
</RadixTooltip.Provider>
)
}

View file

@ -47,6 +47,7 @@
--border: var(--lighter-gray);
--warning: rgb(27, 134, 23);
--link: #3291ff;
color-scheme: dark;
}
[data-theme="light"] {
@ -69,8 +70,10 @@
--header-bg: rgba(255, 255, 255, 0.8);
--gray-alpha: rgba(19, 20, 21, 0.5);
--selection: var(0, 0, 0, .6);
color-scheme: light;
}
* {
box-sizing: border-box;
}
@ -96,7 +99,6 @@ body {
font-family: var(--font-sans);
display: flex;
flex-direction: column;
/* TODO: this should be unnecessary. Overides the browser background for color-scheme while geist-ui catches up */
background: var(--bg);
}
@ -131,6 +133,12 @@ code {
font-family: var(--font-mono) !important;
}
hr {
border: 0;
border-bottom: 1px solid var(--light-gray);
margin: var(--gap) 0;
}
@media print {
:root {
--bg: #fff;
@ -166,3 +174,4 @@ main {
margin-top: 0 !important;
padding-top: 0 !important;
}

View file

@ -27,7 +27,7 @@
"jest": "^29.3.1",
"next": "13.0.3",
"next-auth": "^4.16.4",
"next-themes": "npm:@wits/next-themes@0.2.7",
"next-themes": "^0.2.1",
"rc-table": "7.24.1",
"react": "18.2.0",
"react-datepicker": "4.8.0",
@ -69,5 +69,9 @@
"components",
"lib"
]
},
"overrides": {
"react": "18.2.0",
"react-dom": "18.2.0"
}
}

View file

@ -25,7 +25,7 @@ specifiers:
katex: ^0.16.3
next: 13.0.3
next-auth: ^4.16.4
next-themes: npm:@wits/next-themes@0.2.7
next-themes: ^0.2.1
next-unused: 0.0.6
prettier: 2.6.2
prisma: ^4.6.1
@ -58,7 +58,7 @@ dependencies:
jest: 29.3.1_@types+node@17.0.23
next: 13.0.3_biqbaboplfbrettd7655fr4n2y
next-auth: 4.16.4_ogpkrxaz2lg6nectum6dl66tn4
next-themes: /@wits/next-themes/0.2.7_ogpkrxaz2lg6nectum6dl66tn4
next-themes: 0.2.1_ogpkrxaz2lg6nectum6dl66tn4
rc-table: 7.24.1_biqbaboplfbrettd7655fr4n2y
react: 18.2.0
react-datepicker: 4.8.0_biqbaboplfbrettd7655fr4n2y
@ -1594,18 +1594,6 @@ packages:
- supports-color
dev: false
/@wits/next-themes/0.2.7_ogpkrxaz2lg6nectum6dl66tn4:
resolution: {integrity: sha512-CpmNH3RRqf2w0i1Xbrz5GKNE/d5gMq1oBlGpofY9LWcjH225nUgrxP15wKRITRAbn68ERDbsBGEBiaRECTmQag==}
peerDependencies:
next: '*'
react: '*'
react-dom: '*'
dependencies:
next: 13.0.3_biqbaboplfbrettd7655fr4n2y
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
dev: false
/abbrev/1.1.1:
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
dev: false
@ -5129,6 +5117,18 @@ packages:
uuid: 8.3.2
dev: false
/next-themes/0.2.1_ogpkrxaz2lg6nectum6dl66tn4:
resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==}
peerDependencies:
next: '*'
react: '*'
react-dom: '*'
dependencies:
next: 13.0.3_biqbaboplfbrettd7655fr4n2y
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
dev: false
/next-unused/0.0.6:
resolution: {integrity: sha512-dHFNNBanFq4wvYrULtsjfWyZ6BzOnr5VYI9EYMGAZYF2vkAhFpj2JOuT5Wu2o3LbFSG92PmAZnSUF/LstF82pA==}
hasBin: true