Repository navigation
Commit a194073
UN-4503 [FEAT] Add the v1 DocumentViewer for PDFs, images and text, with highlights, toolbar and zoom (#2405)
* feat(frontend): v1 scaffold, strict TypeScript and import boundary (UN-4270)
Adds frontend/src/v1/ as an isolated tree for the v1 frontend (P0-T01):
- Folder taxonomy from the spec, a public entry (src/v1/index.ts, imported
by v0 as `@v1`) and a cloud overlay placeholder.
- src/v1/tsconfig.json: strict, no JS, unused locals/params are errors,
`@v1` alias only. v0's tsconfig/jsconfig exclude src/v1.
- Biome overrides for the two-way import boundary and v1 bans (no `../`,
`@/`, prop-types, antd, component CSS; noExplicitAny).
- scripts/check-v1.mjs for what Biome cannot express: TypeScript-only
files, the folder taxonomy and import contract, PropTypes, `ant-`.
- typecheck:v1, lint:v1 and test:v1 scripts; v0 scripts are unchanged.
- test-fixtures/v1-boundary/ proves each rule fails and passes as
specified, run with node --test so the v0 vitest suite never sees it.
- .gitignore re-includes src/v1/lib/ (the Python `lib/` rule matched it).
v0 build output, test counts, typecheck and Biome results are unchanged.
* fix(frontend): close v1 guardrail bypasses found in self-review (UN-4270)
check-v1.mjs:
- Scan comments with a string/template-aware tokenizer; a `/*` or `//`
inside a string or line comment no longer hides the imports after it.
- Read template-literal specifiers, import.meta.glob and `import(..., opts)`.
- Flag specifiers that leave src/v1 (`/`, `..`, `@/`, `src/`) itself, not
only via Biome, which skips template literals.
- Check `@import`/`url()` and `ant-` in styles/ CSS; report symlinks;
catch built `ant-` names; treat .test/.spec .mts/.cts as test files;
ignore .DS_Store; drop the unused --root option.
biome.json: also ban `..`, `./../**`, `/**`, `antd/**`, `prop-types/**`.
tsconfig: skipLibCheck off so v1's own .d.ts files are checked.
.gitignore: re-include lib/ at any depth under src/v1.
Fixtures: a generated matrix over every folder-contract pair, every import
form, every outside spelling, a symlink case, and cases for each new rule;
tsc cases assert error codes; output matching excludes the command text and
spawn errors fail loudly. Comment and README corrections.
* feat(frontend): run v1 checks in CI and tighten what pages may import (UN-4270)
- ci-frontend-lint: the typecheck job also runs `typecheck:v1` and
`lint:v1`, so the strict v1 tsconfig and the scripts/check-v1.mjs checks
(folder contract, TypeScript-only files, PropTypes, `ant-`) gate PRs now
rather than from P0-T10.
- Pages may no longer import app/, test/ or the v1 entry, only the shared
folders (components, shell, services, stores, hooks, lib, styles,
plugins). Test files in a page may still import test/. Contract table,
README and the fixture matrix updated together.
* refactor(frontend): address SonarCloud findings on the v1 guardrails (UN-4270)
check-v1.mjs: split the comment scanner into small helpers (cognitive
complexity 40 -> under 15), drop the nested ternary and nested template
literal, use String.raw for the import regexes, keep CLI flags in a Set.
Fixture runner:
- Cases and the expected folder contract are parsed text tables instead of
repeated object literals (the 10% duplication).
- Sorts use an explicit localeCompare comparator.
- The contract matrix is built by a helper.
- Spawns /bin/sh by absolute path and no longer needs `git init` (Biome only
needs the ignore file). The git check-ignore call keeps a PATH lookup, with
the reason and a NOSONAR marker.
Fixture cases: re-exports rewritten as small functions and read-only props,
without changing what each case exercises.
* refactor(frontend): split the contract-matrix builder to cut its complexity (UN-4270)
* fix(frontend): check every glob pattern and v0 -> v1 imports Biome cannot see (UN-4270)
Addresses Greptile review on #2380:
- import.meta.glob([...]): every pattern in the array is checked, not only
the first, so an allowed first pattern no longer hides a forbidden one.
- check-v1 --code also scans v0 (src/ outside v1, including the cloud
src/plugins overlay) for template-literal imports, globs, require(),
@/v1, src/v1, relative paths and CSS @import that reach into v1 other
than through `@v1`. Biome still covers plain imports.
Fixtures: Greptile's example as a case, a glob array with a forbidden later
pattern, and a generated test of every v0 -> v1 form (denied and allowed).
The v0 scan reports nothing on the current tree.
* UN-4299 [MISC] Turn on skipLibCheck for v1: vitest 3.2 and @types/chai 5.2 both declare containSubset
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UN-4299 [FEAT] Add the v1 theme model: three modes, follow-system and per-browser preference
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UN-4299 [FEAT] Add the v1 portal container and the theme preference store
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UN-4299 [FEAT] Add the v1 token source, the generator and tokens.css in three modes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UN-4299 [FIX] Prefix the spacing utilities in the token table so they match the v1 Tailwind root
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UN-4299 [FEAT] Add cn() configured for the v1 utility prefix
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UN-4299 [FIX] Pin the v1 prefix in cn() with a test that fails without it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UN-4299 [FIX] Use "ui" as the v1 utility prefix: Tailwind accepts lowercase letters only
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UN-4299 [FEAT] Add the prefixed v1 Tailwind root with fonts, modes, theme mapping and the focus utility
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UN-4299 [FIX] Exclude the generated tokens.css from Biome formatting so biome ci passes
src/v1/styles/tokens.css is written by scripts/build-v1-tokens.mjs and checked byte-for-byte by styles/tokens.test.ts; the formatter would rewrap it and break the check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UN-4299 [FIX] Sort the imports in cn.ts so biome ci passes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UN-4299 [FIX] Make the root compile test fail on unscoped selector lists and on missing variants
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UN-4299 [FEAT] Add ThemeProvider: the v1 root, follow-system binding and portal sync
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UN-4299 [FIX] Apply the stored theme before first paint with useLayoutEffect
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UN-4299 [FEAT] Enforce the v1 styling policy: raw colours, pixels, inline styles, CSS imports and !important fail lint
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UN-4299 [FIX] Scope the styling lint to class strings and cover vh/vw arbitrary values
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UN-4299 [FIX] Keep v0's Tailwind scan out of src/v1, the lint scripts and the fixtures
v0's root auto-detected sources across the whole frontend, so the v1 tree, scripts/check-v1-styles.mjs and test-fixtures/ added 12 unprefixed utilities and 8 theme variables to v0's stylesheet. With these three @source not lines the v0 build is byte-identical to the base again (239/239 files).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UN-4299 [FIX] Teach cn the type roles so a role and a text colour no longer cancel each other
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UN-4299 [FIX] Reject colour literals outside class strings, named-colour arbitrary values and !important after a variant
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UN-4299 [FIX] Keep the focus ring visible in forced-colors mode and scope variants, shadows and the root fill to v1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UN-4299 [FIX] Create the portal container before the mode changes so the theme test exercises the mode effect
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UN-4299 [FIX] Document the v0 rules that reach v1, the focus-ring usage, the pixel escape and the current unprefixed-class rationale
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UN-4299 [FIX] Name the theme storage key in the v1 README
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UN-4299 [MISC] Name the three theme modes in the v1 README
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* UN-4502 [FEAT] Add the v1 PDF loader, usePdfDocument hook and viewer states
- lib/pdf/pdfjs.ts is the only v1 import of pdf.js: the existing
pdfjs-dist 3.11.174 legacy build, its worker bundled with ?url, both
loaded on first use. getDocument always gets isEvalSupported: false
(CVE-2024-4367), and in-memory bytes are copied before the worker
transfer empties them.
- Page sizes are read in parallel, bounded at 16 in flight.
- hooks/usePdfDocument tracks empty, loading, password (missing or
wrong), unsupported, failed (with Retry) and ready. It reopens only
when the source's URL, request options or bytes change, and destroys
the previous load on change and on unmount.
- components/patterns/document-viewer/ViewerStatus renders every state
except ready.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4503 [FEAT] Render v1 PDF pages in a windowed, scrolling list
- PdfPageList lays every page out at its own size from the page sizes
read at load, so the scrollbar is right from the start, and gives a
canvas only to the pages in view plus two on each side.
- usePageWindow tracks pages in view with one IntersectionObserver on
the scroll container; lib/pdf/windowing holds the window, pixel ratio
and CSS size arithmetic.
- PdfPage renders a frame after it enters the window, so a fast scroll
draws nothing for pages it passes. Leaving the window or a zoom change
cancels the render in flight, cleans up the page and zeroes the
canvas. The pixel ratio is capped at 2 and lowered further for pages
that would exceed the largest canvas Safari allocates.
- Page geometry is set as custom properties read by ui: classes, since
v1 bans the style attribute.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4299 [FEAT] Let a v1 style attribute set CSS custom properties only
Runtime geometry (a page's aspect ratio, a highlight's position) cannot be
a token or a class. Rule S1 now accepts a style attribute whose value is an
object literal with only "--name" keys, so the numbers travel through custom
properties that ui: classes read (ui:aspect-(--page-aspect)); a string, a
variable, a spread or a plain key still fails. Three fixtures pin it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* UN-4299 [FIX] Make check-v1-styles.mjs executable to match its shebang
pre-commit's check-shebang-scripts-are-executable fails on branches stacked
on this one; check-v1.mjs already carries the bit.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* UN-4503 [FEAT] Size v1 PDF pages through style custom properties
S1 now allows a style attribute whose keys are all custom properties, so
the page size is set directly instead of through a ref and layout effect.
React's CSSProperties gains a --* index signature so this typechecks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4503 [FEAT] Format PdfPage imports
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4299 [FIX] Type the custom-property keys a v1 style attribute may set
React's CSSProperties has no index signature for "--*" keys, so the object
literal rule S1 allows fails TS2353 under strict mode. Augment it under
lib/, and typecheck the passing fixture so the augmentation is exercised.
Raised by Ali on the viewer stack.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* UN-4503 [FEAT] Clean up late pages and expose page contents to assistive tech
- A page whose getPage resolves after it left the render window is now
cleaned up; the effect cleanup had already run with no page to free.
- Pages are figures instead of role="img", whose children are
presentational and would hide the highlight buttons.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4502 Keep signed URLs out of load errors and test the loader retry
pdf.js puts the full URL in MissingPDFException and
UnexpectedResponseException messages, so a signed URL's token reached the
screen. Build those messages from the status instead. Also cover the
reset that lets Retry load pdf.js again after a failed chunk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4503 Test that a failed page render ends in the error state
Covers a render task that rejects with a real error and a canvas with no
2D context, so a regression that leaves the page in rendering fails.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4503 Say when a page failed to render and offer Retry
A page whose render failed was left blank, so it could not be told from
an empty page. It now says it didn't load, with a Retry for that page.
Raised by Greptile on #2405.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4502 Put the focus back in the password field after a wrong password
Submitting swaps the prompt for the loading state, so the field lost focus
and a keyboard user had to find it again. Raised by Greptile on #2404.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4502 Answer passwords on the same load, keep damaged pages from failing the open
From review on #2404:
- A password goes to pdf.js's onPassword callback on the live loading task,
so a password (right or wrong) no longer downloads the file again or starts
a new worker, and Retry carries no stale password.
- A page whose size cannot be read borrows a neighbour's size and is logged;
only a document with no readable page fails to open.
- An unknown pdf.js error shows a plain message and is logged, instead of
showing pdf.js's own wording.
- destroy() handles the rejection pdf.js gives when the worker is gone.
- The hook's request type is OpenRequest, not a shadow of fetch's Request.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4503 Keep the picture through a zoom, follow pixel-ratio changes, let pages stay memoised
From review on #2405:
- A page draws off screen and copies the result in one step, so a zoom
change keeps the old picture up instead of blanking every page; the
canvas is still zeroed when the page leaves the window or unmounts.
- useDevicePixelRatio follows a move to another screen or a browser zoom,
and a change redraws the pages.
- PdfPage calls renderOverlay itself, so a window change re-renders only
the pages that entered or left it.
- A page is 'rendering' from the moment it asks for the page, and a failed
render is logged with its page number.
- usePageWindow skips an element without data-page-index instead of
reading it as page 0.
- isRenderCancelled and pdfErrorName live in lib/pdf/pdfjs.ts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4502 Load the same pdf.js build and worker as v0, not the legacy build
The legacy build put a second pdf.js engine and worker next to v0's. v1 now
imports pdfjs-dist and its default worker, the copy v0 already ships.
Also hands the rejected destroy promise in the loader test a handler up front,
so Vitest no longer reports it as an unhandled rejection while the test waits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4504 [DEV] Add v1 PDF highlight layer and highlight normalisers (#2407)
* UN-4504 [FEAT] Add v1 PDF highlight layer and highlight normalisers
- lib/pdf/highlights turns every producer's shape into one Highlight
{id, page, x, y, w, h, variant, tone, confidence?}, in fractions of
the page box, so highlights hold their place at any zoom:
- LLMWhisperer line_metadata lines, with line confidence read by type
- word boxes, both as a separate list (confidence_metadata) and as the
5th element the agentic table worker sends with page_width; without
page_width, x comes from the PDF page's aspect ratio rather than
v0's division by page height
- Agentic Prompt Studio percentage boxes
Padding, zero-size and junk entries are dropped, and boxes are clamped
to the page. A box spanning the page reads as a line.
- HighlightLayer draws a page's highlights in default, active and
low-confidence tones, as buttons reporting onHighlightClick when a
handler is given.
- PdfPageList takes highlights, activeHighlightId and onHighlightClick
and hands each memoised page only its own highlights.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4504 [FEAT] Place v1 highlights through style custom properties
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4504 [FIX] Address review: unique word ids, word-only tone, painted-only blend, per-page active id
- normaliseWords puts the line's page and position in the default id, so
words on different lines no longer share ids.
- Line entries keep their confidence for the label but are not toned; v0
never toned lines, and line confidence on scans sits below 0.9.
- Warn with the page index when word boxes are dropped for want of a page
width.
- Highlights use mix-blend-multiply only once the page canvas holds a
picture, so they stay visible on the dark placeholder.
- Only the page holding the active highlight receives its id, so moving it
re-renders two pages, not all of them.
- HighlightLayer builds the style and data props once for both branches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* UN-4505 [DEV] Add the v1 DocumentViewer with toolbar, zoom, keys and scroll handle (#2408)
* UN-4504 [FEAT] Add v1 PDF highlight layer and highlight normalisers
- lib/pdf/highlights turns every producer's shape into one Highlight
{id, page, x, y, w, h, variant, tone, confidence?}, in fractions of
the page box, so highlights hold their place at any zoom:
- LLMWhisperer line_metadata lines, with line confidence read by type
- word boxes, both as a separate list (confidence_metadata) and as the
5th element the agentic table worker sends with page_width; without
page_width, x comes from the PDF page's aspect ratio rather than
v0's division by page height
- Agentic Prompt Studio percentage boxes
Padding, zero-size and junk entries are dropped, and boxes are clamped
to the page. A box spanning the page reads as a line.
- HighlightLayer draws a page's highlights in default, active and
low-confidence tones, as buttons reporting onHighlightClick when a
handler is given.
- PdfPageList takes highlights, activeHighlightId and onHighlightClick
and hands each memoised page only its own highlights.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4505 [FEAT] Add the v1 DocumentViewer with toolbar, zoom, keys and scroll handle
- DocumentViewer composes the loader, status screens, toolbar and page
list: <DocumentViewer source highlights activeHighlightId
onHighlightClick initialZoom ref />, with scrollToPage and
scrollToHighlight on the ref. A scroll asked for while loading runs
once the pages are laid out, and the active highlight is scrolled into
view when it changes.
- ViewerToolbar: previous/next, a page box (n / N, Enter to jump), zoom
-/+, the zoom level, fit width and fit page, each a labelled button
reachable with Tab.
- Zoom: fit width by default, fitted from the viewer's size with a
ResizeObserver; -/+ step through 25%-400% from wherever a fit left
it. A zoom change keeps the same spot of the same page in view.
- Keys while the pages have focus: PageUp/PageDown, Home/End, + and -;
modified keys are left to the browser.
- PdfPageList scrolls by the pages' laid-out offsets, which are exact
before any page renders, and reports the page showing most of itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* UN-4504 [FEAT] Place v1 highlights through style custom properties
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4505 [FEAT] Keep fit-page steady and the page draft while scrolling
- Fit page fits every page instead of the current one. With mixed page
sizes, following the current page rescaled the whole document each time
scrolling crossed into a page of another size.
- The page box keeps a half-typed number and its focus when the current
page changes; it follows the current page only while not being edited.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4505 [FEAT] Drop a stale comment about remounting the page box
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4505 Read the current page from the layout and tie waiting scrolls to their source
The current page came from intersection ratios reported at quarter steps,
so for a page more than four times the viewer's height it was arbitrary,
and after a jump it lagged a frame: the toolbar showed the wrong page,
PageDown could skip one, and the zoom anchor was recorded against the old
page. Both now come from the laid-out page offsets, on scroll and straight
after a jump.
A scroll waiting for a document is dropped when another source is shown,
and a highlight scroll whose highlight is not in the list yet waits and is
tried again when the highlights change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4505 Fix the end-of-document page and keep waiting scrolls made at commit
Round two of the review:
- the last page is current once the end is reached, so Next and PageDown
no longer stick when a short last page or two whole pages show
- the page in view is measured in the content's own coordinates, with one
query per scroll, and again when the viewer changes height
- a waiting scroll is stamped with its source and dropped only at flush
time, so one made in a parent's layout effect on mount survives
- the active highlight is re-armed for each new source, and clearing it
drops only a request it made
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4505 Keep the page jumped to current while the view stays there
When the last few pages fit together, a jump to one of them is clamped at
the end of the scroll and the end rule reported the last page again, so
Previous and PageUp could not leave it. The page jumped to now stays
current until the reader scrolls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4505 Track the current page by share shown, centre highlights exactly, clamp a starting zoom
- The current page is the one showing the largest share of itself, a page
taller than the view counting the view as whole, so part of a tall page
no longer beats a short page shown whole.
- scrollToHighlight centres on the page's own top, padding included; page
jumps keep the gap above the page.
- A numeric initialZoom goes through clampZoom like a fitted one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4504 [FIX] Address review: unique word ids, word-only tone, painted-only blend, per-page active id
- normaliseWords puts the line's page and position in the default id, so
words on different lines no longer share ids.
- Line entries keep their confidence for the label but are not toned; v0
never toned lines, and line confidence on scans sits below 0.9.
- Warn with the page index when word boxes are dropped for want of a page
width.
- Highlights use mix-blend-multiply only once the page canvas holds a
picture, so they stay visible on the dark placeholder.
- Only the page holding the active highlight receives its id, so moving it
re-renders two pages, not all of them.
- HighlightLayer builds the style and data props once for both branches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4505 [FIX] Address review: keys in fields, measured scrollbar, page reset, deferred fit layout
- viewerKeyAction leaves keys typed into an input, textarea, select or
contenteditable to that field.
- Fit modes measure the platform scrollbar once (lib/scrollbar) instead of
assuming 16 px, so a 17 px classic scrollbar no longer forces a
horizontal scrollbar.
- The page index resets while rendering when the document changes, so the
toolbar never shows the last document's page against the new count.
- In a fit mode the page list waits for the viewer's size, so pages are not
laid out at 100% first and again at the fitted zoom.
- sync() reads the page elements once per list, not on every scroll.
- Document identity uses objectKey from lib/pdf/pdfjs, shared with
pdfSourceKey.
- useElementSize measures once where ResizeObserver is missing, matching
the guard in PdfPageList.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4505 [FIX] Drop a waiting active-highlight scroll once the highlight is no longer active
The retry effect runs before the effect that cancels a cleared selection,
so a render that both delivers the highlight and clears it scrolled to a
selection the reader had just cleared.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4505 Drop a waiting scroll as soon as the source changes
A scroll queued for document A survived a switch to B and back to A while
neither had opened, because the retry effect only clears a stale request once
pages are shown; by then the key matched A again. It is now dropped on the
source change itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4505 Restore a typed page that is not a whole number
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
* UN-4503 Follow a highlight shown after a page jump
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4506 [DEV] Show text and images in the v1 DocumentViewer, with a switcher slot (#2409)
* UN-4504 [FEAT] Add v1 PDF highlight layer and highlight normalisers
- lib/pdf/highlights turns every producer's shape into one Highlight
{id, page, x, y, w, h, variant, tone, confidence?}, in fractions of
the page box, so highlights hold their place at any zoom:
- LLMWhisperer line_metadata lines, with line confidence read by type
- word boxes, both as a separate list (confidence_metadata) and as the
5th element the agentic table worker sends with page_width; without
page_width, x comes from the PDF page's aspect ratio rather than
v0's division by page height
- Agentic Prompt Studio percentage boxes
Padding, zero-size and junk entries are dropped, and boxes are clamped
to the page. A box spanning the page reads as a line.
- HighlightLayer draws a page's highlights in default, active and
low-confidence tones, as buttons reporting onHighlightClick when a
handler is given.
- PdfPageList takes highlights, activeHighlightId and onHighlightClick
and hands each memoised page only its own highlights.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4505 [FEAT] Add the v1 DocumentViewer with toolbar, zoom, keys and scroll handle
- DocumentViewer composes the loader, status screens, toolbar and page
list: <DocumentViewer source highlights activeHighlightId
onHighlightClick initialZoom ref />, with scrollToPage and
scrollToHighlight on the ref. A scroll asked for while loading runs
once the pages are laid out, and the active highlight is scrolled into
view when it changes.
- ViewerToolbar: previous/next, a page box (n / N, Enter to jump), zoom
-/+, the zoom level, fit width and fit page, each a labelled button
reachable with Tab.
- Zoom: fit width by default, fitted from the viewer's size with a
ResizeObserver; -/+ step through 25%-400% from wherever a fit left
it. A zoom change keeps the same spot of the same page in view.
- Keys while the pages have focus: PageUp/PageDown, Home/End, + and -;
modified keys are left to the browser.
- PdfPageList scrolls by the pages' laid-out offsets, which are exact
before any page renders, and reports the page showing most of itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* UN-4506 [FEAT] Show text and images in the v1 DocumentViewer, with a switcher slot
- DocumentViewer takes any DocumentSource: a PDF (URL or bytes), an
image ({kind: "image", url}) or text ({kind: "text", text}).
- TextView shows text line by line with line numbers, keeping the
layout's spacing, and highlights whole lines (0-based, as LLMWhisperer
numbers them) in default, active and low-confidence tones; highlighted
lines report clicks and can be scrolled to.
- ImageView shows an image as a one-page document: the toolbar's zoom
and fits apply (zoom 1 is its natural size), page highlights draw over
it, and a failed load offers Retry.
- The toolbar gains a leading slot for the document switcher, shown in
every state so a failed document can be switched away from; paging
shows only for PDFs and zoom only for PDFs and images.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4504 [FEAT] Place v1 highlights through style custom properties
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4506 [FEAT] Size the v1 image view through style custom properties
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4505 [FEAT] Keep fit-page steady and the page draft while scrolling
- Fit page fits every page instead of the current one. With mixed page
sizes, following the current page rescaled the whole document each time
scrolling crossed into a page of another size.
- The page box keeps a half-typed number and its focus when the current
page changes; it follows the current page only while not being edited.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4505 [FEAT] Drop a stale comment about remounting the page box
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4506 [FEAT] Fix keys, scroll timing and highlights for text and images
- Paging keys on text and images keep the browser's own scrolling; they
used to be swallowed and jump to the top. Text ignores zoom keys.
- Scroll requests wait until an image has loaded, not just until its view
has mounted, so an early highlight scroll lands once the image is sized.
- Returning to an image shown earlier waits for its reload instead of
reusing the old size.
- The image view draws only page 0's highlights.
- Text rows are memoised, so moving the active highlight re-renders only
the lines involved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4505 Read the current page from the layout and tie waiting scrolls to their source
The current page came from intersection ratios reported at quarter steps,
so for a page more than four times the viewer's height it was arbitrary,
and after a jump it lagged a frame: the toolbar showed the wrong page,
PageDown could skip one, and the zoom anchor was recorded against the old
page. Both now come from the laid-out page offsets, on scroll and straight
after a jump.
A scroll waiting for a document is dropped when another source is shown,
and a highlight scroll whose highlight is not in the list yet waits and is
tried again when the highlights change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4505 Fix the end-of-document page and keep waiting scrolls made at commit
Round two of the review:
- the last page is current once the end is reached, so Next and PageDown
no longer stick when a short last page or two whole pages show
- the page in view is measured in the content's own coordinates, with one
query per scroll, and again when the viewer changes height
- a waiting scroll is stamped with its source and dropped only at flush
time, so one made in a parent's layout effect on mount survives
- the active highlight is re-armed for each new source, and clearing it
drops only a request it made
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4505 Keep the page jumped to current while the view stays there
When the last few pages fit together, a jump to one of them is clamped at
the end of the scroll and the end rule reported the last page again, so
Previous and PageUp could not leave it. The page jumped to now stays
current until the reader scrolls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4506 Give every text row one line-number gutter
Each row sized its own number column, so line 10 started further right than
line 9 and the text lost its columns. The gutter is now as wide as the
longest line number, shared through a custom property. Raised by Greptile
on #2409.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4505 Track the current page by share shown, centre highlights exactly, clamp a starting zoom
- The current page is the one showing the largest share of itself, a page
taller than the view counting the view as whole, so part of a tall page
no longer beats a short page shown whole.
- scrollToHighlight centres on the page's own top, padding included; page
jumps keep the gap above the page.
- A numeric initialZoom goes through clampZoom like a fitted one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4506 Keep the horizontal place when jumping to a line on an image
Only a box highlight moves the image view across, as on a PDF page; a line
spans the image and keeps the reader's horizontal scroll.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4504 [FIX] Address review: unique word ids, word-only tone, painted-only blend, per-page active id
- normaliseWords puts the line's page and position in the default id, so
words on different lines no longer share ids.
- Line entries keep their confidence for the label but are not toned; v0
never toned lines, and line confidence on scans sits below 0.9.
- Warn with the page index when word boxes are dropped for want of a page
width.
- Highlights use mix-blend-multiply only once the page canvas holds a
picture, so they stay visible on the dark placeholder.
- Only the page holding the active highlight receives its id, so moving it
re-renders two pages, not all of them.
- HighlightLayer builds the style and data props once for both branches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4505 [FIX] Address review: keys in fields, measured scrollbar, page reset, deferred fit layout
- viewerKeyAction leaves keys typed into an input, textarea, select or
contenteditable to that field.
- Fit modes measure the platform scrollbar once (lib/scrollbar) instead of
assuming 16 px, so a 17 px classic scrollbar no longer forces a
horizontal scrollbar.
- The page index resets while rendering when the document changes, so the
toolbar never shows the last document's page against the new count.
- In a fit mode the page list waits for the viewer's size, so pages are not
laid out at 100% first and again at the fitted zoom.
- sync() reads the page elements once per list, not on every scroll.
- Document identity uses objectKey from lib/pdf/pdfjs, shared with
pdfSourceKey.
- useElementSize measures once where ResizeObserver is missing, matching
the guard in PdfPageList.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4506 [FIX] Address review: text identity, image zoom and loading, one source key, empty text
- A text source can carry an id; the view keeps its place while the id
stays the same, so a live-edited text no longer remounts per keystroke.
- One sourceKey drives every per-source reset (image size, page index)
and keys the text and image views, replacing three separate mechanisms.
- Zoom keys are ignored until an image has a size, so it still opens in
its fit mode.
- A scroll to a highlight on another page of an image is dropped instead
of waiting for ever.
- ImageView resets when its URL changes, reports a highlight scroll as not
yet possible until it has loaded, and keeps the centre spot when zooming.
- The image region is named Image viewer, so it no longer repeats the
image's alt text.
- Blank text says no text was found rather than no document.
- Text lines use content-visibility, so the browser skips layout and paint
for lines off screen while every line stays in the DOM.
- One NO_HIGHLIGHTS constant and one page frame class are shared.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4505 [FIX] Drop a waiting active-highlight scroll once the highlight is no longer active
The retry effect runs before the effect that cancels a cleared selection,
so a render that both delivers the highlight and clears it scrolled to a
selection the reader had just cleared.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4506 [FIX] Retry a waiting scroll when a text fills in, and anchor the first image zoom on the view
- A text kept by its id changes in place, so a scroll waiting on it is
retried when the text changes, not only when the highlights do.
- ImageView reads its anchor from the view once the image is first laid
out, instead of starting from the image's centre.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4505 Drop a waiting scroll as soon as the source changes
A scroll queued for document A survived a switch to B and back to A while
neither had opened, because the retry effect only clears a stale request once
pages are shown; by then the key matched A again. It is now dropped on the
source change itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
* UN-4505 Restore a typed page that is not a whole number
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
* UN-4506 Centre a wrapped text highlight once its rows are drawn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
---------
Co-authored-by: Jaseem Jas <jaseem@zipstack.com>
Co-authored-by: pk-zipstack <praveen@zipstack.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>1 parent 595a84c commit a194073
34 files changed
Lines changed: 5539 additions & 20 deletions
File tree
- frontend/src/v1
- components/patterns/document-viewer
- hooks
- lib
- pdf
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
0 commit comments