Skip to content

Commit c919584

Browse files
committed
feat: vaul in mobile
Signed-off-by: Innei <[email protected]>
1 parent 180449e commit c919584

File tree

6 files changed

+126
-1
lines changed

6 files changed

+126
-1
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"socket.io-client": "4.7.2",
8080
"tailwind-merge": "2.0.0",
8181
"uniqolor": "1.1.0",
82+
"vaul": "0.7.7",
8283
"xss": "1.0.14"
8384
},
8485
"devDependencies": {

pnpm-lock.yaml

+17
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/layout.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,9 @@ export default async function RootLayout(props: Props) {
155155
appConfig={themeConfig.config}
156156
/>
157157

158-
<Root>{children}</Root>
158+
<div data-theme>
159+
<Root>{children}</Root>
160+
</div>
159161

160162
<TocAutoScroll />
161163
<SearchPanelWithHotKey />

src/components/ui/sheet/Sheet.tsx

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import React, { useEffect, useMemo, useState } from 'react'
2+
import { atom, useStore } from 'jotai'
3+
import { Drawer } from 'vaul'
4+
import type { FC, PropsWithChildren } from 'react'
5+
6+
export interface PresentSheetProps {
7+
content: JSX.Element | FC
8+
open?: boolean
9+
onOpenChange?: (value: boolean) => void
10+
title?: string
11+
zIndex?: number
12+
dismissible?: boolean
13+
}
14+
15+
export const sheetStackAtom = atom([] as HTMLDivElement[])
16+
17+
export const PresentSheet: FC<PropsWithChildren<PresentSheetProps>> = (
18+
props,
19+
) => {
20+
const { content, children, zIndex = 998, title, dismissible = true } = props
21+
const nextRootProps = useMemo(() => {
22+
const nextProps = {} as any
23+
if (props.open !== undefined) {
24+
nextProps.open = props.open
25+
}
26+
27+
if (props.onOpenChange !== undefined) {
28+
nextProps.onOpenChange = props.onOpenChange
29+
}
30+
31+
return nextProps
32+
}, [props])
33+
const [holderRef, setHolderRef] = useState<HTMLDivElement | null>()
34+
const store = useStore()
35+
36+
useEffect(() => {
37+
const holder = holderRef
38+
if (!holder) return
39+
store.set(sheetStackAtom, (p) => {
40+
return p.concat(holder)
41+
})
42+
43+
return () => {
44+
store.set(sheetStackAtom, (p) => {
45+
return p.filter((item) => item !== holder)
46+
})
47+
}
48+
}, [holderRef, store])
49+
50+
const Root = Drawer.Root
51+
52+
const overlayZIndex = zIndex - 1
53+
const contentZIndex = zIndex
54+
55+
return (
56+
<Root dismissible={dismissible} {...nextRootProps}>
57+
<Drawer.Trigger asChild>{children}</Drawer.Trigger>
58+
<Drawer.Portal>
59+
<Drawer.Content
60+
style={{
61+
zIndex: contentZIndex,
62+
}}
63+
className="fixed bottom-0 left-0 right-0 mt-24 flex max-h-[95vh] flex-col rounded-t-[10px] bg-base-100 p-4"
64+
>
65+
{dismissible && (
66+
<div className="mx-auto mb-8 h-1.5 w-12 flex-shrink-0 rounded-full bg-zinc-300 dark:bg-neutral-800" />
67+
)}
68+
69+
{title && <Drawer.Title>{title}</Drawer.Title>}
70+
71+
{React.isValidElement(content)
72+
? content
73+
: typeof content === 'function'
74+
? React.createElement(content)
75+
: null}
76+
<div ref={setHolderRef} />
77+
</Drawer.Content>
78+
<Drawer.Overlay
79+
className="fixed inset-0 bg-neutral-800/40"
80+
style={{
81+
zIndex: overlayZIndex,
82+
}}
83+
/>
84+
</Drawer.Portal>
85+
</Root>
86+
)
87+
}

src/components/ui/sheet/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './Sheet'

src/providers/root/modal-stack-provider.tsx

+17
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ import { usePathname } from 'next/navigation'
1616
import type { Target, Transition } from 'framer-motion'
1717
import type { FC, PropsWithChildren, SyntheticEvent } from 'react'
1818

19+
import { useIsMobile } from '~/atoms'
1920
import { CloseIcon } from '~/components/icons/close'
2021
import { DialogOverlay } from '~/components/ui/dialog/DialogOverlay'
2122
import { Divider } from '~/components/ui/divider'
23+
import { PresentSheet, sheetStackAtom } from '~/components/ui/sheet'
2224
import { microReboundPreset } from '~/constants/spring'
2325
import { useIsClient } from '~/hooks/common/use-is-client'
2426
import { useIsUnMounted } from '~/hooks/common/use-is-unmounted'
@@ -195,6 +197,21 @@ const Modal: Component<{
195197
dismiss: close,
196198
}
197199

200+
const isMobile = useIsMobile()
201+
202+
if (isMobile) {
203+
const drawerLength = jotaiStore.get(sheetStackAtom).length
204+
205+
return (
206+
<PresentSheet
207+
open
208+
zIndex={1000 + drawerLength}
209+
onOpenChange={onClose}
210+
content={createElement(content, ModalProps)}
211+
/>
212+
)
213+
}
214+
198215
if (CustomModalComponent) {
199216
return (
200217
<Dialog.Root open onOpenChange={onClose}>

0 commit comments

Comments
 (0)