From 23b399549a6a478f1c8a843b16ccb48de344c586 Mon Sep 17 00:00:00 2001 From: Ben Jacobson Date: Tue, 13 Oct 2026 17:57:02 +0000 Subject: [PATCH 1/2] Add document analysis primitives and reference demo --- .changeset/soft-clowns-push.md | 11 + README.md | 5 +- apps/demo/e2e/document-analysis.scenarios.ts | 112 ++++ apps/demo/e2e/pdf-viewer.scenarios.ts | 28 +- apps/demo/e2e/workflow.e2e.test.ts | 4 + apps/demo/src/app/message.ts | 2 + apps/demo/src/app/model.ts | 6 + apps/demo/src/app/route.ts | 10 + apps/demo/src/app/subscriptions.ts | 8 + apps/demo/src/app/update.ts | 14 +- apps/demo/src/app/view.ts | 106 +-- apps/demo/src/document-analysis/fixture.ts | 89 +++ apps/demo/src/document-analysis/message.ts | 20 + apps/demo/src/document-analysis/model.ts | 29 + apps/demo/src/document-analysis/styles.ts | 24 + .../src/document-analysis/subscriptions.ts | 8 + apps/demo/src/document-analysis/update.ts | 84 +++ apps/demo/src/document-analysis/view.ts | 183 ++++++ apps/demo/src/pdf-viewer/view.ts | 1 + apps/demo/src/styles.css | 1 + docs/document-analysis.md | 184 ++++++ docs/planned-primitives.md | 4 +- packages/data-grid/README.md | 13 + packages/data-grid/src/core.ts | 2 + packages/data-grid/src/styles.css | 17 + packages/data-grid/src/view.ts | 2 + packages/diagram/README.md | 59 ++ packages/diagram/package.json | 11 +- packages/diagram/src/graph.test.ts | 106 +++ packages/diagram/src/graph.ts | 611 ++++++++++++++++++ packages/diagram/src/index.ts | 1 + packages/diagram/src/styles.css | 105 +++ packages/diff-viewer/README.md | 55 +- packages/diff-viewer/src/index.ts | 7 + packages/diff-viewer/src/prose.test.ts | 45 ++ packages/diff-viewer/src/prose.ts | 177 +++++ packages/diff-viewer/src/styles.css | 276 ++++++-- packages/pdf-viewer/README.md | 59 +- packages/pdf-viewer/package.json | 3 +- packages/pdf-viewer/src/index.ts | 2 +- packages/pdf-viewer/src/pdf-viewer.ts | 8 +- packages/pdf-viewer/src/render-session.ts | 266 ++++++++ packages/pdf-viewer/src/styles.css | 64 ++ packages/pdf-viewer/src/viewer-message.ts | 6 + packages/pdf-viewer/src/viewer-model.ts | 22 +- packages/pdf-viewer/src/viewer-update.test.ts | 55 +- packages/pdf-viewer/src/viewer-update.ts | 162 ++--- packages/pdf-viewer/src/viewer-view.ts | 114 +++- packages/pdf/README.md | 31 + packages/pdf/src/index.ts | 11 + packages/pdf/src/render.ts | 29 +- packages/pdf/src/text.test.ts | 51 ++ packages/pdf/src/text.ts | 186 ++++++ packages/text-intelligence/README.md | 48 ++ .../src/highlighted-text.test.ts | 68 ++ .../text-intelligence/src/highlighted-text.ts | 187 ++++++ packages/text-intelligence/src/index.ts | 10 + packages/text-intelligence/src/styles.css | 78 +++ packages/ui/README.md | 36 ++ packages/ui/src/citation.styles.ts | 24 + packages/ui/src/citation.test.ts | 46 ++ packages/ui/src/citation.ts | 103 +++ packages/ui/src/index.ts | 1 + pnpm-lock.yaml | 3 + scripts/check-packages.mjs | 11 +- 65 files changed, 3882 insertions(+), 222 deletions(-) create mode 100644 .changeset/soft-clowns-push.md create mode 100644 apps/demo/e2e/document-analysis.scenarios.ts create mode 100644 apps/demo/src/document-analysis/fixture.ts create mode 100644 apps/demo/src/document-analysis/message.ts create mode 100644 apps/demo/src/document-analysis/model.ts create mode 100644 apps/demo/src/document-analysis/styles.ts create mode 100644 apps/demo/src/document-analysis/subscriptions.ts create mode 100644 apps/demo/src/document-analysis/update.ts create mode 100644 apps/demo/src/document-analysis/view.ts create mode 100644 docs/document-analysis.md create mode 100644 packages/diagram/src/graph.test.ts create mode 100644 packages/diagram/src/graph.ts create mode 100644 packages/diagram/src/styles.css create mode 100644 packages/diff-viewer/src/prose.test.ts create mode 100644 packages/diff-viewer/src/prose.ts create mode 100644 packages/pdf-viewer/src/render-session.ts create mode 100644 packages/pdf/src/text.test.ts create mode 100644 packages/pdf/src/text.ts create mode 100644 packages/text-intelligence/src/highlighted-text.test.ts create mode 100644 packages/text-intelligence/src/highlighted-text.ts create mode 100644 packages/ui/src/citation.styles.ts create mode 100644 packages/ui/src/citation.test.ts create mode 100644 packages/ui/src/citation.ts diff --git a/.changeset/soft-clowns-push.md b/.changeset/soft-clowns-push.md new file mode 100644 index 0000000..9d44341 --- /dev/null +++ b/.changeset/soft-clowns-push.md @@ -0,0 +1,11 @@ +--- +"@foldworks/pdf": minor +"@foldworks/pdf-viewer": minor +"@foldworks/text-intelligence": minor +"@foldworks/diagram": minor +"@foldworks/diff-viewer": minor +"@foldworks/ui": minor +"@foldworks/data-grid": minor +--- + +Add generic document-analysis building blocks: positioned PDF text and explicit quote location, multi-region passage highlights, selectable PDF text and bounded viewport rendering, overlap-safe highlighted text and context snippets, read-only graph exploration, word/sentence prose diffs, citation previews, and per-cell grid tones. diff --git a/README.md b/README.md index 3855d12..1882bf0 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,8 @@ rows and the code editor, placeholder rows, structure policy, a structural tree diff, and a lazily loaded value tree. See [planned work-surface primitives](./docs/planned-primitives.md) for the -next layers of the library to explore. +next layers of the library to explore, and [document analysis](./docs/document-analysis.md) +for available primitives for checking extracted values against their source documents. ## WorldVM @@ -305,3 +306,5 @@ package-boundary decisions are documented in [Desktop primitives](packages/ui/do The outliner and native Markdown editor share plain-text mentions and tags. See the [design and adapter contract](./docs/mentions-and-tags.md) for host lookup, syntax exclusions, stable identity, and persistence. + +[Document analysis demo](https://foldworks.dev/document-analysis) composes PDF passage highlights and text selection, transcript ranges, prose diffs, citation previews, and a read-only relationship graph using synthetic documents. diff --git a/apps/demo/e2e/document-analysis.scenarios.ts b/apps/demo/e2e/document-analysis.scenarios.ts new file mode 100644 index 0000000..4b78f44 --- /dev/null +++ b/apps/demo/e2e/document-analysis.scenarios.ts @@ -0,0 +1,112 @@ +import type { Page } from "playwright"; +import { describe, expect, it } from "vitest"; + +export const documentAnalysisScenarios = ( + getPage: () => Page, + appUrl: string, + screenshot: (name: string) => Promise, +) => { + describe("document analysis", () => { + it("links cross-page evidence, selects PDF text, and bounds rendering across jumps and remounts", async () => { + const page = getPage(); + await page.emulateMedia({ reducedMotion: "reduce" }); + await page.goto(`${appUrl}/document-analysis`, { waitUntil: "networkidle" }); + const pages = page.locator("[data-pdf-viewer-page]"); + const images = page.locator(".fk-pdf-viewer__page-image"); + await expect.poll(() => pages.count()).toBe(12); + await expect.poll(() => pages.nth(0).getAttribute("data-state")).toBe("rendered"); + expect(await images.count()).toBeLessThanOrEqual(3); + expect(await pages.nth(11).getAttribute("data-state")).toBe("pending"); + const citation = page.locator('[data-citation="1"]'); + await citation.focus(); + await expect.poll(() => page.getByRole("tooltip").isVisible()).toBe(true); + await citation.press("Escape"); + await expect.poll(() => page.getByRole("tooltip").count()).toBe(0); + await citation.press("Enter"); + const highlight = page.locator('button[data-pdf-overlay-key="quote"]'); + await expect.poll(() => highlight.getAttribute("aria-pressed")).toBe("true"); + expect(await highlight.count()).toBe(1); + expect(await page.locator('[data-pdf-overlay-key="quote"]').count()).toBe(2); + const selectText = async () => { + await page + .locator('[data-pdf-viewer-page="0"] [data-pdf-text-layer] span') + .filter({ hasText: "Employees with" }) + .evaluate((element) => { + const range = document.createRange(); + range.setStart(element.firstChild!, 0); + range.setEnd(element.firstChild!, 9); + const selection = document.getSelection()!; + selection.removeAllRanges(); + selection.addRange(range); + document.dispatchEvent(new PointerEvent("pointerup")); + }); + await expect + .poll(() => page.locator("[data-pdf-selected-text]").textContent()) + .toBe("Selected: Employees"); + }; + await selectText(); + await page.getByRole("button", { name: "Jump to last page" }).click(); + await expect.poll(() => pages.nth(11).getAttribute("data-state")).toBe("rendered"); + expect(await images.count()).toBeLessThanOrEqual(3); + await expect.poll(() => pages.nth(0).locator("[data-pdf-text-layer] span").count()).toBe(0); + await citation.click(); + await expect.poll(() => pages.nth(0).getAttribute("data-state")).toBe("rendered"); + await selectText(); + await page.getByRole("link", { name: "Document editor", exact: true }).click(); + await page.getByRole("link", { name: "Document analysis", exact: true }).click(); + await expect.poll(() => pages.nth(0).getAttribute("data-state")).toBe("rendered"); + await selectText(); + expect(await images.count()).toBeLessThanOrEqual(3); + await citation.click(); + // Wait for the citation to place both sides of the page boundary in the viewport. + await expect + .poll(async () => { + const rect = await highlight.boundingBox(); + const viewer = await page.locator("[data-pdf-viewer-scroller]").boundingBox(); + return ( + rect !== null && + viewer !== null && + rect.y > viewer.y && + rect.y < viewer.y + viewer.height + ); + }) + .toBe(true); + await screenshot("document-analysis"); + }); + + it("compares prose and explores a graph using keyboard, compound disclosure, and neighborhoods", async () => { + const page = getPage(); + await page.goto(`${appUrl}/document-analysis`, { waitUntil: "networkidle" }); + const diff = page.locator(".fk-prose-diff"); + await page.getByRole("button", { name: "Side-by-side comparison" }).click(); + await expect.poll(() => diff.getAttribute("data-mode")).toBe("split"); + expect(await diff.locator("ins").allTextContents()).toContain("six"); + expect(await diff.locator("del").allTextContents()).toContain("may"); + await page.getByRole("button", { name: "Fit graph", exact: true }).click(); + const article = page.locator('[data-graph-node="article"]'); + const policy = page.locator('[data-graph-node="policy"]'); + await article.focus(); + await article.press("ArrowDown"); + await expect.poll(() => policy.getAttribute("aria-pressed")).toBe("true"); + await expect + .poll(() => policy.evaluate((element) => element === document.activeElement)) + .toBe(true); + await policy.press("Enter"); + await expect + .poll(() => page.locator("[data-analysis-announcement]").textContent()) + .toBe("Activated: policy"); + await page.getByRole("button", { name: "Collapse Agreement", exact: true }).click(); + await expect.poll(() => article.count()).toBe(0); + await page.getByRole("button", { name: "Expand Agreement", exact: true }).click(); + await expect.poll(() => article.count()).toBe(1); + await page.getByRole("button", { name: "Focus Article 12 neighborhood" }).click(); + await expect.poll(() => policy.count()).toBe(0); + await page.getByRole("button", { name: "Expand neighborhood", exact: true }).click(); + await expect.poll(() => policy.count()).toBe(1); + await page.getByText("Graph as a list", { exact: true }).click(); + expect(await page.locator("[data-graph-list-node]").count()).toBe(3); + await page.getByRole("button", { name: "Show all", exact: true }).click(); + await expect.poll(() => page.locator("[data-graph-list-node]").count()).toBe(4); + }); + }); +}; diff --git a/apps/demo/e2e/pdf-viewer.scenarios.ts b/apps/demo/e2e/pdf-viewer.scenarios.ts index 9b5a0d6..83b3877 100644 --- a/apps/demo/e2e/pdf-viewer.scenarios.ts +++ b/apps/demo/e2e/pdf-viewer.scenarios.ts @@ -16,13 +16,7 @@ export const pdfViewerScenarios = ( const pages = page.locator("[data-pdf-viewer-page]"); const controls = page.getByRole("toolbar", { name: "PDF controls" }); await expect.poll(() => pages.count()).toBe(3); - await expect - .poll(() => - pages.evaluateAll((elements) => - elements.map((element) => element.getAttribute("data-state")), - ), - ) - .toEqual(["rendered", "rendered", "rendered"]); + await expect.poll(() => pages.nth(0).getAttribute("data-state")).toBe("rendered"); await expect .poll(() => page.getByText("Synthetic preview", { exact: true }).isVisible()) .toBe(true); @@ -49,6 +43,26 @@ export const pdfViewerScenarios = ( .poll(() => controls.getByText("Page 3 of 3", { exact: true }).isVisible()) .toBe(true); + await expect.poll(() => rotatedPage.getAttribute("data-state")).toBe("rendered"); + const title = rotatedPage + .locator("[data-pdf-text-layer] span") + .filter({ hasText: "Supplement B. Reverification" }); + await expect.poll(() => title.count()).toBe(1); + // Measure both boxes in one frame while page navigation may still be animating. + const textGeometry = await title.evaluate((element) => { + const text = element.getBoundingClientRect(); + const page = element.closest("[data-pdf-viewer-page]")!.getBoundingClientRect(); + return { + x: (text.x - page.x) / page.width, + y: (text.y - page.y) / page.height, + width: text.width, + height: text.height, + }; + }); + expect(textGeometry.x).toBeCloseTo(72 / 792, 2); + expect(textGeometry.y).toBeGreaterThan(50 / 612); + expect(textGeometry.y).toBeLessThan(90 / 612); + expect(textGeometry.width).toBeGreaterThan(textGeometry.height * 5); await controls.getByRole("button", { name: "Previous page" }).click(); await expect .poll(() => controls.getByText("Page 2 of 3", { exact: true }).isVisible()) diff --git a/apps/demo/e2e/workflow.e2e.test.ts b/apps/demo/e2e/workflow.e2e.test.ts index f12ba13..997e00a 100644 --- a/apps/demo/e2e/workflow.e2e.test.ts +++ b/apps/demo/e2e/workflow.e2e.test.ts @@ -1,3 +1,4 @@ +import { documentAnalysisScenarios } from "./document-analysis.scenarios"; import { themeBuilderScenarios } from "./theme-builder.scenarios"; import { docsScenarios } from "./docs.scenarios"; import { desktopScenarios } from "./desktop.scenarios"; @@ -150,6 +151,7 @@ describe.sequential("structured workflow builder", () => { "Form builder", "Workflow builder", "Diff review", + "Document analysis", "PDF viewer", "PDF annotator", "Statechart editor", @@ -158,6 +160,7 @@ describe.sequential("structured workflow builder", () => { "Text intelligence", "PDF rendering", "UI system", + "Theme builder", ]); await expect.poll(() => sidebar.getByText("Foldworks", { exact: true }).count()).toBe(1); await expect.poll(() => sidebar.getByText("Design system", { exact: true }).count()).toBe(0); @@ -1648,6 +1651,7 @@ describe.sequential("structured workflow builder", () => { agentScenarios(() => page, appUrl, screenshot); diffViewerScenarios(() => page, appUrl, screenshot); pdfViewerScenarios(() => page, appUrl, screenshot); + documentAnalysisScenarios(() => page, appUrl, screenshot); statechartScenarios(() => page, appUrl, screenshot); outlinerScenarios(() => page, appUrl, screenshot); orchestratorScenarios(() => page, appUrl, screenshot); diff --git a/apps/demo/src/app/message.ts b/apps/demo/src/app/message.ts index 2b60166..8d92007 100644 --- a/apps/demo/src/app/message.ts +++ b/apps/demo/src/app/message.ts @@ -1,3 +1,4 @@ +import { Message as DocumentAnalysisMessage } from "../document-analysis/message"; import { Message as OrchestratorMessage } from "../orchestrator/message"; import { Schema as S } from "effect"; import { ActionIntent } from "@foldworks/generative-ui/core"; @@ -49,6 +50,7 @@ export const Message = defineMessageUnion({ GotQueryBuilderDemoMessage: { message: QueryBuilderDemoMessage }, GotOutlinerDemoMessage: { message: OutlinerDemoMessage }, GotPdfAnnotatorMessage: { message: PdfAnnotator.Message }, + GotDocumentAnalysisMessage: { message: DocumentAnalysisMessage }, GotPdfViewerDemoMessage: { message: PdfViewerDemoMessage }, GotEditorMessage: { message: EditorMessage }, GotSidebarMessage: { message: Sidebar.Message }, diff --git a/apps/demo/src/app/model.ts b/apps/demo/src/app/model.ts index 460e954..8bd48f4 100644 --- a/apps/demo/src/app/model.ts +++ b/apps/demo/src/app/model.ts @@ -1,3 +1,7 @@ +import { + Model as DocumentAnalysisModel, + init as initDocumentAnalysis, +} from "../document-analysis/model"; import { Model as OrchestratorModel, initialModel as initialOrchestrator, @@ -62,6 +66,7 @@ export const Model = S.Struct({ outlinerDemo: OutlinerDemoModel, pdfAnnotator: PdfAnnotator.Model, pdfViewerDemo: PdfViewerDemoModel, + documentAnalysis: DocumentAnalysisModel, editor: ArticleEditor.Model, sidebar: Sidebar.Model, uiKit: UiKitModel, @@ -106,6 +111,7 @@ export const init = ( sampleUrl: "/foldworks-sample.pdf", }), pdfViewerDemo: initialPdfViewer, + documentAnalysis: initDocumentAnalysis(), editor: initialEditor(), sidebar: Sidebar.init({ id: "foldworks-sidebar", defaultCollapsed: route._tag === "Theme" }), uiKit: initialUiKit, diff --git a/apps/demo/src/app/route.ts b/apps/demo/src/app/route.ts index a44070c..48bc0f9 100644 --- a/apps/demo/src/app/route.ts +++ b/apps/demo/src/app/route.ts @@ -24,6 +24,7 @@ export type Demo = | "Outliner" | "PdfAnnotator" | "PdfViewer" + | "DocumentAnalysis" | "UiKit" | "Theme"; @@ -52,6 +53,7 @@ export const AppRoute = defineRouteUnion({ Outliner: {}, PdfAnnotator: {}, PdfViewer: {}, + DocumentAnalysis: {}, UiKit: {}, Theme: {}, NotFound: { path: S.String }, @@ -140,6 +142,11 @@ export const pdfAnnotatorRouter = pipe( Route.mapTo(AppRoute.PdfAnnotator), ); +export const documentAnalysisRouter = pipe( + Route.literal("document-analysis"), + Route.mapTo(AppRoute.DocumentAnalysis), +); + export const pdfViewerRouter = pipe(Route.literal("pdf-viewer"), Route.mapTo(AppRoute.PdfViewer)); export const editorRouter = pipe(Route.literal("editor"), Route.mapTo(AppRoute.Editor)); @@ -162,6 +169,7 @@ const routeParser = Route.oneOf( outlinerRouter, pdfAnnotatorRouter, pdfViewerRouter, + documentAnalysisRouter, uiKitRouter, themeRouter, homeRouter, @@ -201,6 +209,8 @@ export const demoFromRoute = (route: AppRoute): Demo => { return "Outliner"; case "PdfAnnotator": return "PdfAnnotator"; + case "DocumentAnalysis": + return "DocumentAnalysis"; case "PdfViewer": return "PdfViewer"; case "Theme": diff --git a/apps/demo/src/app/subscriptions.ts b/apps/demo/src/app/subscriptions.ts index 6860f6c..b4635db 100644 --- a/apps/demo/src/app/subscriptions.ts +++ b/apps/demo/src/app/subscriptions.ts @@ -1,3 +1,4 @@ +import { subscriptions as documentAnalysisSubscriptions } from "../document-analysis/subscriptions"; import { subscriptions as orchestratorSubscriptions } from "../orchestrator/subscriptions"; import { Effect, Schema as S, Stream } from "effect"; import { PdfAnnotator } from "@foldworks/pdf-annotator"; @@ -129,6 +130,12 @@ const pdfAnnotator = Subscription.lift(PdfAnnotator.subscriptions) Message.GotPdfAnnotatorMessage({ message }), }); +const documentAnalysis = Subscription.lift(documentAnalysisSubscriptions)({ + when: (model) => model.route._tag === "DocumentAnalysis", + toChildModel: (model) => model.documentAnalysis, + toParentMessage: (message) => Message.GotDocumentAnalysisMessage({ message }), +}); + export const subscriptions = Subscription.aggregate()( workflow, statechart, @@ -139,6 +146,7 @@ export const subscriptions = Subscription.aggregate()( workbench, queryBuilder, pdfAnnotator, + documentAnalysis, themeSubscriptions, historySubscriptions, ); diff --git a/apps/demo/src/app/update.ts b/apps/demo/src/app/update.ts index b82c9d5..e96d4bc 100644 --- a/apps/demo/src/app/update.ts +++ b/apps/demo/src/app/update.ts @@ -1,3 +1,5 @@ +import { update as updateDocumentAnalysis } from "../document-analysis/update"; +import { Message as DocumentAnalysisMessage } from "../document-analysis/message"; import { update as updateOrchestrator } from "../orchestrator/update"; import { update as updateDocs } from "../docs/update"; import { Effect, Option, Schema as S } from "effect"; @@ -167,7 +169,9 @@ export const enterRoute = (model: Model): UpdateReturn => ? foldStatechart(model, StatechartMessage.ClickedFit()) : model.route._tag === "PdfViewer" ? enterPdfViewer(model) - : { model }; + : model.route._tag === "DocumentAnalysis" + ? foldDocumentAnalysis(model, DocumentAnalysisMessage.Initialize()) + : { model }; const foldCodeEditor = Update.foldChild({ update: updateCodeEditor, @@ -260,6 +264,13 @@ const foldPdfViewer = Update.foldChild({ toParentMessage: (message) => Message.GotPdfViewerDemoMessage({ message }), }); +const foldDocumentAnalysis = Update.foldChild({ + update: updateDocumentAnalysis, + read: (model: Model) => Option.some(model.documentAnalysis), + write: (model, documentAnalysis) => ({ ...model, documentAnalysis }), + toParentMessage: (message) => Message.GotDocumentAnalysisMessage({ message }), +}); + // Generate the sample PDF the first time the viewer demo opens. const enterPdfViewer = (model: Model): UpdateReturn => model.pdfViewerDemo.viewer.document._tag === "Empty" @@ -411,6 +422,7 @@ export const update = (model: Model, message: Message): UpdateReturn => foldQueryBuilder(model, childMessage), GotOutlinerDemoMessage: ({ message: childMessage }) => foldOutliner(model, childMessage), GotPdfAnnotatorMessage: ({ message: childMessage }) => foldPdfAnnotator(model, childMessage), + GotDocumentAnalysisMessage: ({ message }) => foldDocumentAnalysis(model, message), GotPdfViewerDemoMessage: ({ message: childMessage }) => foldPdfViewer(model, childMessage), GotEditorMessage: ({ message }) => foldEditor(model, message), GotSidebarMessage: ({ message: childMessage }) => foldSidebar(model, childMessage), diff --git a/apps/demo/src/app/view.ts b/apps/demo/src/app/view.ts index a2a1890..045c092 100644 --- a/apps/demo/src/app/view.ts +++ b/apps/demo/src/app/view.ts @@ -1,3 +1,4 @@ +import { view as documentAnalysisView } from "../document-analysis/view"; import { view as docsView, catalog as docsCatalog } from "../docs/view"; import { docsPath } from "../docs/catalog"; import { groupPackages, packageIcon } from "../docs/packages"; @@ -67,6 +68,7 @@ import { outlinerRouter, pdfAnnotatorRouter, pdfViewerRouter, + documentAnalysisRouter, queryBuilderRouter, statechartRouter, uiKitRouter, @@ -322,6 +324,13 @@ const navigationGroups = (model: Model): ReadonlyArray icon: FileDiff, isActive: demo === "DiffViewer", }, + { + id: "document-analysis", + label: "Document analysis", + href: documentAnalysisRouter(), + icon: FileSearch, + isActive: demo === "DocumentAnalysis", + }, { id: "pdf-viewer", label: "PDF viewer", @@ -488,11 +497,13 @@ const toolbar = (model: Model, h: HtmlBuilder): Html => { ? "Outline workspace" : demo === "PdfAnnotator" ? "PDF annotator" - : demo === "PdfViewer" - ? "Onboarding packet review" - : demo === "Home" - ? "Foldworks" - : "@foldworks/ui"; + : demo === "DocumentAnalysis" + ? "Document analysis" + : demo === "PdfViewer" + ? "Onboarding packet review" + : demo === "Home" + ? "Foldworks" + : "@foldworks/ui"; const description = demo === "Docs" ? docsDescription(model) @@ -528,11 +539,13 @@ const toolbar = (model: Model, h: HtmlBuilder): Html => { ? "Notes · threads · context · results" : demo === "PdfAnnotator" ? `${model.pdfAnnotator.annotations.length} annotations · drag, resize, and export` - : demo === "PdfViewer" - ? "Read-only pages · overlay hotspots · crop boxes and rotation" - : demo === "Home" - ? "The UI standard library for Foldkit and StyleX" - : "61 components · Foldkit behavior · StyleX"; + : demo === "DocumentAnalysis" + ? "Source evidence · highlighted text · prose comparison · graph exploration" + : demo === "PdfViewer" + ? "Read-only pages · overlay hotspots · crop boxes and rotation" + : demo === "Home" + ? "The UI standard library for Foldkit and StyleX" + : "61 components · Foldkit behavior · StyleX"; return Toolbar.view( { title, @@ -623,15 +636,22 @@ const toolbar = (model: Model, h: HtmlBuilder): Html => { h, ), ] - : demo === "Home" || demo === "Docs" - ? [] - : [ + : demo === "DocumentAnalysis" + ? [ toolbarBadge( - { label: "61 components", tone: "info", dot: true }, + { label: "Synthetic corpus", tone: "info" }, h, ), - toolbarBadge({ label: "StyleX + Foldkit" }, h), - ]), + ] + : demo === "Home" || demo === "Docs" + ? [] + : [ + toolbarBadge( + { label: "61 components", tone: "info", dot: true }, + h, + ), + toolbarBadge({ label: "StyleX + Foldkit" }, h), + ]), iconSelect( Palette, toolbarSelectStyles.theme, @@ -808,6 +828,14 @@ const content = (model: Model, h: HtmlBuilder): Html => { toParentMessage: (message) => Message.GotPdfAnnotatorMessage({ message }), }); } + if (demo === "DocumentAnalysis") { + return h.submodel({ + slotId: "document-analysis-content", + model: model.documentAnalysis, + view: documentAnalysisView, + toParentMessage: (message) => Message.GotDocumentAnalysisMessage({ message }), + }); + } if (demo === "PdfViewer") { return h.submodel({ slotId: "pdf-viewer-content", @@ -827,28 +855,30 @@ const content = (model: Model, h: HtmlBuilder): Html => { const documentTitle = (demo: Demo): string => demo === "Theme" ? "Theme builder · Foldworks" - : Match.value(demo).pipe( - Match.when("Docs", () => "Documentation · Foldworks"), - Match.when("Editor", () => "Document editor · Foldworks"), - Match.when("Orchestrator", () => "Outline workspace · Foldworks"), - Match.when("Agent", () => "Interactive agent · Foldworks"), - Match.when("CodeEditor", () => "Code editor · Foldworks"), - Match.when("Codebase", () => "Codebase workbench · Foldworks"), - Match.when("DiffViewer", () => "Code review · Foldworks"), - Match.when("Workbench", () => "Workers workbench · Foldworks"), - Match.when("Home", () => "Foldworks · The UI standard library for Foldkit and StyleX"), - Match.when("DataTable", () => "Data table · Foldworks"), - Match.when("DataGrid", () => "Data grid · Foldworks"), - Match.when("FormBuilder", () => "Form builder · Foldworks"), - Match.when("QueryBuilder", () => "Query builder · Foldworks"), - Match.when("Outliner", () => "Outliner · Foldworks"), - Match.when("PdfAnnotator", () => "PDF annotator · Foldworks"), - Match.when("PdfViewer", () => "PDF viewer · Foldworks"), - Match.when("UiKit", () => "UI components · Foldworks"), - Match.when("Workflow", () => "Workflow · Foldworks"), - Match.when("Statechart", () => "Statechart · Foldworks"), - Match.exhaustive, - ); + : demo === "DocumentAnalysis" + ? "Document analysis · Foldworks" + : Match.value(demo).pipe( + Match.when("Docs", () => "Documentation · Foldworks"), + Match.when("Editor", () => "Document editor · Foldworks"), + Match.when("Orchestrator", () => "Outline workspace · Foldworks"), + Match.when("Agent", () => "Interactive agent · Foldworks"), + Match.when("CodeEditor", () => "Code editor · Foldworks"), + Match.when("Codebase", () => "Codebase workbench · Foldworks"), + Match.when("DiffViewer", () => "Code review · Foldworks"), + Match.when("Workbench", () => "Workers workbench · Foldworks"), + Match.when("Home", () => "Foldworks · The UI standard library for Foldkit and StyleX"), + Match.when("DataTable", () => "Data table · Foldworks"), + Match.when("DataGrid", () => "Data grid · Foldworks"), + Match.when("FormBuilder", () => "Form builder · Foldworks"), + Match.when("QueryBuilder", () => "Query builder · Foldworks"), + Match.when("Outliner", () => "Outliner · Foldworks"), + Match.when("PdfAnnotator", () => "PDF annotator · Foldworks"), + Match.when("PdfViewer", () => "PDF viewer · Foldworks"), + Match.when("UiKit", () => "UI components · Foldworks"), + Match.when("Workflow", () => "Workflow · Foldworks"), + Match.when("Statechart", () => "Statechart · Foldworks"), + Match.exhaustive, + ); export const view = (model: Model, h: HtmlBuilder): Document => { const demo = demoFromRoute(model.route); diff --git a/apps/demo/src/document-analysis/fixture.ts b/apps/demo/src/document-analysis/fixture.ts new file mode 100644 index 0000000..41440d5 --- /dev/null +++ b/apps/demo/src/document-analysis/fixture.ts @@ -0,0 +1,89 @@ +import { Effect } from "effect"; +import { PDFDocument, StandardFonts, rgb } from "pdf-lib"; +import { bytesToBase64, locateQuote, readPdfText } from "@foldworks/pdf"; +import type { GraphDocument } from "@foldworks/diagram/graph"; + +export const quote = + "Employees with one year of service may bid on schedules during each bid period."; +export const revisedQuote = + "Employees with six months of service must bid on schedules during each bid period."; +export const graphDocument: GraphDocument = { + nodes: [ + { id: "agreement", data: { label: "Agreement", tone: "info" } }, + { id: "article", parentId: "agreement", data: { label: "Article 12" } }, + { id: "policy", parentId: "agreement", data: { label: "Schedule bidding", tone: "success" } }, + { id: "rider", data: { label: "2026 rider", tone: "warning" } }, + ], + edges: [ + { + id: "source", + source: { nodeId: "article" }, + target: { nodeId: "policy" }, + data: { label: "Supports" }, + }, + { + id: "replacement", + source: { nodeId: "rider" }, + target: { nodeId: "policy" }, + data: { label: "Amends", tone: "warning" }, + }, + ], + annotations: [], +}; + +/** A synthetic 12-page agreement. The cited passage crosses pages 1 and 2. */ +export const sample = Effect.promise(async () => { + const pdf = await PDFDocument.create(); + const font = await pdf.embedFont(StandardFonts.Helvetica); + for (let index = 0; index < 12; index++) { + const page = pdf.addPage([612, 792]); + page.drawText(`Synthetic agreement - page ${index + 1}`, { x: 50, y: 738, size: 18, font }); + page.drawText("For demonstration only", { + x: 50, + y: 710, + size: 11, + font, + color: rgb(0.4, 0.4, 0.4), + }); + if (index === 0) { + page.drawText("Article 12. Schedule bidding", { x: 50, y: 665, size: 14, font }); + page.drawText("Employees with one year of service may bid on", { + x: 50, + y: 60, + size: 12, + font, + }); + } else if (index === 1) { + page.drawText("schedules during each bid period.", { x: 50, y: 665, size: 12, font }); + } else { + page.drawText(`Article ${index + 12}. Additional provisions`, { + x: 50, + y: 665, + size: 14, + font, + }); + page.drawText("This section is intentionally brief to demonstrate page caching.", { + x: 50, + y: 630, + size: 12, + font, + }); + } + } + return pdf.save(); +}).pipe( + Effect.flatMap((bytes) => + readPdfText(bytes).pipe( + Effect.map((pages) => { + // Restrict matching to body text. Headers are unrelated to the cross-page quote. + const body = pages.map((page) => ({ + ...page, + items: page.items.filter((item) => item.rect.y < 700), + })); + const location = locateQuote(body, quote); + if (location._tag !== "Found") throw new Error(`Sample quote location: ${location._tag}`); + return { bytesBase64: bytesToBase64(bytes), regions: location.regions }; + }), + ), + ), +); diff --git a/apps/demo/src/document-analysis/message.ts b/apps/demo/src/document-analysis/message.ts new file mode 100644 index 0000000..429114a --- /dev/null +++ b/apps/demo/src/document-analysis/message.ts @@ -0,0 +1,20 @@ +import { Schema as S } from "effect"; +import { defineMessageUnion } from "foldkit/message"; +import { PdfViewer } from "@foldworks/pdf-viewer"; +import { PdfTextRegion, PdfTextSelection } from "@foldworks/pdf"; +import * as Graph from "@foldworks/diagram/graph"; +import { Citation } from "@foldworks/ui"; +export const Message = defineMessageUnion({ + Initialize: {}, + GotSample: { bytesBase64: S.String, regions: S.Array(PdfTextRegion) }, + FailedSample: { reason: S.String }, + GotViewerMessage: { message: PdfViewer.Message }, + GotGraphMessage: { message: Graph.Message }, + GotCitationMessage: { message: Citation.Message }, + OpenedQuote: {}, + SelectedRange: { key: S.String }, + SelectedPdfText: { selection: PdfTextSelection }, + ChangedDiffMode: { mode: S.Literals(["inline", "split"]) }, + ShowNeighborhood: {}, +}); +export type Message = typeof Message.Type; diff --git a/apps/demo/src/document-analysis/model.ts b/apps/demo/src/document-analysis/model.ts new file mode 100644 index 0000000..28c4ce0 --- /dev/null +++ b/apps/demo/src/document-analysis/model.ts @@ -0,0 +1,29 @@ +import { Schema as S } from "effect"; +import { PdfViewer } from "@foldworks/pdf-viewer"; +import { PdfTextRegion } from "@foldworks/pdf"; +import * as Graph from "@foldworks/diagram/graph"; +import { Citation } from "@foldworks/ui"; + +export const Model = S.Struct({ + viewer: PdfViewer.Model, + graph: Graph.Model, + citation: Citation.Model, + regions: S.Array(PdfTextRegion), + selectedKey: S.String, + selectedText: S.String, + diffMode: S.Literals(["inline", "split"]), + announcement: S.String, + error: S.String, +}); +export type Model = typeof Model.Type; +export const init = (): Model => ({ + viewer: PdfViewer.init({ id: "analysis-pdf", maxCachedPages: 3 }), + graph: Graph.init({ id: "analysis-graph" }), + citation: Citation.init({ id: "analysis-citation", showDelay: 100 }), + regions: [], + selectedKey: "", + selectedText: "", + diffMode: "inline", + announcement: "", + error: "", +}); diff --git a/apps/demo/src/document-analysis/styles.ts b/apps/demo/src/document-analysis/styles.ts new file mode 100644 index 0000000..b51c91c --- /dev/null +++ b/apps/demo/src/document-analysis/styles.ts @@ -0,0 +1,24 @@ +import * as stylex from "@stylexjs/stylex"; +import { colors, space, radii } from "@foldworks/ui/tokens.stylex"; +export const styles = stylex.create({ + root: { + display: "grid", + gridTemplateColumns: "minmax(0, 1fr) minmax(0, 1fr)", + gap: space.lg, + height: "100%", + minHeight: 0, + padding: space.lg, + "@media (max-width: 900px)": { gridTemplateColumns: "minmax(0, 1fr)", overflowY: "auto" }, + }, + pdf: { + minHeight: 0, + overflow: "hidden", + border: `1px solid ${colors.border}`, + borderRadius: radii.md, + "@media (max-width: 900px)": { height: "500px" }, + }, + panel: { minWidth: 0, minHeight: 0, overflowY: "auto", padding: space.sm }, + section: { marginBottom: space.xl }, + controls: { display: "flex", flexWrap: "wrap", gap: space.sm, marginBlock: space.md }, + hint: { color: colors.foregroundMuted, fontSize: "12px" }, +}); diff --git a/apps/demo/src/document-analysis/subscriptions.ts b/apps/demo/src/document-analysis/subscriptions.ts new file mode 100644 index 0000000..27c6e9a --- /dev/null +++ b/apps/demo/src/document-analysis/subscriptions.ts @@ -0,0 +1,8 @@ +import { Subscription } from "foldkit"; +import * as Graph from "@foldworks/diagram/graph"; +import type { Model } from "./model"; +import { Message } from "./message"; +export const subscriptions = Subscription.lift(Graph.subscriptions)({ + toChildModel: (model) => model.graph, + toParentMessage: (message) => Message.GotGraphMessage({ message }), +}); diff --git a/apps/demo/src/document-analysis/update.ts b/apps/demo/src/document-analysis/update.ts new file mode 100644 index 0000000..adb6771 --- /dev/null +++ b/apps/demo/src/document-analysis/update.ts @@ -0,0 +1,84 @@ +import { Effect, Option } from "effect"; +import { Command, Update } from "foldkit"; +import { PdfViewer } from "@foldworks/pdf-viewer"; +import * as Graph from "@foldworks/diagram/graph"; +import { Citation } from "@foldworks/ui"; +import { graphDocument, sample } from "./fixture"; +import { Message } from "./message"; +import type { Model } from "./model"; + +type UpdateReturn = Update.Return; +const GenerateSample = Command.define("GenerateDocumentAnalysisSample", { + messages: [Message.GotSample, Message.FailedSample], + execute: sample.pipe( + Effect.map(Message.GotSample), + Effect.catchCause((cause) => Effect.succeed(Message.FailedSample({ reason: String(cause) }))), + ), +}); +const viewer = Update.foldChild({ + update: PdfViewer.update, + read: (model: Model) => Option.some(model.viewer), + write: (model, viewer) => ({ ...model, viewer }), + toParentMessage: (message) => Message.GotViewerMessage({ message }), +}); +const graph = Update.foldChild({ + update: (model: Graph.Model, message: Graph.Message) => + Graph.update(model, message, graphDocument), + read: (model: Model) => Option.some(model.graph), + write: (model, graph) => ({ ...model, graph }), + toParentMessage: (message) => Message.GotGraphMessage({ message }), + foldOutMessage: + (outMessage) => + (model): UpdateReturn => ({ + model: { + ...model, + announcement: + outMessage._tag === "ClearedSelection" + ? "Cleared selection" + : `${outMessage._tag}: ${outMessage.id}`, + }, + }), +}); +const citation = Update.foldChild({ + update: Citation.update, + read: (model: Model) => Option.some(model.citation), + write: (model, citation) => ({ ...model, citation }), + toParentMessage: (message) => Message.GotCitationMessage({ message }), + foldOutMessage: + () => + (model): UpdateReturn => ({ model }), +}); +export const update = (model: Model, message: Message): UpdateReturn => + Message.match(message, { + Initialize: () => + model.viewer.document._tag === "Empty" ? { model, commands: [GenerateSample()] } : { model }, + GotSample: ({ bytesBase64, regions }) => { + const opened = viewer( + { ...model, regions }, + PdfViewer.Message.RequestedLoadBytes({ name: "synthetic-agreement.pdf", bytesBase64 }), + ); + const fitted = graph(opened.model, Graph.Message.RequestedFit()); + return { + model: fitted.model, + commands: [...(opened.commands ?? []), ...(fitted.commands ?? [])], + }; + }, + FailedSample: ({ reason }) => ({ model: { ...model, error: reason } }), + GotViewerMessage: ({ message }) => viewer(model, message), + GotGraphMessage: ({ message }) => graph(model, message), + GotCitationMessage: ({ message }) => citation(model, message), + OpenedQuote: () => viewer({ ...model, selectedKey: "quote" }, PdfViewer.scrollTo("quote")), + SelectedRange: ({ key }) => ({ + model: { ...model, selectedKey: key, announcement: `Selected ${key}` }, + }), + SelectedPdfText: ({ selection }) => ({ + model: { + ...model, + selectedText: selection.text, + announcement: `Selected text on ${selection.regions.length} page(s).`, + }, + }), + ChangedDiffMode: ({ mode }) => ({ model: { ...model, diffMode: mode } }), + ShowNeighborhood: () => + graph(model, Graph.Message.ChangedNeighborhood({ id: Option.some("article"), hops: 0 })), + }); diff --git a/apps/demo/src/document-analysis/view.ts b/apps/demo/src/document-analysis/view.ts new file mode 100644 index 0000000..46d9f53 --- /dev/null +++ b/apps/demo/src/document-analysis/view.ts @@ -0,0 +1,183 @@ +import { defineView } from "foldkit/submodel"; +import { PdfViewer } from "@foldworks/pdf-viewer"; +import * as Graph from "@foldworks/diagram/graph"; +import { HighlightedText } from "@foldworks/text-intelligence"; +import { ProseDiff } from "@foldworks/diff-viewer"; +import { Alert, Button, Citation } from "@foldworks/ui"; +import { className } from "../workflow/styles"; +import { graphDocument, quote, revisedQuote } from "./fixture"; +import type { Model } from "./model"; +import { Message } from "./message"; +import { styles } from "./styles"; + +export const view = defineView((model, h) => + h.div( + [h.Class(className(styles.root))], + [ + h.div( + [h.Class(className(styles.pdf))], + [ + PdfViewer.view( + { + model: model.viewer, + toParentMessage: (message) => Message.GotViewerMessage({ message }), + label: "Synthetic agreement", + selectableText: true, + onSelectText: (selection) => Message.SelectedPdfText({ selection }), + highlights: [ + { + key: "quote", + regions: model.regions, + label: "Schedule bidding evidence", + tone: "warning", + isSelected: model.selectedKey === "quote", + onClick: Message.SelectedRange({ key: "quote" }), + }, + ], + banner: Alert.view( + { + title: "Synthetic document", + description: + "Select text to inspect its provenance. Pages render near the viewport.", + }, + h, + ), + }, + h, + ), + ], + ), + h.div( + [h.Class(className(styles.panel))], + [ + ...(model.error === "" + ? [] + : [Alert.view({ title: "Sample error", description: model.error, tone: "danger" }, h)]), + h.section( + [h.Class(className(styles.section))], + [ + h.h2([], ["Evidence in context"]), + h.p( + [], + [ + "Schedule bidding, Article 12, pages 1–2 ", + h.submodel({ + slotId: "analysis-citation", + model: model.citation, + view: Citation.view, + viewInputs: Citation.styledViewInputs( + { label: 1, preview: quote, onActivate: Message.OpenedQuote() }, + h, + ), + toParentMessage: (message) => Message.GotCitationMessage({ message }), + }), + ], + ), + HighlightedText.view( + { + text: quote, + selectedKey: model.selectedKey, + ranges: [ + { + key: "quote", + from: 0, + to: quote.length, + label: "Schedule bidding evidence", + tooltip: "PDF pages 1–2", + tone: "warning", + onClick: Message.OpenedQuote(), + }, + { + key: "eligibility", + from: 15, + to: 27, + label: "One year of service", + tooltip: "Eligibility requirement", + tone: "info", + onClick: Message.SelectedRange({ key: "eligibility" }), + }, + ], + }, + h, + ), + h.p( + [h.Class(className(styles.hint))], + [ + `Retained page images: ${model.viewer.cachedPageIndexes.length} / ${model.viewer.maxCachedPages}. This sample has 12 pages.`, + ], + ), + Button.view( + { + label: "Jump to last page", + onClick: Message.GotViewerMessage({ message: PdfViewer.goToPage(11) }), + variant: "outline", + size: "sm", + }, + h, + ), + h.p( + [h.DataAttribute("pdf-selected-text", "true"), h.AriaLive("polite")], + [ + model.selectedText === "" + ? "Select text in the PDF to see it here." + : `Selected: ${model.selectedText}`, + ], + ), + ], + ), + h.section( + [h.Class(className(styles.section))], + [ + h.h2([], ["Compare clause wording"]), + h.div( + [h.Class(className(styles.controls))], + [ + ...(["inline", "split"] as const).map((mode) => + Button.view( + { + label: mode === "inline" ? "Inline comparison" : "Side-by-side comparison", + onClick: Message.ChangedDiffMode({ mode }), + variant: model.diffMode === mode ? "secondary" : "outline", + size: "sm", + }, + h, + ), + ), + ], + ), + ProseDiff.view({ oldText: quote, newText: revisedQuote, mode: model.diffMode }, h), + ], + ), + h.section( + [h.Class(className(styles.section))], + [ + h.h2([], ["Explore relationships"]), + Button.view( + { + label: "Focus Article 12 neighborhood", + onClick: Message.ShowNeighborhood(), + variant: "outline", + size: "sm", + }, + h, + ), + Graph.view( + { + model: model.graph, + document: graphDocument, + height: 410, + toParentMessage: (message) => Message.GotGraphMessage({ message }), + }, + h, + ), + ], + ), + h.p( + [h.AriaLive("polite"), h.DataAttribute("analysis-announcement", "true")], + [model.announcement], + ), + ], + ), + ], + ), +); diff --git a/apps/demo/src/pdf-viewer/view.ts b/apps/demo/src/pdf-viewer/view.ts index 6c29656..cad884f 100644 --- a/apps/demo/src/pdf-viewer/view.ts +++ b/apps/demo/src/pdf-viewer/view.ts @@ -173,6 +173,7 @@ export const view = defineView((model, h) => model: model.viewer, toParentMessage: (message) => Message.GotViewerMessage({ message }), label: "Synthetic onboarding packet", + selectableText: true, overlays: overlays(model), watermark: "Synthetic", banner: diff --git a/apps/demo/src/styles.css b/apps/demo/src/styles.css index 9518a90..7eec5a0 100644 --- a/apps/demo/src/styles.css +++ b/apps/demo/src/styles.css @@ -1,3 +1,4 @@ +@import "@foldworks/diagram/styles.css"; @import "@foldworks/editor/styles.css"; @import "@foldworks/ui/base.css"; @import "@foldworks/ui/themes/shadcn.css"; diff --git a/docs/document-analysis.md b/docs/document-analysis.md new file mode 100644 index 0000000..d276d4c --- /dev/null +++ b/docs/document-analysis.md @@ -0,0 +1,184 @@ +# Document analysis primitives + +A document analysis workspace reads a corpus of long documents, extracts values +from them, and lets a reviewer check every value against the exact words that +support it. It also compares the same clause across documents and maps how +documents and their parts relate. A labor-relations team working with hundreds +of collective bargaining agreements is the motivating workload. Their +agreements run to about 60 pages, and each one carries up to a few hundred +attributes. + +This note records which pieces of that workspace belong in Foldworks and which +stay with the application. The five generic primitives below are implemented, +along with per-cell grid tones. The [document-analysis demo](https://foldworks.dev/document-analysis) +composes them using a synthetic 12-page agreement. Application record UI and the +items under “Later” remain design work. + +## Boundary + +The `@foldworks/ui` rule decides the split: product concepts and feature +behavior stay in their application or feature package. Foldworks gets generic +text, PDF, graph, and diff building blocks. Claims, attributes, confidence, and +review stay with the application. + +| Layer | Owns | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Foldworks | PDF highlights, text layers, and page rendering; text with highlighted ranges; graph views; text diffs; citation markers | +| Application record UI | A value card (value, quote, status, confidence), a source pane that keeps the PDF and text in sync, a review queue and its actions, and an attribute catalog | +| Application product | Domain wording, branding, specific maps, extraction, inference, clustering, ontology storage, queries, policy languages and evaluation | + +The first consumer is a document corpus explorer in the WorldVM family. It +should keep its record UI in a package beside its data layer, not in Foldworks +and not in its demo. The [WorldVM developer tools](./worldvm-devtools.md#boundary) +rules apply: no Foldworks package names WorldVM or its products, and each +primitive below needs a second use outside that explorer. + +## Available primitives + +The order follows value to the first consumer. Items 1 and 2 make extracted +values checkable against their source, which is the workspace's core promise. + +### 1. PDF highlights, text layer, and on-demand rendering + +Packages: `@foldworks/pdf` and `@foldworks/pdf-viewer`. + +`PdfOverlay` keeps its single-rectangle API. `PdfHighlight` adds +`regions: [{ pageIndex, rects }]`, translucent tones, selection, and activation. +The first rectangle owns one accessible control; all rectangles dispatch the +same message. PDF geometry and placeholders exist before rasterization, so +`scrollTo` works on unrendered pages. + +`readPdfText` and the pure `pdfTextItems` conversion expose text item bounds in +PDF user space. `locateQuote` normalizes whitespace, compatibility characters +(including ligatures), and explicit line-end hyphenation. It reports missing, +empty, and ambiguous quotes rather than guessing. Bounds cover **whole PDF.js +items**, not individual glyphs; partial-item quotes can highlight extra words. +The host may filter headers or provide OCR positions before matching. + +`selectableText` mounts PDF.js' native text layer. `onSelectText` reports text +and user-space rectangles for selections across rendered pages. The mounted +viewer renders near the viewport from one shared PDF.js document, releases it +on unmount or document replacement, and retains at most `maxCachedPages` +images and associated text layers (default 8). It still mounts lightweight +wrappers for every page. See the [PDF viewer guide](../packages/pdf-viewer/README.md) +for cache behavior and selection limits. + +Second uses: form review, contract and invoice checking, and any grounded answer +that cites a PDF. + +### 2. Text with highlighted ranges + +Package: `@foldworks/text-intelligence`, which already owns `TextRange`, +`segments`, the hover popup, and the shared token stylesheet. + +`HighlightedText.view` displays read-only text with character ranges marked: clauses, +transcripts, and Markdown converted from PDFs. Each range has a key, a tone, an +optional label, an optional activation message, and optional tooltip text. + +- Ranges may overlap. Painting splits the text with `segments`; a segment covered + by several ranges shows that depth and activates the innermost range. +- Each activatable range is one keyboard stop in reading order, including fully + covered ranges. Enter and Space activate it; `selectedKey` controls painting. +- A pure `snippets` helper cuts keyword-in-context windows around ranges, with + ellipses and merged neighbors. Search-term highlighting uses the same view. + +The application's source pane composes this view with item 1: activating a +range in the text highlights the same quote in the PDF, and the reverse. + +### 3. Graph view + +Package: `@foldworks/diagram`. It already has a normalized compound graph +document, layered layout, edge routing with label positions, and a `Diagram` +interaction submodel for selection, panning, and zoom. A ready-made, read-only view for exploration is available as +`@foldworks/diagram/graph`, also exported as `Graph` from the root. + +- Themed node labels and edges using the shared UI CSS tokens, with edge labels + and tones, replacing hand-picked coordinates and colors. +- Keyboard movement between nodes along edges and between siblings. Today's + arrow keys nudge the selected element, which suits editing, not reading. +- Expand and collapse for compound nodes and for neighborhoods, so a large + ontology can be explored from one node outward. +- Selection and activation messages, fit-to-content, and focus on a node. +- An accessible list or tree fallback that presents the same nodes and edges. + +Uses in the first consumer: the component map, the automation graph, the +ontology map, rider-to-master edges, and the lineage view. Outside it: the +[developer tools](./worldvm-devtools.md#surfaces) ontology browser and program +view, and dependency graphs. + +### 4. Text diff + +Package: `@foldworks/diff-viewer`, alongside its line-based patch renderer. + +- `diffText` preserves exact whitespace and punctuation, with word or sentence + granularity and a bounded search fallback for large replacements. +- `ProseDiff.view` supports inline and side-by-side views with semantic + addition/removal markup and theme tones. It does not infer moved sentences. + +Uses: comparing a clause with a reference clause, rider replacements of master +sections, and language variants. A platform workbench already hand-builds form +and change-review diffs, so a second consumer exists. + +### 5. Citation marker + +Package: `@foldworks/ui`. + +`Citation.styledViewInputs` creates a superscript number or chip with a +hover/focus preview and an activation message. The preview is application content, +often a snippet built with item 2. It reuses the `Stateful.Tooltip` engine; `Citation.popoverViewInputs` uses +`Stateful.Popover` for interactive previews with a separate source button. Any grounded answer +or AI interface needs it. + +### Smaller additions + +- **Timeline.** Date ranges and point events on a horizontal axis, for + effective windows and expiration calendars. `Calendar` picks dates and does + not display ranges. This belongs with + [time and scheduling](./planned-primitives.md#time-and-scheduling). +- **Per-cell tone in `DataGrid` is available.** A column + `tone(context)` callback provides neutral, info, success, warning, or danger + styling while preserving selection and editing errors. The host supplies + status labels such as conflict, unresolved, and verified. +- **Filter bar with facet counts** that can be serialized into the URL. This + belongs with [collection views](./planned-primitives.md#collection-views-and-boards). + +## Already available + +The first consumer should use these now rather than hand-build them: + +| Need | Foldworks API | +| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | +| Documents × attributes matrix | `@foldworks/data-grid`: sticky headers, pinned columns, row virtualization, row groups, TSV clipboard | +| Article and section navigation | `Tree` in `@foldworks/ui` | +| Source in one pane, values in another | `Workspace` for split panes and `PanelStack` for an inspector to drill into | +| Search across everything | `Stateful.Command` | +| Citation preview engines | `Stateful.Popover` and `Stateful.Tooltip` | +| Charts, stat tiles, meters, and app chrome | `@foldworks/ui` and `@foldworks/sidebar`; richer charts are [planned](./planned-primitives.md#charts-and-data-visualization) | + +The data grid virtualizes rows, not columns, so put documents in rows and +attributes in columns. Hand-built graph code should sit behind one module whose +input is shaped like a `DiagramDocument`, so it can move to item 3 without +touching its callers. + +## Later + +These came up while exploring the workload. Each waits for a host that needs it. + +- **Annotated Markdown.** The application may store converted documents as + Markdown with `{#id}` section IDs, `` markers, links to + ontology terms with a host scheme such as `[seniority](term:seniority)`, and + fenced policy blocks. Item 2 can display that text without Foldworks knowing + the conventions. A source-mapped, virtualized Markdown reader with host + renderers for link schemes and fenced blocks would follow if the application + needs one. +- **Anchors that survive edits.** Re-anchoring a quote after its text changes is + shared with [anchored review threads](./planned-primitives.md#anchored-review-threads) + and the stable node attachments in the + [outline workspace backlog](./outline-workspace.md#primitive-backlog). Design + them together once documents are edited rather than only read. +- **Term references with an identity.** `Mentions` inserts plain spellings by + design. Completion that inserts a link with a stable ID would extend it. +- **Policy editing.** Fenced policy blocks in an s-expression language need + bracket matching and structural selection in `@foldworks/code-editor`. +- **Clause variants across many documents.** Grouping near-identical wording + and diffing each group against a baseline is record UI built on item 4. diff --git a/docs/planned-primitives.md b/docs/planned-primitives.md index 0bc6001..e55be0e 100644 --- a/docs/planned-primitives.md +++ b/docs/planned-primitives.md @@ -6,7 +6,9 @@ proposals, not settled package APIs or implementation commitments. Promote an area into implementation work after testing it against a real application view. [Outline workspace](./outline-workspace.md) records a working composition and -the primitive gaps discovered while building it. +the primitive gaps discovered while building it. [Document analysis](./document-analysis.md) +records the available PDF, text, graph, diff, and citation primitives for a +document analysis workspace, with the remaining work kept separate. ## Current foundation and adjacent work diff --git a/packages/data-grid/README.md b/packages/data-grid/README.md index 0538acf..067edb2 100644 --- a/packages/data-grid/README.md +++ b/packages/data-grid/README.md @@ -383,3 +383,16 @@ writes against its current data at save time. A submitted batch does not prescribe atomicity or any particular persistence backend. Infinite loading remains future work. + +### Per-cell tones + +A column's `tone(context)` returns `Neutral`, `Info`, `Success`, `Warning`, or +`Danger` for each rendered cell. This overrides the row's background for that +cell while preserving the selection and editing-error treatment. It does not +alter the value, sorting, or clipboard serialization. Render a label or icon as +well when a status matters; color alone should not convey its meaning. + +```ts +{ id: "status", header: "Status", accessor: (row) => row.status, + tone: ({ row }) => row.status === "conflict" ? "Danger" : "Success" } +``` diff --git a/packages/data-grid/src/core.ts b/packages/data-grid/src/core.ts index 61f7692..3627b3f 100644 --- a/packages/data-grid/src/core.ts +++ b/packages/data-grid/src/core.ts @@ -80,6 +80,8 @@ export type ColumnDef = Readonly<{ compare?: (left: Row, right: Row) => number; /** Plain-text hover and focus details for a cell. Multiple lines are kept. * Details hold no model state; one shared popover serves the whole grid. */ + /** Per-cell semantic tone, overriding the row background without changing its value. */ + tone?: (context: CellContext) => RowTone | undefined; details?: (context: CellContext) => string | undefined; clipboardValue?: (context: CellContext) => string; renderCell?: (context: CellContext, h: HtmlBuilder) => Html; diff --git a/packages/data-grid/src/styles.css b/packages/data-grid/src/styles.css index 005815a..100df33 100644 --- a/packages/data-grid/src/styles.css +++ b/packages/data-grid/src/styles.css @@ -495,3 +495,20 @@ clip-path: inset(50%); white-space: nowrap; } + +/* Cell states retain selection and editing error precedence. */ +.fk-data-grid__cell[data-tone="neutral"]:not([data-selected="true"]):not([data-error="true"]) { + background: var(--background); +} +.fk-data-grid__cell[data-tone="info"]:not([data-selected="true"]):not([data-error="true"]) { + background: var(--foldworks-ui-info-surface); +} +.fk-data-grid__cell[data-tone="success"]:not([data-selected="true"]):not([data-error="true"]) { + background: var(--foldworks-ui-success-surface); +} +.fk-data-grid__cell[data-tone="warning"]:not([data-selected="true"]):not([data-error="true"]) { + background: var(--foldworks-ui-warning-surface); +} +.fk-data-grid__cell[data-tone="danger"]:not([data-selected="true"]):not([data-error="true"]) { + background: var(--foldworks-ui-danger-surface); +} diff --git a/packages/data-grid/src/view.ts b/packages/data-grid/src/view.ts index 9a92d72..55c1fe8 100644 --- a/packages/data-grid/src/view.ts +++ b/packages/data-grid/src/view.ts @@ -348,6 +348,7 @@ export const view = ( ), }); }; + const cellTone = cell.column.tone?.(cellContext); const details = editing ? undefined : cell.column.details?.(cellContext); const hasCellDetails = details !== undefined && details !== ""; const content = @@ -389,6 +390,7 @@ export const view = ( h.AriaSelected(isSelected), h.Tabindex(isTabStop ? 0 : -1), h.DataAttribute("cell-column-id", cell.column.id), + ...(cellTone === undefined ? [] : [h.DataAttribute("tone", cellTone.toLowerCase())]), h.DataAttribute("pinned", tableColumn?.pinned?.toLowerCase() ?? "false"), h.DataAttribute("pin-boundary", tableColumn?.isPinBoundary ? "true" : "false"), ...(tableColumn?.pinned === "Start" diff --git a/packages/diagram/README.md b/packages/diagram/README.md index c26684e..7a991f1 100644 --- a/packages/diagram/README.md +++ b/packages/diagram/README.md @@ -104,3 +104,62 @@ Drop points are reported in world coordinates; resolve them with `nodeAt`, Arrow keys nudge the selection, Shift moves ten units, Delete and Backspace request removal, and Enter activates the focused element. + +## Read-only graph exploration + +`@foldworks/diagram/graph` provides a themed graph view on this package's +existing compound layout and canvas interaction. It is also exported as `Graph` +from the root package. Import `@foldworks/diagram/styles.css` once. + +```ts +import * as Graph from "@foldworks/diagram/graph"; + +const graph = Graph.init({ id: "relations", direction: "Down" }); +const document: Graph.GraphDocument = { + nodes: [ + { id: "source", data: { label: "Source", tone: "info" } }, + { id: "result", data: { label: "Result", description: "A derived record" } }, + ], + edges: [ + { + id: "supports", + source: { nodeId: "source" }, + target: { nodeId: "result" }, + data: { label: "Supports", tone: "success" }, + }, + ], + annotations: [], +}; + +// Bind your document when folding the reducer: +const updateGraph = (model: Graph.Model, message: Graph.Message) => + Graph.update(model, message, document); + +Graph.view( + { model: graph, document, toParentMessage: (message) => Message.GotGraphMessage({ message }) }, + h, +); +``` + +Lift `Graph.subscriptions` to track pan gestures. Fold `OutMessage.Selected`, +`ClearedSelection`, `Activated`, and `ChangedExpansion` to handle application behavior. Click or +Space selects a node; Enter or double-click activates it. In the default `Down` +layout, Up/Down follow the first incoming/outgoing edge, and Left/Right walk +siblings in document order. `Right` layout rotates those directions. Nodes are +never nudged or edited. One visual node is a tab stop; the optional, visible +“Graph as a list” disclosure offers each node and its outgoing labeled +relations through ordinary buttons. + +Compound disclosure hides descendants and reroutes external edges to their +collapsed ancestor. `ChangedNeighborhood({ id: Option.some(nodeId), hops: 1 })` +filters to an undirected neighborhood and its ancestors; toolbar controls +expand or contract that neighborhood. `Option.none()` restores the whole graph. +`RequestedFit` fits the visible graph, and `RequestedFocus({ id })` reveals a +node's ancestors, centers it, and moves focus. The host must supply unique IDs +and a valid document (use `validateDocument` for imported data). + +Limits: node content is a label with an optional accessible description; use the +lower layers for custom renderers. Free-form annotations are omitted from this +read-only view. All visible nodes render at once. Neighborhood exploration uses +loaded nodes only; fetching neighbors belongs to the host. Layout and graph +state contain no ontology, inference, or application record semantics. diff --git a/packages/diagram/package.json b/packages/diagram/package.json index ee35a2b..556bc6c 100644 --- a/packages/diagram/package.json +++ b/packages/diagram/package.json @@ -10,7 +10,9 @@ "README.md", "LICENSE" ], - "sideEffects": false, + "sideEffects": [ + "**/*.css" + ], "repository": { "type": "git", "url": "git+https://github.com/bjacobso/foldworks.git", @@ -36,7 +38,12 @@ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" - } + }, + "./graph": { + "types": "./dist/graph.d.ts", + "import": "./dist/graph.js" + }, + "./styles.css": "./dist/styles.css" }, "scripts": { "build": "node ../../scripts/build-package.mjs", diff --git a/packages/diagram/src/graph.test.ts b/packages/diagram/src/graph.test.ts new file mode 100644 index 0000000..7dae077 --- /dev/null +++ b/packages/diagram/src/graph.test.ts @@ -0,0 +1,106 @@ +import { Option } from "effect"; +import { inertHtml, type HtmlBuilder } from "foldkit/html"; +import { Scene } from "foldkit/test"; +import { describe, expect, it, vi } from "vitest"; +import * as Graph from "./graph"; + +const document: Graph.GraphDocument = { + nodes: [ + { id: "group", data: { label: "Group" } }, + { id: "a", parentId: "group", data: { label: "A" } }, + { id: "b", parentId: "group", data: { label: "B" } }, + { id: "c", data: { label: "C" } }, + ], + edges: [ + { id: "ab", source: { nodeId: "a" }, target: { nodeId: "b" }, data: { label: "Requires" } }, + { id: "bc", source: { nodeId: "b" }, target: { nodeId: "c" }, data: { tone: "info" } }, + { id: "ca", source: { nodeId: "c" }, target: { nodeId: "a" }, data: {} }, + ], + annotations: [], +}; +const h = inertHtml as unknown as HtmlBuilder; + +describe("read-only graph", () => { + it("explores cyclic neighborhoods without revisiting nodes", () => { + expect([...Graph.neighborhoodIds(document, "a", 0)]).toEqual(["a"]); + expect([...Graph.neighborhoodIds(document, "a", 1)]).toEqual(["a", "b", "c"]); + expect(Graph.neighborhoodIds(document, "a", 20).size).toBe(3); + expect(Graph.neighborhoodIds(document, "missing", 1).size).toBe(0); + }); + it("collapses nested nodes, reroutes external edges, and retains selection on the parent", () => { + const model = Graph.init({ id: "graph" }); + const selected = Graph.update(model, Graph.Message.SelectedNode({ id: "a" }), document).model; + const collapsed = Graph.update(selected, Graph.Message.ToggledNode({ id: "group" }), document); + const visible = Graph.visibleDocument(collapsed.model, document); + expect(visible.nodes.map((node) => node.id)).toEqual(["group", "c"]); + expect(visible.edges.map((edge) => [edge.source.nodeId, edge.target.nodeId])).toEqual([ + ["group", "c"], + ["c", "group"], + ]); + expect(collapsed.model.canvas.selection).toEqual(["group"]); + expect(collapsed.outMessage).toEqual( + Graph.OutMessage.ChangedExpansion({ id: "group", isExpanded: false }), + ); + }); + it("reports selection clearing from the canvas", () => { + const selected = Graph.update( + Graph.init({ id: "graph" }), + Graph.Message.SelectedNode({ id: "a" }), + document, + ).model; + const cleared = Graph.update( + selected, + Graph.Message.GotCanvasMessage({ message: { _tag: "SelectedElements", ids: [] } }), + document, + ); + expect(cleared.model.canvas.selection).toEqual([]); + expect(cleared.outMessage).toEqual(Graph.OutMessage.ClearedSelection()); + }); + it("preserves ancestors while filtering a neighborhood", () => { + const model = Graph.init({ id: "graph", focusId: "a", hops: 0 }); + expect(Graph.visibleDocument(model, document).nodes.map((node) => node.id)).toEqual([ + "group", + "a", + ]); + }); + it("navigates along edges and siblings instead of moving nodes", () => { + expect(Graph.navigationTarget(document, "a", "ArrowDown")).toBe("b"); + expect(Graph.navigationTarget(document, "b", "ArrowUp")).toBe("a"); + expect(Graph.navigationTarget(document, "a", "ArrowRight")).toBe("b"); + expect(Graph.navigationTarget(document, "a", "ArrowLeft")).toBeUndefined(); + const model = Graph.init({ id: "graph" }); + vi.stubGlobal("document", { querySelector: () => null }); + vi.stubGlobal("HTMLElement", class {}); + Scene.scene( + { + update: (model: Graph.Model, message: Graph.Message) => + Graph.update(model, message, document), + view: (model: Graph.Model) => + Graph.view({ model, document, toParentMessage: (message) => message }, h), + }, + Scene.given(model), + Scene.keydown(Scene.selector('[data-graph-node="a"]'), "ArrowDown"), + Scene.expectOutMessage(Graph.OutMessage.Selected({ id: "b" })), + ); + vi.unstubAllGlobals(); + expect(document.nodes[1]?.position).toBeUndefined(); + }); + it("renders labeled edges and the same relations in an accessible list", () => { + const html = Graph.view( + { model: Graph.init({ id: "graph" }), document, toParentMessage: (message) => message }, + h, + )!; + expect(Scene.findAll(html, "[data-graph-list-node]")).toHaveLength(4); + expect(Scene.textContent(html)).toContain("Requires: B"); + expect(Scene.findAll(html, "text").map((node) => Scene.textContent(node))).toContain( + "Requires", + ); + const focused = Graph.update( + Graph.init({ id: "graph", collapsedIds: ["group"], focusId: "c" }), + Graph.Message.RequestedFocus({ id: "a" }), + document, + ).model; + expect(focused.collapsedIds).toEqual([]); + expect(Option.getOrUndefined(focused.focusId)).toBe("a"); + }); +}); diff --git a/packages/diagram/src/graph.ts b/packages/diagram/src/graph.ts new file mode 100644 index 0000000..c4aa586 --- /dev/null +++ b/packages/diagram/src/graph.ts @@ -0,0 +1,611 @@ +import { Effect, Option, Schema as S } from "effect"; +import { Command, Subscription, type Update } from "foldkit"; +import { defineMessageUnion } from "foldkit/message"; +import type { Html, HtmlBuilder } from "foldkit/html"; + +import { + ancestorIds, + childrenOf, + type DiagramDocument, + type DiagramNode, + type NoteData, +} from "./document"; +import { createLayeredLayout, type LayeredDirection } from "./layout"; +import * as Diagram from "./interaction"; + +export type GraphTone = "neutral" | "info" | "success" | "warning" | "danger" | "muted"; +export type GraphNodeData = Readonly<{ label: string; description?: string; tone?: GraphTone }>; +export type GraphEdgeData = Readonly<{ label?: string; tone?: GraphTone }>; +export type GraphDocument = DiagramDocument; + +export const Model = S.Struct({ + canvas: Diagram.Model, + collapsedIds: S.Array(S.String), + focusId: S.Option(S.String), + hops: S.Number, + direction: S.Literals(["Down", "Right"]), +}); +export type Model = typeof Model.Type; +export const Message = defineMessageUnion({ + GotCanvasMessage: { message: Diagram.Message }, + SelectedNode: { id: S.String }, + ActivatedNode: { id: S.String }, + ToggledNode: { id: S.String }, + ChangedNeighborhood: { id: S.Option(S.String), hops: S.Number }, + RequestedFit: {}, + MeasuredCanvas: { width: S.Number, height: S.Number, focusId: S.Option(S.String) }, + RequestedFocus: { id: S.String }, + CompletedFocus: {}, +}); +export type Message = typeof Message.Type; +export const OutMessage = defineMessageUnion({ + Selected: { id: S.String }, + ClearedSelection: {}, + Activated: { id: S.String }, + ChangedExpansion: { id: S.String, isExpanded: S.Boolean }, +}); +export type OutMessage = typeof OutMessage.Type; + +export const init = ( + config: Readonly<{ + id: string; + collapsedIds?: readonly string[]; + focusId?: string; + hops?: number; + direction?: LayeredDirection; + }>, +): Model => ({ + canvas: Diagram.init({ id: config.id }), + collapsedIds: config.collapsedIds ?? [], + focusId: Option.fromUndefinedOr(config.focusId), + hops: Number.isFinite(config.hops) ? Math.max(0, Math.floor(config.hops!)) : 1, + direction: config.direction ?? "Down", +}); + +/** Undirected N-hop exploration, preserving document order and handling cycles. */ +export const neighborhoodIds = ( + document: GraphDocument, + id: string, + hops: number, +): ReadonlySet => { + const ids = new Set(); + if (!document.nodes.some((node) => node.id === id)) return ids; + ids.add(id); + const neighbors = new Map(); + for (const edge of document.edges) { + neighbors.set(edge.source.nodeId, [ + ...(neighbors.get(edge.source.nodeId) ?? []), + edge.target.nodeId, + ]); + neighbors.set(edge.target.nodeId, [ + ...(neighbors.get(edge.target.nodeId) ?? []), + edge.source.nodeId, + ]); + } + let frontier = [id]; + const limit = Number.isFinite(hops) + ? Math.min(document.nodes.length, Math.max(0, Math.floor(hops))) + : 0; + const known = new Set(document.nodes.map((node) => node.id)); + for (let depth = 0; depth < limit; depth++) { + const next: string[] = []; + for (const node of frontier) + for (const neighbor of neighbors.get(node) ?? []) { + if (ids.has(neighbor) || !known.has(neighbor)) continue; + ids.add(neighbor); + next.push(neighbor); + } + frontier = next; + if (frontier.length === 0) break; + } + return ids; +}; + +/** Hide collapsed descendants and reroute external edges to the collapsed parent. */ +export const visibleDocument = (model: Model, document: GraphDocument): GraphDocument => { + const focus = Option.getOrUndefined(model.focusId); + const neighborhood = + focus === undefined ? undefined : new Set(neighborhoodIds(document, focus, model.hops)); + if (neighborhood !== undefined) + for (const id of [...neighborhood]) + for (const ancestor of ancestorIds(document.nodes, id)) neighborhood.add(ancestor); + const nodes = document.nodes.filter( + (node) => + (neighborhood === undefined || neighborhood.has(node.id)) && + !ancestorIds(document.nodes, node.id).some((id) => model.collapsedIds.includes(id)), + ); + const shown = new Set(nodes.map((node) => node.id)); + const endpoint = (id: string) => + shown.has(id) ? id : ancestorIds(document.nodes, id).find((ancestor) => shown.has(ancestor)); + const edges = document.edges.flatMap((edge) => { + // Neighborhood filtering must not invent edges from nodes outside the neighborhood. + if ( + neighborhood !== undefined && + (!neighborhood.has(edge.source.nodeId) || !neighborhood.has(edge.target.nodeId)) + ) + return []; + const source = endpoint(edge.source.nodeId); + const target = endpoint(edge.target.nodeId); + if ( + source === undefined || + target === undefined || + (source === target && edge.source.nodeId !== edge.target.nodeId) + ) + return []; + return [ + { + ...edge, + source: source === edge.source.nodeId ? edge.source : { nodeId: source }, + target: target === edge.target.nodeId ? edge.target : { nodeId: target }, + }, + ]; + }); + return { nodes, edges, annotations: [] }; +}; + +export const layout = (document: GraphDocument, direction: LayeredDirection = "Down") => + createLayeredLayout({ + nodeSize: (node) => node.size ?? { width: 180, height: 64 }, + direction, + edgeRouting: "Orthogonal", + containerPadding: { top: 76, right: 24, bottom: 24, left: 24 }, + })(document); + +/** Up/down follow incoming/outgoing edges; left/right walk siblings in source order. */ +export const navigationTarget = ( + document: GraphDocument, + id: string, + key: string, + direction: LayeredDirection = "Down", +): string | undefined => { + const node = document.nodes.find((node) => node.id === id); + if (node === undefined) return undefined; + const previous = direction === "Down" ? "ArrowUp" : "ArrowLeft"; + const next = direction === "Down" ? "ArrowDown" : "ArrowRight"; + if (key === previous || key === next) { + const edge = document.edges.find((edge) => + key === next + ? edge.source.nodeId === id && edge.target.nodeId !== id + : edge.target.nodeId === id && edge.source.nodeId !== id, + ); + return key === next ? edge?.target.nodeId : edge?.source.nodeId; + } + const siblings = childrenOf(document.nodes, node.parentId); + const at = siblings.findIndex((sibling) => sibling.id === id); + const offset = + key === (direction === "Down" ? "ArrowLeft" : "ArrowUp") + ? -1 + : key === (direction === "Down" ? "ArrowRight" : "ArrowDown") + ? 1 + : 0; + return offset === 0 ? undefined : siblings[at + offset]?.id; +}; + +const MeasureCanvas = Command.define("MeasureGraphCanvas", { + args: { id: S.String, focusId: S.Option(S.String) }, + messages: [Message.MeasuredCanvas], + execute: ({ id, focusId }) => + Effect.sync(() => { + const canvas = document.getElementById(id)?.querySelector("[data-graph-canvas]"); + return Message.MeasuredCanvas({ + width: canvas?.clientWidth ?? 800, + height: canvas?.clientHeight ?? 500, + focusId, + }); + }), +}); +const FocusNode = Command.define("FocusGraphNode", { + args: { id: S.String, nodeId: S.String }, + messages: [Message.CompletedFocus], + execute: ({ id, nodeId }) => + Effect.promise( + () => + new Promise((resolve) => + requestAnimationFrame(() => { + document + .getElementById(id) + ?.querySelector(`[data-graph-node="${CSS.escape(nodeId)}"]`) + ?.focus({ preventScroll: true }); + resolve(Message.CompletedFocus()); + }), + ), + ), +}); + +type UpdateReturn = Update.ReturnWithOutMessage; +const select = (model: Model, id: string): UpdateReturn => ({ + model: { ...model, canvas: Diagram.select(model.canvas, [id]) }, + outMessage: OutMessage.Selected({ id }), +}); + +/** The host supplies its document; graph interaction never edits it. */ +export const update = (model: Model, message: Message, document: GraphDocument): UpdateReturn => + Message.match(message, { + GotCanvasMessage: ({ message }) => { + const result = Diagram.update(model.canvas, message); + return { + model: { ...model, canvas: result.model }, + ...(result.outMessage?._tag === "ChangedSelection" && result.outMessage.ids.length === 0 + ? { outMessage: OutMessage.ClearedSelection() } + : {}), + }; + }, + SelectedNode: ({ id }) => + document.nodes.some((node) => node.id === id) ? select(model, id) : { model }, + ActivatedNode: ({ id }) => + document.nodes.some((node) => node.id === id) + ? { + model: { ...model, canvas: Diagram.select(model.canvas, [id]) }, + outMessage: OutMessage.Activated({ id }), + } + : { model }, + ToggledNode: ({ id }) => { + if (!document.nodes.some((node) => node.id === id)) return { model }; + const isExpanded = model.collapsedIds.includes(id); + const next = { + ...model, + collapsedIds: isExpanded + ? model.collapsedIds.filter((key) => key !== id) + : [...model.collapsedIds, id], + }; + const selected = model.canvas.selection[0]; + if (selected !== undefined && ancestorIds(document.nodes, selected).includes(id)) + next.canvas = Diagram.select(model.canvas, [id]); + return { model: next, outMessage: OutMessage.ChangedExpansion({ id, isExpanded }) }; + }, + ChangedNeighborhood: ({ id, hops }) => ({ + model: { + ...model, + focusId: id, + hops: Number.isFinite(hops) ? Math.max(0, Math.floor(hops)) : 1, + }, + }), + RequestedFit: () => ({ + model, + commands: [MeasureCanvas({ id: model.canvas.id, focusId: Option.none() })], + }), + RequestedFocus: ({ id }) => { + if (!document.nodes.some((node) => node.id === id)) return { model }; + const ancestors = ancestorIds(document.nodes, id); + const next = { + ...model, + collapsedIds: model.collapsedIds.filter((key) => !ancestors.includes(key)), + focusId: Option.isSome(model.focusId) ? Option.some(id) : model.focusId, + canvas: Diagram.select(model.canvas, [id]), + }; + return { + model: next, + commands: [ + MeasureCanvas({ id: model.canvas.id, focusId: Option.some(id) }), + FocusNode({ id: model.canvas.id, nodeId: id }), + ], + }; + }, + MeasuredCanvas: ({ width, height, focusId }) => { + const scene = layout(visibleDocument(model, document), model.direction); + const focused = Option.isSome(focusId) ? scene.nodes.get(focusId.value) : undefined; + const bounds = focused ?? scene.bounds; + return { + model: { + ...model, + canvas: { ...model.canvas, viewport: Diagram.fitViewport(bounds, { width, height }) }, + }, + }; + }, + CompletedFocus: () => ({ model }), + }); + +const canvasSubscriptions = Subscription.lift(Diagram.subscriptions)({ + toChildModel: (model) => model.canvas, + toParentMessage: (message) => Message.GotCanvasMessage({ message }), +}); + +export const subscriptions = { + graphDocumentPointer: canvasSubscriptions.documentPointer, + graphDocumentEscape: canvasSubscriptions.documentEscape, +}; + +export type ViewConfig = Readonly<{ + model: Model; + document: GraphDocument; + toParentMessage: (message: Message) => ParentMessage; + label?: string; + height?: number; + showList?: boolean; +}>; + +export const view = ( + config: ViewConfig, + h: HtmlBuilder, +): Html => { + const { model, toParentMessage: send } = config; + const document = visibleDocument(model, config.document); + const scene = layout(document, model.direction); + const edgeById = new Map(document.edges.map((edge) => [edge.id, edge])); + const nodeById = new Map(document.nodes.map((node) => [node.id, node])); + const candidate = model.canvas.selection[0]; + const selected = document.nodes.some((node) => node.id === candidate) + ? candidate + : document.nodes[0]?.id; + const nodeAttrs = (node: DiagramNode, isList = false) => [ + h.Type("button"), + h.DataAttribute(isList ? "graph-list-node" : "graph-node", node.id), + h.DataAttribute( + isList ? "graph-list-dom-id" : "graph-dom-id", + `${encodeURIComponent(model.canvas.id)}:${encodeURIComponent(node.id)}`, + ), + h.DataAttribute("tone", node.data.tone ?? "neutral"), + h.AriaPressed(model.canvas.selection.includes(node.id) ? "true" : "false"), + h.AriaLabel( + node.data.description === undefined + ? node.data.label + : `${node.data.label}: ${node.data.description}`, + ), + h.OnClick(send(Message.SelectedNode({ id: node.id }))), + h.OnDoubleClick(send(Message.ActivatedNode({ id: node.id }))), + h.OnKeyDownFocus((key) => { + if (key === "Enter") + return Option.some({ + focusSelector: `[data-${isList ? "graph-list-dom-id" : "graph-dom-id"}="${encodeURIComponent(model.canvas.id)}:${encodeURIComponent(node.id)}"]`, + message: send(Message.ActivatedNode({ id: node.id })), + }); + const target = navigationTarget(document, node.id, key, model.direction); + return target === undefined + ? Option.none() + : Option.some({ + focusSelector: `[data-${isList ? "graph-list-dom-id" : "graph-dom-id"}="${encodeURIComponent(model.canvas.id)}:${encodeURIComponent(target)}"]`, + message: send(Message.SelectedNode({ id: target })), + }); + }), + ]; + const canvasSend = (message: Diagram.Message) => send(Message.GotCanvasMessage({ message })); + return h.section( + [h.Id(model.canvas.id), h.Class("fw-graph"), h.AriaLabel(config.label ?? "Graph")], + [ + h.div( + [h.Class("fw-graph__toolbar"), h.Role("toolbar"), h.AriaLabel("Graph controls")], + [ + h.button([h.Type("button"), h.OnClick(send(Message.RequestedFit()))], ["Fit graph"]), + h.button( + [h.Type("button"), h.OnClick(canvasSend(Diagram.Message.ClickedZoomOut()))], + ["Zoom out"], + ), + h.button( + [h.Type("button"), h.OnClick(canvasSend(Diagram.Message.ClickedZoomIn()))], + ["Zoom in"], + ), + ...(Option.isNone(model.focusId) + ? [] + : [ + h.button( + [ + h.Type("button"), + h.OnClick( + send( + Message.ChangedNeighborhood({ id: model.focusId, hops: model.hops + 1 }), + ), + ), + ], + ["Expand neighborhood"], + ), + h.button( + [ + h.Type("button"), + h.Disabled(model.hops <= 0), + h.OnClick( + send( + Message.ChangedNeighborhood({ + id: model.focusId, + hops: Math.max(0, model.hops - 1), + }), + ), + ), + ], + ["Collapse neighborhood"], + ), + h.button( + [ + h.Type("button"), + h.OnClick( + send(Message.ChangedNeighborhood({ id: Option.none(), hops: model.hops })), + ), + ], + ["Show all"], + ), + ]), + ], + ), + h.div( + [ + h.Class("fw-graph__canvas"), + h.DataAttribute("graph-canvas", "true"), + h.Style({ height: `${config.height ?? 500}px` }), + ], + [ + h.div( + [ + ...Diagram.canvasAttributes({ model: model.canvas, toParentMessage: canvasSend }, h), + h.Class("fw-graph__background"), + ], + [], + ), + h.div( + [ + h.Class("fw-graph__world"), + h.Style({ transform: Diagram.worldTransform(model.canvas) }), + ], + [ + h.svg( + [ + h.Class("fw-graph__edges"), + h.AriaHidden(true), + h.Width(String(Math.max(1, scene.bounds.x + scene.bounds.width))), + h.Height(String(Math.max(1, scene.bounds.y + scene.bounds.height))), + ], + scene.edges.flatMap((edge) => { + const data = edgeById.get(edge.id)?.data; + const points = edge.points; + const last = points.at(-1); + const previous = points.at(-2); + const arrow = + last === undefined || previous === undefined + ? [] + : (() => { + const angle = Math.atan2(last.y - previous.y, last.x - previous.x); + const back = { + x: last.x - 9 * Math.cos(angle), + y: last.y - 9 * Math.sin(angle), + }; + return [ + h.polyline( + [ + h.Points( + `${back.x + 4 * Math.sin(angle)},${back.y - 4 * Math.cos(angle)} ${last.x},${last.y} ${back.x - 4 * Math.sin(angle)},${back.y + 4 * Math.cos(angle)}`, + ), + ], + [], + ), + ]; + })(); + return [ + h.g( + [h.DataAttribute("tone", data?.tone ?? "neutral")], + [ + h.polyline( + [h.Points(points.map((point) => `${point.x},${point.y}`).join(" "))], + [], + ), + ...arrow, + ], + ), + ]; + }), + ), + ...[...scene.nodes.values()].map((position) => { + const node = nodeById.get(position.id)!; + const hasChildren = config.document.nodes.some( + (child) => child.parentId === node.id, + ); + return h.keyed("div")( + node.id, + [ + h.Class("fw-graph__node"), + h.DataAttribute("container", position.isContainer ? "true" : "false"), + h.DataAttribute("tone", node.data.tone ?? "neutral"), + h.Style({ + left: `${position.x}px`, + top: `${position.y}px`, + width: `${position.width}px`, + height: `${position.height}px`, + }), + ], + [ + h.button( + [ + ...nodeAttrs(node), + h.Class("fw-graph__node-label"), + h.Tabindex(node.id === selected ? 0 : -1), + ], + [node.data.label], + ), + ...(hasChildren + ? [ + h.button( + [ + h.Type("button"), + h.Class("fw-graph__disclosure"), + h.AriaLabel( + `${model.collapsedIds.includes(node.id) ? "Expand" : "Collapse"} ${node.data.label}`, + ), + h.AriaExpanded(!model.collapsedIds.includes(node.id)), + h.OnClick(send(Message.ToggledNode({ id: node.id }))), + ], + [model.collapsedIds.includes(node.id) ? "+" : "−"], + ), + ] + : []), + ], + ); + }), + // Labels paint above node headers; compound edges can pass through them. + h.svg( + [ + h.Class("fw-graph__edges"), + h.AriaHidden(true), + h.Width(String(Math.max(1, scene.bounds.x + scene.bounds.width))), + h.Height(String(Math.max(1, scene.bounds.y + scene.bounds.height))), + ], + scene.edges.flatMap((edge) => { + const data = edgeById.get(edge.id)?.data; + return data?.label === undefined || edge.labelPosition === undefined + ? [] + : [ + h.g( + [h.DataAttribute("tone", data.tone ?? "neutral")], + [ + h.text( + [ + h.X(String(edge.labelPosition.x)), + h.Y(String(edge.labelPosition.y - 6)), + h.TextAnchor("middle"), + ], + [data.label], + ), + ], + ), + ]; + }), + ), + ], + ), + ], + ), + ...(config.showList === false + ? [] + : [ + h.details( + [h.Class("fw-graph__list")], + [ + h.summary([], ["Graph as a list"]), + h.ul( + [], + document.nodes.map((node) => + h.li( + [], + [ + h.button([...nodeAttrs(node, true)], [node.data.label]), + ...(config.document.nodes.some((child) => child.parentId === node.id) + ? [ + h.button( + [ + h.Type("button"), + h.AriaExpanded(!model.collapsedIds.includes(node.id)), + h.OnClick(send(Message.ToggledNode({ id: node.id }))), + ], + [model.collapsedIds.includes(node.id) ? "Expand" : "Collapse"], + ), + ] + : []), + h.ul( + [], + document.edges + .filter((edge) => edge.source.nodeId === node.id) + .map((edge) => + h.li( + [], + [ + `${edge.data.label ?? "To"}: ${document.nodes.find((target) => target.id === edge.target.nodeId)?.data.label ?? edge.target.nodeId}`, + ], + ), + ), + ), + ], + ), + ), + ), + ], + ), + ]), + ], + ); +}; diff --git a/packages/diagram/src/index.ts b/packages/diagram/src/index.ts index e0b9811..96777e7 100644 --- a/packages/diagram/src/index.ts +++ b/packages/diagram/src/index.ts @@ -4,3 +4,4 @@ export * from "./layout"; export * from "./operations"; export * from "./scene"; export * as Diagram from "./interaction"; +export * as Graph from "./graph"; diff --git a/packages/diagram/src/styles.css b/packages/diagram/src/styles.css new file mode 100644 index 0000000..053daa0 --- /dev/null +++ b/packages/diagram/src/styles.css @@ -0,0 +1,105 @@ +.fw-graph { + font-family: var(--font-sans); + color: var(--foreground); + min-width: 0; +} +.fw-graph__toolbar { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 8px; +} +.fw-graph button { + font: inherit; + color: inherit; + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 4px 8px; + cursor: pointer; +} +.fw-graph button:focus-visible { + outline: 2px solid var(--ring); + outline-offset: 2px; +} +.fw-graph button[aria-pressed="true"] { + box-shadow: inset 0 0 0 2px var(--ring); +} +.fw-graph__canvas { + position: relative; + overflow: hidden; + background: var(--muted); + border: 1px solid var(--border); +} +.fw-graph__background { + position: absolute; + inset: 0; +} +.fw-graph__world { + position: absolute; + inset: 0; + transform-origin: 0 0; + pointer-events: none; +} +.fw-graph__edges { + overflow: visible; + position: absolute; + pointer-events: none; +} +.fw-graph__edges polyline { + stroke: var(--graph-tone, var(--muted-foreground)); + stroke-width: 1.5; + fill: none; +} +.fw-graph__edges text { + fill: var(--graph-tone, var(--foreground)); + stroke: var(--muted); + stroke-width: 4px; + paint-order: stroke; + font-size: 12px; +} +.fw-graph__node { + position: absolute; + border: 1px solid var(--graph-tone, var(--border)); + border-radius: var(--radius-md); + background: var(--card); + pointer-events: none; +} +.fw-graph__node-label { + position: absolute; + inset: 0 0 auto; + width: 100%; + min-height: 62px; + pointer-events: auto; +} +.fw-graph__disclosure { + position: absolute; + right: 4px; + top: 4px; + pointer-events: auto; +} +.fw-graph [data-tone="info"] { + --graph-tone: var(--foldworks-ui-info); +} +.fw-graph [data-tone="success"] { + --graph-tone: var(--foldworks-ui-success); +} +.fw-graph [data-tone="warning"] { + --graph-tone: var(--foldworks-ui-warning); +} +.fw-graph [data-tone="danger"] { + --graph-tone: var(--foldworks-ui-danger); +} +.fw-graph [data-tone="muted"] { + --graph-tone: var(--muted-foreground); +} +.fw-graph__list { + margin-top: 12px; +} +.fw-graph__list li { + margin-block: 4px; +} +/* Container frames must not cover edges routed between their children. */ +.fw-graph__node[data-container="true"] { + background: transparent; +} diff --git a/packages/diff-viewer/README.md b/packages/diff-viewer/README.md index d76850c..ee434c5 100644 --- a/packages/diff-viewer/README.md +++ b/packages/diff-viewer/README.md @@ -12,16 +12,19 @@ import "@foldworks/diff-viewer/styles.css"; const [file] = parseUnifiedDiff(patch); -DiffViewer.view({ - file, - mode: "Split", - selectedRange: { path: file.path, side: "new", startLine: 20, endLine: 24 }, - onSelectLine: (selection) => Message.SelectedLine(selection), - onStartSelection: (selection) => Message.StartedSelection(selection), - onExtendSelection: (selection, method) => Message.ExtendedSelection({ ...selection, method }), - onEndSelection: () => Message.EndedSelection(), - onCancelSelection: () => Message.CancelledSelection(), -}, h); +DiffViewer.view( + { + file, + mode: "Split", + selectedRange: { path: file.path, side: "new", startLine: 20, endLine: 24 }, + onSelectLine: (selection) => Message.SelectedLine(selection), + onStartSelection: (selection) => Message.StartedSelection(selection), + onExtendSelection: (selection, method) => Message.ExtendedSelection({ ...selection, method }), + onEndSelection: () => Message.EndedSelection(), + onCancelSelection: () => Message.CancelledSelection(), + }, + h, +); ``` Drag across line-number gutters to select a range, or focus a gutter and use @@ -29,3 +32,35 @@ Shift+Arrow Up/Down. Escape cancels the selection. The view is deliberately controlled: view mode, reviewed state, selected range, and thread metadata all belong to the application. This keeps the renderer useful with local Git, GitHub, GitLab, or agent-authored review workflows. + +## Prose comparison + +```ts +import { diffText, ProseDiff } from "@foldworks/diff-viewer"; + +const parts = diffText(originalClause, revisedClause); +ProseDiff.view( + { + oldText: originalClause, + newText: revisedClause, + mode: "split", // "inline" by default + oldLabel: "Reference", + newLabel: "Rider", + }, + h, +); +``` + +`diffText` returns compact `equal`, `addition`, and `removal` parts with exact +text and UTF-16 `oldFrom`/`newFrom` offsets. Filtering out additions reconstructs +the original; filtering out removals reconstructs the revision, including all +whitespace and punctuation. Word mode handles Unicode words and separate +punctuation. `{ granularity: "sentence", locale: "en" }` uses `Intl.Segmenter`. +It does not classify moved sentences or align unrelated sections. + +Myers' search is bounded by `maxSearchCells` (default 1,000,000). If that budget +is exhausted, the changed middle becomes a removal plus an addition; common +prefix and suffix tokens remain equal. This retains exact source text while +avoiding unbounded diff-search memory on long, unrelated inputs. The view is +controlled and read-only, uses semantic ``/`` markup and theme tones, +and needs the same package stylesheet as `DiffViewer`. diff --git a/packages/diff-viewer/src/index.ts b/packages/diff-viewer/src/index.ts index ab087c7..02eedb2 100644 --- a/packages/diff-viewer/src/index.ts +++ b/packages/diff-viewer/src/index.ts @@ -1,3 +1,10 @@ export * from "./core"; export { view, type ViewConfig } from "./view"; export * as DiffViewer from "./view"; +export { + diffText, + ProseDiff, + type DiffTextOptions, + type ProseDiffConfig, + type TextDiffPart, +} from "./prose"; diff --git a/packages/diff-viewer/src/prose.test.ts b/packages/diff-viewer/src/prose.test.ts new file mode 100644 index 0000000..687bb05 --- /dev/null +++ b/packages/diff-viewer/src/prose.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from "vitest"; +import { diffText } from "./prose"; +const sources = (parts: ReturnType) => [ + parts + .filter((part) => part.kind !== "addition") + .map((part) => part.text) + .join(""), + parts + .filter((part) => part.kind !== "removal") + .map((part) => part.text) + .join(""), +]; +describe("diffText", () => { + it("isolates changed words and punctuation", () => { + expect( + diffText("Employees may bid.", "Employees must bid!").map(({ kind, text }) => [kind, text]), + ).toEqual([ + ["equal", "Employees "], + ["removal", "may"], + ["addition", "must"], + ["equal", " bid"], + ["removal", "."], + ["addition", "!"], + ]); + }); + it("reconstructs exact sources including Unicode and whitespace, with valid offsets", () => { + for (const granularity of ["word", "sentence"] as const) + for (const old of ["", "α 😀 café.\n", "a a b\t!"]) + for (const next of ["", "α café!\n\n", "b a b\t!"]) { + const parts = diffText(old, next, { granularity }); + expect(sources(parts)).toEqual([old, next]); + for (const part of parts) { + if (part.kind !== "addition") + expect(old.slice(part.oldFrom, part.oldFrom + part.text.length)).toBe(part.text); + if (part.kind !== "removal") + expect(next.slice(part.newFrom, part.newFrom + part.text.length)).toBe(part.text); + } + } + }); + it("falls back within its search budget without losing source text", () => { + const old = "prefix " + "old ".repeat(2000) + " suffix"; + const next = "prefix " + "new ".repeat(2000) + " suffix"; + expect(sources(diffText(old, next, { maxSearchCells: 10 }))).toEqual([old, next]); + }); +}); diff --git a/packages/diff-viewer/src/prose.ts b/packages/diff-viewer/src/prose.ts new file mode 100644 index 0000000..1dcbaab --- /dev/null +++ b/packages/diff-viewer/src/prose.ts @@ -0,0 +1,177 @@ +import type { Html, HtmlBuilder } from "foldkit/html"; + +export type TextDiffPart = Readonly<{ + kind: "equal" | "addition" | "removal"; + text: string; + /** UTF-16 offsets at the start of this part in each source. */ + oldFrom: number; + newFrom: number; +}>; + +export type DiffTextOptions = Readonly<{ + granularity?: "word" | "sentence"; + locale?: string; + /** Bounds Myers' search memory/work. Larger changes fall back to a replacement. */ + maxSearchCells?: number; +}>; + +type Edit = Readonly<{ kind: TextDiffPart["kind"]; text: string }>; +const tokenize = (text: string, options: DiffTextOptions): string[] => + options.granularity === "sentence" + ? [ + ...new Intl.Segmenter(options.locale ?? "en", { granularity: "sentence" }).segment(text), + ].map((part) => part.segment) + : (text.match(/[\p{L}\p{N}\p{M}_]+|\s+|[^\p{L}\p{N}\p{M}_\s]/gu) ?? []); + +const myers = (old: readonly string[], next: readonly string[], maxCells: number): Edit[] => { + const trace: Map[] = []; + let frontier = new Map([[1, 0]]); + let cells = 0; + for (let distance = 0; distance <= old.length + next.length; distance++) { + cells += 2 * distance + 1; + if (cells > maxCells) break; + trace.push(new Map(frontier)); + for (let diagonal = -distance; diagonal <= distance; diagonal += 2) { + let x = + diagonal === -distance || + (diagonal !== distance && + (frontier.get(diagonal - 1) ?? -Infinity) < (frontier.get(diagonal + 1) ?? -Infinity)) + ? (frontier.get(diagonal + 1) ?? 0) + : (frontier.get(diagonal - 1) ?? 0) + 1; + let y = x - diagonal; + while (x < old.length && y < next.length && old[x] === next[y]) { + x++; + y++; + } + frontier.set(diagonal, x); + if (x < old.length || y < next.length) continue; + const edits: Edit[] = []; + x = old.length; + y = next.length; + for (let depth = distance; depth >= 0; depth--) { + const previous = trace[depth]!; + const k = x - y; + const previousK = + k === -depth || + (k !== depth && (previous.get(k - 1) ?? -Infinity) < (previous.get(k + 1) ?? -Infinity)) + ? k + 1 + : k - 1; + const previousX = previous.get(previousK) ?? 0; + const previousY = previousX - previousK; + while (x > previousX && y > previousY) { + edits.push({ kind: "equal", text: old[--x]! }); + y--; + } + if (depth === 0) break; + if (x === previousX) edits.push({ kind: "addition", text: next[--y]! }); + else edits.push({ kind: "removal", text: old[--x]! }); + } + return edits.reverse(); + } + } + return [ + ...old.map((text): Edit => ({ kind: "removal", text })), + ...next.map((text): Edit => ({ kind: "addition", text })), + ]; +}; + +/** Exact, whitespace-preserving prose diff. No inference of moved sentences. */ +export const diffText = ( + oldText: string, + newText: string, + options: DiffTextOptions = {}, +): readonly TextDiffPart[] => { + const old = tokenize(oldText, options); + const next = tokenize(newText, options); + let start = 0; + while (start < Math.min(old.length, next.length) && old[start] === next[start]) start++; + let end = 0; + while ( + end < Math.min(old.length, next.length) - start && + old[old.length - 1 - end] === next[next.length - 1 - end] + ) + end++; + const budget = + options.maxSearchCells !== undefined && Number.isFinite(options.maxSearchCells) + ? Math.max(0, Math.floor(options.maxSearchCells)) + : 1_000_000; + const edits: Edit[] = [ + ...old.slice(0, start).map((text): Edit => ({ kind: "equal", text })), + ...myers(old.slice(start, old.length - end), next.slice(start, next.length - end), budget), + ...old.slice(old.length - end).map((text): Edit => ({ kind: "equal", text })), + ]; + const parts: Array<{ + kind: TextDiffPart["kind"]; + text: string; + oldFrom: number; + newFrom: number; + }> = []; + let oldFrom = 0; + let newFrom = 0; + for (const edit of edits) { + const previous = parts.at(-1); + if (previous?.kind === edit.kind) previous.text += edit.text; + else parts.push({ ...edit, oldFrom, newFrom }); + if (edit.kind !== "addition") oldFrom += edit.text.length; + if (edit.kind !== "removal") newFrom += edit.text.length; + } + return parts; +}; + +export type ProseDiffConfig = Readonly<{ + oldText: string; + newText: string; + mode?: "inline" | "split"; + options?: DiffTextOptions; + label?: string; + oldLabel?: string; + newLabel?: string; +}>; + +export const ProseDiff = { + view: (config: ProseDiffConfig, h: HtmlBuilder): Html => { + const parts = diffText(config.oldText, config.newText, config.options); + const content = (side?: "old" | "new") => + parts + .filter((part) => + side === "old" + ? part.kind !== "addition" + : side === "new" + ? part.kind !== "removal" + : true, + ) + .map((part) => { + const attrs = [h.Class("fk-prose-diff__part"), h.DataAttribute("kind", part.kind)]; + return part.kind === "addition" + ? h.ins(attrs, [part.text]) + : part.kind === "removal" + ? h.del(attrs, [part.text]) + : h.span(attrs, [part.text]); + }); + return h.section( + [ + h.Class("fk-prose-diff"), + h.DataAttribute("mode", config.mode ?? "inline"), + h.AriaLabel(config.label ?? "Text comparison"), + ], + config.mode === "split" + ? [ + h.div( + [h.Class("fk-prose-diff__column")], + [ + h.h3([], [config.oldLabel ?? "Original"]), + h.div([h.Class("fk-prose-diff__text")], content("old")), + ], + ), + h.div( + [h.Class("fk-prose-diff__column")], + [ + h.h3([], [config.newLabel ?? "Revised"]), + h.div([h.Class("fk-prose-diff__text")], content("new")), + ], + ), + ] + : [h.div([h.Class("fk-prose-diff__text")], content())], + ); + }, +}; diff --git a/packages/diff-viewer/src/styles.css b/packages/diff-viewer/src/styles.css index 3f39132..7f0e7e5 100644 --- a/packages/diff-viewer/src/styles.css +++ b/packages/diff-viewer/src/styles.css @@ -66,22 +66,43 @@ color: #9a7000; display: inline-flex; flex: 0 0 auto; - font: 700 10px/1 ui-monospace, SFMono-Regular, Menlo, monospace; + font: + 700 10px/1 ui-monospace, + SFMono-Regular, + Menlo, + monospace; height: 19px; justify-content: center; width: 19px; } -.fk-diff-viewer__status[data-status="added"] { background: var(--diff-add); color: #167746; } -.fk-diff-viewer__status[data-status="deleted"] { background: var(--diff-delete); color: #b33d46; } -.fk-diff-viewer__status[data-status="renamed"] { background: var(--diff-hunk); color: #476bc7; } +.fk-diff-viewer__status[data-status="added"] { + background: var(--diff-add); + color: #167746; +} +.fk-diff-viewer__status[data-status="deleted"] { + background: var(--diff-delete); + color: #b33d46; +} +.fk-diff-viewer__status[data-status="renamed"] { + background: var(--diff-hunk); + color: #476bc7; +} .fk-diff-viewer__additions, .fk-diff-viewer__deletions { - font: 650 11px ui-monospace, SFMono-Regular, Menlo, monospace; + font: + 650 11px ui-monospace, + SFMono-Regular, + Menlo, + monospace; +} +.fk-diff-viewer__additions { + color: #218354; +} +.fk-diff-viewer__deletions { + color: #c34d55; } -.fk-diff-viewer__additions { color: #218354; } -.fk-diff-viewer__deletions { color: #c34d55; } .fk-diff-viewer__reviewed { background: var(--diff-surface); @@ -93,14 +114,24 @@ height: 27px; padding: 0 9px; } -.fk-diff-viewer__reviewed:hover { color: var(--diff-text); } -.fk-diff-viewer__reviewed[data-checked="true"] { background: var(--diff-selection); border-color: color-mix(in srgb, var(--diff-accent) 30%, var(--diff-border)); color: var(--diff-accent); } +.fk-diff-viewer__reviewed:hover { + color: var(--diff-text); +} +.fk-diff-viewer__reviewed[data-checked="true"] { + background: var(--diff-selection); + border-color: color-mix(in srgb, var(--diff-accent) 30%, var(--diff-border)); + color: var(--diff-accent); +} .fk-diff-viewer__gap { background: var(--diff-subtle); border-bottom: 1px solid var(--diff-border); color: var(--diff-muted); - font: 11px/30px ui-monospace, SFMono-Regular, Menlo, monospace; + font: + 11px/30px ui-monospace, + SFMono-Regular, + Menlo, + monospace; height: 30px; padding-left: 16px; } @@ -117,23 +148,44 @@ padding: 0 14px; white-space: nowrap; } -.fk-diff-viewer__hunk-header code { font: 11px ui-monospace, SFMono-Regular, Menlo, monospace; } -.fk-diff-viewer__hunk-header span { color: var(--diff-muted); font-size: 11px; overflow: hidden; text-overflow: ellipsis; } +.fk-diff-viewer__hunk-header code { + font: + 11px ui-monospace, + SFMono-Regular, + Menlo, + monospace; +} +.fk-diff-viewer__hunk-header span { + color: var(--diff-muted); + font-size: 11px; + overflow: hidden; + text-overflow: ellipsis; +} .fk-diff-viewer__line { display: grid; grid-template-columns: 52px 52px 22px minmax(max-content, 1fr); min-height: 26px; } -.fk-diff-viewer__line--addition { background: var(--diff-add); } -.fk-diff-viewer__line--deletion { background: var(--diff-delete); } +.fk-diff-viewer__line--addition { + background: var(--diff-add); +} +.fk-diff-viewer__line--deletion { + background: var(--diff-delete); +} .fk-diff-viewer__line[data-selected="true"], .fk-diff-viewer__split-cell[data-selected="true"] { background: color-mix(in srgb, #4f7bef 15%, var(--diff-surface)); box-shadow: inset 3px 0 #4f7bef; } -.fk-diff-viewer__line--addition .fk-diff-viewer__prefix { background: var(--diff-add-strong); color: #207b4e; } -.fk-diff-viewer__line--deletion .fk-diff-viewer__prefix { background: var(--diff-delete-strong); color: #b6424b; } +.fk-diff-viewer__line--addition .fk-diff-viewer__prefix { + background: var(--diff-add-strong); + color: #207b4e; +} +.fk-diff-viewer__line--deletion .fk-diff-viewer__prefix { + background: var(--diff-delete-strong); + color: #b6424b; +} .fk-diff-viewer__gutter { align-items: center; @@ -143,22 +195,47 @@ color: var(--diff-muted); cursor: pointer; display: grid; - font: 10px/1 ui-monospace, SFMono-Regular, Menlo, monospace; + font: + 10px/1 ui-monospace, + SFMono-Regular, + Menlo, + monospace; grid-template-columns: 14px 1fr; min-width: 0; padding: 0 5px 0 2px; position: relative; text-align: right; } -.fk-diff-viewer__gutter:disabled { cursor: default; } +.fk-diff-viewer__gutter:disabled { + cursor: default; +} .fk-diff-viewer__gutter:not(:disabled):hover, -.fk-diff-viewer__gutter[data-selected="true"] { background: var(--diff-accent); color: #fff; } -.fk-diff-viewer__gutter:focus-visible { outline: 2px solid var(--diff-accent); outline-offset: -2px; z-index: 2; } -.fk-diff-viewer__gutter--empty { display: block; } -.fk-diff-viewer__add-comment { font-size: 15px; font-weight: 700; opacity: 0; text-align: center; } +.fk-diff-viewer__gutter[data-selected="true"] { + background: var(--diff-accent); + color: #fff; +} +.fk-diff-viewer__gutter:focus-visible { + outline: 2px solid var(--diff-accent); + outline-offset: -2px; + z-index: 2; +} +.fk-diff-viewer__gutter--empty { + display: block; +} +.fk-diff-viewer__add-comment { + font-size: 15px; + font-weight: 700; + opacity: 0; + text-align: center; +} .fk-diff-viewer__gutter:not(:disabled):hover .fk-diff-viewer__add-comment, -.fk-diff-viewer__gutter[data-selected="true"] .fk-diff-viewer__add-comment { opacity: 1; } -.fk-diff-viewer__line-number { overflow: hidden; text-overflow: clip; } +.fk-diff-viewer__gutter[data-selected="true"] .fk-diff-viewer__add-comment { + opacity: 1; +} +.fk-diff-viewer__line-number { + overflow: hidden; + text-overflow: clip; +} .fk-diff-viewer__thread-count { align-items: center; background: #4f7bef; @@ -166,7 +243,9 @@ border-radius: 999px; color: #fff; display: flex; - font: 700 8px/1 ui-monospace, monospace; + font: + 700 8px/1 ui-monospace, + monospace; height: 16px; justify-content: center; position: absolute; @@ -180,12 +259,22 @@ align-items: center; color: var(--diff-muted); display: flex; - font: 12px/1 ui-monospace, SFMono-Regular, Menlo, monospace; + font: + 12px/1 ui-monospace, + SFMono-Regular, + Menlo, + monospace; justify-content: center; user-select: none; } .fk-diff-viewer__code { - font: 12px/26px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font: + 12px/26px ui-monospace, + SFMono-Regular, + Menlo, + Monaco, + Consolas, + monospace; padding: 0 14px 0 8px; tab-size: 2; white-space: pre; @@ -203,36 +292,125 @@ min-width: 0; overflow: hidden; } -.fk-diff-viewer__split-cell:first-child { border-right: 1px solid var(--diff-border-strong); } -.fk-diff-viewer__split-cell--addition { background: var(--diff-add); } -.fk-diff-viewer__split-cell--deletion { background: var(--diff-delete); } -.fk-diff-viewer__split-cell--empty { background: color-mix(in srgb, var(--diff-subtle) 65%, transparent); } -.fk-diff-viewer__split-cell--addition .fk-diff-viewer__prefix { background: var(--diff-add-strong); color: #207b4e; } -.fk-diff-viewer__split-cell--deletion .fk-diff-viewer__prefix { background: var(--diff-delete-strong); color: #b6424b; } -.fk-diff-viewer__split-cell .fk-diff-viewer__code { min-width: 0; overflow: hidden; text-overflow: clip; } +.fk-diff-viewer__split-cell:first-child { + border-right: 1px solid var(--diff-border-strong); +} +.fk-diff-viewer__split-cell--addition { + background: var(--diff-add); +} +.fk-diff-viewer__split-cell--deletion { + background: var(--diff-delete); +} +.fk-diff-viewer__split-cell--empty { + background: color-mix(in srgb, var(--diff-subtle) 65%, transparent); +} +.fk-diff-viewer__split-cell--addition .fk-diff-viewer__prefix { + background: var(--diff-add-strong); + color: #207b4e; +} +.fk-diff-viewer__split-cell--deletion .fk-diff-viewer__prefix { + background: var(--diff-delete-strong); + color: #b6424b; +} +.fk-diff-viewer__split-cell .fk-diff-viewer__code { + min-width: 0; + overflow: hidden; + text-overflow: clip; +} -.fk-diff-viewer__token--comment { color: #768078; font-style: italic; } -.fk-diff-viewer__token--string { color: #8b5b25; } -.fk-diff-viewer__token--number { color: #9b4f98; } -.fk-diff-viewer__token--type { color: #286f83; } -.fk-diff-viewer__token--keyword { color: #5a55b4; } +.fk-diff-viewer__token--comment { + color: #768078; + font-style: italic; +} +.fk-diff-viewer__token--string { + color: #8b5b25; +} +.fk-diff-viewer__token--number { + color: #9b4f98; +} +.fk-diff-viewer__token--type { + color: #286f83; +} +.fk-diff-viewer__token--keyword { + color: #5a55b4; +} -.fk-diff-viewer__empty { color: var(--diff-muted); padding: 64px 20px; text-align: center; } -.fk-diff-viewer__sr-only { clip: rect(0, 0, 0, 0); clip-path: inset(50%); height: 1px; overflow: hidden; position: absolute; white-space: nowrap; width: 1px; } +.fk-diff-viewer__empty { + color: var(--diff-muted); + padding: 64px 20px; + text-align: center; +} +.fk-diff-viewer__sr-only { + clip: rect(0, 0, 0, 0); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; +} [data-mode="dark"] .fk-diff-viewer__token--comment, -.dark .fk-diff-viewer__token--comment { color: #87918b; } +.dark .fk-diff-viewer__token--comment { + color: #87918b; +} [data-mode="dark"] .fk-diff-viewer__token--string, -.dark .fk-diff-viewer__token--string { color: #d5a467; } +.dark .fk-diff-viewer__token--string { + color: #d5a467; +} [data-mode="dark"] .fk-diff-viewer__token--number, -.dark .fk-diff-viewer__token--number { color: #cf86ca; } +.dark .fk-diff-viewer__token--number { + color: #cf86ca; +} [data-mode="dark"] .fk-diff-viewer__token--type, -.dark .fk-diff-viewer__token--type { color: #79bdcd; } +.dark .fk-diff-viewer__token--type { + color: #79bdcd; +} [data-mode="dark"] .fk-diff-viewer__token--keyword, -.dark .fk-diff-viewer__token--keyword { color: #a6a0ef; } +.dark .fk-diff-viewer__token--keyword { + color: #a6a0ef; +} @media (max-width: 900px) { - .fk-diff-viewer { min-width: 520px; } - .fk-diff-viewer__path { display: none; } - .fk-diff-viewer__split-row { grid-template-columns: minmax(260px, 1fr) minmax(260px, 1fr); } + .fk-diff-viewer { + min-width: 520px; + } + .fk-diff-viewer__path { + display: none; + } + .fk-diff-viewer__split-row { + grid-template-columns: minmax(260px, 1fr) minmax(260px, 1fr); + } +} + +.fk-prose-diff { + color: var(--foreground); + font-family: var(--font-sans); +} +.fk-prose-diff[data-mode="split"] { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; +} +.fk-prose-diff__column { + min-width: 0; +} +.fk-prose-diff__column h3 { + font-size: 12px; + color: var(--muted-foreground); +} +.fk-prose-diff__text { + white-space: pre-wrap; + overflow-wrap: anywhere; + line-height: 1.6; +} +.fk-prose-diff__part[data-kind="addition"] { + color: var(--foldworks-ui-success); + background: var(--foldworks-ui-success-surface); + text-decoration: underline; +} +.fk-prose-diff__part[data-kind="removal"] { + color: var(--foldworks-ui-danger); + background: var(--foldworks-ui-danger-surface); + text-decoration: line-through; } diff --git a/packages/pdf-viewer/README.md b/packages/pdf-viewer/README.md index b7b7b7e..02cafdf 100644 --- a/packages/pdf-viewer/README.md +++ b/packages/pdf-viewer/README.md @@ -3,7 +3,7 @@ A read-only Foldkit PDF viewer composed from `@foldworks/pdf` page surfaces, rendering, and geometry. -`PdfViewer` renders every page of a PDF to an image and adds page navigation, +`PdfViewer` renders pages near the viewport and adds page navigation, zoom (fit width, or 50–200%), download, and skeletons while pages render. It has no editing UI. @@ -61,11 +61,62 @@ Other view options: `actions` (extra toolbar controls), `label`, fills its container and scrolls internally, so give it a sized parent. Import `@foldworks/pdf-viewer/styles.css` once. +## Passage highlights and selection + +```ts +PdfViewer.view( + { + model: model.viewer, + toParentMessage: (message) => Message.GotViewerMessage({ message }), + highlights: [ + { + key: "evidence", + label: "Schedule bidding evidence", + regions: location.regions, // from locateQuote, native selection, or OCR + tone: "warning", + isSelected: model.selectedId === "evidence", + onClick: Message.SelectedEvidence({ id: "evidence" }), + }, + ], + selectableText: true, + onSelectText: (selection) => Message.SelectedPdfText({ selection }), + }, + h, +); +``` + +`PdfHighlight` accepts several rectangles per page and several pages per +passage. Its first rectangle is the single accessible toggle button; the other +rectangles paint the same selection and dispatch the same activation message. +Keys must be unique across highlights and single-rectangle overlays, and each +page should appear once in `regions`. `scrollTo(key)` finds the first rectangle. +Existing boxed `PdfOverlay` inputs keep their behavior. + +`selectableText` mounts PDF.js' transparent native text layer, including font +metrics and rotation. On pointer release or keyboard selection, `onSelectText` +receives `{ text, regions: [{ pageIndex, rects }] }` with user-space rectangles. +Selections must start and end in this viewer's text layers. Selection is not +persisted by the viewer. Without a callback, the same data travels through +`Message.SelectedText` for the host to intercept. Scanned pages need OCR from +the host and have no selectable text. Highlights can intercept pointer presses; +selection can begin outside them, or the host can temporarily hide highlights. + +See the [document-analysis demo](https://foldworks.dev/document-analysis) for a +cross-page passage, linked transcript, and a 12-page bounded-cache example. + ## Known behavior -- **Memory:** rendered pages are kept in the model as PNG `data:` URLs, and - pages render one at a time, each from a fresh PDF.js session. This suits forms - and short documents; very long documents hold every page image in memory. +- **Memory:** at most `maxCachedPages` page images (default 8, minimum 1) are + retained as PNG `data:` URLs. A mounted viewer opens one shared PDF.js document + for sequential rendering and releases it on document replacement or unmount. + Page geometry is inspected separately before rendering. Pages within one + viewport height of the scroller are prioritized by distance to its center. + Offscreen cache entries are evicted first, using least-recently-used order; + evicted pages render again when revisited. Text layers are released with their + page images. All page placeholders and geometry remain mounted, so this bounds + raster/text-layer memory, not the number of page wrappers or the source bytes. + A very small cache may leave some visible pages pending; set it large enough + for your viewport. Rendering requires the view to be mounted. - **Fit width** scales the widest page to the viewer, so all pages share one scale. In mixed-orientation documents, portrait pages are narrower than the viewer. Fit width is capped at 200% and may go below 50% in narrow viewers. diff --git a/packages/pdf-viewer/package.json b/packages/pdf-viewer/package.json index 1ff03be..032a0a1 100644 --- a/packages/pdf-viewer/package.json +++ b/packages/pdf-viewer/package.json @@ -49,7 +49,8 @@ "dependencies": { "@foldworks/pdf": "workspace:*", "@foldworks/ui": "workspace:*", - "@lucide/icons": "catalog:" + "@lucide/icons": "catalog:", + "pdfjs-dist": "catalog:" }, "devDependencies": { "effect": "catalog:", diff --git a/packages/pdf-viewer/src/index.ts b/packages/pdf-viewer/src/index.ts index e262d7c..60f3988 100644 --- a/packages/pdf-viewer/src/index.ts +++ b/packages/pdf-viewer/src/index.ts @@ -1,2 +1,2 @@ export * as PdfViewer from "./pdf-viewer"; -export type { PdfOverlay, PdfOverlayTone } from "./viewer-view"; +export type { PdfHighlight, PdfOverlay, PdfOverlayTone } from "./viewer-view"; diff --git a/packages/pdf-viewer/src/pdf-viewer.ts b/packages/pdf-viewer/src/pdf-viewer.ts index 57f25d6..05af799 100644 --- a/packages/pdf-viewer/src/pdf-viewer.ts +++ b/packages/pdf-viewer/src/pdf-viewer.ts @@ -15,4 +15,10 @@ export { type InitConfig, } from "./viewer-model"; export { goToPage, loadBytes, loadUrl, scrollTo, update } from "./viewer-update"; -export { view, type PdfOverlay, type PdfOverlayTone, type ViewConfig } from "./viewer-view"; +export { + view, + type PdfHighlight, + type PdfOverlay, + type PdfOverlayTone, + type ViewConfig, +} from "./viewer-view"; diff --git a/packages/pdf-viewer/src/render-session.ts b/packages/pdf-viewer/src/render-session.ts new file mode 100644 index 0000000..20ca3bc --- /dev/null +++ b/packages/pdf-viewer/src/render-session.ts @@ -0,0 +1,266 @@ +import { Effect, Queue, Schema as S, Stream } from "effect"; +import { Mount } from "foldkit"; +import type { PDFPageProxy, TextLayer as PdfJsTextLayer } from "pdfjs-dist"; +import { + base64ToBytes, + displayRectToUserSpace, + getPage, + pageInfo, + rasterizePage, + withPdfDocument, +} from "@foldworks/pdf"; + +import { Message } from "./viewer-message"; + +/** A mounted document owns one worker, the render queue, and optional native text layers. */ +export const RenderVisiblePages = Mount.defineStream("RenderVisiblePdfPages", { + args: { + revision: S.Number, + bytesBase64: S.String, + scale: S.Number, + maxCachedPages: S.Number, + selectableText: S.Boolean, + }, + messages: [ + Message.StartedRenderSession, + Message.FailedRenderSession, + Message.ChangedVisiblePages, + Message.CompletedRenderPage, + Message.FailedRenderPage, + Message.SelectedText, + ], + execute: ({ element, revision, bytesBase64, scale, maxCachedPages, selectableText }) => + Stream.callback((output) => + Effect.gen(function* () { + const requests = yield* Queue.sliding(1); + const root = element as HTMLElement; + const scroller = root.parentElement!; + const inFlight = new Set(); + const resources = new Map(); + let desired: number[] = []; + let reported = ""; + const emit = ( + message: Extract< + Message, + { + _tag: + | "StartedRenderSession" + | "FailedRenderSession" + | "ChangedVisiblePages" + | "CompletedRenderPage" + | "FailedRenderPage" + | "SelectedText"; + } + >, + ) => Queue.offerUnsafe(output, message); + const disposePages = () => { + for (const resource of resources.values()) { + resource.layer?.cancel(); + resource.page.cleanup(); + } + resources.clear(); + }; + emit(Message.StartedRenderSession({ revision })); + const elements = () => + Array.from(root.querySelectorAll("[data-pdf-viewer-page]")); + const nodeFor = (index: number) => + root.querySelector(`[data-pdf-viewer-page="${index}"]`); + const schedule = () => { + const viewport = scroller.getBoundingClientRect(); + const center = (viewport.top + viewport.bottom) / 2; + desired = elements() + .filter((node) => { + const bounds = node.getBoundingClientRect(); + return ( + bounds.bottom > viewport.top - viewport.height && + bounds.top < viewport.bottom + viewport.height + ); + }) + .sort( + (a, b) => + Math.abs( + (a.getBoundingClientRect().top + a.getBoundingClientRect().bottom) / 2 - center, + ) - + Math.abs( + (b.getBoundingClientRect().top + b.getBoundingClientRect().bottom) / 2 - center, + ), + ) + .slice(0, maxCachedPages) + .map((node) => Number(node.dataset.pdfViewerPage)); + const next = desired.join(","); + if (next !== reported) { + reported = next; + emit(Message.ChangedVisiblePages({ revision, pageIndexes: desired })); + } + for (const [index, resource] of resources) { + const node = nodeFor(index); + if (node?.dataset.state !== "pending") { + inFlight.delete(index); + continue; + } + // Keep the completed result until Foldkit has applied the corresponding patch. + if (inFlight.has(index)) continue; + resource.layer?.cancel(); + node.querySelector("[data-pdf-text-layer]")?.replaceChildren(); + resource.page.cleanup(); + resources.delete(index); + } + Queue.offerUnsafe(requests, undefined); + }; + const updateLayers = () => { + for (const [index, resource] of resources) { + const node = nodeFor(index); + if (node === null || resource.layer === undefined) continue; + const info = pageInfo(resource.page, index); + const layerNode = node.querySelector("[data-pdf-text-layer]"); + const displayScale = node.clientWidth / info.width; + layerNode?.style.setProperty("--total-scale-factor", String(displayScale)); + resource.layer.update({ viewport: resource.page.getViewport({ scale: displayScale }) }); + } + schedule(); + }; + const select = () => { + if (!selectableText) return; + const selection = root.ownerDocument.getSelection(); + if (selection === null || selection.isCollapsed || selection.rangeCount === 0) return; + const range = selection.getRangeAt(0); + const parent = (node: Node) => (node instanceof Element ? node : node.parentElement); + if ( + !parent(range.startContainer)?.closest("[data-pdf-text-layer]") || + !parent(range.endContainer)?.closest("[data-pdf-text-layer]") || + !root.contains(range.startContainer) || + !root.contains(range.endContainer) + ) + return; + const regions = []; + const texts: string[] = []; + for (const node of elements()) { + const layer = node.querySelector("[data-pdf-text-layer]"); + if (layer === null || !range.intersectsNode(layer)) continue; + const resource = resources.get(Number(node.dataset.pdfViewerPage)); + if (resource === undefined) continue; + const part = range.cloneRange(); + if (!layer.contains(part.startContainer)) part.setStart(layer, 0); + if (!layer.contains(part.endContainer)) part.setEnd(layer, layer.childNodes.length); + const bounds = node.getBoundingClientRect(); + const info = pageInfo(resource.page, Number(node.dataset.pdfViewerPage)); + const seen = new Set(); + const rects = Array.from(part.getClientRects()) + .filter((rect) => rect.width > 0 && rect.height > 0) + .filter((rect) => { + const key = [rect.left, rect.top, rect.width, rect.height] + .map((value) => value.toFixed(3)) + .join(","); + if (seen.has(key)) return false; + seen.add(key); + return true; + }) + .map((rect) => + displayRectToUserSpace(info, { + x: ((rect.left - bounds.left) * info.width) / bounds.width, + y: ((rect.top - bounds.top) * info.height) / bounds.height, + width: (rect.width * info.width) / bounds.width, + height: (rect.height * info.height) / bounds.height, + }), + ); + if (rects.length > 0) { + regions.push({ pageIndex: info.pageIndex, rects }); + texts.push(part.toString()); + } + } + if (regions.length > 0) + emit( + Message.SelectedText({ revision, selection: { text: texts.join("\n"), regions } }), + ); + }; + yield* Effect.acquireRelease( + Effect.sync(() => { + const resize = new ResizeObserver(updateLayers); + resize.observe(root); + const mutations = new MutationObserver(schedule); + mutations.observe(root, { + subtree: true, + attributes: true, + attributeFilter: ["data-state"], + }); + scroller.addEventListener("scroll", schedule, { passive: true }); + root.ownerDocument.addEventListener("pointerup", select); + root.ownerDocument.addEventListener("keyup", select); + schedule(); + return () => { + resize.disconnect(); + mutations.disconnect(); + scroller.removeEventListener("scroll", schedule); + root.ownerDocument.removeEventListener("pointerup", select); + root.ownerDocument.removeEventListener("keyup", select); + disposePages(); + }; + }), + (dispose) => Effect.sync(dispose), + ); + yield* withPdfDocument(base64ToBytes(bytesBase64), (document) => + Effect.gen(function* () { + const pdfjs = selectableText + ? yield* Effect.promise(() => import("pdfjs-dist")) + : undefined; + while (true) { + yield* Queue.take(requests); + const index = desired.find( + (index) => nodeFor(index)?.dataset.state === "pending" && !inFlight.has(index), + ); + if (index === undefined) continue; + inFlight.add(index); + yield* Effect.gen(function* () { + const page = yield* getPage(document, index); + resources.set(index, { page }); + const raster = yield* rasterizePage(page, scale); + if (selectableText && pdfjs !== undefined) { + const node = nodeFor(index)!; + const container = node.querySelector("[data-pdf-text-layer]")!; + const displayScale = node.clientWidth / pageInfo(page, index).width; + container.style.setProperty("--total-scale-factor", String(displayScale)); + const content = yield* Effect.promise(() => page.getTextContent()); + const layer = new pdfjs.TextLayer({ + textContentSource: content, + container, + viewport: page.getViewport({ scale: displayScale }), + }); + resources.set(index, { page, layer }); + yield* Effect.promise(() => layer.render()); + } + emit( + Message.CompletedRenderPage({ revision, pageIndex: index, url: raster.imageUrl }), + ); + }).pipe( + Effect.catchCause((cause) => + Effect.sync(() => { + emit( + Message.FailedRenderPage({ + revision, + pageIndex: index, + reason: String(cause), + }), + ); + }), + ), + ); + // Wait for Foldkit's patch before releasing the request. A result + // can be evicted immediately if the viewport changed during rendering; + // that must not leave the page permanently marked as in flight. + yield* Effect.promise( + () => new Promise((resolve) => requestAnimationFrame(() => resolve())), + ); + inFlight.delete(index); + schedule(); + } + }).pipe(Effect.ensuring(Effect.sync(disposePages))), + ).pipe( + Effect.catch((error) => + Effect.sync(() => { + emit(Message.FailedRenderSession({ revision, reason: error.reason })); + }), + ), + ); + }), + ), +}); diff --git a/packages/pdf-viewer/src/styles.css b/packages/pdf-viewer/src/styles.css index e6c5ae2..b806005 100644 --- a/packages/pdf-viewer/src/styles.css +++ b/packages/pdf-viewer/src/styles.css @@ -283,3 +283,67 @@ button.fk-pdf-viewer__overlay:hover { transition: none; } } + +.fk-pdf-viewer__highlight { + background: color-mix(in srgb, var(--pdf-viewer-overlay-color) 28%, transparent); + border: 0; + border-radius: 0; +} +.fk-pdf-viewer__highlight-group { + position: absolute; + inset: 0; + pointer-events: none; +} +.fk-pdf-viewer__highlight[data-clickable="true"] { + pointer-events: auto; + cursor: pointer; +} +.fk-pdf-viewer__text-layer { + position: absolute; + inset: 0; + overflow: clip; + line-height: 1; + text-align: initial; + letter-spacing: normal; + word-spacing: normal; + text-size-adjust: none; + transform-origin: 0 0; + --min-font-size: 1; + --text-scale-factor: calc(var(--total-scale-factor) * var(--min-font-size)); + --min-font-size-inv: calc(1 / var(--min-font-size)); +} +.fk-pdf-viewer__text-layer :is(span, br) { + color: transparent; + position: absolute; + white-space: pre; + cursor: text; + transform-origin: 0 0; + user-select: text; +} +.fk-pdf-viewer__text-layer > :not(.markedContent), +.fk-pdf-viewer__text-layer .markedContent span:not(.markedContent) { + --font-height: 0; + --scale-x: 1; + --rotate: 0deg; + font-size: calc(var(--text-scale-factor) * var(--font-height)); + transform: rotate(var(--rotate)) scaleX(var(--scale-x)) scale(var(--min-font-size-inv)); +} +.fk-pdf-viewer__text-layer .markedContent { + display: contents; +} +.fk-pdf-viewer__text-layer ::selection { + background: color-mix(in srgb, var(--foldworks-ui-info) 30%, transparent); +} +.fk-pdf-viewer__text-layer { + --scale-round-x: 1px; + --scale-round-y: 1px; +} +.fk-pdf-viewer__text-layer[data-main-rotation="90"] { + transform: rotate(90deg) translateY(-100%); +} +.fk-pdf-viewer__text-layer[data-main-rotation="180"] { + transform: rotate(180deg) translate(-100%, -100%); +} +.fk-pdf-viewer__text-layer[data-main-rotation="270"] { + transform: rotate(270deg) translateX(-100%); +} diff --git a/packages/pdf-viewer/src/viewer-message.ts b/packages/pdf-viewer/src/viewer-message.ts index b8a5aba..978a463 100644 --- a/packages/pdf-viewer/src/viewer-message.ts +++ b/packages/pdf-viewer/src/viewer-message.ts @@ -1,6 +1,8 @@ import { Schema as S } from "effect"; import { defineMessageUnion } from "foldkit/message"; +import { PdfTextSelection } from "@foldworks/pdf"; + import { PageInfo } from "./viewer-model"; export const Message = defineMessageUnion({ @@ -15,6 +17,10 @@ export const Message = defineMessageUnion({ FailedLoad: { revision: S.Number, name: S.String, reason: S.String }, CompletedRenderPage: { revision: S.Number, pageIndex: S.Number, url: S.String }, FailedRenderPage: { revision: S.Number, pageIndex: S.Number, reason: S.String }, + StartedRenderSession: { revision: S.Number }, + FailedRenderSession: { revision: S.Number, reason: S.String }, + ChangedVisiblePages: { revision: S.Number, pageIndexes: S.Array(S.Number) }, + SelectedText: { revision: S.Number, selection: PdfTextSelection }, RequestedPage: { pageIndex: S.Number }, CompletedScrollToPage: {}, ScrolledToPage: { pageIndex: S.Number }, diff --git a/packages/pdf-viewer/src/viewer-model.ts b/packages/pdf-viewer/src/viewer-model.ts index f294485..5460715 100644 --- a/packages/pdf-viewer/src/viewer-model.ts +++ b/packages/pdf-viewer/src/viewer-model.ts @@ -56,6 +56,9 @@ export const Model = S.Struct({ /** Increments on every load so late results from an earlier document are ignored. */ revision: S.Number, renderScale: S.Number, + maxCachedPages: S.Number, + visiblePageIndexes: S.Array(S.Number), + cachedPageIndexes: S.Array(S.Number), zoom: Zoom, viewportWidth: S.Number, currentPageIndex: S.Number, @@ -69,6 +72,8 @@ export type InitConfig = Readonly<{ zoom?: "fit-width" | number; /** Pixels per PDF point used when rasterizing pages. Defaults to 2. */ renderScale?: number; + /** Maximum retained page images, default 8. Visible pages are prioritized. */ + maxCachedPages?: number; }>; export const MIN_ZOOM_PERCENT = 50; @@ -88,6 +93,11 @@ export const init = (config: InitConfig): Model => ({ id: config.id, document: ViewerDocument.Empty(), revision: 0, + maxCachedPages: Number.isFinite(config.maxCachedPages) + ? Math.max(1, Math.floor(config.maxCachedPages!)) + : 8, + visiblePageIndexes: [], + cachedPageIndexes: [], renderScale: config.renderScale !== undefined && Number.isFinite(config.renderScale) && @@ -131,15 +141,3 @@ export const nextZoomStep = (percent: number, direction: "in" | "out"): number = : [...ZOOM_STEPS].reverse().find((candidate) => candidate < current); return step ?? (direction === "in" ? MAX_ZOOM_PERCENT : MIN_ZOOM_PERCENT); }; - -/** Next page still waiting for a raster, starting at `fromIndex` and wrapping. */ -export const nextPendingPageIndex = ( - pages: ReadonlyArray, - fromIndex: number, -): number | undefined => { - for (let offset = 0; offset < pages.length; offset += 1) { - const page = pages[(fromIndex + offset) % pages.length]; - if (page?.image._tag === "Pending") return page.pageIndex; - } - return undefined; -}; diff --git a/packages/pdf-viewer/src/viewer-update.test.ts b/packages/pdf-viewer/src/viewer-update.test.ts index fee62da..2e5176a 100644 --- a/packages/pdf-viewer/src/viewer-update.test.ts +++ b/packages/pdf-viewer/src/viewer-update.test.ts @@ -41,7 +41,7 @@ const loaded = (pages: ReadonlyArray = [page(0), page(1), page(2)]): M }; describe("PDF viewer update", () => { - it("opens a document, then renders its pages one at a time", () => { + it("opens page geometry and lets the mounted viewport request rendering", () => { const requested = update( init({ id: "journey-pdf" }), loadBytes("i-9.pdf", new Uint8Array([37, 80, 68, 70])), @@ -59,7 +59,7 @@ describe("PDF viewer update", () => { }), ); expect(opened.model.document._tag).toBe("Ready"); - expect(commandNames(opened)).toEqual(["RenderPdfViewerPage"]); + expect(commandNames(opened)).toEqual([]); const first = update( opened.model, @@ -69,7 +69,7 @@ describe("PDF viewer update", () => { url: "data:image/png;base64,AA==", }), ); - expect(commandNames(first)).toEqual(["RenderPdfViewerPage"]); + expect(commandNames(first)).toEqual([]); const second = update( first.model, @@ -84,6 +84,55 @@ describe("PDF viewer update", () => { expect(pages.map((item) => item.image._tag)).toEqual(["Rendered", "Failed"]); }); + it("bounds the image cache and evicts outside the visible window first", () => { + let model = { ...loaded(), maxCachedPages: 2 }; + model = update( + model, + Message.ChangedVisiblePages({ revision: model.revision, pageIndexes: [1, 2] }), + ).model; + for (const pageIndex of [0, 1, 2]) + model = update( + model, + Message.CompletedRenderPage({ + revision: model.revision, + pageIndex, + url: `page-${pageIndex}`, + }), + ).model; + expect(model.cachedPageIndexes).toEqual([1, 2]); + expect(model.document._tag === "Ready" && model.document.pages[0]?.image._tag).toBe("Pending"); + const stale = update( + model, + Message.CompletedRenderPage({ revision: model.revision - 1, pageIndex: 0, url: "stale" }), + ); + expect(stale.model).toBe(model); + expect(init({ id: "bounded", maxCachedPages: NaN }).maxCachedPages).toBe(8); + }); + + it("restarts cached pages on remount and surfaces a session failure", () => { + let model = loaded(); + model = update( + model, + Message.CompletedRenderPage({ revision: model.revision, pageIndex: 0, url: "first" }), + ).model; + const restarted = update( + model, + Message.StartedRenderSession({ revision: model.revision }), + ).model; + expect(restarted.cachedPageIndexes).toEqual([]); + expect(restarted.document._tag === "Ready" && restarted.document.pages[0]?.image._tag).toBe( + "Pending", + ); + const failed = update( + restarted, + Message.FailedRenderSession({ revision: model.revision, reason: "Worker unavailable" }), + ).model; + expect(failed.document._tag).toBe("Failed"); + expect( + update(failed, Message.StartedRenderSession({ revision: model.revision - 1 })).model, + ).toBe(failed); + }); + it("ignores results that arrive after a newer document was requested", () => { const first = update(init({ id: "journey-pdf" }), loadBytes("first.pdf", new Uint8Array([1]))); const second = update(first.model, loadBytes("second.pdf", new Uint8Array([2]))); diff --git a/packages/pdf-viewer/src/viewer-update.ts b/packages/pdf-viewer/src/viewer-update.ts index 3b3f7db..4222e3b 100644 --- a/packages/pdf-viewer/src/viewer-update.ts +++ b/packages/pdf-viewer/src/viewer-update.ts @@ -1,12 +1,6 @@ import { Effect, Schema as S } from "effect"; import { Command, type Update } from "foldkit"; -import { - base64ToBytes, - bytesToBase64, - downloadBytes, - readPdfPages, - renderPages, -} from "@foldworks/pdf"; +import { base64ToBytes, bytesToBase64, downloadBytes, readPdfPages } from "@foldworks/pdf"; import { Message } from "./viewer-message"; import { @@ -15,7 +9,6 @@ import { Zoom, clampZoomPercent, effectiveZoomPercent, - nextPendingPageIndex, pagesOf, type Model, type PageInfo, @@ -96,30 +89,6 @@ const LoadBytes = Command.define("LoadPdfViewerBytes", { ), }); -const RenderPage = Command.define("RenderPdfViewerPage", { - args: { revision: S.Number, bytesBase64: S.String, pageIndex: S.Number, scale: S.Number }, - messages: [Message.CompletedRenderPage, Message.FailedRenderPage], - execute: ({ revision, bytesBase64, pageIndex, scale }) => - Effect.suspend(() => - renderPages(base64ToBytes(bytesBase64), { scale, pageIndexes: [pageIndex] }), - ).pipe( - Effect.map(([page]) => - page === undefined - ? Message.FailedRenderPage({ revision, pageIndex, reason: "The page did not render." }) - : Message.CompletedRenderPage({ revision, pageIndex, url: page.imageUrl }), - ), - Effect.catch((error) => - Effect.succeed( - Message.FailedRenderPage({ - revision, - pageIndex, - reason: failureReason(error), - }), - ), - ), - ), -}); - const scrollBehavior = (): ScrollBehavior => window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth"; @@ -180,44 +149,36 @@ const Download = Command.define("DownloadPdfViewerDocument", { const pageLabel = (pageIndex: number, pageCount: number): string => `Page ${pageIndex + 1} of ${pageCount}`; -/** Render the next page still waiting, preferring pages near the one in view. */ -const renderNext = (model: Model): UpdateReturn => { - if (model.document._tag !== "Ready") return { model }; - const pageIndex = nextPendingPageIndex(model.document.pages, model.currentPageIndex); - if (pageIndex === undefined) { - return { - model: { - ...model, - announcement: `${model.document.name} ready, ${model.document.pages.length} page${model.document.pages.length === 1 ? "" : "s"}.`, - }, - }; +/** LRU cache, preferring eviction of pages outside the observer's render window. */ +const replacePage = (model: Model, pageIndex: number, image: PageImage): Model => { + if ( + model.document._tag !== "Ready" || + !model.document.pages.some((page) => page.pageIndex === pageIndex) + ) + return model; + const cached = model.cachedPageIndexes.filter((index) => index !== pageIndex); + if (image._tag !== "Pending") cached.push(pageIndex); + while (cached.length > model.maxCachedPages) { + const outside = cached.findIndex((index) => !model.visiblePageIndexes.includes(index)); + cached.splice(outside < 0 ? 0 : outside, 1); } + const retained = new Set(cached); return { - model, - commands: [ - RenderPage({ - revision: model.revision, - bytesBase64: model.document.bytesBase64, - pageIndex, - scale: model.renderScale, + ...model, + cachedPageIndexes: cached, + document: { + ...model.document, + pages: model.document.pages.map((page): ViewerPage => { + if (page.pageIndex === pageIndex) + return { ...page, image: retained.has(pageIndex) ? image : PageImage.Pending() }; + return page.image._tag !== "Pending" && !retained.has(page.pageIndex) + ? { ...page, image: PageImage.Pending() } + : page; }), - ], + }, }; }; -const replacePage = (model: Model, pageIndex: number, image: PageImage): Model => - model.document._tag !== "Ready" - ? model - : { - ...model, - document: { - ...model.document, - pages: model.document.pages.map( - (page): ViewerPage => (page.pageIndex === pageIndex ? { ...page, image } : page), - ), - }, - }; - const setZoom = (model: Model, zoom: Zoom): UpdateReturn => { const next = { ...model, zoom }; const label = @@ -232,6 +193,8 @@ const startLoad = (model: Model, name: string): Model => ({ document: ViewerDocument.Loading({ name }), revision: model.revision + 1, currentPageIndex: 0, + cachedPageIndexes: [], + visiblePageIndexes: [], announcement: `Opening ${name}.`, }); @@ -248,16 +211,18 @@ export const update = (model: Model, message: Message): UpdateReturn => CompletedLoad: ({ revision, name, bytesBase64, pages }) => revision !== model.revision ? { model } - : renderNext({ - ...model, - document: ViewerDocument.Ready({ - name, - bytesBase64, - pages: pages.map((page) => ({ ...page, image: PageImage.Pending() })), - }), - currentPageIndex: 0, - announcement: `Rendering ${name}.`, - }), + : { + model: { + ...model, + document: ViewerDocument.Ready({ + name, + bytesBase64, + pages: pages.map((page) => ({ ...page, image: PageImage.Pending() })), + }), + currentPageIndex: 0, + announcement: `${name} ready, ${pages.length} pages.`, + }, + }, FailedLoad: ({ revision, name, reason }) => revision !== model.revision ? { model } @@ -271,14 +236,57 @@ export const update = (model: Model, message: Message): UpdateReturn => CompletedRenderPage: ({ revision, pageIndex, url }) => revision !== model.revision ? { model } - : renderNext(replacePage(model, pageIndex, PageImage.Rendered({ url }))), + : { model: replacePage(model, pageIndex, PageImage.Rendered({ url })) }, FailedRenderPage: ({ revision, pageIndex, reason }) => revision !== model.revision ? { model } - : renderNext(replacePage(model, pageIndex, PageImage.Failed({ reason }))), + : { model: replacePage(model, pageIndex, PageImage.Failed({ reason })) }, + StartedRenderSession: ({ revision }) => + revision !== model.revision || model.document._tag !== "Ready" + ? { model } + : { + model: { + ...model, + cachedPageIndexes: [], + document: { + ...model.document, + pages: model.document.pages.map((page) => ({ + ...page, + image: PageImage.Pending(), + })), + }, + }, + }, + FailedRenderSession: ({ revision, reason }) => + revision !== model.revision || model.document._tag !== "Ready" + ? { model } + : { + model: { + ...model, + document: ViewerDocument.Failed({ name: model.document.name, reason }), + cachedPageIndexes: [], + announcement: reason, + }, + }, + ChangedVisiblePages: ({ revision, pageIndexes }) => + revision !== model.revision || model.document._tag !== "Ready" + ? { model } + : { + model: { + ...model, + visiblePageIndexes: [...new Set(pageIndexes)] + .filter((index) => pagesOf(model).some((page) => page.pageIndex === index)) + .slice(0, model.maxCachedPages), + cachedPageIndexes: [ + ...model.cachedPageIndexes.filter((index) => !pageIndexes.includes(index)), + ...model.cachedPageIndexes.filter((index) => pageIndexes.includes(index)), + ], + }, + }, + SelectedText: () => ({ model }), RequestedPage: ({ pageIndex }) => { const pages = pagesOf(model); - if (pages.length === 0) return { model }; + if (pages.length === 0 || !Number.isFinite(pageIndex)) return { model }; const target = Math.min(pages.length - 1, Math.max(0, Math.round(pageIndex))); return { model: { diff --git a/packages/pdf-viewer/src/viewer-view.ts b/packages/pdf-viewer/src/viewer-view.ts index beb22b1..f7a5f78 100644 --- a/packages/pdf-viewer/src/viewer-view.ts +++ b/packages/pdf-viewer/src/viewer-view.ts @@ -13,7 +13,15 @@ import { ZoomOut, } from "@lucide/icons"; import { Button, Empty, Icon, Skeleton } from "@foldworks/ui"; -import { overlayPosition, pageSurface, type PdfRect } from "@foldworks/pdf"; +import { + overlayPosition, + pageSurface, + type PdfRect, + type PdfTextRegion, + type PdfTextSelection, +} from "@foldworks/pdf"; + +import { RenderVisiblePages } from "./render-session"; import { Message } from "./viewer-message"; import { @@ -50,10 +58,25 @@ export type PdfOverlay = Readonly<{ title?: string; }>; +/** A passage may span lines and pages. The first rectangle owns its one keyboard control. */ +export type PdfHighlight = Readonly<{ + key: string; + regions: ReadonlyArray; + label: string; + tone?: PdfOverlayTone; + isSelected?: boolean; + onClick?: ParentMessage; + title?: string; +}>; + export type ViewConfig = Readonly<{ model: Model; toParentMessage: (message: Message) => ParentMessage; overlays?: ReadonlyArray>; + highlights?: ReadonlyArray>; + /** Mount PDF.js' native transparent, selectable text layer. Defaults to false. */ + selectableText?: boolean; + onSelectText?: (selection: PdfTextSelection) => ParentMessage; /** Rendered between the toolbar and the pages, e.g. a "Synthetic preview" alert. */ banner?: Html; /** Text stamped faintly across every page. Decorative; pair it with a banner for screen readers. */ @@ -219,6 +242,57 @@ const overlayView = ( ); }; +const highlightView = ( + page: ViewerPage, + highlight: PdfHighlight, + h: HtmlBuilder, +): Html => { + const first = highlight.regions.find((region) => region.rects.length > 0); + let primary = first?.pageIndex === page.pageIndex; + return h.keyed("span")( + `highlight-${highlight.key}`, + [h.Class("fk-pdf-viewer__highlight-group")], + highlight.regions + .filter((region) => region.pageIndex === page.pageIndex) + .flatMap((region) => + region.rects.map((rect, index) => { + const isPrimary = primary; + primary = false; + const attrs = [ + h.Class("fk-pdf-viewer__overlay fk-pdf-viewer__highlight"), + h.Style(overlayPosition(page, rect)), + h.DataAttribute("pdf-overlay-key", highlight.key), + h.DataAttribute("tone", highlight.tone ?? "warning"), + h.DataAttribute("selected", highlight.isSelected === true ? "true" : "false"), + h.Title(highlight.title ?? highlight.label), + ...(isPrimary ? [h.AriaLabel(highlight.label)] : [h.AriaHidden(true)]), + ]; + return isPrimary && highlight.onClick !== undefined + ? h.button( + [ + ...attrs, + h.Type("button"), + h.AriaPressed(highlight.isSelected === true ? "true" : "false"), + h.OnClick(highlight.onClick), + ], + [], + ) + : h.span( + [ + ...attrs, + ...(isPrimary ? [h.Role("img")] : []), + ...(highlight.onClick === undefined + ? [] + : [h.OnClick(highlight.onClick), h.DataAttribute("clickable", "true")]), + h.DataAttribute("highlight-rect", String(index)), + ], + [], + ); + }), + ), + ); +}; + const pageView = ( config: ViewConfig, name: string, @@ -261,6 +335,17 @@ const pageView = ( config.watermark === undefined ? h.empty : h.span([h.Class("fk-pdf-viewer__watermark"), h.AriaHidden(true)], [config.watermark]), + config.selectableText === true + ? h.div( + [h.Class("fk-pdf-viewer__text-layer"), h.DataAttribute("pdf-text-layer", "true")], + [], + ) + : h.empty, + ...(config.highlights ?? []) + .filter((highlight) => + highlight.regions.some((region) => region.pageIndex === page.pageIndex), + ) + .map((highlight) => highlightView(page, highlight, h)), overlays.length === 0 ? h.empty : h.div( @@ -474,8 +559,25 @@ const contentView = ( const document = model.document; const overlays = config.overlays ?? []; return h.keyed("div")( - `document-${model.revision}`, - [h.Class("fk-pdf-viewer__pages")], + `document-${model.revision}-${model.renderScale}-${model.maxCachedPages}-${config.selectableText === true}`, + [ + h.Class("fk-pdf-viewer__pages"), + h.OnMount( + Mount.mapMessage( + RenderVisiblePages({ + revision: model.revision, + bytesBase64: document.bytesBase64, + scale: model.renderScale, + maxCachedPages: model.maxCachedPages, + selectableText: config.selectableText === true, + }), + (message) => + message._tag === "SelectedText" && config.onSelectText !== undefined + ? config.onSelectText(message.selection) + : config.toParentMessage(message), + ), + ), + ], document.pages.map((page) => pageView( config, @@ -520,7 +622,11 @@ export const view = ( h.AriaBusy( config.model.document._tag === "Loading" || (config.model.document._tag === "Ready" && - config.model.document.pages.some((page) => page.image._tag === "Pending")), + config.model.document.pages.some( + (page) => + config.model.visiblePageIndexes.includes(page.pageIndex) && + page.image._tag === "Pending", + )), ), h.OnMount(Mount.mapMessage(ObserveScroller(), config.toParentMessage)), ], diff --git a/packages/pdf/README.md b/packages/pdf/README.md index 5964a4a..a071c91 100644 --- a/packages/pdf/README.md +++ b/packages/pdf/README.md @@ -79,3 +79,34 @@ directly inside Foldkit Commands. The ready-made [`@foldworks/pdf-viewer`](../pdf-viewer) and [`@foldworks/pdf-annotator`](../pdf-annotator) compose these primitives. + +## Positioned text and quote location + +```ts +import { locateQuote, readPdfText } from "@foldworks/pdf"; + +const positioned = yield * readPdfText(bytes); +const location = locateQuote(positioned, "Employees may bid on schedules."); +if (location._tag === "Found") { + // location.regions is [{ pageIndex, rects }], in PDF user space. +} +``` + +`readPageText(page, pageIndex)` reads one already-open page. `pdfTextItems(content)` +is the pure conversion of PDF.js text items and font metrics into user-space +rectangles; it accounts for rotated baselines. `PdfPageText`, `PdfTextItem`, +`PdfTextRegion`, and `PdfTextSelection` also have Effect schemas. OCR word boxes +can be supplied as `PdfPageText` without any PDF.js extraction. + +`locateQuote(pages, quote)` matches case-sensitive text after normalizing +whitespace, Unicode compatibility forms (including ligatures), and hyphens at +explicit line or page breaks. It returns `Found`, `NotFound`, `EmptyQuote`, or +`Ambiguous`. To resolve a repeated quote deliberately, pass `{ occurrence: 1 }` +for its second occurrence. There is no fuzzy matching. + +Rectangles cover whole PDF.js text items, which may contain several words or a +whole line. They are **not glyph bounds**: a partial-item quote can highlight +extra words. Use a viewer's native text selection for tighter rectangles. +Extraction follows PDF.js item order, not inferred reading order: multi-column, +vertical, or unusually encoded documents may require host-supplied OCR or text +positions. No OCR is performed, and scanned pages have empty item arrays. diff --git a/packages/pdf/src/index.ts b/packages/pdf/src/index.ts index 595d488..0296c51 100644 --- a/packages/pdf/src/index.ts +++ b/packages/pdf/src/index.ts @@ -30,3 +30,14 @@ export { type SurfaceOverlay, } from "./surface"; export { base64ToBytes, bytesToBase64, downloadBytes } from "./bytes"; +export { + PdfTextItem, + PdfPageText, + PdfTextRegion, + PdfTextSelection, + pdfTextItems, + readPageText, + readPdfText, + locateQuote, + type QuoteLocation, +} from "./text"; diff --git a/packages/pdf/src/render.ts b/packages/pdf/src/render.ts index 3698f92..05183d0 100644 --- a/packages/pdf/src/render.ts +++ b/packages/pdf/src/render.ts @@ -42,9 +42,11 @@ export const DEFAULT_RENDER_SCALE = 2; const failureReason = (error: unknown): string => error instanceof Error ? error.message : "The PDF could not be rendered."; -const tryPdf = (run: () => PromiseLike): Effect.Effect => +const tryPdf = ( + run: (signal: AbortSignal) => PromiseLike, +): Effect.Effect => Effect.tryPromise({ - try: async () => run(), + try: async (signal) => run(signal), catch: (cause) => new PdfRenderError({ reason: failureReason(cause), cause }), }); @@ -103,19 +105,28 @@ export const rasterizePage = ( scale: number, imageType: RenderPagesOptions["imageType"] = "image/png", ): Effect.Effect => - tryPdf(async () => { + tryPdf(async (signal) => { const viewport = page.getViewport({ scale }); const canvas = window.document.createElement("canvas"); canvas.width = Math.ceil(viewport.width); canvas.height = Math.ceil(viewport.height); const context = canvas.getContext("2d", { alpha: false }); if (context === null) throw new Error("Canvas rendering is not available."); - await page.render({ canvas, canvasContext: context, viewport }).promise; - return { - imageUrl: canvas.toDataURL(imageType), - pixelWidth: canvas.width, - pixelHeight: canvas.height, - }; + const task = page.render({ canvas, canvasContext: context, viewport }); + const cancel = () => task.cancel(); + signal.addEventListener("abort", cancel, { once: true }); + try { + await task.promise; + return { + imageUrl: canvas.toDataURL(imageType), + pixelWidth: canvas.width, + pixelHeight: canvas.height, + }; + } finally { + signal.removeEventListener("abort", cancel); + canvas.width = 0; + canvas.height = 0; + } }); const allPageIndexes = (document: PDFDocumentProxy): ReadonlyArray => diff --git a/packages/pdf/src/text.test.ts b/packages/pdf/src/text.test.ts new file mode 100644 index 0000000..4a158b5 --- /dev/null +++ b/packages/pdf/src/text.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from "vitest"; +import { locateQuote, pdfTextItems, type PdfPageText } from "./text"; +const box = { x: 10, y: 20, width: 50, height: 10 }; +const pages: readonly PdfPageText[] = [ + { pageIndex: 0, items: [{ text: "An office em-", hasEOL: true, rect: box }] }, + { pageIndex: 1, items: [{ text: "ployee may bid.", hasEOL: false, rect: box }] }, +]; +describe("PDF text", () => { + it("maps a rotated baseline and font ascent to user-space bounds", () => { + const item = { str: "abc", dir: "ltr", width: 30, height: 10, fontName: "f", hasEOL: false }; + expect( + pdfTextItems({ items: [{ ...item, transform: [10, 0, 0, 10, 20, 40] }], styles: {} })[0] + ?.rect, + ).toEqual({ x: 20, y: 38, width: 30, height: 10 }); + expect( + pdfTextItems({ items: [{ ...item, transform: [0, 10, -10, 0, 20, 40] }], styles: {} })[0] + ?.rect, + ).toEqual({ x: 12, y: 40, width: 10, height: 30 }); + }); + it("matches whitespace, ligatures and explicit line-end hyphenation across pages", () => { + expect(locateQuote(pages, "office employee may bid")).toEqual({ + _tag: "Found", + regions: [ + { pageIndex: 0, rects: [box] }, + { pageIndex: 1, rects: [box] }, + ], + }); + expect(locateQuote(pages, "may not bid")).toEqual({ _tag: "NotFound" }); + expect(locateQuote(pages, " \n ")).toEqual({ _tag: "EmptyQuote" }); + }); + it("does not join quotes across omitted or scanned pages", () => { + expect(locateQuote([pages[0]!, { ...pages[1]!, pageIndex: 2 }], "office employee")._tag).toBe( + "NotFound", + ); + expect( + locateQuote( + [pages[0]!, { pageIndex: 1, items: [] }, { ...pages[1]!, pageIndex: 2 }], + "office employee", + )._tag, + ).toBe("NotFound"); + }); + it("requires an occurrence for repeated quotes and preserves ordinary hyphens", () => { + const repeated = [ + { pageIndex: 0, items: [{ text: "full-time full-time", hasEOL: false, rect: box }] }, + ]; + expect(locateQuote(repeated, "full-time")).toEqual({ _tag: "Ambiguous", occurrences: 2 }); + expect(locateQuote(repeated, "full-time", { occurrence: 1 })._tag).toBe("Found"); + expect(locateQuote(repeated, "full-time", { occurrence: -1 })._tag).toBe("NotFound"); + expect(locateQuote(repeated, "fulltime")._tag).toBe("NotFound"); + }); +}); diff --git a/packages/pdf/src/text.ts b/packages/pdf/src/text.ts new file mode 100644 index 0000000..306a683 --- /dev/null +++ b/packages/pdf/src/text.ts @@ -0,0 +1,186 @@ +import { Effect, Schema as S } from "effect"; +import type { PDFPageProxy } from "pdfjs-dist"; +type TextContent = Awaited>; + +import { PdfRect } from "./rect"; +import { getPage, PdfRenderError, withPdfDocument } from "./render"; + +/** One PDF.js text item (or an OCR word box), in PDF user space. */ +export const PdfTextItem = S.Struct({ text: S.String, rect: PdfRect, hasEOL: S.Boolean }); +export type PdfTextItem = typeof PdfTextItem.Type; +export const PdfPageText = S.Struct({ pageIndex: S.Number, items: S.Array(PdfTextItem) }); +export type PdfPageText = typeof PdfPageText.Type; +export const PdfTextRegion = S.Struct({ pageIndex: S.Number, rects: S.Array(PdfRect) }); +export type PdfTextRegion = typeof PdfTextRegion.Type; +export const PdfTextSelection = S.Struct({ text: S.String, regions: S.Array(PdfTextRegion) }); +export type PdfTextSelection = typeof PdfTextSelection.Type; + +/** Pure conversion of PDF.js text positions to item bounds; respects rotated baselines. */ +export const pdfTextItems = ( + content: Pick, +): readonly PdfTextItem[] => + content.items.flatMap((item) => { + if (!("str" in item) || (item.str.length === 0 && !item.hasEOL)) return []; + const [a, b, c, d, x, y] = item.transform as number[]; + if ([a, b, c, d, x, y, item.width, item.height].some((value) => !Number.isFinite(value))) + return []; + const height = Math.hypot(c!, d!) || item.height; + const advance = Math.hypot(a!, b!); + if (height === 0 || advance === 0) return []; + const style = content.styles[item.fontName]; + const ascent = style?.ascent ?? (style?.descent === undefined ? 0.8 : 1 + style.descent); + const ux = a! / advance; + const uy = b! / advance; + const vx = c! / height; + const vy = d! / height; + const width = item.width; + const bottom = (ascent - 1) * height; + const top = ascent * height; + const corners = [ + { x: x! + vx * bottom, y: y! + vy * bottom }, + { x: x! + vx * top, y: y! + vy * top }, + { x: x! + ux * width + vx * bottom, y: y! + uy * width + vy * bottom }, + { x: x! + ux * width + vx * top, y: y! + uy * width + vy * top }, + ]; + const left = Math.min(...corners.map((point) => point.x)); + const low = Math.min(...corners.map((point) => point.y)); + return [ + { + text: item.str, + hasEOL: item.hasEOL, + rect: { + x: left, + y: low, + width: Math.max(...corners.map((point) => point.x)) - left, + height: Math.max(...corners.map((point) => point.y)) - low, + }, + }, + ]; + }); + +export const readPageText = ( + page: PDFPageProxy, + pageIndex: number, +): Effect.Effect => + Effect.tryPromise({ + try: () => page.getTextContent(), + catch: (cause) => new PdfRenderError({ reason: "The page's text could not be read.", cause }), + }).pipe(Effect.map((content) => ({ pageIndex, items: pdfTextItems(content) }))); + +/** Extract positioned text from a PDF. Scanned pages yield empty item arrays; OCR belongs to the host. */ +export const readPdfText = ( + bytes: Uint8Array, +): Effect.Effect => + withPdfDocument(bytes, (document) => + Effect.forEach( + Array.from({ length: document.numPages }, (_, index) => index), + (pageIndex) => + getPage(document, pageIndex).pipe(Effect.flatMap((page) => readPageText(page, pageIndex))), + ), + ); + +type Source = Readonly<{ pageIndex: number; itemIndex: number }>; +type Normalized = Readonly<{ text: string; sources: readonly (Source | undefined)[] }>; +const normalize = (raw: string, sources: readonly (Source | undefined)[]): Normalized => { + let text = ""; + const mapped: Array = []; + for (let index = 0; index < raw.length; ) { + // Only join a hyphen at an explicit line/page break, never ordinary word hyphens. + const hyphen = + raw[index] === "-" || raw[index] === "\u00ad" + ? /^[\-\u00ad][ \t]*\r?\n[\s]*/.exec(raw.slice(index)) + : null; + if ( + hyphen !== null && + /\p{L}/u.test(text.at(-1) ?? "") && + /\p{L}/u.test(raw[index + hyphen[0].length] ?? "") + ) { + index += hyphen[0].length; + continue; + } + const char = String.fromCodePoint(raw.codePointAt(index)!); + const normalized = char.normalize("NFKC"); + for (const value of normalized) { + if (/\s/u.test(value)) { + if (text.length > 0 && !text.endsWith(" ")) { + text += " "; + mapped.push(sources[index]); + } + } else { + text += value; + for (let unit = 0; unit < value.length; unit++) mapped.push(sources[index]); + } + } + index += char.length; + } + if (text.endsWith(" ")) { + text = text.slice(0, -1); + mapped.pop(); + } + return { text, sources: mapped }; +}; + +export type QuoteLocation = + | Readonly<{ _tag: "Found"; regions: readonly PdfTextRegion[] }> + | Readonly<{ _tag: "NotFound" }> + | Readonly<{ _tag: "EmptyQuote" }> + | Readonly<{ _tag: "Ambiguous"; occurrences: number }>; + +/** + * Locate an exact normalized quote, including passages crossing pages. + * Returns whole item boxes, never invented glyph positions. Repeated quotes + * require an explicit zero-based occurrence; otherwise they are ambiguous. + */ +export const locateQuote = ( + pages: readonly PdfPageText[], + quote: string, + options: Readonly<{ occurrence?: number }> = {}, +): QuoteLocation => { + const needle = normalize(quote, []).text; + if (needle.includes("\0")) return { _tag: "NotFound" }; + if (needle.length === 0) return { _tag: "EmptyQuote" }; + let raw = ""; + const sources: Array = []; + const ordered = [...pages].sort((a, b) => a.pageIndex - b.pageIndex); + const add = (value: string, source?: Source) => { + raw += value; + for (let index = 0; index < value.length; index++) sources.push(source); + }; + let previousPage: number | undefined; + for (const page of ordered) { + if (previousPage !== undefined && page.pageIndex !== previousPage + 1) add("\0"); + if (!page.items.some((item) => item.text.trim().length > 0)) add("\0"); + if (raw.length > 0) add("\n"); + previousPage = page.pageIndex; + page.items.forEach((item, itemIndex) => { + add(item.text, { pageIndex: page.pageIndex, itemIndex }); + // PDF.js includes spaces in items; insert a separator only at explicit EOLs. + if (item.hasEOL) add("\n"); + }); + } + const haystack = normalize(raw, sources); + const matches: number[] = []; + for (let at = haystack.text.indexOf(needle); at >= 0; at = haystack.text.indexOf(needle, at + 1)) + matches.push(at); + if (matches.length === 0) return { _tag: "NotFound" }; + if (options.occurrence === undefined && matches.length > 1) + return { _tag: "Ambiguous", occurrences: matches.length }; + const occurrence = options.occurrence ?? 0; + if (!Number.isInteger(occurrence) || occurrence < 0 || matches[occurrence] === undefined) + return { _tag: "NotFound" }; + const at = matches[occurrence]!; + const matched = haystack.sources.slice(at, at + needle.length); + const regions = ordered.flatMap((page) => { + const indexes = new Set( + matched + .filter((source) => source?.pageIndex === page.pageIndex) + .map((source) => source!.itemIndex), + ); + const rects = page.items + .filter((_, index) => indexes.has(index)) + .map((item) => item.rect) + .filter((rect) => rect.width > 0 && rect.height > 0); + return rects.length === 0 ? [] : [{ pageIndex: page.pageIndex, rects }]; + }); + return { _tag: "Found", regions }; +}; diff --git a/packages/text-intelligence/README.md b/packages/text-intelligence/README.md index 66fee7f..c77236a 100644 --- a/packages/text-intelligence/README.md +++ b/packages/text-intelligence/README.md @@ -112,3 +112,51 @@ policy. Unknown mentions produce diagnostics only when `unknownMention(name)` is supplied. Names match NFC and case-insensitively; suggestions also match display label words. See the [shared design and integration contract](../../docs/mentions-and-tags.md) for representation, stale-result rules, and the real outliner/Markdown adapters. + +## Read-only highlighted text + +```ts +import { HighlightedText, snippets } from "@foldworks/text-intelligence"; + +HighlightedText.view( + { + text: transcript, + selectedKey: model.selectedEvidenceId, + ranges: [ + { + key: "evidence-1", + from: 15, + to: 40, + tone: "warning", + label: "Eligibility requirement", + tooltip: "Source page 3", + onClick: Message.SelectedEvidence({ id: "evidence-1" }), + }, + ], + }, + h, +); + +const windows = snippets(transcript, searchHits, 40); +``` + +Ranges use UTF-16 offsets with exclusive ends and unique keys. They are clipped +to the text; invalid, empty, and out-of-bounds ranges are ignored. `segments` +splits overlaps; the narrowest covering range determines the tone and click +message, with input order breaking ties. `data-depth` records overlap depth and +`selectedKey` paints all pieces of a selected range. Tones are `neutral`, `info`, +`success`, `warning`, `danger`, and `muted`. + +Every actionable range has one keyboard stop in reading order, even when it is +fully covered by a narrower range. Enter or Space dispatches its message. +Fully covered controls appear when focused. Tooltip text appears on hover or +focus and is exposed as an accessible description. It is non-interactive; +compose a host-owned popover when the preview contains actions. The view is not +virtualized, so use snippets or a host-managed block window for very long text. + +`snippets(text, ranges, context)` creates keyword-in-context windows, merges +touching windows, and returns source `from`/`to`, `text`, clipped source ranges, +and `leadingEllipsis`/`trailingEllipsis` flags. Ranges remain in original source +coordinates: subtract the window's `from` before passing them to a local view, +or use `segments(window.text, layers, window.from)` directly. Context defaults +to 40 UTF-16 units. Empty hits produce no snippets. diff --git a/packages/text-intelligence/src/highlighted-text.test.ts b/packages/text-intelligence/src/highlighted-text.test.ts new file mode 100644 index 0000000..07b705d --- /dev/null +++ b/packages/text-intelligence/src/highlighted-text.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from "vitest"; +import { inertHtml, type HtmlBuilder } from "foldkit/html"; +import { Scene } from "foldkit/test"; +import { HighlightedText, innermostRange, snippets } from "./highlighted-text"; + +const h = inertHtml as unknown as HtmlBuilder; +describe("highlighted text", () => { + it("chooses the narrowest range deterministically", () => { + const outer = { key: "outer", from: 0, to: 10 }; + const inner = { key: "inner", from: 2, to: 4 }; + expect(innermostRange([outer, inner])?.key).toBe("inner"); + expect(innermostRange([inner, { ...inner, key: "tie" }])?.key).toBe("inner"); + }); + it("keeps overlapping and fully covered ranges accessible", () => { + const html = HighlightedText.view( + { + text: "abcdef", + ranges: [ + { key: "a", from: 0, to: 6, onClick: "a" }, + { key: "b", from: 2, to: 4, onClick: "b", tooltip: "Evidence" }, + { key: "c", from: 2, to: 4, onClick: "c" }, + ], + }, + h, + )!; + expect(Scene.findAll(html, '[data-depth="3"]')).toHaveLength(1); + expect(Scene.findAll(html, '[aria-description="Evidence"]')).toHaveLength(1); + expect(Scene.findAll(html, '[data-highlight-key="c"]')).toHaveLength(1); + expect(Scene.findAll(html, '[tabIndex="0"]')).toHaveLength(2); + expect(Scene.findAll(html, '[tabIndex="-1"]')).toHaveLength(1); + }); + it("merges context windows and retains source offsets", () => { + const text = "0123456789abcdefghijklmnop"; + expect( + snippets( + text, + [ + { from: 5, to: 7 }, + { from: 10, to: 12 }, + ], + 2, + ), + ).toEqual([ + { + from: 3, + to: 14, + text: text.slice(3, 14), + leadingEllipsis: true, + trailingEllipsis: true, + ranges: [ + { from: 5, to: 7 }, + { from: 10, to: 12 }, + ], + }, + ]); + expect( + snippets( + text, + [ + { from: -3, to: 1 }, + { from: 5, to: 5 }, + { from: NaN, to: 3 }, + ], + 0, + )[0]?.text, + ).toBe("0"); + }); +}); diff --git a/packages/text-intelligence/src/highlighted-text.ts b/packages/text-intelligence/src/highlighted-text.ts new file mode 100644 index 0000000..d48ae90 --- /dev/null +++ b/packages/text-intelligence/src/highlighted-text.ts @@ -0,0 +1,187 @@ +import { Option } from "effect"; +import type { Html, HtmlBuilder } from "foldkit/html"; + +import { segments } from "./segments"; +import type { TextRange } from "./vocabulary"; + +export type HighlightTone = "neutral" | "info" | "success" | "warning" | "danger" | "muted"; + +/** UTF-16 offsets into the original text; keys must be unique within a view. */ +export type HighlightRange = TextRange & + Readonly<{ + key: string; + tone?: HighlightTone; + label?: string; + onClick?: Message; + tooltip?: string; + }>; + +export type HighlightedTextConfig = Readonly<{ + text: string; + ranges?: ReadonlyArray>; + selectedKey?: string; + label?: string; +}>; + +const validRanges = (text: string, ranges: ReadonlyArray>) => + ranges + .filter( + (range) => + Number.isFinite(range.from) && + Number.isFinite(range.to) && + range.to > range.from && + range.to > 0 && + range.from < text.length, + ) + .map((range) => ({ + ...range, + from: Math.max(0, Math.floor(range.from)), + to: Math.min(text.length, Math.ceil(range.to)), + })); + +/** Narrowest covering range wins; ties use input order. */ +export const innermostRange = (ranges: ReadonlyArray>) => + ranges.reduce | undefined>( + (best, range) => + best === undefined || range.to - range.from < best.to - best.from ? range : best, + undefined, + ); + +/** Overlap-safe read-only text. Each actionable range has one keyboard stop. */ +export const HighlightedText = { + view: (config: HighlightedTextConfig, h: HtmlBuilder): Html => { + const ranges = validRanges(config.text, config.ranges ?? []); + const pieces = segments(config.text, { ranges }); + const first = new Map(); + for (const piece of pieces) { + const range = innermostRange(piece.covering.ranges); + if (range !== undefined && !first.has(range.key)) first.set(range.key, piece.from); + } + const controls = ranges.filter((range) => range.onClick !== undefined && !first.has(range.key)); + return h.div( + [ + h.Class("fw-highlighted-text"), + ...(config.label === undefined ? [] : [h.AriaLabel(config.label)]), + ], + [ + ...pieces.flatMap((piece) => { + const range = innermostRange(piece.covering.ranges); + const hidden = controls + .filter((control) => Math.max(0, control.from) === piece.from) + .map((control) => + h.button( + [ + h.Type("button"), + h.Class("fw-highlighted-text__hidden-control"), + h.DataAttribute("highlight-key", control.key), + h.OnClick(control.onClick!), + h.AriaLabel( + control.label ?? config.text.slice(Math.max(0, control.from), control.to), + ), + ], + [control.label ?? control.key], + ), + ); + if (range === undefined) return [...hidden, h.span([], [piece.text])]; + const attrs = [ + h.Class("fw-highlighted-text__range"), + h.DataAttribute("highlight-key", range.key), + h.DataAttribute("tone", range.tone ?? "info"), + h.DataAttribute("depth", String(piece.covering.ranges.length)), + h.DataAttribute( + "selected", + piece.covering.ranges.some((item) => item.key === config.selectedKey) + ? "true" + : "false", + ), + ...(range.tooltip === undefined ? [] : [h.AriaDescription(range.tooltip)]), + ]; + const painted = + range.onClick === undefined + ? h.mark(attrs, [piece.text]) + : h.span( + [ + ...attrs, + h.Role("button"), + h.Tabindex(first.get(range.key) === piece.from ? 0 : -1), + h.AriaLabel(range.label ?? config.text.slice(range.from, range.to)), + h.AriaPressed(range.key === config.selectedKey ? "true" : "false"), + h.OnClick(range.onClick), + h.OnKeyDownPreventDefault((key) => + key === "Enter" || key === " " ? Option.some(range.onClick!) : Option.none(), + ), + ], + [piece.text], + ); + return [ + ...hidden, + range.tooltip === undefined + ? painted + : h.span( + [h.Class("fw-highlighted-text__group")], + [ + painted, + h.span( + [ + h.Class("fw-highlighted-text__tooltip"), + h.Role("tooltip"), + h.DataAttribute("text-skip", "true"), + ], + [range.tooltip], + ), + ], + ), + ]; + }), + ], + ); + }, +}; + +export type TextSnippet = Readonly<{ + from: number; + to: number; + text: string; + leadingEllipsis: boolean; + trailingEllipsis: boolean; + /** Clipped ranges, still in original source coordinates (use `segments`' offset). */ + ranges: ReadonlyArray; +}>; + +/** Keyword-in-context windows, merging touching neighbors. Context is in UTF-16 units. */ +export const snippets = ( + text: string, + ranges: ReadonlyArray, + context = 40, +): ReadonlyArray => { + const padding = Number.isFinite(context) ? Math.max(0, Math.floor(context)) : 40; + const clipped = ranges + .filter( + (range) => + Number.isFinite(range.from) && + Number.isFinite(range.to) && + range.to > range.from && + range.from < text.length && + range.to > 0, + ) + .map((range) => ({ + from: Math.max(0, Math.floor(range.from)), + to: Math.min(text.length, Math.ceil(range.to)), + })); + const windows: Array<{ from: number; to: number }> = []; + for (const range of [...clipped].sort((a, b) => a.from - b.from)) { + const from = Math.max(0, range.from - padding); + const to = Math.min(text.length, range.to + padding); + const previous = windows.at(-1); + if (previous !== undefined && from <= previous.to) previous.to = Math.max(previous.to, to); + else windows.push({ from, to }); + } + return windows.map(({ from, to }) => ({ + from, + to, + text: text.slice(from, to), + leadingEllipsis: from > 0, + trailingEllipsis: to < text.length, + ranges: clipped.filter((range) => range.from < to && range.to > from), + })); +}; diff --git a/packages/text-intelligence/src/index.ts b/packages/text-intelligence/src/index.ts index 3fd5c3c..96c6fa6 100644 --- a/packages/text-intelligence/src/index.ts +++ b/packages/text-intelligence/src/index.ts @@ -23,3 +23,13 @@ export { } from "./popup"; export * as Mentions from "./mentions"; + +export { + HighlightedText, + innermostRange, + snippets, + type HighlightedTextConfig, + type HighlightRange, + type HighlightTone, + type TextSnippet, +} from "./highlighted-text"; diff --git a/packages/text-intelligence/src/styles.css b/packages/text-intelligence/src/styles.css index 0993df4..78f4c55 100644 --- a/packages/text-intelligence/src/styles.css +++ b/packages/text-intelligence/src/styles.css @@ -164,3 +164,81 @@ color: var(--muted-foreground, #71717a); font-size: 12px; } + +.fw-highlighted-text { + white-space: pre-wrap; + overflow-wrap: anywhere; + line-height: 1.6; +} +.fw-highlighted-text__range { + --highlight-color: var(--foldworks-ui-info, #2563eb); + background: color-mix(in srgb, var(--highlight-color) 15%, transparent); + color: inherit; + font: inherit; + padding: 0; + border: 0; + border-radius: 2px; + white-space: inherit; + text-align: inherit; + box-decoration-break: clone; +} +button.fw-highlighted-text__range { + cursor: pointer; +} +.fw-highlighted-text__range[data-tone="neutral"], +.fw-highlighted-text__range[data-tone="muted"] { + --highlight-color: var(--muted-foreground); +} +.fw-highlighted-text__range[data-tone="success"] { + --highlight-color: var(--foldworks-ui-success); +} +.fw-highlighted-text__range[data-tone="warning"] { + --highlight-color: var(--foldworks-ui-warning); +} +.fw-highlighted-text__range[data-tone="danger"] { + --highlight-color: var(--foldworks-ui-danger); +} +.fw-highlighted-text__range:not([data-depth="1"]) { + background: color-mix(in srgb, var(--highlight-color) 28%, transparent); +} +.fw-highlighted-text__range[data-selected="true"] { + box-shadow: 0 0 0 1px var(--highlight-color); +} +.fw-highlighted-text__range:focus-visible { + outline: 2px solid var(--ring); + outline-offset: 2px; +} +.fw-highlighted-text__hidden-control:not(:focus) { + position: absolute; + width: 1px; + height: 1px; + clip-path: inset(50%); + overflow: hidden; +} +.fw-highlighted-text__range[role="button"] { + cursor: pointer; +} +.fw-highlighted-text__group { + position: relative; +} +.fw-highlighted-text__tooltip { + display: none; + position: absolute; + z-index: 10; + bottom: 100%; + left: 0; + padding: 4px 8px; + border-radius: var(--radius-sm); + border: 1px solid var(--border); + background: var(--popover); + color: var(--popover-foreground); + font-size: 12px; + white-space: normal; + min-width: 140px; + max-width: 280px; + pointer-events: none; +} +.fw-highlighted-text__group:hover > .fw-highlighted-text__tooltip, +.fw-highlighted-text__group:focus-within > .fw-highlighted-text__tooltip { + display: block; +} diff --git a/packages/ui/README.md b/packages/ui/README.md index 3051d21..6019ebb 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -730,3 +730,39 @@ and browser-test screenshots. controlled desktop workflows. See the [architecture and integration guide](docs/desktop.md) and the **Desktop workspace** example in `/ui-kit`. Command definitions are shared through `@foldworks/keyboard`; application actions remain host-owned. + +## Citations + +`Citation` is an inline superscript or chip with a hover/focus preview and a +source activation message. It uses the existing tooltip model and reducer: + +```ts +const citation = Citation.init({ id: "citation-1", showDelay: 150 }); + +h.submodel({ + slotId: citation.id, + model: citation, + view: Citation.view, + viewInputs: Citation.styledViewInputs( + { + label: 1, + preview: "Employees may bid on schedules.", + onActivate: Message.OpenedSource({ id: "source-1" }), + }, + h, + ), + toParentMessage: (message) => Message.GotCitationMessage({ message }), +}); +``` + +Fold `Citation.update` as a child and handle or discard its tooltip +`Shown`/`Hidden` out-messages. Escape dismisses the preview. `variant: "chip"` +uses baseline alignment; the default is `superscript`. Slots support the same +attributes and StyleX overrides as other components. Previews are application +content and have no citation storage or provenance semantics. + +For interactive previews, use `Citation.popoverViewInputs` with +`Stateful.Popover.init`, `.view`, and `.update`. Its marker opens the preview; +a separate “Open source” button inside dispatches `onActivate`, so opening the +popover and navigating to the source remain distinct actions. Tooltip previews +must contain non-interactive content. diff --git a/packages/ui/src/citation.styles.ts b/packages/ui/src/citation.styles.ts new file mode 100644 index 0000000..d3447c5 --- /dev/null +++ b/packages/ui/src/citation.styles.ts @@ -0,0 +1,24 @@ +import * as stylex from "@stylexjs/stylex"; +import { colors, radii, typography } from "./tokens.stylex.js"; + +export const citationStyles = stylex.create({ + marker: { + appearance: "none", + backgroundColor: colors.infoSurface, + borderWidth: 0, + borderRadius: radii.sm, + color: colors.info, + cursor: "pointer", + display: "inline-flex", + fontFamily: typography.fontFamily, + fontSize: "11px", + fontWeight: 600, + lineHeight: 1.5, + paddingBlock: "0px", + paddingInline: "4px", + outline: { default: "none", ":focus-visible": `2px solid ${colors.focus}` }, + outlineOffset: "2px", + }, + superscript: { verticalAlign: "super", fontSize: "10px", marginInline: "2px" }, + chip: { verticalAlign: "baseline", paddingBlock: "1px", paddingInline: "6px" }, +}); diff --git a/packages/ui/src/citation.test.ts b/packages/ui/src/citation.test.ts new file mode 100644 index 0000000..8e1a725 --- /dev/null +++ b/packages/ui/src/citation.test.ts @@ -0,0 +1,46 @@ +import { inertHtml as h } from "foldkit/html"; +import { Scene } from "foldkit/test"; +import { describe, it } from "vitest"; +import * as Citation from "./citation"; +import * as Tooltip from "./stateful/tooltip"; +import * as Popover from "./stateful/popover"; + +const inputs = Citation.styledViewInputs( + { label: 1, preview: "Employees may bid.", onActivate: undefined as never }, + h, +); +describe("citations", () => { + it("shows its preview on keyboard focus and preserves escape dismissal", () => { + Scene.scene( + { update: Citation.update, view: Scene.withViewInputs(Citation.view, inputs)() }, + Scene.given(Citation.init({ id: "citation-one" })), + Scene.focus(Scene.role("button", { name: "Open citation 1" })), + Scene.Mount.resolve(Tooltip.AnchorTooltip, Tooltip.Message.CompletedAnchorTooltip()), + Scene.expect(Scene.role("tooltip")).toExist(), + Scene.expect(Scene.role("button", { name: "Open citation 1" })).toHaveAttr( + "aria-describedby", + "citation-one-panel", + ), + Scene.keydown(Scene.role("button", { name: "Open citation 1" }), "Escape"), + Scene.Mount.expectEnded(Tooltip.AnchorTooltip), + Scene.expect(Scene.role("tooltip")).not.toExist(), + ); + }); + it("keeps source activation separate from opening an interactive preview", () => { + const inputs = Citation.popoverViewInputs( + { label: "§12", preview: [h.p([], ["Source excerpt"])], onActivate: undefined as never }, + h, + ); + Scene.scene( + { update: Popover.update, view: Scene.withViewInputs(Popover.view, inputs)() }, + Scene.given({ ...Popover.init({ id: "citation-popover" }), isOpen: true }), + Scene.Mount.resolve( + Popover.PortalPopoverBackdrop, + Popover.Message.CompletedPortalPopoverBackdrop(), + ), + Scene.Mount.resolve(Popover.AnchorPopover, Popover.Message.CompletedAnchorPopover()), + Scene.expect(Scene.role("dialog", { name: "Citation §12" })).toExist(), + Scene.expect(Scene.role("button", { name: "Open source" })).toExist(), + ); + }); +}); diff --git a/packages/ui/src/citation.ts b/packages/ui/src/citation.ts new file mode 100644 index 0000000..4d977c3 --- /dev/null +++ b/packages/ui/src/citation.ts @@ -0,0 +1,103 @@ +import type { HtmlBuilder } from "foldkit/html"; +import { citationStyles } from "./citation.styles"; +import { slotAttrs, type Children, type StyledConfig, type WithSlotProps } from "./catalog.shared"; +import * as Tooltip from "./stateful/tooltip"; +import * as Popover from "./stateful/popover"; + +/** Citation previews use the existing tooltip model, messages, and reducer. */ +export { Model, Message, OutMessage, init, update, view } from "./stateful/tooltip"; +export type Slot = Tooltip.Slot; +export type ViewConfig = StyledConfig & + WithSlotProps & + Readonly<{ + label: string | number; + preview: string | Children; + ariaLabel?: string; + variant?: "superscript" | "chip"; + onActivate: Message; + anchor?: Tooltip.AnchorConfig; + }>; + +/** Pass these inputs to `h.submodel({ view: Citation.view, ... })`. */ +export const styledViewInputs = ( + config: ViewConfig, + h: HtmlBuilder, +): Tooltip.ViewInputs => + Tooltip.styledViewInputs( + { + ...config, + trigger: [String(config.label)], + label: config.preview, + ariaLabel: config.ariaLabel ?? `Open citation ${config.label}`, + renderTrigger: (attributes, children) => + h.button( + [ + ...attributes, + h.Type("button"), + h.DataAttribute("citation", String(config.label)), + h.OnClick(config.onActivate), + ...slotAttrs( + config.slotProps?.trigger, + h, + citationStyles.marker, + config.variant === "chip" ? citationStyles.chip : citationStyles.superscript, + ), + ], + children, + ), + }, + h, + ); + +export type PopoverConfig = StyledConfig & + WithSlotProps & + Readonly<{ + label: string | number; + preview: Children; + ariaLabel?: string; + variant?: "superscript" | "chip"; + onActivate: Message; + activateLabel?: string; + anchor?: Popover.AnchorConfig; + }>; + +/** Interactive preview: use Stateful.Popover's model/view; activation is a separate source button. */ +export const popoverViewInputs = ( + config: PopoverConfig, + h: HtmlBuilder, +): Popover.ViewInputs => + Popover.styledViewInputs( + { + ...config, + trigger: [String(config.label)], + ariaLabel: config.ariaLabel ?? `Citation ${config.label}`, + slotProps: { + ...config.slotProps, + trigger: { + ...config.slotProps?.trigger, + attributes: [ + ...(config.slotProps?.trigger?.attributes ?? []), + h.DataAttribute("citation", String(config.label)), + ...slotAttrs( + undefined, + h, + citationStyles.marker, + config.variant === "chip" ? citationStyles.chip : citationStyles.superscript, + ), + ], + }, + }, + content: [ + ...config.preview, + h.button( + [ + h.Type("button"), + h.OnClick(config.onActivate), + ...slotAttrs(undefined, h, citationStyles.marker), + ], + [config.activateLabel ?? "Open source"], + ), + ], + }, + h, + ); diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index b003134..6e68b7c 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -158,3 +158,4 @@ export * as TokenField from "./token-field"; export * as PanelStack from "./panel-stack"; export * as ReadOnlyValue from "./read-only-value"; export * as DateInput from "./date-input"; +export * as Citation from "./citation"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd1407d..251b7a5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -548,6 +548,9 @@ importers: '@lucide/icons': specifier: 'catalog:' version: 1.40.0 + pdfjs-dist: + specifier: 'catalog:' + version: 6.3.289 devDependencies: effect: specifier: 'catalog:' diff --git a/scripts/check-packages.mjs b/scripts/check-packages.mjs index bc627dd..9a67e21 100644 --- a/scripts/check-packages.mjs +++ b/scripts/check-packages.mjs @@ -234,6 +234,10 @@ import "@foldworks/code-editor/styles.css"; import "@foldworks/data-table/styles.css"; import "@foldworks/data-grid/styles.css"; import "@foldworks/pdf-annotator/styles.css"; +import "@foldworks/pdf-viewer/styles.css"; +import "@foldworks/text-intelligence/styles.css"; +import "@foldworks/diff-viewer/styles.css"; +import "@foldworks/diagram/styles.css"; import * as Agent from "@foldworks/agent"; import * as Editor from "@foldworks/editor"; @@ -243,6 +247,11 @@ import * as StructuredEditor from "@foldworks/code-editor/structured"; import * as DataTable from "@foldworks/data-table"; import * as DataGrid from "@foldworks/data-grid"; import * as Diagram from "@foldworks/diagram"; +import * as Graph from "@foldworks/diagram/graph"; +import * as Pdf from "@foldworks/pdf"; +import * as PdfViewer from "@foldworks/pdf-viewer"; +import * as TextIntelligence from "@foldworks/text-intelligence"; +import * as DiffViewer from "@foldworks/diff-viewer"; import * as FormBuilder from "@foldworks/form-builder"; import * as History from "@foldworks/history"; import * as Keyboard from "@foldworks/keyboard"; @@ -254,7 +263,7 @@ import * as UiIcon from "@foldworks/ui/icon"; import * as UiTokens from "@foldworks/ui/tokens.stylex"; import * as Workflow from "@foldworks/workflow"; -const modules = [Agent, Editor, CodeEditor, EditorContracts, StructuredEditor, DataTable, DataGrid, Diagram, FormBuilder, History, Keyboard, PdfAnnotator, QueryBuilder, Sidebar, Ui, UiIcon, UiTokens, Workflow]; +const modules = [Agent, Editor, CodeEditor, EditorContracts, StructuredEditor, DataTable, DataGrid, Diagram, Graph, Pdf, PdfViewer, TextIntelligence, DiffViewer, FormBuilder, History, Keyboard, PdfAnnotator, QueryBuilder, Sidebar, Ui, UiIcon, UiTokens, Workflow]; const app = document.querySelector("#app"); if (app === null) throw new Error("Missing smoke-test mount point."); app.textContent = "Loaded " + modules.reduce((count, module) => count + Object.keys(module).length, 0) + " Foldworks exports"; From 345ee2854d6d025f85d6aed72de5a6e7179f93fe Mon Sep 17 00:00:00 2001 From: Ben Jacobson Date: Tue, 13 Oct 2026 18:12:54 +0000 Subject: [PATCH 2/2] Isolate dialog visual snapshots from application navigation --- .../polaris-dark/dialog-open.png | Bin 98205 -> 11220 bytes .../polaris-light/dialog-open.png | Bin 108000 -> 11213 bytes apps/demo/visual/ui.visual.spec.ts | 6 +++++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/demo/visual/__screenshots__/polaris-dark/dialog-open.png b/apps/demo/visual/__screenshots__/polaris-dark/dialog-open.png index 5236f4243d60ff14edb7072a730cee51fcf01be1..248b585366d83ce38bb3f826e68ecc50a37c3474 100644 GIT binary patch literal 11220 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

