diff --git a/app/(home-pages)/(writer)/WriterShell.tsx b/app/(home-pages)/(writer)/WriterShell.tsx index b939d6ab..176dafe9 100644 --- a/app/(home-pages)/(writer)/WriterShell.tsx +++ b/app/(home-pages)/(writer)/WriterShell.tsx @@ -13,19 +13,16 @@ const PAGE_META = [ prefix: "/home", id: "home", title: "Home", - icon: , }, { prefix: "/looseleafs", id: "looseleafs", title: "Looseleafs", - icon: , }, { prefix: "/notifications", id: "notifications", title: "Notifications", - icon: , }, ]; @@ -36,9 +33,7 @@ export function WriterShell(props: { children: React.ReactNode }) { return ( - } + pageTitle={meta && } actions={} > {props.children} diff --git a/app/(home-pages)/(writer)/home/Actions/CreateNewButton.tsx b/app/(home-pages)/(writer)/home/Actions/CreateNewButton.tsx index f3892d58..cb9f8f54 100644 --- a/app/(home-pages)/(writer)/home/Actions/CreateNewButton.tsx +++ b/app/(home-pages)/(writer)/home/Actions/CreateNewButton.tsx @@ -23,6 +23,7 @@ export const CreateNewLeafletButton = (props: { compact?: boolean }) => { asChild side={isMobile ? "top" : "right"} align={isMobile ? "center" : "start"} + className="z-[60]!" trigger={ props.compact ? ( @@ -34,7 +35,7 @@ export const CreateNewLeafletButton = (props: { compact?: boolean }) => { id="new-leaflet-button" primary icon= - label="New" + label="New Doc" /> ) } diff --git a/app/(home-pages)/p/[didOrHandle]/layout.tsx b/app/(home-pages)/p/[didOrHandle]/layout.tsx index 6e7de913..8ee3c992 100644 --- a/app/(home-pages)/p/[didOrHandle]/layout.tsx +++ b/app/(home-pages)/p/[didOrHandle]/layout.tsx @@ -85,18 +85,7 @@ export default async function ProfilePageLayout(props: { return ( - } - pageTitle={displayName} - /> - } + pageTitle={} tabs={{ Inbox: { href: "/reader" }, Trending: { href: "/reader/trending" }, @@ -108,10 +97,7 @@ export default async function ProfilePageLayout(props: { showHeader={false} > - + <>{props.children} diff --git a/app/(home-pages)/reader/layout.tsx b/app/(home-pages)/reader/layout.tsx index 7f299d24..daa500bc 100644 --- a/app/(home-pages)/reader/layout.tsx +++ b/app/(home-pages)/reader/layout.tsx @@ -14,7 +14,7 @@ export default async function ReaderLayout(props: { return ( } pageTitle="Reader" />} + pageTitle={} tabs={tabs} > {props.children} diff --git a/app/(home-pages)/tag/[tag]/layout.tsx b/app/(home-pages)/tag/[tag]/layout.tsx index e55bcb8c..b44c4703 100644 --- a/app/(home-pages)/tag/[tag]/layout.tsx +++ b/app/(home-pages)/tag/[tag]/layout.tsx @@ -1,6 +1,5 @@ import { DashboardShell } from "components/PageLayouts/DashboardShell"; import { PageTitle } from "components/ActionBar/DesktopNavigation"; -import { TagTiny } from "components/Icons/TagTiny"; export default async function TagLayout(props: { children: React.ReactNode; @@ -12,7 +11,7 @@ export default async function TagLayout(props: { return ( } pageTitle={decodedTag} />} + pageTitle={} tabs={{ Inbox: { href: "/reader" }, Trending: { href: "/reader/trending" }, diff --git a/app/lish/[did]/[publication]/dashboard/DashboardChrome.tsx b/app/lish/[did]/[publication]/dashboard/DashboardChrome.tsx deleted file mode 100644 index 05fe46f0..00000000 --- a/app/lish/[did]/[publication]/dashboard/DashboardChrome.tsx +++ /dev/null @@ -1,66 +0,0 @@ -"use client"; - -import { Actions } from "./Actions"; -import { DashboardShell } from "components/PageLayouts/DashboardShell"; -import { type NormalizedPublication } from "src/utils/normalizeRecords"; -import { useCanSeePro } from "src/hooks/useEntitlement"; -import { PubIcon } from "components/ActionBar/Publications"; -import { PageTitle } from "components/ActionBar/DesktopNavigation"; -import { SettingsSmall } from "components/Icons/SettingsSmall"; -import { - PublicationSWRDataProvider, -} from "./PublicationSWRProvider"; -import type { GetPublicationDataReturnType } from "app/api/rpc/[command]/get_publication_data"; -import { PublicationThemeProviderDashboard } from "components/ThemeManager/PublicationThemeProvider"; - -export function DashboardChrome(props: { - publication_did: string; - publication_rkey: string; - publication_data: GetPublicationDataReturnType["result"]; - pubUri: string; - record: NormalizedPublication; - routeDid: string; - routePublication: string; - children: React.ReactNode; -}) { - let canSeePro = useCanSeePro(); - let baseHref = `/lish/${props.routeDid}/${props.routePublication}/dashboard`; - - return ( - - - - } - /> - } - actions={} - tabs={{ - Drafts: { href: baseHref }, - Posts: { href: `${baseHref}/posts` }, - Subs: { href: `${baseHref}/subs` }, - ...(canSeePro - ? { Analytics: { href: `${baseHref}/analytics` } } - : {}), - Settings: { - href: `${baseHref}/settings`, - icon: , - }, - }} - > - {props.children} - - - - ); -} diff --git a/app/lish/[did]/[publication]/dashboard/layout.tsx b/app/lish/[did]/[publication]/dashboard/layout.tsx index 46aea374..aa23ece6 100644 --- a/app/lish/[did]/[publication]/dashboard/layout.tsx +++ b/app/lish/[did]/[publication]/dashboard/layout.tsx @@ -6,7 +6,12 @@ import { normalizePublicationRecord } from "src/utils/normalizeRecords"; import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout"; import { LoginModal } from "components/LoginButton"; import { AtUri } from "@atproto/syntax"; -import { DashboardChrome } from "./DashboardChrome"; +import { PageTitle } from "components/ActionBar/DesktopNavigation"; +import { SettingsSmall } from "components/Icons/SettingsSmall"; +import { DashboardShell } from "components/PageLayouts/DashboardShell"; +import { PublicationThemeProviderDashboard } from "components/ThemeManager/PublicationThemeProvider"; +import { Actions } from "./Actions"; +import { PublicationSWRDataProvider } from "./PublicationSWRProvider"; export async function generateMetadata(props: { params: Promise<{ publication: string; did: string }>; @@ -76,19 +81,35 @@ export default async function PublicationDashboardLayout(props: { } let uri = new AtUri(publication.uri); + let baseHref = `/lish/${params.did}/${params.publication}/dashboard`; return ( - - {props.children} - + + } + actions={} + tabs={{ + Drafts: { href: baseHref }, + Posts: { href: `${baseHref}/posts` }, + Subs: { href: `${baseHref}/subs` }, + Analytics: { href: `${baseHref}/analytics` }, + Settings: { + href: `${baseHref}/settings`, + icon: , + }, + }} + > + {props.children} + + + ); } diff --git a/components/ActionBar/DesktopNavigation.tsx b/components/ActionBar/DesktopNavigation.tsx index c530b822..feb1840b 100644 --- a/components/ActionBar/DesktopNavigation.tsx +++ b/components/ActionBar/DesktopNavigation.tsx @@ -15,6 +15,8 @@ import { ActionButton } from "./ActionButton"; import { AccountSmall } from "components/Icons/AccountSmall"; import { TabsSmall } from "components/Icons/TabsSmall"; import { SpeedyLink } from "components/SpeedyLink"; +import { GoToArrow } from "components/Icons/GoToArrow"; +import { GoToArrowLined } from "components/Icons/GoToArrowLined"; type NavigationProps = { pageTitle: React.ReactNode; @@ -49,43 +51,41 @@ function pickActiveTabHref( export const NavigationContent = (props: NavigationProps) => { let { identity } = useIdentityData(); let pathname = usePathname(); - let activeTabHref = props.tabs ? pickActiveTabHref(pathname, props.tabs) : null; + let activeTabHref = props.tabs + ? pickActiveTabHref(pathname, props.tabs) + : null; let onWriterPage = useIsOnWriterPage(); return ( <> {props.pageTitle} +
{props.actions && ( <> -
{props.actions}
+
{props.actions}

)} - {props.tabs && - Object.entries(props.tabs).map(([name, { href, icon }]) => ( - - } - label={name} - active={href === activeTabHref} - /> - - ))} - - {onWriterPage && ( + {props.tabs && ( <> -
-
- PUBLICATIONS -
- + {Object.entries(props.tabs).map(([name, { href, icon }]) => ( + + } + label={name} + active={href === activeTabHref} + /> + + ))} )} + {onWriterPage && } +
{ export const PageTitle = (props: { pageTitle: string; - icon: React.ReactNode; + showBackButton?: boolean; }) => { return (
- {props.icon} + {props.showBackButton && ( + + + + )}
{props.pageTitle}
diff --git a/components/ActionBar/NavigationButtons.tsx b/components/ActionBar/NavigationButtons.tsx index 496f6bf5..5a16fe1a 100644 --- a/components/ActionBar/NavigationButtons.tsx +++ b/components/ActionBar/NavigationButtons.tsx @@ -50,7 +50,12 @@ export const WriterButton = () => { let current = useIsOnWriterPage(); return ( - } label="Write" active={current} /> + } + label="Write" + active={current} + /> ); }; @@ -59,7 +64,12 @@ export const ReaderButton = (props: { subs: boolean }) => { let current = useIsActive("/reader"); return ( - } label="Read" active={current} /> + } + label="Read" + active={current} + /> ); }; diff --git a/components/ActionBar/Publications.tsx b/components/ActionBar/Publications.tsx index f8eb646f..23f388c0 100644 --- a/components/ActionBar/Publications.tsx +++ b/components/ActionBar/Publications.tsx @@ -41,6 +41,7 @@ export const PublicationButtons = (props: { className?: string }) => {
+
PUBLICATIONS
{hasLooseleafs && ( <> @@ -108,10 +109,13 @@ const PubListEmpty = () => { return ( } subtext="Start a blog on ATProto!" - onClick={() => { + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); setState("info"); }} /> @@ -127,6 +131,7 @@ const PubListEmpty = () => { asChild trigger={ } @@ -173,7 +178,7 @@ export const PubListEmptyContent = (props: { compact?: boolean }) => { noEmailLogin asChild trigger={ - + {identity ? "Link to" : "Log in with"} Atmosphere } diff --git a/components/Icons/GoToArrowLined.tsx b/components/Icons/GoToArrowLined.tsx new file mode 100644 index 00000000..e0d5736a --- /dev/null +++ b/components/Icons/GoToArrowLined.tsx @@ -0,0 +1,22 @@ +import { Props } from "./Props"; + +export const GoToArrowLined = (props: Props) => { + return ( + + + + ); +}; diff --git a/components/Modal.tsx b/components/Modal.tsx index 893aad65..5bd2df16 100644 --- a/components/Modal.tsx +++ b/components/Modal.tsx @@ -1,6 +1,9 @@ +"use client"; import * as Dialog from "@radix-ui/react-dialog"; import React from "react"; +import { isIOS } from "src/utils/isDevice"; import { CloseTiny } from "./Icons/CloseTiny"; +import { useVisualViewport } from "./ViewportSizeLayout"; export const Modal = ({ className, @@ -19,14 +22,33 @@ export const Modal = ({ title?: React.ReactNode; children: React.ReactNode; }) => { + let { height, offsetTop, difference } = useVisualViewport(); + // iOS keyboard open: re-center modal against the visual viewport. Android + // resizes the layout viewport via interactiveWidget: "resizes-content". + let keyboardOpen = isIOS() && difference !== 0 && height > 0; return ( {trigger !== undefined && ( {trigger} )} - + - {props.children} -
- ); -} - -function useViewportDifference(): number { - let [difference, setDifference] = useState(0); - - useEffect(() => { - // Use visualViewport api to track available height even on iOS virtual keyboard opening - let onResize = () => { - setDifference(window.innerHeight - getViewportSize().height); - }; - - if (!visualViewport) { - window.addEventListener("resize", onResize); - } else { - visualViewport.addEventListener("resize", onResize); - } - - return () => { - if (!visualViewport) { - window.removeEventListener("resize", onResize); - } else { - visualViewport.removeEventListener("resize", onResize); - } - }; - }, []); - - return difference; -} +type VisualViewportState = { + width: number; + height: number; + offsetTop: number; + difference: number; +}; -function getViewportSize() { - if (typeof window === "undefined") return { width: 0, height: 0 }; +function getVisualViewport(): VisualViewportState { + if (typeof window === "undefined") { + return { width: 0, height: 0, offsetTop: 0, difference: 0 }; + } + let height = visualViewport?.height || window.innerHeight; return { - width: visualViewport?.width || window?.innerWidth, - height: visualViewport?.height || window?.innerHeight, + width: visualViewport?.width || window.innerWidth, + height, + offsetTop: visualViewport?.offsetTop || 0, + difference: window.innerHeight - height, }; } -export function useViewportSize(): { - width: number; - height: number; -} { - let [size, setSize] = useState(() => getViewportSize()); +export function useVisualViewport(): VisualViewportState { + let [state, setState] = useState(getVisualViewport); useEffect(() => { - // Use visualViewport api to track available height even on iOS virtual keyboard opening let onResize = () => { - setSize((size) => { - let newSize = getViewportSize(); - if (newSize.width === size.width && newSize.height === size.height) { - return size; + setState((prev) => { + let next = getVisualViewport(); + if ( + prev.width === next.width && + prev.height === next.height && + prev.offsetTop === next.offsetTop + ) { + return prev; } - return newSize; + return next; }); }; - if (!visualViewport) { - window.addEventListener("resize", onResize); - } else { - visualViewport.addEventListener("resize", onResize); - } - - return () => { - if (!visualViewport) { - window.removeEventListener("resize", onResize); - } else { - visualViewport.removeEventListener("resize", onResize); - } - }; + let target: VisualViewport | Window = visualViewport || window; + target.addEventListener("resize", onResize); + return () => target.removeEventListener("resize", onResize); }, []); - return size; + return state; +} + +export function ViewportSizeLayout(props: { children: React.ReactNode }) { + let { height, difference } = useVisualViewport(); + return ( +
+ {props.children} +
+ ); }