From b26017d68cbdc57acc21b6007e5c5cff9e76a712 Mon Sep 17 00:00:00 2001 From: Kobe Attias Date: Mon, 5 Oct 2026 20:33:18 -0400 Subject: [PATCH] The document heads with a byline, and Publish asks for the rest in a dialog The detail blocks and the workspace strip are gone. The open title sits in the app's top bar; under the note's title one quiet line carries the authors, the funding goal, the RFP and an offer for anything unset. Nothing on the document marks a detail as missing: Publish opens a dialog that lists what is still empty, takes it there, holds the settings, and publishes under the note's title. Co-Authored-By: Claude Fable 5.1 --- app/layouts/PageLayout.tsx | 13 +- app/layouts/TopBar.tsx | 11 +- app/layouts/topbar/TopBarBreadcrumb.tsx | 9 +- components/AIMode/WorkspacePage.tsx | 59 ++-- components/AIMode/chat/ChatPane.tsx | 10 +- components/AIMode/copy.ts | 5 +- components/AIMode/document/DocumentPane.tsx | 146 ++++++-- components/AIMode/document/MissingDetails.tsx | 308 ++++++++++++++++ components/AIMode/document/PublishDialog.tsx | 331 ++++++++++++++++++ components/AIMode/document/PublishPill.tsx | 76 ++++ .../document/details/AllDetailsBlock.tsx | 69 ---- .../AIMode/document/details/DetailBlock.tsx | 141 -------- .../document/details/DetailBlockStrip.tsx | 86 ----- .../AIMode/document/details/detailBlocks.ts | 138 -------- .../AIMode/document/masthead/AmountWidget.tsx | 110 ++++++ .../document/masthead/ApplyingToWidget.tsx | 56 +++ .../AIMode/document/masthead/CoverImage.tsx | 105 ++++++ .../AIMode/document/masthead/Masthead.tsx | 172 +++++++++ .../AIMode/document/masthead/MastheadLine.tsx | 95 +++++ .../document/masthead/MastheadTitle.tsx | 84 +++++ .../AIMode/document/masthead/PeopleWidget.tsx | 127 +++++++ .../SelectGrantForNoteModal.tsx | 2 +- .../AIMode/document/masthead/TextWidget.tsx | 160 +++++++++ .../document/masthead/mastheadWidgets.ts | 197 +++++++++++ .../document/masthead/useLeaveToCommit.ts | 47 +++ .../document/masthead/usePublishReadiness.ts | 23 ++ .../AIMode/document/useAIModeDocument.ts | 25 ++ components/AIMode/shell/AIModeHeader.tsx | 66 ---- components/AIMode/shell/WorkspacePanes.tsx | 6 +- components/AIMode/shell/WorkspaceTopBar.tsx | 69 ++++ components/AIMode/sidebar/DraftsSection.tsx | 23 +- .../AIMode/sidebar/WorkspaceSidebar.tsx | 5 +- components/Nonprofit/EndaomentInfoPopover.tsx | 2 +- components/Nonprofit/NonprofitInfoPopover.tsx | 2 +- .../Nonprofit/NonprofitSearchSection.tsx | 4 +- .../PublishingForm/PublishingFormProvider.tsx | 56 ++- .../GrantApplicationVisibilitySection.tsx | 5 +- .../PreregistrationPrivacySection.tsx | 3 +- .../components/WorkImageSection.tsx | 50 +-- .../PublishingForm/useCoverImageUpload.ts | 58 +++ .../PublishingForm/usePublishAction.ts | 31 +- components/modals/ConfirmPublishModal.tsx | 76 ++-- components/ui/form/SearchableMultiSelect.tsx | 19 +- components/ui/form/SearchableUserSelect.tsx | 27 +- contexts/TopBarSlotContext.tsx | 21 +- hooks/useFittingCount.ts | 69 ---- 46 files changed, 2437 insertions(+), 760 deletions(-) create mode 100644 components/AIMode/document/MissingDetails.tsx create mode 100644 components/AIMode/document/PublishDialog.tsx create mode 100644 components/AIMode/document/PublishPill.tsx delete mode 100644 components/AIMode/document/details/AllDetailsBlock.tsx delete mode 100644 components/AIMode/document/details/DetailBlock.tsx delete mode 100644 components/AIMode/document/details/DetailBlockStrip.tsx delete mode 100644 components/AIMode/document/details/detailBlocks.ts create mode 100644 components/AIMode/document/masthead/AmountWidget.tsx create mode 100644 components/AIMode/document/masthead/ApplyingToWidget.tsx create mode 100644 components/AIMode/document/masthead/CoverImage.tsx create mode 100644 components/AIMode/document/masthead/Masthead.tsx create mode 100644 components/AIMode/document/masthead/MastheadLine.tsx create mode 100644 components/AIMode/document/masthead/MastheadTitle.tsx create mode 100644 components/AIMode/document/masthead/PeopleWidget.tsx rename components/AIMode/document/{details => masthead}/SelectGrantForNoteModal.tsx (91%) create mode 100644 components/AIMode/document/masthead/TextWidget.tsx create mode 100644 components/AIMode/document/masthead/mastheadWidgets.ts create mode 100644 components/AIMode/document/masthead/useLeaveToCommit.ts create mode 100644 components/AIMode/document/masthead/usePublishReadiness.ts delete mode 100644 components/AIMode/shell/AIModeHeader.tsx create mode 100644 components/AIMode/shell/WorkspaceTopBar.tsx create mode 100644 components/Notebook/PublishingForm/useCoverImageUpload.ts delete mode 100644 hooks/useFittingCount.ts 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' )} > -