(Z zmptfy{U7eg>CvCGN6uq&=W(6UlA{!#k{0K@vNd#iS_v+LhqqkTqq#}dLZ;4JE~l^b z7_jaEYiofj-RJp*?&nnM65KBpm*2|D!tojzg@8>*Bo@k;P|MM+KsCSqy^YNAx?<+E za%wDObUJ3_FlULiL33jM_6`a5zkv?{@_3i!LUs63UY5=$JhN4WLo-m!deBOQE42W$ zVE?}vYx$q&G+ja^0iLCQ@!|z1CnqcG$itHl%&{(h2?+@xvgzsQH0mNHKA#K+S=O7OE*TIn8y>NfMc|x)78b>V(1oOSRxiLylRC!S@7TW*p zI6OwDi`SY6H`{~~)-8$@R)o89Oh84YfWS%IoS7D(Pw0RIt8@$m3*nI3DuacIDGM5> z7ol}~TL!%1It!f6PC!ACk(PEHZEGEXg3af=1sEB!qb;B_6Ga+7Km39=>NWd-&g``o z`vz7b8mS(d30F^uf5{kFI}jKcc)Kc&l*VHx1QHq$C~r_MAE%2z!U9b|x5idyN7*lE z%ECQLKm*cYLeId!u%nLn?;LPp8_-BQiS;;}UMSxHgUNYkVh=cxABX-6c~Fu7r=A9L zL**v@J*Iy=Pq*GR`*=@GFc=@N^@7exTLvfqlV)Wt@Y`K?r~K?e*7q1owfN&pQXc&1=FsK#t(U@_ZuDdU&wcV)YVBb2YX&467$)?fhVB|Iuj2lpFJ zh5!~i1QQ>M3JQ-jUm7$uy0%8Z=I*n>S&3@8Vd3F@Z78sT>5}HPUJM8`2N>tjh$zyS z7IxW9Nu&dPkROa1<%kA+CL(^V`VFYp(i~-~6Gh>~EC5j)0oy)pplAQlk^*jdwH{Cb z;7%Ky7JmWiu?pRnKiR(_xsF zD0n|QKXfYc*en@CmxPBpfRokL_ZP@F`v748)At!UH>I$ItL?PV8Oxx{=y5t9T9()$fB1OR07CSDOyqxA_(N9%}XxpsS&qv(1A42 z(9v5nIiu~*G)h_N>AS{~KjB>^{ukys+J1gb#gghN6_c>n^pAnhdKVoXJv^;;$AUeT zw5sMK^Z2~kQ&Z}JyU<0^CAw0KSx1V&$JAvOwy?0M4M)a!Uh4HMuMC#3e#!Rvv4Sb; zYj=XFK#e8|b#--1%YU5$^4kC9KQN*2o72%~9@E9rLtUS?CQC+?bDU+5uFUGAbK^&?N`K+&hZUWDL$aHA-j>kV2 zOn%A9$N=D6>-M#uK9qmKN?)G_(U54xQF5KGTnsI{y@8zKkU?W^(@0qv-#=o;^loRC zLiPC)f&rMLpl8PgdI{BaqLxqP<2~y;_(bkQW74?Td{H0kE@I{E60$y4y zS9u89$p2yVq(c}EpXSM4W&kQBr!?b}9cpUogBunffcF@2@(KOUggwu#MMOS--+{)2v+Wjx=gb_FQYa@5VR$wPBB8PCMQQAW$n literal 98205 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≈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 P$j(VCoK8D+`R@M${#Cn! diff --git a/apps/demo/visual/ui.visual.spec.ts b/apps/demo/visual/ui.visual.spec.ts index 296435b..4f94cb5 100644 --- a/apps/demo/visual/ui.visual.spec.ts +++ b/apps/demo/visual/ui.visual.spec.ts @@ -133,7 +133,11 @@ for (const mode of ["Light", "Dark"] as const) { await page.getByRole("button", { name: "Dialog", exact: true }).click(); const dialog = page.getByRole("dialog", { name: "Dialog example" }); await expect(dialog).toBeVisible(); - await expect(dialog).toHaveScreenshot([`polaris-${mode.toLowerCase()}`, "dialog-open.png"]); + // The native

fills the viewport, including the application behind + // its backdrop. Compare the component panel so unrelated navigation changes + // do not invalidate this fixture. + const panel = dialog.locator("section[data-size]"); + await expect(panel).toHaveScreenshot([`polaris-${mode.toLowerCase()}`, "dialog-open.png"]); }); test("open dropdown menu matches its approved image", async ({ page }) => {