diff --git a/.context/research/paratext-9-features/00_Master_Feature_Index.md b/.context/research/paratext-9-features/00_Master_Feature_Index.md index bb94556f8ae..f7b2437eeda 100644 --- a/.context/research/paratext-9-features/00_Master_Feature_Index.md +++ b/.context/research/paratext-9-features/00_Master_Feature_Index.md @@ -1,8 +1,8 @@ # Paratext 9 - Master Feature Index (v2) -**Version**: 3.8 -**Date**: January 22, 2026 -**Total Features**: 152 features across 16 categories +**Version**: 3.9 +**Date**: September 15, 2026 +**Total Features**: 153 features across 16 categories **Methodology**: Six-source validation (MS, FR, R, M, H, C) --- @@ -59,6 +59,7 @@ | 1.15 | Autocorrect | MS C | | 1.16 | Text Normalization Utilities | MS | | 1.17 | Editor Real-time Validation | MS M H C | +| 1.18 | Pane Zoom | MS H C | --- @@ -442,6 +443,7 @@ - **Offline Support** → 16.7 ### P +- **Pane Zoom** → 1.18 - **Parallel Passages Tool** → 9.2 - **Paratext Live (Real-time Collaboration)** → 10.3 - **Plugin System** → 14.1 @@ -536,13 +538,13 @@ ## Statistics - **Total Categories**: 16 -- **Total Features**: 152 +- **Total Features**: 153 **Features by validation level:** - 6 sources: 23 features - 5 sources: 27 features - 4 sources: 36 features -- 3 sources: 28 features +- 3 sources: 29 features - 2 sources: 18 features - 1 source: 20 features @@ -552,6 +554,7 @@ | Ver | Date | Changes | |-----|------|---------| +| v3.9 | 2026-09-15 | Added 1.18 Pane Zoom (≡ Tab > View > Zoom; `FocusedPaneZoom`/`SecondaryViewZooms`, `FormZoomer`, `HtmlZoomHelper`, `DefaultZoomMemento`, `CommentListForm` body zoom) and corrected 2.3's "Font size and zoom controls" bullet, which pointed at the project font size rather than at pane zoom; total now 153 features | | v3.8 | 2026-01-22 | Validation pass: fixed 13.10 sources (added C); updated statistics | | v3.7 | 2026-01-22 | Added 2.10 Vertical Script Support based on source code analysis (supports traditional Mongolian and other vertical scripts); total now 152 features | | v3.6 | 2026-01-22 | Added 3 features from HelpData coverage audit: 2.9 Encoding Converter, 11.9 Translation Priorities; expanded 13.10 Project Conversion (MS→MS FR H); total now 151 features | diff --git a/.context/research/paratext-9-features/01_Translation_Workspace.md b/.context/research/paratext-9-features/01_Translation_Workspace.md index ccb73229e87..2de4c4a21fc 100644 --- a/.context/research/paratext-9-features/01_Translation_Workspace.md +++ b/.context/research/paratext-9-features/01_Translation_Workspace.md @@ -1143,6 +1143,60 @@ investigation): --- +### 1.18 Pane Zoom (View > Zoom) + +**Description**: Scale the text of the focused pane of a text window without changing the window's chrome, menus or toolbars. Zoom is per pane, not per window: the footnote pane of a Scripture text window keeps a zoom of its own, and the level is remembered per project and per window type between sessions. This is the display-size control users reach for; it is distinct from the project font size set in Language Settings (2.3), which is a project-wide property of the text itself. + +**Sub-Features**: +- Zoom in / zoom out / actual size on the focused pane, from the ≡ Tab > View > Zoom menu and from Ctrl + Plus / Minus / Zero +- Discrete zoom steps (0.25× through 4.0×) with the current percentage shown in the menu +- The Scripture text window's footnote pane zooms separately from its text pane +- The level is remembered per project and per window type, and restored when the window reopens +- Comment/note list windows zoom their HTML body the same way + +**Sources**: + +| Source | Reference | Status | +|--------|-----------|--------| +| Menu Structure | Menu: `≡ Tab > View > Zoom`; Item: `zoomToolStripMenuItem` (a `ZoomMenuItem`); Owner: `TextForm` | `[MS]` | +| HelpData | Keyword: "How do I change the size at which a text is displayed?" | `[H]` | +| Code | `IZoomable` implemented by the zoomable windows; `FormZoomer` interprets the keyboard chords; `DefaultZoomMemento` persists the level | `[C]` | + +**Implementation**: + +| Depth | File | Found Via | Evidence | +|-------|------|-----------|----------| +| 0 | `Paratext/TextForm.Designer.cs` | Menu Structure | `zoomToolStripMenuItem = new Paratext.Base.MegaMenu.ZoomMenuItem()`, `Text = "Zoom"`, `ShortcutKeyDisplayString = "Ctrl + Plus/Minus/Zero"` | +| 0 | `Paratext/TextForm.cs` | Owner of D0 | delegates `Zoom` to `uiScriptureEditor.FocusedPaneZoom`; saves and restores `SecondaryViewZooms` | +| 1 | `ParatextBase/MegaMenu/MegaMenu.cs` | Hosts D0 | `IZoomable zoomProvider`, the `zoomSteps` ladder (0.25 … 4.0), `ZoomString` for the displayed percentage | +| 1 | `ParatextBase/MegaMenu/ZoomMenuItem.cs` | Class of D0 | the menu item itself | +| 1 | `PtxUtils.UI/IZoomable.cs` | Interface in D0/D1 | the `Zoom` contract every zoomable window implements | +| 1 | `PtxUtils.UI/Winforms/FormZoomer.cs` | Keyboard path | `ZoomAction { Larger, Smaller, Actual, NoAction }`, `InterpretKeyCode(Keys)` | +| 1 | `ParatextBase/ScriptureEditor/UsfmMultiPaneControl.cs` | Field in D0 | `FocusedPaneZoom` (the focused pane), `SecondaryViewZooms` (the footnote pane's own levels) | +| 2 | `ParatextBase/ScriptureEditor/UsfmSinglePaneControl.cs` | Owned by D1 | regenerates the pane stylesheet per zoom via `usfmXhtmlConverter.GetCss(Zoom, …)` | +| 2 | `HtmlEditor/HtmlZoomHelper.cs` | HTML-hosted windows | `Setup(IHtmlEditorAdaptor, IZoomable)`, `ZoomChanged` — the zoom path for HTML-rendered bodies | +| 2 | `ParatextBase/DefaultZoomMemento.cs` | Persistence | `SaveZoom(ScrText, formName, zoom)`, `GetZoom(ScrText, formName)`, `ResetZoom(ScrText)` over a `SerializableDictionary` | +| 2 | `Paratext/TextFormMemento.cs` | Persistence | `Zoom` and `SecondaryViewZooms` restored with the window | +| 2 | `Paratext/ProjectComments/CommentListForm.cs` | Variant | implements `IZoomable` and zooms its list body through `HtmlZoomHelper.Setup(listControl.HtmlEditor, listControl)`; persisted in its own memento | + +**UI Entry Points**: +- ≡ Tab > View > Zoom (shows the current percentage; zoom in / out / actual size) + - Menu Structure: `TextForm`, item `zoomToolStripMenuItem` + - File: `Paratext/TextForm.Designer.cs` +- Ctrl + Plus / Ctrl + Minus / Ctrl + 0 on the focused pane + - File: `PtxUtils.UI/Winforms/FormZoomer.cs` + +**Related Features**: +- 1.1 Text Editor - the panes that zoom +- 1.2 Editor Views - zoom applies to every view mode +- 1.11 Insert Footnotes & Endnotes - the footnote pane that zooms separately +- 2.3 Font Management (OpenType/Graphite) - the **project** font size, a different control; zoom scales on top of it +- 10.4 Project Notes - the comment/note list windows that zoom their HTML body + +**Validation**: [MS] - - - [H] [C] — Last verified: 2026-09-15 + +--- + ## Cross-References **Related Categories**: @@ -1175,6 +1229,7 @@ investigation): | 1.15 Autocorrect | ✓ | - | - | - | ✓ | ✓ | 2026-01-20 | | 1.16 Text Normalization | ✓ | - | - | - | - | ✓ | 2026-01-21 | | 1.17 Editor Real-time Validation | ✓ | - | - | ✓ | ✓ | ✓ | 2026-01-22 | +| 1.18 Pane Zoom | ✓ | - | - | - | ✓ | ✓ | 2026-09-15 | ## Notes - FormattedEditor is at repo root (`FormattedEditor/`), not under `Paratext/` diff --git a/.context/research/paratext-9-features/02_Language_Writing_System.md b/.context/research/paratext-9-features/02_Language_Writing_System.md index a073f599b79..616852e589d 100644 --- a/.context/research/paratext-9-features/02_Language_Writing_System.md +++ b/.context/research/paratext-9-features/02_Language_Writing_System.md @@ -4,7 +4,7 @@ **Focus**: Keyboards, fonts, locale data, Unicode handling **User Roles**: All users (configuration by administrators) **Manual Chapters**: 4 (Keyboarding) -**Last Updated**: 2026-01-22 +**Last Updated**: 2026-09-15 --- @@ -130,7 +130,9 @@ Language and writing system features enable Paratext to support the world's lang - Graphite font rendering (SIL Graphite) - Per-project font assignment - Font feature configuration -- Font size and zoom controls +- Per-project font size (the project-wide text size set here; the per-pane display zoom is a separate control — see 1.18 Pane Zoom) + +> **Not the same as zoom.** This dialog sets the project's font and font *size*, a property of the project that travels with it. Scaling what one pane shows on screen is a separate feature — ≡ Tab > View > Zoom, remembered per pane and per project — catalogued as **1.18 Pane Zoom** in `01_Translation_Workspace.md`. **Sources**: @@ -181,7 +183,7 @@ Language and writing system features enable Paratext to support the world's lang **HelpData Items**: - ID: `1af5d594-c397-4cc4-be27-5e88759c6d27` - "How do I change the font used for my project?" - ID: `0b4bf85c-5cc8-428c-960c-f610f1796ad6` - "How do I choose font features for Graphite fonts?" -- ID: `7e6a26d9-899a-4a6f-b3b8-aca642044750` - "How do I change the size at which a text is displayed?" +- ID: `7e6a26d9-899a-4a6f-b3b8-aca642044750` - "How do I change the size at which a text is displayed?" (display size — see 1.18 Pane Zoom) - ID: `63426c4c-a463-4718-b80e-fc875c0e8507` - "Guide: Project > Project settings > Language settings: Font" - Dialogs: `LanguageSettingsForm_tabFont`, `LanguageSettingsForm_tabGraphite` diff --git a/.context/standards/Architecture-Decisions.md b/.context/standards/Architecture-Decisions.md index b1cc2cc533a..c844d90c140 100644 --- a/.context/standards/Architecture-Decisions.md +++ b/.context/standards/Architecture-Decisions.md @@ -4052,6 +4052,44 @@ and the rename lands with the `ProjectSelector` migration (PT-4549). Both names - **Source:** PT-4341 "Open Find from any scripture tab type" (PR #2677) — review finding that the branch diverged from `adr-app-global-shortcuts-in-main` without recording why. +## adr-per-web-view-state-lives-in-the-definition: Per-web-view state lives in the web view definition; user defaults and cross-instance memory live in settings + +- **Date:** 2026-09-15 +- **Status:** Accepted +- **Context:** Per-pane content zoom (epic PT-4575) needs three different lifetimes for one number: + the level the pane on screen is at now, the level to hand the next pane of the same project and + kind, and the user's default for panes that have never been zoomed. The platform already has a + home for the first — a web view's own `state` inside its `SavedWebViewDefinition`, written through + `updateWebViewDefinition` and read back in a view with `useWebViewState`. That store is the one the + dock layout serializes, so it survives a restart and travels with the tab when the tab moves to + another window (`adr-web-view-ids-are-unique-from-birth`, `adr-durable-window-ids`). An April + prototype of this feature (PR #2211) instead kept a `Record` inside a renderer + zoom service and mirrored it into a hidden setting, making the pane's own level a second copy that + had to be reconciled with the definition on every open, move, reload and close. +- **Decision:** Per-web-view state of any kind lives in that web view's `SavedWebViewDefinition.state`, + written with `updateWebViewDefinition`. State that must outlive a single pane — a user-level + default, or "what this project's editor was last set to" — lives in **user settings**: a visible + setting for the default the user controls, a hidden one for the memory. Services keep no parallel + store keyed by web view id. Content zoom is the first feature written to the rule: the pane's own + levels are `platform.contentZoomLevels` in the definition state, the default and the per-project + memory are `platform.webViewContentZoom` and `platform.webViewContentZoomMemory` + (`src/renderer/services/web-view-content-zoom.service.ts`). +- **Alternatives:** (a) **A per-view service with its own hidden `Record` store** — the April + content-zoom prototype, PR #2211 — rejected: two stores for one value, and the one that is not the + definition has to be taught by hand about every lifecycle event the definition gets for free. + (b) **Everything in settings, keyed by web view id** — rejected: ids are minted per pane and never + reused, so such a setting grows without bound and needs a pruning story; that cost is accepted only + for the memory setting, which is keyed by project and kind rather than by pane, and even there, + whether to prune at all is decided in PT-4585. (c) **Everything in the definition, nothing in settings** — + rejected: a level that dies with its pane cannot answer "the same zoom next time I open this + project", which is what Paratext 9 does (`ParatextBase/DefaultZoomMemento.cs`). +- **Consequences:** Per-pane state travels through restart, window move and layout share for free, and + a feature that wants it writes no storage code. In exchange, definition state is semi-public — it + is visible in a shared layout and readable by the view itself — so nothing sensitive belongs there, + and every key needs a name that cannot collide, hence the `platform.`-prefixed key. **Revisit** if a + per-pane value ever grows larger than a layout file should reasonably carry. +- **Source:** PT-4576 (PR #2803) and PT-4580, epic PT-4575; the rejected alternative is PR #2211. + ## adr-per-window-focus-ring-keys-off-broadcast-window-id: A per-window focus ring keys off a main-broadcast window id, not local DOM focus - **Date:** 2026-09-03 @@ -5710,6 +5748,46 @@ and the rename lands with the `ProjectSelector` migration (PT-4549). Both names `adr-window-min-width-shared-constant`. - **Source:** PT-4344; Jolie Rabideau measured the shipped floor in the running app on macOS during review of PR #2701, 2026-08-24. +## adr-simple-mode-tab-menu-offers-zoom-only: Simple mode's tab menu is the platform's own contributed menu, narrowed to the content-zoom group + +- **Date:** 2026-09-15 +- **Status:** Accepted +- **Context:** Simple mode had no tab menu at all, because every item the platform's tab menu could + offer — floating, moving to another window — is a no-op there: floating targets a second window + Simple mode does not have, and moving reaches windows Simple mode does not expose. Content zoom is + the first per-tab action that is meaningful in both modes: zooming a tab's content behaves + identically whether or not a second window exists. The menu converter flattens contributed groups + into one list with separators, and a converted item no longer says which group it came from, so + narrowing a menu to one group has to happen on the contributed menu before conversion, not after. +- **Decision:** Both interface modes read the same contributed tab menu. Simple mode narrows it to + the `platform.tabZoom` group before conversion; items in that group are shown disabled on a tab + whose web view marks no zoom area, read from the content-zoom resolver at the moment the menu + opens. A tab hosting no web view gets no menu at all in either mode. The menu renders only once the + interface mode is known, so a tab never briefly shows the wrong mode's menu before the setting + resolves. +- **Alternatives:** **Keep Simple mode menu-less and rely on chords alone** — rejected: a keyboard + shortcut with no menu entry is not discoverable, and content zoom is meant to be found by browsing + the tab menu. **Disable items by area presence at the contribution level** — rejected: whether a + view marks a zoom area is a signal that only resolves asynchronously, well after the menu contract + is declared, so it cannot gate what the contribution itself offers. **A per-item + `hiddenInterfaceModes` field** — rejected: that mechanism hides individual items, but the need here + is to restrict an entire menu to one group, which a per-item hide cannot express without listing + every other item's id. +- **Consequences:** Simple mode now pays one contributed-menu read per tab at mount, the same + mount-time cost Power mode already paid. The tab menu is reachable only where a tab bar is visible, + which in Simple mode is Column 3 alone — so the editor pane, which has no visible tab bar, needs + its own entry point to content zoom; that entry point is the editor's own hamburger menu, delivered + together with the editor's zoom areas. Product narrowed PT-4578's scope on 2026-09-16: the tab-menu + route to content zoom is required only where Simple mode already has a visible, interactive tab bar + — Column 3 — not on the editor's own tab, whose hamburger menu is its sole entry point instead. The + headless Column 1/2 tab bars keep their existing `pointer-events: none`; this feature does not make + them interactive to reach that parity. The disabled state is a snapshot taken when the menu opens, + not a subscription: a zoom area that arrives while the menu is up leaves the items greyed until the + menu is closed and reopened. That bounds the cost to a menu left open across a pane's first + moments, and it is why the items are greyed rather than hidden — the shape of the menu does not + change under the pointer. +- **Source:** PT-4578 (PR #2821), epic PT-4575. + ## adr-single-verse-surfaces-resolve-verse-zero-to-one: Verse 0 resolves to verse 1 on single-verse display surfaces (display-only) - **Formerly:** ADR-0019 @@ -6697,6 +6775,82 @@ and the rename lands with the `ProjectSelector` migration (PT-4549). Both names - **Source:** the multi-agent review of #2654 and the follow-up decision on its finding about `policyRemedy`. +## adr-web-view-content-zoom-in-iframe-shortcuts: Content-zoom chords are handled by a platform-injected bootstrap inside each web view + +- **Date:** 2026-09-15 +- **Status:** Accepted (narrows `adr-app-global-shortcuts-in-main`; refines `adr-per-web-view-ctrl-f-for-find`) +- **Context:** Content zoom (epic PT-4575) is scoped to one **pane**, and a pane may hold several + independently zoomable areas — the Scripture editor's text and its footnotes list. Acting on the + right one needs the id of the web view the user is in and the area holding the caret or the + pointer. The main process, which claimed Ctrl+`+`/`-`/`0` in its `before-input-event` handler for + app-wide zoom, has neither: it can name the focused window, not the focused tab, and it cannot see + which element inside an `about:srcdoc` iframe has focus. `adr-per-web-view-ctrl-f-for-find` already + settled this shape of problem for Ctrl+F by moving the listener into the view and holding it in one + shared hook — but that hook is extension code every view has to mount, and content zoom has to work + for views that know nothing about it, including plain-HTML ones. +- **Decision:** The chords are handled inside each web view by the **platform's own injected + bootstrap** (`getContentZoomBootstrapScript` in + `src/renderer/services/web-view-content-zoom.bootstrap-script.ts`), appended to the script the + renderer already injects into every non-URL web view. It targets the area holding keyboard focus, + else the area under the pointer for the wheel, else the pane's last active area, and acts with the + pane's own id. This is `adr-per-web-view-ctrl-f-for-find`'s shared hook taken one step further: the + shared handler is platform-injected, so a view opts in by marking **content**, not by mounting + code. The bootstrap prefers in-process functions the window's web-view shard binds on the iframe's + `window` and falls back to the three PAPI commands `platform.webViewContentZoomIn` / `…Out` / + `…Reset`, which keeps `adr-renderer-registers-no-names` intact — the names are registered by the + router, not by the renderer — and keeps a wheel burst off the network. Main gave up the chords in + PT-4577 (PR #2821): the three Windows/Linux `before-input-event` zoom branches were deleted, + `platform.zoomIn` and `platform.zoomOut` stay as commands with no default chord, macOS binds + explicit View-menu items carrying ⌘=/⌘-/⌘0 in place of the native `zoomIn`/`zoomOut`/`resetZoom` + roles, and a renderer top-document `keydown` listener covers the case where keyboard focus is on + window chrome — the tab bar, the reference box, a toolbar button — where no iframe sees the key at + all. The three chord copies — the window-chrome listener, the in-view bootstrap and the macOS + View-menu accelerators — are all built from one table, `CONTENT_ZOOM_CHORDS` in + `src/shared/models/content-zoom.model.ts`; the bootstrap gets it serialized into the script it + injects, the same way the injected stylesheet's rule template travels there. The View menu carries + a hidden ⇧⌘= duplicate so ⌘+, which is what a Mac reports for that chord, reaches the menu path + too. +- **Alternatives:** (a) **Keep it in main and route to the focused window** — rejected: main knows the + window, not the pane and not the area; the same objection `adr-per-web-view-ctrl-f-for-find` + records. (b) **A renderer window-level listener that forwards keys into the right iframe** — + rejected as the general mechanism: it is focus-blind, since the renderer cannot see which element + inside a sandboxed iframe holds the caret, and it adds a hop to keep in sync. It survives only as + the narrow window-chrome case above, where there is no iframe to ask. (c) **A shared React hook + every view mounts**, as Find does — rejected here: it would make zoom an opt-in for *code* rather + than for *content*, it would repeat the coverage gap that decision already names, and plain-HTML + views could not opt in at all. +- **Consequences:** A view that marks no zoom area ignores the chords entirely — the bootstrap does + not even register its wheel listener while a pane has no areas — and the platform scales such a view + whole at the Settings default instead. On Windows and Linux this is what a user notices first: + Ctrl+`+`, Ctrl+`-` and Ctrl+`0` now do nothing anywhere except a view that answers them itself — + today the Scripture editor, through the zoom areas it marks, and Enhanced Resources, through the + keydown handler it has always had for its own scripture-pane zoom + (`extensions/src/platform-enhanced-resources/src/web-views/enhanced-resource.web-view.tsx`). Main + no longer claims those chords, nothing replaces them, and a pane with no marked area deliberately + leaves the keystroke to whoever else may want it rather than swallowing it for no effect. So a user + on Notes, or on the Text Collection — whose own pane zoom is wheel and menu only — presses Ctrl+0 + and nothing happens. That is the intended cost of scoping zoom to a pane rather than to the window, + and it shrinks as views adopt the mechanism (the Text Collection grid in PT-4582, Enhanced + Resources in PT-4583). The bootstrap listens in the **bubble** phase on purpose, so + a view that owns Ctrl+wheel for a sub-region keeps precedence by stopping propagation in the capture + phase; the Text Collection grid's per-resource zoom does exactly that + (`extensions/src/platform-scripture-editor/src/scripture-text-grid/use-resource-zoom-input.hook.ts`). + On macOS the chord arrives through the menu accelerator rather than through the iframe, so it + resolves the pane's *active* area rather than the area holding the caret; the bootstrap's + `pointerdown`/`focusin` tracking re-converges the two, except while a click's own answering refocus + into another area is being suppressed — that one move is held off so the clicked area stays the + target, and a Tab ends the suppression so a deliberate focus move retargets zoom at once. The + window-chrome listener also stands down behind the three full-screen overlays that bypass + `OverlayHost` (connection lost, workspace updating, first run), not only behind a modal dialog: + the level is persisted, so a zoom made behind one of those would outlive it. The in-view bootstrap + needs no gate of its own, because all three of those covers are Radix modal dialogs: each takes + keyboard focus out of the web views while it is up and hands it back when it goes, so no chord + reaches a view from behind a cover. + **Revisit** if a second platform-injected shortcut appears + — two bootstraps competing for one key would want a shared dispatcher rather than two listeners. +- **Source:** PT-4576 (PR #2803, the bootstrap and the injected stylesheet) and PT-4577 (PR #2821, + chord ownership), epic PT-4575. + ## adr-web-view-error-boundary-placement: Web views get one error boundary at the shared mount point, not one per extension - **Date:** 2026-08-27 @@ -7092,3 +7246,47 @@ and the rename lands with the `ProjectSelector` migration (PT-4549). Both names drift left by PR #2365 (silently raised `Z_INDEX_ABOVE_DOCK` 250 → 600, burying every tooltip) and PR #2229 (placed a menu at `Z_INDEX_OVERLAY` underneath its own `Z_INDEX_ABOVE_DOCK` host) by adding the ordering tests in `z-index.test.tsx` and this decision record. + +## adr-zoom-composition: A pane shows Electron zoom × project font size × content zoom, and content zoom is CSS `zoom` on marked areas + +- **Date:** 2026-09-15 +- **Status:** Accepted +- **Context:** Platform.Bible had one scaling control — the app-wide Electron zoom behind the + `platform.zoomFactor` setting, which scales the chrome along with everything else — plus a + project-level font size the editor's Standard view honours, plus two private per-view zooms that + grew where the platform had nothing to offer (the Text Collection grid's per-resource factor, and + the Enhanced Resources viewer's own handler). Adding a per-pane zoom on top of that raises two + questions at once: what the layers multiply out to on screen, and what the new layer is implemented + with. Spikes S1 and S2 of the epic's investigation measured the candidates on the real Scripture + editor. +- **Decision:** On screen a pane shows **Electron zoom × project font size × content zoom** — the + content zoom of the area, its own level if it has one, otherwise the Settings default — with the + Text Collection's per-resource factor multiplying inside the grid's area. Content zoom composes + with the project font size and never replaces or resets it: it scales whatever base size the area + already has, and reset returns to the Settings default, not to the project font size. It is + implemented as CSS `zoom` on each element carrying `data-platform-content-zoom-root`, driven by a + custom property per area (`--platform-content-zoom-`, falling back to + `--platform-content-zoom-default`). **Zoom areas are a platform capability**: a view marks one or + more non-nested areas and the platform owns the targeting (focus, then pointer, then last active + area), the per-area state, the memory and the indicator. Views do not build their own zoom stacks. +- **Alternatives:** (a) **A font-size cascade on the content root** — rejected: it does not reach the + editor's rendered scripture, which sets its own sizes (PT-4167), so the one view the feature exists + for would not scale. (b) **`transform: scale`** — rejected: it breaks hit-testing, so clicks and + caret placement land off-target, which S1 reproduced. (c) **Scaling the whole `