import { useState, type ReactNode } from "react"; import type YProvider from "y-partyserver/provider"; import { useNavigate, useParams } from "@tanstack/react-router"; import { PanelRightOpen } from "lucide-react"; import { useCanonicalPageContext } from "@/client/components/workspace/use-canonical-page-context"; import { useAuthStore } from "@/client/stores/auth-store"; import { deriveWorkspacePageAffordance, type WorkspacePageAffordance } from "@/client/lib/affordance/workspace-page"; import { isActionEnabled, isActionVisible } from "@/client/lib/affordance/action-state"; import type { ActivePageSnapshot } from "@/client/lib/active-page-model"; import { DocumentPage } from "@/client/components/editor/document-page"; import { CanvasPage } from "@/client/components/canvas/canvas-page"; import { PageBreadcrumbs } from "@/client/components/ui/page-breadcrumbs"; import { PageByline } from "@/client/components/ui/page-byline"; import { PageCover } from "@/client/components/ui/page-cover"; import { PageErrorState } from "@/client/components/ui/page-error-state"; import { PageLoadingSkeleton } from "@/client/components/ui/page-loading-skeleton"; import { type CanvasStageLayout } from "@/client/components/ui/page-layout"; import { PageChrome, PageEmojiIcon } from "@/client/components/ui/page-chrome"; import { AvatarStack } from "@/client/components/presence/avatar-stack"; import { SyncStatusDot } from "@/client/components/presence/sync-status"; import { IconPicker } from "@/client/components/icon-picker"; import { CoverPicker } from "@/client/components/cover-picker"; import { ShareDialog } from "@/client/components/workspace/share-dialog"; import { SummarizeSheet } from "@/client/components/workspace/summarize-sheet"; import { useSyncStatus, type SyncStatus } from "@/client/hooks/use-sync"; import { useOnline } from "@/client/hooks/use-online"; import { DEFAULT_PAGE_TITLE } from "@/shared/constants"; import { useDocumentTitle } from "@/client/hooks/use-document-title"; import { CanonicalActivePageBoundary } from "@/client/components/active-page/canonical"; import { useActivePageActions, useActivePageState, useActivePageSync, } from "@/client/components/active-page/use-active-page"; import { useCanonicalPageActions } from "@/client/components/workspace/use-canonical-page-actions"; import { useWorkspaceLayoutMode } from "@/client/components/workspace/layout-mode"; import type { Page, PageAncestor, Workspace } from "@/shared/types"; export function PageView() { const { pageId } = useParams({ strict: false }) as { pageId: string }; return ( ); } function PageViewContent() { const params = useParams({ strict: false }) as { workspaceSlug: string; pageId: string; }; const navigate = useNavigate(); const activePageState = useActivePageState(); const { syncProvider, setSyncProvider } = useActivePageSync(); const { patchPage } = useActivePageActions(); const { workspaceId: effectiveWorkspaceId, workspace, currentPageMeta, pages, members, accessMode, workspaceRole, } = useCanonicalPageContext(); const currentUser = useAuthStore((s) => s.user); const online = useOnline(); const isSharedMode = accessMode === "shared"; const [summarizeOpen, setSummarizeOpen] = useState(false); const { status } = useSyncStatus(syncProvider, online); const { expanded } = useWorkspaceLayoutMode(); const canvasLayout: CanvasStageLayout = expanded ? "stage" : "centered"; const page = activePageState.kind === "ready" ? activePageState.snapshot : null; const ancestors = activePageState.kind === "ready" ? activePageState.ancestors : []; const creator = currentPageMeta && currentPageMeta.created_by ? (members.find((member) => member.user_id === currentPageMeta.created_by)?.user ?? null) : null; const docFooterLeading = creator && currentPageMeta ? : undefined; useDocumentTitle(page?.title || DEFAULT_PAGE_TITLE); const { handleTitleChange, handleIconChange, handleCoverChange } = useCanonicalPageActions({ workspace, page, syncProvider, patchPage, }); if (activePageState.kind === "loading") { return ( ); } if (activePageState.kind === "unavailable" || !page || !effectiveWorkspaceId) { return ( { if (window.history.length > 1) { window.history.back(); } else { navigate({ to: "/$workspaceSlug", params: { workspaceSlug: params.workspaceSlug }, }); } }, }} /> ); } const pageAffordance = deriveWorkspacePageAffordance({ accessMode, workspaceRole: workspaceRole ?? "none", pageKind: page.kind, pageAccess: activePageState.access.mode, ownsPage: currentUser?.id === currentPageMeta?.created_by, workspaceId: effectiveWorkspaceId, online, }); const headerActions = ( <> {pageAffordance.kind === "doc" && pageAffordance.editor.canSummarizePage && ( )} {!isSharedMode && workspace && isActionVisible(pageAffordance.shareDialog) && ( )} ); const workspaceChrome = ( ); return ( <> {pageAffordance.kind === "doc" ? ( ) : ( )} {pageAffordance.kind === "doc" && (pageAffordance.editor.canSummarizePage || pageAffordance.editor.canAskPage) && ( setSummarizeOpen(false)} workspaceId={effectiveWorkspaceId} pageId={page.id} canSummarize={pageAffordance.editor.canSummarizePage} canAsk={pageAffordance.editor.canAskPage} /> )} ); } interface WorkspacePageChromeProps { page: ActivePageSnapshot; currentPageMeta: Page | null; workspace: Workspace | null; pages: Page[]; ancestors: PageAncestor[]; pageAffordance: WorkspacePageAffordance; workspaceSlug: string; syncProvider: YProvider | null; status: SyncStatus; onIconChange: (icon: string | null) => void; onCoverChange: (cover: string | null) => void; headerActions?: ReactNode; } function WorkspacePageChrome({ page, currentPageMeta, workspace, pages, ancestors, pageAffordance, workspaceSlug, syncProvider, status, onIconChange, onCoverChange, headerActions, }: WorkspacePageChromeProps) { const canEditMetadata = !!workspace && isActionVisible(pageAffordance.editPageMetadata); const editDisabled = !isActionEnabled(pageAffordance.editPageMetadata); const editDisabledTitle = pageAffordance.editPageMetadata.kind === "disabled" ? pageAffordance.editPageMetadata.reason : undefined; const breadcrumb = pageAffordance.breadcrumbMode === "restricted" ? ( ) : ( ); return ( : null} coverOverlay={ canEditMetadata && workspace && page.coverUrl ? ( ) : null } breadcrumb={breadcrumb} breadcrumbActions={ <> {headerActions} } icon={ canEditMetadata && workspace ? ( <> {!page.coverUrl ? ( ) : null} ) : page.icon ? ( ) : null } /> ); }