diff --git a/src/components/DiagnosticsPanel.tsx b/src/components/DiagnosticsPanel.tsx index d2cb665..d08616d 100644 --- a/src/components/DiagnosticsPanel.tsx +++ b/src/components/DiagnosticsPanel.tsx @@ -1,8 +1,16 @@ +import { useState } from 'react'; + import type { ParserDiagnostic } from '../onenote/model/diagnostics.js'; +import { + DEFAULT_UI_RENDER_LIMITS, + limitUiString, + resolveUiLimit, +} from './ui-render-budget.js'; interface DiagnosticsPanelProps { diagnostics: ParserDiagnostic[]; title?: string; + pageSize?: number; } function formatOffset(offset: number): string { @@ -12,13 +20,69 @@ function formatOffset(offset: number): string { export function DiagnosticsPanel({ diagnostics, title = 'Diagnostics', + pageSize: requestedPageSize, }: DiagnosticsPanelProps) { + const [requestedPage, setRequestedPage] = useState(0); + const pageSize = resolveUiLimit( + requestedPageSize, + DEFAULT_UI_RENDER_LIMITS.maxDiagnosticsPerPage + ); + const pageCount = Math.max(1, Math.ceil(diagnostics.length / pageSize)); + const page = Math.min(requestedPage, pageCount - 1); + const firstDiagnostic = page * pageSize; + const limitedTitle = limitUiString( + title, + DEFAULT_UI_RENDER_LIMITS.maxPanelTitleCharacters + ); + const displayedDiagnostics = diagnostics + .slice(firstDiagnostic, firstDiagnostic + pageSize) + .map((diagnostic, index) => { + const code = limitUiString( + diagnostic.code, + DEFAULT_UI_RENDER_LIMITS.maxDiagnosticCodeCharacters + ); + const message = limitUiString( + diagnostic.message, + DEFAULT_UI_RENDER_LIMITS.maxDiagnosticMessageCharacters + ); + const structure = + diagnostic.structure === undefined + ? undefined + : limitUiString( + diagnostic.structure, + DEFAULT_UI_RENDER_LIMITS.maxDiagnosticContextCharacters + ); + const propertyId = + diagnostic.propertyId === undefined + ? undefined + : limitUiString( + diagnostic.propertyId, + DEFAULT_UI_RENDER_LIMITS.maxDiagnosticContextCharacters + ); + return { + diagnostic, + index: firstDiagnostic + index, + code: code.value, + message: message.value, + structure: structure?.value, + propertyId: propertyId?.value, + truncated: + code.truncated || + message.truncated || + structure?.truncated === true || + propertyId?.truncated === true, + }; + }); + const textTruncated = + limitedTitle.truncated || + displayedDiagnostics.some((diagnostic) => diagnostic.truncated); + return (

Parser details

-

{title}

+

{limitedTitle.value}

{diagnostics.length}
@@ -27,36 +91,36 @@ export function DiagnosticsPanel({

No parser diagnostics for this view.

) : ( )} + {diagnostics.length > 0 ? ( + + ) : null} + {textTruncated ? ( +

+ Long diagnostic text was shortened for safe browser display. +

+ ) : null}
); } diff --git a/src/components/PackageInspector.tsx b/src/components/PackageInspector.tsx index 6b51c1a..14e78cc 100644 --- a/src/components/PackageInspector.tsx +++ b/src/components/PackageInspector.tsx @@ -1,7 +1,15 @@ +import { useState } from 'react'; + import type { OneNotePackageDto } from '../onenote/model/dto.js'; +import { + DEFAULT_UI_RENDER_LIMITS, + limitUiString, + resolveUiLimit, +} from './ui-render-budget.js'; interface PackageInspectorProps { notebook: OneNotePackageDto; + pageSize?: number; } function formatBytes(value: number): string { @@ -16,14 +24,56 @@ function formatBytes(value: number): string { return `${amount.toFixed(amount >= 10 ? 1 : 2)} ${unit}`; } -export function PackageInspector({ notebook }: PackageInspectorProps) { - const totalExpanded = notebook.entries.reduce( - (total, entry) => total + entry.uncompressedSize, - 0 +export function PackageInspector({ + notebook, + pageSize: requestedPageSize, +}: PackageInspectorProps) { + const [requestedPage, setRequestedPage] = useState(0); + const pageSize = resolveUiLimit( + requestedPageSize, + DEFAULT_UI_RENDER_LIMITS.maxPackageEntriesPerPage ); - const sectionCount = notebook.entries.filter( - (entry) => entry.kind === 'section' - ).length; + const pageCount = Math.max(1, Math.ceil(notebook.entries.length / pageSize)); + const page = Math.min(requestedPage, pageCount - 1); + const firstEntry = page * pageSize; + const displayedEntries = notebook.entries + .slice(firstEntry, firstEntry + pageSize) + .map((entry, index) => { + const normalizedPath = limitUiString( + entry.normalizedPath, + DEFAULT_UI_RENDER_LIMITS.maxPackagePathCharacters + ); + const originalPath = limitUiString( + entry.path, + DEFAULT_UI_RENDER_LIMITS.maxPackagePathCharacters + ); + const compressionMethod = limitUiString( + entry.compressionMethod || 'unknown', + DEFAULT_UI_RENDER_LIMITS.maxPackageMethodCharacters + ); + return { + entry, + index: firstEntry + index, + normalizedPath: normalizedPath.value, + originalPath: originalPath.value, + compressionMethod: compressionMethod.value, + truncated: + normalizedPath.truncated || + originalPath.truncated || + compressionMethod.truncated, + }; + }); + const textTruncated = displayedEntries.some((entry) => entry.truncated); + + let totalExpanded = 0; + let sectionCount = 0; + for (const entry of notebook.entries) { + if (entry.kind === 'section') sectionCount += 1; + const size = Number.isFinite(entry.uncompressedSize) + ? Math.max(0, entry.uncompressedSize) + : 0; + totalExpanded = Math.min(Number.MAX_SAFE_INTEGER, totalExpanded + size); + } return (
@@ -66,20 +116,22 @@ export function PackageInspector({ notebook }: PackageInspectorProps) { - {notebook.entries.map((entry) => ( - + {displayedEntries.map((displayed) => ( + - {entry.normalizedPath} - {entry.path !== entry.normalizedPath ? ( - Original: {entry.path} + {displayed.normalizedPath} + {displayed.entry.path !== displayed.entry.normalizedPath ? ( + Original: {displayed.originalPath} ) : null} - {entry.kind} - {formatBytes(entry.uncompressedSize)} - {entry.compressionMethod || 'unknown'} + {displayed.entry.kind} + {formatBytes(displayed.entry.uncompressedSize)} + {displayed.compressionMethod} - - {entry.parseStatus} + + {displayed.entry.parseStatus} @@ -87,6 +139,42 @@ export function PackageInspector({ notebook }: PackageInspectorProps) { + {notebook.entries.length > 0 ? ( + + ) : null} + {textTruncated ? ( +

+ Long package paths or compression labels were shortened for safe + browser display. +

+ ) : null}
); diff --git a/src/components/SectionNavigation.tsx b/src/components/SectionNavigation.tsx index ef8cc46..31af0d7 100644 --- a/src/components/SectionNavigation.tsx +++ b/src/components/SectionNavigation.tsx @@ -6,12 +6,21 @@ import type { OneNoteSectionDto, } from '../onenote/model/dto.js'; import { buildPageTree, type PageTreeNode } from './page-tree.js'; +import { + DEFAULT_UI_RENDER_LIMITS, + limitUiString, + prepareNotebookForNavigation, + preparePagesForNavigation, + resolveUiLimit, + type NavigationRenderLimits, +} from './ui-render-budget.js'; interface PageListProps { pages: OneNotePageSummaryDto[]; selectedPageId?: string; onSelectPage(pageId: string): void; nested?: boolean; + renderLimits?: Partial; } function PageTreeItems({ @@ -52,19 +61,31 @@ function PageList({ selectedPageId, onSelectPage, nested = false, + renderLimits, }: PageListProps) { if (pages.length === 0) { return

No pages were extracted.

; } + const projection = preparePagesForNavigation(pages, renderLimits); + return ( - + <> + + {projection.truncated ? ( +

+ {projection.pages.length < pages.length + ? `Navigation display is limited to the first ${projection.pages.length} of ${pages.length} pages. Long titles and previews may also be shortened.` + : 'Long page titles or previews were shortened for safe browser display.'} +

+ ) : null} + ); } @@ -72,24 +93,41 @@ interface SingleSectionNavigationProps { section: OneNoteSectionDto; selectedPageId?: string; onSelectPage(pageId: string): void; + renderLimits?: Partial; } export function SingleSectionNavigation({ section, selectedPageId, onSelectPage, + renderLimits, }: SingleSectionNavigationProps) { + const titleLimit = resolveUiLimit( + renderLimits?.maxNavigationTitleCharacters, + DEFAULT_UI_RENDER_LIMITS.maxNavigationTitleCharacters + ); + const heading = limitUiString( + section.sectionName || section.sourceName, + titleLimit + ); + return ( ); @@ -102,6 +140,7 @@ interface PackageNavigationProps { selectedPageId?: string; onSelectSection(sectionId: string): void; onSelectPage(pageId: string): void; + renderLimits?: Partial; } function NotebookTreeItems({ @@ -111,6 +150,7 @@ function NotebookTreeItems({ selectedPageId, onSelectSection, onSelectPage, + renderLimits, }: { nodes: OneNoteNotebookNodeDto[]; sections: ReadonlyMap; @@ -118,6 +158,7 @@ function NotebookTreeItems({ selectedPageId?: string; onSelectSection(sectionId: string): void; onSelectPage(pageId: string): void; + renderLimits?: Partial; }) { return nodes.map((node) => { if (node.kind === 'section-group') { @@ -133,6 +174,7 @@ function NotebookTreeItems({ selectedPageId={selectedPageId} onSelectSection={onSelectSection} onSelectPage={onSelectPage} + renderLimits={renderLimits} /> @@ -147,12 +189,12 @@ function NotebookTreeItems({