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 (
+
+ )
+}
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 (
-
- )
-}
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 (
+
+ )
+}
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'