diff --git a/package.json b/package.json
index 7e63ad9a62..01d90da90f 100644
--- a/package.json
+++ b/package.json
@@ -70,6 +70,7 @@
"@segment/analytics-react-native": "^2.10.1",
"@segment/sovran-react-native": "^0.4.5",
"@sentry/react-native": "5.5.0",
+ "@tamagui/focus-scope": "^1.84.1",
"@tanstack/react-query": "^5.8.1",
"@tiptap/core": "^2.0.0-beta.220",
"@tiptap/extension-document": "^2.0.0-beta.220",
diff --git a/src/App.native.tsx b/src/App.native.tsx
index a9dfafe36f..39485ee24f 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -41,6 +41,7 @@ import {
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import * as persisted from '#/state/persisted'
import {Splash} from '#/Splash'
+import {Provider as PortalProvider, Outlet as PortalOutlet} from '#/view/com/Portal'
SplashScreen.preventAutoHideAsync()
@@ -76,6 +77,7 @@ function InnerApp() {
+
@@ -113,7 +115,9 @@ function App() {
-
+
+
+
diff --git a/src/App.web.tsx b/src/App.web.tsx
index 9ee9a4fd71..5760b0d4a2 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -30,6 +30,7 @@ import {
} from 'state/session'
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import * as persisted from '#/state/persisted'
+import {Provider as PortalProvider, Outlet as PortalOutlet} from '#/view/com/Portal'
function InnerApp() {
const {isInitialLoad, currentAccount} = useSession()
@@ -58,6 +59,7 @@ function InnerApp() {
+
@@ -94,7 +96,9 @@ function App() {
-
+
+
+
diff --git a/src/view/com/Dialog/index.tsx b/src/view/com/Dialog/index.tsx
new file mode 100644
index 0000000000..fc3f784704
--- /dev/null
+++ b/src/view/com/Dialog/index.tsx
@@ -0,0 +1,7 @@
+import React from 'react'
+
+import {DialogProps} from '#/view/com/Dialog/types'
+
+export function Dialog(props: React.PropsWithChildren) {
+ return null
+}
diff --git a/src/view/com/Dialog/index.web.tsx b/src/view/com/Dialog/index.web.tsx
new file mode 100644
index 0000000000..9c6f09f4f3
--- /dev/null
+++ b/src/view/com/Dialog/index.web.tsx
@@ -0,0 +1,96 @@
+import React from 'react'
+import {View, TouchableWithoutFeedback, DimensionValue} from 'react-native'
+import {FocusScope} from '@tamagui/focus-scope'
+import Animated, {FadeInDown, FadeIn, FadeOut} from 'react-native-reanimated'
+
+import {useTheme, atoms as a, useBreakpoints} from '#/alf'
+import {Portal} from '#/view/com/Portal'
+import {DialogProps} from '#/view/com/Dialog/types'
+
+const Context = React.createContext<{
+ dismiss: () => void
+}>({
+ dismiss: () => {},
+})
+
+export function useDialog() {
+ return React.useContext(Context)
+}
+
+export function Dialog({
+ isOpen,
+ onDismiss,
+ children,
+}: React.PropsWithChildren) {
+ const t = useTheme()
+ const {gtMobile} = useBreakpoints()
+
+ const dismiss = React.useCallback(() => {
+ onDismiss()
+ }, [onDismiss])
+
+ React.useEffect(() => {
+ function handler(e: KeyboardEvent) {
+ if (e.key === 'Escape') dismiss()
+ }
+
+ document.addEventListener('keydown', handler)
+
+ return () => document.removeEventListener('keydown', handler)
+ }, [dismiss])
+
+ const context = React.useMemo(() => ({dismiss}), [dismiss])
+
+ return (
+ <>
+ {isOpen && (
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+ )}
+ >
+ )
+}
diff --git a/src/view/com/Dialog/types.ts b/src/view/com/Dialog/types.ts
new file mode 100644
index 0000000000..eb9dc56865
--- /dev/null
+++ b/src/view/com/Dialog/types.ts
@@ -0,0 +1,4 @@
+export type DialogProps = {
+ isOpen: boolean
+ onDismiss: () => void
+}
diff --git a/src/view/com/Portal.tsx b/src/view/com/Portal.tsx
new file mode 100644
index 0000000000..47a216bd80
--- /dev/null
+++ b/src/view/com/Portal.tsx
@@ -0,0 +1,58 @@
+import React from 'react'
+
+type Component = React.ReactElement
+
+type ContextType = {
+ outlet: Component | null
+ append(id: string, component: Component): void
+ remove(id: string): void
+}
+
+type ComponentMap = {
+ [id: string]: Component
+}
+
+export const Context = React.createContext({
+ outlet: null,
+ append: () => {},
+ remove: () => {},
+})
+
+export function Provider(props: React.PropsWithChildren<{}>) {
+ const map = React.useRef({})
+ const [outlet, setOutlet] = React.useState(null)
+
+ const append = React.useCallback((id, component) => {
+ if (map.current[id]) return
+ map.current[id] = {component}
+ setOutlet(<>{Object.values(map.current)}>)
+ }, [])
+
+ const remove = React.useCallback(id => {
+ delete map.current[id]
+ setOutlet(<>{Object.values(map.current)}>)
+ }, [])
+
+ console.log(outlet)
+
+ return (
+
+ {props.children}
+
+ )
+}
+
+export function Outlet() {
+ const ctx = React.useContext(Context)
+ return ctx.outlet
+}
+
+export function Portal({children}: React.PropsWithChildren<{}>) {
+ const {append, remove} = React.useContext(Context)
+ const id = React.useId()
+ React.useEffect(() => {
+ append(id, children as Component)
+ return () => remove(id)
+ }, [id, children, append, remove])
+ return null
+}
diff --git a/src/view/screens/DebugNew.tsx b/src/view/screens/DebugNew.tsx
index 444fe4f29a..ce7f5c20a8 100644
--- a/src/view/screens/DebugNew.tsx
+++ b/src/view/screens/DebugNew.tsx
@@ -12,6 +12,7 @@ import {Text, H1, H2, H3, H4, H5, H6} from '#/view/com/Typography'
import {InputText} from '#/view/com/forms/InputText'
import {InputDate, utils} from '#/view/com/forms/InputDate'
import {Logo} from '#/view/icons/Logo'
+import {Dialog} from '#/view/com/Dialog'
function ThemeSelector() {
const setColorMode = useSetColorMode()
@@ -316,6 +317,28 @@ function Forms() {
)
}
+function Dialogs() {
+ const [isOpen, setIsOpen] = React.useState(false)
+
+ console.log({isOpen})
+
+ return (
+ <>
+
+
+ >
+ )
+}
+
export function DebugScreen() {
const t = useTheme()
@@ -325,6 +348,8 @@ export function DebugScreen() {
+
+
diff --git a/yarn.lock b/yarn.lock
index c1db264bc6..7d0ceecd38 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6921,6 +6921,31 @@
"@svgr/plugin-svgo" "^5.5.0"
loader-utils "^2.0.0"
+"@tamagui/compose-refs@1.84.1":
+ version "1.84.1"
+ resolved "https://registry.yarnpkg.com/@tamagui/compose-refs/-/compose-refs-1.84.1.tgz#244735edc3ac2e617389297f005d5bc25872465f"
+ integrity sha512-oZ0rUmQABlGm/QKQITxAW9WLV3qjyq1ehgoWcZVmtc1Kc/hkFQe2J+wRQV726CmTAnuUgUXi3eoNMwBVoZksfQ==
+
+"@tamagui/constants@1.84.1":
+ version "1.84.1"
+ resolved "https://registry.yarnpkg.com/@tamagui/constants/-/constants-1.84.1.tgz#62e41837dbe844d14e255f3eea9c2583044d2509"
+ integrity sha512-QmvyCqtEIugqXutQI35GJQ1hlpSapYCdOHx9QlgsOWjAY34pu55MaY/tDrQeQ0AUmI/qx30vy7TsCJxB4QFEoQ==
+
+"@tamagui/focus-scope@^1.84.1":
+ version "1.84.1"
+ resolved "https://registry.yarnpkg.com/@tamagui/focus-scope/-/focus-scope-1.84.1.tgz#e9f061184048c75f87da023f54b9c5abccdd460d"
+ integrity sha512-0E1Wc3jmKhafETfH1dUuJYmGK1bDNA/9TySbOeTjTToxUoL3V0G2W5JSwSMCDqR1Bl+xrGlGwzXTUhouw8qSog==
+ dependencies:
+ "@tamagui/compose-refs" "1.84.1"
+ "@tamagui/use-event" "1.84.1"
+
+"@tamagui/use-event@1.84.1":
+ version "1.84.1"
+ resolved "https://registry.yarnpkg.com/@tamagui/use-event/-/use-event-1.84.1.tgz#a095a1bde9c40c4a397226c57c3fa32f6018f504"
+ integrity sha512-U88WCxvMz7ZSfMFMJEFbG3tJjK/Lf+PHlmtYvlx1V+YiqRBoj5+milzoM8PclENn5vZMiJW0ozYRgzI/cdE7Eg==
+ dependencies:
+ "@tamagui/constants" "1.84.1"
+
"@tanstack/query-core@5.8.1":
version "5.8.1"
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.8.1.tgz#5215a028370d9b2f32e83787a0ea119e2f977996"