Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/site-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,7 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
- `/mockups/document-search/source` - Route discovered from app directory Source: `src/app/mockups/document-search/source/page.tsx`.
- `/mockups/document-search/source-overlays` - Route discovered from app directory Source: `src/app/mockups/document-search/source-overlays/page.tsx`.
- `/mockups/document-search/source/evidence` - Route discovered from app directory Source: `src/app/mockups/document-search/source/evidence/page.tsx`.
- `/mockups/document-top-navigation` - Route discovered from app directory Source: `src/app/mockups/document-top-navigation/page.tsx`.
- `/mockups/favourites-command-console` - Route discovered from app directory Source: `src/app/mockups/favourites-command-console/page.tsx`.
- `/mockups/favourites-command-desk` - Route discovered from app directory Source: `src/app/mockups/favourites-command-desk/page.tsx`.
- `/mockups/favourites-hub` - Route discovered from app directory Source: `src/app/mockups/favourites-hub/page.tsx`.
Expand Down
5 changes: 5 additions & 0 deletions src/app/mockups/document-top-navigation/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { DocumentTopNavigationMockups } from "@/components/document-top-navigation-mockups";

export default function DocumentTopNavigationMockupPage() {
return <DocumentTopNavigationMockups />;
}
4 changes: 3 additions & 1 deletion src/app/mockups/mockups-layout-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) {
const isSourceOverlayRedesignMockup = pathname === "/mockups/document-search/source-overlays";
const isStandaloneDocumentFlow = pathname === "/mockups/document-search";
const isUniversalSearchRedesignMockup = pathname === "/mockups/universal-search-redesign";
const isDocumentTopNavigationMockup = pathname === "/mockups/document-top-navigation";
// The calculators search page owns its own search input (top on desktop, docked
// at the bottom on phones), so the shared universal composer is suppressed here
// to avoid a second, floating search bar.
Expand All @@ -34,9 +35,10 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) {
!isFavouritesPageMockup &&
!isStandaloneDocumentFlow &&
!isUniversalSearchRedesignMockup &&
!isDocumentTopNavigationMockup &&
!isCalculatorsSearchPageMockup
}
chromeVisible={!isSourceOverlayRedesignMockup}
chromeVisible={!isSourceOverlayRedesignMockup && !isDocumentTopNavigationMockup}
>
{children}
</GlobalMockupSearchShell>
Expand Down
67 changes: 50 additions & 17 deletions src/components/DocumentViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
Plus,
RefreshCw,
Search,
Send,
Sparkles,
Target,
} from "lucide-react";
Expand Down Expand Up @@ -302,6 +301,8 @@ export function DocumentViewer({
const [isOnline, setIsOnline] = useState(true);
const [localProjectReady, setLocalProjectReady] = useState(true);
const [mobileActionsOpen, setMobileActionsOpen] = useState(false);
const documentSearchInputRef = useRef<HTMLInputElement | null>(null);
const focusDocumentSearchAfterSheetCloseRef = useRef(false);
// Phone-only hide-on-scroll for the bottom composer: never hide while the
// mobile actions sheet is open or while focus sits inside the composer
// (keyboard users must not tab into invisible controls).
Expand Down Expand Up @@ -333,6 +334,20 @@ export function DocumentViewer({
resetKey: `${documentId}:${activePage}:${activeChunkId ?? ""}`,
});
const composerScrollHidden = scrollHidden && !mobileActionsOpen && !composerChromeFocused;
const focusDocumentSearch = useCallback(() => {
setComposerChromeFocused(true);
window.requestAnimationFrame(() => {
const input = documentSearchInputRef.current;
input?.focus({ preventScroll: true });
input?.select();
});
}, []);
useEffect(() => {
if (mobileActionsOpen || !focusDocumentSearchAfterSheetCloseRef.current) return;
focusDocumentSearchAfterSheetCloseRef.current = false;
const timeout = window.setTimeout(focusDocumentSearch, 0);
return () => window.clearTimeout(timeout);
}, [focusDocumentSearch, mobileActionsOpen]);
// Read localStorage once on mount, then seed both derived states from it.
const [initialPdfViewerMode] = useState(getInitialPdfViewerMode);
const [useNativePdfViewer, setUseNativePdfViewer] = useState(initialPdfViewerMode.useNativePdfViewer);
Expand Down Expand Up @@ -857,8 +872,7 @@ export function DocumentViewer({
setSummaryError("Load a source document before summarising.");
return;
}
const summaryMode = sourceSearch.trim().length === 0;
const query = summaryMode ? documentSummaryQuestion : sourceSearch.trim();
const query = documentSummaryQuestion;
const controller = new AbortController();
summaryAbortRef.current?.abort();
summaryAbortRef.current = controller;
Expand Down Expand Up @@ -886,7 +900,7 @@ export function DocumentViewer({
"Content-Type": "application/json",
...(clientDemoMode ? {} : authorizationHeader),
},
body: JSON.stringify({ query, documentId, ...(summaryMode ? { summaryMode: true } : {}) }),
body: JSON.stringify({ query, documentId, summaryMode: true }),
signal: controller.signal,
});
if (response.status === 401) markSessionExpired();
Expand Down Expand Up @@ -976,6 +990,7 @@ export function DocumentViewer({
: documentHomeHref;
const usefulPageHref = (page: number) => documentPageHref(documentId, page);
const canSummarizeDocument = viewerState === "ready" && !loadingSummary && canUsePrivateApis;
const canSubmitDocumentSearch = viewerState === "ready" && canViewSourceDocuments && sourceSearch.trim().length >= 2;
const summarizeTitle = !canUsePrivateApis
? "Sign in before answering from this document"
: viewerState !== "ready" || loadingSummary
Expand Down Expand Up @@ -1111,6 +1126,17 @@ export function DocumentViewer({
</h1>

<div className="ml-auto flex shrink-0 items-center gap-1.5">
<button
type="button"
onClick={focusDocumentSearch}
disabled={!readyDocument}
title={readyDocument ? "Search within this document" : "Load a source document before searching"}
className="grid h-tap w-tap place-items-center rounded-full text-[color:var(--text-muted)] hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--text)] disabled:cursor-not-allowed disabled:opacity-40"
aria-label="Go to document search"
aria-controls="document-viewer-search-input"
>
<Search aria-hidden="true" className="h-5 w-5" />
</button>
Comment on lines +1129 to +1139

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

aria-controls can reference a nonexistent element.

The composer <form>/input only render when readyDocument is truthy (Line 1708), but this button unconditionally sets aria-controls="document-viewer-search-input" even while disabled (!readyDocument). That leaves aria-controls pointing at an ID that doesn't exist in the DOM while the document is loading/errored.

🛡️ Suggested fix
               aria-label="Go to document search"
-              aria-controls="document-viewer-search-input"
+              aria-controls={readyDocument ? "document-viewer-search-input" : undefined}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
type="button"
onClick={focusDocumentSearch}
disabled={!readyDocument}
title={readyDocument ? "Search within this document" : "Load a source document before searching"}
className="grid h-tap w-tap place-items-center rounded-full text-[color:var(--text-muted)] hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--text)] disabled:cursor-not-allowed disabled:opacity-40"
aria-label="Go to document search"
aria-controls="document-viewer-search-input"
>
<Search aria-hidden="true" className="h-5 w-5" />
</button>
<button
type="button"
onClick={focusDocumentSearch}
disabled={!readyDocument}
title={readyDocument ? "Search within this document" : "Load a source document before searching"}
className="grid h-tap w-tap place-items-center rounded-full text-[color:var(--text-muted)] hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--text)] disabled:cursor-not-allowed disabled:opacity-40"
aria-label="Go to document search"
aria-controls={readyDocument ? "document-viewer-search-input" : undefined}
>
<Search aria-hidden="true" className="h-5 w-5" />
</button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/DocumentViewer.tsx` around lines 1129 - 1139, Update the
document search button near focusDocumentSearch so aria-controls is only present
when readyDocument is truthy; otherwise omit the attribute while the search
input is not rendered. Preserve the existing button disabled behavior and
document-viewer-search-input ID for ready documents.

<Link
href={scopedDocumentHref}
className="hidden h-tap w-tap place-items-center rounded-full text-[color:var(--text-muted)] hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--text)] min-[380px]:grid"
Expand Down Expand Up @@ -1153,8 +1179,8 @@ export function DocumentViewer({
<button
type="button"
onClick={() => {
focusDocumentSearchAfterSheetCloseRef.current = true;
setMobileActionsOpen(false);
setSourceSearch(documentDisplayTitle(readyDocument));
}}
className={cn(secondaryButton, "min-h-12 justify-start text-xs")}
>
Expand Down Expand Up @@ -1330,7 +1356,7 @@ export function DocumentViewer({
) : null}

<div className="min-w-0 space-y-4 sm:space-y-5 lg:mx-auto lg:w-full lg:max-w-4xl">
<DocumentViewerAnchors evidenceHref="#source-evidence" textHref="#source-text" className="lg:hidden" />
<DocumentViewerAnchors evidenceHref="#source-evidence" className="lg:hidden" />

<div id="pdf-preview-section" className={cn(panel, "scroll-mt-24 overflow-hidden")}>
<div data-testid="pdf-preview">
Expand Down Expand Up @@ -1491,7 +1517,7 @@ export function DocumentViewer({
) : null}

<div className="hidden lg:block">
<DocumentViewerAnchors evidenceHref="#source-evidence-rail" textHref="#source-text" className="mb-3" />
<DocumentViewerAnchors evidenceHref="#source-evidence-rail" className="mb-3" />
<PinnedSourceEvidence
loading={effectiveLoadingDocument}
chunk={selectedChunk}
Expand Down Expand Up @@ -1683,8 +1709,13 @@ export function DocumentViewer({
<form
onSubmit={(event) => {
event.preventDefault();
if (canSummarizeDocument) void summarize();
if (!canSubmitDocumentSearch) return;
window.document
.getElementById("source-text")
?.scrollIntoView({ block: "start", behavior: resolveScrollBehavior() });
}}
role="search"
aria-label="Search within this document"
data-scroll-hidden={composerScrollHidden ? "true" : undefined}
onFocusCapture={() => setComposerChromeFocused(true)}
onBlurCapture={(event) => {
Expand All @@ -1707,25 +1738,27 @@ export function DocumentViewer({
<Plus aria-hidden="true" className="h-5 w-5" />
</button>
<label className="relative flex min-w-0 flex-1 items-center overflow-hidden">
<span className="sr-only">Search or answer from this document</span>
<span className="sr-only">Search within this document</span>
<input
ref={documentSearchInputRef}
id="document-viewer-search-input"
type="search"
name="document-search"
enterKeyHint="search"
autoComplete="off"
value={sourceSearch}
onChange={(event) => setSourceSearch(event.target.value)}
placeholder="Search or answer from this document..."
placeholder="Search within this document..."
className="min-h-tap min-w-0 flex-1 bg-transparent px-2 text-base font-medium text-[color:var(--text)] outline-none placeholder:text-[color:var(--text-soft)]"
/>
</label>
<button
type="submit"
disabled={!canSummarizeDocument}
disabled={!canSubmitDocumentSearch}
className="grid h-tap w-tap shrink-0 place-items-center rounded-full bg-[color:var(--clinical-accent)] text-[color:var(--clinical-accent-contrast)] shadow-[var(--shadow-inset),var(--shadow-tight)] hover:bg-[color:var(--clinical-accent-hover)] disabled:cursor-not-allowed disabled:opacity-50"
aria-label="Answer from this document"
aria-label="Search document text"
>
{loadingSummary ? (
<Loader2 aria-hidden="true" className="h-4 w-4 animate-spin" />
) : (
<Send aria-hidden="true" className="h-4 w-4" />
)}
<Search aria-hidden="true" className="h-4 w-4" />
</button>
</form>
) : null}
Expand Down
Loading
Loading