` with a submit button.
+ * @returns A `data-testid="book-chapter-control"` container holding the reference and that button.
*/
export function BookChapterControl({
scrRef,
@@ -558,13 +493,6 @@ export function BookChapterControl({
/**
* Stub toggle switch rendered as a native checkbox so tests can read and change the checked state
* without the real Radix UI implementation.
- *
- * @param props - Component props.
- * @param props.checked - Whether the switch is on.
- * @param props.disabled - Whether the switch is disabled.
- * @param props.id - HTML `id` attribute forwarded to the input.
- * @param props.onCheckedChange - Called with the new boolean state on change.
- * @returns A native `
` element.
*/
export function Switch({
checked,
@@ -601,13 +529,6 @@ const RadioGroupContext = createContext<{
* Stub radio group rendered as a `
` that shares its selected value and change
* handler with its {@link RadioGroupItem} children via context. The real component is built on Radix
* primitives; this stub reproduces just the controlled selection behavior tests drive.
- *
- * @param props - Component props.
- * @param props.children - The {@link RadioGroupItem}s (and any surrounding markup).
- * @param props.className - CSS class names.
- * @param props.onValueChange - Called with an item's value when it becomes selected.
- * @param props.value - Currently selected item value.
- * @returns A `
` providing selection context to its items.
*/
export function RadioGroup({
children,
@@ -634,14 +555,6 @@ export function RadioGroup({
* context, marking itself checked when they match and reporting its own value on change. (The real
* component renders a `
`; a native radio is close enough for these tests and
* plays nicely with jest-dom's checked/disabled matchers.)
- *
- * @param props - Component props.
- * @param props.className - CSS class names.
- * @param props['data-testid'] - Test identifier.
- * @param props.disabled - Whether the item is disabled.
- * @param props.id - HTML `id` attribute (a {@link Label}'s `htmlFor` binds to it).
- * @param props.value - This item's value, compared against the group value to derive `checked`.
- * @returns A native ` ` wired to the group's selection state.
*/
export function RadioGroupItem({
className,
@@ -674,10 +587,6 @@ export function RadioGroupItem({
* Stub popover root that renders its children unconditionally. The extension conditionally mounts
* the content component while open (so its draft state re-initializes per open), so visibility
* needs no simulation here.
- *
- * @param props - Component props.
- * @param props.children - Anchor and (while open) content elements.
- * @returns The children unchanged.
*/
export function Popover({
children,
@@ -688,10 +597,6 @@ export function Popover({
/**
* Stub popover anchor that renders its children as-is, matching the real component's `asChild`
* pass-through behavior.
- *
- * @param props - Component props.
- * @param props.children - The element the popover is anchored to.
- * @returns The children unchanged.
*/
export function PopoverAnchor({
children,
@@ -716,22 +621,6 @@ export function PopoverAnchor({
* simulating a pointer press outside the popover.
* - A sentinel `data-testid="popover-close"` button invokes `onCloseAutoFocus` on click,
* simulating Radix's focus-restoration event fired as the popover closes.
- *
- * @param props - Component props.
- * @param props.children - Panel content.
- * @param props.className - CSS class names forwarded to the div.
- * @param props.onEscapeKeyDown - Called with the native `KeyboardEvent` when Escape is pressed
- * inside the content, matching Radix's signature.
- * @param props.onPointerDownOutside - Called with a `CustomEvent` carrying the original pointer
- * event in `detail.originalEvent` when the sentinel outside button is clicked, matching the shape
- * of Radix's `PointerDownOutsideEvent`.
- * @param props.onOpenAutoFocus - Called once, on the commit that first renders the children, with a
- * cancelable `Event`; preventing it suppresses the stub's own focusing, as in Radix.
- * @param props.onCloseAutoFocus - Called with a plain `Event` when the sentinel close button is
- * clicked, mirroring Radix's close-time focus-restoration event.
- * @param props.onClick - Click handler forwarded to the div.
- * @param props.onMouseDown - Mouse-down handler forwarded to the div.
- * @returns A `` with the panel content and sentinel controls.
*/
export function PopoverContent({
children,
@@ -820,12 +709,6 @@ export function PopoverContent({
/**
* Stub label rendered as a native `
` element.
- *
- * @param props - Component props.
- * @param props.children - Label content.
- * @param props.className - CSS class names.
- * @param props.htmlFor - ID of the associated form control.
- * @returns A native `` element.
*/
export function Label({
children,
@@ -849,8 +732,6 @@ export function Label({
* reads its text children and projects them onto the trigger (see there) so the tooltip text is
* assertable on the trigger element without simulating hover.
*
- * @param props - Component props.
- * @param props.children - The tooltip text.
* @returns `null`; the text is surfaced by {@link Tooltip}, not rendered here.
*/
export function TooltipContent({ children: _children }: Readonly<{ children?: ReactNode }>): null {
@@ -862,10 +743,6 @@ export function TooltipContent({ children: _children }: Readonly<{ children?: Re
* its trigger onto the single child element rather than rendering a wrapper; this stub renders the
* child unchanged and lets {@link Tooltip} clone it to attach the tooltip text. `asChild` is assumed
* throughout, so no non-`asChild` fallback is modeled.
- *
- * @param props - Component props.
- * @param props.children - The element the tooltip is anchored to.
- * @returns The child unchanged.
*/
export function TooltipTrigger({
children,
@@ -881,8 +758,6 @@ export function TooltipTrigger({
* element (mirroring where hover text lived before the migration) without simulating hover, while
* the real component supplies the modifier-key-immune tooltip in production.
*
- * @param props - Component props.
- * @param props.children - A {@link TooltipTrigger} and a {@link TooltipContent}, in either order.
* @returns The trigger's child element cloned with the tooltip text as its `title`.
*/
export function Tooltip({ children }: Readonly<{ children?: ReactNode }>): ReactNode {
@@ -901,10 +776,6 @@ export function Tooltip({ children }: Readonly<{ children?: ReactNode }>): React
/**
* Stub tooltip provider that shares hover-delay config across nested tooltips. The stub renders its
* children unchanged; the delay has no effect in tests.
- *
- * @param props - Component props.
- * @param props.children - The subtree whose tooltips share this provider.
- * @returns The children unchanged.
*/
export function TooltipProvider({
children,
diff --git a/__mocks__/platform-bible-utils.ts b/__mocks__/platform-bible-utils.ts
index dabd262d..9d29e0b8 100644
--- a/__mocks__/platform-bible-utils.ts
+++ b/__mocks__/platform-bible-utils.ts
@@ -17,7 +17,6 @@ class UnsubscriberAsyncList {
readonly unsubscribers: Set;
/**
- * @param _name - Optional label used only for diagnostics; has no runtime effect in this stub.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
constructor(_name = 'Anonymous') {
@@ -27,8 +26,6 @@ class UnsubscriberAsyncList {
/**
* Registers one or more unsubscribers. Accepts either a sync/async function returning boolean or
* an object with a dispose() method; in the latter case the bound dispose is stored.
- *
- * @param unsubscribers - One or more unsubscribers or disposable objects to register.
*/
add(...unsubscribers: (Unsubscriber | UnsubscriberAsync | Dispose)[]): void {
unsubscribers.forEach((unsubscriber) => {
@@ -47,7 +44,6 @@ class UnsubscriberAsyncList {
/**
* Runs all registered unsubscribers (awaiting any promises) and clears the set.
- * @returns true if every unsubscriber returned true.
*/
async runAllUnsubscribers(): Promise {
const unsubs = [...this.unsubscribers].map((fn) => fn());
@@ -72,9 +68,6 @@ interface PlatformError {
/**
* Returns `true` when `error` is a {@link PlatformError}, identified by the presence of the
* `platformErrorVersion` discriminant field.
- *
- * @param error - Value to test.
- * @returns `true` if `error` is a {@link PlatformError}, `false` otherwise.
*/
const isPlatformError = (error: unknown): error is PlatformError =>
typeof error === 'object' &&
diff --git a/e2e-tests/fixtures/app.fixture.ts b/e2e-tests/fixtures/app.fixture.ts
index 6c1b9042..7f077d73 100644
--- a/e2e-tests/fixtures/app.fixture.ts
+++ b/e2e-tests/fixtures/app.fixture.ts
@@ -63,34 +63,26 @@ export const test = base.extend({
/**
* Log an uncaught page error to the console.
*
- * @param err The error thrown in the page context.
+ * @param err - The error thrown in the page context.
*/
const onPageError = (err: Error) => console.error(`Page error: ${err.message}`);
/**
* Log console error messages from the page to the process console.
*
- * @param msg The console message emitted by the page.
+ * @param msg - The console message emitted by the page.
*/
const onConsoleMsg = (msg: ConsoleMessage) => {
if (msg.type() === 'error') console.error(`Console error: ${msg.text()}`);
};
- /**
- * Attach error and console listeners to a page for test observability.
- *
- * @param page The Playwright page to listen on.
- */
+ /** Attach error and console listeners to a page for test observability. */
const attachListeners = (page: Page) => {
page.on('pageerror', onPageError);
page.on('console', onConsoleMsg);
};
- /**
- * Remove error and console listeners previously attached by {@link attachListeners}.
- *
- * @param page The Playwright page to stop listening on.
- */
+ /** Remove error and console listeners previously attached by {@link attachListeners}. */
const detachListeners = (page: Page) => {
page.off('pageerror', onPageError);
page.off('console', onConsoleMsg);
diff --git a/e2e-tests/fixtures/helpers.ts b/e2e-tests/fixtures/helpers.ts
index 4e049f85..a82c8281 100644
--- a/e2e-tests/fixtures/helpers.ts
+++ b/e2e-tests/fixtures/helpers.ts
@@ -109,8 +109,6 @@ export interface LaunchElectronAppOptions {
/**
* Wait for the WebSocket server to be ready on the specified port.
*
- * @param port Port number to connect to.
- * @param timeout Maximum time in milliseconds to wait before throwing.
* @returns Resolves when a WebSocket connection to the port succeeds.
* @throws {Error} If the WebSocket server is not ready within `timeout` milliseconds.
*/
@@ -191,9 +189,8 @@ const SETTINGS_ABSENT_SENTINEL = '__SETTINGS_ABSENT__';
* Self-heals a stale backup left by a prior hard-killed run of EITHER tier (restoring it first), so
* the backup always captures the true original settings, never an already-seeded file.
*
- * @param overrides Setting keys to merge into the file (e.g. `{ 'platform.firstRunComplete': true
+ * @param overrides - Setting keys to merge into the file (e.g. `{ 'platform.firstRunComplete': true
* }`).
- * @returns Nothing.
*/
export function backupAndSeedSettings(overrides: Record): void {
restoreBackedUpSettings();
@@ -225,8 +222,6 @@ export function backupAndSeedSettings(overrides: Record): void
* {@link SETTINGS_BACKUP_FILE} (or delete it if the backup marks that no settings file existed
* before seeding), then remove the backup marker. A no-op when no backup exists. Best-effort:
* guarded so a settings-restore failure never throws.
- *
- * @returns Nothing.
*/
export function restoreBackedUpSettings(): void {
if (!fs.existsSync(SETTINGS_BACKUP_FILE)) return;
@@ -247,7 +242,7 @@ export function restoreBackedUpSettings(): void {
* timeout SIGKILL, Ctrl+C) leaves a recoverable backup instead of losing the developer's original
* settings with the process.
*
- * @param overrides Setting keys to merge into the file (e.g. `{ 'platform.firstRunComplete': true
+ * @param overrides - Setting keys to merge into the file (e.g. `{ 'platform.firstRunComplete': true
* }`).
* @returns A restore function that puts the file back to its exact pre-call contents (or deletes it
* if it did not exist). Call it AFTER the app has closed so the developer's saved settings are
@@ -262,7 +257,7 @@ export function preConfigureSettings(overrides: Record): () =>
* Launch a fresh Electron instance (paranext-core) with the interlinearizer extension loaded via
* `--extensions`.
*
- * @param opts Optional launch options (e.g. environment variable overrides).
+ * @param opts - Optional launch options (e.g. environment variable overrides).
* @returns The app handle, the isolated user-data directory path, and a promise that resolves when
* the app closes.
* @throws If Electron fails to launch or the WebSocket server does not become ready.
@@ -379,7 +374,7 @@ export async function launchElectronWithExtension(
* so a following read of the file sees the captured output rather than an empty buffer.
* Best-effort: a stream error resolves rather than rejects.
*
- * @param appLog The write stream created for {@link SMOKE_APP_LOG_FILE}.
+ * @param appLog - The write stream created for {@link SMOKE_APP_LOG_FILE}.
* @returns Resolves once the stream has flushed and closed (or errored).
*/
function flushAppLog(appLog: fs.WriteStream): Promise {
@@ -417,7 +412,7 @@ function dumpSmokeAppLog(): void {
* Tear down an Electron instance: kill the process group, wait for close, and clean up the isolated
* user-data directory.
*
- * @param ctx The app context returned by {@link launchElectronWithExtension}.
+ * @param ctx - The app context returned by {@link launchElectronWithExtension}.
* @returns Resolves when the Electron process has been killed and user-data cleaned up.
*/
export async function teardownElectronApp(ctx: ElectronAppContext): Promise {
@@ -447,12 +442,12 @@ export async function teardownElectronApp(ctx: ElectronAppContext): Promise(
* Send a single JSON-RPC request where `method` is a PAPI request type (e.g. `rpc.discover`). Opens
* a connection, sends one request, waits for the matching response id, then closes.
*
- * @param method PAPI request type to invoke (e.g. `rpc.discover`).
- * @param params Positional parameters to send with the request.
- * @param port WebSocket port to connect to.
- * @param perRequestTimeoutMs Milliseconds before the request times out.
* @returns The `result` field of the JSON-RPC response, typed as `T`.
* @throws {Error} If the request times out or the server returns a JSON-RPC error.
*/
@@ -535,9 +526,9 @@ export async function sendPapiRequestOnce(
/**
* Poll `rpc.discover` until `methodName` appears in `result.methods` or `timeoutMs` elapses.
*
- * @param methodName The fully-qualified PAPI method name to wait for (e.g. `command:foo.bar`).
- * @param port WebSocket port to connect to.
- * @param timeoutMs Maximum time in milliseconds to poll before throwing.
+ * @param methodName - The fully-qualified PAPI method name to wait for (e.g. `command:foo.bar`).
+ * @param port - The PAPI WebSocket port; defaults to the suite-wide port.
+ * @param timeoutMs - Total budget across all polls, not per poll.
* @returns Resolves when the method appears in `rpc.discover`.
* @throws {Error} If the method is not registered within `timeoutMs` milliseconds.
*/
@@ -583,8 +574,6 @@ export async function waitForPapiMethodRegistered(
* The three waits run concurrently and share the one `timeout` budget (the hosts register in
* parallel, so serializing would triple the worst-case wait for no benefit).
*
- * @param timeout Maximum time in milliseconds to wait for all three hosts. Floored to a small
- * positive value so an already-thin remaining budget still gets one real poll.
* @returns Resolves once all three service-host providers are listed in `rpc.discover`.
* @throws {Error} If any of the three hosts is not registered within `timeout` milliseconds.
*/
@@ -627,9 +616,9 @@ interface DockTabTitlesOptions {
* "tripwire fired" from an ordinary `fn` rejection, so only a genuine fatal error is remapped to
* the fast failure.
*
- * @param page The Playwright `Page` for the Platform.Bible renderer window.
- * @param enabled Whether to arm the tripwire. When `false`, `fn` runs with no listener attached.
- * @param fn The readiness work to run under the tripwire.
+ * @param page - The renderer page to listen on; the listener is removed from it in `finally`.
+ * @param enabled - Whether to arm the tripwire. When `false`, `fn` runs with no listener attached.
+ * @param fn - The readiness work to run under the tripwire.
* @returns Resolves with `fn`'s result once it completes without the tripwire firing.
* @throws If the renderer emitted a fatal startup error while `fn` was in flight (with `enabled`),
* or whatever `fn` itself throws.
@@ -683,12 +672,10 @@ export async function withFatalStartupTripwire(
* {@link withFatalStartupTripwire}, which callers wrap around the whole readiness sequence; this
* wait does not arm that tripwire itself.
*
- * @param page The Playwright `Page` for the Platform.Bible renderer window.
- * @param timeout Maximum time in milliseconds to wait before throwing. Must be positive: Playwright
- * treats `waitForFunction({ timeout: 0 })` as an unbounded wait, so a non-positive value (an
- * already-exhausted budget) is failed fast rather than forwarded, to avoid hanging on the exact
- * "Unknown"-tab stall this helper bounds.
- * @param options Readiness options; see {@link DockTabTitlesOptions}.
+ * @param page - The renderer page hosting the dock layout.
+ * @param timeout - Remaining budget for this wait, in ms; a non-positive value is an exhausted
+ * budget and throws rather than waiting.
+ * @param options - Readiness options; see {@link DockTabTitlesOptions}.
* @returns Resolves once the dock is ready per the chosen `strict` mode.
* @throws If `timeout` is non-positive (budget exhausted before this wait began), if tab titles
* have not resolved within `timeout` milliseconds, or if the renderer page was closed while
@@ -767,8 +754,8 @@ interface AppReadyOptions {
* tab-title poll only runs once the data behind the titles exists. On a healthy startup the hosts
* are already up.
*
- * @param page The Playwright `Page` for the Platform.Bible renderer window.
- * @param options Readiness options; see {@link AppReadyOptions}.
+ * @param page - The renderer page to wait on.
+ * @param options - Readiness options; see {@link AppReadyOptions}.
* @returns Resolves when the dock layout is visible with resolved tab titles, the service hosts are
* registered, and `platform.about` is registered.
* @throws If the dock layout, service hosts, resolved tab titles, or the `platform.about` command
@@ -801,10 +788,6 @@ export async function waitForAppReady(page: Page, options: AppReadyOptions = {})
* Wait for the interlinearizer extension to finish activating by polling `rpc.discover` until
* `interlinearizer.openForWebView` is listed.
*
- * @param timeoutMs Maximum time in milliseconds to poll before throwing. `undefined` selects the
- * generous default (a cold instance can be slow to register the command); callers threading a
- * shared budget pass the remaining time, clamped to a small floor so an already-exhausted budget
- * still gets one real poll rather than throwing instantly.
* @returns Resolves when `interlinearizer.openForWebView` is listed in `rpc.discover`.
* @throws {Error} If the extension does not register within `timeoutMs` milliseconds.
*/
@@ -832,7 +815,6 @@ export async function waitForInterlinearizerReady(
* inherits — which is exactly why those runs went red then green on retry. Waiting it out on the
* first attempt costs nothing on a warm machine and saves a retry on a cold one.
*
- * @param timeoutMs Maximum time in milliseconds to poll before throwing.
* @returns Resolves once at least one project is registered.
* @throws {Error} If no project is registered within `timeoutMs` milliseconds.
*/
@@ -905,9 +887,9 @@ const HOME_CLOSE_TIMEOUT_MS = 5_000;
* {@link waitForAtLeastOneProjectMetadata}) before calling, so an empty table means a dead WebView
* rather than projects that have not finished installing.
*
- * @param page The Playwright `Page` for the Platform.Bible renderer window.
- * @param homeTab Locator for the Home dock tab.
- * @param homeButton Locator for the toolbar's Home button.
+ * @param page - The renderer page hosting the Home WebView.
+ * @param homeTab - Locator for the Home dock tab.
+ * @param homeButton - Locator for the toolbar's Home button.
* @returns Resolves once the Home WebView shows at least one project row.
* @throws {Error} If Home still shows no project row after being reopened.
*/
@@ -995,8 +977,8 @@ async function ensureHomeWebViewLoaded(
* 5. Click "Open Interlinearizer for this Project".
* 6. Wait for the "Interlinearizer" dock tab and click it.
*
- * @param page The Playwright `Page` for the Platform.Bible renderer window.
- * @param projectName Name of the project to load into the editor and open the Interlinearizer for
+ * @param page - The renderer page to drive the dock, Home, and ≡ menu on.
+ * @param projectName - Name of the project to load into the editor and open the Interlinearizer for
* (default: `"WEB"`).
* @returns Resolves when the Interlinearizer tab is focused and visible.
* @throws If any step does not complete within its timeout.
@@ -1143,7 +1125,6 @@ export async function openInterlinearizerFromScriptureEditor(
*
* Best-effort, non-throwing, and bounded: a picker that refuses to close must not fail the caller.
*
- * @param page The Playwright `Page` for the Platform.Bible renderer window.
* @returns Resolves once no picker tab remains, or after the bounded attempts are exhausted (a
* no-op on the common warm path where no picker was ever opened).
*/
@@ -1178,7 +1159,6 @@ export async function closeSelectProjectPickers(page: Page): Promise {
* Frame locator for the Interlinearizer WebView's iframe, where all of the extension's own UI
* (toolbar, token strips, modals) renders.
*
- * @param page The Playwright `Page` for the Platform.Bible renderer window.
* @returns A `FrameLocator` scoped to the Interlinearizer WebView iframe.
*/
export function getInterlinearizerFrame(page: Page): FrameLocator {
@@ -1193,7 +1173,6 @@ export function getInterlinearizerFrame(page: Page): FrameLocator {
* Interlinearizer"), whose title also contains the word. Centralizes the exclusion so callers can't
* forget it (the `getInterlinearizerFrame` iframe uses a prefix match for the same purpose).
*
- * @param page The Playwright `Page` for the Platform.Bible renderer window.
* @returns A `Locator` for the Interlinearizer WebView dock tab.
*/
function interlinearizerTabLocator(page: Page): Locator {
@@ -1224,8 +1203,8 @@ interface AppAndInterlinearizerReadyOptions {
* {@link waitForAppReady} and {@link waitForInterlinearizerReady}, splitting the timeout budget
* across both so an explicit (shorter) budget caps the WHOLE wait, not just the first half.
*
- * @param page The Playwright `Page` for the Platform.Bible renderer window.
- * @param options Readiness options; see {@link AppAndInterlinearizerReadyOptions.cdp} for the
+ * @param page - The renderer page to wait on.
+ * @param options - Readiness options; see {@link AppAndInterlinearizerReadyOptions.cdp} for the
* shared-CDP-instance profile feature tests pass.
* @returns Resolves when `interlinearizer.openForWebView` is listed in `rpc.discover`.
* @throws If the app or extension do not finish starting up within the timeout budget.
@@ -1260,7 +1239,6 @@ export async function waitForAppAndInterlinearizerReady(
* dialogs are a plain `` (not `showModal()`), so native Escape doesn't fire their
* onCancel. Modals can chain, so cancel in a bounded loop until no overlay remains.
*
- * @param page The Playwright `Page` for the Platform.Bible renderer window.
* @returns Resolves once no modal overlay remains in the iframe (a no-op on the common clean path).
*/
export async function dismissLeftoverModals(page: Page): Promise {
@@ -1311,7 +1289,6 @@ export async function dismissLeftoverModals(page: Page): Promise {
* tab left open (via {@link closeSelectProjectPickers}, whose accumulation would trip strict mode on
* the `.select-project-dialog` locator).
*
- * @param page The Playwright `Page` for the Platform.Bible renderer window.
* @returns Resolves when the Interlinearizer tab is focused and its toolbar is interactive.
* @throws If the Interlinearizer cannot be opened or its toolbar does not render within the
* timeouts.
@@ -1366,8 +1343,8 @@ export async function ensureInterlinearizerOpenOnWeb(page: Page): Promise
* was never resolved (editor closed, or its project failed to load), so this THROWS rather than
* silently navigating against the wrong verse.
*
- * @param page The Playwright `Page` for the Platform.Bible renderer window.
- * @param reference Fully-qualified scripture reference to navigate to (e.g. `"GEN 1:1"`).
+ * @param page - The renderer page whose toolbar reference control is driven.
+ * @param reference - Fully-qualified scripture reference to navigate to (e.g. `"GEN 1:1"`).
* @returns Resolves once the reference has been submitted.
* @throws If the control never becomes enabled within the wait (no navigation target resolved), or
* if it is enabled but its popover does not open or close within the timeouts.
@@ -1406,7 +1383,6 @@ export async function navigateToScriptureRef(page: Page, reference: string): Pro
* Open the Interlinearizer's ≡ ("Project") top menu inside its iframe and wait for the dropdown to
* appear.
*
- * @param page The Playwright `Page` for the Platform.Bible renderer window.
* @returns The frame locator for the Interlinearizer iframe, for chaining menu-item clicks.
* @throws If the menu button or the opened menu does not become visible within the timeouts.
*/
@@ -1437,7 +1413,6 @@ const UNSAVED_TAB_MARKER = '●';
* Read whether the draft has unsaved changes from the Interlinearizer dock tab's title marker (the
* only place the dirty state is observable outside the WebView).
*
- * @param page The Playwright `Page` for the Platform.Bible renderer window.
* @returns `true` when the tab title carries the unsaved-changes glyph.
*/
async function isDraftDirty(page: Page): Promise {
@@ -1449,7 +1424,6 @@ async function isDraftDirty(page: Page): Promise {
* Open the "Select Interlinear Project" modal from the Project menu and wait for its project list
* to finish loading (the modal's buttons are disabled while the fetch is in flight).
*
- * @param page The Playwright `Page` for the Platform.Bible renderer window.
* @returns The frame locator for the Interlinearizer iframe, for chaining clicks in the modal.
* @throws If the modal does not open or its list does not finish loading within the timeouts.
*/
@@ -1472,7 +1446,6 @@ async function openSelectProjectModal(page: Page): Promise {
* hold a developer's unsaved work, so nothing is silently discarded. Clears the draft's
* unsaved-changes state as a side effect (the rescue project becomes the active Save target).
*
- * @param page The Playwright `Page` for the Platform.Bible renderer window.
* @returns Resolves when the Save As modal has closed and the unsaved marker has cleared.
* @throws If the Save As modal does not open, close, or clear the unsaved marker within the
* timeouts.
@@ -1508,11 +1481,10 @@ async function rescueDraftToNewProject(page: Page): Promise {
* Mutating tests call this at the START (with rescue on) to establish their precondition, and again
* at the END (with rescue off) to discard their own leftovers so the next run starts clean.
*
- * @param page The Playwright `Page` for the Platform.Bible renderer window.
- * @param opts Options object.
- * @param opts.rescueDirtyDraft Whether a dirty draft not owned by the e2e project is rescued before
- * being replaced (default `true`). Pass `false` only at the end of a test, where the dirty state
- * is known to be the test's own leftovers.
+ * @param page - The renderer page whose modals and draft state are driven.
+ * @param opts.rescueDirtyDraft - Whether a dirty draft not owned by the e2e project is rescued
+ * before being replaced (default `true`). Pass `false` only at the end of a test, where the dirty
+ * state is known to be the test's own leftovers.
* @returns Resolves when the e2e project is active and all modals have closed.
* @throws If the modals do not open/close or the project cannot be selected or created within the
* timeouts.
@@ -1582,7 +1554,6 @@ export async function ensureE2eProjectActive(
* START of a test (never at the end) so a previously failed run self-heals instead of poisoning the
* next one.
*
- * @param page The Playwright `Page` for the Platform.Bible renderer window.
* @returns Resolves when the wipe dialog has closed after confirming.
* @throws If the wipe dialog does not open or does not close after confirming.
*/
@@ -1607,7 +1578,6 @@ export async function wipeDraft(page: Page): Promise {
* Close the Interlinearizer dock tab via its close button and wait for it to disappear. Used by
* tests that verify draft persistence across a close/reopen cycle.
*
- * @param page The Playwright `Page` for the Platform.Bible renderer window.
* @returns Resolves when the Interlinearizer tab is gone.
* @throws If the tab is not visible, or the tab does not close within the timeout.
*/
diff --git a/e2e-tests/global-setup-cdp.ts b/e2e-tests/global-setup-cdp.ts
index 938f8253..1f8f508d 100644
--- a/e2e-tests/global-setup-cdp.ts
+++ b/e2e-tests/global-setup-cdp.ts
@@ -78,7 +78,7 @@ const RENDERER_SETTLE_TIMEOUT = process.env.CI ? 180_000 : 120_000;
* developer's instance is reused and left running. This keeps the manual
* iterate-against-a-warm-instance workflow working through the same config.
*
- * @param _config Playwright config object — unused; required by Playwright's global-setup
+ * @param _config - Playwright config object — unused; required by Playwright's global-setup
* interface.
* @returns Resolves once a usable app is available (launched here, or an already-running one).
* @throws {Error} If the app's WebSocket or CDP port do not become ready in time.
@@ -246,8 +246,6 @@ export default async function globalSetupCdp(_config: FullConfig): Promise
* The Playwright connection is closed before returning either way — it only disconnects; the app
* keeps running for the test fixtures to connect to.
*
- * @param timeout Maximum time in milliseconds to wait for the renderer page, settled tabs, and the
- * extension's activation.
* @returns Resolves when the renderer page shows at least one dock tab with no "Unknown" titles and
* the interlinearizer extension has registered its open-webview command.
* @throws {Error} If no renderer page appears, tab titles do not resolve, or the extension does not
@@ -312,8 +310,6 @@ async function waitForRendererSettled(timeout: number): Promise {
* this run owns none of the resources those markers describe, so a stale marker from a prior
* launched run would make {@link globalTeardownCdp} act on foreign resources (and a stale settings
* seed would linger in the developer's dev-appdata). Best-effort: each removal is guarded.
- *
- * @returns Nothing.
*/
function clearStaleOwnershipMarkers(): void {
// Undo any seed a prior launched run left behind before it crashed, so the developer's warm
@@ -331,8 +327,6 @@ function clearStaleOwnershipMarkers(): void {
/**
* Seed {@link E2E_SETTINGS_OVERRIDES} before launching the app. Thin wrapper around
* {@link backupAndSeedSettings} — see its doc for the backup/self-heal behavior.
- *
- * @returns Nothing.
*/
function seedE2ESettingsOverrides(): void {
backupAndSeedSettings(E2E_SETTINGS_OVERRIDES);
@@ -341,8 +335,6 @@ function seedE2ESettingsOverrides(): void {
/**
* Undo {@link seedE2ESettingsOverrides}. Thin wrapper around {@link restoreBackedUpSettings} — see
* its doc for the restore/self-heal behavior.
- *
- * @returns Nothing.
*/
export function restoreSeededSettings(): void {
restoreBackedUpSettings();
diff --git a/e2e-tests/global-setup.ts b/e2e-tests/global-setup.ts
index eba2b266..d4415b34 100644
--- a/e2e-tests/global-setup.ts
+++ b/e2e-tests/global-setup.ts
@@ -19,7 +19,6 @@ export const DEV_SERVER_PID_FILE = path.join(__dirname, '.dev-server.pid');
/**
* Check if a port is already in use.
*
- * @param port Port number to probe.
* @returns Resolves to `true` if the port is occupied, `false` if it is free.
*/
export function isPortInUse(port: number): Promise {
@@ -43,8 +42,8 @@ export function isPortInUse(port: number): Promise {
* Webpack-dev-middleware holds requests open until the initial compilation finishes, so a
* successful response guarantees the initial renderer bundle is ready.
*
- * @param url URL to probe.
- * @param timeout Maximum time in milliseconds to wait before rejecting.
+ * @param url - URL to probe.
+ * @param timeout - Total budget in ms, spanning all retries rather than one probe.
* @returns Resolves when the server returns a non-5xx response.
* @throws {Error} If the server does not respond within `timeout` milliseconds.
*/
@@ -57,7 +56,7 @@ function waitForHttpOk(url: string, timeout: number): Promise {
/**
* Mark the probe as failed, destroy the in-flight request, and reject the outer promise.
*
- * @param message Human-readable failure reason passed to the rejected Error.
+ * @param message - Human-readable failure reason passed to the rejected Error.
*/
const fail = (message: string) => {
if (done) return;
@@ -114,8 +113,6 @@ function waitForHttpOk(url: string, timeout: number): Promise {
/**
* Wait until a port is accepting connections.
*
- * @param port Port number to poll.
- * @param timeout Maximum time in milliseconds to wait before rejecting.
* @returns Resolves when a TCP connection to the port succeeds.
* @throws {Error} If the port does not become available within `timeout` milliseconds.
*/
@@ -253,7 +250,7 @@ export async function bootstrapRendererDevServer(): Promise {
* the renderer dev server via {@link bootstrapRendererDevServer}; the smoke fixture
* (`app.fixture.ts`) then launches its own Electron instance per worker.
*
- * @param _config Playwright config object — unused; required by Playwright's global-setup
+ * @param _config - Playwright config object — unused; required by Playwright's global-setup
* interface.
* @returns Resolves when the renderer dev server is ready.
* @throws {Error} If port 8876 is already in use.
diff --git a/e2e-tests/global-teardown-cdp.ts b/e2e-tests/global-teardown-cdp.ts
index a81f7869..e70fd4c4 100644
--- a/e2e-tests/global-teardown-cdp.ts
+++ b/e2e-tests/global-teardown-cdp.ts
@@ -11,7 +11,7 @@ import { removeDirWithRetry, waitForPidExit } from './process-utils';
* user-data dir, then delegates to the shared {@link globalTeardown} to stop the renderer dev server
* and sweep any lingering core processes.
*
- * @param config Playwright config object — forwarded to the shared teardown.
+ * @param config - Playwright config object — forwarded to the shared teardown.
* @returns Resolves when the launched app is killed, its user-data dir removed, and shared teardown
* has completed.
*/
diff --git a/e2e-tests/global-teardown.ts b/e2e-tests/global-teardown.ts
index ca99905a..398a9672 100644
--- a/e2e-tests/global-teardown.ts
+++ b/e2e-tests/global-teardown.ts
@@ -25,9 +25,9 @@ export interface KillFromPidFileResult {
* present, so no run leaves a stale marker behind. A filesystem error (concurrent deletion, a
* locked file) is warned about and swallowed so teardown continues rather than aborting.
*
- * @param pidFile Absolute path to the file holding the target process's PID.
- * @param signal Kill signal to send (`'SIGKILL'` when the target may ignore SIGTERM).
- * @param label Human-readable name of the process, used in the "Stopping " log line.
+ * @param pidFile - Absolute path to the file holding the target process's PID.
+ * @param signal - Kill signal to send (`'SIGKILL'` when the target may ignore SIGTERM).
+ * @param label - Human-readable name of the process, used in the "Stopping " log line.
* @returns See {@link KillFromPidFileResult}.
*/
export function killProcessFromPidFile(
@@ -63,7 +63,7 @@ export function killProcessFromPidFile(
* Stops the renderer dev server started by {@link globalSetup} (if any), then runs `npm run stop` in
* paranext-core to terminate any lingering Electron processes.
*
- * @param _config Playwright config object — unused; required by Playwright's global-teardown
+ * @param _config - Playwright config object — unused; required by Playwright's global-teardown
* interface.
* @returns Resolves when all cleanup steps have completed.
*/
diff --git a/e2e-tests/process-utils.ts b/e2e-tests/process-utils.ts
index 7c5886bd..bf9b9bb7 100644
--- a/e2e-tests/process-utils.ts
+++ b/e2e-tests/process-utils.ts
@@ -14,8 +14,9 @@ import fs from 'fs';
* - Elsewhere the target was spawned `detached` (its own process-group leader): signal the negative
* PID to hit the whole group, falling back to the bare PID if the group is already gone.
*
- * @param pid PID of the detached process to kill. Non-positive values are rejected (see the guard).
- * @param signal POSIX signal to send when not on Windows (ignored on Windows). Defaults to
+ * @param pid - PID of the detached process to kill. Non-positive values are rejected (see the
+ * guard).
+ * @param signal - POSIX signal to send when not on Windows (ignored on Windows). Defaults to
* `'SIGTERM'`.
* @returns `true` if a kill was issued, `false` if the PID was invalid or the process was already
* gone.
@@ -56,8 +57,9 @@ export function killProcessTree(pid: number, signal: NodeJS.Signals = 'SIGTERM')
*
* For a caller with only a bare PID and no handle to listen on, use {@link waitForPidExit} instead.
*
- * @param exitSignal Promise that resolves when the process has exited.
- * @param timeoutMs Maximum time in milliseconds to wait before giving up.
+ * @param exitSignal - Promise that resolves when the process has exited.
+ * @param timeoutMs - Cap on the wait; elapsing it resolves rather than throwing, so callers cannot
+ * distinguish a clean exit from a timeout.
* @returns Resolves once `exitSignal` settles, or once `timeoutMs` elapses — whichever is first.
*/
export async function waitForProcessExit(
@@ -82,9 +84,10 @@ export async function waitForProcessExit(
* with only a bare PID and no live process handle to listen on (e.g. teardown, which reads the PID
* back from a marker file a separate setup invocation wrote).
*
- * @param pid PID to poll.
- * @param timeoutMs Maximum time in milliseconds to wait before giving up.
- * @param pollIntervalMs Milliseconds between existence checks. Defaults to 100.
+ * @param pid - PID to poll.
+ * @param timeoutMs - Cap on the wait; elapsing it resolves rather than throwing, so callers cannot
+ * distinguish an exit from a timeout.
+ * @param pollIntervalMs - Milliseconds between existence checks. Defaults to 100.
* @returns Resolves once the PID no longer exists or `timeoutMs` elapses — whichever is first.
*/
export async function waitForPidExit(
@@ -113,10 +116,10 @@ export async function waitForPidExit(
* Best-effort and non-throwing: a persistent failure is logged, never thrown, so callers can rely
* on this never aborting their own cleanup sequence.
*
- * @param dir Directory to remove.
- * @param label Human-readable name for the directory, used in the warning log if removal ultimately
- * fails (e.g. `'user data dir'`, `'CDP user-data dir'`).
- * @param retryDelayMs Milliseconds to wait before the retry attempt. Defaults to 3000.
+ * @param dir - Directory to remove.
+ * @param label - Human-readable name for the directory, used in the warning log if removal
+ * ultimately fails (e.g. `'user data dir'`, `'CDP user-data dir'`).
+ * @param retryDelayMs - Milliseconds to wait before the retry attempt. Defaults to 3000.
* @returns Resolves once removal succeeds or the retry attempt is exhausted.
*/
export async function removeDirWithRetry(
diff --git a/e2e-tests/tests/_example/example-interlinearizer-feature.spec.ts b/e2e-tests/tests/_example/example-interlinearizer-feature.spec.ts
index fa9e8a96..a1426548 100644
--- a/e2e-tests/tests/_example/example-interlinearizer-feature.spec.ts
+++ b/e2e-tests/tests/_example/example-interlinearizer-feature.spec.ts
@@ -19,7 +19,7 @@ import { waitForAppAndInterlinearizerReady } from '../../fixtures/helpers';
/**
* Filter out expected/benign console errors from a list of captured error messages.
*
- * @param errors Array of console error message strings to filter.
+ * @param errors - Array of console error message strings to filter.
* @returns The subset of `errors` that are not considered benign.
*/
function filterConsoleErrors(errors: string[]): string[] {
diff --git a/src/__tests__/components/AltHeldContext.test.tsx b/src/__tests__/components/AltHeldContext.test.tsx
index 9d3b8ea1..cf740442 100644
--- a/src/__tests__/components/AltHeldContext.test.tsx
+++ b/src/__tests__/components/AltHeldContext.test.tsx
@@ -1,15 +1,10 @@
-/** @file Unit tests for components/AltHeldContext.tsx. */
///
///
import { render, screen } from '@testing-library/react';
import { AltHeldProvider, useAltHeldValue } from '../../components/AltHeldContext';
-/**
- * Renders the current Alt-held value from context as a testable string.
- *
- * @returns A span containing the current Alt-held value stringified.
- */
+/** Renders the current Alt-held value from context as a testable string. */
function AltHeldProbe() {
const altHeld = useAltHeldValue();
return {String(altHeld)} ;
diff --git a/src/__tests__/components/AnalysisStore.test.tsx b/src/__tests__/components/AnalysisStore.test.tsx
index 00c3632e..5a077bfc 100644
--- a/src/__tests__/components/AnalysisStore.test.tsx
+++ b/src/__tests__/components/AnalysisStore.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for components/AnalysisStore.tsx. */
///
///
@@ -37,14 +36,7 @@ import type { ResolvedTokenAnalysis } from '../../utils/suggestion-engine';
// Helpers
// ---------------------------------------------------------------------------
-/**
- * Builds a minimal `TextAnalysis` with a single approved `TokenAnalysis` for the given token.
- *
- * @param tokenRef - Token reference string.
- * @param gloss - Gloss value for the `'und'` language key.
- * @param surfaceText - Surface text of the token.
- * @returns A `TextAnalysis` seeded with one approved token analysis.
- */
+/** Builds a minimal `TextAnalysis` with a single approved `TokenAnalysis` for the given token. */
function makeAnalysisWithGloss(
tokenRef: string,
gloss: string,
@@ -73,10 +65,6 @@ function makeAnalysisWithGloss(
/**
* Builds a `TextAnalysis` where two tokens (`tok-1`, `tok-2`) share one approved `TokenAnalysis`
* payload, so editing either token must fork the payload to avoid affecting its sibling.
- *
- * @param gloss - Gloss value shared by both tokens for the `'und'` language key.
- * @param surfaceText - Surface text of both tokens.
- * @returns A `TextAnalysis` with one payload referenced by two approved links.
*/
function makeSharedAnalysis(gloss = 'a', surfaceText = 'word'): TextAnalysis {
const ta: TokenAnalysis = { id: 'shared-analysis', surfaceText, gloss: { und: gloss } };
@@ -93,35 +81,19 @@ function makeSharedAnalysis(gloss = 'a', surfaceText = 'word'): TextAnalysis {
};
}
-/**
- * Renders a component that displays the gloss for a single token, used to assert on `useGloss`.
- *
- * @param tokenRef - Token ref to subscribe to.
- * @returns JSX element suitable for passing to `render`.
- */
+/** Renders a component that displays the gloss for a single token, used to assert on `useGloss`. */
function GlossReader({ tokenRef }: Readonly<{ tokenRef: string }>) {
const gloss = useGloss(tokenRef);
return {gloss} ;
}
-/**
- * Renders a component that displays the full analysis as JSON, used to assert on `useAnalysis`.
- *
- * @returns JSX element suitable for passing to `render`.
- */
+/** Renders a component that displays the full analysis as JSON, used to assert on `useAnalysis`. */
function AnalysisReader() {
const analysis = useAnalysis();
return {JSON.stringify(analysis)} ;
}
-/**
- * Renders a button that calls `useGlossDispatch` to write a gloss, used to test dispatch.
- *
- * @param props.tokenRef - Token ref to write.
- * @param props.surfaceText - Surface text of the token.
- * @param props.value - Gloss value to write.
- * @returns JSX element suitable for passing to `render`.
- */
+/** Renders a button that calls `useGlossDispatch` to write a gloss, used to test dispatch. */
function GlossWriter({
tokenRef,
surfaceText,
@@ -140,9 +112,7 @@ function GlossWriter({
* callbacks and read its selectors directly off `result.current`, without a throwaway button-and-
* click component. Mirrors the `renderHook` + `wrapper` idiom used elsewhere in the suite.
*
- * @param useHook - Callback invoking the store hooks under test; its return is `result.current`.
* @param options - Provider props; `analysisLanguage` defaults to `'und'`.
- * @returns The `renderHook` result.
*/
function renderStoreHook(
useHook: () => T,
@@ -230,9 +200,6 @@ describe('useGloss', () => {
* Renders the current gloss for a token while counting how many times it re-renders, so tests
* can assert that unrelated gloss changes do not cause extra renders.
*
- * @param props - Component props.
- * @param props.tokenRef - The token whose approved gloss to read via {@link useGloss}.
- * @returns A span containing the gloss string.
* @throws When called outside an {@link AnalysisStoreProvider}.
*/
function CountingGlossReader({ tokenRef }: Readonly<{ tokenRef: string }>) {
@@ -477,11 +444,7 @@ const PHRASE_ANALYSIS: TextAnalysis = {
],
};
-/**
- * Renders a component that displays the phrase link map size, used to assert `usePhraseLinkMap`.
- *
- * @returns JSX element suitable for passing to `render`.
- */
+/** Renders a component that displays the phrase link map size, used to assert `usePhraseLinkMap`. */
function PhraseLinkMapReader() {
const map = usePhraseLinkMap();
return {map.size} ;
@@ -490,10 +453,6 @@ function PhraseLinkMapReader() {
/**
* Renders a component that displays the phrase link analysisId for a given token ref, used to
* assert `usePhraseLinkForToken`.
- *
- * @param props - Component props.
- * @param props.tokenRef - Token ref to look up.
- * @returns JSX element suitable for passing to `render`.
*/
function PhraseLinkReader({ tokenRef }: Readonly<{ tokenRef: string }>) {
const link = usePhraseLinkForToken(tokenRef);
@@ -577,8 +536,6 @@ describe('usePhraseLinkForToken', () => {
/**
* Renders a component that displays the phrase-link-by-id map size, used to assert
* `usePhraseLinkByIdMap`.
- *
- * @returns JSX element suitable for passing to `render`.
*/
function PhraseLinkByIdMapReader() {
const map = usePhraseLinkByIdMap();
@@ -625,8 +582,6 @@ describe('usePhraseLinkByIdMap', () => {
/**
* Renders a component that reads the phrase-link map through `usePhraseLinkByIdGetter` at render
* time and displays its size, used to assert the getter resolves current store state.
- *
- * @returns JSX element suitable for passing to `render`.
*/
function PhraseLinkByIdGetterReader() {
const getPhraseLinkById = usePhraseLinkByIdGetter();
@@ -744,13 +699,7 @@ describe('usePhraseDispatch', () => {
// usePhraseGloss
// ---------------------------------------------------------------------------
-/**
- * Renders the phrase gloss for a given phraseId, used to assert on `usePhraseGloss`.
- *
- * @param props - Component props.
- * @param props.phraseId - Phrase id to look up.
- * @returns JSX element.
- */
+/** Renders the phrase gloss for a given phraseId, used to assert on `usePhraseGloss`. */
function PhraseGlossReader({ phraseId }: Readonly<{ phraseId: string }>) {
const gloss = usePhraseGloss(phraseId);
return {gloss} ;
@@ -857,10 +806,6 @@ const SEGMENT_ANALYSIS_WITH_TRANSLATION: TextAnalysis = {
/**
* Renders the free translation for a given segmentId, used to assert on
* `useSegmentFreeTranslation`.
- *
- * @param props - Component props.
- * @param props.segmentId - Segment id to look up.
- * @returns JSX element.
*/
function SegmentTranslationReader({ segmentId }: Readonly<{ segmentId: string }>) {
const value = useSegmentFreeTranslation(segmentId);
@@ -939,22 +884,13 @@ describe('useSegmentFreeTranslationDispatch', () => {
// Morpheme hooks
// ---------------------------------------------------------------------------
-/**
- * Renders the morpheme forms for a token, used to assert on `useMorphemes`.
- *
- * @param props.tokenRef - Token ref to subscribe to.
- * @returns JSX element with joined morpheme forms.
- */
+/** Renders the morpheme forms for a token, used to assert on `useMorphemes`. */
function MorphemeReader({ tokenRef }: Readonly<{ tokenRef: string }>) {
const morphemes = useMorphemes(tokenRef);
return {morphemes.map((m) => m.form).join(',')} ;
}
-/**
- * Renders the analysis language, used to assert on `useAnalysisLanguage`.
- *
- * @returns JSX element with the analysis language string.
- */
+/** Renders the analysis language, used to assert on `useAnalysisLanguage`. */
function LanguageReader() {
const lang = useAnalysisLanguage();
return {lang} ;
@@ -1242,8 +1178,6 @@ describe('useMorphemeGlossDispatch', () => {
* Reports its `isEditing` prop through {@link useReportGlossEditing}, used to drive the provider's
* pending-edits accounting from tests.
*
- * @param props - Component props.
- * @param props.isEditing - Whether this stand-in input currently holds uncommitted text.
* @returns An empty fragment; the component exists only for its hook side effect.
*/
function EditingReporter({ isEditing }: Readonly<{ isEditing: boolean }>) {
@@ -1349,13 +1283,6 @@ describe('useReportGlossEditing', () => {
/**
* Renders the merged analysis status for a token, used to assert on `useResolvedTokenAnalysis`.
* Records every render's resolved value so a test can assert referential stability.
- *
- * @param props.tokenRef - Token ref to resolve.
- * @param props.surfaceText - The token's surface text.
- * @param props.enabled - Whether the hook should derive; `false` short-circuits to `undefined`.
- * @param props.sink - Array each render appends its resolved value to (for render-count
- * assertions).
- * @returns JSX element suitable for passing to `render`.
*/
function ResolvedReader({
tokenRef,
@@ -1374,11 +1301,7 @@ function ResolvedReader({
}
describe('useShowSuggestions', () => {
- /**
- * Renders the active show-suggestions flag, used to assert on `useShowSuggestions`.
- *
- * @returns JSX element suitable for passing to `render`.
- */
+ /** Renders the active show-suggestions flag, used to assert on `useShowSuggestions`. */
function ShowSuggestionsReader() {
return {String(useShowSuggestions())} ;
}
@@ -1488,11 +1411,6 @@ describe('useSuggestionAfterClearing', () => {
/**
* Renders the status of the cleared-token preview for a token, used to assert on
* `useSuggestionAfterClearing`.
- *
- * @param props.tokenRef - Token ref to resolve.
- * @param props.surfaceText - Surface text to match against the pool.
- * @param props.enabled - Whether the hook should derive (mirrors the chip's gating).
- * @returns JSX element suitable for passing to `render`.
*/
function ClearingReader({
tokenRef,
diff --git a/src/__tests__/components/ArcOverlay.test.tsx b/src/__tests__/components/ArcOverlay.test.tsx
index 6a03e70b..56c31fa1 100644
--- a/src/__tests__/components/ArcOverlay.test.tsx
+++ b/src/__tests__/components/ArcOverlay.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for components/ArcOverlay.tsx. */
///
///
@@ -12,13 +11,7 @@ import { makePhraseLink } from '../test-helpers';
// Helpers
// ---------------------------------------------------------------------------
-/**
- * Builds a minimal `ArcPath` fixture.
- *
- * @param phraseId - Phrase id for the arc.
- * @param splitAfterTokenRef - Token ref marking the end of the earlier fragment.
- * @returns An `ArcPath` with placeholder geometry.
- */
+/** Builds a minimal `ArcPath` fixture. */
function makeArcPath(phraseId: string, splitAfterTokenRef = 'tok-a'): ArcPath {
// `d` is derived from splitAfterTokenRef so distinct split points yield distinct
// path data — matching real arcs, where the React key `${phraseId}-${d}` stays unique.
diff --git a/src/__tests__/components/ContinuousView.test.tsx b/src/__tests__/components/ContinuousView.test.tsx
index c66d4fa7..5606edf9 100644
--- a/src/__tests__/components/ContinuousView.test.tsx
+++ b/src/__tests__/components/ContinuousView.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for components/ContinuousView.tsx. */
///
///
@@ -392,12 +391,7 @@ function makeLargeBook(count: number): Book {
const scrollIntoViewMock = jest.fn();
-/**
- * Builds the lookup maps that ContinuousView's parent supplies, derived from a Book.
- *
- * @param book - The book to scan.
- * @returns The token-segment-id lookup and word-token-ref lookup.
- */
+/** Builds the lookup maps that ContinuousView's parent supplies, derived from a Book. */
function buildLookups(book: Book): {
tokenSegmentMap: ReadonlyMap;
tokenDocOrder: ReadonlyMap;
@@ -424,10 +418,6 @@ function buildLookups(book: Book): {
* Minimal required props for ContinuousView. Spread into render calls so tests only need to
* override what they actually care about. The lookup maps are derived from `book` so they always
* agree with what's rendered.
- *
- * @param book - The book the test will render with.
- * @param overrides - Optional prop overrides.
- * @returns A complete ContinuousView props object.
*/
function requiredProps(
book: Book,
@@ -1179,8 +1169,6 @@ describe('ContinuousView scroll behavior', () => {
* GEN 1:1) so a single Next step crosses into GEN 1:2. The slot between tok-0 and tok-1 lives in
* GEN 1:1 and shows a link icon only while that segment is active, so it's a clean probe for
* whether the active-segment relayout has committed.
- *
- * @returns A predicate reporting whether that in-segment link icon mounted in the latest render.
*/
function renderHideInactiveCrossing(): () => boolean {
const book = makeBook();
@@ -1297,11 +1285,7 @@ describe('ContinuousView scroll behavior', () => {
const { tokenSegmentMap, tokenDocOrder, wordTokenByRef } = buildLookups(book);
const mergedLookups = buildLookups(merged);
let applyBoundaryEdit: () => void = () => {};
- /**
- * Stateful parent that starts on the verse book and swaps to the merged book on demand.
- *
- * @returns The rendered `ContinuousView` element.
- */
+ /** Stateful parent that starts on the verse book and swaps to the merged book on demand. */
function Parent() {
const [ref, setRef] = useState('tok-1');
const [edited, setEdited] = useState(false);
@@ -1475,11 +1459,6 @@ describe('ContinuousView segmentation edits', () => {
/**
* Reads the inline opacity of the link-slot wrapper between `prevRef` and `nextRef`, the style
* `PhraseSlot` uses to suppress link buttons outside the active segment.
- *
- * @param container - The render container to query.
- * @param prevRef - Token ref on the start side of the slot.
- * @param nextRef - Token ref on the end side of the slot.
- * @returns The wrapper's inline `opacity` value.
*/
function slotOpacity(container: HTMLElement, prevRef: string, nextRef: string): string {
const icon = container.querySelector(
@@ -1520,7 +1499,6 @@ describe('ContinuousView split marker', () => {
* split-gap marker can be exercised in the horizontal strip.
*
* @param altHeld - Whether Alt is held (defaults to held, so the marker appears).
- * @returns The dispatch spy for assertions.
*/
function renderStrip(altHeld = true) {
const book = makeBook();
diff --git a/src/__tests__/components/InterlinearNavContext.test.tsx b/src/__tests__/components/InterlinearNavContext.test.tsx
index ef2a3cd5..3c3fc8a9 100644
--- a/src/__tests__/components/InterlinearNavContext.test.tsx
+++ b/src/__tests__/components/InterlinearNavContext.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for components/InterlinearNavContext.tsx. */
///
///
@@ -13,12 +12,7 @@ import {
import { RECENTER_FADE_MS } from '../../components/recenter-fade';
import { makeScrollGroupHook, type ScrollGroupTuple } from '../test-helpers';
-/**
- * Renders {@link useInterlinearNav} inside a provider wired to the given scroll-group hook.
- *
- * @param hook - The `useWebViewScrollGroupScrRef` stub the provider should call.
- * @returns The render-hook result whose `current` is the nav surface.
- */
+/** Renders {@link useInterlinearNav} inside a provider wired to the given scroll-group hook. */
function renderNav(hook: () => ScrollGroupTuple) {
const wrapper = ({ children }: { children: ReactNode }) => (
{children}
@@ -30,10 +24,6 @@ function renderNav(hook: () => ScrollGroupTuple) {
* Renders the nav hook with a scroll-group stub whose reference can be restaged between rerenders,
* so a cross-book navigation can be simulated. A fresh object identity is required on each change
* so the provider adopts it as a new `scrRef`.
- *
- * @param initial - The reference reported on the first render.
- * @returns The render-hook result plus a `setRef` to stage the next reference (call inside `act`,
- * then `rerender`).
*/
function renderNavMutable(initial: SerializedVerseRef) {
let current = initial;
diff --git a/src/__tests__/components/Interlinearizer.test.tsx b/src/__tests__/components/Interlinearizer.test.tsx
index 9c3f2c08..81c3044c 100644
--- a/src/__tests__/components/Interlinearizer.test.tsx
+++ b/src/__tests__/components/Interlinearizer.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for components/Interlinearizer.tsx. */
///
///
@@ -28,17 +27,9 @@ import { allFalseViewOptions } from './test-helpers';
jest.mock('lucide-react', () => ({
__esModule: true,
- /**
- * Stub for the LocateFixed icon; renders a minimal SVG so icon-presence assertions work.
- *
- * @returns An SVG element with `data-testid="locate-fixed-icon"`.
- */
+ /** Stub for the LocateFixed icon; renders a minimal SVG so icon-presence assertions work. */
LocateFixed: () => ,
- /**
- * Stub for the Merge icon used by the between-rows merge control.
- *
- * @returns An SVG element with `data-testid="merge-icon"`.
- */
+ /** Stub for the Merge icon used by the between-rows merge control. */
Merge: () => ,
}));
@@ -107,44 +98,24 @@ jest.mock('../../components/AnalysisStore', () => ({
/**
* Pass-through provider stub that renders children directly, keeping AnalysisStore.tsx out of
* scope.
- *
- * @param props - Component props.
- * @param props.children - Child nodes to render.
- * @returns The children unchanged.
*/
AnalysisStoreProvider({ children }: Readonly<{ children: ReactNode }>) {
return children;
},
- /**
- * Returns a fixed empty gloss string for any token.
- *
- * @returns An empty string.
- */
+ /** Returns a fixed empty gloss string for any token. */
useGloss: () => '',
- /**
- * Returns a no-op dispatch function.
- *
- * @returns A function that accepts any arguments and does nothing.
- */
+ /** Returns a no-op dispatch function. */
useGlossDispatch: () => () => {},
- /**
- * Returns an empty map; cross-segment arc logic is a layout effect that no-ops in jsdom.
- *
- * @returns An empty `Map`.
- */
+ /** Returns an empty map; cross-segment arc logic is a layout effect that no-ops in jsdom. */
usePhraseLinkMap: () => new Map(),
/**
* Returns the test-owned phrase-link map so straddled-boundary tests can seed phrases the
* component's `straddledBoundaryRefs` memo sees.
- *
- * @returns The current phrase-link-by-id map.
*/
usePhraseLinkByIdMap: () => mockPhraseLinkById,
/**
* Returns a getter over the test-owned phrase-link map so force-break tests can seed straddling
* phrases.
- *
- * @returns A function returning the current map.
*/
usePhraseLinkByIdGetter: () => () => mockPhraseLinkById,
usePhraseDispatch: () => ({
@@ -159,9 +130,6 @@ jest.mock('../../components/ContinuousView', () => ({
/**
* ContinuousView stub; captures its props and the segmentation context (the wrapped,
* force-breaking dispatch) so tests can invoke the dispatch directly.
- *
- * @param props - The props passed by Interlinearizer.
- * @returns A minimal div carrying the focused token ref.
*/
default: function ContinuousViewStub(props: CapturedContinuousViewProps) {
capturedContinuousViewProps = props;
@@ -174,17 +142,7 @@ jest.mock('../../components/ContinuousView', () => ({
jest.mock('../../components/SegmentView', () => ({
__esModule: true,
- /**
- * Named export stub for SegmentView; captures received props and renders a minimal div.
- *
- * @param props - The props passed by Interlinearizer.
- * @param props.segment - The segment being rendered.
- * @param props.isActive - Whether this segment is the active verse.
- * @param props.hoveredPhraseId - PhraseId currently hovered.
- * @param props.onHoverPhrase - Hover callback.
- * @param props.rest - Any additional props forwarded from the parent.
- * @returns A div with `data-testid="segment-view"` and the segment id.
- */
+ /** Named export stub for SegmentView; captures received props and renders a minimal div. */
SegmentView: ({
segment,
isActive,
@@ -207,17 +165,7 @@ jest.mock('../../components/SegmentView', () => ({
/>
);
},
- /**
- * Default export stub for SegmentView; captures received props and renders a minimal div.
- *
- * @param props - The props passed by Interlinearizer.
- * @param props.segment - The segment being rendered.
- * @param props.isActive - Whether this segment is the active verse.
- * @param props.hoveredPhraseId - PhraseId currently hovered.
- * @param props.onHoverPhrase - Hover callback.
- * @param props.rest - Any additional props forwarded from the parent.
- * @returns A div with `data-testid="segment-view"` and the segment id.
- */
+ /** Default export stub for SegmentView; captures received props and renders a minimal div. */
default: function MockSegmentView({
segment,
isActive,
@@ -248,11 +196,7 @@ jest.mock('../../components/SegmentView', () => ({
jest.mock('../../components/controls/EditPhraseControls', () => ({
__esModule: true,
- /**
- * Minimal EditPhraseControls stub exposing the done button the toolbar tests assert on.
- *
- * @returns A stub div carrying the `done-edit-btn` test id.
- */
+ /** Minimal EditPhraseControls stub exposing the done button the toolbar tests assert on. */
default: () => (
@@ -264,11 +208,7 @@ jest.mock('../../components/controls/EditPhraseControls', () => ({
jest.mock('../../components/modals/UnlinkPhraseConfirm', () => ({
__esModule: true,
- /**
- * Minimal UnlinkPhraseConfirm stub exposing the confirm container the toolbar tests assert on.
- *
- * @returns A stub div carrying the `unlink-confirm` test id.
- */
+ /** Minimal UnlinkPhraseConfirm stub exposing the confirm container the toolbar tests assert on. */
default: () =>
,
}));
@@ -285,9 +225,6 @@ type BookWithOptionalVerseStarts = Omit & {
* hand-built fixture segment that lacks one, so the fixtures satisfy the `Segment` shape without
* every literal repeating the boilerplate. Segments that already carry `verseStarts` (e.g. produced
* by `resegmentBook`) are left untouched.
- *
- * @param book - The fixture book to normalize; its segments may omit `verseStarts`.
- * @returns The book with every segment carrying at least one verse start.
*/
function withDefaultVerseStarts(book: BookWithOptionalVerseStarts): Book {
return {
@@ -312,9 +249,6 @@ const GEN_EMPTY_BOOK: Book = { id: 'GEN', bookRef: 'GEN', textVersion: 'v1', seg
* Builds a GEN book with `count` single-token verses in chapter 1. Used to exercise the segment
* window's recenter fade, which only triggers when the new active verse is outside the rendered
* window — impossible with the small fixtures above.
- *
- * @param count - Number of verses to generate.
- * @returns A {@link Book} with `count` chapter-1 segments.
*/
function makeLargeBook(count: number): Book {
const segments: Segment[] = [];
@@ -572,11 +506,6 @@ const GEN_SUPERSCRIPTION_BOOK: Book = withDefaultVerseStarts({
* `useInterlinearNav` call resolves. `Interlinearizer` writes the reference through the context's
* `navigate` (which calls the scroll-group hook's setter), so navigation assertions hang off the
* `navigate` spy supplied here.
- *
- * @param ui - The `` element to wrap.
- * @param navigate - Spy wired as the scroll-group hook's setter; receives the reference each
- * `navigate` call writes. Defaults to a noop.
- * @returns The element wrapped in a nav provider.
*/
function withNav(ui: ReactNode, navigate: (r: SerializedVerseRef) => void = () => {}): ReactNode {
const scrollGroupHook = (): ScrollGroupTuple => [
@@ -597,9 +526,6 @@ function withNav(ui: ReactNode, navigate: (r: SerializedVerseRef) => void = () =
* Renders an Interlinearizer component with sensible defaults, allowing individual props to be
* overridden per test. Wrapped in an {@link InterlinearNavProvider} via {@link withNav}; `navigate`
* is the spy that captures references the component writes through the context.
- *
- * @param options - Partial props to merge over the defaults.
- * @returns The render result from @testing-library/react.
*/
function renderInterlinearizer({
book = GEN_1_1_BOOK,
@@ -1293,9 +1219,6 @@ describe('Interlinearizer', () => {
* rect as zero). Every `[data-segment-id]` before the target is placed fully above the top edge
* (negative bottom); the target and those after it sit at/below it. The container reports top 0.
* Restored automatically by `restoreMocks`.
- *
- * @param orderedSegmentIds - Segment ids in document order.
- * @param topSegmentId - The segment id to place flush against the container top.
*/
function positionSegmentAtTop(orderedSegmentIds: string[], topSegmentId: string): void {
const targetIndex = orderedSegmentIds.indexOf(topSegmentId);
@@ -1634,8 +1557,6 @@ describe('Interlinearizer', () => {
* Stateful wrapper that feeds its own `ref` state as the `scrRef` prop to
* {@link Interlinearizer}, letting `updateRef` simulate navigate calls from outside the
* component tree.
- *
- * @returns The wrapped Interlinearizer element.
*/
function Wrapper() {
const [ref, setRef] = useState({
@@ -1725,8 +1646,6 @@ describe('segmentation dispatch force-break', () => {
/**
* Builds a raw segmentation-dispatch spy; the wrapped dispatch the views receive must delegate
* every call to it.
- *
- * @returns A dispatch whose methods are jest spies.
*/
function makeRawDispatch(): SegmentationDispatch {
return { merge: jest.fn(), split: jest.fn(), move: jest.fn() };
@@ -1735,10 +1654,6 @@ describe('segmentation dispatch force-break', () => {
/**
* Renders with continuous scroll on (so the stubbed ContinuousView captures the segmentation
* context) and returns the wrapped dispatch.
- *
- * @param raw - The raw dispatch handed to the component.
- * @param book - The book fixture to render.
- * @returns The wrapped dispatch captured from the segmentation context.
*/
function renderAndCaptureDispatch(raw: SegmentationDispatch, book: Book): SegmentationDispatch {
renderInterlinearizer({ book, continuousScroll: true, segmentationDispatch: raw });
@@ -1983,10 +1898,6 @@ describe('focus preservation across segmentation edits', () => {
/**
* Builds the wrapped `` element used by these tests, so the initial render and
* the post-edit rerender share identical props apart from the book.
- *
- * @param book - The (re)segmented book to render.
- * @param scrRef - The active scripture reference.
- * @returns The element wrapped in a nav provider.
*/
function interlinearizerEl(book: Book, scrRef: SerializedVerseRef): ReactNode {
return withNav(
@@ -2055,9 +1966,6 @@ describe('straddled boundary refs', () => {
* Renders with continuous scroll on (so the stubbed ContinuousView captures the segmentation
* context) and returns the straddled-boundary set the component computed. Phrase links must be
* seeded into `mockPhraseLinkById` before calling.
- *
- * @param book - The book fixture to render.
- * @returns The `straddledBoundaryRefs` set captured from the segmentation context.
*/
function renderAndCaptureStraddled(book: Book): ReadonlySet {
renderInterlinearizer({ book, continuousScroll: true });
@@ -2100,10 +2008,6 @@ describe('segmentationVersion pass-through', () => {
/**
* Builds the wrapped `` element for the pass-through tests, varying only the
* book and segmentation version between renders.
- *
- * @param book - The (re)segmented book to render.
- * @param segmentationVersion - The boundary-edit counter forwarded to the segment window.
- * @returns The element wrapped in a nav provider.
*/
function versionedEl(book: Book, segmentationVersion: number): ReactNode {
return withNav(
diff --git a/src/__tests__/components/InterlinearizerLoader.test.tsx b/src/__tests__/components/InterlinearizerLoader.test.tsx
index 6de4d185..70fa4d89 100644
--- a/src/__tests__/components/InterlinearizerLoader.test.tsx
+++ b/src/__tests__/components/InterlinearizerLoader.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for components/InterlinearizerLoader.tsx. */
///
///
@@ -113,12 +112,6 @@ jest.mock('../../components/modals/WipeModal', () => ({
/**
* Minimal WipeModal stand-in exposing per-scope confirm buttons and cancel so tests can drive the
* loader's wipe handlers without the real dialog's localization or scope-picker UI.
- *
- * @param hasActiveBook - Whether a book is loaded; surfaced so tests can assert the loader passes
- * it through.
- * @param onConfirm - Invoked with the chosen scope (`'book'` or `'all'`) when the user confirms.
- * @param onCancel - Invoked when the user backs out.
- * @returns A panel with a confirm button per scope plus cancel.
*/
WipeModal: ({
hasActiveBook,
@@ -215,17 +208,6 @@ jest.mock('../../components/modals/ProjectModals', () => ({
* loader now passes (`hasUnsavedWork`, `getDraftSnapshot`, `loadFromProject`, `markSynced`);
* `hasUnsavedWork` is surfaced as a `data-*` attribute so tests can assert the loader feeds it
* the combined committed-and-pending unsaved state.
- *
- * @param modal - Current modal identifier controlling which stub panel is rendered.
- * @param setModal - Callback to transition to a different modal state.
- * @param activeProject - The currently active interlinear project, or undefined when none is
- * selected.
- * @param defaultAnalysisLanguage - BCP 47 tag forwarded as the create modal's default language.
- * @param hasUnsavedWork - Whether the draft has committed-but-unsaved changes or uncommitted
- * in-progress typing; gates the discard confirmation in the real component.
- * @param useWebViewState - Injected hook used to read and write persisted WebView state; must
- * support the `'activeProject'` key.
- * @returns A JSX element containing the stub modal panels keyed by `modal`.
*/
default: function StubProjectModals({
modal,
@@ -360,11 +342,9 @@ jest.mock('../../components/modals/ProjectModals', () => ({
* group and WebView-state hooks. Centralizing the render keeps every call site supplying the
* required `updateWebViewDefinition` prop.
*
- * @param options - Optional overrides.
* @param options.useWebViewScrollGroupScrRef - Scroll-group hook; defaults to a GEN 1:1 stub.
* @param options.useWebViewState - WebView-state hook; defaults to a fresh empty store.
* @param options.projectId - Source project ID; defaults to {@link testProjectId}.
- * @returns The Testing Library render result plus the `updateWebViewDefinition` spy.
*/
function renderLoader(
options: {
@@ -385,11 +365,7 @@ function renderLoader(
return { ...result, updateWebViewDefinition };
}
-/**
- * Configures useInterlinearizerBookData to return the given state.
- *
- * @param overrides - Partial hook result; all fields default to a successful loaded state
- */
+/** Configures useInterlinearizerBookData to return the given state. */
function mockBookData(
overrides: Partial<{
book: Book | undefined;
@@ -415,7 +391,6 @@ function mockBookData(
* @param value - The current boolean value applied to every setting; defaults to `false`
* @param onChange - The change handler for every setting; defaults to a distinct jest.fn() per key
* @param isLoading - Whether the settings are loading; defaults to `false`
- * @returns A map from setting key to that key's `onChange` mock.
*/
function mockOptimisticSetting(
value = false,
@@ -1318,7 +1293,6 @@ describe('InterlinearizerLoader', () => {
* Returns the segmentation dispatch captured from the rendered interlinearizer, failing the
* test if none was captured.
*
- * @returns The captured `segmentationDispatch`.
* @throws If the interlinearizer did not render and capture a dispatch.
*/
function getSegmentationDispatch(): SegmentationDispatch {
@@ -1531,7 +1505,6 @@ describe('InterlinearizerLoader', () => {
* Renders the loader on {@link BOUNDARY_SHAPES_BOOK} and returns the captured segmentation
* dispatch so a test can drive boundary edits.
*
- * @returns The captured `segmentationDispatch`.
* @throws If the interlinearizer did not render and capture a dispatch.
*/
async function renderBoundaryBook(): Promise {
@@ -2024,8 +1997,6 @@ describe('InterlinearizerLoader', () => {
/**
* Reads the live opacity of the book-fade wrapper the loader renders from the context's fade
* phase.
- *
- * @returns The wrapper's inline `opacity` style value.
*/
function fadeOpacity(): string {
return screen.getByTestId('book-fade-wrapper').style.opacity;
@@ -2034,9 +2005,6 @@ describe('InterlinearizerLoader', () => {
/**
* Builds a scroll-group hook whose reference can be restaged between rerenders. A fresh object
* identity is required each change so the provider adopts it as a new `scrRef`.
- *
- * @param initial - The reference reported on the first render.
- * @returns A `[hook, setRef]` pair.
*/
function makeMutableScrollGroupHook(
initial: SerializedVerseRef,
@@ -2055,9 +2023,6 @@ describe('InterlinearizerLoader', () => {
* Renders the loader with a mutable scroll-group hook, returning a `rerenderNow` that rebuilds
* a fresh element so React re-invokes the component (the stub mutates a closure variable, not
* state, so an identical element would let React bail out).
- *
- * @param initial - The scroll-group reference reported on the first render.
- * @returns `setRef` to stage the next reference and `rerenderNow` to re-render with it.
*/
function renderFadeLoader(initial: SerializedVerseRef) {
const [scrollGroupHook, setRef] = makeMutableScrollGroupHook(initial);
diff --git a/src/__tests__/components/MorphemeBox.test.tsx b/src/__tests__/components/MorphemeBox.test.tsx
index a5fb2082..52f8943b 100644
--- a/src/__tests__/components/MorphemeBox.test.tsx
+++ b/src/__tests__/components/MorphemeBox.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for components/MorphemeBox.tsx. */
///
///
@@ -37,9 +36,6 @@ const MORPHEMES: MorphemeAnalysis[] = [
/**
* Renders {@link MorphemeBox} with required props defaulted so each test overrides only what it
* asserts on.
- *
- * @param props - Overrides merged over the defaults.
- * @returns The render result.
*/
function renderBox(props: Partial[0]> = {}) {
return render(
diff --git a/src/__tests__/components/MorphemeEditor.test.tsx b/src/__tests__/components/MorphemeEditor.test.tsx
index 493058e2..554200d3 100644
--- a/src/__tests__/components/MorphemeEditor.test.tsx
+++ b/src/__tests__/components/MorphemeEditor.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for components/MorphemeEditor.tsx. */
///
///
@@ -28,10 +27,6 @@ beforeEach(() => {
/**
* Renders {@link MorphemeBreakdownPopover} with the two structural props (`surfaceText`,
* `glossInputId`) defaulted so each test only supplies what it asserts on.
- *
- * @param props - Overrides merged over the defaults; callers pass their own `onSave`/`onClose`
- * spies.
- * @returns The render result.
*/
function renderPopover(props: Partial> = {}) {
return render(
@@ -379,9 +374,6 @@ describe('MorphemeBreakdownPopover', () => {
/**
* Renders the popover on a glossed, solely-linked breakdown — the state in which a reset is
* irreversible, so both reset routes confirm first.
- *
- * @param props - Overrides merged over the confirming defaults.
- * @returns The render result.
*/
function renderConfirming(
props: Partial> = {},
diff --git a/src/__tests__/components/PhraseBox.test.tsx b/src/__tests__/components/PhraseBox.test.tsx
index c71602a0..ddf2382c 100644
--- a/src/__tests__/components/PhraseBox.test.tsx
+++ b/src/__tests__/components/PhraseBox.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for PhraseBox component. */
///
///
@@ -29,13 +28,7 @@ const mockUsePhraseGlossDispatch = jest.fn().mockReturnValue(jest.fn());
jest.mock('../../components/AnalysisStore', () => ({
__esModule: true,
- /**
- * Pass-through AnalysisStoreProvider stub.
- *
- * @param props - Component props.
- * @param props.children - Children to render.
- * @returns Children unchanged.
- */
+ /** Pass-through AnalysisStoreProvider stub. */
AnalysisStoreProvider({ children }: Readonly<{ children: import('react').ReactNode }>) {
return children;
},
@@ -54,17 +47,6 @@ jest.mock('../../components/TokenChip', () => {
* Minimal TokenChip stub that renders the token's surface text, a controlled gloss input, and an
* optional remove button. Lets PhraseBox tests verify gloss-forwarding, focus callbacks, and
* token-removal interactions without pulling in the real TokenChip implementation.
- *
- * @param props - Component props.
- * @param props.onFocus - Called when the gloss input receives focus.
- * @param props.token - The word token to render.
- * @param props.isSplitFree - When true, marks the chip as a would-be-free token.
- * @param props.onRemove - Called when the remove button is clicked; omitted for edge tokens.
- * @param props.showMorphology - Exposed as a data attribute so tests can verify every PhraseBox
- * render path forwards the strip-wide morphology toggle. When true, also renders a
- * `data-morpheme-gloss` input before the main gloss input, mirroring the real chip's DOM order
- * so focus-routing tests exercise the morpheme-exclusion selector.
- * @returns A span containing the surface text, a gloss input, and an optional remove button.
*/
function MockTokenChip({
onFocus,
@@ -108,13 +90,7 @@ jest.mock('../../components/TokenChip', () => {
);
}
- /**
- * Minimal InertTokenChip stub rendering the token's surface text.
- *
- * @param props - Component props.
- * @param props.token - The non-word token to render.
- * @returns A span containing the surface text.
- */
+ /** Minimal InertTokenChip stub rendering the token's surface text. */
function MockInertTokenChip({ token }: Readonly<{ token: Token }>) {
return {token.surfaceText} ;
}
@@ -123,13 +99,7 @@ jest.mock('../../components/TokenChip', () => {
jest.mock('../../components/modals/UnlinkPhraseConfirm', () => ({
__esModule: true,
- /**
- * Minimal UnlinkPhraseConfirm stub that renders confirm/cancel buttons.
- *
- * @param props - Component props.
- * @param props.setPhraseMode - Called to exit confirm-unlink mode.
- * @returns A stub div with confirm and cancel buttons.
- */
+ /** Minimal UnlinkPhraseConfirm stub that renders confirm/cancel buttons. */
default: ({
setPhraseMode,
}: Readonly<{ phraseId: string; setPhraseMode: (m: unknown) => void }>) => (
@@ -207,8 +177,6 @@ type PhraseBoxTestProps = {
/**
* Minimal required props for PhraseBox. Spread into render calls so tests only need to override
* what they actually care about.
- *
- * @returns An object containing all required PhraseBox props set to no-op stubs.
*/
function requiredProps(): PhraseBoxTestProps {
return {
@@ -224,10 +192,6 @@ function requiredProps(): PhraseBoxTestProps {
* Renders a `PhraseBox` wrapped in both the analysis store and strip-context providers. Strip-wide
* state (phrase mode, edit context, hover callbacks) now comes from `PhraseStripContext`, so tests
* pass those as `context` overrides rather than as props.
- *
- * @param ui - The `PhraseBox` element to render.
- * @param context - Partial strip-context overrides (phraseMode, edit context, hover callbacks).
- * @returns The Testing Library render result.
*/
function renderBox(ui: ReactElement, context: Partial = {}) {
return render(
diff --git a/src/__tests__/components/PhraseStripContext.test.tsx b/src/__tests__/components/PhraseStripContext.test.tsx
index 47d96417..0b8787b6 100644
--- a/src/__tests__/components/PhraseStripContext.test.tsx
+++ b/src/__tests__/components/PhraseStripContext.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for components/PhraseStripContext.tsx. */
///
///
@@ -9,8 +8,6 @@ import { makePhraseStripContext } from '../test-helpers';
/**
* Test consumer that reads the strip context and renders one of its values so the test can assert
* the provided value reached the consumer.
- *
- * @returns A span containing the resolved `editPhraseSegmentId`.
*/
function Consumer() {
const { editPhraseSegmentId } = usePhraseStripContext();
diff --git a/src/__tests__/components/PhraseStripParts.test.tsx b/src/__tests__/components/PhraseStripParts.test.tsx
index 2a34d718..62d4ee4e 100644
--- a/src/__tests__/components/PhraseStripParts.test.tsx
+++ b/src/__tests__/components/PhraseStripParts.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for components/PhraseStripParts.tsx. */
///
///
@@ -81,13 +80,7 @@ jest.mock('../../components/PhraseBox', () => ({
// Helpers
// ---------------------------------------------------------------------------
-/**
- * Creates a punctuation token fixture.
- *
- * @param ref - Token ref.
- * @param surfaceText - Surface text.
- * @returns A punctuation token.
- */
+/** Creates a punctuation token fixture. */
function mkPunct(ref: string, surfaceText = '.'): Token {
return { ref, surfaceText, writingSystem: 'en', type: 'punctuation', charStart: 0, charEnd: 1 };
}
@@ -111,9 +104,6 @@ function slotProps(slot: LinkSlot): Parameters[0] {
/**
* Wraps `ui` in a {@link PhraseStripProvider} with default context so components that call
* {@link usePhraseStripContext} can render without a provider in the tree.
- *
- * @param ui - The element to wrap.
- * @returns The wrapped element.
*/
function withProvider(ui: ReactElement): ReactElement {
return {ui} ;
@@ -366,10 +356,9 @@ describe('PhraseSlot boundary controls', () => {
/**
* Renders a PhraseSlot inside all three providers (segmentation, phrase strip, and Alt-held).
*
- * @param props - Overrides for the slot props (e.g. prev/next segment ids).
+ * @param props - Overrides merged over the default `PhraseSlot` props.
* @param options - Optional fixture overrides: merged-away boundaries, straddled boundary refs,
* the phrase mode, and whether Alt is held (defaults to held, so the split marker appears).
- * @returns The dispatch, for asserting on its calls.
*/
function renderBoundary(
props: Partial[0]>,
@@ -776,13 +765,7 @@ describe('MemoizedPhraseGroup', () => {
// ---------------------------------------------------------------------------
describe('PhraseStrip', () => {
- /**
- * Builds default `PhraseStrip` props with the given items and overrides.
- *
- * @param items - The normalized strip items to render.
- * @param overrides - Partial prop overrides.
- * @returns A complete `PhraseStrip` props object.
- */
+ /** Builds default `PhraseStrip` props with the given items and overrides. */
function stripProps(
items: StripItem[],
overrides: Partial[0]> = {},
@@ -802,13 +785,7 @@ describe('PhraseStrip', () => {
};
}
- /**
- * Builds a group strip item for a single phrase link.
- *
- * @param link - The phrase link (or `undefined` for a solo token).
- * @param refs - Token refs in the group.
- * @returns A group {@link StripItem}.
- */
+ /** Builds a group strip item for a single phrase link. */
function groupItem(link: PhraseAnalysisLink | undefined, refs: string[]): StripItem {
const tokens = refs.map((r) => makeWordToken(r));
return {
diff --git a/src/__tests__/components/SegmentView.test.tsx b/src/__tests__/components/SegmentView.test.tsx
index 409586b9..a10ac4a6 100644
--- a/src/__tests__/components/SegmentView.test.tsx
+++ b/src/__tests__/components/SegmentView.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for components/SegmentView.tsx. */
///
///
@@ -217,8 +216,6 @@ const PUNCT_SEGMENT: Segment = {
/**
* Minimal required props for SegmentView. Spread into render calls so tests only need to override
* what they actually care about.
- *
- * @returns An object containing all required SegmentView props set to no-op stubs.
*/
function requiredProps(): {
displayMode: 'token-chip';
@@ -579,10 +576,6 @@ describe('SegmentView', () => {
/**
* Renders a SegmentView in baseline-text mode wrapped in the segmentation and Alt-held
* providers, so the split gap markers can be exercised.
- *
- * @param options - Fixture overrides: the segment, whether Alt is held, the phrase mode, and
- * the straddled/former-boundary maps.
- * @returns The dispatch spy and the onSelect spy for assertions.
*/
function renderBaseline(
options: {
diff --git a/src/__tests__/components/SegmentationStore.test.tsx b/src/__tests__/components/SegmentationStore.test.tsx
index 49915580..e629e996 100644
--- a/src/__tests__/components/SegmentationStore.test.tsx
+++ b/src/__tests__/components/SegmentationStore.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for components/SegmentationStore.tsx. */
///
///
diff --git a/src/__tests__/components/TokenChip.suggestions.test.tsx b/src/__tests__/components/TokenChip.suggestions.test.tsx
index 4d0d8a13..f4c11d21 100644
--- a/src/__tests__/components/TokenChip.suggestions.test.tsx
+++ b/src/__tests__/components/TokenChip.suggestions.test.tsx
@@ -25,13 +25,7 @@ beforeEach(() => {
Element.prototype.scrollIntoView = jest.fn();
});
-/**
- * Builds a word token spanning its surface text.
- *
- * @param ref - The token ref.
- * @param surfaceText - The token's surface text.
- * @returns A word `Token`.
- */
+/** Builds a word token spanning its surface text. */
function wordToken(ref: string, surfaceText: string): Token & { type: 'word' } {
return {
ref,
@@ -46,11 +40,6 @@ function wordToken(ref: string, surfaceText: string): Token & { type: 'word' } {
/**
* Builds an analysis seeding one approved payload, so a different token with the same surface form
* resolves to it as a suggestion.
- *
- * @param gloss - The English gloss on the approved payload, or `undefined` for a payload with no
- * English gloss.
- * @param surfaceText - The approved payload's surface form.
- * @returns A `TextAnalysis` with one approved link from `tok-approved`.
*/
function poolWithOneApproved(gloss: string | undefined, surfaceText = 'logos'): TextAnalysis {
const ta: TokenAnalysis = {
@@ -69,9 +58,6 @@ function poolWithOneApproved(gloss: string | undefined, surfaceText = 'logos'):
/**
* Builds an analysis for the homograph 'bank': `riverbank` approved twice (the suggested pick) and
* `finance` approved once (a candidate).
- *
- * @param financeGloss - The English gloss on the candidate payload, or `undefined` for none.
- * @returns A `TextAnalysis` with two competing approved payloads for 'bank'.
*/
function homographBankPool(financeGloss: string | undefined): TextAnalysis {
const river: TokenAnalysis = { id: 'ta-river', surfaceText: 'bank', gloss: { en: 'riverbank' } };
@@ -92,8 +78,6 @@ function homographBankPool(financeGloss: string | undefined): TextAnalysis {
* Builds the homograph 'bank' where the MOST-frequent analysis has no active-language (English)
* gloss — only French — and a lower-frequency one carries `en:'finance'`. Exercises falling through
* a blank-in-active-language top pick to the next glossed analysis.
- *
- * @returns A `TextAnalysis` whose top-ranked 'bank' payload is blank in English.
*/
function homographTopBlankPool(): TextAnalysis {
const blank: TokenAnalysis = { id: 'ta-blank', surfaceText: 'bank', gloss: { fr: 'rive' } };
@@ -106,17 +90,7 @@ function homographTopBlankPool(): TextAnalysis {
return { ...emptyAnalysis(), tokenAnalyses: [blank, fin], tokenAnalysisLinks: links };
}
-/**
- * Renders a {@link TokenChip} inside a real provider seeded with the given analysis pool.
- *
- * @param token - The word token to render.
- * @param options - Provider configuration.
- * @param options.initialAnalysis - The seed analysis (the pool).
- * @param options.showSuggestions - Whether the provider opts into suggestions (default `true`).
- * @param options.onSave - Optional save spy.
- * @param options.onGlossChange - Optional gloss-write spy.
- * @returns The Testing Library render result.
- */
+/** Renders a {@link TokenChip} inside a real provider seeded with the given analysis pool. */
function renderChip(
token: Token & { type: 'word' },
{
@@ -147,9 +121,6 @@ function renderChip(
/**
* Focuses a chip's gloss input (which opens the dropdown whenever the token has suggestions),
* returning the input element.
- *
- * @param surfaceText - The token's surface form, used to find the labeled input.
- * @returns The focused gloss input element.
*/
async function focusGloss(surfaceText: string): Promise {
const input = screen.getByLabelText(`Gloss for ${surfaceText}`);
@@ -675,8 +646,6 @@ describe('TokenChip suggestion dropdown scrolling', () => {
* Stubs the gloss input's `getBoundingClientRect` so the dropdown's scroll handler sees a
* definite on- or off-screen anchor (jsdom returns an all-zero rect by default, which reads as
* on-screen).
- *
- * @param top - The faked viewport-relative top of the input; `bottom` is `top + 10`.
*/
function stubGlossRect(top: number): void {
jest.spyOn(HTMLInputElement.prototype, 'getBoundingClientRect').mockReturnValue({
diff --git a/src/__tests__/components/TokenChip.test.tsx b/src/__tests__/components/TokenChip.test.tsx
index 5486bd93..314fb898 100644
--- a/src/__tests__/components/TokenChip.test.tsx
+++ b/src/__tests__/components/TokenChip.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for components/TokenChip.tsx. */
///
///
@@ -21,12 +20,7 @@ beforeEach(() => {
jest.mocked(useLocalizedStrings).mockReturnValue([LOCALIZED, false]);
});
jest.mock('../../components/MorphemeEditor', () => ({
- /**
- * Stub popover that renders a save button so tests can trigger onSave.
- *
- * @param props - Receives the same props as the real popover.
- * @returns A test stub element with a save button.
- */
+ /** Stub popover that renders a save button so tests can trigger onSave. */
MorphemeBreakdownPopover({
onSave,
onClose,
@@ -63,9 +57,6 @@ jest.mock('../../components/MorphemeBox', () => ({
* Stub box that surfaces its `onEditBreakdown` callback as a button so analyzed-path tests can
* open the editor, and echoes its `disabled`/`popoverOpen` props for assertions. The box's grid
* internals (forms, gloss inputs, RTL order, hover, active look) are tested in MorphemeBox.test.
- *
- * @param props - Receives the same props as the real box.
- * @returns A test stub element with an edit-breakdown trigger.
*/
MorphemeBox({
onEditBreakdown,
@@ -101,8 +92,6 @@ const WORD_TOKEN = {
/**
* Minimal required props for {@link TokenChip}. Spread into render calls so tests only need to
* override what they actually care about.
- *
- * @returns An object with all required props set to no-op stubs.
*/
function requiredProps(): { token: Token & { type: 'word' }; onFocus: () => void } {
return {
diff --git a/src/__tests__/components/TokenLinkIcon.test.tsx b/src/__tests__/components/TokenLinkIcon.test.tsx
index 4cce3785..7153006e 100644
--- a/src/__tests__/components/TokenLinkIcon.test.tsx
+++ b/src/__tests__/components/TokenLinkIcon.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for components/TokenLinkIcon.tsx. */
///
///
@@ -40,9 +39,6 @@ jest.mock('../../components/AnalysisStore', () => ({
* Builds a `slotFocus` bundle. Defaults to "no focus, same segment" — the baseline used by the
* link-icon tests, which then layer on `focusedSideIsPrev` / `focusedPhraseLink` /
* `focusedFreeToken` as needed.
- *
- * @param overrides - Fields to override on the default bundle.
- * @returns A `SlotFocusInfo`.
*/
function slotFocus(overrides: Partial = {}): SlotFocusInfo {
return {
@@ -69,10 +65,6 @@ function requiredProps(): ComponentProps {
/**
* Renders a `TokenLinkIcon` inside a `PhraseStripProvider`. The phrase mode, document-order lookup,
* and hover callbacks now come from strip context rather than props.
- *
- * @param ui - The `TokenLinkIcon` element to render.
- * @param context - Partial strip-context overrides (phraseMode, tokenDocOrder, hover callbacks).
- * @returns The Testing Library render result.
*/
function renderIcon(ui: ReactElement, context: Partial = {}) {
return render(
@@ -533,9 +525,6 @@ describe('TokenLinkIcon', () => {
/**
* Renders a `TokenLinkIcon` for a slot straddling a segment boundary (neighbors in different
* segments, so `isSameSegmentAsFocus` is false).
- *
- * @param focusedSideIsPrev - Which side holds focus.
- * @returns The render result.
*/
function renderCrossSegment(focusedSideIsPrev: boolean) {
return render(
diff --git a/src/__tests__/components/controls/EditPhraseControls.test.tsx b/src/__tests__/components/controls/EditPhraseControls.test.tsx
index 3bcc0bbc..64b740fd 100644
--- a/src/__tests__/components/controls/EditPhraseControls.test.tsx
+++ b/src/__tests__/components/controls/EditPhraseControls.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for components/EditPhraseControls.tsx. */
///
///
@@ -35,7 +34,6 @@ describe('EditPhraseControls', () => {
* link for `phrase-1` containing `tokenCount` tokens so the live phrase has a known size. A
* `tokenCount` of `0` returns an empty map, so the component sees the phrase as absent.
*
- * @param tokenCount - Number of tokens to place in the seeded phrase link; `0` omits the link.
* @returns A map from phrase id to its `PhraseAnalysisLink`, empty when `tokenCount` is `0`.
*/
function makePhraseLinkByIdMap(tokenCount: number): Map {
@@ -44,13 +42,7 @@ describe('EditPhraseControls', () => {
return new Map([['phrase-1', makePhraseLink('phrase-1', tokenRefs)]]);
}
- /**
- * Renders `EditPhraseControls` with the phrase-link map stubbed to a phrase of the given size.
- *
- * @param tokenCount - Number of tokens in the seeded phrase.
- * @param setPhraseMode - Setter spy passed to the component.
- * @returns The rendered element tree.
- */
+ /** Renders `EditPhraseControls` with the phrase-link map stubbed to a phrase of the given size. */
function renderWithPhraseSize(tokenCount: number, setPhraseMode: jest.Mock): ReactElement {
mockUsePhraseLinkByIdMap.mockReturnValue(makePhraseLinkByIdMap(tokenCount));
return ;
diff --git a/src/__tests__/components/controls/ScriptureNavControls.test.tsx b/src/__tests__/components/controls/ScriptureNavControls.test.tsx
index 5280c916..ebf58bb0 100644
--- a/src/__tests__/components/controls/ScriptureNavControls.test.tsx
+++ b/src/__tests__/components/controls/ScriptureNavControls.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for components/ScriptureNavControls.tsx. */
///
///
diff --git a/src/__tests__/components/controls/ViewOptionsDropdown.test.tsx b/src/__tests__/components/controls/ViewOptionsDropdown.test.tsx
index 21acbdc3..7147fbd7 100644
--- a/src/__tests__/components/controls/ViewOptionsDropdown.test.tsx
+++ b/src/__tests__/components/controls/ViewOptionsDropdown.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for components/ViewOptionsDropdown.tsx. */
///
///
diff --git a/src/__tests__/components/modals/CreateProjectModal.test.tsx b/src/__tests__/components/modals/CreateProjectModal.test.tsx
index 23615f88..ff9f5b70 100644
--- a/src/__tests__/components/modals/CreateProjectModal.test.tsx
+++ b/src/__tests__/components/modals/CreateProjectModal.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for CreateProjectModal (configures a new draft; does not persist a project). */
///
///
diff --git a/src/__tests__/components/modals/DiscardDraftConfirm.test.tsx b/src/__tests__/components/modals/DiscardDraftConfirm.test.tsx
index b9889a5e..f47b606b 100644
--- a/src/__tests__/components/modals/DiscardDraftConfirm.test.tsx
+++ b/src/__tests__/components/modals/DiscardDraftConfirm.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for DiscardDraftConfirm. */
///
///
diff --git a/src/__tests__/components/modals/ProjectMetadataModal.test.tsx b/src/__tests__/components/modals/ProjectMetadataModal.test.tsx
index f694390b..3ec9a710 100644
--- a/src/__tests__/components/modals/ProjectMetadataModal.test.tsx
+++ b/src/__tests__/components/modals/ProjectMetadataModal.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for ProjectMetadataModal. */
///
///
diff --git a/src/__tests__/components/modals/ProjectModals.test.tsx b/src/__tests__/components/modals/ProjectModals.test.tsx
index 2a685231..9fe66b46 100644
--- a/src/__tests__/components/modals/ProjectModals.test.tsx
+++ b/src/__tests__/components/modals/ProjectModals.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for ProjectModals (draft-based New / Open / Save As / metadata routing). */
///
///
@@ -285,9 +284,6 @@ type ModalsOverrides = Partial<{
/**
* Builds a complete ProjectModals prop set, filling required props with sensible defaults so each
* test only specifies what it cares about.
- *
- * @param overrides - Props to override.
- * @returns The full prop object to spread onto ProjectModals.
*/
function buildProps(overrides: ModalsOverrides = {}) {
return {
@@ -310,11 +306,6 @@ function buildProps(overrides: ModalsOverrides = {}) {
* `'activeProject'` key, so a test can assert how the active project was updated or cleared. Any
* unspecified spy — and every key other than `'activeProject'` — gets a no-op. Mirrors
* {@link makeWebViewState}'s `[value, setter, reset]` shape without type assertions.
- *
- * @param spies - The spies to install for the `'activeProject'` slot.
- * @param spies.set - Spy invoked when the `'activeProject'` slot's setter is called.
- * @param spies.reset - Spy invoked when the `'activeProject'` slot is reset.
- * @returns A `useWebViewState`-shaped hook stub.
*/
function makeWebViewStateWithActiveProjectSpies({
set = () => {},
diff --git a/src/__tests__/components/modals/ProjectSummaryDetails.test.tsx b/src/__tests__/components/modals/ProjectSummaryDetails.test.tsx
index 454cba4e..2c7b98e1 100644
--- a/src/__tests__/components/modals/ProjectSummaryDetails.test.tsx
+++ b/src/__tests__/components/modals/ProjectSummaryDetails.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for the shared ProjectSummaryDetails row-detail block. */
///
///
diff --git a/src/__tests__/components/modals/SaveAsProjectModal.test.tsx b/src/__tests__/components/modals/SaveAsProjectModal.test.tsx
index d1019c4f..1adf33d8 100644
--- a/src/__tests__/components/modals/SaveAsProjectModal.test.tsx
+++ b/src/__tests__/components/modals/SaveAsProjectModal.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for SaveAsProjectModal. */
///
///
diff --git a/src/__tests__/components/modals/SelectInterlinearProjectModal.test.tsx b/src/__tests__/components/modals/SelectInterlinearProjectModal.test.tsx
index 726f60ee..4fe25ea5 100644
--- a/src/__tests__/components/modals/SelectInterlinearProjectModal.test.tsx
+++ b/src/__tests__/components/modals/SelectInterlinearProjectModal.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for SelectInterlinearProjectModal. */
///
///
diff --git a/src/__tests__/components/modals/UnlinkPhraseConfirm.test.tsx b/src/__tests__/components/modals/UnlinkPhraseConfirm.test.tsx
index 458c669d..5e8de1fa 100644
--- a/src/__tests__/components/modals/UnlinkPhraseConfirm.test.tsx
+++ b/src/__tests__/components/modals/UnlinkPhraseConfirm.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for UnlinkPhraseConfirm component. */
///
///
diff --git a/src/__tests__/components/modals/WipeModal.test.tsx b/src/__tests__/components/modals/WipeModal.test.tsx
index 10c739e3..d547430f 100644
--- a/src/__tests__/components/modals/WipeModal.test.tsx
+++ b/src/__tests__/components/modals/WipeModal.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for WipeModal. */
///
///
diff --git a/src/__tests__/components/test-helpers.tsx b/src/__tests__/components/test-helpers.tsx
index 8f7f6b59..a21c627c 100644
--- a/src/__tests__/components/test-helpers.tsx
+++ b/src/__tests__/components/test-helpers.tsx
@@ -1,5 +1,3 @@
-/** @file Shared component-test render helpers. */
-
import type { ReactNode } from 'react';
import { AnalysisStoreProvider } from '../../components/AnalysisStore';
import { ViewOptions } from '../../types/view-options';
diff --git a/src/__tests__/hooks/useAltHeld.test.ts b/src/__tests__/hooks/useAltHeld.test.ts
index 8ddc518c..cb7ecc72 100644
--- a/src/__tests__/hooks/useAltHeld.test.ts
+++ b/src/__tests__/hooks/useAltHeld.test.ts
@@ -1,4 +1,3 @@
-/** @file Unit tests for the useAltHeld hook. */
///
import { act, renderHook } from '@testing-library/react';
diff --git a/src/__tests__/hooks/useArcPaths.test.ts b/src/__tests__/hooks/useArcPaths.test.ts
index 7ef27290..4dd216ac 100644
--- a/src/__tests__/hooks/useArcPaths.test.ts
+++ b/src/__tests__/hooks/useArcPaths.test.ts
@@ -1,4 +1,3 @@
-/** @file Unit tests for hooks/useArcPaths.ts. */
///
import { act, renderHook } from '@testing-library/react';
@@ -82,8 +81,6 @@ describe('useArcPaths', () => {
/**
* Fires the ResizeObserver callback and flushes the queued rAF, then reports how many
* measurement passes (`computeAllArcPaths` calls) that observer tick triggered.
- *
- * @returns The number of `computeAllArcPaths` calls made during this pump.
*/
const pump = (): number => {
const before = computeAllArcPaths.mock.calls.length;
@@ -323,12 +320,7 @@ describe('useArcPaths', () => {
});
describe('settle-aware measurement', () => {
- /**
- * Builds a minimal measurement result whose signature is distinguished by the arc's `d` string.
- *
- * @param d - The SVG path string to embed in the single returned arc.
- * @returns A `computeAllArcPaths`-shaped result with one arc and zero paddings.
- */
+ /** Builds a minimal measurement result whose signature is distinguished by the arc's `d` string. */
const measurementWithPath = (d: string) => ({
paths: [
{ phraseId: 'p1', d, midX: 5, midY: 0, runLeft: 0, runRight: 10, splitAfterTokenRef: 't' },
diff --git a/src/__tests__/hooks/useDraftProject.test.ts b/src/__tests__/hooks/useDraftProject.test.ts
index 6516d6f9..49f397e5 100644
--- a/src/__tests__/hooks/useDraftProject.test.ts
+++ b/src/__tests__/hooks/useDraftProject.test.ts
@@ -1,4 +1,3 @@
-/** @file Unit tests for useDraftProject hook. */
///
import papi, { logger } from '@papi/frontend';
@@ -13,12 +12,7 @@ const PLATFORM_LANGUAGE = 'en';
/** Handle to the mocked PAPI sendCommand so tests can assert on / override its calls. */
const mockSendCommand = jest.mocked(papi.commands.sendCommand);
-/**
- * Builds a `DraftProject` for seeding the `getDraft` mock, overriding any fields needed by a test.
- *
- * @param overrides - Partial fields to merge over the baseline draft.
- * @returns A fresh `DraftProject` with the overrides applied.
- */
+/** Builds a `DraftProject` for seeding the `getDraft` mock, overriding any fields needed by a test. */
function makeDraft(overrides: Partial = {}): DraftProject {
return {
sourceProjectId: SOURCE_PROJECT_ID,
@@ -32,9 +26,6 @@ function makeDraft(overrides: Partial = {}): DraftProject {
/**
* Builds a `TextAnalysis` carrying a single token analysis so tests can prove a specific analysis
* object round-trips through the draft.
- *
- * @param id - Identifier for the lone token analysis, used to distinguish instances.
- * @returns A `TextAnalysis` containing one token analysis with the given id.
*/
function analysisWithToken(id: string): TextAnalysis {
return {
@@ -49,8 +40,6 @@ function analysisWithToken(id: string): TextAnalysis {
/**
* Points the `getDraft` command at a resolved JSON draft while leaving `saveDraft` resolving void.
* All other commands resolve undefined so an unexpected call never rejects.
- *
- * @param draft - The draft the `getDraft` command should return (JSON-serialized).
*/
function mockGetDraftResolves(draft: DraftProject): void {
mockSendCommand.mockImplementation((...args: Parameters) => {
@@ -62,8 +51,6 @@ function mockGetDraftResolves(draft: DraftProject): void {
/**
* Renders the hook and waits for the initial `getDraft` load to settle so `draft` is populated and
* `isDraftLoading` is false before a test exercises a callback.
- *
- * @returns The renderHook result handle, post-load.
*/
async function renderLoaded() {
const view = renderHook(() => useDraftProject(SOURCE_PROJECT_ID, PLATFORM_LANGUAGE));
@@ -74,7 +61,6 @@ async function renderLoaded() {
/**
* Returns the JSON payload of the most recent `saveDraft` call, parsed back into a `DraftProject`.
*
- * @returns The persisted draft from the latest `saveDraft` invocation.
* @throws If no `saveDraft` call has been made, or its second argument was not a string.
*/
function lastSavedDraft(): DraftProject {
diff --git a/src/__tests__/hooks/useInterlinearizerBookData.test.ts b/src/__tests__/hooks/useInterlinearizerBookData.test.ts
index d1a16e41..2c4f3ab5 100644
--- a/src/__tests__/hooks/useInterlinearizerBookData.test.ts
+++ b/src/__tests__/hooks/useInterlinearizerBookData.test.ts
@@ -1,4 +1,3 @@
-/** @file Unit tests for useInterlinearizerBookData hook. */
///
import { logger } from '@papi/frontend';
@@ -89,11 +88,7 @@ const TEST_BOOK: Book = {
const GEN_1_1_SRC_REF = { book: 'GEN', chapterNum: 1, verseNum: 1 };
-/**
- * Mocks useProjectSetting to return a specified default state.
- *
- * @param defaultState - The value to return as the current setting state.
- */
+/** Mocks useProjectSetting to return a specified default state. */
function mockUseProjectSettings(defaultState: string | PlatformError | undefined) {
jest.mocked(useProjectSetting).mockReturnValue([defaultState, jest.fn(), jest.fn(), false]);
}
diff --git a/src/__tests__/hooks/useOptimisticBooleanSetting.test.ts b/src/__tests__/hooks/useOptimisticBooleanSetting.test.ts
index 020215ee..429764e9 100644
--- a/src/__tests__/hooks/useOptimisticBooleanSetting.test.ts
+++ b/src/__tests__/hooks/useOptimisticBooleanSetting.test.ts
@@ -1,4 +1,3 @@
-/** @file Unit tests for useOptimisticBooleanSetting hook. */
///
import { useProjectSetting } from '@papi/frontend/react';
@@ -8,11 +7,7 @@ import useOptimisticBooleanSetting from '../../hooks/useOptimisticBooleanSetting
/** Mock function for setting project settings. */
const mockSetSetting = jest.fn();
-/**
- * Mocks useProjectSetting to return a specified default state.
- *
- * @param defaultState - The value to return as the current setting state.
- */
+/** Mocks useProjectSetting to return a specified default state. */
function mockUseProjectSettings(defaultState: boolean | undefined) {
jest.mocked(useProjectSetting).mockReturnValue([defaultState, mockSetSetting, jest.fn(), false]);
}
diff --git a/src/__tests__/hooks/usePhraseHoverState.test.ts b/src/__tests__/hooks/usePhraseHoverState.test.ts
index 50b5c029..dd159223 100644
--- a/src/__tests__/hooks/usePhraseHoverState.test.ts
+++ b/src/__tests__/hooks/usePhraseHoverState.test.ts
@@ -1,4 +1,3 @@
-/** @file Unit tests for the usePhraseHoverState hook. */
///
import { act, renderHook } from '@testing-library/react';
diff --git a/src/__tests__/hooks/useSegmentWindow.test.ts b/src/__tests__/hooks/useSegmentWindow.test.ts
index 2b67e0f9..371ef787 100644
--- a/src/__tests__/hooks/useSegmentWindow.test.ts
+++ b/src/__tests__/hooks/useSegmentWindow.test.ts
@@ -18,13 +18,8 @@ declare global {
}
/**
- * Builds a single-token word segment for the given chapter/verse. Token surface text is irrelevant
- * to windowing, so it is a fixed stub.
- *
- * @param chapter - Chapter number for the segment's refs.
- * @param verse - Verse number for the segment's refs.
- * @param book - Book code for the segment's refs; defaults to `GEN`.
- * @returns A minimal {@link Segment}.
+ * Builds a single-token word segment for the given chapter/verse, in `book` (default `GEN`). Token
+ * surface text is irrelevant to windowing, so it is a fixed stub.
*/
function makeSegment(chapter: number, verse: number, book = 'GEN'): Segment {
return {
@@ -48,12 +43,7 @@ function makeSegment(chapter: number, verse: number, book = 'GEN'): Segment {
/**
* Builds a book whose segments span two chapters: `chapter1Count` verses in chapter 1 followed by
- * `chapter2Count` verses in chapter 2.
- *
- * @param chapter1Count - Number of verses in chapter 1.
- * @param chapter2Count - Number of verses in chapter 2.
- * @param book - Book code for the book and its segments; defaults to `GEN`.
- * @returns A {@link Book} with the combined flat segment list.
+ * `chapter2Count` verses in chapter 2, all in `book` (default `GEN`).
*/
function makeBook(chapter1Count: number, chapter2Count: number, book = 'GEN'): Book {
const segments: Segment[] = [];
@@ -65,10 +55,6 @@ function makeBook(chapter1Count: number, chapter2Count: number, book = 'GEN'): B
/**
* Renders {@link useSegmentWindow} with a real, attached scroll container so sentinel ref callbacks
* register with the stubbed observer and `scrollHeight`/`scrollTop` are writable for assertions.
- *
- * @param book - The book to window.
- * @param scrRef - The scripture reference whose verse anchors the window.
- * @returns The render-hook result plus the scroll container element.
*/
function renderSegmentWindow(
book: Book,
@@ -129,11 +115,6 @@ function renderSegmentWindow(
/**
* Mounts the rendered window's sentinel elements into `container` so the observer has real targets.
* Returns the created top/bottom elements.
- *
- * @param container - The scroll container the sentinels live in.
- * @param topRef - The hook's top sentinel ref callback.
- * @param bottomRef - The hook's bottom sentinel ref callback.
- * @returns The mounted sentinel elements.
*/
function mountSentinels(
container: HTMLElement,
@@ -154,9 +135,6 @@ function mountSentinels(
/**
* Installs a stub `ResizeObserver` that records the most recently created callback and the elements
* it observes, for tests that drive the scroll-compensation / re-snap observer by hand.
- *
- * @returns `fire` to invoke the recorded observer callback, `observedTargets` to read the elements
- * the most recent observer watches, and `restore` to reinstate the original.
*/
function installResizeObserver(): {
fire: () => void;
@@ -203,10 +181,7 @@ function installResizeObserver(): {
/**
* Stubs `getBoundingClientRect` on an element to report fixed top and bottom edges, so the window
* hook's geometry reads (cull walks, extend anchors, sentinel offsets) are deterministic in jsdom.
- *
- * @param el - The element to stub.
- * @param top - The `top` value the rect should report.
- * @param bottom - The `bottom` value the rect should report; defaults to `top` (zero height).
+ * `bottom` defaults to `top`, giving a zero-height rect.
*/
function stubRect(el: Element, top: number, bottom: number = top): void {
el.getBoundingClientRect = () => ({
@@ -225,10 +200,6 @@ function stubRect(el: Element, top: number, bottom: number = top): void {
/**
* Mounts one stub segment root per id into `container`, carrying the `data-segment-id` attribute
* the window hook uses to enumerate mounted segments for cull measurement and extend anchoring.
- *
- * @param container - The scroll container to mount into.
- * @param ids - Segment ids in window order.
- * @returns The mounted elements, index-aligned with `ids`.
*/
function mountSegmentEls(container: HTMLElement, ids: readonly string[]): HTMLElement[] {
return ids.map((id) => {
@@ -242,9 +213,6 @@ function mountSegmentEls(container: HTMLElement, ids: readonly string[]): HTMLEl
/**
* Mounts a stub segment marked `aria-current="true"` — the recenter target the window hook snaps to
* the top — into `container`.
- *
- * @param container - The scroll container to mount into.
- * @returns The mounted active-segment element.
*/
function mountActiveSegment(container: HTMLElement): HTMLElement {
const active = document.createElement('div');
@@ -256,10 +224,6 @@ function mountActiveSegment(container: HTMLElement): HTMLElement {
/**
* Mounts a stub `[data-snap-spacer]` div — the element `snapActiveToTop` grows when
* `scrollIntoView` can't reach the top — into `container`.
- *
- * @param container - The scroll container to mount into.
- * @param initialHeight - Optional starting `style.height` (e.g. to assert it resets to `0px`).
- * @returns The mounted spacer element.
*/
function mountSnapSpacer(container: HTMLElement, initialHeight?: string): HTMLElement {
const spacer = document.createElement('div');
@@ -1401,8 +1365,6 @@ describe('useSegmentWindow', () => {
* the recenter-in-flight gate), mounts the segment wrapper plus the window's segment roots
* (anchor candidates), and stubs the container and first-segment rects so the anchor seeds
* deterministically (first segment visible at offset 10).
- *
- * @returns The render result plus the observer `fire` helper and the mounted segment elements.
*/
function renderSettledWindow() {
const { fire } = installBlockResizeObserver();
diff --git a/src/__tests__/interlinearizer.web-view.test.tsx b/src/__tests__/interlinearizer.web-view.test.tsx
index c38df8f3..e8129ef4 100644
--- a/src/__tests__/interlinearizer.web-view.test.tsx
+++ b/src/__tests__/interlinearizer.web-view.test.tsx
@@ -1,4 +1,3 @@
-/** @file Unit tests for interlinearizer.web-view.tsx. */
///
///
diff --git a/src/__tests__/main.test.ts b/src/__tests__/main.test.ts
index e6231db9..b1e700dc 100644
--- a/src/__tests__/main.test.ts
+++ b/src/__tests__/main.test.ts
@@ -1,4 +1,3 @@
-/** @file Unit tests for the extension entry point (main.ts). */
///
import type { OpenWebViewOptions, SavedWebViewDefinition, WebViewDefinition } from '@papi/core';
@@ -30,9 +29,6 @@ interface PapiBackendTestMock {
/**
* Type guard for the mocked @papi/backend default export. Allows destructuring mocks without type
* assertions.
- *
- * @param m - The value to test, typically the default export of the mocked module.
- * @returns `true` if `m` exposes all expected `__mock*` properties.
*/
function isPapiBackendTestMock(m: unknown): m is PapiBackendTestMock {
return (
@@ -67,12 +63,7 @@ const {
__mockLogger,
} = papiBackendMock;
-/**
- * Type guard that narrows an unknown value to a callable function.
- *
- * @param f - The value to test.
- * @returns True if f is a function.
- */
+/** Type guard that narrows an unknown value to a callable function. */
function isCallable(f: unknown): f is (...args: unknown[]) => unknown {
return typeof f === 'function';
}
@@ -85,12 +76,7 @@ type WebViewProvider = {
): Promise;
};
-/**
- * Type guard that narrows an unknown value to a {@link WebViewProvider}.
- *
- * @param x - The value to test.
- * @returns `true` if `x` has a callable `getWebView` method.
- */
+/** Type guard that narrows an unknown value to a {@link WebViewProvider}. */
function isWebViewProvider(x: unknown): x is WebViewProvider {
return !!x && typeof x === 'object' && 'getWebView' in x && typeof x.getWebView === 'function';
}
@@ -102,12 +88,7 @@ function getRegisteredProvider(): WebViewProvider {
return raw;
}
-/**
- * Finds the handler registered for `commandName` in the most recent `activate()` call.
- *
- * @param commandName - The fully-qualified command name to look up.
- * @returns The registered handler function, or `undefined` if none was registered for that name.
- */
+/** Finds the handler registered for `commandName` in the most recent `activate()` call. */
function findRegisteredHandler(commandName: string): ((...args: unknown[]) => unknown) | undefined {
const call = jest.mocked(__mockRegisterCommand).mock.calls.find((c) => c[0] === commandName);
const rawHandler: unknown = call?.[1];
@@ -118,8 +99,6 @@ function findRegisteredHandler(commandName: string): ((...args: unknown[]) => un
* Activates the extension with a fresh test context and returns the handler registered for
* `commandName`, cast to `T`.
*
- * @param commandName - The fully-qualified command name to look up.
- * @returns The handler registered during `activate()`, cast to `T`.
* @throws If the handler was not registered during `activate()`.
*/
async function activateAndGetHandler(commandName: string): Promise {
@@ -187,7 +166,6 @@ const getSaveDraftHandler = () =>
/**
* Retrieves the callback passed to onDidOpenWebView during the most recent activate() call.
*
- * @returns A typed wrapper around the captured callback.
* @throws If no callback was registered (i.e. activate was not called first).
*/
function getOpenWebViewCallback(): (event: { webView: SavedWebViewDefinition }) => void {
@@ -199,7 +177,6 @@ function getOpenWebViewCallback(): (event: { webView: SavedWebViewDefinition })
/**
* Retrieves the callback passed to onDidCloseWebView during the most recent activate() call.
*
- * @returns A typed wrapper around the captured callback.
* @throws If no callback was registered (i.e. activate was not called first).
*/
function getCloseWebViewCallback(): (event: { webView: SavedWebViewDefinition }) => void {
diff --git a/src/__tests__/parsers/papi/bookTokenizer.test.ts b/src/__tests__/parsers/papi/bookTokenizer.test.ts
index 4d483f56..33fa11d1 100644
--- a/src/__tests__/parsers/papi/bookTokenizer.test.ts
+++ b/src/__tests__/parsers/papi/bookTokenizer.test.ts
@@ -1,4 +1,3 @@
-/** @file Unit tests for {@link tokenizeBook}. */
///
import { tokenizeBook } from 'parsers/papi/bookTokenizer';
@@ -7,9 +6,6 @@ import type { RawBook } from 'parsers/papi/usjBookExtractor';
/**
* Builds a minimal RawBook fixture for GEN with the given verses. Each verse's rendered `number`
* defaults to the verse portion of its sid when not given, so existing call sites stay terse.
- *
- * @param verses - Array of verse objects (sid + text, optional number) to include in the book.
- * @returns A RawBook with fixed bookCode, writingSystem, and contentHash.
*/
function makeRawBook(verses: { sid: string; text: string; number?: string }[]): RawBook {
return {
diff --git a/src/__tests__/parsers/papi/resegmentBook.test.ts b/src/__tests__/parsers/papi/resegmentBook.test.ts
index 9d1d478d..b159c483 100644
--- a/src/__tests__/parsers/papi/resegmentBook.test.ts
+++ b/src/__tests__/parsers/papi/resegmentBook.test.ts
@@ -1,4 +1,3 @@
-/** @file Unit tests for {@link resegmentBook}. */
///
import type { Book } from 'interlinearizer';
@@ -8,9 +7,6 @@ import { resegmentBook } from 'parsers/papi/resegmentBook';
/**
* Builds a verse-tokenized GEN book from the given verses. Each verse's rendered `number` defaults
* to the verse portion of its sid when not given.
- *
- * @param verses - Verse SID + text pairs (optional number).
- * @returns The tokenized book.
*/
function makeBook(verses: { sid: string; text: string; number?: string }[]): Book {
return tokenizeBook({
diff --git a/src/__tests__/parsers/papi/usjBookExtractor.test.ts b/src/__tests__/parsers/papi/usjBookExtractor.test.ts
index 254a9a91..8d0a0734 100644
--- a/src/__tests__/parsers/papi/usjBookExtractor.test.ts
+++ b/src/__tests__/parsers/papi/usjBookExtractor.test.ts
@@ -1,4 +1,3 @@
-/** @file Unit tests for {@link extractBookFromUsj}. */
///
import { extractBookFromUsj, type UsjDocument } from 'parsers/papi/usjBookExtractor';
diff --git a/src/__tests__/parsers/pt9/interlinearXmlParser.test.ts b/src/__tests__/parsers/pt9/interlinearXmlParser.test.ts
index f319d773..f2126ec7 100644
--- a/src/__tests__/parsers/pt9/interlinearXmlParser.test.ts
+++ b/src/__tests__/parsers/pt9/interlinearXmlParser.test.ts
@@ -1,4 +1,3 @@
-/** @file Unit tests for {@link InterlinearXmlParser}. */
///
import * as fs from 'node:fs';
diff --git a/src/__tests__/services/projectStorage.test.ts b/src/__tests__/services/projectStorage.test.ts
index 401781c0..ed117d4f 100644
--- a/src/__tests__/services/projectStorage.test.ts
+++ b/src/__tests__/services/projectStorage.test.ts
@@ -1,4 +1,3 @@
-/** @file Unit tests for projectStorage.ts. */
///
import papiBackendMock from '@papi/backend';
@@ -33,7 +32,6 @@ interface StorageMock {
/**
* Type guard that narrows `m` to `StorageMock`.
*
- * @param m - The value to test.
* @returns `m is StorageMock` — `true` when `m` has all three mock storage properties and the mock
* logger.
*/
@@ -57,8 +55,6 @@ const token = createTestActivationContext().executionToken;
/**
* Constructs an ENOENT Error that mirrors the error thrown by `papi.storage.readUserData` when a
* storage key has never been written.
- *
- * @returns An `Error` with `code` set to `'ENOENT'`.
*/
function enoentError(): Error {
return Object.assign(new Error('ENOENT: no such file or directory'), { code: 'ENOENT' });
@@ -522,8 +518,6 @@ describe('projectStorage', () => {
/**
* Makes `readUserData` return `pendingCleanup` as the given id list and ENOENT for every other
* key, so a sweep sees exactly `ids` as its work set.
- *
- * @param ids - The project IDs the pending-cleanup set should contain.
*/
function stubPendingCleanup(ids: string[]): void {
__mockReadUserData.mockImplementation((_t: unknown, key: unknown) =>
diff --git a/src/__tests__/store/analysisSlice.test.ts b/src/__tests__/store/analysisSlice.test.ts
index 98d3d845..0cd815d6 100644
--- a/src/__tests__/store/analysisSlice.test.ts
+++ b/src/__tests__/store/analysisSlice.test.ts
@@ -1,4 +1,3 @@
-/** @file Unit tests for store/analysisSlice.ts. */
///
import type {
@@ -38,12 +37,7 @@ import {
import { emptyAnalysis } from '../../types/empty-factories';
import { makePhraseLink } from '../test-helpers';
-/**
- * Builds an approved `TokenAnalysisLink` for `tok-1` pointing at the given `TokenAnalysis`.
- *
- * @param ta - The `TokenAnalysis` the link should reference.
- * @returns A `TokenAnalysisLink` with `status: 'approved'` and `tokenRef: 'tok-1'`.
- */
+/** Builds an approved `TokenAnalysisLink` for `tok-1` pointing at the given `TokenAnalysis`. */
function makeApprovedLink(ta: TokenAnalysis): TokenAnalysisLink {
return {
analysisId: ta.id,
@@ -52,12 +46,7 @@ function makeApprovedLink(ta: TokenAnalysis): TokenAnalysisLink {
};
}
-/**
- * Builds a minimal `TextAnalysis` with a single approved link for `tok-1`/`ta-1`.
- *
- * @param ta - The `TokenAnalysis` payload to include.
- * @returns A `TextAnalysis` seeded with one approved token analysis link.
- */
+/** Builds a minimal `TextAnalysis` with a single approved link for `tok-1`/`ta-1`. */
function makeAnalysis(ta: TokenAnalysis): TextAnalysis {
return {
...emptyAnalysis(),
@@ -70,8 +59,6 @@ function makeAnalysis(ta: TokenAnalysis): TextAnalysis {
* Counts approved links on the same payload as `tokenRef`'s own approved link, used to assert
* sharing without reaching into the link arrays at each call site.
*
- * @param state - The analysis slice state.
- * @param tokenRef - The `Token.ref` whose payload's shared count is wanted.
* @returns The number of approved links on that token's payload, or 0 when it has no approved link.
*/
function approvedLinkCountForPayload(state: AnalysisState, tokenRef: string): number {
@@ -388,12 +375,7 @@ describe('selectApprovedGloss', () => {
// Phrase reducers
// ---------------------------------------------------------------------------
-/**
- * Builds a `TextAnalysis` seeded with the given approved phrase link.
- *
- * @param link - The `PhraseAnalysisLink` to include.
- * @returns A `TextAnalysis` with the link and its corresponding `PhraseAnalysis`.
- */
+/** Builds a `TextAnalysis` seeded with the given approved phrase link. */
function makeAnalysisWithPhrase(link: PhraseAnalysisLink): TextAnalysis {
return {
...emptyAnalysis(),
@@ -762,11 +744,8 @@ describe('selectPhraseGloss', () => {
});
/**
- * Builds a `TextAnalysis` seeded with a single approved `SegmentAnalysis` for `seg-1`.
- *
- * @param analysis - The `SegmentAnalysis` payload to include.
- * @param link - Optional override for the link; defaults to an approved link for `seg-1`.
- * @returns A `TextAnalysis` with the segment analysis and its link.
+ * Builds a `TextAnalysis` seeded with a single approved `SegmentAnalysis` for `seg-1`. `link`
+ * overrides the joining link, which otherwise approves `analysis` for `seg-1`.
*/
function makeAnalysisWithSegment(
analysis: SegmentAnalysis,
@@ -2095,10 +2074,6 @@ describe('selectMorphemeResetLosesGlosses', () => {
/**
* Writes a two-morpheme breakdown for `tokenRef` and returns the id of its first morpheme, so
* tests can gloss a morpheme whose id the reducer generated.
- *
- * @param store - The analysis store to dispatch into.
- * @param tokenRef - The `Token.ref` to break down.
- * @returns The first morpheme's generated id.
*/
function breakDown(store: ReturnType, tokenRef: string): string {
store.dispatch(writeMorphemes(tokenRef, 'cats', ['cat', '-s'], 'en'));
diff --git a/src/__tests__/test-helpers.ts b/src/__tests__/test-helpers.ts
index 4d3f03f3..88a2672e 100644
--- a/src/__tests__/test-helpers.ts
+++ b/src/__tests__/test-helpers.ts
@@ -1,4 +1,3 @@
-/** @file Shared test helpers for unit and component tests. */
import type { SerializedVerseRef } from '@sillsdev/scripture';
import type { ExecutionActivationContext, UseWebViewScrollGroupScrRefHook } from '@papi/core';
import type { Book, InterlinearProject, PhraseAnalysisLink, Token } from 'interlinearizer';
@@ -25,13 +24,6 @@ type StateSlot = { get: () => T; set: (v: T) => void };
/**
* Returns a `useWebViewState` hook stub that stores values in typed per-key closures so state
* persists across re-renders within the same test without requiring any type assertions.
- *
- * @param seed - Optional map of key → initial value. When a key is present in `seed` the slot is
- * pre-populated with that value instead of using the hook's `defaultValue` argument.
- * @returns A hook function with the signature `(key, defaultValue) => [value, setter, reset]` where
- * `value` is the current stored value for `key` (initially `defaultValue` or the seeded value),
- * `setter` updates it, and `reset` removes the slot so the next call re-initializes from
- * `defaultValue`.
*/
export function makeWebViewState(seed: Record = {}) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -64,9 +56,6 @@ export function makeWebViewState(seed: Record = {}) {
* Builds a {@link PhraseStripContextValue} for component tests, with no-op callbacks and empty
* lookups by default. Tests that consume strip context wrap their subject in `PhraseStripProvider`
* with this value (overriding only the fields they assert on).
- *
- * @param overrides - Partial context fields to override the defaults.
- * @returns A complete `PhraseStripContextValue`.
*/
export function makePhraseStripContext(
overrides: Partial = {},
@@ -114,7 +103,6 @@ export type ScrollGroupTuple = ReturnType;
* @param scrollGroupId - The active scroll-group id; defaults to `undefined` (unlinked).
* @param setScrollGroupId - The scroll-group setter; defaults to a no-op.
* @param sourceProjectId - The scroll group's source project id; defaults to `undefined`.
- * @returns A hook returning the assembled tuple.
*/
export function makeScrollGroupHook(
ref: SerializedVerseRef = defaultScrRef,
@@ -162,9 +150,6 @@ const mockElevatedPrivileges = {
/**
* Builds a minimal ExecutionActivationContext for unit testing activate(). Uses
* UnsubscriberAsyncList from the platform-bible-utils Jest mock.
- *
- * @returns Context that satisfies ExecutionActivationContext for tests that only use
- * registrations.add
*/
export function createTestActivationContext(): ExecutionActivationContext {
return {
@@ -178,9 +163,6 @@ export function createTestActivationContext(): ExecutionActivationContext {
/**
* Builds a minimal `InterlinearProject` test fixture with stable defaults used across command
* tests.
- *
- * @param id - Project ID override for tests that need a specific identifier.
- * @returns A project with fixed metadata and a fresh empty analysis object.
*/
export function makeStubProject(id = 'proj-id'): InterlinearProject {
return {
@@ -197,10 +179,6 @@ export function makeStubProject(id = 'proj-id'): InterlinearProject {
* Builds a minimal word token for use in component tests. When `surfaceText` is omitted it defaults
* to `ref`, which is appropriate for tests that only need a syntactically valid token and do not
* assert on surface text independently.
- *
- * @param ref - Token reference string.
- * @param surfaceText - Display text; defaults to `ref` when omitted.
- * @returns A word token with the given ref and surface text.
*/
export function makeWordToken(ref: string, surfaceText = ref): Token & { type: 'word' } {
return { ref, surfaceText, writingSystem: 'en', type: 'word', charStart: 0, charEnd: 1 };
@@ -209,11 +187,10 @@ export function makeWordToken(ref: string, surfaceText = ref): Token & { type: '
/**
* Builds an approved `PhraseAnalysisLink` fixture for unit tests.
*
- * @param phraseId - The analysis id for both the link and its corresponding `PhraseAnalysis`.
- * @param tokenRefs - Token refs to include.
+ * @param phraseId - Doubles as the link's `analysisId`, so tests can address the phrase by one id.
+ * @param tokenRefs - The linked token refs, in phrase order.
* @param surfaceTexts - Surface text for each token, parallel to `tokenRefs`. Defaults to the ref
* string when omitted, which is only appropriate when drift detection is not under test.
- * @returns A `PhraseAnalysisLink` with `status: 'approved'`.
*/
export function makePhraseLink(
phraseId: string,
diff --git a/src/__tests__/utils/analysis-book.test.ts b/src/__tests__/utils/analysis-book.test.ts
index d2cc8f9c..a3664908 100644
--- a/src/__tests__/utils/analysis-book.test.ts
+++ b/src/__tests__/utils/analysis-book.test.ts
@@ -1,4 +1,3 @@
-/** @file Unit tests for utils/analysis-book.ts. */
///
import type {
@@ -19,46 +18,25 @@ import { makePhraseLink } from '../test-helpers';
// Helpers
// ---------------------------------------------------------------------------
-/**
- * Creates a minimal `TokenAnalysis` payload record fixture.
- *
- * @param id - Analysis id.
- * @param surfaceText - Surface text; defaults to `id` when omitted.
- * @returns A `TokenAnalysis` with the given id.
- */
+/** Creates a minimal `TokenAnalysis` payload record fixture, with `surfaceText` defaulting to `id`. */
function mkTokenAnalysis(id: string, surfaceText = id): TokenAnalysis {
return { id, surfaceText };
}
-/**
- * Creates a `TokenAnalysisLink` joining a token ref to an analysis id.
- *
- * @param analysisId - The `TokenAnalysis.id` this link points at.
- * @param tokenRef - The token ref the analysis is attached to.
- * @returns An approved `TokenAnalysisLink`.
- */
+/** Creates a `TokenAnalysisLink` joining a token ref to an analysis id. */
function mkTokenLink(analysisId: string, tokenRef: string): TokenAnalysisLink {
return { analysisId, status: 'approved', token: { tokenRef, surfaceText: tokenRef } };
}
/**
- * Creates a minimal `SegmentAnalysis` payload record fixture.
- *
- * @param id - Analysis id.
- * @param surfaceText - Surface text; defaults to `id` when omitted.
- * @returns A `SegmentAnalysis` with the given id.
+ * Creates a minimal `SegmentAnalysis` payload record fixture, with `surfaceText` defaulting to
+ * `id`.
*/
function mkSegmentAnalysis(id: string, surfaceText = id): SegmentAnalysis {
return { id, surfaceText };
}
-/**
- * Creates a `SegmentAnalysisLink` joining a segment id to an analysis id.
- *
- * @param analysisId - The `SegmentAnalysis.id` this link points at.
- * @param segmentId - The segment id the analysis is attached to.
- * @returns An approved `SegmentAnalysisLink`.
- */
+/** Creates a `SegmentAnalysisLink` joining a segment id to an analysis id. */
function mkSegmentLink(analysisId: string, segmentId: string): SegmentAnalysisLink {
return { analysisId, status: 'approved', segmentId };
}
@@ -98,8 +76,6 @@ describe('removeBookFromAnalysis', () => {
* - An EXO-only phrase (should survive) and a cross-book GEN+EXO phrase (should be removed),
* - An orphan token analysis (`tok-orphan`) referenced only by a GEN link, so removing GEN leaves
* the payload unreferenced and it must be dropped by orphan cleanup.
- *
- * @returns A populated `TextAnalysis` fixture.
*/
function makeTwoBookAnalysis(): TextAnalysis {
return {
diff --git a/src/__tests__/utils/analysis-identity.test.ts b/src/__tests__/utils/analysis-identity.test.ts
index 8237f744..d36a28db 100644
--- a/src/__tests__/utils/analysis-identity.test.ts
+++ b/src/__tests__/utils/analysis-identity.test.ts
@@ -1,4 +1,3 @@
-/** @file Unit tests for utils/analysis-identity.ts. */
///
import type { TokenAnalysis } from 'interlinearizer';
@@ -20,9 +19,6 @@ describe('normalizeSurfaceForm', () => {
/**
* Builds a `TokenAnalysis` with the given fields over a stable id/surface, so each matrix test can
* vary exactly one field.
- *
- * @param overrides - Partial fields to merge onto the base analysis.
- * @returns A `TokenAnalysis` for use in equality assertions.
*/
function ta(overrides: Partial): TokenAnalysis {
return { id: 'id', surfaceText: 'word', ...overrides };
diff --git a/src/__tests__/utils/localized-strings.test.ts b/src/__tests__/utils/localized-strings.test.ts
index f393346a..c805bd0e 100644
--- a/src/__tests__/utils/localized-strings.test.ts
+++ b/src/__tests__/utils/localized-strings.test.ts
@@ -1,4 +1,3 @@
-/** @file Unit tests for utils/localized-strings.ts. */
///
import { resolvedOrEmpty } from '../../utils/localized-strings';
diff --git a/src/__tests__/utils/multi-string.test.ts b/src/__tests__/utils/multi-string.test.ts
index d702ef95..0eaf6a38 100644
--- a/src/__tests__/utils/multi-string.test.ts
+++ b/src/__tests__/utils/multi-string.test.ts
@@ -1,4 +1,3 @@
-/** @file Unit tests for utils/multi-string.ts. */
///
import { isEmptyMultiString } from '../../utils/multi-string';
diff --git a/src/__tests__/utils/phrase-arc.test.ts b/src/__tests__/utils/phrase-arc.test.ts
index ce48a2f6..c0a8c90f 100644
--- a/src/__tests__/utils/phrase-arc.test.ts
+++ b/src/__tests__/utils/phrase-arc.test.ts
@@ -1,4 +1,3 @@
-/** @file Unit tests for utils/phrase-arc.ts */
///
import {
@@ -27,12 +26,6 @@ import { makePhraseLink } from '../test-helpers';
/**
* Creates a minimal `DOMRect`-like plain object. jsdom's `getBoundingClientRect` returns zeroes by
* default; we override per-element via `jest.spyOn` in each test that needs real geometry.
- *
- * @param left - Left edge in pixels.
- * @param top - Top edge in pixels.
- * @param width - Width in pixels.
- * @param height - Height in pixels.
- * @returns A plain object shaped like `DOMRect`.
*/
function rect(left: number, top: number, width: number, height: number): DOMRect {
return {
@@ -53,9 +46,6 @@ function rect(left: number, top: number, width: number, height: number): DOMRect
* (ARC_BASE_STEM + level * ARC_LEVEL_STEP)`, so inverting `midY` against the row top yields the
* level the leveling assigned — the public stand-in for the removed `levelByPhraseId`.
*
- * @param paths - Computed arc paths from {@link computeAllArcPaths}.
- * @param phraseId - The phrase whose level is wanted.
- * @param rowTop - The visual top of the row the phrase's (upper) run rides above.
* @returns The recovered nesting level, or `undefined` when the phrase has no path.
*/
function levelOf(paths: ArcPath[], phraseId: string, rowTop: number): number | undefined {
@@ -68,9 +58,6 @@ function levelOf(paths: ArcPath[], phraseId: string, rowTop: number): number | u
* Builds a fake scroll container element with a list of `[data-phrase-box]` children. Each child's
* `getBoundingClientRect` is stubbed to return the provided rect. The container's own rect is fixed
* at (0, 0) with no scroll offset.
- *
- * @param boxes - Array of `{ phraseId, rect }` describing each phrase-box child.
- * @returns The container element.
*/
function buildContainer(boxes: { phraseId: string; r: DOMRect }[]): Element {
const container = document.createElement('div');
@@ -984,13 +971,6 @@ describe('deconflictSplitButtons', () => {
/**
* Builds an `ArcPath` whose button position and run extent are the only fields that matter to
* {@link deconflictSplitButtons}; `d`/`phraseId`/`splitAfterTokenRef` are placeholders.
- *
- * @param midX - Button x center.
- * @param midY - Button y (channel line).
- * @param runLeft - Left bound of the arc run.
- * @param runRight - Right bound of the arc run.
- * @param phraseId - Phrase id (defaults unique-ish per call site via the caller).
- * @returns A populated `ArcPath`.
*/
function path(
midX: number,
diff --git a/src/__tests__/utils/project-summary-format.test.ts b/src/__tests__/utils/project-summary-format.test.ts
index 06b3fe85..594c13b4 100644
--- a/src/__tests__/utils/project-summary-format.test.ts
+++ b/src/__tests__/utils/project-summary-format.test.ts
@@ -1,4 +1,3 @@
-/** @file Unit tests for utils/project-summary-format.ts (project-list sort + label helpers). */
///
import {
diff --git a/src/__tests__/utils/segment-labels.test.ts b/src/__tests__/utils/segment-labels.test.ts
index 7f2ce7a7..1897cad3 100644
--- a/src/__tests__/utils/segment-labels.test.ts
+++ b/src/__tests__/utils/segment-labels.test.ts
@@ -1,4 +1,3 @@
-/** @file Unit tests for utils/segment-labels.ts. */
///
import type { Segment } from 'interlinearizer';
@@ -9,10 +8,6 @@ import { buildSegmentLabels } from '../../utils/segment-labels';
* `verseStarts` covered-verse set, so `covered` enumerates each covered verse as a `[chapter,
* verse]` pair; `startRef`/`endRef` are set from the first and last covered verse for completeness
* but are not consulted by the labeler.
- *
- * @param id - The segment id.
- * @param covered - The verses the segment covers, in document order, as `[chapter, verse]` pairs.
- * @returns The assembled segment.
*/
function makeSegment(id: string, covered: [number, number][]): Segment {
const [startChapter, startVerse] = covered[0];
diff --git a/src/__tests__/utils/segmentation.test.ts b/src/__tests__/utils/segmentation.test.ts
index 9aa83f9a..f3423f49 100644
--- a/src/__tests__/utils/segmentation.test.ts
+++ b/src/__tests__/utils/segmentation.test.ts
@@ -1,4 +1,3 @@
-/** @file Unit tests for the pure segmentation-delta transforms. */
///
import type { Book, SegmentationDelta } from 'interlinearizer';
@@ -14,12 +13,7 @@ import {
splitSegmentBefore,
} from '../../utils/segmentation';
-/**
- * Builds a verse-tokenized GEN book from the given verses for use as the `verseBook` argument.
- *
- * @param verses - Verse SID + text pairs.
- * @returns The tokenized book.
- */
+/** Builds a verse-tokenized GEN book from the given verses for use as the `verseBook` argument. */
function makeBook(verses: { sid: string; number: string; text: string }[]): Book {
return tokenizeBook({ bookCode: 'GEN', writingSystem: 'en', contentHash: 'h', verses });
}
diff --git a/src/__tests__/utils/split-anchor.test.ts b/src/__tests__/utils/split-anchor.test.ts
index 9d047ad1..795e3807 100644
--- a/src/__tests__/utils/split-anchor.test.ts
+++ b/src/__tests__/utils/split-anchor.test.ts
@@ -1,4 +1,3 @@
-/** @file Unit tests for utils/split-anchor.ts (the punctuation-travel split-anchor resolver). */
///
import type { Token } from 'interlinearizer';
@@ -13,10 +12,6 @@ import { resolveSplitAnchor } from '../../utils/split-anchor';
* whitespace between them is taken verbatim from `baselineText` (never a token). The returned run
* always begins and ends with a word token, matching how {@link resolveSplitAnchor} is called for a
* word-word gap.
- *
- * @param baselineText - The literal baseline string the tokens are sliced from.
- * @param spans - Ordered `[surfaceText, type]` pairs naming each token's surface text and kind.
- * @returns The tokens in document order, each carrying `ref`, `charStart`, and `charEnd`.
*/
function tokensFrom(
baselineText: string,
@@ -44,10 +39,6 @@ function tokensFrom(
* Resolves the split anchor for a word-word gap described as a literal baseline string plus its
* token spans. The first span must be the preceding word and the last the following word; the spans
* in between are the gap punctuation.
- *
- * @param baselineText - The literal baseline string for the gap.
- * @param spans - Ordered `[surfaceText, type]` pairs; first and last must be word tokens.
- * @returns The resolved anchor token ref.
*/
function anchorFor(
baselineText: string,
diff --git a/src/__tests__/utils/suggestion-engine.test.ts b/src/__tests__/utils/suggestion-engine.test.ts
index 62b10d4c..2dc23ac1 100644
--- a/src/__tests__/utils/suggestion-engine.test.ts
+++ b/src/__tests__/utils/suggestion-engine.test.ts
@@ -1,4 +1,3 @@
-/** @file Unit tests for utils/suggestion-engine.ts. */
///
import type { TokenAnalysis } from 'interlinearizer';
@@ -9,14 +8,7 @@ import {
resolvedTokenAnalysisEqual,
} from '../../utils/suggestion-engine';
-/**
- * Builds a gloss-only `TokenAnalysis` for the pool-construction tests.
- *
- * @param id - The analysis id.
- * @param surfaceText - The analyzed surface form.
- * @param gloss - The English gloss to attach.
- * @returns A `TokenAnalysis` with the given id, surface text, and English gloss.
- */
+/** Builds a gloss-only `TokenAnalysis` for the pool-construction tests. */
function ta(id: string, surfaceText: string, gloss: string): TokenAnalysis {
return { id, surfaceText, gloss: { en: gloss } };
}
diff --git a/src/__tests__/utils/token-layout.test.ts b/src/__tests__/utils/token-layout.test.ts
index 6a1b2655..3df028dd 100644
--- a/src/__tests__/utils/token-layout.test.ts
+++ b/src/__tests__/utils/token-layout.test.ts
@@ -1,4 +1,3 @@
-/** @file Unit tests for utils/token-layout.ts. */
///
import type { Token } from 'interlinearizer';
@@ -17,24 +16,12 @@ import { makePhraseLink } from '../test-helpers';
// Helpers
// ---------------------------------------------------------------------------
-/**
- * Creates a word token fixture.
- *
- * @param ref - Token reference.
- * @param surfaceText - Surface text.
- * @returns A word token.
- */
+/** Creates a word token fixture. */
function mkWord(ref: string, surfaceText = ref): Token & { type: 'word' } {
return { ref, surfaceText, writingSystem: 'en', type: 'word', charStart: 0, charEnd: 1 };
}
-/**
- * Creates a punctuation token fixture.
- *
- * @param ref - Token reference.
- * @param surfaceText - Surface text.
- * @returns A punctuation token.
- */
+/** Creates a punctuation token fixture. */
function mkPunct(ref: string, surfaceText = '.'): Token {
return { ref, surfaceText, writingSystem: 'en', type: 'punctuation', charStart: 0, charEnd: 1 };
}
@@ -97,10 +84,6 @@ describe('resolveSlotFocus', () => {
/**
* Builds a focus context with the given segment id and optional phrase/free token; all other
* fields default to undefined.
- *
- * @param focusedSegmentId - Segment id of the focused token, or `undefined`.
- * @param overrides - Optional `focusedPhraseLink` / `focusedFreeToken` overrides.
- * @returns A `FocusContext` for `resolveSlotFocus`.
*/
function focusWithSegment(
focusedSegmentId: string | undefined,
diff --git a/src/__tests__/utils/verse-ref.test.ts b/src/__tests__/utils/verse-ref.test.ts
index 3f5ecc79..35aa1902 100644
--- a/src/__tests__/utils/verse-ref.test.ts
+++ b/src/__tests__/utils/verse-ref.test.ts
@@ -1,4 +1,3 @@
-/** @file Unit tests for utils/verse-ref.ts. */
///
import type { SerializedVerseRef } from '@sillsdev/scripture';
@@ -10,9 +9,6 @@ import { firstVerseNumber, segmentContainsVerse } from '../../utils/verse-ref';
* segment's `verseStarts`, so `covered` enumerates each covered verse as a `[chapter, verse]` pair,
* mirroring how a merged segment carries one verse start per absorbed verse. `startRef` / `endRef`
* are taken from the first and last covered verse.
- *
- * @param covered - The verses the segment covers, in document order, as `[chapter, verse]` pairs.
- * @returns The assembled segment.
*/
function makeSegment(covered: [number, number][]): Segment {
const [startChapter, startVerse] = covered[0];
@@ -31,14 +27,7 @@ function makeSegment(covered: [number, number][]): Segment {
};
}
-/**
- * Builds a `SerializedVerseRef` in the test book.
- *
- * @param chapterNum - The chapter number.
- * @param verseNum - The verse number.
- * @param book - The book id; defaults to the segments' book.
- * @returns The assembled reference.
- */
+/** Builds a `SerializedVerseRef`, defaulting `book` to the one the test segments use. */
function makeRef(chapterNum: number, verseNum: number, book: string = 'GEN'): SerializedVerseRef {
return { book, chapterNum, verseNum };
}
diff --git a/src/__tests__/utils/verse-superscripts.test.ts b/src/__tests__/utils/verse-superscripts.test.ts
index 33e73ca0..9bbba683 100644
--- a/src/__tests__/utils/verse-superscripts.test.ts
+++ b/src/__tests__/utils/verse-superscripts.test.ts
@@ -1,4 +1,3 @@
-/** @file Unit tests for utils/verse-superscripts.ts. */
///
import type { Book, Token } from 'interlinearizer';
@@ -10,9 +9,6 @@ import { buildVerseStartLabels, slotVerseLabel } from '../../utils/verse-supersc
/**
* Builds a verse-tokenized book from the given verses. Each verse's rendered `number` defaults to
* the verse portion of its sid.
- *
- * @param verses - Verse SID + text pairs (optional number).
- * @returns The tokenized book.
*/
function makeBook(verses: { sid: string; text: string; number?: string }[]): Book {
return tokenizeBook({
@@ -27,13 +23,7 @@ function makeBook(verses: { sid: string; text: string; number?: string }[]): Boo
});
}
-/**
- * Reads the superscript labels for a segment by id, failing the test when absent.
- *
- * @param book - The book to label.
- * @param segmentId - Segment id to look up.
- * @returns The segment's parallel-by-index verse-start labels.
- */
+/** Reads the superscript labels for a segment by id, failing the test when absent. */
function labelsFor(book: Book, segmentId: string): string[] {
const labels = buildVerseStartLabels(book.segments).get(segmentId);
if (!labels) throw new Error(`no labels for ${segmentId}`);
@@ -101,22 +91,12 @@ describe('buildVerseStartLabels', () => {
});
describe('slotVerseLabel', () => {
- /**
- * Builds a minimal word token fixture with only the fields `slotVerseLabel` reads.
- *
- * @param ref - The token ref.
- * @returns A word token carrying that ref.
- */
+ /** Builds a minimal word token fixture with only the fields `slotVerseLabel` reads. */
function wordToken(ref: string): Token & { type: 'word' } {
return { ref, surfaceText: ref, type: 'word', writingSystem: 'en', charStart: 0, charEnd: 1 };
}
- /**
- * Builds a token group from the given word tokens.
- *
- * @param tokens - The group's word tokens, in order.
- * @returns A token group with no phrase link and empty punctuation-between.
- */
+ /** Builds a token group from the given word tokens. */
function group(tokens: (Token & { type: 'word' })[]): TokenGroup {
return {
tokens,
@@ -126,13 +106,7 @@ describe('slotVerseLabel', () => {
};
}
- /**
- * Builds a between-group slot with the given next group and gap punctuation.
- *
- * @param nextGroup - The group following the slot, or `undefined`.
- * @param punctuation - The slot's gap punctuation tokens.
- * @returns A link slot.
- */
+ /** Builds a between-group slot with the given next group and gap punctuation. */
function slot(nextGroup: TokenGroup | undefined, punctuation: Token[] = []): LinkSlot {
return { prevGroup: undefined, nextGroup, punctuation };
}
diff --git a/src/components/AltHeldContext.tsx b/src/components/AltHeldContext.tsx
index 78382b68..c9fe6742 100644
--- a/src/components/AltHeldContext.tsx
+++ b/src/components/AltHeldContext.tsx
@@ -1,14 +1,14 @@
-/**
- * @file Dedicated context carrying only whether the Alt key is currently held, consumed by the
- * split-gap markers so an Alt press re-renders just those markers.
- *
- * Kept separate from the memoized `SegmentationContext`: a frequently-flipping boolean folded into
- * that context would defeat its memoization for every consumer, so the churn is isolated here.
- */
import { createContext, useContext } from 'react';
import type { ReactNode } from 'react';
-/** The Alt-held context. Defaults to `false` so consumers outside a provider read "not held". */
+/**
+ * Carries only whether the Alt key is currently held, so an Alt press re-renders just the split-gap
+ * markers that consume it. Deliberately separate from the memoized segmentation context: a
+ * frequently-flipping boolean folded in there would defeat that memoization for every consumer, so
+ * the churn is isolated here.
+ *
+ * Defaults to `false`, so consumers outside a provider read "not held".
+ */
const AltHeldContext = createContext(false);
/** Props for {@link AltHeldProvider}. */
@@ -25,7 +25,6 @@ type AltHeldProviderProps = Readonly<{
* @param props - Component props.
* @param props.value - Whether the Alt key is currently held.
* @param props.children - The subtree.
- * @returns The children wrapped in the context provider.
*/
export function AltHeldProvider({ value, children }: AltHeldProviderProps) {
return {children} ;
@@ -34,8 +33,6 @@ export function AltHeldProvider({ value, children }: AltHeldProviderProps) {
/**
* Reads whether the Alt key is currently held, falling back to `false` outside a provider so leaf
* components can be unit-tested without wiring one.
- *
- * @returns `true` while Alt is held, `false` otherwise.
*/
export function useAltHeldValue(): boolean {
return useContext(AltHeldContext);
diff --git a/src/components/AnalysisStore.tsx b/src/components/AnalysisStore.tsx
index b4eb01de..6b82be19 100644
--- a/src/components/AnalysisStore.tsx
+++ b/src/components/AnalysisStore.tsx
@@ -1,4 +1,3 @@
-/** @file Analysis store backed by Redux Toolkit with per-token subscriptions via `useSelector`. */
import type {
MorphemeAnalysis,
PhraseAnalysisLink,
@@ -128,7 +127,6 @@ type AnalysisStoreProviderProps = Readonly<{
* uncommitted text, so the caller can show the unsaved indicator while the user types
* @param props.showSuggestions - When true, un-approved tokens render the engine's derived
* suggestion with accept / promote affordances
- * @returns A context provider wrapping the subtree
*/
export function AnalysisStoreProvider({
children,
@@ -212,7 +210,6 @@ export function AnalysisStoreProvider({
* called outside a provider. Centralizes the guard every analysis hook shares.
*
* @param hookName - Name of the calling hook, used in the thrown error message.
- * @returns The provider's {@link CallbackRefs}.
* @throws When called outside an {@link AnalysisStoreProvider}.
*/
function useRequiredCallbacks(hookName: string): CallbackRefs {
@@ -227,7 +224,6 @@ function useRequiredCallbacks(hookName: string): CallbackRefs {
* `onSave`. Factors out the dispatch-then-save pattern every write hook repeats.
*
* @param hookName - Name of the calling hook, used in the guard's error message.
- * @returns The provider callbacks, the typed `dispatch`, and a stable `save` callback.
* @throws When called outside an {@link AnalysisStoreProvider}.
*/
function useAnalysisSave(hookName: string): {
@@ -392,7 +388,6 @@ export function useMorphemeResetLosesGlosses(tokenRef: string): boolean {
/**
* Returns the active BCP 47 analysis-language tag from the nearest {@link AnalysisStoreProvider}.
*
- * @returns The analysis language string.
* @throws When called outside an {@link AnalysisStoreProvider}.
*/
export function useAnalysisLanguage(): string {
@@ -405,7 +400,6 @@ export function useAnalysisLanguage(): string {
* Returns the current `TextAnalysis` snapshot, re-rendering on every analysis change. Intended for
* components that need the full analysis (e.g. an analysis-selection popup).
*
- * @returns The current `TextAnalysis` from the nearest {@link AnalysisStoreProvider}.
* @throws When called outside an {@link AnalysisStoreProvider}.
*/
export function useAnalysis(): TextAnalysis {
@@ -420,7 +414,6 @@ export function useAnalysis(): TextAnalysis {
* rewrites the others. Commits immediately, invoking `onSave` and the optional (test-only)
* `onGlossChange` spy.
*
- * @returns A function `(tokenRef, surfaceText, value) => void`.
* @throws When called outside an {@link AnalysisStoreProvider}.
*/
export function useGlossDispatch(): (tokenRef: string, surfaceText: string, value: string) => void {
@@ -434,9 +427,6 @@ export function useGlossDispatch(): (tokenRef: string, surfaceText: string, valu
* adds an approved link to the existing payload (raising its frequency), it does not rewrite the
* shared content, so no other token's gloss changes.
*
- * @returns A function `(tokenRef, surfaceText, analysisId) => void`, where `analysisId` is the
- * payload to approve and `surfaceText` is this token's surface text, snapshotted on the new
- * link.
* @throws When called outside an {@link AnalysisStoreProvider}.
*/
export function useApproveAnalysisDispatch(): (
@@ -461,9 +451,6 @@ export function useApproveAnalysisDispatch(): (
* re-segmenting, so editing one token's breakdown never rewrites the others. Commits immediately
* and triggers `onSave`.
*
- * @returns A function `(tokenRef, surfaceText, forms, writingSystem) => void`, where
- * `writingSystem` is the BCP 47 tag of the token's surface text (`Token.writingSystem`), stored
- * on each morpheme as the writing system of its form.
* @throws When called outside an {@link AnalysisStoreProvider}.
*/
export function useMorphemeBreakdownDispatch(): (
@@ -489,7 +476,6 @@ export function useMorphemeBreakdownDispatch(): (
* with no other content — no gloss, POS, features, or lexicon sense reference). Dispatches the
* `deleteMorphemes` action and triggers `onSave`.
*
- * @returns A function `(tokenRef) => void`.
* @throws When called outside an {@link AnalysisStoreProvider}.
*/
export function useMorphemeDeleteDispatch(): (tokenRef: string) => void {
@@ -510,7 +496,6 @@ export function useMorphemeDeleteDispatch(): (tokenRef: string) => void {
* before writing, so editing one token's morpheme gloss never rewrites the others. Commits
* immediately and triggers `onSave`.
*
- * @returns A function `(tokenRef, morphemeId, value) => void`.
* @throws When called outside an {@link AnalysisStoreProvider}.
*/
export function useMorphemeGlossDispatch(): (
@@ -537,7 +522,6 @@ export function useMorphemeGlossDispatch(): (
* Returns a `Map` from every token ref that belongs to an approved phrase to its
* `PhraseAnalysisLink`. Re-renders only when the phrase link map reference changes.
*
- * @returns The current phrase link map.
* @throws When called outside an {@link AnalysisStoreProvider}.
*/
export function usePhraseLinkMap(): Map {
@@ -550,7 +534,6 @@ export function usePhraseLinkMap(): Map {
* Returns a `Map` from `analysisId` to the approved `PhraseAnalysisLink` for O(1) phrase lookup by
* id. Re-renders only when the phrase link map reference changes.
*
- * @returns The current phrase-link-by-id map.
* @throws When called outside an {@link AnalysisStoreProvider}.
*/
export function usePhraseLinkByIdMap(): Map {
@@ -609,7 +592,6 @@ export function usePhraseGloss(phraseId: string): string {
/**
* Returns a stable callback that writes a gloss value for the given phrase, then calls `onSave`.
*
- * @returns A function `(phraseId, value) => void`.
* @throws When called outside an {@link AnalysisStoreProvider}.
*/
export function usePhraseGlossDispatch(): (phraseId: string, value: string) => void {
@@ -630,7 +612,6 @@ export type PhraseDispatch = {
* Creates a new approved phrase from an ordered list of token snapshots.
*
* @param tokens - Ordered `TokenSnapshot`s in document order.
- * @returns The UUID assigned to the new phrase.
*/
createPhrase: (tokens: TokenSnapshot[]) => string;
/**
@@ -668,8 +649,6 @@ export type PhraseDispatch = {
* the corresponding Redux action then calls `onSave` with the updated `TextAnalysis`, matching the
* pattern of {@link useGlossDispatch}.
*
- * @returns An object with `createPhrase`, `updatePhrase`, `deletePhrase`, and `mergePhrases`
- * functions.
* @throws When called outside an {@link AnalysisStoreProvider}.
*/
export function usePhraseDispatch(): PhraseDispatch {
@@ -744,8 +723,6 @@ export function useSegmentFreeTranslation(segmentId: string): string {
* `onSave`. Mirrors {@link useGlossDispatch}: a blank value clears the translation and may drop the
* now-empty `SegmentAnalysis` record.
*
- * @returns A function `(segmentId, surfaceText, value) => void`, where `surfaceText` is the
- * segment's current baseline text, stored on the `SegmentAnalysis` record.
* @throws When called outside an {@link AnalysisStoreProvider}.
*/
export function useSegmentFreeTranslationDispatch(): (
diff --git a/src/components/ArcOverlay.tsx b/src/components/ArcOverlay.tsx
index 1308d6d6..504aee7f 100644
--- a/src/components/ArcOverlay.tsx
+++ b/src/components/ArcOverlay.tsx
@@ -212,7 +212,6 @@ export function ArcOverlay({
* hover/focus stroke.
*
* @param arc - The arc path to render.
- * @returns The SVG path element.
*/
const renderArcPath = ({ phraseId, d, splitAfterTokenRef }: ArcPath) => {
const effectiveHoveredPhraseId = candidatePhraseIds.has(phraseId) ? phraseId : hoveredPhraseId;
@@ -241,7 +240,6 @@ export function ArcOverlay({
* Classifies a phrase into one of three emphasis tiers (see {@link EmphasisTier}).
*
* @param phraseId - The phrase id to classify.
- * @returns The emphasis tier.
*/
const tierOf = (phraseId: string): EmphasisTier => {
if (phraseId === focusedPhraseId) return 'focused';
@@ -266,7 +264,6 @@ export function ArcOverlay({
* tier so its red stroke renders above other arcs.
*
* @param arc - The arc path to classify.
- * @returns The emphasis tier the arc should be painted in.
*/
const effectiveTierOf = (arc: ArcPath): EmphasisTier => {
if (isSplitHovered(arc.phraseId, arc.splitAfterTokenRef) && splitHoveredArc?.kind === 'free')
diff --git a/src/components/ContinuousView.tsx b/src/components/ContinuousView.tsx
index b27d35b0..f9268516 100644
--- a/src/components/ContinuousView.tsx
+++ b/src/components/ContinuousView.tsx
@@ -152,7 +152,6 @@ type ContinuousViewProps = Readonly<{
* merges
* @param props.wordTokenByRef - Word token ref → token lookup for focus resolution
* @param props.viewOptions - Bundled display toggles forwarded to the strip.
- * @returns A horizontal phrase strip with previous/next navigation arrows and edge-fade overlays
*/
export default function ContinuousView({
book,
diff --git a/src/components/InterlinearNavContext.tsx b/src/components/InterlinearNavContext.tsx
index 890e1dcd..b5134785 100644
--- a/src/components/InterlinearNavContext.tsx
+++ b/src/components/InterlinearNavContext.tsx
@@ -68,7 +68,6 @@ function areScrRefsEqual(a: SerializedVerseRef, b: SerializedVerseRef): boolean
* 1.
*
* @param ref - The scripture reference to key.
- * @returns A `book:chapter:verse` string uniquely identifying the verse.
*/
export function verseKey(ref: SerializedVerseRef): string {
return `${ref.book}:${ref.chapterNum}:${ref.verseNum}`;
@@ -139,7 +138,6 @@ export interface InterlinearNav {
* misclassify a later external navigation to the un-echoed verse.
*
* @param ref - The reference whose pending classification to consume.
- * @returns `true` if the navigation to `ref` was internal (skip the fade), else `false`.
*/
consumeInternalNav: (ref: SerializedVerseRef) => boolean;
/** The currently active scroll-group ID (`undefined` = unlinked). */
@@ -185,7 +183,6 @@ const InterlinearNavContext = createContext(undefine
* @param props.useWebViewScrollGroupScrRef - The PAPI hook exposing the shared scroll-group
* reference and its setter; injected by the host (not imported) so it can be stubbed in tests.
* @param props.children - The subtree that consumes navigation through {@link useInterlinearNav}.
- * @returns The provider wrapping `children`.
*/
export function InterlinearNavProvider({
useWebViewScrollGroupScrRef,
@@ -380,7 +377,6 @@ export function InterlinearNavProvider({
/**
* Reads the {@link InterlinearNav} surface from the nearest {@link InterlinearNavProvider}.
*
- * @returns The navigation surface.
* @throws {Error} When called outside an {@link InterlinearNavProvider}.
*/
export function useInterlinearNav(): InterlinearNav {
diff --git a/src/components/Interlinearizer.tsx b/src/components/Interlinearizer.tsx
index 3b13f869..623213b1 100644
--- a/src/components/Interlinearizer.tsx
+++ b/src/components/Interlinearizer.tsx
@@ -117,7 +117,6 @@ type InterlinearizerProps = Readonly<{
* transition modes.
* @param props.viewOptions - Bundled display toggles forwarded to the segment list and continuous
* views.
- * @returns The interlinearizer layout without the provider wrapper.
*/
function InterlinearizerInner({
book,
@@ -473,7 +472,6 @@ function InterlinearizerInner({
* views.
* @param props.showSuggestions - When true, un-approved tokens render the engine's derived
* suggestion with accept / promote affordances; forwarded to {@link AnalysisStoreProvider}.
- * @returns The full interlinearizer layout with optional continuous strip and segment list
*/
export default function Interlinearizer({
initialAnalysis,
diff --git a/src/components/InterlinearizerLoader.tsx b/src/components/InterlinearizerLoader.tsx
index 4612ee56..97e2d1f5 100644
--- a/src/components/InterlinearizerLoader.tsx
+++ b/src/components/InterlinearizerLoader.tsx
@@ -68,7 +68,6 @@ const UNSAVED_TAB_MARKER = ' ●';
* by the PAPI host
* @param props.updateWebViewDefinition - Host-injected callback to update this WebView's
* definition; used to toggle the tab's unsaved-changes title marker
- * @returns The nav provider wrapping {@link InterlinearizerLoaderInner}
*/
export default function InterlinearizerLoader({
projectId,
@@ -104,8 +103,6 @@ export default function InterlinearizerLoader({
* by the PAPI host
* @param props.updateWebViewDefinition - Host-injected callback used to toggle the tab's
* unsaved-changes title marker
- * @returns The toolbar and either an error/loading state or the fully rendered
- * {@link Interlinearizer}
*/
function InterlinearizerLoaderInner({
projectId,
@@ -397,8 +394,6 @@ function InterlinearizerLoaderInner({
* Saves the current draft to the active project. When no project is active there is nothing to
* save to yet, so it opens Save As instead. Errors are logged; the backend surfaces the
* notification.
- *
- * @returns A promise that resolves once the save completes or Save As is opened.
*/
const handleSave = useCallback(async () => {
if (!activeProject) {
diff --git a/src/components/MorphemeBox.tsx b/src/components/MorphemeBox.tsx
index 21191a83..a3da86e8 100644
--- a/src/components/MorphemeBox.tsx
+++ b/src/components/MorphemeBox.tsx
@@ -1,11 +1,3 @@
-/**
- * @file Inline display of an analyzed token's morpheme breakdown, rendered inside {@link TokenChip}
- * when the morphology toggle is active and the token has a breakdown. {@link MorphemeBox} boxes
- * the breakdown and lays it out as a grid so each morpheme form aligns vertically with its gloss
- * field (and, in future, its lexicon link); {@link MorphemeGlossInput} provides a per-morpheme
- * gloss field that fills its grid column. The breakdown _editor_ (the popover where forms are
- * entered) lives separately in {@link ./MorphemeEditor}.
- */
import type { MorphemeAnalysis, Token } from 'interlinearizer';
import { useLocalizedStrings } from '@papi/frontend/react';
import { PopoverAnchor } from 'platform-bible-react';
@@ -21,11 +13,14 @@ const MORPHEME_BOX_STRING_KEYS = [
] as const satisfies `%${string}%`[];
/**
- * Renders an analyzed token's morpheme breakdown as a boxed grid: each grid column is one morpheme,
- * with its form on the top row directly above its gloss field on the bottom row, so a morpheme and
- * its gloss always share a column (a future lexicon link slots into a third row with the same
- * column alignment). The box appears only for tokens that have a breakdown; an unanalyzed token's
- * "define breakdown" affordance lives in {@link TokenChip} instead.
+ * Inline _display_ of an analyzed token's morpheme breakdown, shown inside the token chip when the
+ * morphology toggle is active. The popover where forms are actually entered lives separately.
+ *
+ * The breakdown renders as a boxed grid: each grid column is one morpheme, with its form on the top
+ * row directly above its gloss field on the bottom row, so a morpheme and its gloss always share a
+ * column (a future lexicon link slots into a third row with the same column alignment). The box
+ * appears only for tokens that have a breakdown; an unanalyzed token's "define breakdown"
+ * affordance lives in {@link TokenChip} instead.
*
* The whole forms row is a single accessible "edit breakdown" control rather than one labeled
* button per morpheme: every form cell opens the same whole-breakdown editor, so per-cell labels
@@ -50,7 +45,6 @@ const MORPHEME_BOX_STRING_KEYS = [
* @param props.onGlossFocus - Called when any morpheme gloss input receives focus, so the chip can
* report the token as focused; these fields are gloss fields of the same token as the chip's own
* gloss input, so focusing one must move the view's focus just as focusing that input does.
- * @returns A boxed grid of morpheme forms and their gloss fields, wrapped in a popover anchor.
*/
export function MorphemeBox({
token,
@@ -173,7 +167,6 @@ export function MorphemeBox({
* @param props.column - 1-based grid column the input occupies (shared with the morpheme's form).
* @param props.onFocus - Called when the input receives focus, so the containing chip can report
* its token as focused.
- * @returns A cell-filling text input for the morpheme gloss, placed in the gloss row.
*/
export function MorphemeGlossInput({
morpheme,
diff --git a/src/components/MorphemeEditor.tsx b/src/components/MorphemeEditor.tsx
index 7e3bc995..dbb24b82 100644
--- a/src/components/MorphemeEditor.tsx
+++ b/src/components/MorphemeEditor.tsx
@@ -1,9 +1,3 @@
-/**
- * @file The morpheme breakdown editor rendered inside {@link TokenChip} when the morphology toggle
- * is active. {@link MorphemeBreakdownPopover} lets the user define or re-split a token's morpheme
- * forms. The inline _display_ of the breakdown (the boxed grid of forms and their gloss fields)
- * lives separately in {@link ./MorphemeBox}.
- */
import { useLocalizedStrings } from '@papi/frontend/react';
import { Button, Input, Label, PopoverContent } from 'platform-bible-react';
import { useId, useRef, useState } from 'react';
@@ -20,10 +14,13 @@ const POPOVER_STRING_KEYS = [
] as const satisfies `%${string}%`[];
/**
- * Inline popover for defining or editing a token's morpheme breakdown. The user types
- * space-separated morpheme forms (e.g. "un- believe -able") and commits with Enter, Done, or by
- * clicking outside the popover (matching the commit-on-blur behavior of gloss inputs). Cancel and
- * Escape dismiss without saving.
+ * Inline popover for defining or re-splitting a token's morpheme breakdown, shown inside the token
+ * chip when the morphology toggle is active. This is the _editor_ only; the inline display of a
+ * breakdown — the boxed grid of forms and their gloss fields — lives separately.
+ *
+ * The user types space-separated morpheme forms (e.g. "un- believe -able") and commits with Enter,
+ * Done, or by clicking outside the popover (matching the commit-on-blur behavior of gloss inputs).
+ * Cancel and Escape dismiss without saving.
*
* Committing resolves to one of three outcomes:
*
@@ -110,7 +107,6 @@ export function MorphemeBreakdownPopover({
* Collapses leading/trailing and repeated internal whitespace to a single space.
*
* @param s - The string to normalize.
- * @returns The string with surrounding whitespace trimmed and internal runs collapsed.
*/
const normalize = (s: string) => s.trim().replace(/\s+/g, ' ');
diff --git a/src/components/PhraseBox.tsx b/src/components/PhraseBox.tsx
index fa3cdf71..9c48cb37 100644
--- a/src/components/PhraseBox.tsx
+++ b/src/components/PhraseBox.tsx
@@ -1,4 +1,3 @@
-/** @file Shared phrase-box wrapper used around word tokens. */
import type { PhraseAnalysisLink, Token } from 'interlinearizer';
import { Trash2 } from 'lucide-react';
import { Button } from 'platform-bible-react';
@@ -29,7 +28,6 @@ import MemoizedTokenLinkIcon from './TokenLinkIcon';
* @param props.disabled - When true, the input is read-only.
* @param props.onFocus - Called when the input receives focus; used to center this phrase in the
* strip.
- * @returns An input element sized to its content.
*/
function PhraseGlossInput({
phraseId,
@@ -160,7 +158,6 @@ type PhraseBoxProps = Readonly<{
* matching chip renders with a destructive border as a preview
* @param props.punctuationBetween - Punctuation tokens between adjacent word tokens, in document
* order; `punctuationBetween[i]` sits between `tokens[i]` and `tokens[i+1]`
- * @returns A bordered inline container
*/
export function PhraseBox({
isFocused = false,
@@ -531,7 +528,6 @@ export function PhraseBox({
* so each token chip is removable via the keyboard as well as by click.
*
* @param tokenRef - Ref of the token the returned handler removes.
- * @returns A keydown event handler for that token's chip.
*/
const handlePerTokenKeyDown = (tokenRef: string) => (e: KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
diff --git a/src/components/PhraseStripContext.tsx b/src/components/PhraseStripContext.tsx
index f8b2fa8c..040b451b 100644
--- a/src/components/PhraseStripContext.tsx
+++ b/src/components/PhraseStripContext.tsx
@@ -1,14 +1,3 @@
-/**
- * @file Render-scoped context shared by the two phrase strips (`SegmentView` and `ContinuousView`).
- *
- * Holds the values that are identical for every phrase group and link slot within a single strip
- * render: the edit-mode context and the hover-preview callbacks. Delivering them via context
- * keeps the structural intermediaries (`PhraseGroup`/`PhraseSlot`) from forwarding props they
- * never touch, so each remaining prop on them describes something genuinely per-group/per-slot.
- *
- * Per-instance values (focus, highlight, arc offset, slot geometry) are intentionally **not** here
- * — they vary per item and belong at the call site as props.
- */
import { createContext, useContext } from 'react';
import type { Dispatch, ReactNode, SetStateAction } from 'react';
import type { PhraseAnalysisLink } from 'interlinearizer';
@@ -17,6 +6,13 @@ import type { PhraseMode } from '../types/phrase-mode';
/**
* The stable, strip-wide context shared by every phrase group and link slot in one render. Both
* strips build one value per render and wrap their token row in a {@link PhraseStripProvider}.
+ *
+ * It holds the values identical across a whole strip render — the edit-mode context and the
+ * hover-preview callbacks — so the structural intermediaries never forward props they don't touch,
+ * and each prop that remains on them describes something genuinely per-group or per-slot.
+ *
+ * Per-instance values such as focus, highlight, arc offset, and slot geometry are deliberately
+ * **not** here: they vary per item and belong at the call site as props.
*/
export type PhraseStripContextValue = Readonly<{
/** Current phrase-interaction mode; controls rendering and click behavior in all leaves. */
@@ -116,7 +112,6 @@ type PhraseStripProviderProps = Readonly<{
* @param props - Component props.
* @param props.value - The strip-wide context value.
* @param props.children - The strip's token row.
- * @returns The children wrapped in the context provider.
*/
export function PhraseStripProvider({ value, children }: PhraseStripProviderProps) {
return {children} ;
@@ -125,7 +120,6 @@ export function PhraseStripProvider({ value, children }: PhraseStripProviderProp
/**
* Reads the strip-wide phrase context. Must be called from inside a {@link PhraseStripProvider}.
*
- * @returns The current {@link PhraseStripContextValue}.
* @throws If called outside a {@link PhraseStripProvider}.
*/
export function usePhraseStripContext(): PhraseStripContextValue {
diff --git a/src/components/PhraseStripParts.tsx b/src/components/PhraseStripParts.tsx
index 202d2075..7586dfc0 100644
--- a/src/components/PhraseStripParts.tsx
+++ b/src/components/PhraseStripParts.tsx
@@ -1,4 +1,3 @@
-/** @file Shared render parts for the two phrase strips (SegmentView and ContinuousView). */
import type { Token } from 'interlinearizer';
import { Merge, Split } from 'lucide-react';
import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'platform-bible-react';
@@ -41,7 +40,6 @@ type BoundaryButtonProps = Readonly<{
* @param props.testId - `data-testid` for the button element.
* @param props.icon - The icon rendered inside the button.
* @param props.action - The boundary edit to run on click.
- * @returns The styled boundary button.
*/
function BoundaryButton({ label, title, testId, icon, action }: BoundaryButtonProps) {
return (
@@ -220,7 +218,6 @@ type SplitMarkerProps = Readonly<{
* @param props - Component props.
* @param props.label - Accessible label and tooltip.
* @param props.onSplit - Runs the split; called only for a genuine Alt+click.
- * @returns The split-marker span.
*/
function SplitMarker({ label, onSplit }: SplitMarkerProps) {
return (
@@ -479,7 +476,6 @@ type PhraseGroupProps = Readonly<{
* @param props.setHoveredGroupKey - Called with groupKey on pointer enter/leave
* @param props.onFocusPhrase - Called with groupKey when this group's gloss input gains focus
* @param props.groupRef - Optional DOM-ref callback for the wrapper span
- * @returns A wrapper span containing the phrase box.
*/
export const MemoizedPhraseGroup = memo(function PhraseGroup({
group,
@@ -548,7 +544,6 @@ export const MemoizedPhraseGroup = memo(function PhraseGroup({
* @param props - Component props.
* @param props.label - The verse label to display (verbatim number, or `chapter:number` at a
* chapter transition).
- * @returns A superscript element carrying the verse label.
*/
export function VerseSuperscript({ label }: Readonly<{ label: string }>) {
return (
@@ -646,7 +641,6 @@ type PhraseStripProps = Readonly<{
* @param props.onHoverPhrase - Phrase-box enter/leave callback
* @param props.setHoveredGroupKey - Hovered-group-key setter
* @param props.onFocusPhrase - Gloss-input focus callback, by group key
- * @returns The strip's ordered slot and group elements.
*/
export function PhraseStrip({
items,
diff --git a/src/components/SegmentFreeTranslationInput.tsx b/src/components/SegmentFreeTranslationInput.tsx
index fe35358c..e83711da 100644
--- a/src/components/SegmentFreeTranslationInput.tsx
+++ b/src/components/SegmentFreeTranslationInput.tsx
@@ -1,4 +1,3 @@
-/** @file Segment-level free-translation input rendered by the segment view. */
import { useLocalizedStrings } from '@papi/frontend/react';
import { useEffect, useState } from 'react';
import {
@@ -28,7 +27,6 @@ const STRING_KEYS = [
* record so it can detect drift if the baseline changes later.
* @param props.onFocus - Called when the input receives focus; used by `SegmentView` to make the
* segment active.
- * @returns A full-width text input.
*/
export default function SegmentFreeTranslationInput({
segmentId,
diff --git a/src/components/SegmentListView.tsx b/src/components/SegmentListView.tsx
index 0aaba628..d7a9a3c4 100644
--- a/src/components/SegmentListView.tsx
+++ b/src/components/SegmentListView.tsx
@@ -184,7 +184,6 @@ type SegmentListViewProps = Readonly<{
* @param props.tokenSegmentMap - Token ref → segment id lookup.
* @param props.tokenDocOrder - Word token ref → flat book-level index.
* @param props.wordTokenByRef - Word token ref → token lookup for the whole book.
- * @returns The scrollable segment list with its fade wrapper, sentinels, and locate button.
*/
export default function SegmentListView({
book,
diff --git a/src/components/SegmentView.tsx b/src/components/SegmentView.tsx
index 09ef5b64..a3297e21 100644
--- a/src/components/SegmentView.tsx
+++ b/src/components/SegmentView.tsx
@@ -91,7 +91,6 @@ type BaselinePiece =
* @param verseStartLabelByOffset - Char offset → verse superscript label.
* @param splitGapByOffset - Anchor char offset → split anchor ref; the gap slice ending at that
* offset becomes the splittable gap.
- * @returns The ordered baseline pieces.
*/
function buildBaselinePieces(
segment: Segment,
@@ -137,7 +136,6 @@ function buildBaselinePieces(
* @param props - Component props.
* @param props.label - The verse-range label to render, or `undefined` to render an empty gutter
* (reserving the column width so the content stays aligned across cards).
- * @returns The fixed-width gutter cell.
*/
function SegmentGutter({ label }: { label: string | undefined }) {
return (
@@ -297,8 +295,6 @@ type SegmentViewProps = Readonly<{
* @param props.wordTokenByRef - Word token ref → token lookup; used to resolve focus context.
* @param props.viewOptions - Bundled display toggles; `showFreeTranslation` gates the
* free-translation input, while the rest pass through to the phrase strip context.
- * @returns A div containing the segment content (baseline text or token chips) with either inline
- * verse superscripts or a left verse-range gutter, depending on `viewOptions.showVerseGutter`.
*/
export function SegmentView({
displayMode,
diff --git a/src/components/SegmentationStore.tsx b/src/components/SegmentationStore.tsx
index bdc7cfe6..0c056eee 100644
--- a/src/components/SegmentationStore.tsx
+++ b/src/components/SegmentationStore.tsx
@@ -1,17 +1,15 @@
-/**
- * @file Render-scoped context exposing segment-boundary editing to the deep leaves that trigger it
- * (the cross-segment link icon and the merge/split boundary controls).
- *
- * The {@link SegmentationDispatch} closes over the draft's current boundary delta and the original
- * verse-tokenized book, applying the pure transforms in `utils/segmentation.ts` and auto-saving
- * the result. Boundary edits flow draft → re-segmentation → new `book.segments`, so consumers
- * only need to call a dispatch method; they never see the delta itself.
- */
import type { Segment } from 'interlinearizer';
import { createContext, useContext } from 'react';
import type { ReactNode } from 'react';
-/** The boundary-editing operations available to leaf controls. Each one auto-saves the result. */
+/**
+ * The boundary-editing operations available to the deep leaves that trigger them — the
+ * cross-segment link icon and the merge/split boundary controls. Each one auto-saves its result.
+ *
+ * Each operation closes over the draft's current boundary delta and the original verse-tokenized
+ * book. Edits flow from draft through re-segmentation to a new segment list, so consumers only ever
+ * call a method here and never see the delta itself.
+ */
export type SegmentationDispatch = Readonly<{
/**
* Merges the segment that begins at `secondSegmentStartRef` into the segment before it.
@@ -99,7 +97,6 @@ type SegmentationProviderProps = Readonly<{
* @param props - Component props.
* @param props.value - The segmentation context value.
* @param props.children - The subtree.
- * @returns The children wrapped in the context provider.
*/
export function SegmentationProvider({ value, children }: SegmentationProviderProps) {
return {children} ;
diff --git a/src/components/SuggestionDropdown.tsx b/src/components/SuggestionDropdown.tsx
index fe8ef052..ae362701 100644
--- a/src/components/SuggestionDropdown.tsx
+++ b/src/components/SuggestionDropdown.tsx
@@ -1,10 +1,3 @@
-/**
- * @file The pop-down listbox a {@link TokenChip} shows while its gloss input is the active combobox.
- * Rendering and positioning live here; the combobox state (open, active row, keyboard) is owned
- * by the chip, which drives this purely through props. Portaled to `document.body` so it escapes
- * the clipping and stacking of the interlinear view's scroll viewports and token-row stacking
- * contexts.
- */
import { useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { STATUS_TEXT_COLOR_CLASS } from '../types/status-colors';
@@ -39,14 +32,17 @@ type SuggestionDropdownProps = Readonly<{
}>;
/**
- * Renders the portaled suggestion listbox for a token's gloss combobox. Each row is colored and
- * labeled by its own `status` — `'suggested'` (blue, "accept") or `'candidate'` (grey, "promote") —
- * carried on the entry rather than inferred from position, so a dropped blank-in-language pick can
- * never leave a candidate masquerading as the accept row. Each row suppresses its mouse-down
- * default so clicking it never blurs the input.
+ * The pop-down listbox a token chip shows while its gloss input is the active combobox. Rendering
+ * and positioning live here; the combobox state — open, active row, keyboard — is owned by the
+ * chip, which drives this purely through props. Portaled to the document body so it escapes the
+ * clipping and stacking of the interlinear view's scroll viewports and token rows.
+ *
+ * Each row is colored and labeled by its own `status` — `'suggested'` (blue, "accept") or
+ * `'candidate'` (grey, "promote") — carried on the entry rather than inferred from position, so a
+ * dropped blank-in-language pick can never leave a candidate masquerading as the accept row. Each
+ * row suppresses its mouse-down default so clicking it never blurs the input.
*
* @param props - Component props (see {@link SuggestionDropdownProps}).
- * @returns A `document.body` portal containing the listbox, positioned under the anchor input.
*/
export default function SuggestionDropdown({
anchorRef,
diff --git a/src/components/TokenChip.tsx b/src/components/TokenChip.tsx
index 210782e5..9a9b194a 100644
--- a/src/components/TokenChip.tsx
+++ b/src/components/TokenChip.tsx
@@ -63,7 +63,6 @@ const STRING_KEYS = [
* @param props.glossPlaceholder - Placeholder for the gloss input, resolved once per strip and
* passed down (see `PhraseStripContextValue.glossPlaceholder`). Passed as a prop rather than read
* from context so the chip's memoization keeps shielding it from unrelated context churn.
- * @returns A styled label containing the surface text, optionally morpheme rows, and a gloss input.
*/
export function TokenChip({
token,
@@ -234,7 +233,6 @@ export function TokenChip({
* assistive tech follows the keyboard-highlighted row.
*
* @param index - The row's index in {@link glossedRanked}.
- * @returns The option element id.
*/
const optionId = useCallback((index: number) => `${listboxId}-opt-${index}`, [listboxId]);
@@ -552,7 +550,6 @@ export function TokenChip({
*
* @param props - Component props
* @param props.token - The non-word token to render.
- * @returns A muted inline span.
*/
export function InertTokenChip({ token }: Readonly<{ token: Token }>) {
return (
diff --git a/src/components/TokenLinkIcon.tsx b/src/components/TokenLinkIcon.tsx
index ed3a9bce..ca08c213 100644
--- a/src/components/TokenLinkIcon.tsx
+++ b/src/components/TokenLinkIcon.tsx
@@ -1,4 +1,3 @@
-/** @file Inline link / unlink icon rendered between adjacent word token groups. */
import type { PhraseAnalysisLink, Token } from 'interlinearizer';
import { Link2, Unlink2 } from 'lucide-react';
import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'platform-bible-react';
diff --git a/src/components/__mocks__/AnalysisStore.tsx b/src/components/__mocks__/AnalysisStore.tsx
index 1c4435ab..488c05da 100644
--- a/src/components/__mocks__/AnalysisStore.tsx
+++ b/src/components/__mocks__/AnalysisStore.tsx
@@ -1,4 +1,3 @@
-/** @file Manual mock for AnalysisStore — reactive useState-based stub so AnalysisStore.tsx stays out of test scope. */
import { createContext, useCallback, useContext, useMemo, useState } from 'react';
import type { ReactNode } from 'react';
@@ -20,9 +19,6 @@ const MockCtx = createContext({
/**
* Test-only provider that seeds glosses from `initialAnalysis` and keeps them in local state,
* forwarding updates to `onGlossChange` without depending on the real AnalysisStore.
- *
- * @param props - Same surface props as the real `AnalysisStoreProvider`.
- * @returns A React element wrapping `children` in the mock context.
*/
export function AnalysisStoreProvider({
children,
@@ -66,9 +62,6 @@ export function AnalysisStoreProvider({
/**
* Returns the committed gloss for a token, or an empty string if none is set.
- *
- * @param tokenRef - The token reference key.
- * @returns The current gloss string from mock context.
*/
export function useGloss(tokenRef: string) {
return useContext(MockCtx).glosses[tokenRef] ?? '';
@@ -76,8 +69,6 @@ export function useGloss(tokenRef: string) {
/**
* Returns the dispatch function that updates a token's gloss in mock context.
- *
- * @returns The mock dispatch function.
*/
export function useGlossDispatch() {
return useContext(MockCtx).dispatch;
@@ -88,9 +79,6 @@ const EMPTY_MORPHEMES: readonly MorphemeAnalysis[] = [];
/**
* Returns the morpheme breakdown for a token. Always returns an empty array in mock context.
- *
- * @param _tokenRef - The token reference key (unused in mock).
- * @returns An empty readonly morpheme array.
*/
export function useMorphemes(_tokenRef: string): readonly MorphemeAnalysis[] {
return EMPTY_MORPHEMES;
@@ -108,8 +96,6 @@ export function useAnalysisLanguage(): string {
/**
* Returns a no-op dispatch for writing morpheme breakdowns in mock context.
- *
- * @returns A no-op function matching the real signature.
*/
export function useMorphemeBreakdownDispatch(): (
tokenRef: string,
@@ -122,8 +108,6 @@ export function useMorphemeBreakdownDispatch(): (
/**
* Returns a no-op dispatch for deleting morpheme breakdowns in mock context.
- *
- * @returns A no-op function matching the real signature.
*/
export function useMorphemeDeleteDispatch(): (tokenRef: string) => void {
return () => {};
@@ -132,8 +116,6 @@ export function useMorphemeDeleteDispatch(): (tokenRef: string) => void {
/**
* Reports that a morpheme reset never loses glosses in mock context, so the editor takes its
* unconfirmed path by default. Tests covering the confirm step mock this module member directly.
- *
- * @returns `false`.
*/
export function useMorphemeResetLosesGlosses(): boolean {
return false;
@@ -141,8 +123,6 @@ export function useMorphemeResetLosesGlosses(): boolean {
/**
* Returns a no-op dispatch for writing morpheme glosses in mock context.
- *
- * @returns A no-op function matching the real signature.
*/
export function useMorphemeGlossDispatch(): (
tokenRef: string,
@@ -155,19 +135,12 @@ export function useMorphemeGlossDispatch(): (
/**
* No-op stand-in for the real pending-edits reporter. The mock has no provider-level editing
* accounting, so it simply ignores the flag.
- *
- * @param _isEditing - Whether the input currently holds uncommitted text (unused in mock).
*/
export function useReportGlossEditing(_isEditing: boolean): void {}
/**
* Returns the merged token analysis in mock context. The mock pool is empty, so it never derives
* a suggestion — always `undefined`. Suggestion behavior is covered against the real store.
- *
- * @param _tokenRef - Token ref (unused in mock).
- * @param _surfaceText - Surface text (unused in mock).
- * @param _enabled - Whether to resolve (unused in mock).
- * @returns `undefined`.
*/
export function useResolvedTokenAnalysis(
_tokenRef: string,
@@ -180,11 +153,6 @@ export function useResolvedTokenAnalysis(
/**
* Returns the cleared-token suggestion preview in mock context. The mock pool is empty, so it never
* previews a suggestion — always `undefined`. Clearing behavior is covered against the real store.
- *
- * @param _tokenRef - Token ref (unused in mock).
- * @param _surfaceText - Surface text (unused in mock).
- * @param _enabled - Whether to derive (unused in mock).
- * @returns `undefined`.
*/
export function useSuggestionAfterClearing(
_tokenRef: string,
@@ -196,8 +164,6 @@ export function useSuggestionAfterClearing(
/**
* Returns whether suggestions should render in mock context — always `false`.
- *
- * @returns `false`.
*/
export function useShowSuggestions(): boolean {
return false;
@@ -205,8 +171,6 @@ export function useShowSuggestions(): boolean {
/**
* Returns a no-op dispatch for approving an analysis (accept / promote) in mock context.
- *
- * @returns A no-op function matching the real signature.
*/
export function useApproveAnalysisDispatch(): (
tokenRef: string,
diff --git a/src/components/__mocks__/TokenChip.tsx b/src/components/__mocks__/TokenChip.tsx
index 61839ef9..b40edb97 100644
--- a/src/components/__mocks__/TokenChip.tsx
+++ b/src/components/__mocks__/TokenChip.tsx
@@ -1,13 +1,8 @@
-/** @file Manual mock for TokenChip — renders surface text only, keeping TokenChip.tsx out of test scope. */
import type { Token } from 'interlinearizer';
/**
* Minimal stub for `InertTokenChip` that renders the token's surface text in a span.
- *
- * @param props - Component props.
- * @param props.token - The token whose surface text is rendered.
- * @returns A span containing the token's surface text.
*/
export function InertTokenChip({ token }: Readonly<{ token: Token }>) {
return {token.surfaceText} ;
diff --git a/src/components/controls/EditPhraseControls.tsx b/src/components/controls/EditPhraseControls.tsx
index b432e00c..0524f5a6 100644
--- a/src/components/controls/EditPhraseControls.tsx
+++ b/src/components/controls/EditPhraseControls.tsx
@@ -1,4 +1,3 @@
-/** @file Done/Cancel controls shown in the confirm bar while editing a phrase. */
import { Button } from 'platform-bible-react';
import type { Dispatch, SetStateAction } from 'react';
import type { PhraseMode } from '../../types/phrase-mode';
diff --git a/src/components/controls/ScriptureNavControls.tsx b/src/components/controls/ScriptureNavControls.tsx
index ddd63a1d..0f24c8e6 100644
--- a/src/components/controls/ScriptureNavControls.tsx
+++ b/src/components/controls/ScriptureNavControls.tsx
@@ -33,7 +33,6 @@ type ScriptureNavControlsProps = Pick`.
* @param props.value - Value string shown as ``.
* @param props.mono - When true, renders the value in a monospace font.
- * @returns A ` `/` ` pair.
*/
function MetadataRow({
label,
diff --git a/src/components/modals/ProjectModals.tsx b/src/components/modals/ProjectModals.tsx
index ecb2069d..a49ee823 100644
--- a/src/components/modals/ProjectModals.tsx
+++ b/src/components/modals/ProjectModals.tsx
@@ -192,7 +192,6 @@ export default function ProjectModals({
* and notifies on failure, leaving the draft untouched.
*
* @param project - The project summary the user chose to open.
- * @returns A promise that resolves once the draft is loaded or the failure has been handled.
*/
const openProject = useCallback(
async (project: InterlinearProjectSummary) => {
@@ -314,7 +313,6 @@ export default function ProjectModals({
* open so the user can retry without re-entering their inputs.
*
* @param config - The configuration collected by the New dialog.
- * @returns A promise that resolves once the creation settles or was ignored as re-entrant.
*/
const createDraftAndClose = useCallback(
async (config: CreateDraftConfig) => {
@@ -381,7 +379,6 @@ export default function ProjectModals({
*
* @param name - Trimmed project name, or `undefined`.
* @param description - Trimmed project description, or `undefined`.
- * @returns A promise that resolves once the save completes or the failure has been handled.
*/
const handleSaveAsNew = useCallback(
async (name?: string, description?: string) => {
@@ -438,7 +435,6 @@ export default function ProjectModals({
* error notification; here we only log.
*
* @param project - The existing project to overwrite.
- * @returns A promise that resolves once the overwrite completes or the failure has been handled.
*/
const handleOverwrite = useCallback(
async (project: InterlinearProjectSummary) => {
diff --git a/src/components/modals/ProjectSummaryDetails.tsx b/src/components/modals/ProjectSummaryDetails.tsx
index 5f52d8d6..ca521cd5 100644
--- a/src/components/modals/ProjectSummaryDetails.tsx
+++ b/src/components/modals/ProjectSummaryDetails.tsx
@@ -18,7 +18,6 @@ import { formatModified } from '../../utils/project-summary-format';
* @param props.modifiedPrefix - Localized `"Modified"` label preceding the formatted date.
* @param props.project - The project summary to describe.
* @param props.unnamedLabel - Localized fallback label rendered when the project has no name.
- * @returns The detail block.
*/
export function ProjectSummaryDetails({
activeBadgeLabel,
diff --git a/src/components/modals/UnlinkPhraseConfirm.tsx b/src/components/modals/UnlinkPhraseConfirm.tsx
index 0bae3ff9..02710ad2 100644
--- a/src/components/modals/UnlinkPhraseConfirm.tsx
+++ b/src/components/modals/UnlinkPhraseConfirm.tsx
@@ -1,4 +1,3 @@
-/** @file Confirmation controls shown in the confirm bar for unlinking (deleting) a phrase. */
import { Button } from 'platform-bible-react';
import type { Dispatch, SetStateAction } from 'react';
import type { PhraseMode } from '../../types/phrase-mode';
diff --git a/src/components/recenter-fade.ts b/src/components/recenter-fade.ts
index faebbb7f..73cc593a 100644
--- a/src/components/recenter-fade.ts
+++ b/src/components/recenter-fade.ts
@@ -1,10 +1,3 @@
-/**
- * @file Shared fade timing for the recenter animation used by both the continuous strip and the
- * segment list. Both views fade out, refocus on the externally-navigated verse, and fade back in;
- * importing the duration and easing from a single source keeps the two animations in lockstep so
- * an external navigation never shows one view fading on a different clock than the other.
- */
-
/**
* CSS easing for the recenter opacity fade-in/out. A sine-like curve gives a natural feel at both
* ends of the transition.
@@ -12,8 +5,10 @@
const RECENTER_FADE_EASING = 'cubic-bezier(0.65, 0, 0.35, 1)';
/**
- * Duration of the recenter fade, in milliseconds. Both views must use this value for their CSS
- * transition and for the `setTimeout` that swaps content at the midpoint, so they fade as one.
+ * Duration of the recenter fade, in milliseconds. Both views fade out, refocus on the
+ * externally-navigated verse, and fade back in; both must use this value for their CSS transition
+ * and for the timeout that swaps content at the midpoint, so an external navigation never shows one
+ * view fading on a different clock than the other.
*/
export const RECENTER_FADE_MS = 500;
diff --git a/src/hooks/useAltHeld.ts b/src/hooks/useAltHeld.ts
index 92366bcf..733818a2 100644
--- a/src/hooks/useAltHeld.ts
+++ b/src/hooks/useAltHeld.ts
@@ -9,8 +9,6 @@ import { useEffect, useRef, useState } from 'react';
* `window` `blur` and `document` `visibilitychange` to hidden — which is the common "stuck Alt"
* failure mode after Alt+Tab out of the iframe. A `keyup` reading `altKey === false` likewise
* clears it.
- *
- * @returns `true` while Alt is held, `false` otherwise.
*/
export function useAltHeld(): boolean {
const [altHeld, setAltHeld] = useState(false);
@@ -21,26 +19,14 @@ export function useAltHeld(): boolean {
const altHeldRef = useRef(false);
useEffect(() => {
- /**
- * Sets the held state to `next` only when it differs from the current value, so the repeated
- * `keydown` events fired while Alt is held do not trigger a re-render each frame.
- *
- * @param next - The desired held state.
- */
const set = (next: boolean) => {
if (altHeldRef.current === next) return;
altHeldRef.current = next;
setAltHeld(next);
};
- /**
- * Updates the held state from a keyboard event's `altKey` flag.
- *
- * @param event - The keyboard event whose `altKey` is read.
- */
const onKey = (event: KeyboardEvent) => set(event.altKey);
- /** Clears the held state when focus leaves or the document is hidden. */
const clear = () => set(false);
window.addEventListener('keydown', onKey);
diff --git a/src/hooks/useArcPaths.ts b/src/hooks/useArcPaths.ts
index e5a2af62..ff667de5 100644
--- a/src/hooks/useArcPaths.ts
+++ b/src/hooks/useArcPaths.ts
@@ -59,12 +59,6 @@ const SETTLE_VERIFY_DELAYS_MS = [200, 400, 800];
* — leaving the button in its pre-shift position. Two measurements with the same signature
* therefore produce both the same applied padding and the same rendered arcs, so they cannot
* represent genuine progress.
- *
- * @param paths - The measured arc paths.
- * @param maxLevel - The measured max nesting level.
- * @param leftPadding - The measured left gutter padding.
- * @param rightPadding - The measured right gutter padding.
- * @returns A signature string that is equal iff the layout- and render-affecting outputs match.
*/
function signatureOf(
paths: ArcPath[],
@@ -81,11 +75,10 @@ function signatureOf(
}
/**
- * Measures the rendered phrase boxes inside `containerRef` after each layout commit and computes
- * the arcs connecting every discontiguous phrase's runs. Shared by SegmentView and ContinuousView
- * so the two strip layouts can't drift apart. State is only replaced when the serialized arc shape
- * changes, so the layout effect settles after one extra pass; when `enabled` is `false` the result
- * is reset to empty instead of measured.
+ * Measures the rendered phrase boxes after each layout commit and computes the arcs connecting
+ * every discontiguous phrase's runs. Shared by both strips so the two layouts can't drift apart.
+ * State is only replaced when the serialized arc shape changes, so the layout effect settles after
+ * one extra pass. A disabled container resets to empty instead of being measured.
*
* Measurement is settle-aware: because box geometry can drift without any resize event on the
* observed container (see {@link SETTLE_VERIFY_DELAYS_MS}), each pass arms a delayed verification
@@ -96,14 +89,11 @@ function signatureOf(
* padding shifts the layout the arcs are measured against — otherwise a 0→1 arc transition would
* leave paths positioned for the old padding until an unrelated render.
*
- * @param containerRef - Ref to the element wrapping the `[data-phrase-box]` elements to measure.
- * @param enabled - Whether the container is mounted and should be measured; `false` resets to
- * empty.
+ * @param containerRef - Wraps the phrase-box elements to measure.
+ * @param enabled - Whether the container is mounted and should be measured.
* @param hasRealPhrase - Whether any committed phrase is rendered; feeds the controls headroom.
- * @param deps - Extra dependencies that should trigger a re-measure (token data, phrase mode,
- * etc.).
- * @returns The current arc paths, max nesting level, and the strip's top/row-gap/left/right
- * padding.
+ * @param deps - Extra dependencies that should trigger a re-measure, such as token data or phrase
+ * mode.
*/
export function useArcPaths(
containerRef: RefObject,
@@ -226,15 +216,9 @@ export function useArcPaths(
// other applies prepend onto a 2-deep window so entry 0 stays the applied signature.
recentArcSignaturesRef.current = force ? [signature] : [signature, ...history].slice(0, 2);
setArcPaths((prev) => {
- /**
- * Serializes one arc path into an identity key for the referential-equality guard below.
- * Includes the split-button geometry (midX/midY and run bounds) so a deconfliction-only
- * shift — which mutates midX without touching `d` — still replaces the paths rather than
- * leaving the button in its pre-shift position.
- *
- * @param p - The arc path to serialize.
- * @returns A string equal iff the rendered arc and its split-button geometry match.
- */
+ // Identity key for the referential-equality guard below. Includes the split-button geometry
+ // so a deconfliction-only shift — which moves the button without touching the arc path —
+ // still replaces the paths rather than leaving the button in its pre-shift position.
const key = (p: ArcPath) =>
`${p.phraseId}:${p.splitAfterTokenRef}:${p.d}:${p.midX}:${p.midY}:${p.runLeft}:${p.runRight}`;
const prevKey = prev.map(key).join('|');
diff --git a/src/hooks/useBookIndexes.ts b/src/hooks/useBookIndexes.ts
index 94011d03..3df7ee10 100644
--- a/src/hooks/useBookIndexes.ts
+++ b/src/hooks/useBookIndexes.ts
@@ -36,10 +36,9 @@ export interface BookIndexes {
/**
* Builds the book-wide lookup indexes the interlinear views share, in a single pass over
* `book.segments`. The indexes always travel together through the view prop plumbing, so deriving
- * them in one memo keeps them in lockstep (one traversal, one identity change per book change)
+ * them in one memo keeps them in lockstep — one traversal, one identity change per book change —
* instead of separate memos each walking the segment list.
*
- * @param book - The tokenized book to index.
* @returns The lookup indexes; stable identities until `book.segments` changes.
*/
export default function useBookIndexes(book: Book): BookIndexes {
diff --git a/src/hooks/useDraftProject.ts b/src/hooks/useDraftProject.ts
index be059217..f9229432 100644
--- a/src/hooks/useDraftProject.ts
+++ b/src/hooks/useDraftProject.ts
@@ -1,4 +1,3 @@
-/** @file Hook owning the always-present, auto-saved draft buffer for one source project. */
import papi, { logger } from '@papi/frontend';
import type {
DraftProject,
@@ -62,30 +61,22 @@ export type UseDraftProjectResult = {
/**
* Returns the latest draft envelope synchronously by reading the live ref rather than a render
* snapshot. Save / Save As must use this so they persist edits that auto-saved without a
- * re-render.
- *
- * @returns The current draft, or `undefined` before the initial load completes.
+ * re-render. Yields `undefined` before the initial load completes.
*/
getDraftSnapshot: () => DraftProject | undefined;
/**
* Persists an edited analysis into the draft and marks it dirty. Wire as the editor's
* `onSaveAnalysis`.
- *
- * @param analysis - The updated analysis from the store.
*/
autosaveAnalysis: (analysis: TextAnalysis) => void;
/**
* Persists an edited segment-boundary delta into the draft and marks it dirty. Pass `undefined`
* (or a default/empty delta) to clear custom boundaries back to the default verse segmentation.
- *
- * @param segmentation - The updated boundary delta, or `undefined` for the default segmentation.
*/
autosaveSegmentation: (segmentation: SegmentationDelta | undefined) => void;
/**
* Replaces the draft with a working copy of an existing project's analysis and config — the
* "Open" flow.
- *
- * @param project - The project whose analysis / languages / target to copy into the draft.
*/
loadFromProject: (project: OpenableProject) => void;
/**
@@ -93,15 +84,9 @@ export type UseDraftProjectResult = {
* languages and retains the typed name/description as `suggestedName`/`suggestedDescription`. The
* new draft is clean (`dirty: false`), so the unsaved-changes indicator stays clear until the
* first edit. The caller is responsible for immediately persisting the project to the backend.
- *
- * @param config - The languages and optional suggested name/description from the New dialog.
*/
newDraft: (config: NewDraftConfig) => void;
- /**
- * Removes one book's analysis from the draft and marks it dirty.
- *
- * @param bookCode - The 3-letter book code (e.g. `"GEN"`) to wipe.
- */
+ /** Removes one book's analysis from the draft, by 3-letter book code, and marks it dirty. */
wipeBook: (bookCode: string) => void;
/**
* Clears the draft's analysis entirely and marks it **not** dirty — a wiped draft is treated as a
@@ -115,9 +100,8 @@ export type UseDraftProjectResult = {
* save round-trip), the draft is left dirty so the unsaved-changes indicator and the next Save
* reflect that un-persisted edit rather than being cleared against a now-stale snapshot.
*
- * @param savedAnalysis - The `TextAnalysis` reference that was actually persisted to the project.
- * @param savedSegmentation - The `SegmentationDelta` reference that was persisted alongside it
- * (`undefined` when the draft had the default segmentation).
+ * Pass the exact references that were written; a `undefined` boundary delta means the draft had
+ * the default segmentation.
*/
markSynced: (
savedAnalysis: TextAnalysis,
@@ -135,8 +119,7 @@ export type UseDraftProjectResult = {
* per-edit auto-saves never re-render the loader unless the dirty flag actually flips.
*
* @param sourceProjectId - The Platform.Bible source project whose draft to manage.
- * @param platformLanguage - BCP 47 tag used to seed `analysisLanguages` for a brand-new source.
- * @returns Draft state and the callbacks described on {@link UseDraftProjectResult}.
+ * @param platformLanguage - BCP 47 tag used to seed the analysis languages of a brand-new source.
*/
export default function useDraftProject(
sourceProjectId: string,
@@ -166,8 +149,6 @@ export default function useDraftProject(
* unavailable during editing the backend sends one error notification per failed write; should
* that notification itself fail, edits in that window are silently lost on the next refresh. The
* `dirty` flag is set optimistically before the write, not in response to its outcome.
- *
- * @param draft - The draft envelope to write.
*/
const persist = useCallback(
(draft: DraftProject) => {
@@ -185,8 +166,6 @@ export default function useDraftProject(
/**
* Loads the stored draft for the source (falling back to an empty draft on failure), seeds a
* gloss language when none is present, and publishes it to the ref and state.
- *
- * @returns A promise that resolves once the draft has been published or the load was canceled.
*/
const load = async () => {
let draft: DraftProject;
@@ -225,8 +204,6 @@ export default function useDraftProject(
/**
* Applies a wholesale draft replacement: update the ref, persist, refresh `dirty`, and bump the
* remount counter so the editor reseeds.
- *
- * @param next - The replacement draft.
*/
const applyReplacement = useCallback(
(next: DraftProject) => {
@@ -245,10 +222,11 @@ export default function useDraftProject(
);
/**
- * Shared per-edit auto-save pipeline: derives the next draft from the current one via `mutate`,
- * swaps it into the ref, debounces the persistence write, and marks the draft dirty. No version
- * bump (no remount) and `setDirty(true)` is a no-op when already dirty, so editing does not
- * re-render.
+ * Shared per-edit auto-save pipeline: swaps the mutated draft into the ref, debounces the
+ * persistence write, and marks the draft dirty. There is no version bump and so no remount, and
+ * re-marking an already-dirty draft is a no-op, so editing does not re-render.
+ *
+ * Returns `false` without applying anything when no draft has loaded yet.
*
* @param mutate - Produces the next draft from the current one; must set `dirty: true`.
* @returns `true` when the edit was applied; `false` when no draft has loaded yet.
diff --git a/src/hooks/useInterlinearizerBookData.ts b/src/hooks/useInterlinearizerBookData.ts
index d7cefc3b..75ef0b1a 100644
--- a/src/hooks/useInterlinearizerBookData.ts
+++ b/src/hooks/useInterlinearizerBookData.ts
@@ -36,13 +36,6 @@ export interface UseInterlinearizerBookDataResult {
* scripture picker firing two signals in quick succession): when the serialized content is
* identical to the previous result, the prior reference is preserved so consumers avoid redundant
* tokenization and recenter churn.
- *
- * @param args - Hook arguments.
- * @param args.projectId - PAPI project ID whose USJ book data should be loaded.
- * @param args.scrRef - Current scripture reference; only `book` is used (chapter/verse are fixed to
- * 1).
- * @returns The tokenized book (reference-stable across duplicate USJ results), loading state, and
- * any errors encountered.
*/
export default function useInterlinearizerBookData({
projectId,
@@ -76,10 +69,7 @@ export default function useInterlinearizerBookData({
return rawBookResult;
}, [rawBookResult]);
- /**
- * Writing-system tag used for tokenization and error logging; falls back to `'und'` when the
- * project setting is unavailable or empty.
- */
+ // Falls back to `'und'` when the project setting is unavailable or empty.
const writingSystemTag = isPlatformError(writingSystem) ? 'und' : writingSystem || 'und';
const [book, tokenizeError] = useMemo((): [
diff --git a/src/hooks/useLatestRef.ts b/src/hooks/useLatestRef.ts
index d06c78b3..6c46c737 100644
--- a/src/hooks/useLatestRef.ts
+++ b/src/hooks/useLatestRef.ts
@@ -8,9 +8,6 @@ import { useRef } from 'react';
* value changes identity each render — the PAPI host, for instance, hands `scrRef` back as a fresh
* object on many renders, and closing over it directly would churn the identity of any callback
* that reads it. Reading through the ref decouples that churn.
- *
- * @param value - The value to mirror; the returned ref's `.current` is set to it on every render.
- * @returns A stable ref object whose `.current` tracks the latest `value`.
*/
export default function useLatestRef(value: T): RefObject {
const ref = useRef(value);
diff --git a/src/hooks/useOptimisticBooleanSetting.ts b/src/hooks/useOptimisticBooleanSetting.ts
index 8350f419..afca9f8e 100644
--- a/src/hooks/useOptimisticBooleanSetting.ts
+++ b/src/hooks/useOptimisticBooleanSetting.ts
@@ -11,7 +11,8 @@ type BooleanProjectSettingKey = {
const TIMEOUT_MS = 15_000;
/**
- * Manages a boolean project setting with optimistic UI updates.
+ * Manages a boolean project setting with optimistic UI updates, falling back to the given default
+ * until the setting has been persisted for the first time.
*
* The local value is updated immediately on change and stays locked for {@link TIMEOUT_MS} to allow
* the stored setting to finish updating without causing a visible bounce. If the platform confirms
@@ -19,9 +20,6 @@ const TIMEOUT_MS = 15_000;
* responds after the timeout), the lock clears and subsequent platform updates flow through
* normally.
*
- * @param projectId - PAPI project ID
- * @param settingKey - A valid key for a boolean setting
- * @param defaultValue - Default value used when the setting has not been persisted yet
* @returns `isLoading` — whether the setting value is still loading from the platform; `onChange` —
* stable change handler; `value` — the current display value
*/
diff --git a/src/hooks/usePhraseHoverState.ts b/src/hooks/usePhraseHoverState.ts
index 3f8216d8..7129fd88 100644
--- a/src/hooks/usePhraseHoverState.ts
+++ b/src/hooks/usePhraseHoverState.ts
@@ -1,6 +1,6 @@
import { useCallback, useState } from 'react';
-/** Hover-driven preview state shared by SegmentView and ContinuousView. */
+/** Hover-driven preview state shared by both phrase strips. */
export type PhraseHoverState = {
/**
* Group key (first token ref) of the phrase box currently hovered; drives controls-pill placement
@@ -22,18 +22,13 @@ export type PhraseHoverState = {
*/
splitFreeTokenRefs: ReadonlySet;
/**
- * Mirrors the free token refs emitted by `ArcOverlay`'s internal split-hover state so the phrase
- * boxes can show a destructive border preview.
- *
- * @param freeTokenRefs - Token refs that would become solo after the split, or an empty set on
- * leave.
+ * Mirrors the free token refs emitted by the arc overlay's own split-hover state so the phrase
+ * boxes can show a destructive border preview. Pass an empty set on leave.
*/
handleSplitHoverChange: (freeTokenRefs: ReadonlySet) => void;
/**
- * Sets (or clears) the would-become-free token refs previewed with a destructive border when a
- * link/unlink icon is hovered.
- *
- * @param refs - The would-be-free token refs, or `undefined`/empty on leave.
+ * Sets the would-become-free token refs previewed with a destructive border when a link or unlink
+ * icon is hovered. Pass `undefined` or an empty array on leave.
*/
handleHoverSplitFreeTokens: (refs: readonly string[] | undefined) => void;
/** Clears every hover preview at once; wired to the token row's `onMouseLeave`. */
@@ -41,16 +36,13 @@ export type PhraseHoverState = {
};
/**
- * Owns the hover-preview state that both phrase strips ({@link SegmentView} and
- * {@link ContinuousView}) need: the hovered group key, link-candidate token refs, and
- * would-become-free token refs, plus the stable callbacks that feed them from `ArcOverlay` and the
- * link/unlink icons. Extracted so the two views can never drift apart in how these previews are
- * wired, and so each view's body is freed of five near-identical state declarations.
+ * Owns the hover-preview state both phrase strips need, along with the stable callbacks that feed
+ * it from the arc overlay and the link icons. Extracted so the two views can never drift apart in
+ * how these previews are wired, and so neither view's body carries five near-identical state
+ * declarations.
*
- * `hoveredPhraseId` is intentionally **not** owned here: ContinuousView keeps it locally while
- * SegmentView receives it as a prop from its parent, so the two views manage it differently.
- *
- * @returns The shared hover-preview state and its setters/handlers.
+ * The hovered phrase id is deliberately **not** owned here: one view keeps it locally while the
+ * other receives it as a prop, so the two manage it differently.
*/
export function usePhraseHoverState(): PhraseHoverState {
const [hoveredGroupKey, setHoveredGroupKey] = useState();
diff --git a/src/hooks/usePhraseStripSetup.ts b/src/hooks/usePhraseStripSetup.ts
index 11977e9e..848c306e 100644
--- a/src/hooks/usePhraseStripSetup.ts
+++ b/src/hooks/usePhraseStripSetup.ts
@@ -1,10 +1,7 @@
/**
- * @file Shared phrase-strip setup hooks used by both `SegmentView` and `ContinuousView`.
- *
- * Consolidates the setup logic the two views share — the arc-split handler, the candidate-phrase-id
- * derivation, and the strip-wide context value — so they can't drift apart in how a split is
- * dispatched, how hovered candidates resolve to phrase ids, or which fields the leaf components
- * receive.
+ * Setup hooks shared by both phrase strips — the arc-split handler, the candidate-phrase-id
+ * derivation, and the strip-wide context value — so the two views cannot drift apart in how a split
+ * is dispatched, how hovered candidates resolve to phrase ids, or which fields the leaves receive.
*/
import { useCallback, useMemo } from 'react';
import type { Dispatch, SetStateAction } from 'react';
@@ -18,7 +15,6 @@ import { splitPhraseAtBoundary } from '../utils/phrase-arc';
* Returns the token list of the phrase currently being edited, or `undefined` outside edit mode.
* Reads the by-id phrase-link map internally so both views share one derivation.
*
- * @param phraseMode - Current phrase-interaction mode; only `edit` mode resolves a token list.
* @returns The edit-mode phrase's token snapshots, or `undefined` when not editing.
*/
export function useEditPhraseTokens(phraseMode: PhraseMode): TokenSnapshot[] | undefined {
@@ -35,14 +31,10 @@ export function useEditPhraseTokens(phraseMode: PhraseMode): TokenSnapshot[] | u
}
/**
- * Returns a memoized handler that splits a phrase arc at a token boundary and dispatches the
- * resulting create/update/delete operations via {@link splitPhraseAtBoundary}. The handler no-ops
- * when its `phraseId` is not in the phrase-link-by-id map. Reads the by-id map internally so
- * callers only supply `tokenDocOrder`.
- *
- * @param tokenDocOrder - Word token ref → flat document index, used to keep the split fragments in
- * document order.
- * @returns A stable `(phraseId, splitAfterTokenRef) => void` callback.
+ * Returns a stable handler that splits a phrase arc at a token boundary and dispatches the
+ * resulting create, update, and delete operations. Reads the by-id phrase map internally, so
+ * callers supply only the document order used to keep the split fragments ordered. Splitting a
+ * phrase that is not in the map is a no-op.
*/
export function useArcSplitHandler(
tokenDocOrder: ReadonlyMap,
@@ -67,12 +59,7 @@ export function useArcSplitHandler(
/**
* Derives the set of phrase ids that contain at least one of the hovered link-candidate tokens, so
- * the arcs of those phrases can be highlighted as link targets. Returns an empty set when nothing
- * is hovered.
- *
- * @param candidateTokenRefs - Token refs a hovered link icon would join into a new phrase.
- * @param phraseLinkByRef - Token ref → phrase link map for the whole strip.
- * @returns The set of `analysisId`s whose phrase contains a candidate token.
+ * the arcs of those phrases can be highlighted as link targets. Empty when nothing is hovered.
*/
export function useCandidatePhraseIds(
candidateTokenRefs: ReadonlySet,
@@ -122,7 +109,7 @@ export type PhraseStripContextParams = Readonly<{
boundaryMergeAltHint: string;
/** Label and tooltip for the Alt-gated split marker. */
boundarySplitLabel: string;
- /** Placeholder for all gloss inputs, fetched once per strip (see the context field's doc). */
+ /** Placeholder for all gloss inputs, fetched once per strip rather than per token. */
glossPlaceholder: string;
/** When true, the link-slot sliding-door transition is suppressed (duration 0ms). */
skipLinkTransition: boolean;
@@ -132,12 +119,9 @@ export type PhraseStripContextParams = Readonly<{
/**
* Builds the memoized strip-wide {@link PhraseStripContextValue} shared by every phrase group and
- * link slot in one render, so the leaf `MemoizedPhraseBox` / `MemoizedTokenLinkIcon` consumers
- * don't re-render on unrelated changes. Centralizing the build keeps the (long) dependency list in
- * one place so the two views can't drift apart when a field is added.
- *
- * @param params - The fields each view resolves and passes in.
- * @returns The memoized strip-wide context value.
+ * link slot in one render, so the memoized leaves don't re-render on unrelated changes.
+ * Centralizing the build keeps the long dependency list in one place, so the two views cannot drift
+ * apart when a field is added.
*/
export function usePhraseStripContextValue(
params: PhraseStripContextParams,
diff --git a/src/hooks/useRecenterSnap.ts b/src/hooks/useRecenterSnap.ts
index 33b00f51..4815aa4a 100644
--- a/src/hooks/useRecenterSnap.ts
+++ b/src/hooks/useRecenterSnap.ts
@@ -89,21 +89,14 @@ export interface UseRecenterSnapResult {
* Owns the post-recenter re-snap and settle lifecycle for the segment window.
*
* After a recenter rebuilds the window the freshly-mounted segments do not reach their final
- * heights synchronously — arc padding is measured and applied by `useArcPaths` across several later
- * frames (a ResizeObserver → rAF → setState chain) — so a one-shot snap can compute its target
- * against stale heights and leave the verse off screen, pinned to an edge. This hook re-snaps the
- * verse against each settling wave (event-driven, via {@link UseRecenterSnapResult.relayResize}, not
- * a per-frame loop) and reports settled once the layout goes quiet. All of it runs behind the
- * recenter fade, so none of the intermediate corrections are seen.
+ * heights synchronously, because arc padding is measured and applied across several later frames. A
+ * one-shot snap would therefore compute its target against stale heights and leave the verse off
+ * screen, pinned to an edge. This hook instead re-snaps the verse against each settling wave —
+ * event-driven rather than a per-frame loop — and reports settled once the layout goes quiet. All
+ * of it runs behind the recenter fade, so none of the intermediate corrections are seen.
*
- * Extracted from {@link useSegmentWindow} so its intricate timing (epoch bump, quiet-debounce, and
- * deadline backstop) lives behind one boundary rather than tangled into the main hook body.
- *
- * @param args - Hook arguments.
- * @param args.snapActiveToTop - Snaps the recenter target to the top of the container.
- * @param args.needsInitialSnap - Whether the initial mount needs a snap (cross-book remount).
- * @param args.onSettled - Reported once per settle; lifts the cross-book loader curtain.
- * @returns The recenter epoch, in-flight ref, and the start/begin/relay handlers.
+ * Extracted from the segment-window hook so its intricate timing (epoch bump, quiet-debounce, and
+ * deadline backstop) lives behind one boundary rather than tangled into that hook's body.
*/
export default function useRecenterSnap({
snapActiveToTop,
diff --git a/src/hooks/useSegmentWindow.ts b/src/hooks/useSegmentWindow.ts
index e35bca89..1c9806f1 100644
--- a/src/hooks/useSegmentWindow.ts
+++ b/src/hooks/useSegmentWindow.ts
@@ -98,8 +98,6 @@ export interface UseSegmentWindowArgs {
* Routing this through a callback in the timeout — rather than the parent reacting to a
* hook-returned value via an effect, which would land a commit later — keeps the two in one
* commit, so the strip is present when the snap settles.
- *
- * @param displayContinuousScroll - The continuous-scroll mode to render from now on.
*/
onDisplayContinuousScrollChange: (displayContinuousScroll: boolean) => void;
/**
@@ -160,8 +158,6 @@ export interface UseSegmentWindowResult {
* the segment that actually contains it rather than only to exact segment starts. Falls back to the
* first segment of the same book+chapter, then to `0`, so there is always a valid anchor.
*
- * @param segments - The book's flat segment list.
- * @param scrRef - The scripture reference whose owning segment to locate.
* @returns The index of the anchor segment, clamped to a valid position (or `0` when empty).
*/
function findAnchorIndex(segments: readonly Segment[], scrRef: SerializedVerseRef): number {
@@ -173,13 +169,7 @@ function findAnchorIndex(segments: readonly Segment[], scrRef: SerializedVerseRe
return chapter === -1 ? 0 : chapter;
}
-/**
- * Builds the initial/recentered window range surrounding `anchorIndex`, clamped to `[0, total)`.
- *
- * @param anchorIndex - Index of the segment to center on.
- * @param total - Total number of segments in the book.
- * @returns The half-open window range to mount.
- */
+/** Builds the half-open window range centered on an anchor segment, clamped to the book. */
function buildCenteredRange(anchorIndex: number, total: number): WindowRange {
const start = Math.max(0, anchorIndex - INITIAL_HALF_WINDOW);
const end = Math.min(total, anchorIndex + INITIAL_HALF_WINDOW + 1);
@@ -201,17 +191,6 @@ function buildCenteredRange(anchorIndex: number, total: number): WindowRange {
* the mounted window, so the fade and the strip's fade can never disagree. Internal navigation (a
* segment/token click here, or strip arrow nav echoed back) skips the fade entirely: the target is
* already on screen.
- *
- * @param args - Hook arguments.
- * @param args.book - The tokenized book whose `segments` are windowed.
- * @param args.scrRef - Current scripture reference; its verse is the recenter anchor.
- * @param args.segmentationVersion - Monotonic boundary-edit counter; classifies segments-identity
- * changes as boundary edits (no fade) vs. re-tokenizations (recenter with fade).
- * @param args.scrollContainerRef - Ref to the scrollable list container.
- * @param args.consumeInternalNav - Returns whether the navigation to a given verse was internal
- * (and clears the marker); used to suppress the fade for navigation that came from within the
- * views.
- * @returns The mounted segment slice, fade state, and the sentinel/content ref callbacks.
*/
export default function useSegmentWindow({
book,
diff --git a/src/hooks/useSubmitGuard.ts b/src/hooks/useSubmitGuard.ts
index a3f94318..f6a9c8fe 100644
--- a/src/hooks/useSubmitGuard.ts
+++ b/src/hooks/useSubmitGuard.ts
@@ -1,4 +1,3 @@
-/** @file Hook that guards an async submit handler against re-entrant (double-click) invocation. */
import { useCallback, useRef, useState } from 'react';
/** Return value of {@link useSubmitGuard}. */
@@ -6,10 +5,9 @@ export type UseSubmitGuardResult = {
/** True while a guarded call is in flight; wire to the submit controls' `disabled`. */
isSubmitting: boolean;
/**
- * Runs `fn` unless a prior guarded call is still in flight, in which case the call is ignored.
- * Flips {@link isSubmitting} for the duration of `fn` and always clears it afterward.
+ * Runs the given submit work unless a prior guarded call is still in flight, in which case the
+ * call is ignored. Flips {@link isSubmitting} for the duration and always clears it afterward.
*
- * @param fn - The async submit work to run.
* @returns A promise that resolves once `fn` settles (or immediately when the call is ignored).
*/
runGuarded: (fn: () => Promise) => Promise;
@@ -18,11 +16,10 @@ export type UseSubmitGuardResult = {
/**
* Guards an async submit handler against re-entrant invocation (a double-click, or a programmatic
* second call before the re-render that disables the control lands). A ref mirror short-circuits
- * the second call synchronously; the state drives the disabled UI. Factors out the `isSubmitting` +
- * `isSubmittingRef` + try/finally pattern the Save As and metadata modals each repeated for every
- * submit handler.
+ * the second call synchronously, while the state drives the disabled UI.
*
- * @returns The in-flight flag and a `runGuarded` wrapper for submit handlers.
+ * Factors out the flag-plus-mirror-plus-try/finally pattern the Save As and metadata modals each
+ * repeated for every submit handler.
*/
export default function useSubmitGuard(): UseSubmitGuardResult {
const [isSubmitting, setIsSubmitting] = useState(false);
diff --git a/src/interlinearizer.web-view.tsx b/src/interlinearizer.web-view.tsx
index 8a89fa45..d6a673da 100644
--- a/src/interlinearizer.web-view.tsx
+++ b/src/interlinearizer.web-view.tsx
@@ -4,16 +4,13 @@ import InterlinearizerLoader from './components/InterlinearizerLoader';
/**
* Root WebView component for the Interlinearizer.
*
- * @param props - WebView props injected by the PAPI host
- * @param props.projectId - PAPI project ID passed from the host; undefined when the WebView is
- * opened outside a project context
- * @param props.useWebViewScrollGroupScrRef - Hook that exposes the shared scroll-group scripture
- * reference and its setter
- * @param props.useWebViewState - Hook for reading and writing values persisted in the WebView's
- * saved state (survives tab restores)
- * @param props.updateWebViewDefinition - Host-injected callback to update this WebView's
- * definition; forwarded so the loader can toggle the tab's unsaved-changes title marker
- * @returns The full interlinearizer WebView layout
+ * @param props.projectId - `undefined` when the WebView is opened outside a project context.
+ * @param props.useWebViewScrollGroupScrRef - Exposes the shared scroll-group scripture reference
+ * and its setter.
+ * @param props.useWebViewState - Reads and writes values persisted in the WebView's saved state,
+ * which survives tab restores.
+ * @param props.updateWebViewDefinition - Forwarded so the loader can toggle the tab's
+ * unsaved-changes title marker.
*/
globalThis.webViewComponent = function InterlinearizerWebView({
projectId,
diff --git a/src/main.ts b/src/main.ts
index 8d058f90..3e8c5fd5 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -35,7 +35,6 @@ const mainWebViewProvider: IWebViewProvider = {
*
* @param savedWebView - Platform-provided definition (webViewType, etc.).
* @param openWebViewOptions - Options passed by the caller; may include a projectId to link.
- * @returns WebView definition with title, content, and styles.
* @throws {TypeError} When savedWebView.webViewType is not the Interlinearizer type.
*/
async getWebView(
@@ -132,7 +131,6 @@ async function openInterlinearizerForWebView(webViewId?: string): Promise :0"`, parsed downstream into a verse-0 `Segment`. When the chapter node
* carries no `number` the scope cannot be named, so it is not opened.
- *
- * @param node - The `chapter` USJ node; `node.number` is the chapter number (e.g. `"3"`).
- * @param state - Shared traversal state updated in place.
*/
function handleChapterNode(node: UsjNode, state: TraversalState): void {
closeCurrentVerse(state);
@@ -192,7 +180,6 @@ function handleChapterNode(node: UsjNode, state: TraversalState): void {
* `"GEN 1:7"`, `"1a"` from `"GEN 1:1a"`). Used as the rendered verse number when a `verse` marker
* omits its `number` attribute.
*
- * @param sid - Verse SID string (e.g. `"GEN 1:7"`).
* @returns The verse portion after the final colon; the whole `sid` when it contains no colon.
*/
function verseNumberFromSid(sid: string): string {
@@ -204,8 +191,6 @@ function verseNumberFromSid(sid: string): string {
* verse number is the marker's verbatim `number` attribute, or the sid-derived verse portion when
* the marker omits it.
*
- * @param node - The `verse` USJ node; must carry a `sid` attribute (e.g. `"GEN 1:1"`).
- * @param state - Shared traversal state updated in place.
* @throws {SyntaxError} If the `verse` node is missing its required `sid` attribute.
* @throws {SyntaxError} If the `verse` SID has already been seen (duplicate verse SID).
*/
@@ -227,9 +212,6 @@ function handleVerseNode(node: UsjNode, state: TraversalState): void {
* Recurses into a `para` node's content, appending a space between adjacent para nodes when needed.
* Heading-class paragraphs (see {@link HEADING_PARA_MARKERS}) are skipped entirely so their text is
* not included in the verse baseline.
- *
- * @param node - The `para` USJ node; `node.marker` determines whether to skip or recurse.
- * @param state - Shared traversal state updated in place.
*/
function handleParaNode(node: UsjNode, state: TraversalState): void {
if (node.marker && HEADING_PARA_MARKERS.has(node.marker)) return;
@@ -257,8 +239,6 @@ const NODE_HANDLERS: Partial :0"`,
* but only when it has text; see {@link handleChapterNode}.
*
- * @param usj - USJ document returned by `useProjectData('platformScripture.USJ_Book', ...)`.
- * @param writingSystem - BCP 47 tag for the baseline, from `platform.languageTag`.
- * @returns A `RawBook` with `bookCode`, `writingSystem`, `contentHash`, and `verses` populated.
* @throws {SyntaxError} If no `book` marker with a `code` attribute is found in the document.
* @throws {SyntaxError} If a `verse` marker is missing its required `sid` attribute (propagated
* from {@link handleVerseNode} via {@link traverse}).
diff --git a/src/parsers/pt9/interlinearXmlParser.ts b/src/parsers/pt9/interlinearXmlParser.ts
index 04d37574..5e8acab0 100644
--- a/src/parsers/pt9/interlinearXmlParser.ts
+++ b/src/parsers/pt9/interlinearXmlParser.ts
@@ -138,10 +138,9 @@ interface ParsedInterlinearXml {
}
/**
- * Maps a parsed Cluster's Lexeme array to {@link LexemeData} array.
+ * Maps a parsed Cluster's Lexeme children to {@link LexemeData}. A lexeme with no gloss id yields an
+ * empty sense id rather than being dropped.
*
- * @param clusterElement - Parsed Cluster from fast-xml-parser (may have Lexeme array or none).
- * @returns Array of LexemeData with LexemeId (from Id) and SenseId (from GlossId, or '').
* @throws {SyntaxError} If any Lexeme element is missing the required Id attribute.
*/
function extractLexemesFromCluster(clusterElement: ParsedCluster): LexemeData[] {
@@ -160,10 +159,8 @@ function extractLexemesFromCluster(clusterElement: ParsedCluster): LexemeData[]
* Parses a string to a non-negative integer, returning `undefined` for empty or non-integer values.
* Used to validate XML attribute strings before converting them to numeric ranges.
*
- * @param raw - The string to parse (e.g. an XML attribute value). `undefined` is accepted because
- * fast-xml-parser may omit attributes at runtime despite the declared type.
- * @returns The parsed non-negative integer, or `undefined` if the input is absent, empty, or
- * non-integer.
+ * @param raw - `undefined` is accepted because fast-xml-parser may omit attributes at runtime
+ * despite the declared type.
*/
const parseStrictNumber = (raw: string | undefined): number | undefined => {
if (raw === undefined || raw.trim() === '') return undefined;
@@ -178,11 +175,6 @@ const parseStrictNumber = (raw: string | undefined): number | undefined => {
* non-integer `Index`/`Length`) are silently filtered out rather than throwing. Punctuation data is
* non-critical to the interlinear display; clusters are validated strictly in
* {@link extractClustersFromVerse} because they are required for alignment rendering.
- *
- * @param verseDataElement - Parsed VerseData from fast-xml-parser (may have Punctuation array or
- * none).
- * @returns Array of PunctuationData (TextRange from Range Index/Length, BeforeText, AfterText).
- * Entries without a valid Range element are skipped.
*/
function extractPunctuationsFromVerse(verseDataElement: ParsedVerseData): PunctuationData[] {
const elements = verseDataElement.Punctuation ?? [];
@@ -204,11 +196,10 @@ function extractPunctuationsFromVerse(verseDataElement: ParsedVerseData): Punctu
}
/**
- * Maps a parsed VerseData's Cluster array to {@link ClusterData} array.
+ * Maps a parsed VerseData's Cluster children to {@link ClusterData}. Each cluster's composite `Id`
+ * is its slash-joined lexeme ids followed by its text range, or the range alone when it has no
+ * lexemes.
*
- * @param verseDataElement - Parsed VerseData from fast-xml-parser (may have Cluster array or none).
- * @returns Array of ClusterData: TextRange from Cluster's Range, Lexemes from Lexeme children,
- * LexemesId (slash-joined), Id (LexemesId/Index-Length or Index-Length when no lexemes).
* @throws {SyntaxError} If a Cluster is missing its Range element, Range is missing Index or
* Length, or Range Index/Length are not non-negative integers.
* @throws {SyntaxError} If any Lexeme element in a Cluster is missing the required Id attribute
@@ -296,11 +287,8 @@ export class InterlinearXmlParser {
/**
* Parses an interlinear XML string into {@link InterlinearData}.
*
- * @param xml - Raw XML string (e.g. file contents). Must be valid interlinear XML with
- * InterlinearData root, GlossLanguage and BookId attributes, and Verses containing item
- * entries.
- * @returns Parsed interlinear data: ScrTextName, GlossLanguage, BookId, and Verses (record of
- * verse key to {@link VerseData} with Hash, Clusters, Punctuations).
+ * @param xml - Must have an InterlinearData root with GlossLanguage and BookId attributes, and a
+ * Verses element containing item entries.
* @throws {SyntaxError} If the `InterlinearData` root element is absent.
* @throws {SyntaxError} If `GlossLanguage` or `BookId` attributes are missing or empty.
* @throws {SyntaxError} If the `Verses` element is absent.
diff --git a/src/services/projectStorage.ts b/src/services/projectStorage.ts
index 3300134a..73d61ee1 100644
--- a/src/services/projectStorage.ts
+++ b/src/services/projectStorage.ts
@@ -56,7 +56,6 @@ const draftQueues = new Map>();
* @param get - Returns the current tail of the queue to chain `fn` after.
* @param set - Stores the new tail of the queue (a promise that settles once `fn` settles).
* @param fn - The async function to serialize.
- * @returns A promise that resolves or rejects with the return value of `fn`.
* @throws Whatever `fn` throws; the queue advances past the error so later operations are not
* blocked.
*/
@@ -74,8 +73,6 @@ function enqueueOnQueue(
* Enqueues `fn` on the index serialization queue. Thin wrapper over {@link enqueueOnQueue} bound to
* {@link indexQueue}.
*
- * @param fn - The async function to serialize.
- * @returns A promise that resolves or rejects with the return value of `fn`.
* @throws Whatever `fn` throws; the queue advances past the error so later operations are not
* blocked.
*/
@@ -93,8 +90,6 @@ function enqueueIndexOp(fn: () => Promise): Promise {
* Enqueues `fn` on the pending-cleanup serialization queue. Thin wrapper over {@link enqueueOnQueue}
* bound to {@link pendingCleanupQueue}.
*
- * @param fn - The async function to serialize.
- * @returns A promise that resolves or rejects with the return value of `fn`.
* @throws Whatever `fn` throws; the queue advances past the error so later operations are not
* blocked.
*/
@@ -116,7 +111,6 @@ function enqueuePendingCleanupOp(fn: () => Promise): Promise {
* @param queues - The queue map to serialize on; one chain per `key`.
* @param key - The key whose queue `fn` should join.
* @param fn - The async function to serialize.
- * @returns A promise that resolves or rejects with the return value of `fn`.
* @throws Whatever `fn` throws; the queue entry is removed and the rejection propagates to the
* caller.
*/
@@ -140,9 +134,6 @@ function enqueueSerialized(
* Enqueues `fn` on the per-project serialization queue for `id`. Thin wrapper over
* {@link enqueueSerialized} bound to {@link projectQueues}.
*
- * @param id - The project UUID whose queue `fn` should join.
- * @param fn - The async function to serialize.
- * @returns A promise that resolves or rejects with the return value of `fn`.
* @throws Whatever `fn` throws; the queue entry is removed and the rejection propagates to the
* caller.
*/
@@ -150,22 +141,12 @@ function enqueueProjectOp(id: string, fn: () => Promise): Promise {
return enqueueSerialized(projectQueues, id, fn);
}
-/**
- * Returns the storage key for a project by ID.
- *
- * @param id - The project UUID.
- * @returns The storage key string used to read and write the project record.
- */
+/** Returns the storage key for a project by ID. */
function projectKey(id: string): string {
return `project:${id}`;
}
-/**
- * Returns the storage key for a source project's draft.
- *
- * @param sourceProjectId - The Platform.Bible source project ID the draft belongs to.
- * @returns The storage key string used to read and write the draft record.
- */
+/** Returns the storage key for a source project's draft. */
function draftKey(sourceProjectId: string): string {
return `draft:${sourceProjectId}`;
}
@@ -173,9 +154,6 @@ function draftKey(sourceProjectId: string): string {
/**
* Returns true when `e` is a file-not-found error (ENOENT) from the Node.js file system, which is
* what `papi.storage.readUserData` throws when the requested key has never been written.
- *
- * @param e - The caught value.
- * @returns Whether the error represents a missing storage key.
*/
function isNotFound(e: unknown): boolean {
return !!e && typeof e === 'object' && 'code' in e && e.code === 'ENOENT';
@@ -184,9 +162,6 @@ function isNotFound(e: unknown): boolean {
/**
* Type guard for a JSON-parsed value that must be an array of strings — the shape of both the
* `projectIds` index and the `pendingCleanup` set.
- *
- * @param value - The parsed value to test.
- * @returns Whether `value` is an array whose every element is a string.
*/
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every((id) => typeof id === 'string');
@@ -197,8 +172,6 @@ function isStringArray(value: unknown): value is string[] {
* Shared by {@link readIds} and {@link readPendingCleanup}. Returns the raw parsed value as `unknown`
* and does not validate that it is an array of strings; each caller validates the shape itself.
*
- * @param token - The execution token for storage access.
- * @param key - The storage key to read.
* @returns The parsed value, or an empty array if `key` has never been written (ENOENT).
* @throws {SyntaxError} If the stored value contains invalid JSON.
* @throws If `papi.storage.readUserData` rejects for any non-ENOENT reason (e.g. permission denied,
@@ -221,7 +194,6 @@ async function readJsonArray(token: ExecutionToken, key: string): Promise
* per-record deletions run concurrently within the cycle; the set is small and each targets a
* distinct key.
*
- * @param token - The execution token for storage access.
* @returns A promise resolving to the number of orphaned records successfully deleted this pass
* (live ids dropped from the set without deleting their record are not counted).
* @throws If reading the set or rewriting it (`papi.storage.writeUserData`) rejects for a
@@ -387,7 +354,6 @@ export function sweepPendingCleanup(token: ExecutionToken): Promise {
* when omitted the project is analysis-only and `links` is left undefined.
* @param name - Optional user-facing name for the project.
* @param description - Optional user-facing description for the project.
- * @returns The newly created project record.
* @throws {SyntaxError} If the `projectIds` storage value contains invalid JSON.
* @throws If the project or index `papi.storage.writeUserData` rejects. On an index-write failure
* the original error is rethrown after best-effort rollback; a failed rollback does not throw —
@@ -444,8 +410,6 @@ export async function createProject(
/**
* Returns the project with the given ID.
*
- * @param token - The execution token for storage access.
- * @param id - The project UUID.
* @returns The project record, or `undefined` if it does not exist in storage (ENOENT).
* @throws {SyntaxError} If the project's storage value contains invalid JSON.
* @throws If `papi.storage.readUserData` rejects for any non-ENOENT reason.
@@ -467,7 +431,6 @@ export async function getProject(
* after a failed delete) are silently omitted. Projects that fail to read or parse are logged and
* skipped so a single corrupted record does not prevent access to the rest.
*
- * @param token - The execution token for storage access.
* @returns All stored projects, ordered by creation time.
* @throws {SyntaxError} If `projectIds` contains invalid JSON.
* @throws If `papi.storage.readUserData` rejects for any non-ENOENT reason when reading the index.
@@ -491,8 +454,6 @@ export async function listProjects(token: ExecutionToken): Promise t.surfaceText).join(' ');
}
@@ -124,9 +119,6 @@ function phraseSurfaceText(tokens: TokenSnapshot[]): string {
/**
* Removes the `PhraseAnalysis` record and its `PhraseAnalysisLink` matching `phraseId` from the
* Immer draft state in a single step, ensuring both collections stay in sync.
- *
- * @param state - Current slice state (Immer draft).
- * @param phraseId - ID of the phrase to remove.
*/
function removePhraseById(state: AnalysisState, phraseId: string): void {
state.analysis.phraseAnalyses = state.analysis.phraseAnalyses.filter((pa) => pa.id !== phraseId);
@@ -142,8 +134,6 @@ function removePhraseById(state: AnalysisState, phraseId: string): void {
* or accumulates duplicate approved links — and `undefined` is returned as if no approved link
* existed. Mirrors {@link resolveApprovedAnalysis} for the segment layer.
*
- * @param state - Current slice state (Immer draft).
- * @param segmentId - `Segment.id` of the segment to look up.
* @returns The approved link and its analysis, or `undefined` when the segment has none.
*/
function resolveApprovedSegmentAnalysis(
@@ -173,7 +163,6 @@ function resolveApprovedSegmentAnalysis(
* holding only whitespace is dropped — mirroring how {@link isEmptyTokenAnalysis} preserves
* morphemes/pos.
*
- * @param analysis - The `SegmentAnalysis` to inspect.
* @returns `true` when the record holds no content worth keeping.
*/
function isEmptySegmentAnalysis(analysis: SegmentAnalysis): boolean {
@@ -185,10 +174,6 @@ function isEmptySegmentAnalysis(analysis: SegmentAnalysis): boolean {
/**
* Removes a `SegmentAnalysis` record and its `SegmentAnalysisLink` from the draft in a single step,
* keeping the two collections in sync. Called when an edit empties an analysis of all content.
- *
- * @param state - Current slice state (Immer draft).
- * @param analysis - The `SegmentAnalysis` record to remove.
- * @param link - The `SegmentAnalysisLink` referencing it.
*/
function removeSegmentAnalysis(
state: AnalysisState,
@@ -212,8 +197,6 @@ function removeSegmentAnalysis(
* approved links — and `undefined` is returned as if no approved link existed. Every token-analysis
* reducer resolves through this helper so they all repair orphaned links the same way.
*
- * @param state - Current slice state (Immer draft).
- * @param tokenRef - `Token.ref` of the token to look up.
* @returns The approved link and its analysis, or `undefined` when the token has none.
*/
function resolveApprovedAnalysis(
@@ -241,10 +224,6 @@ function resolveApprovedAnalysis(
* records _this_ token's surface text (from `analysis.surfaceText`), not the shared payload's, so
* per-token drift detection stays accurate even when a sentence-initial form links to a payload
* first created from a mid-sentence form.
- *
- * @param state - Current slice state (Immer draft).
- * @param analysis - The candidate `TokenAnalysis` record to link or, if novel, append.
- * @param tokenRef - `Token.ref` of the token the link points at.
*/
function appendApprovedAnalysis(
state: AnalysisState,
@@ -268,10 +247,6 @@ function appendApprovedAnalysis(
* remaining references is what stops an edit on one token from orphaning a payload that another
* token still links to. A payload kept alive by a surviving link may be momentarily empty; it is
* reclaimed when that last link is cleared.
- *
- * @param state - Current slice state (Immer draft).
- * @param analysis - The emptied `TokenAnalysis` payload.
- * @param link - The `TokenAnalysisLink` from the editing token to remove.
*/
function detachTokenAnalysisLink(
state: AnalysisState,
@@ -293,9 +268,6 @@ function detachTokenAnalysisLink(
* token. The shared/sole distinction is what tells a clear, delete, or fork whether it must work on
* a private clone (to spare the co-linked tokens) or may mutate the payload in place.
*
- * @param state - Current slice state (Immer draft).
- * @param link - The editing token's approved `TokenAnalysisLink` (excluded from the check).
- * @param analysisId - `TokenAnalysis.id` of the payload to test.
* @returns `true` when at least one other link points at the same payload.
*/
function isPayloadSharedByOtherLinks(
@@ -316,12 +288,6 @@ function isPayloadSharedByOtherLinks(
* and `morphemes` containers so the returned draft can be edited or cleared in the same reducer
* without writing through to the frozen shared payload. Used by the per-token edit, clear, and
* delete paths so a token can edit or detach from a shared analysis without affecting the others.
- *
- * @param state - Current slice state (Immer draft).
- * @param link - The editing token's approved `TokenAnalysisLink`, repointed to the clone.
- * @param analysis - The shared payload to clone.
- * @param cloneId - Pre-generated UUID for the clone (from the action's `prepare`).
- * @returns The clone's draft, for the caller to edit or clear in place.
*/
function forkSharedAnalysis(
state: AnalysisState,
@@ -348,9 +314,6 @@ function forkSharedAnalysis(
* payload and `analysis` is dropped — collapsing a homograph instance that was edited to match a
* sibling back onto one shared payload (frequency re-merged, no duplicate suggestion). A no-op when
* the edit left the payload unique.
- *
- * @param state - Current slice state (Immer draft).
- * @param analysis - The payload just edited in place.
*/
function mergeIntoIdenticalPayload(state: AnalysisState, analysis: TokenAnalysis): void {
const other = state.analysis.tokenAnalyses.find(
@@ -379,7 +342,6 @@ function mergeIntoIdenticalPayload(state: AnalysisState, analysis: TokenAnalysis
* a future workflow needs provenance-only records (e.g. imported parser metadata) to survive a
* gloss clear, add the relevant fields to the check below.
*
- * @param analysis - The `TokenAnalysis` to inspect.
* @returns `true` when the record holds no analysis content worth keeping.
*/
function isEmptyTokenAnalysis(analysis: TokenAnalysis): boolean {
@@ -401,11 +363,6 @@ const analysisSlice = createSlice({
/**
* Generates a UUID for a potential new `TokenAnalysis` record before the action reaches the
* reducer, keeping the reducer pure.
- *
- * @param tokenRef - `Token.ref` of the token being glossed.
- * @param surfaceText - Surface text of the token.
- * @param value - New gloss string.
- * @returns The prepared action payload including a pre-generated `id`.
*/
prepare(tokenRef: string, surfaceText: string, value: string) {
return { payload: { tokenRef, surfaceText, value, id: crypto.randomUUID() } };
@@ -432,9 +389,6 @@ const analysisSlice = createSlice({
* gloss rather than being stranded on an emptied payload. A blank write to a token with no
* approved analysis is a no-op, so a focus/blur cycle on an empty gloss never creates a
* record.
- *
- * @param state - Current slice state (Immer draft).
- * @param action - Action carrying the `WriteGlossPayload`.
*/
reducer(state, action: PayloadAction) {
const { tokenRef, surfaceText, value, id } = action.payload;
@@ -484,14 +438,8 @@ const analysisSlice = createSlice({
writeMorphemes: {
/**
* Generates UUIDs for new morpheme records and a potential new `TokenAnalysis` before the
- * action reaches the reducer.
- *
- * @param tokenRef - `Token.ref` of the token whose morphemes are being set.
- * @param surfaceText - Surface text of the token.
- * @param forms - Ordered morpheme form strings as entered by the user.
- * @param writingSystem - BCP 47 tag of the token's surface text (`Token.writingSystem`),
- * stored on each morpheme as the writing system of its form.
- * @returns The prepared action payload.
+ * action reaches the reducer. The token's own writing system is stored on each morpheme as
+ * the writing system of its form.
*/
prepare(tokenRef: string, surfaceText: string, forms: string[], writingSystem: string) {
return {
@@ -520,9 +468,6 @@ const analysisSlice = createSlice({
* stamped with the supplied writing system, so records written before the writing system was
* threaded through (which wrongly stored the analysis language) self-correct on the next
* save.
- *
- * @param state - Current slice state (Immer draft).
- * @param action - Action carrying the morpheme payload.
*/
reducer(
state,
@@ -588,10 +533,6 @@ const analysisSlice = createSlice({
* only when the breakdown is removed from a shared payload — keeping the reducer pure.
* Accepts the same `{ tokenRef }` argument the action took before, so call sites are
* unchanged.
- *
- * @param arg - Object carrying the `tokenRef` whose breakdown is removed.
- * @param arg.tokenRef - `Token.ref` of the token whose morphemes are removed.
- * @returns The prepared action payload including a pre-generated clone `id`.
*/
prepare(arg: { tokenRef: string }) {
return { payload: { tokenRef: arg.tokenRef, id: crypto.randomUUID() } };
@@ -605,10 +546,6 @@ const analysisSlice = createSlice({
* {@link forkSharedAnalysis}) so the co-linked tokens keep their morphemes. No-ops when the
* token has no approved analysis or the analysis has no morphemes (an orphaned approved link
* is still repaired; see {@link resolveApprovedAnalysis}).
- *
- * @param state - Current slice state (Immer draft).
- * @param action - Action carrying the `tokenRef` whose breakdown is removed and the clone
- * `id`.
*/
reducer(state, action: PayloadAction<{ tokenRef: string; id: string }>) {
const { tokenRef, id } = action.payload;
@@ -655,21 +592,11 @@ const analysisSlice = createSlice({
/**
* Generates a UUID for the clone a per-token edit forks from a shared payload, before the
* action reaches the reducer, keeping the reducer pure. Unused when the payload is not
- * shared.
- *
- * @param arg - Object carrying the token, morpheme, and new gloss value.
- * @param arg.tokenRef - `Token.ref` of the token whose morpheme gloss is written.
- * @param arg.morphemeId - Id of the morpheme within the token's approved analysis.
- * @param arg.value - New gloss string (blank clears the morpheme's active-language gloss).
- * @returns The prepared action payload including a pre-generated clone `id`.
+ * shared. A blank gloss value clears the morpheme's active-language gloss.
*/
prepare(arg: { tokenRef: string; morphemeId: string; value: string }) {
return { payload: { ...arg, id: crypto.randomUUID() } };
},
- /**
- * @param state - Current slice state (Immer draft).
- * @param action - Action carrying the morpheme gloss payload and a pre-generated clone `id`.
- */
reducer(
state,
action: PayloadAction<{
@@ -733,11 +660,6 @@ const analysisSlice = createSlice({
* as a fresh orphan. The link's snapshot records _this_ token's `surfaceText` (not the shared
* payload's), matching {@link appendApprovedAnalysis} so per-token drift detection stays
* accurate.
- *
- * @param state - Current slice state (Immer draft).
- * @param action - Action carrying the accepting `tokenRef`, its `surfaceText`, and the
- * `analysisId` of the payload to approve (the suggested payload, or a candidate when
- * promoting).
*/
approveAnalysisForToken(
state,
@@ -774,19 +696,11 @@ const analysisSlice = createSlice({
/**
* Generates a UUID for the new `PhraseAnalysis` before the action reaches the reducer,
* keeping the reducer pure.
- *
- * @param tokens - Ordered `TokenSnapshot`s forming the phrase, in document order.
- * @returns The prepared action payload including a pre-generated `id`.
*/
prepare(tokens: TokenSnapshot[]) {
return { payload: { id: crypto.randomUUID(), tokens } };
},
- /**
- * Appends a new approved `PhraseAnalysis` and its `PhraseAnalysisLink` to the analysis.
- *
- * @param state - Current slice state (Immer draft).
- * @param action - Action carrying the `CreatePhrasePayload`.
- */
+ /** Appends a new approved `PhraseAnalysis` and its `PhraseAnalysisLink` to the analysis. */
reducer(state, action: PayloadAction) {
const { id, tokens } = action.payload;
const newAnalysis: PhraseAnalysis = { id, surfaceText: phraseSurfaceText(tokens) };
@@ -802,9 +716,6 @@ const analysisSlice = createSlice({
* phrase id and any gloss already written on it. When `tokens` is empty the phrase is removed
* entirely (both the analysis record and its link) so a zero-token phrase can never persist in
* the store.
- *
- * @param state - Current slice state (Immer draft).
- * @param action - Action carrying the `UpdatePhrasePayload`.
*/
updatePhrase(state, action: PayloadAction) {
const { phraseId, tokens } = action.payload;
@@ -817,12 +728,7 @@ const analysisSlice = createSlice({
const analysis = state.analysis.phraseAnalyses.find((pa) => pa.id === phraseId);
if (analysis) analysis.surfaceText = phraseSurfaceText(tokens);
},
- /**
- * Removes the `PhraseAnalysis` record and its `PhraseAnalysisLink` for the given phrase id.
- *
- * @param state - Current slice state (Immer draft).
- * @param action - Action carrying the `DeletePhrasePayload`.
- */
+ /** Removes the `PhraseAnalysis` record and its `PhraseAnalysisLink` for the given phrase id. */
deletePhrase(state, action: PayloadAction) {
const { phraseId } = action.payload;
removePhraseById(state, phraseId);
@@ -837,9 +743,6 @@ const analysisSlice = createSlice({
*
* No-ops when `absorbedPhraseId === targetPhraseId` to prevent the update from being
* immediately undone by the delete.
- *
- * @param state - Current slice state (Immer draft).
- * @param action - Action carrying the `MergePhrasesPayload`.
*/
mergePhrases(state, action: PayloadAction) {
const { targetPhraseId, tokens, absorbedPhraseId } = action.payload;
@@ -854,9 +757,6 @@ const analysisSlice = createSlice({
/**
* Writes a gloss value into the `PhraseAnalysis` record for the given phrase id. No-ops when no
* matching `PhraseAnalysis` is found.
- *
- * @param state - Current slice state (Immer draft).
- * @param action - Action carrying the `WritePhraseGlossPayload`.
*/
writePhraseGloss(state, action: PayloadAction) {
const { phraseId, value } = action.payload;
@@ -870,11 +770,6 @@ const analysisSlice = createSlice({
/**
* Generates a UUID for a potential new `SegmentAnalysis` record before the action reaches the
* reducer, keeping the reducer pure.
- *
- * @param segmentId - `Segment.id` of the segment being translated.
- * @param surfaceText - Baseline text of the segment.
- * @param value - New free-translation string.
- * @returns The prepared action payload including a pre-generated `id`.
*/
prepare(segmentId: string, surfaceText: string, value: string) {
return { payload: { segmentId, surfaceText, value, id: crypto.randomUUID() } };
@@ -892,9 +787,6 @@ const analysisSlice = createSlice({
* ({@link isEmptySegmentAnalysis}) the record and its link are removed entirely. A blank write
* to a segment with no approved analysis is a no-op, so a focus/blur cycle on an empty input
* never creates a record.
- *
- * @param state - Current slice state (Immer draft).
- * @param action - Action carrying the `WriteSegmentFreeTranslationPayload`.
*/
reducer(state, action: PayloadAction) {
const { segmentId, surfaceText, value, id } = action.payload;
@@ -952,28 +844,13 @@ export default analysisSlice.reducer;
// #region Selectors
-/**
- * Projects `tokenAnalyses` out of `AnalysisState` for use as a `createSelector` input.
- *
- * @param state - The analysis slice state.
- * @returns The `tokenAnalyses` array.
- */
+/** Projects `tokenAnalyses` out of `AnalysisState` for use as a `createSelector` input. */
const selectTokenAnalyses = (state: AnalysisState) => state.analysis.tokenAnalyses;
-/**
- * Projects `tokenAnalysisLinks` out of `AnalysisState` for use as a `createSelector` input.
- *
- * @param state - The analysis slice state.
- * @returns The `tokenAnalysisLinks` array.
- */
+/** Projects `tokenAnalysisLinks` out of `AnalysisState` for use as a `createSelector` input. */
const selectTokenAnalysisLinks = (state: AnalysisState) => state.analysis.tokenAnalysisLinks;
-/**
- * Projects `analysisLanguage` out of `AnalysisState` for use as a `createSelector` input.
- *
- * @param state - The analysis slice state.
- * @returns The active BCP 47 analysis language tag.
- */
+/** Projects `analysisLanguage` out of `AnalysisState` for use as a `createSelector` input. */
export const selectAnalysisLanguage = (state: AnalysisState) => state.analysisLanguage;
/**
@@ -1002,20 +879,13 @@ const selectApprovedIdByTokenRef = createSelector(
}, new Map()),
);
-/**
- * Returns the `TextAnalysis` from the analysis slice state.
- *
- * @param state - The analysis slice state.
- * @returns The current `TextAnalysis`.
- */
+/** Returns the `TextAnalysis` from the analysis slice state. */
export const selectAnalysis = (state: AnalysisState) => state.analysis;
/**
* Returns the approved gloss string for `tokenRef` in the active analysis language, or `''` when no
* approved analysis exists or the analysis has no gloss for the active language.
*
- * @param state - The analysis slice state.
- * @param tokenRef - The `Token.ref` to look up.
* @returns The approved gloss string, or `''` when absent.
*/
export function selectApprovedGloss(state: AnalysisState, tokenRef: string): string {
@@ -1071,10 +941,6 @@ export const selectPoolIndex = createSelector(
* selector or pass a shallow/custom `equalityFn` (or `useMemo` the result), or every store change
* will re-render the token and trip react-redux's "selector returned a different result" warning.
*
- * @param state - The analysis slice state.
- * @param tokenRef - The `Token.ref` to resolve.
- * @param surfaceText - The token's current surface text, matched against the pool when the token is
- * unapproved.
* @returns The approved or suggested analysis for the token, or `undefined` when it has neither.
*/
export function selectResolvedTokenAnalysis(
@@ -1102,11 +968,6 @@ export function selectResolvedTokenAnalysis(
* deletion will surface rather than the approved payload's mere alternatives. Returns `undefined`
* when the token has no approval (callers only consult this for an approved token) or when nothing
* in the pool still matches once that approval is discounted.
- *
- * @param state - The analysis slice state.
- * @param tokenRef - The `Token.ref` whose approval to discount.
- * @param surfaceText - The token's current surface text, matched against the pool.
- * @returns The suggested-status resolved analysis as if the token were unapproved, or `undefined`.
*/
export function selectSuggestionAfterClearing(
state: AnalysisState,
@@ -1129,8 +990,6 @@ export function selectSuggestionAfterClearing(
* {@link selectApprovedTokenCountByAnalysisId} — the same approved-link count the reducer's
* `isPayloadSharedByOtherLinks` tests — so the two can never disagree about what "shared" means.
*
- * @param state - The analysis slice state.
- * @param tokenRef - The `Token.ref` whose breakdown would be removed.
* @returns `true` when the reset would irrecoverably discard glosses belonging to this token alone.
*/
export function selectMorphemeResetLosesGlosses(state: AnalysisState, tokenRef: string): boolean {
@@ -1149,11 +1008,9 @@ export function selectMorphemeResetLosesGlosses(state: AnalysisState, tokenRef:
const EMPTY_MORPHEMES: readonly MorphemeAnalysis[] = [];
/**
- * Returns the morpheme array from the approved `TokenAnalysis` for `tokenRef`, or an empty array
- * when no approved analysis exists or it has no morphemes.
+ * Returns the morpheme array from the approved `TokenAnalysis` for `tokenRef`, or a shared
+ * reference-stable empty array when no approved analysis exists or it has no morphemes.
*
- * @param state - The analysis slice state.
- * @param tokenRef - The `Token.ref` to look up.
* @returns The morpheme array, or a stable empty array when absent.
*/
export function selectApprovedMorphemes(
@@ -1166,12 +1023,7 @@ export function selectApprovedMorphemes(
return ta?.morphemes ?? EMPTY_MORPHEMES;
}
-/**
- * Projects `phraseAnalysisLinks` out of `AnalysisState` for use as a `createSelector` input.
- *
- * @param state - The analysis slice state.
- * @returns The `phraseAnalysisLinks` array.
- */
+/** Projects `phraseAnalysisLinks` out of `AnalysisState` for use as a `createSelector` input. */
const selectPhraseAnalysisLinksRaw = (state: AnalysisState) => state.analysis.phraseAnalysisLinks;
/**
@@ -1206,8 +1058,6 @@ export const selectPhraseLinkByAnalysisId = createSelector(
* Returns the approved gloss string for the given phrase in the active analysis language, or `''`
* when no phrase with that id exists or it has no gloss for the active language.
*
- * @param state - The analysis slice state.
- * @param phraseId - The `PhraseAnalysis.id` to look up.
* @returns The gloss string, or `''` when absent.
*/
export function selectPhraseGloss(state: AnalysisState, phraseId: string): string {
@@ -1221,8 +1071,6 @@ export function selectPhraseGloss(state: AnalysisState, phraseId: string): strin
* language. An approved link referencing a missing analysis is treated as absent (read-only here;
* the orphan is repaired on the next write through {@link resolveApprovedSegmentAnalysis}).
*
- * @param state - The analysis slice state.
- * @param segmentId - The `Segment.id` to look up.
* @returns The free-translation string, or `''` when absent.
*/
export function selectSegmentFreeTranslation(state: AnalysisState, segmentId: string): string {
diff --git a/src/store/index.ts b/src/store/index.ts
index a12b3bb2..cb3b7627 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -2,12 +2,9 @@ import { configureStore } from '@reduxjs/toolkit';
import analysisReducer, { type AnalysisState } from './analysisSlice';
/**
- * Creates a Redux store scoped to one `AnalysisStoreProvider` instance. Keeping the store local
- * rather than global means each WebView (or nested provider) has fully isolated state.
- *
- * @param preloadedState - Optional initial state, typically used to seed `initialAnalysis` and
- * `analysisLanguage` from the provider props.
- * @returns A configured Redux store with the analysis reducer mounted at `state.analysis`.
+ * Creates a Redux store scoped to a single analysis provider instance, optionally seeded from that
+ * provider's props. Keeping the store local rather than global means each WebView, or nested
+ * provider, has fully isolated state.
*/
export function createAnalysisStore(preloadedState?: { analysis: AnalysisState }) {
return configureStore({
diff --git a/src/types/empty-factories.ts b/src/types/empty-factories.ts
index 70b1c6b1..818f9a9f 100644
--- a/src/types/empty-factories.ts
+++ b/src/types/empty-factories.ts
@@ -1,15 +1,9 @@
-/**
- * @file Factory functions that return zero-value instances of core types, giving each caller a
- * fresh independent object.
- */
import type { DraftProject, TextAnalysis } from 'interlinearizer';
import type { FocusContext } from './token-layout';
/**
- * Returns a `TextAnalysis` with empty collections for every analysis and link array. Each call
- * produces a fresh object so callers can mutate it without affecting other instances.
- *
- * @returns A new, empty `TextAnalysis` object.
+ * A {@link TextAnalysis} with every analysis and link collection empty. Each call returns a fresh
+ * object, so callers may mutate the result without affecting other instances.
*/
export function emptyAnalysis(): TextAnalysis {
return {
@@ -23,12 +17,11 @@ export function emptyAnalysis(): TextAnalysis {
}
/**
- * Returns a fresh, empty {@link DraftProject} for a source project: empty analysis, no analysis
- * languages yet, and `dirty: false`. Used by the storage layer when no draft has been written and
- * as the seed for the "New" (reset) flow. Each call produces a fresh object with its own analysis.
+ * An empty {@link DraftProject} for a source project: empty analysis, no analysis languages yet, and
+ * not dirty. Used as the fallback when no draft has been written and as the seed for the "New"
+ * (reset) flow. Each call returns a fresh object with its own analysis.
*
* @param sourceProjectId - The Platform.Bible source project ID the draft belongs to.
- * @returns A new, empty `DraftProject` for the given source.
*/
export function emptyDraft(sourceProjectId: string): DraftProject {
return {
@@ -40,10 +33,8 @@ export function emptyDraft(sourceProjectId: string): DraftProject {
}
/**
- * Returns a `FocusContext` with all fields set to `undefined`, representing the state where nothing
- * is focused. Each call produces a fresh object so callers can use it without sharing references.
- *
- * @returns A new, empty `FocusContext` object.
+ * A {@link FocusContext} representing "nothing is focused". Each call returns a fresh object, so
+ * callers never share a reference.
*/
export function emptyFocusContext(): FocusContext {
return {
diff --git a/src/types/interlinear-project-summary.ts b/src/types/interlinear-project-summary.ts
index e1d1968e..e307f1bb 100644
--- a/src/types/interlinear-project-summary.ts
+++ b/src/types/interlinear-project-summary.ts
@@ -14,18 +14,14 @@ export type InterlinearProjectSummary = Pick<
>;
/**
- * Rebuilds a new object holding only the {@link InterlinearProjectSummary} fields. The parameter is
- * statically typed as `InterlinearProjectSummary`, but structural typing lets a full
- * `InterlinearProject` (with its potentially large `analysis`, plus `links` / `segmentation`)
- * satisfy that type, so at runtime the argument may still carry those extra properties. Caching
- * such a value verbatim would persist the whole envelope in WebView state; copying only the summary
- * fields keeps the cached active project lean.
+ * Copies out just the {@link InterlinearProjectSummary} fields, dropping any extra ones.
*
- * @param summary - A value typed as `InterlinearProjectSummary` (e.g. narrowed via
- * `isInterlinearProjectSummary`) that may carry extra runtime properties from a wider
- * `InterlinearProject`.
- * @returns A new object containing only the summary fields, with `targetProjectId` / `name` /
- * `description` present only when they are set on the input.
+ * Structural typing lets a full {@link InterlinearProject} — with its potentially large `analysis`,
+ * `links`, and `segmentation` — satisfy the parameter type, so the argument may carry far more at
+ * runtime than the type admits. Caching such a value verbatim would persist the whole envelope in
+ * WebView state; rebuilding keeps the cached project lean.
+ *
+ * @returns A new object whose optional fields are present only when set on the input.
*/
export function toProjectSummary(summary: InterlinearProjectSummary): InterlinearProjectSummary {
return {
diff --git a/src/types/interlinearizer.d.ts b/src/types/interlinearizer.d.ts
index ce1ff184..ee1714d3 100644
--- a/src/types/interlinearizer.d.ts
+++ b/src/types/interlinearizer.d.ts
@@ -62,16 +62,15 @@ declare module 'papi-shared-types' {
* after the user fills in the create-project modal. Returns the persisted `InterlinearProject`
* serialized as a JSON string.
*
- * @param sourceProjectId Platform.Bible project ID of the source text to interlinearize.
- * @param analysisLanguages BCP 47 tags for all languages used in glosses and annotations (e.g.
- * `['en']`). LCM: one per writing system present on `IWfiGloss.Form`. Paratext: one per
+ * @param sourceProjectId - Platform.Bible project ID of the source text to interlinearize.
+ * @param analysisLanguages - BCP 47 tags for all languages used in glosses and annotations
+ * (e.g. `['en']`). LCM: one per writing system present on `IWfiGloss.Form`. Paratext: one per
* merged `GlossLanguage` file. BT Extension: typically one language.
- * @param targetProjectId Optional Platform.Bible project ID of the target text. Required for BT
+ * @param targetProjectId - Platform.Bible project ID of the target text. Required for BT
* Extension projects so that `AlignmentLink.targetEndpoints` can be resolved at runtime.
* Omitted for analysis-only projects (LCM, PT9 single-sided).
- * @param name Optional user-facing name for the project.
- * @param description Optional user-facing description for the project.
- * @returns The persisted `InterlinearProject` as a JSON string.
+ * @param name - User-facing name for the project.
+ * @param description - User-facing description for the project.
* @throws If storage fails. The error is logged and an error notification is sent before
* rethrowing so callers do not need to send a second notification.
*/
@@ -84,11 +83,11 @@ declare module 'papi-shared-types' {
) => Promise;
/**
- * Returns all interlinearizer projects for the given source project, serialized as a JSON
- * string. Returns `"[]"` when none exist. The WebView uses this to populate the project picker
- * and to decide whether to show "create new" vs "select existing" on first open.
+ * Returns all interlinearizer projects for the given source project. The WebView uses this to
+ * populate the project picker and to decide whether to show "create new" vs "select existing"
+ * on first open.
*
- * @param sourceProjectId Platform.Bible project ID of the source text to query.
+ * @param sourceProjectId - Platform.Bible project ID of the source text to query.
* @returns A JSON string containing an `InterlinearProject[]`; `"[]"` when none exist.
* @throws {SyntaxError} If the project-IDs index or any stored project record contains invalid
* JSON.
@@ -101,7 +100,7 @@ declare module 'papi-shared-types' {
/**
* Deletes an interlinearizer project by UUID. No-ops silently if the project does not exist.
*
- * @param interlinearProjectId UUID of the interlinearizer project to delete.
+ * @param interlinearProjectId - UUID of the interlinearizer project to delete.
* @throws If the underlying storage write fails (the failure is also logged and surfaced as an
* error notification before being re-thrown).
*/
@@ -149,11 +148,10 @@ declare module 'papi-shared-types' {
'interlinearizer.wipe': () => Promise;
/**
- * Returns the interlinearizer project with the given UUID as a JSON string, including its full
- * `TextAnalysis`. The WebView calls this when the active project changes to load the stored
- * analysis.
+ * Returns the interlinearizer project with the given UUID, including its full `TextAnalysis`.
+ * The WebView calls this when the active project changes to load the stored analysis.
*
- * @param interlinearProjectId UUID of the interlinearizer project to fetch.
+ * @param interlinearProjectId - UUID of the interlinearizer project to fetch.
* @returns JSON-stringified `InterlinearProject`, or `undefined` if not found.
* @throws If storage fails (logged before rethrowing).
*/
@@ -164,10 +162,10 @@ declare module 'papi-shared-types' {
* interlinearizer project. Called from the WebView on Save so analysis and boundary changes
* survive tab restores and project switches.
*
- * @param interlinearProjectId UUID of the interlinearizer project to update.
- * @param analysisJson JSON-stringified `TextAnalysis` to persist.
- * @param segmentationJson Optional JSON-stringified `SegmentationDelta` to persist, or the
- * string `"null"` to clear any stored custom boundaries. Omit entirely to leave the project's
+ * @param interlinearProjectId - UUID of the interlinearizer project to update.
+ * @param analysisJson - JSON-stringified `TextAnalysis` to persist.
+ * @param segmentationJson - JSON-stringified `SegmentationDelta` to persist, or the string
+ * `"null"` to clear any stored custom boundaries. Omit entirely to leave the project's
* existing boundaries unchanged.
* @returns The saved `InterlinearProject` (with its refreshed `updatedAt`) as a JSON string, or
* `undefined` if no project with the given ID exists. The WebView uses the refreshed
@@ -187,7 +185,7 @@ declare module 'papi-shared-types' {
* mount to seed the editor. The draft is decoupled from saved projects and never appears in the
* project picker.
*
- * @param sourceProjectId Platform.Bible source project ID whose draft to fetch.
+ * @param sourceProjectId - Platform.Bible source project ID whose draft to fetch.
* @returns JSON-stringified `DraftProject`.
* @throws {SyntaxError} If the stored draft contains invalid JSON.
* @throws If `papi.storage.readUserData` rejects for a reason other than the draft not
@@ -200,9 +198,8 @@ declare module 'papi-shared-types' {
* every edit (auto-save) and whenever the draft is reset (New), opened from a project, or
* wiped.
*
- * @param sourceProjectId Platform.Bible source project ID whose draft to write.
- * @param draftJson JSON-stringified `DraftProject` to persist.
- * @returns Promise that resolves to void once the draft has been written to storage.
+ * @param sourceProjectId - Platform.Bible source project ID whose draft to write.
+ * @param draftJson - JSON-stringified `DraftProject` to persist.
* @throws If JSON parsing, validation, or storage fails. The error is logged and an error
* notification is sent before rethrowing so callers do not need to send a second
* notification.
@@ -210,16 +207,15 @@ declare module 'papi-shared-types' {
'interlinearizer.saveDraft': (sourceProjectId: string, draftJson: string) => Promise;
/**
- * Updates the metadata of an existing interlinearizer project. Returns the updated project as a
- * JSON string, or `undefined` if no project with the given ID exists.
+ * Updates the metadata of an existing interlinearizer project.
*
- * @param interlinearProjectId UUID of the interlinearizer project to update.
- * @param name New user-facing name; omit or pass `undefined` to clear.
- * @param description New user-facing description; omit or pass `undefined` to clear.
- * @param analysisLanguages New BCP 47 analysis language tags. Must be a non-empty array; pass
+ * @param interlinearProjectId - UUID of the interlinearizer project to update.
+ * @param name - New user-facing name; omit or pass `undefined` to clear.
+ * @param description - New user-facing description; omit or pass `undefined` to clear.
+ * @param analysisLanguages - New BCP 47 analysis language tags. Must be a non-empty array; pass
* the current value to leave it unchanged (the field is required and cannot be cleared).
- * @param targetProjectId New target-project ID; omit or pass `undefined` to clear (removes the
- * target-side text binding).
+ * @param targetProjectId - New target-project ID; omit or pass `undefined` to clear (removes
+ * the target-side text binding).
* @returns The updated project as a JSON string, or `undefined` if no project with that ID
* exists.
* @throws If storage fails. The error is logged and an error notification is sent before
diff --git a/src/types/phrase-mode.ts b/src/types/phrase-mode.ts
index eedfe8d7..baa149a8 100644
--- a/src/types/phrase-mode.ts
+++ b/src/types/phrase-mode.ts
@@ -1,17 +1,20 @@
import type { PhraseAnalysisLink } from 'interlinearizer';
-/**
- * Discriminated union describing the current phrase-interaction mode in the Interlinearizer.
- *
- * - `view` — Normal reading/glossing mode; no phrase operation is in progress.
- * - `edit` — User is adding/removing tokens from an existing phrase identified by `phraseId`.
- * `originalTokens` is the token list at the moment edit mode was entered, used to restore state
- * on cancel. When `revert` is `true` the cancel button has been pressed and any component
- * observing this should restore the phrase to `originalTokens` and return to `view`.
- * - `confirm-unlink` — Awaiting user confirmation before deleting the phrase identified by
- * `phraseId`.
- */
+/** The phrase-interaction mode the interlinear view is currently in. */
export type PhraseMode =
+ /** Normal reading and glossing; no phrase operation is in progress. */
| { kind: 'view' }
- | { kind: 'edit'; phraseId: string; originalTokens: PhraseAnalysisLink['tokens']; revert?: true }
+ /** The user is adding tokens to or removing them from an existing phrase. */
+ | {
+ kind: 'edit';
+ phraseId: string;
+ /** The phrase's token list as it stood when edit mode was entered, so a cancel can restore it. */
+ originalTokens: PhraseAnalysisLink['tokens'];
+ /**
+ * Set once cancel has been pressed: observers should restore `originalTokens` and return to
+ * `view`.
+ */
+ revert?: true;
+ }
+ /** Awaiting the user's confirmation before deleting the phrase. */
| { kind: 'confirm-unlink'; phraseId: string };
diff --git a/src/types/status-colors.ts b/src/types/status-colors.ts
index efc34867..d878fa7a 100644
--- a/src/types/status-colors.ts
+++ b/src/types/status-colors.ts
@@ -1,26 +1,16 @@
-/** @file Maps an analysis assignment status to the Tailwind text color the renderer shows it in. */
-
import type { AssignmentStatus } from 'interlinearizer';
/**
- * Tailwind `tw:` text-color classes per assignment status, so approved work always reads as plain
- * foreground while machine output reads as a subordinate color. Kept as a complete map (every
- * `AssignmentStatus` member) even though v1 only ever renders `approved` / `suggested` /
- * `candidate` — the `rejected` / `stale` colors are defined for completeness but no reducer
- * produces those statuses in v1. Indexed directly at the call site, so there is no accessor to
- * test; the map is data, which is why this lives in `types/`.
+ * Tailwind text-color class per assignment status, so human-confirmed work reads as plain
+ * foreground and machine output reads as subordinate to it.
*
- * The colors track Paratext 9: approved renders in the default foreground and the engine's pick in
- * blue; PT9 leaves the unselected alternatives in default, but we set them grey so a homograph
- * candidate reads as subordinate to the suggested pick. Approved uses the core `foreground` theme
- * token; the colored statuses use the local `gloss-suggested` / `gloss-candidate` /
- * `gloss-rejected` / `gloss-stale` utilities defined in `tailwind.css` (each light/dark aware), so
- * all the status colors live in one place rather than as raw palette strings scattered here. See
- * that file for which reuse a theme token (grey, red) and which use a raw palette shade (blue,
- * orange).
+ * The palette tracks Paratext 9, with one deliberate divergence: PT9 leaves unselected homograph
+ * alternatives in the default foreground, but `candidate` is grey here so it reads as subordinate
+ * to the engine's `suggested` pick. The map is complete even though v1 never produces `rejected` or
+ * `stale`.
*
* - `approved` — plain foreground (the canonical, human-confirmed render).
- * - `suggested` — blue (the engine's single best pick; subordinate to approved).
+ * - `suggested` — blue (the engine's single best pick).
* - `candidate` — grey (an unselected homograph alternative a reviewer can promote).
* - `rejected` — orange (a dismissed analysis).
* - `stale` — red (drifted text needing re-review).
diff --git a/src/types/token-layout.ts b/src/types/token-layout.ts
index 3daf40a0..90d367f5 100644
--- a/src/types/token-layout.ts
+++ b/src/types/token-layout.ts
@@ -1,14 +1,8 @@
-/**
- * @file Types describing the intermediate data structures produced when laying out tokens for
- * rendering. These are pure structural types with no runtime behavior; the functions that build
- * and consume them live in `utils/token-layout.ts`.
- */
import type { PhraseAnalysisLink, Token } from 'interlinearizer';
/**
- * Resolved focus state shared by SegmentView and ContinuousView so both views derive their
- * highlight / link-icon rules from the same source. Built once per render from the parent's
- * `focusedTokenRef`.
+ * Resolved focus state shared by the segment and continuous views so both derive their highlight
+ * and link-icon rules from one source. Built once per render.
*/
export type FocusContext = {
/** The focused word token itself, or `undefined` when nothing is focused. */
@@ -24,15 +18,14 @@ export type FocusContext = {
};
/**
- * The focus-derived inputs `TokenLinkIcon` needs for a single between-group slot: the slot-specific
- * direction/segment flags plus the two focus-context fields the icon reads directly
- * (`focusedPhraseLink` / `focusedFreeToken`), bundled so the icon takes one focus object.
+ * The focus-derived inputs a single between-group link slot needs: its own direction and segment
+ * flags plus the two {@link FocusContext} fields the link icon reads, bundled so the icon takes one
+ * focus object.
*/
export type SlotFocusInfo = {
/**
* `true` when the focused group is start-ward of this slot, `false` when end-ward, `undefined`
- * when nothing is focused. Caller must compute this from its own ordering (`focusedGroupSeen`
- * cursor for SegmentView; flat index comparison for ContinuousView).
+ * when nothing is focused. Callers compute this from their own ordering of groups.
*/
focusedSideIsPrev: boolean | undefined;
/**
@@ -48,14 +41,13 @@ export type SlotFocusInfo = {
/** A grouped render unit: one or more adjacent tokens that share the same phrase (or no phrase). */
export type TokenGroup = {
- /** The tokens to render together in one `PhraseBox`. */
+ /** The tokens to render together as a single phrase box. */
tokens: (Token & { type: 'word' })[];
/** The phrase link shared by all tokens in this group, or `undefined` for ungrouped solo tokens. */
phraseLink: PhraseAnalysisLink | undefined;
/**
- * The index of the first token in this group within the flat token array it was built from. Used
- * to slice the flat token list to the group's range (e.g. for the punctuation that renders
- * between groups); not passed to `PhraseBox`.
+ * The index of the first token in this group within the flat token array it was built from, so
+ * callers can slice that array back to the group's range.
*/
firstIndex: number;
/**
diff --git a/src/types/type-guards.ts b/src/types/type-guards.ts
index fc8c9366..b502d33b 100644
--- a/src/types/type-guards.ts
+++ b/src/types/type-guards.ts
@@ -1,4 +1,3 @@
-/** @file Type guards for narrowing interlinearizer types and validating parsed JSON payloads. */
import type {
AssignmentStatus,
DraftProject,
@@ -8,23 +7,12 @@ import type {
} from 'interlinearizer';
import type { InterlinearProjectSummary } from './interlinear-project-summary';
-/**
- * Narrows a `Token` to a word token.
- *
- * @param token - The token to test.
- * @returns `true` when `token.type === 'word'`.
- */
+/** Narrows a token to a word token. */
export function isWordToken(token: Token): token is Token & { type: 'word' } {
return token.type === 'word';
}
-/**
- * Type guard for {@link InterlinearProjectSummary} parsed from unknown JSON.
- *
- * @param p - The value to test, typically a parsed JSON object of unknown shape.
- * @returns `true` if `p` satisfies the {@link InterlinearProjectSummary} shape, narrowing its type
- * accordingly.
- */
+/** Validates an {@link InterlinearProjectSummary} parsed from unknown JSON. */
export function isInterlinearProjectSummary(p: unknown): p is InterlinearProjectSummary {
return (
!!p &&
@@ -46,7 +34,7 @@ export function isInterlinearProjectSummary(p: unknown): p is InterlinearProject
);
}
-/** All valid {@link AssignmentStatus} string literals, used for O(1) membership checks. */
+/** All valid {@link AssignmentStatus} string literals. */
const ASSIGNMENT_STATUSES: readonly string[] = [
'approved',
'suggested',
@@ -55,22 +43,14 @@ const ASSIGNMENT_STATUSES: readonly string[] = [
'stale',
];
-/**
- * Narrows `v` to {@link AssignmentStatus}.
- *
- * @param v - The value to test.
- * @returns `true` if `v` is one of the valid {@link AssignmentStatus} string literals.
- */
function isAssignmentStatus(v: unknown): v is AssignmentStatus {
return typeof v === 'string' && ASSIGNMENT_STATUSES.includes(v);
}
-/**
- * Checks that `v` has the required fields of a `TokenSnapshot` (`tokenRef` and `surfaceText`).
- *
- * @param v - The value to test.
- * @returns `true` if `v` is an object with string `tokenRef` and `surfaceText` properties.
- */
+// The helpers below check shape only and return plain booleans rather than narrowing, because the
+// types they validate are structural fragments that callers never hold on their own.
+
+/** Checks the required fields of a token snapshot. */
function isTokenSnapshot(v: unknown): boolean {
return (
!!v &&
@@ -82,14 +62,7 @@ function isTokenSnapshot(v: unknown): boolean {
);
}
-/**
- * Checks that `v` satisfies the base `Analysis` shape (`id` and `surfaceText`). Used directly as
- * the `.every()` predicate for `segmentAnalyses` and `phraseAnalyses`, and as the base of
- * {@link isTokenAnalysisRecord} for `tokenAnalyses`.
- *
- * @param v - The value to test.
- * @returns `true` if `v` is an object with string `id` and `surfaceText` properties.
- */
+/** Checks the fields common to every analysis record. */
function isAnalysisRecord(v: unknown): boolean {
return (
!!v &&
@@ -101,13 +74,7 @@ function isAnalysisRecord(v: unknown): boolean {
);
}
-/**
- * Checks that `v` has the required fields of a `MorphemeAnalysis` (`id`, `form`, `writingSystem`).
- * Used to validate the elements of a `TokenAnalysis.morphemes` array at the persistence boundary.
- *
- * @param v - The value to test.
- * @returns `true` if `v` is an object with string `id`, `form`, and `writingSystem` properties.
- */
+/** Checks the required fields of a single morpheme analysis. */
function isMorphemeAnalysis(v: unknown): boolean {
return (
!!v &&
@@ -122,14 +89,9 @@ function isMorphemeAnalysis(v: unknown): boolean {
}
/**
- * Checks that `v` satisfies the `TokenAnalysis` shape: the base `Analysis` fields plus, when
- * present, a `morphemes` array whose every element passes {@link isMorphemeAnalysis}. Morphemes are
- * first-class data read by the UI (`m.id` keys, `m.form`, `m.writingSystem`), so a malformed array
- * must be rejected before persisting rather than surfacing as a render-time fault.
- *
- * @param v - The value to test.
- * @returns `true` if `v` is a valid analysis record with a well-formed (or absent) `morphemes`
- * array.
+ * Checks a token analysis, including its morphemes when present. Morphemes are first-class data the
+ * UI reads field by field, so a malformed array has to be rejected at the persistence boundary
+ * rather than surfacing later as a render-time fault.
*/
function isTokenAnalysisRecord(v: unknown): boolean {
return (
@@ -140,14 +102,7 @@ function isTokenAnalysisRecord(v: unknown): boolean {
);
}
-/**
- * Checks that `v` satisfies the base `AnalysisLink` shape (`analysisId` and a valid `status`). Used
- * as a building block by the three link-specific guards.
- *
- * @param v - The value to test.
- * @returns `true` if `v` is an object with a string `analysisId` and a valid
- * {@link AssignmentStatus} `status`.
- */
+/** Checks the fields common to every analysis link. */
function isAnalysisLink(v: unknown): boolean {
return (
!!v &&
@@ -159,13 +114,7 @@ function isAnalysisLink(v: unknown): boolean {
);
}
-/**
- * Checks that `v` satisfies the `SegmentAnalysisLink` shape: valid `AnalysisLink` fields plus a
- * string `segmentId`.
- *
- * @param v - The value to test.
- * @returns `true` if `v` passes {@link isAnalysisLink} and has a string `segmentId` property.
- */
+/** Checks a link that attaches an analysis to a segment. */
function isSegmentAnalysisLink(v: unknown): boolean {
return (
isAnalysisLink(v) &&
@@ -176,28 +125,14 @@ function isSegmentAnalysisLink(v: unknown): boolean {
);
}
-/**
- * Checks that `v` satisfies the `TokenAnalysisLink` shape: valid `AnalysisLink` fields plus a
- * `token` property that passes {@link isTokenSnapshot}.
- *
- * @param v - The value to test.
- * @returns `true` if `v` passes {@link isAnalysisLink} and has a valid `TokenSnapshot` `token`
- * property.
- */
+/** Checks a link that attaches an analysis to a single token. */
function isTokenAnalysisLink(v: unknown): boolean {
return (
isAnalysisLink(v) && !!v && typeof v === 'object' && 'token' in v && isTokenSnapshot(v.token)
);
}
-/**
- * Checks that `v` satisfies the `PhraseAnalysisLink` shape: valid `AnalysisLink` fields plus a
- * non-empty `tokens` array whose every element passes {@link isTokenSnapshot}.
- *
- * @param v - The value to test.
- * @returns `true` if `v` passes {@link isAnalysisLink} and has a `tokens` array of valid
- * `TokenSnapshot` objects.
- */
+/** Checks a link that attaches an analysis to a non-empty run of tokens. */
function isPhraseAnalysisLink(v: unknown): boolean {
return (
isAnalysisLink(v) &&
@@ -211,12 +146,8 @@ function isPhraseAnalysisLink(v: unknown): boolean {
}
/**
- * Type guard for {@link TextAnalysis} parsed from unknown JSON. Validates array presence and minimal
- * element shapes for all six arrays so malformed payloads are rejected before persisting.
- *
- * @param value - The value to test, typically a parsed JSON object of unknown shape.
- * @returns `true` if `value` satisfies the {@link TextAnalysis} shape, narrowing its type
- * accordingly.
+ * Validates a {@link TextAnalysis} parsed from unknown JSON, down to the element shapes of every
+ * collection, so a malformed payload is rejected before it is persisted.
*/
export function isTextAnalysis(value: unknown): value is TextAnalysis {
return (
@@ -244,12 +175,8 @@ export function isTextAnalysis(value: unknown): value is TextAnalysis {
}
/**
- * Type guard for {@link SegmentationDelta} parsed from unknown JSON. Both arrays must be present and
- * contain only strings, so a malformed delta is rejected before it can corrupt re-segmentation.
- *
- * @param value - The value to test, typically a parsed JSON object of unknown shape.
- * @returns `true` if `value` satisfies the {@link SegmentationDelta} shape, narrowing its type
- * accordingly.
+ * Validates a {@link SegmentationDelta} parsed from unknown JSON, so a malformed delta cannot
+ * corrupt re-segmentation.
*/
export function isSegmentationDelta(value: unknown): value is SegmentationDelta {
return (
@@ -265,13 +192,8 @@ export function isSegmentationDelta(value: unknown): value is SegmentationDelta
}
/**
- * Type guard for {@link DraftProject} parsed from unknown JSON. Validates the envelope fields and
- * delegates the `analysis` to {@link isTextAnalysis}, so malformed drafts are rejected before
- * persisting.
- *
- * @param value - The value to test, typically a parsed JSON object of unknown shape.
- * @returns `true` if `value` satisfies the {@link DraftProject} shape, narrowing its type
- * accordingly.
+ * Validates a {@link DraftProject} parsed from unknown JSON, so a malformed draft is rejected before
+ * it is persisted.
*/
export function isDraftProject(value: unknown): value is DraftProject {
return (
diff --git a/src/types/view-options.ts b/src/types/view-options.ts
index 25f89108..3e262f64 100644
--- a/src/types/view-options.ts
+++ b/src/types/view-options.ts
@@ -1,8 +1,7 @@
/**
- * Bundled display toggles threaded from {@link InterlinearizerLoader} down through the segment and
- * continuous views to the phrase strips. Grouping them in one object lets intermediate components
- * forward `viewOptions` unchanged, so adding a new toggle only touches the loader that builds it
- * and the leaf that reads it — not every component in between.
+ * Bundled display toggles threaded from the loader down through the views to the phrase strips.
+ * Grouping them in one object lets intermediate components forward the bundle unchanged, so adding
+ * a toggle touches only the code that builds it and the leaf that reads it.
*/
export type ViewOptions = Readonly<{
/** When true, link buttons between phrases are hidden in segments other than the active verse. */
diff --git a/src/utils/analysis-book.ts b/src/utils/analysis-book.ts
index 1fcbc8d1..2dcdb0f4 100644
--- a/src/utils/analysis-book.ts
+++ b/src/utils/analysis-book.ts
@@ -1,12 +1,10 @@
-/** @file Pure helpers for filtering a `TextAnalysis` by the book a record belongs to. */
import type { SegmentationDelta, TextAnalysis } from 'interlinearizer';
/**
* Returns the 3-letter book code embedded at the start of a segment id or token ref. Both are
- * formatted `" :[:]"` (e.g. `"GEN 1:1"`, `"1JN 2:3:5"`), so the
- * book code is the substring before the first space.
+ * formatted `" :[:]"` (e.g. `"GEN 1:1"`, `"1JN 2:3:5"`). A string
+ * with no space is returned whole.
*
- * @param ref - A `Segment.id` or `Token.ref` / `TokenSnapshot.tokenRef` value.
* @returns The leading book code, or the whole string when it contains no space.
*/
export function bookOfRef(ref: string): string {
@@ -15,15 +13,12 @@ export function bookOfRef(ref: string): string {
}
/**
- * Returns a copy of `analysis` with every record belonging to `bookCode` removed. A token- or
- * segment-level record is dropped when its referenced token/segment is in the book; a phrase is
- * dropped when **any** of its member tokens is in the book (so a rare cross-book phrase is removed
- * when wiping either side). Analysis payloads left unreferenced by a surviving link are also
- * dropped, so no orphans remain.
+ * Returns a copy of the analysis, unmutated, with every record belonging to the book removed.
*
- * @param analysis - The analysis to filter. Not mutated.
- * @param bookCode - The 3-letter book code (e.g. `"GEN"`) whose records to remove.
- * @returns A new `TextAnalysis` with the book's records (and any orphaned payloads) removed.
+ * A token- or segment-level record is dropped when its referenced token or segment is in the book;
+ * a phrase is dropped when **any** of its member tokens is, so a rare cross-book phrase goes when
+ * either side is wiped. Analysis payloads left unreferenced by a surviving link are dropped too, so
+ * no orphans remain.
*/
export function removeBookFromAnalysis(analysis: TextAnalysis, bookCode: string): TextAnalysis {
const tokenAnalysisLinks = analysis.tokenAnalysisLinks.filter(
@@ -51,14 +46,12 @@ export function removeBookFromAnalysis(analysis: TextAnalysis, bookCode: string)
}
/**
- * Returns a copy of `delta` with every boundary anchor belonging to `bookCode` removed, so wiping a
- * book also drops its custom segment boundaries (the anchors are book-prefixed token refs, keyed
- * the same way as analysis records). Returns `undefined` when nothing for any other book remains,
- * so a wiped-then-emptied delta collapses to the default segmentation rather than persisting empty
- * arrays.
+ * Returns a copy of the delta with every boundary anchor belonging to the book removed, so wiping a
+ * book also drops its custom segment boundaries.
+ *
+ * Yields `undefined` when no other book's anchors remain, so an emptied delta collapses back to the
+ * default segmentation rather than persisting empty arrays.
*
- * @param delta - The current boundary delta, or `undefined` for the default segmentation.
- * @param bookCode - The 3-letter book code (e.g. `"GEN"`) whose anchors to remove.
* @returns A new `SegmentationDelta` without the book's anchors, or `undefined` when the result is
* the default segmentation (or the input already was).
*/
diff --git a/src/utils/analysis-identity.ts b/src/utils/analysis-identity.ts
index 3b241b0b..0375e316 100644
--- a/src/utils/analysis-identity.ts
+++ b/src/utils/analysis-identity.ts
@@ -1,42 +1,33 @@
-/** @file Content-identity helpers for deduplicating `TokenAnalysis` payloads on write. */
-
import type { MorphemeAnalysis, TokenAnalysis } from 'interlinearizer';
/**
- * Soft cap on {@link normalizedFormCache}. One project's vocabulary is far smaller than this, so the
- * cap is effectively never hit within a single project; it exists only to bound the module-global
- * cache across a long-lived WebView session that opens many different projects (each a different
- * source vocabulary, plus baseline-drift variants), so the cache cannot grow without limit for the
- * process lifetime. Generous enough that the working set of the active project always fits.
+ * Soft cap on the normalized-form cache. One project's vocabulary is far smaller than this, so the
+ * cap is effectively never hit within a single project. It exists only to bound the module-global
+ * cache across a long-lived WebView session that opens many projects, each with its own vocabulary
+ * plus baseline-drift variants, so memory cannot grow without limit for the process lifetime.
*/
const NORMALIZED_FORM_CACHE_MAX = 50_000;
/**
- * Memoizes {@link normalizeSurfaceForm} by raw input string — called once per visible un-approved
- * token on every store dispatch, so the same forms are normalized repeatedly. The cache is module-
- * global rather than project-scoped, so a bound is needed: when it reaches
- * {@link NORMALIZED_FORM_CACHE_MAX} the oldest entry is evicted (insertion-order FIFO, free from
- * `Map`), keeping memory flat across a session that opens many projects rather than retaining every
- * surface form ever seen.
+ * Memoizes {@link normalizeSurfaceForm} by raw input string. Normalization runs once per visible
+ * un-approved token on every store dispatch, so the same handful of forms would otherwise be
+ * re-derived constantly. Bounded by {@link NORMALIZED_FORM_CACHE_MAX} because the cache is
+ * module-global rather than project-scoped.
*/
const normalizedFormCache = new Map();
/**
- * Normalizes a token surface form for matching and dedupe so trivial Unicode and case differences
- * never split one analysis into two. Applies Unicode NFC (so a composed `é` matches a decomposed
- * `e` + combining acute), then a locale-independent lowercase (so a sentence-initial `"The"`
- * matches a mid-sentence `"the"`), then NFC a final time so the returned key is guaranteed
- * canonical even for the rare code point whose lowercase mapping emits a decomposed sequence — the
- * key never depends on which Unicode form an equivalent input arrived in. Locale-independent
- * lowercasing — `String.prototype.toLowerCase`, not `toLocaleLowerCase` — is deliberate so the
- * result does not depend on the host's locale; the trade-off is that locale-specific folds are not
- * applied (e.g. a Turkish dotted `İ` is not folded to an ASCII `i`), an accepted miss that can only
- * cost a suggestion, never produce a wrong one. The result is memoized per raw input
- * ({@link normalizedFormCache}, bounded by {@link NORMALIZED_FORM_CACHE_MAX}) so the per-token derive
- * path does not re-run three Unicode passes for a constant surface form on every store change.
+ * Normalizes a token surface form for matching and dedupe, so trivial Unicode and case differences
+ * never split one analysis into two.
+ *
+ * Normalization is applied on both sides of the case fold: the leading pass makes a composed `é`
+ * match a decomposed `e` + combining acute, and the trailing pass re-canonicalizes the rare code
+ * point whose lowercase mapping emits a decomposed sequence. The result therefore never depends on
+ * which Unicode form an equivalent input arrived in.
*
- * @param text - The raw surface text to normalize.
- * @returns The case-folded surface form in NFC.
+ * The fold is deliberately locale-independent so the result does not vary with the host's locale.
+ * The trade-off is that locale-specific folds are skipped — a Turkish dotted `İ` is not folded to
+ * an ASCII `i` — an accepted miss, since it can only cost a suggestion, never produce a wrong one.
*/
export function normalizeSurfaceForm(text: string): string {
const cached = normalizedFormCache.get(text);
@@ -57,19 +48,13 @@ export function normalizeSurfaceForm(text: string): string {
}
/**
- * Structural deep-equality for the JSON-shaped values an analysis carries (strings, plain objects,
- * and arrays). Used to compare glosses, features, morpheme refs, and projected morpheme lists
- * without depending on key order. Never receives `null` — the analysis types use absence
- * (`undefined`) rather than `null` — so no `null` guard is needed.
- *
- * Kept as a small, dedicated helper rather than reusing `platform-bible-utils`' `deepEqual`: that
- * package is fully mocked in this project's Jest setup and the mock does not surface `deepEqual`,
- * so depending on it here would mean the dedupe path silently ran against `undefined` under test. A
- * local, directly-tested helper keeps content-identity matching honest in the suite.
+ * Structural deep-equality for the JSON-shaped values an analysis carries, used to compare glosses,
+ * features, and morpheme refs without depending on key order. Never receives `null`, since the
+ * analysis types use absence rather than `null`.
*
- * @param a - First value.
- * @param b - Second value.
- * @returns `true` when the two values are structurally equal.
+ * Kept as a dedicated local helper rather than reusing `platform-bible-utils`' equivalent: that
+ * package is fully mocked in this project's Jest setup and the mock does not surface it, so
+ * depending on it here would mean the dedupe path silently ran against `undefined` under test.
*/
function deepEqual(a: unknown, b: unknown): boolean {
if (a === b) return true;
@@ -84,13 +69,9 @@ function deepEqual(a: unknown, b: unknown): boolean {
}
/**
- * Projects a morpheme down to the fields that define analysis identity — form, gloss, and its
- * lexicon refs (entry, sense, allomorph, grammar) — dropping `id` (a per-instance UUID) and
- * `writingSystem` (a presentation detail that self-corrects on save), so two analyses that differ
- * only in those are still considered identical.
- *
- * @param morpheme - The morpheme to project.
- * @returns A plain object holding only the identity-defining fields.
+ * Projects a morpheme down to the fields that define analysis identity, dropping `id` (a
+ * per-instance UUID) and `writingSystem` (a presentation detail that self-corrects on save), so two
+ * analyses differing only in those still count as identical.
*/
function morphemeIdentity(morpheme: MorphemeAnalysis) {
const { form, gloss, entryRef, senseRef, allomorphRef, grammarRef } = morpheme;
@@ -98,24 +79,19 @@ function morphemeIdentity(morpheme: MorphemeAnalysis) {
}
/**
- * Reports whether two `TokenAnalysis` payloads carry the same meaning and so should share one
- * stored payload instead of being duplicated. Identity is content-based: equal normalized surface
- * forms (see {@link normalizeSurfaceForm}) plus deep-equal `gloss` (all language keys), `pos`,
- * `features`, `glossSenseRef` (the lexicon sense the gloss resolves through), and `morphemes`
- * (compared on form + gloss + refs only — see {@link morphemeIdentity}). Only `morphemes` treats a
- * missing list and an empty one as equal (the absent list is normalized to `[]` before comparison);
- * every other field is compared by exact structural equality, so a missing `gloss`/`features`/
- * `glossSenseRef` does not compare equal to an empty `{}`. That direction is deliberately
- * conservative: the only risk this dedupe carries is merging two analyses that actually differ (see
- * `glossSenseRef` below), never failing to merge two that don't. `glossSenseRef` is part of
- * identity because `isEmptyTokenAnalysis` counts it as content — were it excluded here, two
- * analyses differing only in their lexicon sense would be merged on write and one sense reference
- * silently dropped. Provenance fields (`confidence`, `producer`, `sourceUser`) and the record `id`
- * are intentionally excluded — they describe who produced an analysis, not what it means.
+ * Reports whether two token analyses carry the same meaning and so should share one stored payload
+ * rather than being duplicated.
+ *
+ * Identity is content-based: normalized surface form plus the gloss, part of speech, features,
+ * lexicon sense, and morphemes. Provenance (`confidence`, `producer`, `sourceUser`) and the record
+ * `id` are excluded — they describe who produced an analysis, not what it means. The lexicon sense
+ * _is_ part of identity, because excluding it would merge two analyses differing only in their
+ * sense and silently drop one reference.
*
- * @param a - First analysis.
- * @param b - Second analysis.
- * @returns `true` when the two analyses are content-identical.
+ * Only morphemes treat a missing list and an empty one as equal; every other field is compared by
+ * exact structural equality, so a missing gloss does not equal an empty one. That asymmetry is
+ * deliberately conservative: the only risk this dedupe carries is merging two analyses that
+ * genuinely differ, never failing to merge two that don't.
*/
export function analysesAreIdentical(a: TokenAnalysis, b: TokenAnalysis): boolean {
return (
diff --git a/src/utils/language-tags.ts b/src/utils/language-tags.ts
index 20bcb7c0..625d4e0b 100644
--- a/src/utils/language-tags.ts
+++ b/src/utils/language-tags.ts
@@ -1,12 +1,10 @@
-/** @file Shared parsing for the comma-separated BCP 47 analysis-language inputs in the modals. */
-
/**
- * Parses a comma-separated analysis-language field into BCP 47 tags: splits on commas, trims each
- * entry, and drops empty entries. The single source of this parse, shared by the create and
- * metadata modals so they cannot drift. Does not apply any fallback when the result is empty — each
- * modal decides how to treat an empty list (e.g. defaulting to `['und']` or disabling Save).
+ * Parses a comma-separated analysis-language field into BCP 47 tags. The single source of this
+ * parse, shared by the create and metadata modals so the two cannot drift.
+ *
+ * Applies no fallback when the result is empty — each modal decides how to treat an empty list (for
+ * instance defaulting to `['und']`, or disabling Save).
*
- * @param input - The raw comma-separated language field value.
* @returns The trimmed, non-empty tags in input order; an empty array when the field is blank.
*/
export function parseLanguageTags(input: string): string[] {
diff --git a/src/utils/localized-strings.ts b/src/utils/localized-strings.ts
index b5c770fb..705bb9f4 100644
--- a/src/utils/localized-strings.ts
+++ b/src/utils/localized-strings.ts
@@ -1,16 +1,12 @@
-/** @file Helpers for working with values returned by PAPI's `useLocalizedStrings` hook. */
-
/**
* Returns a localized value, or an empty string while it is still an unresolved key.
*
- * `useLocalizedStrings` resolves asynchronously: until the lookup completes it returns the raw
- * localize key (e.g. `%interlinearizer_glossInput_placeholder%`) as the value. Rendering that
- * directly flashes the bare `%…%` key in user-visible text — most noticeable in input placeholders,
- * which paint immediately on mount/toggle. Substituting an empty string for an unresolved key
- * replaces that flash with a momentarily-empty field, which then fills in once localization
- * resolves. A resolved localized string is returned unchanged.
+ * PAPI's localization hook resolves asynchronously, yielding the raw localize key (e.g.
+ * `%interlinearizer_glossInput_placeholder%`) until the lookup completes. Rendering that directly
+ * flashes the bare `%…%` key in user-visible text — most noticeable in input placeholders, which
+ * paint immediately on mount. Substituting an empty string turns that flash into a momentarily
+ * empty field instead.
*
- * @param value - A value from a `useLocalizedStrings` result record.
* @returns The value, or `''` when it is still an unresolved `%…%` key.
*/
export function resolvedOrEmpty(value: string): string {
diff --git a/src/utils/multi-string.ts b/src/utils/multi-string.ts
index 4adda8e9..95a47263 100644
--- a/src/utils/multi-string.ts
+++ b/src/utils/multi-string.ts
@@ -1,14 +1,10 @@
-/** @file Helpers for working with `MultiString` (BCP 47 tag → string) values. */
-
import type { MultiString } from 'interlinearizer';
/**
- * Reports whether a `MultiString` carries no usable text, so callers deciding whether an analysis
- * record is worth keeping can treat "absent", "no entries", and "only whitespace entries" the same.
- * A `MultiString` counts as empty when it is `undefined`, has no entries, or every entry is blank
- * once trimmed.
+ * Reports whether a {@link MultiString} carries no usable text, so callers deciding whether an
+ * analysis record is worth keeping can treat "absent", "no entries", and "only whitespace entries"
+ * alike.
*
- * @param value - The `MultiString` to inspect, or `undefined`.
* @returns `true` when the value holds no non-whitespace text.
*/
export function isEmptyMultiString(value: MultiString | undefined): boolean {
diff --git a/src/utils/phrase-arc.ts b/src/utils/phrase-arc.ts
index 1f259fc9..9cb27a7d 100644
--- a/src/utils/phrase-arc.ts
+++ b/src/utils/phrase-arc.ts
@@ -56,13 +56,10 @@ type SplitPhraseDispatch = {
};
/**
- * Sorts token snapshots by flat document index so a stored phrase token list reflects visual
- * left-to-right order. Shared by every path that slices a phrase so document order is computed
- * identically everywhere. Tokens missing from `tokenDocOrder` sort to the front (index 0).
- *
- * @param tokens - Token snapshots to sort. Not mutated; a new array is returned.
- * @param tokenDocOrder - Map from token ref to flat document index.
- * @returns A new array sorted by ascending document index.
+ * Sorts token snapshots by flat document index, without mutating the input, so a stored phrase
+ * token list reflects visual left-to-right order. Shared by every path that slices a phrase, so
+ * document order is computed identically everywhere. Tokens missing from the order map sort to the
+ * front.
*/
export function sortByDocOrder(
tokens: readonly T[],
@@ -75,15 +72,12 @@ export function sortByDocOrder(
}
/**
- * Sorts a phrase's tokens into document order and slices them at the boundary just after
- * `splitAfterTokenRef`. The single source of this slice, read by both {@link computeSplitFreeRefs}
- * and {@link splitPhraseAtBoundary}, so the destructive-border preview can't drift from the split it
+ * Slices a phrase's tokens, in document order, into the half up to and including the boundary token
+ * and the remainder after it; `undefined` when the boundary token is not among them.
+ *
+ * The single source of this slice, so the destructive-border preview cannot drift from the split it
* previews.
*
- * @param tokens - The phrase's token snapshots. Not mutated.
- * @param splitAfterTokenRef - Token ref marking the end of the earlier fragment (`before`).
- * @param tokenDocOrder - Map from token ref to flat document index; tokens are ordered by this
- * before slicing.
* @returns The `before` half (up to and including the boundary token) and the `after` remainder, or
* `undefined` when the boundary token is not found.
*/
@@ -100,15 +94,13 @@ function sliceAtBoundary(
}
/**
- * Enumerates the tokens of `phraseLink` that would become solo (free) after splitting just after
- * `splitAfterTokenRef` — a half with exactly one token leaves it unattached. Shares
- * {@link sliceAtBoundary} with {@link splitPhraseAtBoundary} so the destructive-border preview
- * matches the resulting split.
+ * Enumerates the tokens that a split just after the given boundary would leave solo (free), since a
+ * half with exactly one token leaves it unattached. Yields `undefined` when both halves would keep
+ * at least two tokens, the phrase is absent, or the boundary token is not found.
+ *
+ * Shares its slice with the split itself, so the destructive-border preview matches what the split
+ * will do.
*
- * @param phraseLink - The phrase to split, or `undefined` when it cannot be resolved.
- * @param splitAfterTokenRef - Token ref marking the end of the earlier fragment.
- * @param tokenDocOrder - Map from token ref to flat document index; tokens are ordered by this
- * before slicing.
* @returns The refs of tokens that would become free, or `undefined` when no token would be left
* solo (both halves ≥ 2 tokens), the phrase is absent, or the boundary token is not found.
*/
@@ -129,25 +121,19 @@ export function computeSplitFreeRefs(
}
/**
- * Splits `phraseLink` just after `splitAfterTokenRef` and dispatches the resulting
- * create/update/delete calls. Shared by both views' arc-split buttons and TokenLinkIcon's unlink
- * button so the three paths can't drift apart.
+ * Splits a phrase just after the given token and dispatches the resulting create, update, and
+ * delete calls. Shared by both views' arc-split buttons and by the unlink button, so the three
+ * paths cannot drift apart.
*
- * Outcomes (`before` is the half up to and including `splitAfterTokenRef`, `after` the remainder):
+ * Outcomes, where `before` is the half up to and including the boundary token:
*
- * - Both halves ≤ 1 token → delete the phrase (only 2 tokens to begin with).
- * - Both halves ≥ 2 tokens → shrink the phrase to `before`, create a new phrase from `after`.
- * - Exactly one half has 1 token → shrink the phrase to the larger half; the solo token becomes free.
+ * - Both halves ≤ 1 token → delete the phrase; it only had 2 tokens to begin with.
+ * - Both halves ≥ 2 tokens → shrink the phrase to `before` and create a new phrase from `after`.
+ * - Exactly one half has 1 token → shrink to the larger half; the solo token becomes free.
*
- * The boundary is in document order (how the buttons present it), so tokens are sorted by
- * `tokenDocOrder` before slicing — correct even if the stored list is out of order. No-op when
- * `splitAfterTokenRef` is absent or is the last token (which would leave the phrase unchanged).
- *
- * @param phraseLink - The phrase link to split.
- * @param splitAfterTokenRef - Token ref of the last token to keep in the earlier fragment.
- * @param dispatch - Phrase create/update/delete callbacks.
- * @param tokenDocOrder - Map from token ref to flat document index. Defaults to empty (stored
- * order).
+ * The boundary is in document order, matching how the buttons present it, so this holds even when
+ * the stored token list is out of order. Omitting the document-order map falls back to stored
+ * order. A boundary that is absent or is the phrase's last token is a no-op.
*/
export function splitPhraseAtBoundary(
phraseLink: PhraseAnalysisLink,
@@ -188,17 +174,15 @@ export type StraddledPhrase = {
};
/**
- * Finds every phrase that a segment boundary placed before `boundaryRef` would cut — phrases with
- * tokens on both sides of the boundary in document order. A discontiguous phrase counts even when
- * `boundaryRef` itself is not one of its tokens (the boundary can fall in the gap between two
- * fragments). Used by the segmentation dispatch to force-break straddling phrases, and by the split
- * control to suppress itself at boundaries that would force-break — one predicate, so the two can't
- * drift apart.
+ * Finds every phrase that a segment boundary would cut — those with tokens on both sides of it in
+ * document order — together with where each would have to be severed. A discontiguous phrase counts
+ * even when the boundary token is not one of its own, since the boundary can fall in the gap
+ * between two fragments.
+ *
+ * One predicate serves both the segmentation dispatch, which force-breaks straddling phrases, and
+ * the split control, which suppresses itself at boundaries that would force-break, so the two
+ * cannot drift apart. A boundary ref absent from the document-order map yields no matches.
*
- * @param boundaryRef - Token ref the new segment would begin at.
- * @param phraseLinks - The phrase links to test (one entry per phrase).
- * @param tokenDocOrder - Map from token ref to flat document index. Must contain `boundaryRef` and
- * the phrase tokens; a boundary ref absent from the map yields no matches.
* @returns The straddled phrases with their split points; empty when the boundary cuts nothing.
*/
export function phrasesStraddlingBoundary(
@@ -238,25 +222,18 @@ export function phrasesStraddlingBoundary(
// #region Arc geometry and strip sizing
/**
- * Stem height (px) an arc run rises above its box top at a given nesting level: the base stem plus
- * one {@link ARC_LEVEL_STEP} per level. The single source of this formula, so same-row and cross-row
- * runs at the same level share a channel (see {@link buildSameRowArcPath} /
- * {@link buildCrossRowArcPath}).
- *
- * @param level - The run's nesting level (0 = outermost).
- * @returns The stem height in pixels.
+ * Stem height (px) an arc run rises above its box top at a given nesting level, where level 0 is
+ * outermost. The single source of this formula, so same-row and cross-row runs at the same level
+ * share a channel.
*/
function stemForLevel(level: number): number {
return ARC_BASE_STEM + level * ARC_LEVEL_STEP;
}
/**
- * Vertical room (px) the topmost arc run at `maxArcLevel` needs above the line it rises from: its
- * stem ({@link stemForLevel}), the corner, and the clearance margin. Shared by
- * {@link computeStripTopPadding} and {@link computeStripRowGap} so both grow with arc depth alike.
- *
- * @param maxArcLevel - Maximum arc nesting level among the visible arcs.
- * @returns The clearance height in pixels.
+ * Vertical room (px) the topmost arc run needs above the line it rises from: its stem, the corner,
+ * and the clearance margin. Shared by the strip's top padding and row gap so both grow with arc
+ * depth alike.
*/
function arcClearancePx(maxArcLevel: number): number {
return stemForLevel(maxArcLevel) + ARC_CORNER_RADIUS + ARC_CLEARANCE_MARGIN_PX;
@@ -266,13 +243,11 @@ function arcClearancePx(maxArcLevel: number): number {
* Top padding (px) a token strip needs so arcs and the floating controls pill both fit above the
* boxes: {@link arcClearancePx} when any arc is drawn, plus controls headroom.
*
- * The pill rides the arc top (or box top for contiguous phrases) with its upper half extending
- * above; on box-top phrases it sits at `top: -CONTROLS_HALF_HEIGHT_PX`, so the strip needs `2 *
- * CONTROLS_HALF_HEIGHT_PX` to keep the whole pill visible.
+ * The pill rides the arc top, or the box top for contiguous phrases, with its upper half extending
+ * above — so a box-top phrase needs the pill's full height reserved to stay visible.
+ *
+ * Floored at {@link VERSE_SUPERSCRIPT_HEADROOM_PX} so a peeking verse number is never clipped.
*
- * @param hasArcs - Whether at least one arc is currently drawn.
- * @param maxArcLevel - Maximum arc nesting level among the visible arcs.
- * @param hasRealPhrase - Whether any committed phrase is rendered in the current window.
* @returns The required top padding in pixels, with a floor of {@link VERSE_SUPERSCRIPT_HEADROOM_PX}
* so a peeking verse number is never clipped.
*/
@@ -297,16 +272,10 @@ export const BASE_ROW_GAP_PX = 24;
/**
* Vertical gap (px) between wrapped token rows so arcs above a lower row clear the boxes of the row
- * above. Where {@link computeStripTopPadding} only protects the topmost row, this protects every
- * inter-row gap: a run in a lower row rises {@link arcClearancePx} above its box top into the shared
- * gap, and the controls pill's upper half rides on top of that. Floored at {@link BASE_ROW_GAP_PX}
- * so shallow/absent arcs never pack rows tighter than the static layout.
- *
- * @param hasArcs - Whether at least one arc is currently drawn.
- * @param maxArcLevel - Maximum arc nesting level among the visible arcs.
- * @param hasRealPhrase - Whether any committed phrase is rendered (so a controls pill may ride the
- * arc top of a lower row).
- * @returns The required inter-row vertical gap in pixels, never below {@link BASE_ROW_GAP_PX}.
+ * above. Where {@link computeStripTopPadding} protects only the topmost row, this protects every
+ * inter-row gap: a run in a lower row rises into the shared gap, with the controls pill's upper
+ * half riding on top of that. Floored at {@link BASE_ROW_GAP_PX} so shallow or absent arcs never
+ * pack rows tighter than the static layout.
*/
export function computeStripRowGap(
hasArcs: boolean,
@@ -332,11 +301,8 @@ type ArcStrokeProps = {
strokeWidth: number;
};
-/**
- * Arc stroke constants mirror the `phrase-*` Tailwind utilities in `tailwind.css`. If you change
- * the opacity values here, update the matching `--phrase-stroke-opacity` in the CSS too, and
- * vice-versa.
- */
+// Arc stroke constants mirror the `phrase-*` Tailwind utilities. If you change the opacity values
+// here, update the matching `--phrase-stroke-opacity` in the CSS too, and vice-versa.
/** Matches `phrase-dimmed`: border-color at full opacity. */
const DIMMED_ARC_STROKE: ArcStrokeProps = {
@@ -372,12 +338,6 @@ const HIGHLIGHTED_ARC_STROKE: ArcStrokeProps = {
* - `confirm-unlink`: target arc destructive, others dimmed.
* - `edit`: edited arc foreground (matches its box ring), others dimmed, hover suppressed.
* - `view`: focused arc full-foreground, hovered arc mid-foreground, others border-color.
- *
- * @param phraseMode - Current phrase-interaction mode.
- * @param phraseId - The phraseId of the arc being styled.
- * @param hoveredPhraseId - The phraseId currently hovered, if any.
- * @param focusedPhraseId - The phraseId of the focused token's phrase, if any.
- * @returns Stroke styling props for the arc.
*/
export function getArcStrokeProps(
phraseMode: PhraseMode,
@@ -424,15 +384,13 @@ type ArcSegment = {
};
/**
- * Greedy interval-graph coloring shared by {@link assignSegmentLevels} and {@link assignGutterLanes}:
- * walks `items` in order, giving each the lowest level not already taken by an earlier item it
- * `conflicts` with. The two wrappers differ only in their pre-sort and conflict predicate (which
- * axis they overlap on).
+ * Greedy interval-graph coloring shared by both level assignments, which differ only in their
+ * pre-sort and in the axis their conflict predicate overlaps on.
*
* @param items - The items to color, pre-sorted into the order they should be assigned.
- * @param conflicts - Returns whether two items overlap and so must take different levels.
- * @returns Map from each item to its assigned level. 0 is the level nearest the boxes — outermost
- * nesting for segments, the lane nearest the content edge for descents.
+ * @param conflicts - Whether two items overlap and so must take different levels.
+ * @returns Each item's level, where 0 is nearest the boxes — outermost nesting for arc segments,
+ * the lane nearest the content edge for gutter descents.
*/
function assignLevels(items: readonly T[], conflicts: (a: T, b: T) => boolean): Map {
const levels = new Map();
@@ -460,9 +418,6 @@ function assignLevels(items: readonly T[], conflicts: (a: T, b: T) => boolean
* row's overlaps demand. Cross-row runs share the same top channel as same-row runs (not an
* inter-row gap), so the two kinds do conflict when they share a row, keeping rerouted arcs aware
* of the same-row brackets they cross.
- *
- * @param segments - All arc segments across every phrase.
- * @returns Map from each segment to its assigned nesting level (0 = outermost).
*/
function assignSegmentLevels(segments: ArcSegment[]): Map {
const ordered = [...segments].sort((a, b) => a.left - b.left || a.right - b.right);
@@ -492,9 +447,6 @@ type GutterDescent = {
* {@link assignSegmentLevels}: two descents conflict when they route down the same side and their
* `[top, bottom]` spans overlap. Catches the vertically-nested case (C..D inside A..F) that per-row
* segment levels miss, since a descent's two run lines never share a top channel.
- *
- * @param descents - All cross-row gutter descents across every phrase.
- * @returns Map from each descent to its assigned lane (0 = nearest the content edge).
*/
function assignGutterLanes(descents: GutterDescent[]): Map {
const ordered = [...descents].sort((a, b) => a.top - b.top || a.bottom - b.bottom);
@@ -556,13 +508,7 @@ type ContainerRect = {
bottom: number;
};
-/**
- * Converts a viewport-relative `DOMRect` to container-relative coordinates.
- *
- * @param rect - The viewport-relative bounding rect of a phrase-box element.
- * @param containerRect - The viewport-relative bounding rect of the arc container.
- * @returns The same rect with every edge expressed relative to the container's top-left corner.
- */
+/** Re-expresses a viewport-relative rect against the arc container's top-left corner. */
function toContainerSpace(rect: DOMRect, containerRect: DOMRect): ContainerRect {
const left = rect.left - containerRect.left;
const right = rect.right - containerRect.left;
@@ -572,13 +518,9 @@ function toContainerSpace(rect: DOMRect, containerRect: DOMRect): ContainerRect
}
/**
- * Constructs an {@link ArcSegment} whose `left`/`right` span is `[min(x1, x2), max(x1, x2)]`, so
- * callers pass the two x values in any order without worrying which is smaller.
+ * Constructs an {@link ArcSegment} with its span normalized, so callers may pass the run's two
+ * endpoint x values in either order.
*
- * @param phraseId - The phrase this segment belongs to.
- * @param row - Rounded scroll-space top of the row whose top channel the horizontal run occupies.
- * @param x1 - One endpoint x of the run (scroll-space).
- * @param x2 - Other endpoint x of the run (scroll-space).
* @returns An {@link ArcSegment} with `left`/`right` normalized so `left ≤ right`.
*/
function makeArcSegment(phraseId: string, row: number, x1: number, x2: number): ArcSegment {
@@ -621,26 +563,18 @@ type PhraseBoxMeasurements = {
/** Container-space x of the strip's right content edge (right gutter anchor); 0 when no boxes. */
contentRight: number;
/**
- * Maps a box's top edge to its row's top line — the highest (minimum) top among boxes sharing the
- * box's row band. Cross-row arcs anchor each endpoint here rather than at its own box top, so a
- * gloss-less box of differing height still meets the channel shared by its row-mates.
- *
- * @param boxTop - The top edge of the box whose row top line is wanted.
- * @param boxBottom - The bottom edge of the box; half the box height is the row-matching
- * tolerance.
- * @returns The minimum top across boxes on the same row, never greater than `boxTop`.
+ * Maps a box's top edge to its row's top line — the highest top among boxes sharing that row
+ * band, and so never below the box's own top. Cross-row arcs anchor each endpoint here rather
+ * than at their own box top, so a gloss-less box of differing height still meets the channel
+ * shared by its row-mates. Half the box height serves as the row-matching tolerance.
*/
rowTopFor: (boxTop: number, boxBottom: number) => number;
};
/**
- * Reads every `[data-phrase-box]` element inside `container` once and projects it into the
- * container-relative measurements both arc passes need: per-phrase box lists, the strip's content
- * extent, and the row-top lookup. Splitting this off keeps {@link computeAllArcPaths} a pipeline of
- * named phases (measure → describe → level → build) rather than one long function.
- *
- * @param container - The arc container element to search.
- * @returns The measured phrase boxes, content edges, and row-top lookup.
+ * Reads every phrase-box element in the container once and projects it into the container-relative
+ * measurements both arc passes need. Split out so arc computation reads as a pipeline of named
+ * phases — measure, describe, level, build — rather than one long function.
*/
function measurePhraseBoxes(container: Element): PhraseBoxMeasurements {
const containerRect = container.getBoundingClientRect();
@@ -686,30 +620,20 @@ function measurePhraseBoxes(container: Element): PhraseBoxMeasurements {
}
/**
- * Measures all `[data-phrase-box]` elements inside `container` and computes SVG arc paths (in
- * container-relative coordinates) connecting each phrase's discontiguous boxes — same-row upward
- * brackets and cross-row brackets. Same-row arcs are leveled so they don't overlap; cross-row arcs
- * rise into the upper row's top channel then drop down a side gutter (the side nearer the arc's
- * average x, ties left), one lane further out per descent overlap so legs never cross a box.
+ * Computes the container-relative SVG arc paths connecting each phrase's discontiguous boxes, plus
+ * the nesting depth and gutter padding the strip must reserve.
*
- * @param container - The arc container element to search.
- * @returns The arc paths, max nesting level, and the left/right padding to reserve for gutter
- * lanes.
+ * Same-row arcs are leveled so they never overlap. Cross-row arcs rise into the upper row's top
+ * channel and then drop down a side gutter — whichever side is nearer the arc's average x, ties
+ * going left — one lane further out per overlapping descent, so no leg ever crosses a box.
*/
export function computeAllArcPaths(container: Element): ArcState {
const { boxesByPhrase, contentLeft, contentRight, rowTopFor } = measurePhraseBoxes(container);
/**
* Builds a {@link SameRowPair} for two boxes that share a row. The single arc run spans between
- * their centers, anchored to the row's normalized top channel (minimum box top on the row) so it
- * conflicts correctly with cross-row runs on the same channel.
- *
- * @param phraseId - The phrase the pair belongs to.
- * @param a - Container-space rect of the earlier (document-order) box.
- * @param b - Container-space rect of the later (document-order) box.
- * @param splitAfterTokenRef - Token ref of the last token in box `a`; stored for the split
- * button.
- * @returns A {@link SameRowPair} with its level-assignment segment embedded.
+ * their centers, anchored to the row's normalized top channel so it conflicts correctly with
+ * cross-row runs sharing that channel.
*/
const describeSameRowPair = (
phraseId: string,
@@ -726,17 +650,8 @@ export function computeAllArcPaths(container: Element): ArcState {
/**
* Builds a {@link CrossRowPair} for two boxes on different rows. Emits two independently-leveled
* segments — one per row's top channel, each spanning from its box center to the chosen side
- * gutter — so a nested arc routed out the opposite side doesn't conflict. The side is fixed here
- * (average x vs content edges, ties left) before levels exist.
- *
- * @param phraseId - The phrase the pair belongs to.
- * @param a - Container-space rect of the earlier (document-order) box; assumed upper (`a.top ≤
- * b.top`).
- * @param b - Container-space rect of the later (document-order) box.
- * @param splitAfterTokenRef - Token ref of the last token in box `a`; stored for the split
- * button.
- * @returns A {@link CrossRowPair} with its two level-assignment segments and routing side
- * embedded.
+ * gutter — so a nested arc routed out the opposite side doesn't conflict. The earlier box is
+ * assumed to be the upper one. The routing side is fixed here, before levels exist.
*/
const describeCrossRowPair = (
phraseId: string,
@@ -787,12 +702,6 @@ export function computeAllArcPaths(container: Element): ArcState {
// Deepest nesting level across every run; sizes the strip's top padding.
const maxLevel = segmentLevels.size > 0 ? Math.max(...segmentLevels.values()) : 0;
- /**
- * Reads a segment's assigned nesting level.
- *
- * @param seg - The segment whose level is wanted; always present, having been leveled above.
- * @returns The segment's nesting level (0 = outermost).
- */
const levelOf = (seg: ArcSegment): number =>
/* v8 ignore next -- every descriptor stores segments that were passed to assignSegmentLevels */
segmentLevels.get(seg) ?? 0;
@@ -874,8 +783,6 @@ const SPLIT_BUTTON_HEIGHT_PX = 14;
*
* The scan repeats to a fixed point — nudging one button can collide it with another — capped at
* one pass per button so an unresolvable residual terminates rather than loops.
- *
- * @param paths - All computed arc paths for the current layout; mutated in place.
*/
export function deconflictSplitButtons(paths: ArcPath[]): void {
for (let pass = 0; pass < paths.length; pass += 1) {
@@ -913,15 +820,12 @@ export function deconflictSplitButtons(paths: ArcPath[]): void {
/**
* Builds the SVG path and midpoint for a same-row upward-bracket arc between two boxes. The run
- * sits `stem` px above the box top with corners rounded _into_ the stem (not added on top), so it
- * shares the same channel as a cross-row run at the same stem — keeping intra- and inter-row arcs
- * aligned at a given level. Coordinates are scroll-space.
+ * sits the full stem above the box top with corners rounded _into_ the stem rather than added on
+ * top, so it shares a channel with a cross-row run at the same stem, keeping intra- and inter-row
+ * arcs aligned at a given level.
*
- * @param a - Scroll-space rect of the left/earlier box.
- * @param b - Scroll-space rect of the right/later box.
- * @param stem - Total stem height in pixels (base + level offset).
- * @returns The SVG path `d`, the arc's visual midpoint, and the x-extent of its run (the channel
- * the split button slides along).
+ * All coordinates are scroll-space. The returned run extent is the channel the split button slides
+ * along.
*/
export function buildSameRowArcPath(
a: { left: number; right: number; top: number },
@@ -951,15 +855,12 @@ export function buildSameRowArcPath(
}
/**
- * Builds an SVG path for an axis-aligned polyline through `points`, rounding each interior corner
- * with a quarter-circle of radius `r` (a line stopping `r` short of the corner, an arc onto the
- * next leg, continuing `r` past it). The radius is clamped to half the shorter adjacent leg so a
- * short leg never self-overlaps. Used by {@link buildCrossRowArcPath} so its multi-bend route reads
- * as a single rounded bracket.
+ * Builds an SVG path for an axis-aligned polyline, rounding each interior corner with a
+ * quarter-circle so a multi-bend route reads as a single rounded bracket. The radius is clamped to
+ * half the shorter adjacent leg, so a short leg never self-overlaps.
*
- * @param points - Ordered waypoints; consecutive points must share an x or a y (axis-aligned legs).
- * @param r - Desired corner radius in pixels.
- * @returns The SVG path `d` attribute string starting with `M`.
+ * @param points - Ordered waypoints; consecutive points must share an x or a y.
+ * @param r - Desired corner radius in pixels, before clamping.
*/
export function roundedPolyline(points: { x: number; y: number }[], r: number): string {
const [first] = points;
@@ -993,20 +894,12 @@ export function roundedPolyline(points: { x: number; y: number }[], r: number):
}
/**
- * Builds the SVG path for a cross-row arc between two boxes on different rows, routed so it never
- * passes behind a box: up from the upper box into its row's top channel (`aStem` above the box),
- * across to the gutter at `gutterX`, down the gutter, across into the lower row's channel (`bStem`
- * above that box), then down into its top. Each run sits its own (independently-leveled) stem above
- * its box; keeping the whole descent in the gutter is what avoids the boxes between the rows.
- * Coordinates are scroll-space.
+ * Builds the SVG path for an arc between two boxes on different rows, routed so it never passes
+ * behind a box. Each run sits its own independently-leveled stem above its box, and keeping the
+ * whole descent out in the side gutter is what clears the boxes lying between the two rows.
*
- * @param a - Scroll-space rect (top edge) of the earlier (upper) box.
- * @param b - Scroll-space rect (top edge) of the later (lower) box.
- * @param aStem - Stem height (px) of the upper run above the upper box top.
- * @param bStem - Stem height (px) of the lower run above the lower box top.
- * @param gutterX - Scroll-space x of the box-free side gutter the descent travels down.
- * @returns The SVG path `d`, the midpoint on the upper run line (for the split button), and the
- * x-extent of the upper run (box center → gutter) the button slides along.
+ * All coordinates are scroll-space. The returned midpoint sits on the upper run line, whose extent
+ * is the channel the split button slides along.
*/
export function buildCrossRowArcPath(
a: { left: number; right: number; top: number },
diff --git a/src/utils/project-summary-format.ts b/src/utils/project-summary-format.ts
index 4826a1bd..d7f3e0e5 100644
--- a/src/utils/project-summary-format.ts
+++ b/src/utils/project-summary-format.ts
@@ -1,15 +1,10 @@
/**
- * @file Shared formatting and sorting helpers for interlinear project summaries, used by the
- * project-selection and Save As modals so both lists order and label projects identically.
- */
-
-/**
- * Parses an ISO 8601 timestamp to epoch milliseconds, treating an unparsable string as `0`. The
- * summary type guard only checks that `updatedAt` is a string, so a corrupted value could otherwise
- * yield `NaN` and make the sort comparator's result undefined; normalizing to `0` keeps ordering
- * deterministic (corrupted entries sort last).
+ * Parses an ISO 8601 timestamp to epoch milliseconds, treating an unparsable string as `0`.
+ *
+ * The summary type guard only checks that `updatedAt` is a string, so a corrupted value could
+ * otherwise yield `NaN` and leave the sort comparator's result undefined. Normalizing to `0` keeps
+ * ordering deterministic, with corrupted entries sorting last.
*
- * @param value - The timestamp string to parse.
* @returns The parsed epoch milliseconds, or `0` when `value` is not a valid date.
*/
export function parseUpdatedAt(value: string): number {
@@ -18,12 +13,9 @@ export function parseUpdatedAt(value: string): number {
}
/**
- * Compares two ISO 8601 timestamps for a descending (newest-first) sort by their parsed epoch
- * milliseconds, so ordering is locale-independent (unlike `localeCompare`, whose result can vary by
- * collator).
+ * Compares two ISO 8601 timestamps newest-first. Ordering is locale-independent, unlike
+ * `localeCompare`, whose result can vary by collator.
*
- * @param a - The first ISO 8601 timestamp.
- * @param b - The second ISO 8601 timestamp.
* @returns A negative number when `a` is newer than `b` (sorts first), positive when older, `0`
* when the two timestamps are equal.
*/
@@ -32,13 +24,10 @@ export function compareUpdatedAtDescending(a: string, b: string): number {
}
/**
- * Formats the modified-date subline for a project row, e.g. `"Modified Jan 1, 2026, 12:00 PM"`. The
- * prefix is a localized label; the timestamp is rendered in the user's locale via
- * `toLocaleString`.
+ * Formats the modified-date subline for a project row, e.g. `"Modified Jan 1, 2026, 12:00 PM"`.
*
* @param prefix - Localized `"Modified"` label to precede the date.
- * @param updatedAt - ISO 8601 modification timestamp.
- * @returns The prefix followed by the locale-formatted timestamp.
+ * @param updatedAt - ISO 8601 timestamp, rendered in the user's locale.
*/
export function formatModified(prefix: string, updatedAt: string): string {
return `${prefix} ${new Date(updatedAt).toLocaleString()}`;
diff --git a/src/utils/segment-labels.ts b/src/utils/segment-labels.ts
index 89b62b9d..672c3ec1 100644
--- a/src/utils/segment-labels.ts
+++ b/src/utils/segment-labels.ts
@@ -1,16 +1,3 @@
-/**
- * @file Pure helper deriving each segment's verse-range gutter label in a (re)segmented book, shown
- * in the segment-view gutter column (never in the running text).
- *
- * After boundary edits a segment need not map 1:1 to a verse, so the label names the verse range it
- * covers: a single-verse segment (and each portion of a split verse) shows its bare number, a
- * multi-verse segment the range between its covered ends (`2–3`, `29–2:1`). Split portions are
- * not lettered — two portions of verse 1 both reading `1` reflects the overlap honestly.
- *
- * The label is derived from the segment's `verseStarts` — the exact set of covered source verses —
- * not the `startRef`/`endRef` interval, so a cross-chapter or gapped merge names only the verses
- * it actually covers instead of over-claiming every verse between its endpoints.
- */
import type { Segment, VerseStart } from 'interlinearizer';
/**
@@ -21,25 +8,21 @@ export type SegmentLabel = string;
/**
* Builds the verse-range gutter label of every segment in book order, keyed by segment id. The
- * label is the segment's first covered verse, extended to `start–end` when the segment covers more
- * than one verse, with the end qualified by its chapter (`start–chapter:end`) when the segment
- * crosses a chapter boundary.
+ * labels appear in the segment-view gutter column, never in the running text.
*
- * @param segments - The book's segments in document order.
- * @returns Map from segment id to its {@link SegmentLabel}.
+ * After boundary edits a segment need not map 1:1 to a verse, so a label names the verse range it
+ * covers rather than a single verse. Split portions are deliberately not lettered — two portions of
+ * verse 1 both reading `1` reflects the overlap honestly.
+ *
+ * Labels derive from each segment's covered verse starts rather than its start/end refs, so a
+ * cross-chapter or gapped merge names only the verses it actually covers instead of over-claiming
+ * every verse between its endpoints.
*/
export function buildSegmentLabels(segments: readonly Segment[]): Map {
return new Map(segments.map((seg) => [seg.id, labelForSegment(seg.verseStarts)]));
}
-/**
- * Formats one segment's verse-range label from its covered verse starts: the first verse's bare
- * number, extended to `first–last` when the segment covers more than one verse, with the last
- * qualified by its chapter (`first–chapter:last`) across a chapter boundary.
- *
- * @param verseStarts - The segment's covered verse starts, in document order.
- * @returns The verse-range label.
- */
+/** Formats one segment's label as `first`, `first–last`, or `first–chapter:last`. */
function labelForSegment(verseStarts: readonly VerseStart[]): SegmentLabel {
const first = verseStarts[0];
const last = verseStarts[verseStarts.length - 1];
diff --git a/src/utils/segmentation.ts b/src/utils/segmentation.ts
index b1c4339f..1bcf00f7 100644
--- a/src/utils/segmentation.ts
+++ b/src/utils/segmentation.ts
@@ -1,16 +1,9 @@
/**
- * @file Pure transforms over a {@link SegmentationDelta} — the user's custom segment boundaries
- * expressed as a delta from the default one-segment-per-verse segmentation.
+ * Pure transforms over a {@link SegmentationDelta}, the user's custom segment boundaries expressed
+ * as a delta from the default one-segment-per-verse segmentation.
*
- * A segment is a maximal contiguous run of the book's document-order token stream between "start"
- * tokens. The default start tokens are each verse's first token; the delta records where the
- * user's boundaries differ (a removed verse start merges that verse into the previous segment; an
- * added start splits a verse). Because a segment can only be a contiguous run between starts,
- * discontiguous segments are unrepresentable.
- *
- * Every function here is pure and store-free (mirrors `phrase-arc.ts`). They take the original
- * verse-tokenized {@link Book} (from `tokenizeBook`, before re-segmentation) so they can derive
- * the default verse starts; they never need the re-segmented book.
+ * Every transform takes the _original_ verse-tokenized book — never the re-segmented one — because
+ * that is what the default verse starts are derived from, and returns a normalized delta.
*/
import type { Book, SegmentationDelta } from 'interlinearizer';
@@ -19,7 +12,7 @@ const EMPTY_DELTA: SegmentationDelta = { removedVerseStarts: [], addedStarts: []
/**
* The whole-book lookups every transform in this module needs, derived in a single pass over the
- * token stream so one boundary op walks the book once.
+ * token stream so one boundary operation walks the book once.
*/
type BookLookups = Readonly<{
/**
@@ -36,18 +29,11 @@ type BookLookups = Readonly<{
}>;
/**
- * Per-book cache of {@link BookLookups}, keyed by book reference. `verseBook` identity is stable for
- * a given tokenization (changing only on re-tokenization), so every op reuses one traversal.
+ * Per-book cache of {@link BookLookups}. A tokenized book's identity is stable until it is
+ * re-tokenized, so every operation reuses one traversal.
*/
const bookLookupsCache = new WeakMap();
-/**
- * Returns the {@link BookLookups} for `verseBook`, computing them in one pass on first use and
- * caching by book reference thereafter.
- *
- * @param verseBook - The original verse-tokenized book.
- * @returns The cached (or freshly built) lookups.
- */
function bookLookups(verseBook: Book): BookLookups {
const cached = bookLookupsCache.get(verseBook);
if (cached) return cached;
@@ -75,25 +61,18 @@ function bookLookups(verseBook: Book): BookLookups {
}
/**
- * The default segment-start refs — each verse segment's first token (of any type, so a verse's
- * leading punctuation stays with that verse).
- *
- * @param verseBook - The original verse-tokenized book.
- * @returns The set of first-token refs, one per verse segment that has tokens.
+ * The default segment-start refs — each verse segment's first token, of any type, so a verse's
+ * leading punctuation stays with that verse.
*/
export function defaultVerseStarts(verseBook: Book): ReadonlySet {
return bookLookups(verseBook).defaults;
}
/**
- * The effective set of segment-start refs after applying `delta` to the default verse starts:
- * `(defaults \ removedVerseStarts) ∪ addedStarts`, with added anchors dropped when their token no
- * longer exists and the book's first token always forced to be a start. Shared with `resegmentBook`
- * so re-segmentation and the editing operations agree on where boundaries fall.
- *
- * @param verseBook - The original verse-tokenized book.
- * @param delta - The user's boundary delta, or `undefined` for the default segmentation.
- * @returns The set of token refs that begin a segment.
+ * The token refs that begin a segment once the delta is applied to the default verse starts:
+ * `(defaults \ removedVerseStarts) ∪ addedStarts`. Added anchors whose token no longer exists are
+ * dropped, and the book's first token is always forced to be a start. Shared with re-segmentation
+ * so it and the editing operations agree on where boundaries fall.
*/
export function effectiveStarts(
verseBook: Book,
@@ -116,13 +95,8 @@ export function effectiveStarts(
}
/**
- * Returns a canonicalized copy of `delta`: each array deduped, stripped of no-op entries (a removed
- * ref that is not a default start, or an added ref that is already a default start or whose token
- * is gone), and sorted by document order so equal segmentations serialize identically.
- *
- * @param verseBook - The original verse-tokenized book.
- * @param delta - The delta to canonicalize.
- * @returns A normalized {@link SegmentationDelta}.
+ * Canonicalizes a delta so that equal segmentations serialize identically: each array is deduped,
+ * stripped of no-op entries, and sorted by document order.
*/
function normalize(verseBook: Book, delta: SegmentationDelta): SegmentationDelta {
const { defaults, all, order, first } = bookLookups(verseBook);
@@ -141,18 +115,9 @@ function normalize(verseBook: Book, delta: SegmentationDelta): SegmentationDelta
}
/**
- * Makes `ref` begin a segment — i.e. splits before it.
- *
- * - When `ref` is a default verse start that was merged away, it is un-merged (dropped from
- * `removedVerseStarts`).
- * - Otherwise `ref` is recorded as an added start.
- *
- * No-op (returns an equivalent normalized delta) when `ref` already begins a segment.
- *
- * @param verseBook - The original verse-tokenized book.
- * @param delta - The current delta, or `undefined` for the default segmentation.
- * @param ref - The token ref that should begin a segment.
- * @returns The updated, normalized delta.
+ * Makes a token begin a segment — that is, splits before it. A default verse start that had been
+ * merged away is un-merged; any other token is recorded as an added start. Already being a segment
+ * start is a no-op.
*/
export function addBoundaryBefore(
verseBook: Book,
@@ -174,17 +139,9 @@ export function addBoundaryBefore(
}
/**
- * Stops `ref` from beginning a segment — i.e. merges it into the preceding segment.
- *
- * - When `ref` is a default verse start, it is recorded in `removedVerseStarts`.
- * - Otherwise (it was an added split) it is dropped from `addedStarts`.
- *
- * No-op when `ref` is the book's first token (the first segment cannot be merged leftward).
- *
- * @param verseBook - The original verse-tokenized book.
- * @param delta - The current delta, or `undefined` for the default segmentation.
- * @param ref - The segment-start token ref to remove.
- * @returns The updated, normalized delta.
+ * Stops a token from beginning a segment, merging it into the preceding one. A default verse start
+ * is recorded as removed; a previously added split is dropped. Merging the book's first token is a
+ * no-op, since the first segment cannot merge leftward.
*/
export function removeBoundaryAt(
verseBook: Book,
@@ -207,14 +164,8 @@ export function removeBoundaryAt(
}
/**
- * Moves a boundary from `fromRef` to `toRef` in one step — the primitive behind pulling a single
- * edge token across a segment boundary. Removes the start at `fromRef` and adds one at `toRef`.
- *
- * @param verseBook - The original verse-tokenized book.
- * @param delta - The current delta, or `undefined` for the default segmentation.
- * @param fromRef - The current segment-start ref to remove.
- * @param toRef - The new segment-start ref to add.
- * @returns The updated, normalized delta.
+ * Moves a boundary from one token to another in a single step — the primitive behind pulling one
+ * edge token across a segment boundary.
*/
export function moveBoundary(
verseBook: Book,
@@ -226,14 +177,9 @@ export function moveBoundary(
}
/**
- * Merges the segment that starts at `secondSegmentStartRef` into the segment before it. Thin alias
- * for {@link removeBoundaryAt}, named for the explicit merge control.
- *
- * @param verseBook - The original verse-tokenized book.
- * @param delta - The current delta, or `undefined` for the default segmentation.
- * @param secondSegmentStartRef - The first-token ref of the segment being merged into its
- * predecessor.
- * @returns The updated, normalized delta.
+ * Merges a segment into the one before it, identified by the first-token ref of the _second_
+ * segment — the one absorbed into its predecessor. A thin alias for {@link removeBoundaryAt}, named
+ * for the explicit merge control.
*/
export function mergeSegments(
verseBook: Book,
@@ -244,13 +190,8 @@ export function mergeSegments(
}
/**
- * Splits a segment so a new one begins at `ref`. Thin alias for {@link addBoundaryBefore}, named for
- * the explicit split control.
- *
- * @param verseBook - The original verse-tokenized book.
- * @param delta - The current delta, or `undefined` for the default segmentation.
- * @param ref - The token ref the new segment should begin at.
- * @returns The updated, normalized delta.
+ * Splits a segment so a new one begins at the given token. A thin alias for
+ * {@link addBoundaryBefore}, named for the explicit split control.
*/
export function splitSegmentBefore(
verseBook: Book,
@@ -261,9 +202,8 @@ export function splitSegmentBefore(
}
/**
- * Whether `delta` represents the default verse segmentation (absent or both arrays empty).
+ * Whether the delta represents the default verse segmentation: absent, or both arrays empty.
*
- * @param delta - The delta to test.
* @returns `true` when applying `delta` yields the default segmentation.
*/
export function isDefaultSegmentation(delta: SegmentationDelta | undefined): boolean {
diff --git a/src/utils/split-anchor.ts b/src/utils/split-anchor.ts
index f49a496a..4fbcc1d3 100644
--- a/src/utils/split-anchor.ts
+++ b/src/utils/split-anchor.ts
@@ -1,9 +1,3 @@
-/**
- * @file The punctuation-travel rule that decides which token a segment split is anchored before,
- * for a gap between two adjacent words. `resegmentBook` cuts immediately before whatever anchor
- * it is given, so which side of a split punctuation lands on is entirely a matter of anchor
- * choice.
- */
import type { Token } from 'interlinearizer';
/**
@@ -31,14 +25,9 @@ const OPENING_MARKS: ReadonlySet = new Set([
]);
/**
- * Reports whether the two adjacent tokens `before` and `after` touch — i.e. the literal baseline
- * substring between them (`baselineText.slice(before.charEnd, after.charStart)`) is empty or
- * contains no whitespace. Pure adjacency, script-agnostic: the caller never scans the token text,
- * only the gap between tokens.
+ * Reports whether two adjacent tokens touch, meaning no whitespace separates them. Pure adjacency
+ * and script-agnostic: only the gap between the tokens is examined, never the token text itself.
*
- * @param before - The earlier token in document order.
- * @param after - The later token in document order.
- * @param baselineText - The owning segment's baseline text.
* @returns `true` when there is no whitespace between the two tokens.
*/
function touches(before: Token, after: Token, baselineText: string): boolean {
@@ -47,15 +36,13 @@ function touches(before: Token, after: Token, baselineText: string): boolean {
}
/**
- * Reports whether the punctuation at index `i` in `run` is part of an unbroken (whitespace-free)
- * chain of tokens reaching the run's word at `boundary`, scanning in `step` direction. Because a
- * cluster of adjacent marks with no internal whitespace travels as a unit, a mark reaches the
- * following word when every gap from it forward to `wordNext` is whitespace-free, and the preceding
- * word symmetrically. The word endpoints of `run` are always `boundary` when the chain arrives.
+ * Reports whether the punctuation at index `i` is joined to the word at `boundary` by an unbroken,
+ * whitespace-free chain of tokens. A cluster of adjacent marks with no internal whitespace travels
+ * as a unit, so a mark counts as reaching a word when every gap along the way is whitespace-free.
*
* @param run - The full token run for the gap: `[wordPrev, ...punctuation, wordNext]`.
* @param i - Index of the punctuation token to test.
- * @param step - `+1` to scan toward `wordNext`, `-1` to scan toward `wordPrev`.
+ * @param step - `+1` to scan toward the following word, `-1` toward the preceding one.
* @param boundary - Index of the word the chain must reach (`run.length - 1` forward, `0` back).
* @param baselineText - The owning segment's baseline text; inter-token gaps are read from it.
* @returns `true` when an unbroken whitespace-free chain connects the token to that word.
@@ -99,17 +86,15 @@ function bindsFollowing(run: Token[], i: number, baselineText: string): boolean
}
/**
- * Computes the anchor token ref for a segment split at the word-word gap between `prevToken` and
- * `nextToken`. Walks the punctuation tokens between them and returns the ref of the first token (in
- * document order) that binds to the **following** segment; when no punctuation binds following, the
- * anchor is `nextToken` itself.
+ * Computes the token ref a segment split at a word-word gap should be anchored before, applying the
+ * punctuation-travel rule. Re-segmentation cuts immediately before whatever anchor it is given, so
+ * which side of a split the gap's punctuation lands on is entirely a matter of this choice.
*
* @param prevToken - The word token immediately before the gap.
- * @param nextToken - The word token immediately after the gap (the default anchor).
+ * @param nextToken - The word token immediately after the gap, and the anchor used when no
+ * punctuation binds following.
* @param punctuation - The punctuation tokens sitting in the gap, in document order.
- * @param baselineText - The owning segment's baseline text; whitespace between tokens is read from
- * it via the literal gap substring between adjacent tokens.
- * @returns The ref of the token the boundary should be placed before.
+ * @param baselineText - The owning segment's baseline text; inter-token gaps are read from it.
*/
export function resolveSplitAnchor(
prevToken: Token,
diff --git a/src/utils/suggestion-engine.ts b/src/utils/suggestion-engine.ts
index 2a33df94..842432fa 100644
--- a/src/utils/suggestion-engine.ts
+++ b/src/utils/suggestion-engine.ts
@@ -1,26 +1,24 @@
/**
- * @file Pure suggestion-engine core: builds the analysis pool, ranks competing payloads, and
- * derives per-token suggestions. Everything here is a pure function over plain data so the engine
- * is trivially testable; the memoized Redux selectors that feed it live in
- * `store/analysisSlice`.
+ * Pure suggestion-engine core: builds the analysis pool, ranks competing payloads, and derives
+ * per-token suggestions. Everything here is a pure function over plain data, so the engine is
+ * trivially testable; the memoized selectors that feed it live in the store.
*
- * Suggestions and candidates are never persisted — they are derived on read; only the approved
- * human decisions are stored. The pool is the set of approved analyses in the current draft.
+ * Suggestions and candidates are never persisted — they are derived on read, and only approved
+ * human decisions are stored. The pool is the set of approved analyses in the current draft.
*/
import type { AssignmentStatus, TokenAnalysis } from 'interlinearizer';
import { normalizeSurfaceForm } from './analysis-identity';
/**
- * Shared empty candidate list returned for every non-homograph match (the common case), so
- * {@link deriveTokenSuggestion} never allocates a throwaway `[]` per call. Read-only and never
- * mutated by any consumer.
+ * Shared empty candidate list returned for every non-homograph match, so the common case never
+ * allocates a throwaway array. Read-only and never mutated by any consumer.
*/
const NO_CANDIDATES: readonly TokenAnalysis[] = [];
/** One distinct approved payload in the pool together with how many tokens currently approve it. */
export interface PoolEntry {
- /** The shared approved `TokenAnalysis` payload. */
+ /** The shared approved payload. */
analysis: TokenAnalysis;
/** Number of tokens whose approved link points at this payload — its approval frequency. */
frequency: number;
@@ -28,40 +26,39 @@ export interface PoolEntry {
/**
* The analysis pool indexed for matching: normalized surface form → the distinct approved payloads
- * sharing that form, each bucket pre-ranked best-first ({@link comparePoolEntries}) so its head is
- * the suggested pick. A single-element list is the common case; multiple entries mean a homograph
- * (competing analyses of the same surface form). Buckets are read-only — they are ranked once at
- * build time and never re-sorted per token.
+ * sharing that form. Each bucket is pre-ranked best-first at build time and never re-sorted per
+ * token, so its head is the suggested pick. A single-element bucket is the common case; multiple
+ * entries mean a homograph.
*/
export type PoolIndex = ReadonlyMap;
-/** The engine's derived proposal for one un-approved token (never persisted — derived on read). */
+/** The engine's derived proposal for one un-approved token. Never persisted. */
export interface TokenSuggestion {
- /** The top-ranked matching payload — the engine's single best pick (the `suggested` analysis). */
+ /** The top-ranked matching payload — the engine's single best pick. */
suggested: TokenAnalysis;
/**
- * The remaining matching payloads, in rank order — the `candidate` alternatives a reviewer can
- * promote instead of the suggestion. Empty unless the surface form is a homograph. Read-only so
- * the single shared empty array returned for the common non-homograph case is never mutated.
+ * The remaining matching payloads, in rank order — the alternatives a reviewer can promote
+ * instead. Empty unless the surface form is a homograph. Read-only, because the non-homograph
+ * case returns one shared empty array.
*/
candidates: readonly TokenAnalysis[];
}
/**
* The merged per-token read the renderer consumes: the token's approved decision when one exists,
- * otherwise the engine's derived suggestion. The selector that produces this returns `undefined`
- * (not modeled here) when the token has neither — an unanalyzed token with no pool match.
+ * otherwise the engine's derived suggestion. The selector producing this yields `undefined`, not
+ * modeled here, when the token has neither — an unanalyzed token with no pool match.
*/
export type ResolvedTokenAnalysis =
| {
- /** The token has a human-confirmed analysis; `analysis` is canonical for rendering. */
+ /** The token has a human-confirmed analysis, canonical for rendering. */
status: 'approved';
/** The approved payload. */
analysis: TokenAnalysis;
/**
- * Pool alternatives for this surface form, when any exist, so the suggestion dropdown can
- * offer re-promotion even after the token is approved. `undefined` when the pool has no match
- * for this surface form (the token was manually glossed with no pool peers).
+ * Pool alternatives for this surface form, so the suggestion dropdown can offer re-promotion
+ * even after the token is approved. `undefined` when the pool has no match — the token was
+ * manually glossed with no pool peers.
*/
poolSuggestion?: TokenSuggestion;
}
@@ -71,14 +68,10 @@ export type ResolvedTokenAnalysis =
} & TokenSuggestion);
/**
- * Orders two competing pool entries best-first: the more-approved entry sorts before the less, and
- * a frequency tie is broken by the lower `analysis.id`. The id tiebreak is deterministic and
- * content-independent, so the elected suggestion never flickers between equally-frequent homographs
- * as unrelated edits reorder the pool. Used by {@link buildPoolIndex} to pre-rank each bucket once
- * at build time so per-token derives never re-sort.
+ * Orders two competing pool entries best-first, breaking a frequency tie by the lower analysis id.
+ * That tiebreak is deterministic and content-independent, so the elected suggestion never flickers
+ * between equally-frequent homographs as unrelated edits reorder the pool.
*
- * @param a - First pool entry.
- * @param b - Second pool entry.
* @returns A negative number when `a` ranks first, positive when `b` ranks first.
*/
function comparePoolEntries(a: PoolEntry, b: PoolEntry): number {
@@ -87,23 +80,17 @@ function comparePoolEntries(a: PoolEntry, b: PoolEntry): number {
}
/**
- * Groups the approved analyses into the {@link PoolIndex} used for matching: each distinct payload
- * is filed under the normalized form of its `surfaceText` ({@link normalizeSurfaceForm}), carrying
- * its approval frequency. Because the write path dedupes identical analyses and
- * `appendApprovedAnalysis` only shares a payload across tokens with the same normalized surface
- * form, every token under one key truly competes for the same word — so two entries under one key
- * are genuine homographs, never accidental near-duplicates.
+ * Groups the approved analyses into the {@link PoolIndex} used for matching, filing each distinct
+ * payload under the normalized form of its surface text along with its approval frequency.
*
- * Keying on the normalized surface form alone (not also the writing system) is correct for v1: the
- * pool is a single source project whose word tokens share one writing system, and NFC keeps
- * different scripts on distinct code points — so equal normalized forms already imply the same
- * writing system.
+ * Because the write path dedupes identical analyses and only shares a payload across tokens with
+ * the same normalized surface form, every token under one key truly competes for the same word — so
+ * two entries under one key are genuine homographs, never accidental near-duplicates.
*
- * @param analysisById - Map from `TokenAnalysis.id` to its payload (every approved id resolves
- * here).
- * @param approvedCountByAnalysisId - Map from each approved `TokenAnalysis.id` to its approval
- * frequency; its keys are exactly the distinct approved payloads.
- * @returns The pool indexed by normalized surface form.
+ * Keying on the normalized surface form alone, rather than also the writing system, is correct for
+ * v1: the pool is a single source project whose word tokens share one writing system, and NFC keeps
+ * different scripts on distinct code points, so equal normalized forms already imply the same
+ * writing system.
*/
export function buildPoolIndex(
analysisById: ReadonlyMap,
@@ -126,23 +113,18 @@ export function buildPoolIndex(
}
/**
- * Derives the suggestion for one token from the pool by matching on its normalized surface form
- * ({@link normalizeSurfaceForm}). When the form matches, the matched bucket is already ranked
- * best-first ({@link buildPoolIndex}), so its head is the `suggested` analysis and the rest are the
- * ranked `candidate`s; when it does not match, there is no suggestion. The caller is responsible
- * for only asking about tokens that have no approved analysis (an approved token reads its
- * decision, not a suggestion).
- *
- * Pass `excludeAnalysisId` to preview the suggestion a token would resolve to if its own approval
- * were removed (used the instant an approved gloss is cleared, before the empty value commits):
- * that payload's frequency is decremented — dropping it from the bucket entirely when this was its
- * last approval — and the remainder re-ranked, so the preview matches the pool the committed
- * deletion will produce rather than the approved payload's mere alternatives.
+ * Derives one token's suggestion from the pool by matching on its normalized surface form, or
+ * `undefined` when nothing matches. Callers are responsible for only asking about tokens that have
+ * no approved analysis, since an approved token reads its decision rather than a suggestion.
*
- * @param poolIndex - The pool indexed by normalized surface form.
- * @param surfaceText - The token's raw surface text.
- * @param excludeAnalysisId - When given, the id of an approved payload to discount by one approval
- * before ranking, previewing the pool as if this token were unapproved.
+ * @param poolIndex - The pool to match against, already ranked best-first at build time.
+ * @param surfaceText - The token's raw surface text; normalized here before lookup, so callers pass
+ * it verbatim.
+ * @param excludeAnalysisId - An approved payload to discount by one approval before ranking,
+ * previewing the suggestion this token would fall back to if its own approval were removed. Used
+ * the instant an approved gloss is cleared, before the empty value commits, so the preview
+ * matches the pool the committed deletion will produce rather than the approved payload's mere
+ * alternatives.
* @returns The token's suggestion, or `undefined` when no pooled analysis matches (or none remains
* once the excluded payload is discounted).
*/
@@ -184,36 +166,31 @@ export function deriveTokenSuggestion(
export interface GlossedSuggestionEntry {
/** The matching payload's id — the approve/promote target and the React key. */
id: string;
- /** The payload's gloss in the active analysis language; never blank (blank entries are dropped). */
+ /** The payload's gloss in the active analysis language; never blank. */
gloss: string;
/**
- * How the row is offered: `'suggested'` for the engine's pick on an un-approved token (blue,
- * "accept"), or `'candidate'` for every promotable alternative (grey, "promote"). Carried on the
- * entry — rather than re-derived from row position by the renderer — so dropping a blank-in-
- * language pick can never leave a candidate masquerading as the accept row.
+ * How the row is offered: `'suggested'` for the engine's pick on an un-approved token (the
+ * "accept" row), or `'candidate'` for a promotable alternative. Carried on the entry rather than
+ * re-derived from row position, so dropping a blank-in-language pick can never leave a candidate
+ * masquerading as the accept row.
*/
status: Extract;
}
/**
* Flattens the merged per-token read into the entries the gloss UI renders, in rank order, keeping
- * only those that carry a non-blank gloss in `analysisLanguage`. Centralizes the
- * suggestion-presentation policy in one place: which matches are renderable, how a blank-in-active-
- * language pick falls through to the next-ranked one rather than showing an empty button, the
- * already-approved payload's exclusion from its own promote list, and — critically — each row's
- * assignment status, so every surface ranks, colors, and labels suggestions identically instead of
- * re-deriving any of it from the raw {@link TokenSuggestion} or from row position.
+ * only those with a non-blank gloss in the active language.
*
- * For an un-approved (`'suggested'`) token the highest-ranked renderable match is offered as
- * `'suggested'` (the blue "accept" row) and the rest as `'candidate'` (grey "promote"). The status
- * is assigned _after_ dropping blank-in-language picks, so when the engine's top pick has no gloss
- * in the active language the next-ranked glossed match correctly becomes the accept row rather than
- * the whole suggestion vanishing. For an already-approved token every pool peer is a `'candidate'`
- * (promote) — the approved payload itself is excluded, and there is no accept row, so even the top
- * row reads as a promotion rather than masquerading as an "accept the suggestion" row.
+ * This is the single home of suggestion-presentation policy — which matches are renderable, how a
+ * blank-in-active-language pick falls through, the approved payload's exclusion from its own
+ * promote list, and each row's assignment status — so every surface ranks, colors, and labels
+ * suggestions identically instead of re-deriving any of it from row position.
+ *
+ * Status is assigned _after_ blank picks are dropped. So when the engine's top pick has no gloss in
+ * the active language, the next-ranked glossed match becomes the accept row rather than the whole
+ * suggestion vanishing. An already-approved token has no accept row at all: every pool peer is a
+ * promotion, so even the top row reads as one.
*
- * @param resolved - The token's merged approved/suggested read, or `undefined` when it has neither.
- * @param analysisLanguage - BCP 47 tag whose gloss to read from each matching payload.
* @returns The glossed entries in rank order; empty when there is nothing renderable.
*/
export function glossedSuggestionEntries(
@@ -246,19 +223,17 @@ export function glossedSuggestionEntries(
}
/**
- * Equality predicate for two {@link ResolvedTokenAnalysis} results, for use as a `useSelector`
+ * Equality predicate for two {@link ResolvedTokenAnalysis} results, for use as a selector's
* `equalityFn` so a per-token subscription stays referentially stable across unrelated store
- * changes. {@link selectResolvedTokenAnalysis} (in `store/analysisSlice`) freshly allocates its
- * wrapper object — and the suggested branch a fresh `candidates` array — on every call, so the
- * default `Object.is` comparison would re-render every visible suggested token on any store change.
- * This compares by the meaningful identity instead: the `analysis` / `suggested` payloads and each
- * `candidate` are reference-stable store objects (the pool only re-files the same payloads), so
- * comparing them by reference is both correct and cheap — equal whenever the rendered suggestion is
- * unchanged, even when an incidental edit elsewhere rebuilt the pool index around the same
- * payloads.
+ * changes.
+ *
+ * The selector freshly allocates its wrapper object — and, on the suggested branch, a fresh
+ * candidates array — on every call, so a default `Object.is` comparison would re-render every
+ * visible suggested token on any store change. Comparing the payloads by reference instead is both
+ * correct and cheap, because the pool only ever re-files the same reference-stable store objects:
+ * the result is equal whenever the rendered suggestion is unchanged, even when an incidental edit
+ * elsewhere rebuilt the pool index around those same payloads.
*
- * @param a - The previous resolved analysis (or `undefined` when the token had neither).
- * @param b - The next resolved analysis (or `undefined`).
* @returns `true` when the two describe the same approved decision or suggestion.
*/
export function resolvedTokenAnalysisEqual(
diff --git a/src/utils/token-layout.ts b/src/utils/token-layout.ts
index 7da9ab74..e919f5a8 100644
--- a/src/utils/token-layout.ts
+++ b/src/utils/token-layout.ts
@@ -11,14 +11,10 @@ import { isWordToken } from '../types/type-guards';
export type { FocusContext, LinkSlot, RenderUnit, SlotFocusInfo, TokenGroup };
/**
- * Resolves the focus context from a single `focusedTokenRef`. All views use the same rules; the
- * only thing that differs between layouts is how they discover which token is focused, not what
- * that focus means.
+ * Resolves the {@link FocusContext} implied by a single focused token ref. All views share these
+ * rules; layouts differ only in how they discover which token is focused, not in what that focus
+ * means.
*
- * @param focusedTokenRef - Ref of the focused word token, or `undefined`.
- * @param tokensByRef - Lookup from token ref to the full token (word or other).
- * @param phraseLinkByRef - Map from token ref to the phrase link containing it.
- * @param tokenSegmentMap - Map from token ref to the id of the segment containing it.
* @returns The resolved focus context. All fields are `undefined` when `focusedTokenRef` is unset.
*/
export function resolveFocusContext(
@@ -50,18 +46,13 @@ export function resolveFocusContext(
}
/**
- * Computes the slot's focus-derived inputs to `TokenLinkIcon`. Pure function over the slot's
- * segment ids and the supplied focus context; bundles the slot-specific flags together with the
- * focused phrase/token so the icon receives a single `slotFocus` object.
+ * Computes one link slot's {@link SlotFocusInfo}, bundling the slot-specific flags with the focused
+ * phrase and token so the link icon receives a single focus object.
*
- * @param prevSegmentId - Segment id of the group before the slot, or `undefined` for the leading
- * slot.
- * @param nextSegmentId - Segment id of the group after the slot, or `undefined` for the trailing
- * slot.
+ * @param prevSegmentId - Segment id of the group before the slot; `undefined` for the leading slot.
+ * @param nextSegmentId - Segment id of the group after the slot; `undefined` for the trailing slot.
* @param focus - Resolved focus context for the whole strip.
- * @param focusedSideIsPrev - The layout-specific bool indicating whether focus is start-ward of
- * this slot.
- * @returns Slot focus info ready to pass as `slotFocus` to `MemoizedTokenLinkIcon`.
+ * @param focusedSideIsPrev - Layout-specific flag for whether focus is start-ward of this slot.
*/
export function resolveSlotFocus(
prevSegmentId: string | undefined,
@@ -83,8 +74,8 @@ export function resolveSlotFocus(
}
/**
- * The "no focus" slot-focus bundle: nothing focused, so the link button is inert. Used by
- * `PhraseBox` for the in-phrase unlink icons, which never participate in focus-driven linking.
+ * The "no focus" bundle: nothing focused, so the link button is inert. Used for the unlink icons
+ * inside a phrase, which never participate in focus-driven linking.
*/
export const NO_SLOT_FOCUS: SlotFocusInfo = {
focusedSideIsPrev: undefined,
@@ -94,15 +85,10 @@ export const NO_SLOT_FOCUS: SlotFocusInfo = {
};
/**
- * Groups adjacent word tokens that share the same approved `PhraseAnalysisLink` into single
- * `TokenGroup` entries. Non-word tokens are skipped. Discontiguous phrase members produce separate
- * groups that share the same `phraseLink`. `punctuationBetween` is initialized to empty arrays
- * here; {@link buildRenderUnits} fills it in with any punctuation tokens that appear between the
- * word tokens in document order.
- *
- * @param tokens - The flat token list to group.
- * @param phraseLinkByRef - Map from `tokenRef` to the `PhraseAnalysisLink` containing it.
- * @returns An ordered array of `TokenGroup`s ready for rendering.
+ * Groups adjacent word tokens sharing the same approved phrase into single {@link TokenGroup}
+ * entries, skipping non-word tokens. Discontiguous phrase members produce separate groups that
+ * share one phrase link. Each group's punctuation is left empty here and filled in when the render
+ * units are built.
*/
export function groupTokens(
tokens: Token[],
@@ -123,20 +109,13 @@ export function groupTokens(
}
/**
- * Walks `tokens` in document order and emits an alternating sequence of phrase groups and link
- * slots. A leading slot is always emitted before the first group and a trailing slot after the
- * last, so punctuation at segment boundaries still renders. Slots between groups always carry both
- * `prevGroup` and `nextGroup`. Unlink icons between tokens within a multi-token phrase are rendered
- * inside `PhraseBox`, not as separate slots here.
- *
- * Punctuation that appears between two word tokens of the same group is stored in the group's
- * `punctuationBetween` array (at the index corresponding to the gap between those tokens) so
- * `PhraseBox` can render it inline between the token chips, rather than pushing it into the
- * following inter-group slot.
+ * Emits an alternating sequence of phrase groups and link slots in document order. A leading slot
+ * always precedes the first group and a trailing slot follows the last, so punctuation at segment
+ * boundaries still renders. Unlink icons between tokens of a multi-token phrase are rendered by the
+ * phrase box itself rather than emitted as slots here.
*
- * @param tokens - Flat token list from the segment or strip.
- * @param tokenGroups - Pre-built phrase groups produced by {@link groupTokens}.
- * @returns An ordered list of render units interleaving groups and slots.
+ * Punctuation falling between two word tokens of the same group is stored on that group instead of
+ * being pushed into the following inter-group slot, so it renders inline between the token chips.
*/
export function buildRenderUnits(tokens: Token[], tokenGroups: TokenGroup[]): RenderUnit[] {
const units: RenderUnit[] = [];
diff --git a/src/utils/verse-ref.ts b/src/utils/verse-ref.ts
index e350c2c9..8a7bc3b1 100644
--- a/src/utils/verse-ref.ts
+++ b/src/utils/verse-ref.ts
@@ -2,11 +2,9 @@ import type { SerializedVerseRef } from '@sillsdev/scripture';
import type { ScriptureRef, Segment } from 'interlinearizer';
/**
- * Whether `ref` and `scrRef` name the same verse, bridging `ScriptureRef`'s `chapter`/`verse` field
- * names to `SerializedVerseRef`'s `chapterNum`/`verseNum`.
+ * Whether the two refs name the same verse, bridging {@link ScriptureRef}'s `chapter`/`verse` field
+ * names to {@link SerializedVerseRef}'s `chapterNum`/`verseNum`.
*
- * @param ref - Verse coordinate in the internal `ScriptureRef` shape.
- * @param scrRef - Verse coordinate in the platform's `SerializedVerseRef` shape.
* @returns `true` when both name the same book, chapter, and verse.
*/
export function isSameVerse(ref: ScriptureRef, scrRef: SerializedVerseRef): boolean {
@@ -16,12 +14,10 @@ export function isSameVerse(ref: ScriptureRef, scrRef: SerializedVerseRef): bool
}
/**
- * Parses the leading verse range out of a verbatim USJ verse label. A plain number (e.g. `"7"`)
- * yields `{ first: 7, last: 7 }`; a hyphenated range (e.g. `"3-4"`) yields `{ first: 3, last: 4 }`.
- * A label that begins with no digits (e.g. an empty or note-only marker) yields `undefined`. Shared
- * by {@link firstVerseNumber} and {@link verseLabelCovers} so the label grammar lives in one place.
+ * Parses the leading verse range out of a verbatim USJ verse label: `"7"` yields `7`–`7` and a
+ * hyphenated range `"3-4"` yields `3`–`4`. A label beginning with no digits (an empty or note-only
+ * marker) yields `undefined`. The one place the label grammar is defined.
*
- * @param verseStartNumber - The verse start's verbatim `number`.
* @returns The label's `first`/`last` endpoints (equal for a plain number), or `undefined` when it
* names no verse.
*/
@@ -38,11 +34,9 @@ function parseVerseLabel(verseStartNumber: string): { first: number; last: numbe
}
/**
- * Parses the first verse number out of a verbatim USJ verse label. A plain number returns itself; a
- * hyphenated range (e.g. `"3-4"`) returns its first endpoint. A label that begins with no digits
- * (e.g. an empty or note-only marker) returns `undefined`.
+ * The first verse number named by a verbatim USJ verse label, or `undefined` when the label names
+ * no verse.
*
- * @param verseStartNumber - The verse start's verbatim `number`.
* @returns The label's leading verse number, or `undefined` when it names none.
*/
export function firstVerseNumber(verseStartNumber: string): number | undefined {
@@ -50,13 +44,9 @@ export function firstVerseNumber(verseStartNumber: string): number | undefined {
}
/**
- * Reports whether `verseStartNumber` (a verbatim USJ verse label, e.g. `"7"` or a range like
- * `"3-4"`) names the verse `verseNum`. A plain number matches on equality; a hyphenated range
- * matches any verse from its first to its last endpoint inclusive. A label that parses to no digits
- * matches nothing.
+ * Whether a verbatim USJ verse label names the given verse. A range matches any verse between its
+ * endpoints inclusive; a label that parses to no digits matches nothing.
*
- * @param verseStartNumber - The verse start's verbatim `number`.
- * @param verseNum - The verse number to test for membership.
* @returns `true` when the label names `verseNum`.
*/
function verseLabelCovers(verseStartNumber: string, verseNum: number): boolean {
@@ -66,16 +56,14 @@ function verseLabelCovers(verseStartNumber: string, verseNum: number): boolean {
}
/**
- * Whether `segment` contains the verse named by `scrRef`. Containment is verse-level and tested
- * against the segment's `verseStarts` — the exact set of covered source verses — rather than a
- * lexicographic `startRef`..`endRef` interval: for a cross-chapter merge (e.g. `1:2`..`2:1`) the
- * interval would over-claim every verse in the start chapter above `2`, whereas the covered-verse
- * set claims only `1:2` and `2:1`. Character anchors are ignored, so every portion of a split verse
- * "contains" it. Used wherever a verse must resolve to its owning segment (navigation, active
- * highlight).
+ * Whether the segment contains the verse named by the ref. Used wherever a verse must resolve to
+ * its owning segment, such as navigation and active-verse highlighting.
+ *
+ * Containment is tested against the segment's covered source verses rather than a lexicographic
+ * start-to-end interval: for a cross-chapter merge (say `1:2`..`2:1`) the interval would over-claim
+ * every verse in the start chapter above `2`. Character anchors are ignored, so every portion of a
+ * split verse contains it.
*
- * @param segment - The segment whose covered verses to test.
- * @param scrRef - Verse coordinate in the platform's `SerializedVerseRef` shape.
* @returns `true` when the segment covers the verse named by `scrRef`.
*/
export function segmentContainsVerse(segment: Segment, scrRef: SerializedVerseRef): boolean {
@@ -86,11 +74,8 @@ export function segmentContainsVerse(segment: Segment, scrRef: SerializedVerseRe
}
/**
- * Converts an internal `ScriptureRef` to the platform's `SerializedVerseRef` shape, dropping any
- * character anchor.
- *
- * @param ref - Verse coordinate in the internal `ScriptureRef` shape.
- * @returns The same verse coordinate as a `SerializedVerseRef`.
+ * Converts an internal {@link ScriptureRef} to the platform's {@link SerializedVerseRef} shape,
+ * dropping any character anchor.
*/
export function toSerializedVerseRef(ref: ScriptureRef): SerializedVerseRef {
return { book: ref.book, chapterNum: ref.chapter, verseNum: ref.verse };
diff --git a/src/utils/verse-superscripts.ts b/src/utils/verse-superscripts.ts
index 9388a166..430aa697 100644
--- a/src/utils/verse-superscripts.ts
+++ b/src/utils/verse-superscripts.ts
@@ -1,28 +1,11 @@
-/**
- * @file Pure helper deriving inline verse-superscript labels for a (re)segmented book.
- *
- * Verse numbers render as inline superscripts sourced verbatim from the USJ verse marker (carried
- * on `Segment.verseStarts`). This module decides, over the whole book in document order, which
- * verse starts open a new chapter — those get a `chapter:number` qualifier; all others render
- * their bare verbatim number. (The pinned "Book N" chapter header is a separate scroll-driven
- * overlay in the list, not derived here.)
- *
- * The chapter of a verse start is read from `VerseStart.chapter`, not from a token, so the
- * qualifier triggers off "chapter exceeds every chapter seen so far" regardless of where segment
- * boundaries fall — correct even for an empty verse or one whose baseline begins with
- * whitespace.
- */
import type { Segment, Token, VerseStart } from 'interlinearizer';
import type { LinkSlot } from '../types/token-layout';
/**
- * Finds the token that renders a verse start — the first token whose `charStart` is at or after the
- * verse start's offset. Not an exact match: a verse whose baseline begins with whitespace has its
- * first token a few characters in, which a strict `===` would miss. Returns `undefined` for an
- * empty verse, which has no token to carry the superscript.
+ * Finds the token that renders a verse start. Deliberately not an exact offset match: a verse whose
+ * baseline begins with whitespace has its first token a few characters in. An empty verse has no
+ * token to carry the superscript and yields `undefined`.
*
- * @param segment - The segment the verse start belongs to.
- * @param vs - The verse start whose leading token is wanted.
* @returns The first token at or after the verse start's offset, or `undefined` when none exists.
*/
export function verseStartToken(segment: Segment, vs: VerseStart): Token | undefined {
@@ -31,13 +14,13 @@ export function verseStartToken(segment: Segment, vs: VerseStart): Token | undef
/**
* Builds each segment's inline verse-superscript labels over the whole book in document order,
- * chapter-qualifying the label of every verse start that opens a new chapter.
- *
- * A verse start opens a new chapter when its `chapter` exceeds the highest chapter seen so far; its
- * label becomes `chapter:number`. Every other verse start keeps its bare verbatim number.
+ * keyed by segment id and parallel by index to that segment's verse starts. A verse start opening a
+ * new chapter is qualified as `chapter:number`; every other keeps its bare verbatim number.
*
- * @param segments - The book's segments in document order.
- * @returns Map from segment id to the parallel-by-index labels for that segment's `verseStarts`.
+ * Chapter is read from the verse start itself rather than from a token, so qualification stays
+ * correct regardless of where segment boundaries fall — including for an empty verse or one whose
+ * baseline opens with whitespace. The pinned chapter header is a separate scroll-driven overlay,
+ * not derived here.
*/
export function buildVerseStartLabels(segments: readonly Segment[]): Map {
const labelsBySegmentId = new Map();
@@ -56,21 +39,16 @@ export function buildVerseStartLabels(segments: readonly Segment[]): Map {
const labelsBySegmentId = buildVerseStartLabels(segments);