PT-4585: Realign zoom to UX: text only, pop-ups unscaled, Settings % - #2849
Conversation
280e276 to
57cdcc4
Compare
51bcb2d to
4e1b1af
Compare
b0352df to
66943b4
Compare
f818b3c to
bff2bc0
Compare
66943b4 to
489ee7b
Compare
15895c2 to
833f8a1
Compare
833f8a1 to
30db0b8
Compare
|
Measurement that bears on where the Short version: CSS Why
Measured, headless Chromium, real geometrySame DOM shape as a chapter cell (300px port, 20px sticky header, 80 verses), navigating to verse 40. The ancestor row is the one I would not have predicted: it is not enough to keep What this means hereOn If that marker lands on the cell's content box or any wrapper above it, the chapter-surface scroll @rolfheij-sil your note on #2781 — "that marker probably belongs on the verse blocks rather than on Caveat: this measures the generic DOM shape, not this PR's actual markup, which I have not read. AI-assisted (Claude Code); the table is measured in headless Chromium, not inferred. |
… not the branch's history PR #2849 will squash-merge into main, so its ADR amendments and guide changelog rows must describe the final state of the Text Collection's per-resource zoom, not the removal-then-restoration that only existed inside the branch (2026-09-23 removed it, 2026-09-24 restored it; neither step ever reached main). - adr-resource-panes-name-their-zoom-areas: merged the 2026-09-23 and 2026-09-24 amendments into one, dated 2026-09-24, describing only the final architecture (per-resource resource-<id> areas, text-collection as fallback, menus running the platform's zoom commands, no grid wheel listener). - adr-zoom-areas-mark-project-text (new in this PR): edited its Consequences bullet directly and dropped the self-amendment. - adr-text-collection-resources-are-zoom-areas: rewrote Context and Alternatives to describe main's actual prior state (the grid's own per-resource CSS `zoom`, PT-4155, multiplying with the pane-wide text-collection area) instead of the branch-only pane-wide-only state. - Component-Builder-Patterns.md / Extension-Development-Guide.md: collapsed the changelog row pairs that undid each other, renumbered contiguously from main's last row, and updated the front-matter version. - Applied the review's M8/M9/M12 minor items in these three files, and fixed one further stale removal claim in adr-zoom-composition's amendment (same root cause, different entry) found via the grep safety net. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTCXDyYLo9NMf8wGoDdWSc
1b05e45 to
681501a
Compare
… not the branch's history PR #2849 will squash-merge into main, so its ADR amendments and guide changelog rows must describe the final state of the Text Collection's per-resource zoom, not the removal-then-restoration that only existed inside the branch (2026-09-23 removed it, 2026-09-24 restored it; neither step ever reached main). - adr-resource-panes-name-their-zoom-areas: merged the 2026-09-23 and 2026-09-24 amendments into one, dated 2026-09-24, describing only the final architecture (per-resource resource-<id> areas, text-collection as fallback, menus running the platform's zoom commands, no grid wheel listener). - adr-zoom-areas-mark-project-text (new in this PR): edited its Consequences bullet directly and dropped the self-amendment. - adr-text-collection-resources-are-zoom-areas: rewrote Context and Alternatives to describe main's actual prior state (the grid's own per-resource CSS `zoom`, PT-4155, multiplying with the pane-wide text-collection area) instead of the branch-only pane-wide-only state. - Component-Builder-Patterns.md / Extension-Development-Guide.md: collapsed the changelog row pairs that undid each other, renumbered contiguously from main's last row, and updated the front-matter version. - Applied the review's M8/M9/M12 minor items in these three files, and fixed one further stale removal claim in adr-zoom-composition's amendment (same root cause, different entry) found via the grep safety net. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTCXDyYLo9NMf8wGoDdWSc
|
🤖 Claude: Thanks — the measurement matches our reading of the unit mismatch, and #2849's placement is your "child(pad)" row. In #2849 the cell's only
The inline layout is the same with the marker as the scroll box's direct child ( It is pinned by For #2856's rebase onto #2849: the For #2781's aligned view, the ADR note in #2849 ( |
lyonsil
left a comment
There was a problem hiding this comment.
Review of the content-zoom realignment in this PR: 8 findings. Each comment's severity is the one the reviewer settled on after checking it; checked and confirmed means the finding was verified against the code at this head, and needs a human call means it could not be settled either way and is yours to judge.
Findings that could not be attached to a line in this diff
#3 (low · checked and confirmed) extensions/src/platform-scripture-editor/src/platform-scripture-editor-footnotes.component.tsx:285 - Zooming the editor's footnotes pane shows its level in the Scripture text's top-right corner with nothing saying it belongs to the footnotes.
What happens: placeBadge (web-view-content-zoom.bootstrap-script.ts, around line 906) pins the badge 12px/16px from the iframe's corner rather than the zoomed area's own corner. labelOf (line 889) names an area only from data-platform-content-zoom-label, which only Text Collection cells set. The footnotes ContentZoomRoot here passes no label.
Why it matters: With the footnotes pane open, the user cannot tell which pane changed; Enhanced Resources' footnotes and entries areas share the gap.
Fix: Pass label to this ContentZoomRoot from a new localized key in the extension's localizedStrings.json (en and es), and add a test that the marker carries the label.
How this was checked: Confirmed at extensions/src/platform-scripture-editor/src/platform-scripture-editor-footnotes.component.tsx:285, which wraps the footnote list in <ContentZoomRoot area="footnotes" ...> with no label prop. ContentZoomRoot (lib/platform-bible-react/src/components/advanced/content-zoom-root.component.tsx:96) only writes the data-platform-content-zoom-label attribute when label is passed. In web-view-content-zoom.bootstrap-script.ts, placeBadge (line ~867) always pins the zoom badge to the iframe's own top-right corner via INDICATOR_INSET_TOP/INDICATOR_INSET_INLINE (12px/16px, lines 799-800), and labelOf (line 887) only finds a name if some marker for that area carries the label attribute. This PR (diff against 70e1fdc) newly introduces the label prop on ContentZoomRoot and the badge's label-reading logic, and uses it only at extensions/src/platform-scripture-editor/src/scripture-text-grid/resource-cell-view.component.tsx:319/397 (Text Collection cells) - it does not touch the footnotes file. So zooming the footnotes pane shows a bare "N%" badge at the editor iframe's corner, indistinguishable from zooming the Scripture text itself.
Not attached to a line: not on a changed line; cannot be posted inline
#6 (low · checked and confirmed) src/renderer/components/overlay-host.component.tsx:56 - The overlays still thread an iframe frame scale that is now always 1, so the popover and palette keep logic that can never change their output.
What happens: Nothing sets iframe.style.zoom (pushContentZoom no longer writes it), so getWebViewIframeZoom (overlay-coordinates.ts:52) always returns 1, and frameScale on OverlayPopover and OverlayCommandPalette, plus the scaling in translateCoordinates (line 64), have no effect.
Why it matters: It is maintenance weight that reads as live behavior; the TSDoc has to explain that it is always 1.
Fix: Remove the frameScale prop, its multiplication of anchor?.width/anchor?.height, and its TSDoc from both OverlayPopoverPresentationalProps/OverlayPopover (overlay-popover.component.tsx:59,172,213-214,337-342,392) and OverlayCommandPalettePresentationalProps/OverlayCommandPalette (overlay-command-palette.component.tsx:83,356,664-665,771,776,893). Remove the getWebViewIframeZoom import and the two frameScale={getWebViewIframeZoom(...)} call sites in overlay-host.component.tsx:11,56,65. In overlay-coordinates.ts, delete getWebViewIframeZoom, drop the zoom multiplication in translateCoordinates (return {x: rect.left + position.x, y: rect.top + position.y}), and delete parseIframeZoom once both call sites are gone, since nothing else in the file uses it. Update overlay-coordinates.test.ts (drop the getWebViewIframeZoom describe block and the zoom-multiplication cases for translateCoordinates), overlay-popover.component.test.tsx and overlay-command-palette.component.test.tsx (drop the "forwards frameScale" tests), and overlay-host.component.test.tsx (drop the frameScale/getWebViewIframeZoom mocks and assertions). Because getWebViewIframeZoom is declared in the generated lib/papi-dts/papi.d.ts (module renderer/services/overlays/overlay-coordinates), finish by running npm run build:types to regenerate it rather than hand-editing the removal.
How this was checked: Confirmed. This PR's own diff removes the only writer of the whole-iframe zoom: the merge-base version of web-view-content-zoom.service.ts had iframe.style.zoom = mayScaleWholeIframe(webViewId) ? String(defaultZoom) : '', and HEAD's pushContentZoom (web-view-content-zoom.service.ts:867-897) no longer sets it at all, with a comment stating "The iframe element itself is never scaled." Because of that, parseIframeZoom (overlay-coordinates.ts:27-29) always sees an empty style.zoom and falls back to 1, so getWebViewIframeZoom (overlay-coordinates.ts:52) always returns 1. overlay-host.component.tsx:56 and :65 (both lines this PR edited, per the diff removing the sibling contentScale prop) still pass that value as frameScale into OverlayPopover and OverlayCommandPalette, which multiply anchor?.width/anchor?.height by it (overlay-popover.component.tsx:213-214, overlay-command-palette.component.tsx:664-665) - always a no-op multiply-by-1. translateCoordinates's zoom multiplication (overlay-coordinates.ts:64-74) is equally inert. The function's own TSDoc already concedes this ("currently sets no whole-iframe zoom ... so this returns 1"), which is exactly the maintenance-weight the finding describes.
Not attached to a line: not on a changed line; cannot be posted inline
(AI-assisted, with my guidance)
681501a to
e05360f
Compare
… not the branch's history PR #2849 will squash-merge into main, so its ADR amendments and guide changelog rows must describe the final state of the Text Collection's per-resource zoom, not the removal-then-restoration that only existed inside the branch (2026-09-23 removed it, 2026-09-24 restored it; neither step ever reached main). - adr-resource-panes-name-their-zoom-areas: merged the 2026-09-23 and 2026-09-24 amendments into one, dated 2026-09-24, describing only the final architecture (per-resource resource-<id> areas, text-collection as fallback, menus running the platform's zoom commands, no grid wheel listener). - adr-zoom-areas-mark-project-text (new in this PR): edited its Consequences bullet directly and dropped the self-amendment. - adr-text-collection-resources-are-zoom-areas: rewrote Context and Alternatives to describe main's actual prior state (the grid's own per-resource CSS `zoom`, PT-4155, multiplying with the pane-wide text-collection area) instead of the branch-only pane-wide-only state. - Component-Builder-Patterns.md / Extension-Development-Guide.md: collapsed the changelog row pairs that undid each other, renumbered contiguously from main's last row, and updated the front-matter version. - Applied the review's M8/M9/M12 minor items in these three files, and fixed one further stale removal claim in adr-zoom-composition's amendment (same root cause, different entry) found via the grep safety net. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTCXDyYLo9NMf8wGoDdWSc
|
🤖 Claude: Replies to the two findings from the review body above that could not be attached to a line:
Agreed, and fixed in 1ee52df. All three areas now name themselves on the badge:
The entries text is marked through
Agreed, and removed in a2b5764:
The tests now pin the anchor at the size the pane measured, and the host test keeps a render case for a popover and a palette. |
The per-resource zoom spec aimed its second notch at the center of the chapter panel's zoom marker. The panel holds the whole chapter, so that marker is about 2000 px tall and its center lay below the 1079 px window: the wheel reached no element, the in-frame listener saw no event, and resource A stayed at 1.1. Aimed at the part of the marker inside the web view's frame, the same notch raises A to 1.2. Factor the frame clipping out of `areaBox` into `onScreenBox`, which takes any locator, and use it for the panel text. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTCXDyYLo9NMf8wGoDdWSc
`switchToChapterView` closes the View Options popover with Escape, which returns focus to the View Options button, and a focused trigger keeps its tooltip open (`data-state="instant-open"`) until focus moves. That tooltip hangs over the header end of the last column, where its "⋮" sits, so every click on resource B's "Zoom options" button was intercepted until the test timed out. Hovering the column does not move focus, so it did not help. `openChapterViewZoomOptions` now blurs the focused element and waits for no tooltip to be open before it hovers the column and clicks. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTCXDyYLo9NMf8wGoDdWSc
…word the area label TSDoc `useResourceContentZoom` treated any finite number under `platform.contentZoomLevels` as a resource's own level, and read the stored value as a map without checking it. The platform ignores a non-map value and any level outside MIN_ZOOM_FACTOR..MAX_ZOOM_FACTOR when it applies the levels, so the menus could report a level (and enable Reset) the text did not have, and a `null` map threw. The hook now applies the same checks. The `CONTENT_ZOOM_LABEL_ATTRIBUTE` TSDoc said areas without a label are "shown as before"; it now says an area without a label shows the level alone. papi.d.ts regenerated with `npm run build:types`; the library's mirrored constant does not carry that sentence, so its build output is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTCXDyYLo9NMf8wGoDdWSc
…text marker The `adr-text-collection-resources-are-zoom-areas` consequence for the verse-aligned Grid view said it can mark each verse block with its resource's area. That holds only if the cell does not also mark its text: the cell's `ContentZoomRoot` would enclose those markers, nested markers are ignored, and the wrapper is one more level in the aligned view's subgrid/`display:contents` chain. Name the two ways out. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTCXDyYLo9NMf8wGoDdWSc
… not the branch's history PR #2849 will squash-merge into main, so its ADR amendments and guide changelog rows must describe the final state of the Text Collection's per-resource zoom, not the removal-then-restoration that only existed inside the branch (2026-09-23 removed it, 2026-09-24 restored it; neither step ever reached main). - adr-resource-panes-name-their-zoom-areas: merged the 2026-09-23 and 2026-09-24 amendments into one, dated 2026-09-24, describing only the final architecture (per-resource resource-<id> areas, text-collection as fallback, menus running the platform's zoom commands, no grid wheel listener). - adr-zoom-areas-mark-project-text (new in this PR): edited its Consequences bullet directly and dropped the self-amendment. - adr-text-collection-resources-are-zoom-areas: rewrote Context and Alternatives to describe main's actual prior state (the grid's own per-resource CSS `zoom`, PT-4155, multiplying with the pane-wide text-collection area) instead of the branch-only pane-wide-only state. - Component-Builder-Patterns.md / Extension-Development-Guide.md: collapsed the changelog row pairs that undid each other, renumbered contiguously from main's last row, and updated the front-matter version. - Applied the review's M8/M9/M12 minor items in these three files, and fixed one further stale removal claim in adr-zoom-composition's amendment (same root cause, different entry) found via the grep safety net. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTCXDyYLo9NMf8wGoDdWSc
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTCXDyYLo9NMf8wGoDdWSc
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTCXDyYLo9NMf8wGoDdWSc
Bible Texts, Commentaries and Model Text wrapped their zoom marker around the editor's scroll box, so scrollToVerse added a zoomed-pixel rect distance to the box's unzoomed scrollTop and overshot by the zoom factor. The marker now sits inside the scroll box, around Editorial, as the Text Collection cell and the Scripture editor already do; the messages and spinner stay at interface size. Component tests pin the placement, and adr-zoom-areas-mark-project-text records the rule. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTCXDyYLo9NMf8wGoDdWSc
Only the entry text in the lower panel is marked, so Ctrl/Cmd+wheel over a card's padding, the gaps between entries or the tab bar reached no marker and zoomed the area used last, usually the Bible text. The lower ResizablePanel now carries data-platform-content-zoom-scope="entries", the platform's remedy for unmarked space that belongs to one area. A contract test pins the attribute; the bootstrap's "zoom scope" tests cover how a scope resolves. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTCXDyYLo9NMf8wGoDdWSc
The grid declared `text-collection` as its default area although no resource's text is marked with it: an empty grid showed a badge for a level nothing read, and a grid-wide level remembered from the shipped build was seeded into state where no text used it. ContentZoomDeclaration's defaultArea is now optional and the grid omits it, so a grid showing no resource is not zoomable (no items, chords, wheel, level or badge) and becomes zoomable when its first resource renders. `text-collection` remains only the shared fallback for a resource id that yields no area. The Text Collection ADR entries record the final state and that a grid-wide level from the shipped build is not carried over. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTCXDyYLo9NMf8wGoDdWSc
…badge
Zooming a footnotes pane showed a bare level in the tab's corner, the same
badge the Scripture text shows, so the user could not tell which pane had
changed. The Scripture editor's footnotes pane now names its area with the
existing "Footnotes" string, the Enhanced Resources footnotes pane with a
new "Footnotes" / "Notas al pie" string, and the Enhanced Resources lower
panel after the tab on screen ("Dictionary · 120 %"), reusing the tab
bar's strings.
The entries text is marked through ContentZoomTextProvider, so the
provider gains an optional experimental `label` that every text element it
marks carries; platform-bible-react dist rebuilt.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTCXDyYLo9NMf8wGoDdWSc
…entity The comment above NO_COMPARATIVE_TEXTS described useWebViewState swapping in the caller's latest default on every unrelated state write, which the hook no longer does. The constants stay; the comment now gives the reason that still holds: the values sit in effect dependency lists, as useWebViewState's documentation advises. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTCXDyYLo9NMf8wGoDdWSc
…tion adjustZoomFactor added a step and rounded to the nearest tenth, so after a typed 137 % the + button, the pane zoom keys and the Interface-scaling keys jumped to 150 %, skipping 140 %. An off-grid factor now first moves to the grid mark it has passed in the direction of travel (floor for +, ceil for -, with a 1e-9 tolerance for float noise in the tenths), then steps, clamps and rounds: 137 % goes to 140 % on + and 130 % on -. Utility, stepper and Settings e2e expectations, the stepper TSDoc and the platform.zoomIn/zoomOut docs updated; platform-bible-utils dist rebuilt (platform-bible-react's rebuilt dist is byte-identical) and papi.d.ts regenerated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTCXDyYLo9NMf8wGoDdWSc
Content zoom scales marked areas inside a pane and never the iframe element, so nothing writes iframe.style.zoom any more and the overlay frame scale was always 1. Removed: the frameScale prop from OverlayPopover and OverlayCommandPalette (presentational and store- connected), the OverlayHost calls that supplied it, getWebViewIframeZoom and parseIframeZoom, and the zoom multiplication in translateCoordinates. Tests now pin the anchor at the size the pane measured; the host test keeps a render case for popovers and palettes. papi.d.ts regenerated (one experimental function removed); adr-pop-ups-stay-at-interface-scale updated to match. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTCXDyYLo9NMf8wGoDdWSc
…t area scan Every zoom step ran labelOf, and every placed frame ran isRtlArea, each a document-wide querySelectorAll over every marker; with text-level marking a book-length checklist, Find or inventory carries thousands of markers, at up to 120 wheel notches a second. collectAreas now records each area's first accepted marker and first non-empty label during the scan it already runs on marker-changing mutations, labelOf and isRtlArea read that record, and the observer also watches the label attribute so a rename refreshes it. Tests count querySelectorAll calls: none on a zoom step, one for a rename. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTCXDyYLo9NMf8wGoDdWSc
…n updates The onDidUpdateWebView handler dropped the pane's cached declaration on every update, and updates arrive for every zoom step and every useWebViewState write, so the next tab-title render walked the dock layout again just to re-read the pane's type. That type cannot change while the pane is open (webViewType is not an updatable definition property, and a new view always gets a new id), so the cache is now dropped only when the pane is forgotten. The test pins that an update keeps the declaration. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTCXDyYLo9NMf8wGoDdWSc
Follow-ups from reviewing this round's own changes: - OccurrencesTable picked only the marker attribute out of useContentZoomTextProps, so a labelled provider's name never reached its cells; it now passes the label attribute too (test added, platform-bible-react dist rebuilt). - The Enhanced Resources footnotes label falls back to no name rather than the raw string key, matching the entries label. - The tab bar reads its captions from RESEARCH_TAB_LABEL_KEYS, so the tab captions and the entries area's name share one source; the test that only compared the constant with a copy of itself is dropped. - The bootstrap observer comment names both watched attributes, and a service test fixture comment says what it is for. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTCXDyYLo9NMf8wGoDdWSc
492e882 to
c04243b
Compare
Matt's PR #2849 follow-up (issue comment 5833335163), round-1 verified. 1. Add a contract test to each web view's `content-zoom-markers.contract.test.ts` pinning that the footnotes pane's zoom-area label is wired from a loaded localize key: `platform-scripture-editor.web-view.tsx`'s `FootnotesLayout` `zoomAreaLabel` prop and its `EDITOR_LOCALIZED_STRINGS` entry, and `enhanced-resource.web-view.tsx`'s `EnhancedResourceFootnotesPane` `zoomAreaLabel` prop and its `ENHANCED_RESOURCE_WEB_VIEW_STRING_KEYS` entry. Without either test, dropping the prop or the key still passed every test — the component tests cover the pane, not the view that feeds it. Verified each test fails when the prop is removed and when the key is dropped from its loaded list (both reverted after confirming red); Matt's regexes matched the source as written, so neither needed adjusting. 2. Fix `getContentZoomBootstrapScript`'s `declaredArea` param doc: it reads `undefined` both for a web view type core does not declare and for one declared with no default area (e.g. the Text Collection grid), not only the former. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTCXDyYLo9NMf8wGoDdWSc
…-editing Main reworked content zoom (#2849). The footnotes pane keeps its list wrapper as the footnotes zoom area and now passes main's zoomAreaLabel through to it; the web view passes both the pane's editing props and the label. The pbr dist is rebuilt rather than taken from either side. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/060fba48-2dc4-4718-9172-4a835abd6dc0
Matt's PR #2849 follow-up (issue comment 5833335163), round-1 verified. 1. Add a contract test to each web view's `content-zoom-markers.contract.test.ts` pinning that the footnotes pane's zoom-area label is wired from a loaded localize key: `platform-scripture-editor.web-view.tsx`'s `FootnotesLayout` `zoomAreaLabel` prop and its `EDITOR_LOCALIZED_STRINGS` entry, and `enhanced-resource.web-view.tsx`'s `EnhancedResourceFootnotesPane` `zoomAreaLabel` prop and its `ENHANCED_RESOURCE_WEB_VIEW_STRING_KEYS` entry. Without either test, dropping the prop or the key still passed every test — the component tests cover the pane, not the view that feeds it. Verified each test fails when the prop is removed and when the key is dropped from its loaded list (both reverted after confirming red); Matt's regexes matched the source as written, so neither needed adjusting. 2. Fix `getContentZoomBootstrapScript`'s `declaredArea` param doc: it reads `undefined` both for a web view type core does not declare and for one declared with no default area (e.g. the Text Collection grid), not only the former. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTCXDyYLo9NMf8wGoDdWSc
Rebased onto main after PT-4585 (#2849) moved the zoom markers inside the editor scroll boxes and reworked the resource cell's right-click menu: - Portalled right-click test and copyright stories use the new cell props (required zoomArea, menuStrings fixture, Copy-only menu) - Grid test's verse-text ids no longer match the /^cell-/ cell count - Rebuilt platform-bible-react and platform-bible-utils dist Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebased onto main after PT-4585 (#2849) moved the zoom markers inside the editor scroll boxes and reworked the resource cell's right-click menu: - Portalled right-click test and copyright stories use the new cell props (required zoomArea, menuStrings fixture, Copy-only menu) - Grid test's verse-text ids no longer match the /^cell-/ cell count - Rebuilt platform-bible-react and platform-bible-utils dist Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebased onto main after PT-4585 (#2849) moved the zoom markers inside the editor scroll boxes and reworked the resource cell's right-click menu: - Portalled right-click test and copyright stories use the new cell props (required zoomArea, menuStrings fixture, Copy-only menu) - Grid test's verse-text ids no longer match the /^cell-/ cell count - Rebuilt platform-bible-react and platform-bible-utils dist Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Matt's PR #2849 follow-up (issue comment 5833335163), round-1 verified. 1. Add a contract test to each web view's `content-zoom-markers.contract.test.ts` pinning that the footnotes pane's zoom-area label is wired from a loaded localize key: `platform-scripture-editor.web-view.tsx`'s `FootnotesLayout` `zoomAreaLabel` prop and its `EDITOR_LOCALIZED_STRINGS` entry, and `enhanced-resource.web-view.tsx`'s `EnhancedResourceFootnotesPane` `zoomAreaLabel` prop and its `ENHANCED_RESOURCE_WEB_VIEW_STRING_KEYS` entry. Without either test, dropping the prop or the key still passed every test — the component tests cover the pane, not the view that feeds it. Verified each test fails when the prop is removed and when the key is dropped from its loaded list (both reverted after confirming red); Matt's regexes matched the source as written, so neither needed adjusting. 2. Fix `getContentZoomBootstrapScript`'s `declaredArea` param doc: it reads `undefined` both for a web view type core does not declare and for one declared with no default area (e.g. the Text Collection grid), not only the former. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTCXDyYLo9NMf8wGoDdWSc
Content zoom now scales only project text. Following the UX review of 2026-09-22 on the content-zoom epic (PT-4575), this PR narrows the feature so it does one job: make small project text easier to read. Views that show no project data (Home, New Tab and similar) are no longer zoomed and no longer offer zoom. Inside the views that do zoom, only the text grows; buttons, inputs, filters, headers, card frames and every pop-up keep interface scale. Find, the four inventories, Checks, the Markers Checklist and the Lexical Tools dictionary join the zoomable views. The editor's hamburger keeps its zoom items in Simple mode only, in their own section. In Settings, both zoom settings use a percentage stepper that wraps when narrow and accepts a typed value. The PR's original scope (cross-kind e2e specs, shared zoom maths, the stale-area decision) is unchanged and kept below.
What changed for users
One line per UX point, in the order UX raised them:
\/ Enter / footnote palettes and the inline footnote and comment editors are the same size at 200 % as at 100 %, and still open beside the zoomed text and follow scrolling.+/-/0and Ctrl/⌘ + wheel work too, including in a zoomable view that is still empty (Find before a search, Checks before a run).How it is built
Which panes zoom. Core's declaration map (
CONTENT_ZOOM_DECLARATION_BY_WEB_VIEW_TYPEinsrc/shared/models/content-zoom.model.ts) gives each first-party zoomable web-view type a kind and a default area; it gains Find, the four inventories, Checks, the Markers Checklist and the Lexical Tools dictionary. Word List and Compare Versions are declared when their marker PRs in paratext-bible-extensions and paratext-bible-internal-extensions land, so until then those two views offer no zoom.isContentZoomable(webViewId)in the renderer content-zoom service is true when the pane's type is declared OR the pane currently reports at least one zoom area. A declared pane with no marker rendered yet still acts: chords, wheel and menu target its declared default area, starting from the pane's own level, then the remembered per-project level, then the Settings default. A pane that is not zoomable is never scaled: the whole-iframe fallback is gone, and so is the hidden per-type settingplatform.webViewContentZoomTypesWithAreasthat existed only to prevent its flash (its label key is deprecated inmetadata.json, not deleted). The grace timer now only drops the stale areas of a pane whose bootstrap died. Zoomability changes are published as a renderer-local event,onDidChangeContentZoomable, wrapped byuseIsContentZoomable.Tab menu. The three zoom items are absent, not disabled, on a pane that is not zoomable. The tab title reads zoomability reactively, so a third-party view gains its items when it reports its first area; while a menu is open its zoomability is held for that open, so the menu never changes shape under the pointer.
Editor hamburger. The zoom items move into their own column,
platformScriptureEditor.zoomSection, with the heading "Zoom" (one new localization key, en and es), and each item carrieshiddenInterfaceModes: ["power"]. In Power mode the column is empty and drops out with no heading or separator; in Simple mode it is its own separator-bounded section.Only project text is marked.
ContentZoomRootgainsas?: 'div' | 'span'for inline text, and library components that render project text mark it only inside aContentZoomTextProvider(viauseContentZoomTextProps), which the hosting view opts into. Region markers are replaced by markers on the text itself:mainandfootnotesunchanged; theentriesarea now marks lemma, gloss, definition and article text instead of the whole panel.\markertokens; the edit link, Ref column and indentation do not scale.Pop-ups never scale. Removed: the library's area context (
ContentZoomAreaProvider,useContentZoomArea), the pop-up attributeCONTENT_ZOOM_POPUP_ATTRIBUTEandgetContentZoomPopupStyleand the overlaycontentScaleprop. (ThecontextMenuContainerwiring for the editor library is removed in #2844 itself.) The six shadcn pop-up files return to their bytes on main before #2825, plus a two-line// CUSTOM:note on each pop-up component saying it stays at interface scale. Kept: live anchors (useLivePopoverAnchor, the editor's anchor sources), the overlays'frameScaleandgetWebViewIframeZoom(parseIframeZoomis now private to its module), and the footnote editor's narrow-pane fixes. Neither this branch nor #2844 depends on paranext/scripture-editors#17, which is closed unmerged.Settings zoom steppers (intended visual change). The stepper is now two button groups —
[− +]and[percentage ⟲]— inside one wrapping row, so a narrow Settings pane puts the percentage and reset under the step buttons instead of clipping. Each group rounds its own outer corners: the+button's right corners and the percentage field's left corners are now rounded, where the single merged group had square joins. This is deliberate, not a regression. The percentage is typeable (any whole 50–300 %, Enter or blur commits, Escape abandons), and Interface scaling uses the same stepper, shown as a percentage (stored factor unchanged). Settings cards now cap at the pane width and put each label above its control when narrow; screenshots of General, an extension group and a project's settings at both widths are attached.Settings screenshots: General, an extension group and a project's settings, at default and narrow width
Interface scaling as a percentage.
platform.zoomFactorrenders with the sameZoomStepperas "Tab content default zoom"; both use the 50–300 % range and reset returns to 100 %. The stored float and its consumers in the main process are unchanged. Both settings get new description keys (…_description_2) that talk in percent; the replaced keys are deprecated, and new en/es label keys cover the Interface scaling buttons, limits and the percentage field.Docs. New ADR entries
adr-content-zoom-applies-only-to-zoomable-panes,adr-pop-ups-stay-at-interface-scaleandadr-zoom-areas-mark-project-text;adr-pop-ups-follow-their-content-zoom-areais superseded;adr-editor-context-menu-follows-its-area-via-a-containeris withdrawn to a stub under the log's carve-out;adr-zoom-composition,adr-simple-mode-tab-menu-offers-zoom-onlyandadr-resource-panes-name-their-zoom-areascarry Amended notes. The Extension Development Guide and Component Builder Patterns describe marking project text, the third-party limitation below, and pop-ups at interface scale.Callouts for reviewers
web-view-content-zoom.service.ts.TwoStepDeleteTooltipOverlay, not directly aroundrenderEditor(), because the overlay places its two-step delete hint from rect differences against its own wrapper.resource-cell-view.component.test.tsx, because the grid component's test mocksResourceCell.ContentZoomTextProviderplus the hook rather thanContentZoomRoot as="span". Itsarticle-vieweris a Dialog, so it counts as a pop-up and is not marked.entriesdrops out of the reported areas whenever its text unmounts (loading, empty state, Media/Maps tabs), so after moving to another verse the next Ctrl+= zoomsmainuntil the user clicks entry text. All of these are accepted as documented behaviour (product ruling 2026-09-23).parseIframeZoomis no longer exported (it was@experimentalonpapi.d.tswith no consumer outside its module);papi.d.tsis regenerated.data-platform-content-zoom-rootis rendered; this is documented in the Extension Development Guide and theContentZoomRootTSDoc.useWebViewStatebehaviour change (public hook). Every zoom step saves the level into the web view's state and fires the web view's update event. Two rules were added so that event no longer churns unrelated slots: a slot still showing its default keeps that same object across unrelated updates (a fresh inline[]/{}default no longer re-triggers effects;resetWebViewState()still applies the latest default), and a saved value deeply equal to the current one keeps the current object. The Markers Checklist reloaded its rows on every zoom step because of this; it also now passes stable defaults. Signature unchanged;papi.d.tsdoc text regenerated. What this changes for callers of the@papi/corehook: saving a value deeply equal to the current one keeps the same object identity, and a slot showing its default keeps its identity across unrelated updates, so an effect keyed on the value's identity no longer re-runs for an equal value. A caller that saved an equal new object to force such an effect to re-run no longer gets that re-run; the review found no such caller.platform-bible-reactexports this PR removes or renames are onmaintoday and leave it with this PR:ContentZoomAreaProvider,ContentZoomAreaProviderProps,useContentZoomArea,CONTENT_ZOOM_CSS_VARIABLE_PREFIX,CONTENT_ZOOM_DEFAULT_CSS_VARIABLE,CONTENT_ZOOM_POPUP_ATTRIBUTEandMAIN_CONTENT_ZOOM_AREA_IDare removed, andmeasureRange/measureElementbecome onemeasureBox(target: Range | Element). All nine are@experimentalon main, and no consumer was found in core, paratext-bible-extensions, paratext-bible-internal-extensions or paratext-10-studio, so this is a clean break with no deprecated aliases. The other removals (createContentZoomWheelReaderand its two types inplatform-bible-utils, theplatform.webViewContentZoomTypesWithAreassetting, theoverlay-content-zoom.utilmodule inpapi.d.ts) exist only in PT-4582/4583/4711-4714: zoom for Resources, pop-ups and editor menu #2844 and never reached main.ContentZoomRoot(orContentZoomTextProviderfor library components); the Extension Development Guide's "Content Zoom (experimental)" section and the Component Builder Patterns' "Content Zoom Opt-In (experimental)" section describe how.// CUSTOM:note on each; the diff against main is the removal of PT-4584: Content zoom for the comment list and the Comments panel #2825's zoom-aware pop-up code.Verification
Ran green:
npm run typechecknpm run lintnpm run format:checkpapi.d.ts, PBRdistrc-dockfailure), PBR 1077, legacy-comment-manager 277, platform-scripture-editor 1696, platform-enhanced-resources 215e2e that ran:
content-zoom-tab-menu/tab-menu-zoom-items-presence.spec.ts(new): Simple and Power mode.content-zoom-tools/find-inventory-checks-text-zoom.spec.ts(new).settings-zoom-stepper/settings-zoom-stepper.spec.ts(new): narrow-pane wrap, typing, Escape, Interface scaling.scripture-editor/content-zoom.spec.tswith the pop-up steps inverted, plus the other editor zoom specs.content-zoom-live-sharing/, the resources group, andmulti-window/(including the move-between-windows spec). Thecontent-zoom-restart/editor test failed once on relaunch (DialogService not registered, an environment issue) and passed on retry.Manual checks and what did NOT run:
Comments e2e (
comment-list-content-zoom,comments-panel-content-zoom, the Power-mode twin from the original scope) and the specs that read Comments factors (content-zoom-default-change/, the restart comment-list spec): Comments cannot open in dev builds because of PT-4554: Rework the comments tab filters and make drafts durable #2840 on main, so these died atopenCommentListbefore any zoom assertion. Written, linted and listed.Text Collection grid e2e ran on 2026-09-24 on this branch with two installed resources, after the helper fixes described in the 2026-09-24 section below:
scripture-text-grid-zoom.spec.ts3/3 pass;scripture-text-grid.spec.ts7 pass, 8 fail, 2 skip (the 8 are pre-existing stale assertions unrelated to zoom);cell-reorder.spec.ts3 skip (needs resources). That 3-test spec was then rewritten for per-resource zoom; its later 4/4 run is under "Text Collection: one zoom area per resource" below.Enhanced Resources e2e: attach-mode specs that need a running app with Marble resources. Written, linted and listed.
Windows touchpad and wheel gestures, verified live by Rolf in the Windows dev app on 2026-09-24:
Editor gutter check done 2026-09-23: LTR (Genesis 1) and RTL (
zzz7) at 50/100/200/300 % with the character-marker bar fixed and no text under it; the Backspace/Delete two-step hint sits on the caret line at 150–200 %.Visual spot check of the new views was done on 2026-09-23 in the Windows dev app: Find after a search, the Markers inventory and the Markers Checklist at 50 % and 300 % pass; it surfaced the badge drift and the checklist reload fixed above, both re-checked in the app afterwards.
Storybook: the new inventory ZoomedText, CommentList ZoomedText and ContentZoomRoot stories were eyeballed on 2026-09-23 (the CommentList story needed the Default story's permission callbacks to show the comment menu; fixture only).
Settings label row spacing (a small CSS change from the self-review) has not yet been eyeballed in the app.
Final whole-branch reviews of the two last sub-plans led to one fix wave each:
act.closeDockTabis one shared e2e helper instead of three copies.Review of #2825 and #2844 (2026-09-23): fixes that land here
Matt's second review of #2825 and #2844 found issues in code this PR carries forward unchanged. The fixes land here, not on those PRs, so they reach
mainin the same merge sequence; each reply on #2825/#2844 names its commit. Findings about code this PR removes (zoom-aware pop-ups, the whole-iframe fallback and its per-type setting) are answered there as superseded.web-view-content-zoom.service.ts): a memory key added and removed while a sibling sync is unconfirmed now reaches every pane that missed it (each pane is walked against the keys it missed until its own write is confirmed); a stamped pane fills the areas it has no level for from memory; a level chosen before a pane shows any project is stamped with its kind alone (notes:) and gives way to the first project's remembered level; one helper (hasStaleStamp) decides whether a pane's levels belong to the project it shows, which also stops a re-pointed panel showing or saving the previous project's levels while memory is unreadable and stops a pane whose project is gone flashing the default; an all-invalid level set is no longer stored as an empty map plus a stamp;seedFromMemorysimplified and documented; unstamped panes skip identity work on every definition update; the named subscription helpers restored.measureRange/measureElementare onemeasureBox, and the live-anchor exports carry full TSDoc.createContentZoomWheelReaderand the wheel-parity test are deleted fromplatform-bible-utils, leaving the bootstrap's reader as the only one. PT-4184: Add the verse-aligned Grid view to the Text Collection #2781 (the Grid view) needs a rebase over this. Later on 2026-09-24 the per-resource zoom came back on the platform's mechanism (see "Text Collection: one zoom area per resource" below): each resource is its own content-zoom area, the right-click zoom items and the chapter view's "⋮" return and send the platform's zoom commands, the grid's own Ctrl+wheel listener stays gone, and the four menu strings are live again (their deprecation entries removed). The old per-column levels are still not read or migrated, and the wheel reader stays deleted.wheelDeltaYrounds to 0 now zooms; typing in the text ends a footnote click's hold on the zoom chords (lone modifiers and chords do not), and the shortcut catalog says so.area=in any attribute position; the Comments e2e helper retries its whole open sequence; comment, ADR and spelling corrections.Declined, with the reason given on each thread: a batched write for the one stamp-only write per pane after upgrading, and moving
@experimentalexports to./experimental. Nothing from this review is deferred.Since the review (2026-09-24): Windows pinch fix, Text Collection e2e helper, self-review, restack
Commits are named by subject and short SHA; both #2825 and #2844 have since merged, and the SHAs below are the branch's final ones after its rebase onto
main(see Restack, below).fix(zoom): read a Windows touchpad pinch as a pinch, not a notch per frame(13b62ac318b): the bootstrap treated a physically held Ctrl or ⌘ as proof that a ctrl+wheel frame was a mouse notch, so a pinch made with Ctrl held (the usual Windows gesture) zoomed a full step per frame. Held-key evidence is now consulted only on macOS, where a notch can be as small as a pinch frame; on Windows and Linux the frame size alone separates a pinch from a notch. Verified live by Rolf on Windows (pinch, Ctrl+pinch, Ctrl+trackpad scroll, Ctrl+mouse wheel; see Verification). Follow-up docs:docs(zoom): say precisely when a wheel frame opens a pinch, and cross-link the ADR entries(87c471ae774).test(e2e): open the Text Collection grid by its current title and project(2198071c650) andtest(e2e): seed the setting the Text Collection grid actually reads(1642ba6e4b6): the shared helper looked for the tab title "Scripture text" (renamed "Text Collection" in July) and seededplatformScripture.modelTexts, which the grid stopped reading whenplatformScripture.referencedProjectsAndResourcestook over, so every spec using it opened an empty grid. Both problems are also onmain's copy of the helper; these commits fix them there when this PR merges. Live result on this branch with two installed resources:scripture-text-grid-zoom.spec.ts3/3 pass;scripture-text-grid.spec.ts7 pass, 8 fail, 2 skip;cell-reorder.spec.ts3 skip (needs resources). The 8 failures are pre-existing stale assertions unrelated to zoom (single-resource full-width layout, a renamed loading string, tab shape, a focus-ring check); they are not fixed here and are tracked as PT-4779 (sub-task of PT-4203). The helper's restore writes the default value back where the setting was absent before the spec; the specs are local-only./review-paratextand the OCR delegate review ran on the restacked branch. Findings are fixed in four commits:test(zoom): cover the slow Ctrl+scroll fallback when wheelDeltaY rounds to 0(ab1809430d7),docs(zoom): state the current iframe zoom rule and consolidate this change's ADR amendments(d11b1cbdc0f),style(react): annotate the pop-up components that stay at interface scale(374de6a0cdd) andchore(zoom): self-review minors(e6dec6bec43). Declined with evidence: the theoretical concern that a Ctrl + two-finger touchpad scroll on Windows would be read as a pinch and zoom too fast (the live test was negative); and, by decision with no ticket, the review's rare memory-seeding timing gap where a pane reads its levels from memory during an unconfirmed write.fix(settings): shorten the tab content default zoom hint(2eb45764d22): the newwebViewContentZoom_description_2hover tooltip was far too long for a settings label hint; it now reads "Default size of project text in panes you haven't zoomed. Interface scaling applies on top." in both en and es.8276cb3bdfa), the branch was rebased onto PT-4582/4583/4711-4714: zoom for Resources, pop-ups and editor menu #2844's restacked head: 111 picks and one generated-output rebuild,build: regenerate papi.d.ts and library output after restack onto main(b3e8bd1a0f8). PT-4582/4583/4711-4714: zoom for Resources, pop-ups and editor menu #2844 has since merged too (squash310bc516da8); the branch was rebased ontomaina second time — 117 picks and one generated-output rebuild — and now sits directly onmain.Text Collection: one zoom area per resource (2026-09-24)
What the user gets. Each text in the Text Collection has its own zoom level again. Ctrl+wheel, a pinch, the right-click menu (Copy, then Zoom in, Zoom out, Reset zoom) and the "⋮" in the chapter view's header zoom one text; the others stay as they are. A text's verse row and the chapter panel or chapter column opened for it always show the same size. The zoom keys and the tab menu act on the text last clicked (anywhere in its row or column), and Ctrl+0 resets only that text. The level badge names the text: "HSV · 120 %". Each text's level is remembered per project, follows "Tab content default zoom" until it is zoomed on its own, and comes back after the tab is closed and reopened or the app restarts. Zoom in is disabled at 300 % and Zoom out at 50 %. Reset zoom is disabled while the text has no level of its own, because that is when it would change nothing (the platform's default is a setting, not 100 %).
How it is built.
resource-<id>, the resource id lower-cased with every character outside[a-z0-9-]replaced by-(toResourceZoomAreaId). An id with nothing left falls back to the pane-widetext-collectionarea. Markers, scopes and menu commands all get the id from one helper,resourceZoomAreaOf, inscripture-text-grid/resource-zoom-area.utils.ts.ContentZoomRoot area={zoomArea} label={resourceName}, inside the scroll box, in the verse row and in the chapter view. Resource names, grips, the View Options row and the "⋮" stay at interface scale.platform.webViewContentZoomIn/Out/Resetcommands with the web view id and the resource's area (useResourceContentZoom). They read the level from theplatform.contentZoomLevelsweb-view state and the "Tab content default zoom" setting, and ignore a stored level that is not a map or is outside 50–300 %.web-view-content-zoom.service.tsis unchanged.%webView_scriptureTextGrid_cell_zoomIn%,…_zoomOut%,…_resetZoom%,…_zoomOptions%) are used again with their shipped values; theirdeprecationInfoentries are removed and no string is added. The keyboard catalog'scontent-zoom-wheelcontext sentence now includes the zoom scope the pointer is in.Framework additions (experimental, additive).
data-platform-content-zoom-scope(CONTENT_ZOOM_SCOPE_ATTRIBUTE) marks an unscaled row, column or card as belonging to one area, so a click, focus or Ctrl+wheel anywhere in it targets that area; a marker inside the scope still wins, and the scope scales nothing.data-platform-content-zoom-label(CONTENT_ZOOM_LABEL_ATTRIBUTE, written byContentZoomRoot's newlabelprop) names an area in the badge. Both constants are tagged@experimentalinweb-view.model.ts(so inpapi.d.ts) and in theirplatform-bible-reactmirrors, and so is thelabelprop. Nothing changes for a view that does not write them. No new command, network object, provider or menu contribution, so nothing new goes on the wire.Not migrated. Levels saved by the shipped per-column zoom (
scriptureTextGrid.zoomByResourceId) are not read or migrated: they were per tab and the new ones are per project, and writingplatform.contentZoomLevelsfrom the view would race the platform's seeding. Users re-zoom once. A contract test pins that no grid file reads the old key.Known limit. A Text Collection opened from the default layout, before it is pointed at a project, has no project to remember levels under. Levels set there are dropped when it is pointed at one, as for the pane-wide level before.
Hidden tabs (for the reviewer to check). No sync code was added. A level change reaches a hidden Text Collection tab as a CSS variable, which applies without layout, so the tab shows the right sizes when it is shown. The menus' enabled states are data-driven (
useWebViewState,useSetting). The badge's placement waits for an animation frame, as it does everywhere else.Observed during the live e2e run, not caused by this PR and not fixed here:
\q1etc.) collapse to a few pixels of text. The editor stylesheet_usj-nodes.scssindents them with15vw/-10vw, and viewport units grow with CSS zoom while the column does not. This predates this PR (placing the marker on the scroll box instead gives the same result) and affects the platform editor too.platform.contentZoomLevelsafter the zoom is already applied. A menu opened in that window may briefly show Zoom in enabled at 300 %; it settles without further input.PR #2781 (verse-aligned Grid view). The markers wrap each resource's text, never the column box, and the scope sits on the column container that #2781 keeps. Marking each
.verse-blockwith the resource's area works only if the cell does not also mark its text:ResourceCellViewwraps the whole editor in oneContentZoomRoot, and a marker nested inside another marker is ignored. So the aligned view must either skip the cell's own text marker, or keep it and readvar(--platform-content-zoom-resource-<id>, …)with the wrapper's zoom neutralised. The ADR entryadr-text-collection-resources-are-zoom-areasrecords this. A follow-up comment for #2781 is drafted and not posted.Docs. New ADR entry
adr-text-collection-resources-are-zoom-areas, with amendments toadr-resource-panes-name-their-zoom-areasandadr-zoom-areas-mark-project-text. Component Builder Patterns ("Content Zoom Opt-In") and the Extension Development Guide ("Content Zoom") describe the scope and the label.Tests.
ContentZoomRoot'slabel, the area-id helper, the menu hook, the cell view and cell (menu items, order and disabled states, the "⋮" in the header layout only), the grid (same area in a resource's verse row and chapter panel, a different area per resource, the scope on every resource container in all four layouts, the fallback with one warning), the Text Collection contract test and the localized-strings test.scripture-text-grid-zoom.spec.tswas rewritten and is a local-only CDP-attach spec; CI does not run it. It ran headless in WSL in Power mode with two installed resources (NBV21 and WEB) and passed 4/4 on the final content: Ctrl+wheel zooms one resource and its chapter panel shows and moves the same level; the right-click menu and the chapter view's "⋮" zoom one resource and Zoom in stops at 300 %; the zoom keys act on the resource last clicked and Ctrl+0 resets only that one; each resource keeps its level when the tab is closed and reopened. The e2e covers persistence by closing and reopening the tab, not by restarting the app, because an attach-mode spec cannot restart the app.Original scope
The PR's original description, kept as written. Its "nothing here changes user-visible behaviour" held for that scope; the realignment above changes behaviour throughout, and the Verification section above supersedes the note that the six original specs had not run. The
contextMenuContainertypecheck failure it mentions no longer applies: #2844 dropped that wiring, and neither PR depends on paranext/scripture-editors#17. "Stacked on #2844" no longer applies either: #2844 has merged, and this branch is rebased directly ontomain; see Dependencies.Adds the cross-kind end-to-end checks PT-4585 lists for the per-pane content zoom epic (PT-4575), promotes the duplicated zoom maths into
platform-bible-utils(PT-4725), and records the decision on stale zoom areas (PT-4715 A). Stacked on #2844; nothing here changes user-visible behaviour.What this adds
Cross-kind e2e specs (PT-4585), under
e2e-tests/tests/isolated/:content-zoom-restart/: the Scripture editor's text and footnotes areas, and the comment list, keep their zoom across an app relaunch, with no level indicator on arrival (two-phase relaunch, same pattern as the layout-persistence spec).multi-window/web-view-move-between-windows.spec.ts: a moved tab keeps its text-area zoom in the window it arrives in.content-zoom-default-change/: changing "Tab content default zoom" moves only the pane still at default; a pane with its own level stays put, and the follower shows no indicator.content-zoom-live-sharing/: two open editor tabs of one project follow each other live; reset returns both.notes-content-zoom/comments-panel-content-zoom-power-mode.spec.ts: the Power-mode twin of the Simple-mode tab-menu spec.core-settings-info.data.test.ts: pins thatplatform.webViewContentZoomandplatform.webViewContentZoomMemoryare core settings, not project settings, which is the only Send/Receive-relevant invariant (S/R sync itself is a stub in this repo).Not covered here, by design: restart persistence for the Text Collection grid and Enhanced Resources (they need real resource fixtures the isolated suite does not have); the Windows precision-touchpad pinch check (a person's job, PT-4585).
One implementation of the zoom maths (PT-4725).
clampZoom,roundZoom,adjustZoomFactorand theMIN_ZOOM_FACTOR/MAX_ZOOM_FACTOR/ZOOM_STEPconstants now live once inlib/platform-bible-utils/src/content-zoom.util.ts(each tagged@experimental), next to the wheel reader that already lived there. The core copy and the Text Collection grid's copy are deleted; consumers import the shared version; the wheel reader imports the constants instead of hardcoding them, so the wheel-parity guard that pinned two hardcodings against each other is retired. The core model and platform data modules re-export the constants so existing imports keep working.platform-bible-utilsdist andpapi.d.tsare regenerated;platform-bible-reactexternalizes the utils package and needed no rebuild. The wheel reader itself is later deleted from the utilities package with the Text Collection's per-column zoom (see the review section); the platform's inlined reader is the only one.Stale zoom areas on content replacement (PT-4715 A): decided, not built. A comment at the iframe load hook in
web-view-content-zoom.service.tsrecords why a replacement never clears a pane's area list: the bootstrap's observer re-reports the moment a marker is added or removed, an unchanged list is one the new content still owns, and a realm swap is settled by the grace timer's liveness probe. A "clear on load" flag would regress the reload case, because the load event fires after the new content has already reported.Verification
platform-bible-reactworkspace typecheck fails only oncontextMenuContainer, PT-4582/4583/4711-4714: zoom for Resources, pop-ups and editor menu #2844's documented dependency on Let the host choose where the context menu renders scripture-editors#17.src4751 passed (the 2 failures inrc-dock-tab-cache-patch.test.tsare a stalenode_modules/rc-dockin the worktree, file identical to the base); extensions 3006 passed;platform-bible-utils644 passed.Dependencies
#2821, #2825 and #2844 are all merged; #2825 is on
mainas8276cb3bdfaand #2844 as310bc516da8. This PR's base is nowmain: once both had merged, the branch was rebased ontomaindirectly — 117 picks and one generated-output rebuild — replacing its earlier basis on #2844's restacked head.The merge order is unchanged by the realignment. #2844 changed only to drop the editor's context-menu container and reflow one file for prettier. This PR reverses the parts of #2825 and #2844 that UX rejected (pop-ups following their area, region markers, the whole-iframe fallback). Both #2825's and #2844's parts are on
mainnow, somaincarries that behaviour until this PR merges. The Word List and Compare Versions marker PRs in their own repos merge after this one.AI-assisted — session 1, session 2, session 3
🤖 Generated with Claude Code
https://claude.ai/code/session_01MddYES5CZ1wg3CjnyakeXw
This change is