Skip to content

Commit 5915caa

Browse files
committed
update overlay ui
1 parent eae2638 commit 5915caa

24 files changed

Lines changed: 824 additions & 1065 deletions

app/src/components/SwitchAccountButton.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ export const SwitchAccountButton = (): ReactNode => {
3737
performAccountSwitch(account.ccid)
3838
}}
3939
onDelete={() => {
40-
close()
4140
modal.open(
4241
<ResetSessionModalContent
4342
ccid={account.ccid}
@@ -61,7 +60,6 @@ export const SwitchAccountButton = (): ReactNode => {
6160
key={'$addAccount'}
6261
icon={<MdPersonAdd size={24} />}
6362
onClick={() => {
64-
close()
6563
drawer.open(<AddAccountDrawer previousCcid={client.ccid} onClose={() => drawer.close()} />)
6664
}}
6765
>

app/src/components/message/MessageActions.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ interface LikeState {
3232
export const MessageActions = (props: Props) => {
3333
const { client } = useClient()
3434
const composer = useComposer()
35-
const { select } = useSelect()
35+
const { select, close } = useSelect()
3636
const drawer = useDrawer()
3737
const confirm = useConfirm()
3838
const emojiPicker = useEmojiPicker()
@@ -201,6 +201,7 @@ export const MessageActions = (props: Props) => {
201201
'本当にこの投稿を削除しますか?',
202202
() => {
203203
client?.api.delete(props.message.uri).then(() => hapticSuccess())
204+
close()
204205
},
205206
{
206207
confirmText: '削除'
@@ -218,6 +219,7 @@ export const MessageActions = (props: Props) => {
218219
targetURI={props.message.uri}
219220
onSend={() => {
220221
drawer.close()
222+
close()
221223
hapticSuccess()
222224
}}
223225
/>

app/src/contexts/Confirm.tsx

Lines changed: 1 addition & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1 @@
1-
import { AnimatePresence, motion } from 'motion/react'
2-
import { createContext, ReactNode, useCallback, useContext, useMemo, useState } from 'react'
3-
import { CssVar } from '../types/Theme'
4-
import { Button } from '@concrnt/ui'
5-
6-
export interface ConfirmOptions {
7-
description?: ReactNode
8-
confirmText?: string
9-
cancelText?: string
10-
}
11-
12-
interface ConfirmContextState {
13-
open: (title: string, onConfirm: () => void, opts?: ConfirmOptions) => void
14-
}
15-
16-
interface Props {
17-
children: React.ReactNode
18-
}
19-
20-
export const ConfirmContext = createContext<ConfirmContextState>({
21-
open: () => {}
22-
})
23-
24-
export const ConfirmProvider = (props: Props) => {
25-
const [title, setTitle] = useState<string | null>(null)
26-
const [opts, setOpts] = useState<ConfirmOptions | null>(null)
27-
const [onConfirm, setOnConfirm] = useState<() => void>(() => () => {})
28-
29-
const open = useCallback((title: string, onConfirm: () => void, opts?: ConfirmOptions) => {
30-
setTitle(title)
31-
setOnConfirm(() => onConfirm)
32-
setOpts(opts ?? null)
33-
}, [])
34-
35-
const close = useCallback(() => {
36-
setTitle(null)
37-
setOnConfirm(() => () => {})
38-
setOpts(null)
39-
}, [])
40-
41-
const value = useMemo(
42-
() => ({
43-
open
44-
}),
45-
[open]
46-
)
47-
48-
return (
49-
<ConfirmContext.Provider value={value}>
50-
{props.children}
51-
52-
<AnimatePresence>
53-
{title && (
54-
<>
55-
<motion.div
56-
style={{
57-
position: 'fixed',
58-
top: 0,
59-
left: 0,
60-
width: '100%',
61-
height: '100%',
62-
display: 'flex',
63-
alignItems: 'center',
64-
justifyContent: 'center',
65-
backgroundColor: 'black'
66-
}}
67-
initial={{ opacity: 0 }}
68-
animate={{ opacity: 0.8 }}
69-
exit={{ opacity: 0 }}
70-
/>
71-
<div
72-
style={{
73-
position: 'fixed',
74-
top: 0,
75-
left: 0,
76-
width: '100%',
77-
height: '100%',
78-
display: 'flex',
79-
alignItems: 'center',
80-
justifyContent: 'center'
81-
}}
82-
>
83-
<motion.div
84-
style={{
85-
backgroundColor: CssVar.contentBackground,
86-
padding: CssVar.space(2),
87-
borderRadius: CssVar.round(1),
88-
width: '80vw',
89-
position: 'absolute',
90-
top: '30%'
91-
}}
92-
initial={{ opacity: 0, scale: 0.8 }}
93-
animate={{ opacity: 1, scale: 1 }}
94-
exit={{ opacity: 0, scale: 0.8 }}
95-
>
96-
<h2>{title}</h2>
97-
{opts?.description && <p>{opts.description}</p>}
98-
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: CssVar.space(1) }}>
99-
<Button onClick={close}>{opts?.cancelText ?? 'Cancel'}</Button>
100-
<Button
101-
onClick={() => {
102-
onConfirm()
103-
close()
104-
}}
105-
>
106-
{opts?.confirmText ?? 'Confirm'}
107-
</Button>
108-
</div>
109-
</motion.div>
110-
</div>
111-
</>
112-
)}
113-
</AnimatePresence>
114-
</ConfirmContext.Provider>
115-
)
116-
}
117-
118-
export const useConfirm = (): ConfirmContextState => {
119-
return useContext(ConfirmContext)
120-
}
1+
export { useConfirm, type ConfirmOptions } from '@concrnt/ui'

app/src/contexts/Drawer.tsx

Lines changed: 25 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -1,172 +1,49 @@
1-
import { AnimatePresence, motion, useDragControls, useMotionValue, useTransform } from 'motion/react'
2-
import { createContext, ReactNode, useCallback, useContext, useMemo, useState } from 'react'
3-
import { animate } from 'motion'
4-
import { CssVar } from '../types/Theme'
1+
import { ReactNode, useCallback, useMemo } from 'react'
2+
import { BottomSheet, useOverlayStack } from '@concrnt/ui'
53
import { useKeyboard } from './Keyboard'
64

7-
interface DrawerContextState {
5+
interface DrawerState {
86
open: (content: ReactNode) => void
97
close: () => void
108
}
119

12-
interface Props {
13-
children: React.ReactNode
10+
interface DrawerShellProps {
11+
onDismiss: () => void
12+
children: ReactNode
1413
}
1514

16-
const DrawerContext = createContext<DrawerContextState>({
17-
open: () => {},
18-
close: () => {}
19-
})
20-
21-
export const DrawerProvider = (props: Props) => {
22-
const y = useMotionValue(0)
23-
const dragControls = useDragControls()
15+
const DrawerShell = (props: DrawerShellProps) => {
16+
const keyboard = useKeyboard()
2417

25-
const [content, setContent] = useState<ReactNode>(null)
18+
return (
19+
<BottomSheet height={window.innerHeight * 0.9} keyboardInset={keyboard} onDismiss={props.onDismiss}>
20+
{props.children}
21+
</BottomSheet>
22+
)
23+
}
2624

27-
const height = window.innerHeight * 0.9
28-
const backdropOpacity = useTransform(y, [0, height], [0.5, 0])
25+
export const useDrawer = (): DrawerState => {
26+
const stack = useOverlayStack()
2927

3028
const open = useCallback(
31-
(c: ReactNode) => {
32-
y.set(height)
33-
setContent(c)
29+
(content: ReactNode) => {
30+
stack.push({
31+
kind: 'drawer',
32+
render: (close) => <DrawerShell onDismiss={close}>{content}</DrawerShell>
33+
})
3434
},
35-
[height, y]
35+
[stack]
3636
)
3737

3838
const close = useCallback(() => {
39-
setContent(null)
40-
}, [])
39+
stack.closeKind('drawer')
40+
}, [stack])
4141

42-
const value = useMemo(
42+
return useMemo(
4343
() => ({
4444
open,
4545
close
4646
}),
4747
[open, close]
4848
)
49-
50-
const keyboard = useKeyboard()
51-
52-
return (
53-
<>
54-
<DrawerContext.Provider value={value}>{props.children}</DrawerContext.Provider>
55-
<AnimatePresence>
56-
{content && (
57-
<>
58-
<motion.div
59-
style={{
60-
position: 'absolute',
61-
inset: 0,
62-
background: 'black',
63-
opacity: backdropOpacity
64-
}}
65-
onClick={() => {
66-
close()
67-
}}
68-
/>
69-
<motion.div
70-
style={{
71-
backgroundColor: CssVar.contentBackground,
72-
color: CssVar.contentText,
73-
position: 'absolute',
74-
bottom: 0,
75-
left: 0,
76-
right: 0,
77-
paddingBottom: 'env(safe-area-inset-bottom)',
78-
borderRadius: `${CssVar.round(1)} ${CssVar.round(1)} 0 0`,
79-
height,
80-
y
81-
}}
82-
drag="y"
83-
dragControls={dragControls}
84-
dragListener={false}
85-
dragConstraints={{ top: 0, bottom: height }}
86-
dragElastic={0}
87-
dragMomentum={false}
88-
initial={{ y: height }}
89-
animate={{ y: 0 }}
90-
transition={{ type: 'tween', ease: 'easeOut', duration: 0.2 }}
91-
exit={{ y: height }}
92-
onDragEnd={(_, info) => {
93-
const current = y.get()
94-
const v = info.velocity.y
95-
const dy = info.offset.y
96-
97-
const fast = Math.abs(v) > 50
98-
const far = Math.abs(dy) > height / 2
99-
100-
let shouldClose = false
101-
if (fast) {
102-
shouldClose = v > 0
103-
} else if (far) {
104-
shouldClose = dy > 0
105-
} else {
106-
shouldClose = current > height / 2
107-
}
108-
109-
if (shouldClose) {
110-
close()
111-
} else {
112-
animate(y, 0, { type: 'tween', ease: 'easeOut', duration: 0.2 })
113-
}
114-
}}
115-
>
116-
<div
117-
style={{
118-
display: 'flex',
119-
justifyContent: 'center',
120-
padding: `${CssVar.space(2)} 0`,
121-
position: 'relative',
122-
touchAction: 'none'
123-
}}
124-
onPointerDown={(e) => {
125-
dragControls.start(e)
126-
}}
127-
>
128-
{/* ハンドルの見た目は変えず、当たり判定を縦方向に拡張する透明レイヤー */}
129-
<div
130-
style={{
131-
position: 'absolute',
132-
top: `-${CssVar.space(4)}`,
133-
bottom: `-${CssVar.space(4)}`,
134-
left: 0,
135-
right: 0
136-
}}
137-
/>
138-
<div
139-
style={{
140-
width: '30px',
141-
height: '6px',
142-
borderRadius: CssVar.round(0.5),
143-
backgroundColor: CssVar.divider
144-
}}
145-
/>
146-
</div>
147-
<div
148-
style={{
149-
overflow: 'auto',
150-
flex: 1,
151-
height: '100%'
152-
}}
153-
>
154-
{content}
155-
<div
156-
style={{
157-
height: `${keyboard.height}px`,
158-
transition: `height ${keyboard.duration}s ease-out`
159-
}}
160-
/>
161-
</div>
162-
</motion.div>
163-
</>
164-
)}
165-
</AnimatePresence>
166-
</>
167-
)
168-
}
169-
170-
export const useDrawer = (): DrawerContextState => {
171-
return useContext(DrawerContext)
17249
}

0 commit comments

Comments
 (0)