diff --git a/app/layouts/PageLayout.tsx b/app/layouts/PageLayout.tsx index 6e9c62d6b..1405e473d 100644 --- a/app/layouts/PageLayout.tsx +++ b/app/layouts/PageLayout.tsx @@ -133,15 +133,10 @@ function PageLayoutInner({ {children} ) : ( - /* Scrollable content area. - Mobile: top padding clears the fixed top bar (needed so content - can scroll under the hide-on-scroll bar). - Tablet+: top margin instead, so the scrollport starts below the bar. - When the EndowmentPromoBanner is visible above the TopBar on mobile - we add extra top padding to clear the banner + topbar stack. - The banner itself is hidden at >= 768px (tablet:!hidden) so the - offset is reset by the inner media query below. - Bottom padding on mobile clears the fixed MobileBottomNav. */ + /* The scrollport. Padding clears the fixed top bar on phones, where it + hides on scroll; a margin does from the tablet breakpoint up. The + `page-layout-*` classes reserve room for the promo banner and the + bottom nav where those show. */
{ @@ -105,10 +110,14 @@ export function TopBar({ onMenuClick }: TopBarProps) { {showBackButton && } + {leading} + {pageInfo && ( )} diff --git a/app/layouts/topbar/TopBarBreadcrumb.tsx b/app/layouts/topbar/TopBarBreadcrumb.tsx index 143cef289..825c17943 100644 --- a/app/layouts/topbar/TopBarBreadcrumb.tsx +++ b/app/layouts/topbar/TopBarBreadcrumb.tsx @@ -6,12 +6,15 @@ interface TopBarBreadcrumbProps { variant: 'mobile' | 'desktop'; /** Ellipsize a long title so sibling controls (e.g. sticky tab pills) stay visible. */ truncateTitle?: boolean; + /** `lg` (26px) for a page's own name; `md` (20px) for the longer title of what a page has open. */ + size?: 'lg' | 'md'; } export const TopBarBreadcrumb = ({ pageInfo, variant, truncateTitle = false, + size = 'lg', }: TopBarBreadcrumbProps) => { const isMobile = variant === 'mobile'; @@ -26,7 +29,11 @@ export const TopBarBreadcrumb = ({ truncateTitle ? 'min-w-0 truncate' : 'flex-shrink-0' ); - const titleStyle = isMobile ? undefined : { fontSize: '26px', letterSpacing: '-0.5px' }; + const desktopTitleStyle = + size === 'md' + ? { fontSize: '20px', letterSpacing: '-0.3px' } + : { fontSize: '26px', letterSpacing: '-0.5px' }; + const titleStyle = isMobile ? undefined : desktopTitleStyle; return (
diff --git a/components/AIMode/WorkspacePage.tsx b/components/AIMode/WorkspacePage.tsx index c64fbb457..582e5fc2c 100644 --- a/components/AIMode/WorkspacePage.tsx +++ b/components/AIMode/WorkspacePage.tsx @@ -1,7 +1,7 @@ 'use client'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { PanelRight } from 'lucide-react'; +import { PanelRightOpen } from 'lucide-react'; import { PageLayout } from '@/app/layouts/PageLayout'; import { cn } from '@/utils/styles'; import { useMediaQuery } from '@/hooks/useMediaQuery'; @@ -12,8 +12,8 @@ import { DocumentCard } from './chat/DocumentCard'; import { conversationTitleFor } from './chat/conversationTitle'; import { DocumentPane, type DocumentPaneView } from './document/DocumentPane'; import { useAIModeDocument } from './document/useAIModeDocument'; -import { AIModeHeader } from './shell/AIModeHeader'; import { WorkspacePanes } from './shell/WorkspacePanes'; +import { WorkspaceTopBar } from './shell/WorkspaceTopBar'; import { WorkspaceSidebar } from './sidebar/WorkspaceSidebar'; import { useAIModeChat } from './useAIModeChat'; import { newConversationTitle } from './copy'; @@ -21,8 +21,6 @@ import { layoutFor } from './workspaceUrl'; const CHAT_MIN_WIDTH = 360; const DOCUMENT_MIN_WIDTH = 420; -/** The details form needs more room than the document: authors, image, funding fields. */ -const DETAILS_MIN_WIDTH = 560; /** Share of the viewport the document opens at before the user drags it. */ const DOCUMENT_DEFAULT_SHARE = 0.55; @@ -66,7 +64,7 @@ function Workspace() { // being an icon rail (Tailwind's `sidebar-compact`). Below that they wait // behind a button: a panel over the panes, or on a phone a screen of their own. const listsInColumn = useMediaQuery('(min-width: 1240px)') === true; - // Tailwind's `tablet` breakpoint; the strip and the side-by-side panes only exist from it up. + // Tailwind's `tablet` breakpoint; the side-by-side panes only exist from it up. const isBelowTablet = useMediaQuery('(max-width: 767px)') === true; const [listOpen, setListOpen] = useState(false); const closeList = useCallback(() => setListOpen(false), []); @@ -84,13 +82,12 @@ function Workspace() { // ---- pane widths: the side pane drags, the main pane takes the rest ---- const [panesEl, setPanesEl] = useState(null); const panesWidth = useElementWidth(panesEl); - // Document or details in the document pane; details wants a wider floor. + // Document or details in the phone's drawer; the column only shows the document. const [documentView, setDocumentView] = useState('document'); - const documentMinWidth = documentView === 'details' ? DETAILS_MIN_WIDTH : DOCUMENT_MIN_WIDTH; // The side pane is the document (chat first) or the chat (document first); // it may grow until the main pane is down to its own minimum column. const sideIsDocument = layout === 'chat'; - const sideMinWidth = sideIsDocument ? documentMinWidth : CHAT_MIN_WIDTH; + const sideMinWidth = sideIsDocument ? DOCUMENT_MIN_WIDTH : CHAT_MIN_WIDTH; const sideMaxWidth = Math.max( sideMinWidth, panesWidth - (sideIsDocument ? CHAT_MIN_WIDTH : DOCUMENT_MIN_WIDTH) @@ -121,10 +118,16 @@ function Workspace() { const openDocument = useCallback(() => setDocumentOpen(true), []); const closeDocument = useCallback(() => setDocumentOpen(false), []); const showDocument = noteId != null && documentOpen; - const documentTitle = doc.content?.title?.trim() || state.note?.title?.trim() || 'Document'; - // The strip names what is open: the conversation when the chat is the main - // pane, the document when it is, and the new-conversation screen by what it - // is for — an RFP or a proposal. + // The Drafts list follows the open document's title as it is renamed. + const docTitle = doc.title; + const openNote = useMemo( + () => (noteId != null && docTitle ? { id: noteId, title: docTitle } : null), + [noteId, docTitle] + ); + const documentTitle = doc.title || 'Document'; + // The top bar names what is open: the conversation when the chat is the + // main pane, the document when it is, and the new-conversation screen by + // what it is for — an RFP or a proposal. const { title: conversationTitle } = conversationTitleFor(state); const headerTitle = layout === 'document' @@ -133,10 +136,6 @@ function Workspace() { ? conversationTitle : newConversationTitle(state.intent); - // The document column puts its publish controls up in the strip, where - // they read as the workspace's, not the pane's. It renders into this slot. - const [publishControlsSlot, setPublishControlsSlot] = useState(null); - // The turn that created the document, for seating its card in the transcript. const documentCardExecutionId = useMemo(() => { if (noteId == null) return null; @@ -170,7 +169,6 @@ function Workspace() { view={documentView} onViewChange={setDocumentView} presentation={presentation} - publishControlsSlot={presentation === 'pane' ? publishControlsSlot : null} readOnly={presentation === 'drawer'} className={presentation === 'drawer' ? '-mx-4 -mt-2' : undefined} /> @@ -182,34 +180,35 @@ function Workspace() { leftSidebarContent={ listsInColumn ? (
- +
) : null } >
- {!isBelowTablet && ( - - )} + } + lists={ + listsInColumn ? null : ( + + ) + } listOpen={listOpen} onCloseList={closeList} onCloseDocumentDrawer={closeDocument} chat={ setListOpen(true)} documentCard={documentCard} documentCardExecutionId={documentCardExecutionId} @@ -228,7 +227,7 @@ function Workspace() { : 'text-gray-500 hover:bg-gray-100 hover:text-gray-900' )} > -