diff --git a/src/layout.tsx b/src/layout.tsx index d02a209..8419317 100644 --- a/src/layout.tsx +++ b/src/layout.tsx @@ -2,7 +2,7 @@ import clsx from 'clsx'; import { Bell, LogOut, RotateCcw, Save, Settings } from 'lucide-solid'; import { A } from '@solidjs/router'; import { createQuery, useQueryClient } from '@tanstack/solid-query'; -import { For, Show, createEffect, createSignal, type Component, type JSX } from 'solid-js'; +import { For, Show, createEffect, createSignal, onCleanup, onMount, type Component, type JSX } from 'solid-js'; import { Avatar, buttonStyles, inputStyles } from './components/common'; import { clearApiCaches, resolveActor } from './lib/api'; @@ -11,6 +11,16 @@ import { useLiveEvents, type LiveEvent } from './lib/live-events'; import { formatRelativeTime } from './lib/repo-utils'; import { useAppSettings } from './lib/settings'; +const TOPBAR_MENU_SELECTOR = 'details[data-topbar-menu]'; + +const closeOpenTopbarMenus = (root: HTMLElement, target: Node) => { + root.querySelectorAll(TOPBAR_MENU_SELECTOR).forEach((menu) => { + if (menu.open && !menu.contains(target)) { + menu.open = false; + } + }); +}; + const eventLabel = (event: LiveEvent) => { if (event.source === 'sh.tangled.repo.issue:repo') { return `new issue on ${event.subject}`; @@ -67,7 +77,7 @@ const AppSettingsMenu: Component = () => { }; return ( -
+
@@ -127,24 +137,134 @@ const AppSettingsMenu: Component = () => { ); }; -const Topbar: Component = () => { +const LoginMenu: Component = () => { const auth = useAuth(); - const live = useLiveEvents(); const [identifier, setIdentifier] = createSignal(''); + const onSignIn = async (event: SubmitEvent) => { + event.preventDefault(); + await auth.signIn(identifier()); + }; + + return ( +
+ login +
+
+ setIdentifier(event.currentTarget.value)} + placeholder="handle or did" + class={inputStyles()} + /> + + +

{auth.error()}

+
+
+
+
+ ); +}; + +const NotificationsMenu: Component = () => { + const live = useLiveEvents(); + + return ( +
+ live.markRead()} + > +
+ + 0}> + + {Math.min(live.unread(), 99)} + + +
+
+
+
live activity
+
+ 0} + fallback={
No live events yet.
} + > + + {(event) => ( +
+
{eventLabel(event)}
+
{formatRelativeTime(event.receivedAt)}
+
+ )} +
+
+
+
+
+ ); +}; + +const UserProfileMenu: Component = () => { + const auth = useAuth(); + const viewerQuery = createQuery(() => ({ queryKey: ['viewer', auth.currentDid()], enabled: Boolean(auth.currentDid()), queryFn: async () => resolveActor(auth.currentDid()!), })); - const onSignIn = async (event: SubmitEvent) => { - event.preventDefault(); - await auth.signIn(identifier()); - }; + return ( +
+ + + + +
+
+
{viewerQuery.data?.handle ?? auth.currentDid()}
+
{auth.currentDid()}
+
+ +
+
+ ); +}; + +const Topbar: Component = () => { + const auth = useAuth(); + let topbar: HTMLElement | undefined; + + onMount(() => { + const closeMenusForOutsideClick = (event: PointerEvent) => { + const target = event.target; + if (!topbar || !(target instanceof Node)) { + return; + } + + closeOpenTopbarMenus(topbar, target); + }; + + document.addEventListener('pointerdown', closeMenusForOutsideClick); + + onCleanup(() => { + document.removeEventListener('pointerdown', closeMenusForOutsideClick); + }); + }); return ( -
- } + fallback={} > -
- live.markRead()} - > -
- - 0}> - - {Math.min(live.unread(), 99)} - - -
-
-
-
live activity
-
- 0} - fallback={
No live events yet.
} - > - - {(event) => ( -
-
{eventLabel(event)}
-
{formatRelativeTime(event.receivedAt)}
-
- )} -
-
-
-
-
- -
- - - - -
-
-
{viewerQuery.data?.handle ?? auth.currentDid()}
-
{auth.currentDid()}
-
- -
-
+ +