diff --git a/package.json b/package.json index 3c31e87b..78218533 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@mantine/hooks": "^7.3.2", "@prisma/client": "^5.9.1", "@radix-ui/react-scroll-area": "^1.0.5", + "@radix-ui/react-use-escape-keydown": "^1.0.3", "@storybook/manager-api": "^7.5.1", "@storybook/theming": "^7.5.1", "@uidotdev/usehooks": "^2.4.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 335bda2c..832ed5c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ dependencies: '@radix-ui/react-scroll-area': specifier: ^1.0.5 version: 1.0.5(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-escape-keydown': + specifier: ^1.0.3 + version: 1.0.3(@types/react@18.2.31)(react@18.2.0) '@storybook/manager-api': specifier: ^7.5.1 version: 7.5.1(react-dom@18.2.0)(react@18.2.0) @@ -3232,7 +3235,6 @@ packages: '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.31)(react@18.2.0) '@types/react': 18.2.31 react: 18.2.0 - dev: true /@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.2.31)(react@18.2.0): resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==} diff --git a/src/_pages/ReaderPage/chapterContent/renderChapterContent/ChapterContentComponents.tsx b/src/_pages/ReaderPage/chapterContent/renderChapterContent/ChapterContentComponents.tsx index 7db7d583..2c132ff4 100644 --- a/src/_pages/ReaderPage/chapterContent/renderChapterContent/ChapterContentComponents.tsx +++ b/src/_pages/ReaderPage/chapterContent/renderChapterContent/ChapterContentComponents.tsx @@ -31,7 +31,7 @@ export const JesusWords = ({ children }: { children: ReactNode }) => { return ( {children} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 5a2a6dd3..905122e1 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,8 +1,11 @@ import '~/index.css' import { type Viewport } from 'next' +import { cookies } from 'next/headers' import { token } from 'styled-system/tokens' +import { THEME } from '~/state' + export { RootLayout as default } from '~/layouts' export const metadata = { @@ -14,21 +17,30 @@ export const metadata = { title: 'The Good Book', } -export const viewport = (): Viewport => ({ - initialScale: 1, - maximumScale: 1, - minimumScale: 1, - userScalable: false, - viewportFit: 'cover', - width: 'device-width', - themeColor: [ - { - media: '(prefers-color-scheme: light)', - color: token('colors.white'), - }, - { - media: '(prefers-color-scheme: dark)', - color: token('colors.neutral.800'), - }, - ], -}) +export const viewport = (): Viewport => { + const cookieStore = cookies() + const theme = cookieStore.get('theme')?.value as THEME + + const isSepiaTheme = theme === THEME.Sepia + + return { + initialScale: 1, + maximumScale: 1, + minimumScale: 1, + userScalable: false, + viewportFit: 'cover', + width: 'device-width', + themeColor: [ + { + media: '(prefers-color-scheme: light)', + color: isSepiaTheme ? token('colors.sepia.100') : token('colors.white'), + }, + { + media: '(prefers-color-scheme: dark)', + color: isSepiaTheme + ? token('colors.sepia.950') + : token('colors.neutral.800'), + }, + ], + } +} diff --git a/src/helpers/index.ts b/src/helpers/index.ts index ffcfae19..58ae4022 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -1,3 +1,4 @@ +export * from './steppedRange' export * from './withCache' export * from './withCopyToClipboard' export * from './withPerformanceLog' diff --git a/src/hooks/index.ts b/src/hooks/index.ts index bdeff59e..b5485e8b 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1 +1,2 @@ export * from './useRangeInput' +export * from './useSetupClientState' diff --git a/src/hooks/useSetupClientState.tsx b/src/hooks/useSetupClientState.tsx new file mode 100644 index 00000000..6e3e190e --- /dev/null +++ b/src/hooks/useSetupClientState.tsx @@ -0,0 +1,31 @@ +import { usePrevious } from '@mantine/hooks' +import { type PrimitiveAtom, useAtom } from 'jotai/index' +import { useHydrateAtoms } from 'jotai/utils' +import { useEffect } from 'react' + +import { setCookie } from '~/app/action' + +export const useSetupClientState = ( + atom: PrimitiveAtom, + savedValue: T, + cookieName: string, +) => { + const [value, setValue] = useAtom(atom) + + useEffect(() => { + setValue(savedValue) + }, [savedValue, setValue]) + + const prevValue = usePrevious(value) + + useEffect(() => { + if (prevValue !== value) { + void setCookie( + cookieName, + typeof value === 'string' ? value : JSON.stringify(value), + ) + } + }, [cookieName, prevValue, value]) + + useHydrateAtoms([[atom, savedValue]]) +} diff --git a/src/layouts/RootLayout/GlobalBackdrop.tsx b/src/layouts/RootLayout/GlobalBackdrop.tsx index e4095b99..f8d73a0b 100644 --- a/src/layouts/RootLayout/GlobalBackdrop.tsx +++ b/src/layouts/RootLayout/GlobalBackdrop.tsx @@ -1,13 +1,13 @@ 'use client' import { AnimatePresence, motion } from 'framer-motion' -import { useAtom } from 'jotai' +import { useAtomValue } from 'jotai' import { css } from 'styled-system/css' import { showBackdropAtom } from '~/state' export const GlobalBackdrop = () => { - const [show, setShow] = useAtom(showBackdropAtom) + const show = useAtomValue(showBackdropAtom) return ( @@ -23,7 +23,6 @@ export const GlobalBackdrop = () => { inset: 0, bg: 'bg.canvas', })} - onClick={() => setShow(false)} /> )} diff --git a/src/layouts/RootLayout/Root.layout.tsx b/src/layouts/RootLayout/Root.layout.tsx index 5b0e139f..98b87e5b 100644 --- a/src/layouts/RootLayout/Root.layout.tsx +++ b/src/layouts/RootLayout/Root.layout.tsx @@ -1,10 +1,3 @@ -import { - Inter, - Lexend, - Roboto_Condensed, - Source_Serif_4, -} from 'next/font/google' -import localFont from 'next/font/local' import { cookies } from 'next/headers' import { type ReactNode } from 'react' import { cx } from 'styled-system/css' @@ -12,10 +5,17 @@ import { macrogrid } from 'styled-system/patterns' import { SafeAreaBottom } from '~/components' import { getBookListWithCache } from '~/db' -import { GlobalBackdrop } from '~/layouts/RootLayout/GlobalBackdrop' +import { + fontClean, + fontCondensed, + fontDyslexic, + fontMono, + fontOldStyle, + fontSans, +} from '~/layouts/RootLayout/fonts' import { BottomToolbar, - SetUpPreferencesMenuState, + SetUpPersistedState, TopToolbar, VerseDetailsMenuRoot, } from '~/organisms' @@ -36,70 +36,18 @@ import { showVerseDetailsDefaultValue, type TFont, type TFontSizeOffset, + type THEME, + THEME_COOKIE, + themeDefaultValue, type TLeading, VERSE_BREAKS_LINE_COOKIE, verseBreaksLineDefaultValue, } from '~/state' +import { GlobalBackdrop } from './GlobalBackdrop' import { RootProviders } from './RootProviders' import { UseLockBodyScroll } from './UseLockBodyScroll' -const fontSans = localFont({ - src: [ - { path: './fonts/Geist-Regular.woff2', weight: '400' }, - { - path: './fonts/Geist-Regular.otf', - weight: '400', - }, - { path: './fonts/Geist-Bold.woff2', weight: '700' }, - { path: './fonts/Geist-Bold.otf', weight: '700' }, - ], - variable: '--font-sans', -}) - -const fontMono = localFont({ - src: [ - { path: './fonts/GeistMono-Regular.woff2', weight: '400' }, - { - path: './fonts/GeistMono-Regular.otf', - weight: '400', - }, - { path: './fonts/GeistMono-UltraBlack.woff2', weight: '700' }, - { path: './fonts/GeistMono-UltraBlack.otf', weight: '700' }, - ], - variable: '--font-mono', -}) - -const fontClean = Inter({ - weight: ['400', '700'], - style: ['normal'], - display: 'swap', - variable: '--font-clean', - subsets: ['latin', 'latin-ext'], -}) - -const fontDyslexic = Lexend({ - weight: ['400', '700'], - display: 'swap', - variable: '--font-dyslexic', - subsets: ['latin', 'latin-ext'], -}) - -const fontCondensed = Roboto_Condensed({ - weight: ['400', '700'], - style: ['normal'], - display: 'swap', - variable: '--font-condensed', - subsets: ['latin', 'latin-ext'], -}) - -const fontOldStyle = Source_Serif_4({ - weight: ['400', '700'], - display: 'swap', - variable: '--font-old-style', - subsets: ['latin', 'latin-ext'], -}) - const getBooleanCookieValue = ( cookieValue: string | undefined, fallback: boolean, @@ -110,6 +58,7 @@ export const RootLayout = async ({ children }: { children: ReactNode }) => { const cookieStore = cookies() + const savedTheme = cookieStore.get(THEME_COOKIE)?.value ?? themeDefaultValue const savedFontSizeOffset = cookieStore.get(FONT_SIZE_OFFSET_COOKIE)?.value ?? fontSizeOffsetDefaultValue @@ -128,7 +77,8 @@ export const RootLayout = async ({ children }: { children: ReactNode }) => { return ( - { )} > - + diff --git a/src/organisms/BottomToolbar/PreferencesMenu/FontField.tsx b/src/organisms/BottomToolbar/PreferencesMenu/FontField.tsx index 0bd89b13..deac1ba2 100644 --- a/src/organisms/BottomToolbar/PreferencesMenu/FontField.tsx +++ b/src/organisms/BottomToolbar/PreferencesMenu/FontField.tsx @@ -1,54 +1,34 @@ import { useAtomValue, useSetAtom } from 'jotai' -import { css, cx } from 'styled-system/css' -import { Flex, styled } from 'styled-system/jsx' -import { square } from 'styled-system/patterns' -import { button } from 'styled-system/recipes' -import { Icon } from '~/components' -import { FontOptionsMenuRoot } from '~/organisms/BottomToolbar/PreferencesMenu/FontOptionsMenu/FontOptionsMenuRoot' -import { fontAtom, showPreferencesMenu } from '~/state' +import { + fontAtom, + isPreferencesMenuSuspendedAtom, + showFontMenuAtom, +} from '~/state' -import { fontOptionList } from './FontOptionsMenu/FontOptions' +import { FontMenuRoot } from './FontMenu' import { FontPreview } from './FontPreview' -import { showFontOptionsAtom } from './preferencesMenu.state' +import { SelectField } from './SelectField' export const FontField = () => { const font = useAtomValue(fontAtom) - const setShowPreferencesMenu = useSetAtom(showPreferencesMenu) - const setShowFontOptions = useSetAtom(showFontOptionsAtom) - - const currFontLabel = fontOptionList.find((option) => option.value === font) - ?.label + const setShowFontMenu = useSetAtom(showFontMenuAtom) + const setIsPreferencesMenuSuspended = useSetAtom( + isPreferencesMenuSuspendedAtom, + ) return ( - - - Font - - - - + Font + + ) } diff --git a/src/organisms/BottomToolbar/PreferencesMenu/FontOptionsMenu/FontOptionsMenu.tsx b/src/organisms/BottomToolbar/PreferencesMenu/FontMenu/FontMenu.tsx similarity index 58% rename from src/organisms/BottomToolbar/PreferencesMenu/FontOptionsMenu/FontOptionsMenu.tsx rename to src/organisms/BottomToolbar/PreferencesMenu/FontMenu/FontMenu.tsx index 3a892dbc..332fb04b 100644 --- a/src/organisms/BottomToolbar/PreferencesMenu/FontOptionsMenu/FontOptionsMenu.tsx +++ b/src/organisms/BottomToolbar/PreferencesMenu/FontMenu/FontMenu.tsx @@ -6,14 +6,20 @@ import { Flex, Macrogrid } from 'styled-system/jsx' import { button } from 'styled-system/recipes' import { Header, Icon, Menu } from '~/components' -import { showPreferencesMenu } from '~/state' +import { + isPreferencesMenuSuspendedAtom, + showFontMenuAtom, + showPreferencesMenuAtom, +} from '~/state' -import { showFontOptionsAtom } from '../preferencesMenu.state' import { FontOptions } from './FontOptions' -export const FontOptionsMenu = () => { - const setShowPreferencesMenu = useSetAtom(showPreferencesMenu) - const setShowFontOptions = useSetAtom(showFontOptionsAtom) +export const FontMenu = () => { + const setShowFontMenu = useSetAtom(showFontMenuAtom) + const setIsPreferencesMenuSuspended = useSetAtom( + isPreferencesMenuSuspendedAtom, + ) + const setShowPreferencesMenuOpen = useSetAtom(showPreferencesMenuAtom) return ( @@ -23,7 +29,15 @@ export const FontOptionsMenu = () => {
+ { + setTimeout(() => { + setIsPreferencesMenuSuspended(false) + setShowPreferencesMenuOpen(false) + }, 150) + }} + > } @@ -32,8 +46,8 @@ export const FontOptionsMenu = () => { className={button({ icon: true })} onClick={(e) => { e.stopPropagation() - setShowFontOptions(false) - setTimeout(() => setShowPreferencesMenu(true), 150) + setShowFontMenu(false) + setTimeout(() => setIsPreferencesMenuSuspended(false), 150) }} > diff --git a/src/organisms/BottomToolbar/PreferencesMenu/FontMenu/FontMenuRoot.tsx b/src/organisms/BottomToolbar/PreferencesMenu/FontMenu/FontMenuRoot.tsx new file mode 100644 index 00000000..81f664a6 --- /dev/null +++ b/src/organisms/BottomToolbar/PreferencesMenu/FontMenu/FontMenuRoot.tsx @@ -0,0 +1,56 @@ +'use client' + +import { Dialog } from '@ark-ui/react' +import { useEscapeKeydown } from '@radix-ui/react-use-escape-keydown' +import { useAtom } from 'jotai' +import { useSetAtom } from 'jotai/index' +import { useEffect } from 'react' + +import { + isPreferencesMenuSuspendedAtom, + isScrollLockedAtom, + showFontMenuAtom, + showPreferencesMenuAtom, +} from '~/state' + +import { FontMenu } from './FontMenu' + +export const FontMenuRoot = () => { + const [isMenuOpen, setIsMenuOpen] = useAtom(showFontMenuAtom) + const [isPreferencesMenuSuspended, setIsPreferencesMenuSuspended] = useAtom( + isPreferencesMenuSuspendedAtom, + ) + const setShowPreferencesMenu = useSetAtom(showPreferencesMenuAtom) + const setIsBodyScrollLocked = useSetAtom(isScrollLockedAtom) + + useEffect( + () => setIsBodyScrollLocked(isMenuOpen), + [isMenuOpen, setIsBodyScrollLocked], + ) + + const hideBackdrop = () => { + setIsPreferencesMenuSuspended(false) + setShowPreferencesMenu(false) + } + + useEscapeKeydown(hideBackdrop) + + return ( + { + setIsMenuOpen(open) + !open && + !isPreferencesMenuSuspended && + setIsPreferencesMenuSuspended(false) + }} + onPointerDownOutside={hideBackdrop} + > + + + ) +} diff --git a/src/organisms/BottomToolbar/PreferencesMenu/FontOptionsMenu/FontOptions.tsx b/src/organisms/BottomToolbar/PreferencesMenu/FontMenu/FontOptions.tsx similarity index 75% rename from src/organisms/BottomToolbar/PreferencesMenu/FontOptionsMenu/FontOptions.tsx rename to src/organisms/BottomToolbar/PreferencesMenu/FontMenu/FontOptions.tsx index bf360a4f..198d3cac 100644 --- a/src/organisms/BottomToolbar/PreferencesMenu/FontOptionsMenu/FontOptions.tsx +++ b/src/organisms/BottomToolbar/PreferencesMenu/FontMenu/FontOptions.tsx @@ -1,10 +1,14 @@ import { useAtom, useSetAtom } from 'jotai' import { BleedList, SafeAreaBottom } from '~/components' -import { fontAtom, showPreferencesMenu, type TFont } from '~/state' +import { + fontAtom, + isPreferencesMenuSuspendedAtom, + showFontMenuAtom, + type TFont, +} from '~/state' import { FontPreview } from '../FontPreview' -import { showFontOptionsAtom } from '../preferencesMenu.state' export const fontOptionList = [ { value: 'sans', label: 'Sans-serif' }, @@ -16,8 +20,10 @@ export const fontOptionList = [ ] satisfies { value: TFont; label: string }[] export const FontOptions = () => { - const setShowFontOptions = useSetAtom(showFontOptionsAtom) - const setShowPreferencesMenu = useSetAtom(showPreferencesMenu) + const setShowFontMenu = useSetAtom(showFontMenuAtom) + const setIsPreferencesMenuSuspended = useSetAtom( + isPreferencesMenuSuspendedAtom, + ) const [font, setFont] = useAtom(fontAtom) return ( @@ -27,9 +33,9 @@ export const FontOptions = () => { key={value} onClick={(e) => { e.stopPropagation() - setShowFontOptions(false) - setTimeout(() => setShowPreferencesMenu(true), 150) setFont(value) + setShowFontMenu(false) + setTimeout(() => setIsPreferencesMenuSuspended(false), 150) }} selected={font === value} fontWeight="regular" diff --git a/src/organisms/BottomToolbar/PreferencesMenu/FontMenu/index.ts b/src/organisms/BottomToolbar/PreferencesMenu/FontMenu/index.ts new file mode 100644 index 00000000..7096340c --- /dev/null +++ b/src/organisms/BottomToolbar/PreferencesMenu/FontMenu/index.ts @@ -0,0 +1,2 @@ +export * from './FontMenuRoot' +export { fontOptionList } from './FontOptions' diff --git a/src/organisms/BottomToolbar/PreferencesMenu/FontOptionsMenu/FontOptionsMenuRoot.tsx b/src/organisms/BottomToolbar/PreferencesMenu/FontOptionsMenu/FontOptionsMenuRoot.tsx deleted file mode 100644 index 2b9ef8f7..00000000 --- a/src/organisms/BottomToolbar/PreferencesMenu/FontOptionsMenu/FontOptionsMenuRoot.tsx +++ /dev/null @@ -1,36 +0,0 @@ -'use client' - -import { Dialog } from '@ark-ui/react' -import { useAtom } from 'jotai' -import { useSetAtom } from 'jotai/index' -import { useEffect } from 'react' - -import { isScrollLockedAtom } from '~/state' - -import { showFontOptionsAtom } from '../preferencesMenu.state' -import { FontOptionsMenu } from './FontOptionsMenu' - -export const FontOptionsMenuRoot = () => { - const [isMenuOpen, setIsMenuOpen] = useAtom(showFontOptionsAtom) - - const setIsBodyScrollLocked = useSetAtom(isScrollLockedAtom) - - useEffect( - () => setIsBodyScrollLocked(isMenuOpen), - [isMenuOpen, setIsBodyScrollLocked], - ) - - return ( - setIsMenuOpen(open)} - onExitComplete={() => setIsMenuOpen(false)} - > - - - ) -} diff --git a/src/organisms/BottomToolbar/PreferencesMenu/Preferences.tsx b/src/organisms/BottomToolbar/PreferencesMenu/Preferences.tsx index b7359b19..fb4818f9 100644 --- a/src/organisms/BottomToolbar/PreferencesMenu/Preferences.tsx +++ b/src/organisms/BottomToolbar/PreferencesMenu/Preferences.tsx @@ -22,6 +22,7 @@ import { FontField } from './FontField' import { IncrementField } from './IncrementField' import { SwitchField } from './SwitchField' import { SwitchFieldList } from './SwitchFieldList' +import { ThemeField } from './ThemeField' const fontSizeOffsetRange = range(-3)(8) as TFontSizeOffset[] @@ -79,6 +80,9 @@ export const Preferences = () => { + + + ( - - - - -
- - - } - /> - - - - - - - -) +export const PreferencesMenu = () => { + const setShowBackdrop = useSetAtom(showBackdropAtom) + + return ( + + + + +
setShowBackdrop(false)} + > + + + } + /> + + + + + + + + ) +} diff --git a/src/organisms/BottomToolbar/PreferencesMenu/PreferencesMenuRoot.tsx b/src/organisms/BottomToolbar/PreferencesMenu/PreferencesMenuRoot.tsx index 049c2e39..d71a555a 100644 --- a/src/organisms/BottomToolbar/PreferencesMenu/PreferencesMenuRoot.tsx +++ b/src/organisms/BottomToolbar/PreferencesMenu/PreferencesMenuRoot.tsx @@ -1,25 +1,41 @@ 'use client' import { Dialog, DialogTrigger } from '@ark-ui/react' -import { useAtom } from 'jotai' +import { useAtom, useAtomValue } from 'jotai' import { useSetAtom } from 'jotai/index' import { useEffect } from 'react' import { cx } from 'styled-system/css' import { button } from 'styled-system/recipes' import { Icon } from '~/components' -import { isScrollLockedAtom, showPreferencesMenu } from '~/state' +import { + isPreferencesMenuSuspendedAtom, + isScrollLockedAtom, + showBackdropAtom, + showPreferencesMenuAtom, +} from '~/state' import { PreferencesMenu } from './PreferencesMenu' export const PreferencesMenuRoot = () => { - const [isMenuOpen, setIsMenuOpen] = useAtom(showPreferencesMenu) - + const [showPreferencesMenu, setShowPreferencesMenu] = useAtom( + showPreferencesMenuAtom, + ) + const isPreferencesMenuSuspended = useAtomValue( + isPreferencesMenuSuspendedAtom, + ) + const setShowBackdrop = useSetAtom(showBackdropAtom) const setIsBodyScrollLocked = useSetAtom(isScrollLockedAtom) + const isOpen = showPreferencesMenu && !isPreferencesMenuSuspended + + useEffect(() => { + setShowBackdrop(showPreferencesMenu) + }, [showPreferencesMenu, setShowBackdrop]) + useEffect( - () => setIsBodyScrollLocked(isMenuOpen), - [isMenuOpen, setIsBodyScrollLocked], + () => setIsBodyScrollLocked(showPreferencesMenu), + [showPreferencesMenu, setIsBodyScrollLocked], ) return ( @@ -28,13 +44,12 @@ export const PreferencesMenuRoot = () => { modal trapFocus preventScroll={false} - open={isMenuOpen} - onOpenChange={({ open }) => setIsMenuOpen(open)} + open={isOpen} + onOpenChange={({ open }) => + (open || !isPreferencesMenuSuspended) && setShowPreferencesMenu(open) + } > - setIsMenuOpen(true)} - > + diff --git a/src/organisms/BottomToolbar/PreferencesMenu/SelectField.tsx b/src/organisms/BottomToolbar/PreferencesMenu/SelectField.tsx new file mode 100644 index 00000000..a93cbe5e --- /dev/null +++ b/src/organisms/BottomToolbar/PreferencesMenu/SelectField.tsx @@ -0,0 +1,52 @@ +import { type ButtonHTMLAttributes, type DetailedHTMLProps } from 'react' +import { css, cx } from 'styled-system/css' +import { styled } from 'styled-system/jsx' +import { flex, square } from 'styled-system/patterns' +import { button } from 'styled-system/recipes' + +import { Icon } from '~/components' + +const Container = styled('div', { + base: flex.raw({ + direction: 'column', + gap: 2, + }), +}) + +const Label = styled('label', { + base: { color: 'fg.subtle', fontSize: 'sm', lineHeight: 1 }, +}) + +const Button = ({ + children, + ...props +}: DetailedHTMLProps< + ButtonHTMLAttributes, + HTMLButtonElement +>) => ( + +) + +export const SelectField = { + Container, + Label, + Button, +} diff --git a/src/organisms/BottomToolbar/PreferencesMenu/SetUpPreferencesMenuState.ts b/src/organisms/BottomToolbar/PreferencesMenu/SetUpPersistedState.ts similarity index 67% rename from src/organisms/BottomToolbar/PreferencesMenu/SetUpPreferencesMenuState.ts rename to src/organisms/BottomToolbar/PreferencesMenu/SetUpPersistedState.ts index b711dadf..d52fa7ba 100644 --- a/src/organisms/BottomToolbar/PreferencesMenu/SetUpPreferencesMenuState.ts +++ b/src/organisms/BottomToolbar/PreferencesMenu/SetUpPersistedState.ts @@ -1,12 +1,6 @@ 'use client' -import { usePrevious } from '@mantine/hooks' -import { useAtom } from 'jotai' -import { type PrimitiveAtom } from 'jotai/index' -import { useHydrateAtoms } from 'jotai/utils' -import { useEffect } from 'react' - -import { setCookie } from '~/app/action' +import { useSetupClientState } from '~/hooks' import { FONT_COOKIE, FONT_SIZE_OFFSET_COOKIE, @@ -24,37 +18,16 @@ import { showVerseDetailsAtom, type TFont, type TFontSizeOffset, + type THEME, + THEME_COOKIE, + themeAtom, type TLeading, VERSE_BREAKS_LINE_COOKIE, verseBreaksLineAtom, } from '~/state' -const useSetupClientState = ( - atom: PrimitiveAtom, - savedValue: T, - cookieName: string, -) => { - const [value, setValue] = useAtom(atom) - - useEffect(() => { - setValue(savedValue) - }, [savedValue, setValue]) - - const prevValue = usePrevious(value) - - useEffect(() => { - if (prevValue !== value) { - void setCookie( - cookieName, - typeof value === 'string' ? value : JSON.stringify(value), - ) - } - }, [cookieName, prevValue, value]) - - useHydrateAtoms([[atom, savedValue]]) -} - -export const SetUpPreferencesMenuState = ({ +export const SetUpPersistedState = ({ + savedTheme, savedFontSizeOffset, savedLeading, savedFont, @@ -64,6 +37,7 @@ export const SetUpPreferencesMenuState = ({ savedShowRedLetters, savedShowVerseDetailsReferences, }: { + savedTheme: THEME savedFontSizeOffset: TFontSizeOffset savedLeading: TLeading savedFont: TFont @@ -73,6 +47,7 @@ export const SetUpPreferencesMenuState = ({ savedShowRedLetters: boolean savedShowVerseDetailsReferences: boolean }) => { + useSetupClientState(themeAtom, savedTheme, THEME_COOKIE) useSetupClientState( fontSizeOffsetAtom, savedFontSizeOffset, diff --git a/src/organisms/BottomToolbar/PreferencesMenu/SwitchField.styles.ts b/src/organisms/BottomToolbar/PreferencesMenu/SwitchField.styles.ts index 3de9a59f..4bef6bcb 100644 --- a/src/organisms/BottomToolbar/PreferencesMenu/SwitchField.styles.ts +++ b/src/organisms/BottomToolbar/PreferencesMenu/SwitchField.styles.ts @@ -33,10 +33,9 @@ const Control = styled(ArkSwitch.Control, { disabled: { true: { cursor: 'revert', - bg: 'fg.moreFaded', + bg: 'bg.muted', _checked: { - bg: 'fg.faded', - _osDark: { bg: 'fg.subtle' }, + bg: 'bg.muted', }, }, }, @@ -66,11 +65,7 @@ const Thumb = styled(ArkSwitch.Thumb, { disabled: { true: { cursor: 'revert', - borderColor: 'fg.moreFaded', - _checked: { - borderColor: 'fg.faded', - _osDark: { borderColor: 'fg.subtle' }, - }, + borderColor: 'bg.muted', }, }, }, diff --git a/src/organisms/BottomToolbar/PreferencesMenu/ThemeField.tsx b/src/organisms/BottomToolbar/PreferencesMenu/ThemeField.tsx new file mode 100644 index 00000000..dcd5afd2 --- /dev/null +++ b/src/organisms/BottomToolbar/PreferencesMenu/ThemeField.tsx @@ -0,0 +1,28 @@ +import { useSetAtom } from 'jotai' + +import { isPreferencesMenuSuspendedAtom, showThemeMenuAtom } from '~/state' + +import { SelectField } from './SelectField' +import { ThemeMenuRoot } from './ThemeMenu' + +export const ThemeField = () => { + const setShowThemeMenu = useSetAtom(showThemeMenuAtom) + const setIsPreferencesMenuSuspended = useSetAtom( + isPreferencesMenuSuspendedAtom, + ) + + return ( + + + { + e.stopPropagation() + setIsPreferencesMenuSuspended(true) + setTimeout(() => setShowThemeMenu(true), 150) + }} + > + Theme + + + ) +} diff --git a/src/organisms/BottomToolbar/PreferencesMenu/ThemeMenu/ThemeMenu.tsx b/src/organisms/BottomToolbar/PreferencesMenu/ThemeMenu/ThemeMenu.tsx new file mode 100644 index 00000000..e906e238 --- /dev/null +++ b/src/organisms/BottomToolbar/PreferencesMenu/ThemeMenu/ThemeMenu.tsx @@ -0,0 +1,70 @@ +'use client' + +import { Dialog, Portal } from '@ark-ui/react' +import { useSetAtom } from 'jotai' +import { Flex, Macrogrid } from 'styled-system/jsx' +import { button } from 'styled-system/recipes' + +import { Header, Icon, Menu } from '~/components' +import { + isPreferencesMenuSuspendedAtom, + showPreferencesMenuAtom, + showThemeMenuAtom, +} from '~/state' + +import { ThemeOptions } from './ThemeOptions' + +export const ThemeMenu = () => { + const setShowThemeMenu = useSetAtom(showThemeMenuAtom) + const setIsPreferencesMenuSuspended = useSetAtom( + isPreferencesMenuSuspendedAtom, + ) + const setShowPreferencesMenuOpen = useSetAtom(showPreferencesMenuAtom) + + return ( + + + + +
{ + setTimeout(() => { + setIsPreferencesMenuSuspended(false) + setShowPreferencesMenuOpen(false) + }, 150) + }} + > + + + } + leftButton={ + + } + /> + + + + + + + + ) +} diff --git a/src/organisms/BottomToolbar/PreferencesMenu/ThemeMenu/ThemeMenuRoot.tsx b/src/organisms/BottomToolbar/PreferencesMenu/ThemeMenu/ThemeMenuRoot.tsx new file mode 100644 index 00000000..b36b86e3 --- /dev/null +++ b/src/organisms/BottomToolbar/PreferencesMenu/ThemeMenu/ThemeMenuRoot.tsx @@ -0,0 +1,56 @@ +'use client' + +import { Dialog } from '@ark-ui/react' +import { useEscapeKeydown } from '@radix-ui/react-use-escape-keydown' +import { useAtom } from 'jotai' +import { useSetAtom } from 'jotai/index' +import { useEffect } from 'react' + +import { + isPreferencesMenuSuspendedAtom, + isScrollLockedAtom, + showPreferencesMenuAtom, + showThemeMenuAtom, +} from '~/state' + +import { ThemeMenu } from './ThemeMenu' + +export const ThemeMenuRoot = () => { + const [isMenuOpen, setIsMenuOpen] = useAtom(showThemeMenuAtom) + const [isPreferencesMenuSuspended, setIsPreferencesMenuSuspended] = useAtom( + isPreferencesMenuSuspendedAtom, + ) + const setShowPreferencesMenu = useSetAtom(showPreferencesMenuAtom) + const setIsBodyScrollLocked = useSetAtom(isScrollLockedAtom) + + useEffect( + () => setIsBodyScrollLocked(isMenuOpen), + [isMenuOpen, setIsBodyScrollLocked], + ) + + const hideBackdrop = () => { + setIsPreferencesMenuSuspended(false) + setShowPreferencesMenu(false) + } + + useEscapeKeydown(hideBackdrop) + + return ( + { + setIsMenuOpen(open) + !open && + !isPreferencesMenuSuspended && + setIsPreferencesMenuSuspended(false) + }} + onPointerDownOutside={hideBackdrop} + > + + + ) +} diff --git a/src/organisms/BottomToolbar/PreferencesMenu/ThemeMenu/ThemeOptions.tsx b/src/organisms/BottomToolbar/PreferencesMenu/ThemeMenu/ThemeOptions.tsx new file mode 100644 index 00000000..ac50374c --- /dev/null +++ b/src/organisms/BottomToolbar/PreferencesMenu/ThemeMenu/ThemeOptions.tsx @@ -0,0 +1,43 @@ +import { useAtom, useSetAtom } from 'jotai' + +import { BleedList, SafeAreaBottom } from '~/components' +import { + isPreferencesMenuSuspendedAtom, + showThemeMenuAtom, + THEME, + themeAtom, +} from '~/state' + +export const themeOptionList = [ + { value: THEME.Default, label: 'Default' }, + { value: THEME.Sepia, label: 'Sepia' }, +] satisfies { value: THEME; label: string }[] + +export const ThemeOptions = () => { + const setShowThemeMenu = useSetAtom(showThemeMenuAtom) + const setIsPreferencesMenuSuspended = useSetAtom( + isPreferencesMenuSuspendedAtom, + ) + const [theme, setTheme] = useAtom(themeAtom) + + return ( + + {themeOptionList.map(({ value, label }) => ( + { + e.stopPropagation() + setTheme(value) + setShowThemeMenu(false) + setTimeout(() => setIsPreferencesMenuSuspended(false), 150) + }} + selected={theme === value} + fontWeight="regular" + > + {label} + + ))} + + + ) +} diff --git a/src/organisms/BottomToolbar/PreferencesMenu/ThemeMenu/index.ts b/src/organisms/BottomToolbar/PreferencesMenu/ThemeMenu/index.ts new file mode 100644 index 00000000..83f36b10 --- /dev/null +++ b/src/organisms/BottomToolbar/PreferencesMenu/ThemeMenu/index.ts @@ -0,0 +1,2 @@ +export * from './ThemeMenuRoot' +export { themeOptionList } from './ThemeOptions' diff --git a/src/organisms/BottomToolbar/PreferencesMenu/index.ts b/src/organisms/BottomToolbar/PreferencesMenu/index.ts index 68ee5f63..1be133c6 100644 --- a/src/organisms/BottomToolbar/PreferencesMenu/index.ts +++ b/src/organisms/BottomToolbar/PreferencesMenu/index.ts @@ -1,2 +1,2 @@ export * from './PreferencesMenuRoot' -export * from './SetUpPreferencesMenuState' +export * from './SetUpPersistedState' diff --git a/src/organisms/BottomToolbar/PreferencesMenu/preferencesMenu.state.ts b/src/organisms/BottomToolbar/PreferencesMenu/preferencesMenu.state.ts index deaa7d44..e69de29b 100644 --- a/src/organisms/BottomToolbar/PreferencesMenu/preferencesMenu.state.ts +++ b/src/organisms/BottomToolbar/PreferencesMenu/preferencesMenu.state.ts @@ -1,3 +0,0 @@ -import { atom } from 'jotai' - -export const showFontOptionsAtom = atom(false) diff --git a/src/organisms/BottomToolbar/index.ts b/src/organisms/BottomToolbar/index.ts index 6f0f680b..34e97be0 100644 --- a/src/organisms/BottomToolbar/index.ts +++ b/src/organisms/BottomToolbar/index.ts @@ -1,2 +1,2 @@ export { BottomToolbar } from './BottomToolbar' -export { SetUpPreferencesMenuState } from './PreferencesMenu' +export { SetUpPersistedState } from './PreferencesMenu' diff --git a/src/pandaPresetGoodBook/globalCss.ts b/src/pandaPresetGoodBook/globalCss.ts index e4987bc3..afcd30e8 100644 --- a/src/pandaPresetGoodBook/globalCss.ts +++ b/src/pandaPresetGoodBook/globalCss.ts @@ -5,6 +5,9 @@ export const globalCss = defineGlobalStyles({ '-webkit-tap-highlight-color': 'rgba(0, 0, 0, 0)', cursor: 'default', userSelect: 'none', + transition: 'colors', + transitionDuration: 'fast', + transitionTimingFunction: 'ease-out', }, ':focus-visible': { outline: 'none', diff --git a/src/pandaPresetGoodBook/pandaPresetGoodBook.ts b/src/pandaPresetGoodBook/pandaPresetGoodBook.ts index 85ac48c2..497c2bcb 100644 --- a/src/pandaPresetGoodBook/pandaPresetGoodBook.ts +++ b/src/pandaPresetGoodBook/pandaPresetGoodBook.ts @@ -12,6 +12,7 @@ export const pandaPresetGoodBook = definePreset({ conditions: { extend: { canHover: '@media (hover: hover)', + themeSepia: '[data-theme=sepia] &', }, }, globalCss, diff --git a/src/pandaPresetGoodBook/semanticTokens/colors.semanticTokens/bg.semanticTokens.ts b/src/pandaPresetGoodBook/semanticTokens/colors.semanticTokens/bg.semanticTokens.ts index 1ed4731e..ed2310e7 100644 --- a/src/pandaPresetGoodBook/semanticTokens/colors.semanticTokens/bg.semanticTokens.ts +++ b/src/pandaPresetGoodBook/semanticTokens/colors.semanticTokens/bg.semanticTokens.ts @@ -5,30 +5,50 @@ export const bg = defineSemanticTokens.colors({ value: { base: '{colors.white}', _osDark: '{colors.neutral.800}', + _themeSepia: { + base: '{colors.sepia.50}', + _osDark: '{colors.sepia.950}', + }, }, }, subtle: { value: { base: '{colors.neutral.100}', _osDark: '{colors.neutral.700}', + _themeSepia: { + base: '{colors.sepia.100}', + _osDark: '{colors.sepia.900}', + }, }, }, muted: { value: { base: '{colors.neutral.200}', _osDark: '{colors.neutral.600}', + _themeSepia: { + base: '{colors.sepia.200}', + _osDark: '{colors.sepia.800}', + }, }, }, more_muted: { value: { base: '{colors.neutral.300}', _osDark: '{colors.neutral.500}', + _themeSepia: { + base: '{colors.sepia.300}', + _osDark: '{colors.sepia.700}', + }, }, }, inverted: { value: { base: '{colors.neutral.900}', _osDark: '{colors.white}', + _themeSepia: { + base: '{colors.sepia.900}', + _osDark: '{colors.sepia.50}', + }, }, }, highlight: { diff --git a/src/pandaPresetGoodBook/semanticTokens/colors.semanticTokens/border.semanticTokens.ts b/src/pandaPresetGoodBook/semanticTokens/colors.semanticTokens/border.semanticTokens.ts index d6947ee1..e2a06afc 100644 --- a/src/pandaPresetGoodBook/semanticTokens/colors.semanticTokens/border.semanticTokens.ts +++ b/src/pandaPresetGoodBook/semanticTokens/colors.semanticTokens/border.semanticTokens.ts @@ -4,7 +4,11 @@ export const border = defineSemanticTokens.colors({ DEFAULT: { value: { base: '{colors.neutral.300}', - _osDark: '{colors.neutral.600}', + _osDark: '{colors.neutral.700}', + _themeSepia: { + base: '{colors.sepia.300}', + _osDark: '{colors.sepia.800}', + }, }, }, emphasized: { @@ -16,6 +20,10 @@ export const border = defineSemanticTokens.colors({ value: { base: '{colors.neutral.300}', _osDark: '{colors.neutral.500}', + _themeSepia: { + base: '{colors.sepia.300}', + _osDark: '{colors.sepia.700}', + }, }, }, }) diff --git a/src/pandaPresetGoodBook/semanticTokens/colors.semanticTokens/fg.semanticTokens.ts b/src/pandaPresetGoodBook/semanticTokens/colors.semanticTokens/fg.semanticTokens.ts index 2004de28..70da8c6d 100644 --- a/src/pandaPresetGoodBook/semanticTokens/colors.semanticTokens/fg.semanticTokens.ts +++ b/src/pandaPresetGoodBook/semanticTokens/colors.semanticTokens/fg.semanticTokens.ts @@ -3,44 +3,72 @@ import { defineSemanticTokens } from '@pandacss/dev' export const fg = defineSemanticTokens.colors({ DEFAULT: { value: { - _osDark: '{colors.neutral.300}', base: '{colors.neutral.900}', + _osDark: '{colors.neutral.300}', + _themeSepia: { + base: '{colors.sepia.900}', + _osDark: '{colors.sepia.300}', + }, }, }, - faded: { + muted: { value: { - _osDark: '{colors.neutral.500}', - base: '{colors.neutral.400}', + base: '{colors.neutral.600}', + _osDark: '{colors.neutral.300}', + _themeSepia: { + base: '{colors.sepia.800}', + _osDark: '{colors.sepia.300}', + }, }, }, - inverted: { + subtle: { value: { - _osDark: '{colors.neutral.950}', - base: '{colors.white}', + base: '{colors.neutral.500}', + _osDark: '{colors.neutral.400}', + _themeSepia: { + base: '{colors.sepia.600}', + _osDark: '{colors.sepia.500}', + }, }, }, - moreFaded: { + faded: { value: { - _osDark: '{colors.neutral.600}', - base: '{colors.neutral.300}', + base: '{colors.neutral.400}', + _osDark: '{colors.neutral.500}', + _themeSepia: { + base: '{colors.sepia.500}', + _osDark: '{colors.sepia.600}', + }, }, }, - muted: { + moreFaded: { value: { - _osDark: '{colors.neutral.300}', - base: '{colors.neutral.600}', + base: '{colors.neutral.300}', + _osDark: '{colors.neutral.600}', + _themeSepia: { + base: '{colors.sepia.400}', + _osDark: '{colors.sepia.900}', + }, }, }, - subtle: { + inverted: { value: { - _osDark: '{colors.neutral.400}', - base: '{colors.neutral.500}', + base: '{colors.white}', + _osDark: '{colors.neutral.900}', + _themeSepia: { + base: '{colors.sepia.50}', + _osDark: '{colors.sepia.900}', + }, }, }, - jesus_words: { + jesusWords: { value: { - _osDark: '{colors.red.400}', - base: '{colors.red.700}', + base: '{colors.red}', + _osDark: '{colors.red.light}', + _themeSepia: { + base: '{colors.red.sepia}', + _osDark: '{colors.red.sepia.light}', + }, }, }, }) diff --git a/src/pandaPresetGoodBook/tokens/colors.tokens.ts b/src/pandaPresetGoodBook/tokens/colors.tokens.ts index 2af88e6b..9bb3dd81 100644 --- a/src/pandaPresetGoodBook/tokens/colors.tokens.ts +++ b/src/pandaPresetGoodBook/tokens/colors.tokens.ts @@ -2,18 +2,34 @@ import { defineTokens } from '@pandacss/dev' export const colors = defineTokens.colors({ black: { value: '{colors.neutral.900}' }, - primary: { - 100: { value: '{colors.green.100}' }, - 200: { value: '{colors.green.200}' }, - 300: { value: '{colors.green.300}' }, - 400: { value: '{colors.green.400}' }, - 50: { value: '{colors.green.50}' }, - 500: { value: '{colors.green.500}' }, - 600: { value: '{colors.green.600}' }, - 700: { value: '{colors.green.700}' }, - 800: { value: '{colors.green.800}' }, - 900: { value: '{colors.green.900}' }, - 950: { value: '{colors.green.950}' }, + sepia: { + 50: { value: '#f4f1ea' }, + 100: { value: '#eee8dd' }, + 200: { value: '#e7dfd0' }, + 300: { value: '#dfd5bf' }, + 400: { value: '#d3c6aa' }, + 500: { value: '#bbab89' }, + 600: { value: '#a39069' }, + 700: { value: '#978362' }, + 800: { value: '#7a6950' }, + 900: { value: '#5a5041' }, + 950: { value: '#3c3631' }, + }, + red: { + DEFAULT: { + value: '{colors.red.700}', + }, + light: { + value: '#f37c7c', + }, + sepia: { + DEFAULT: { + value: '#bb523e', + }, + light: { + value: '#e88575', + }, + }, }, white: { value: 'white' }, }) diff --git a/src/state/reader.state.ts b/src/state/reader.state.ts index c6945b34..fb0288c3 100644 --- a/src/state/reader.state.ts +++ b/src/state/reader.state.ts @@ -54,11 +54,25 @@ export const selectedReferenceAtom = atom( undefined, ) -export const showPreferencesMenu = atom(false) +export const showPreferencesMenuAtom = atom(false) + +export const isPreferencesMenuSuspendedAtom = atom(false) + +export const showFontMenuAtom = atom(false) + +export const showThemeMenuAtom = atom(false) /** * PREFERENCES STATE */ +export enum THEME { + Default = 'default', + Sepia = 'sepia', +} + +export const THEME_COOKIE = 'theme' +export const themeDefaultValue = THEME.Default +export const themeAtom = atom(THEME.Default) export type TFontSizeOffset = -3 | -2 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 export const FONT_SIZE_OFFSET_COOKIE = 'font-size-offset'