Skip to content
Merged
Show file tree
Hide file tree
Changes from 53 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
f2731b1
Add UI for linking phrases together
alex-rawlings-yyc May 28, 2026
44b2566
Adjustments to line styles
alex-rawlings-yyc May 28, 2026
461c3d2
Add link buttons and improve styling (TESTS NOT UPDATED YET)
alex-rawlings-yyc May 28, 2026
56ed244
Rework focusRef, update tests
alex-rawlings-yyc May 28, 2026
3422293
Symmetrize token ref focus
alex-rawlings-yyc Jun 1, 2026
5413140
Hoist out phrase strip parts, improve split button handling
alex-rawlings-yyc Jun 1, 2026
69fc0f1
Add useArcPaths hook
alex-rawlings-yyc Jun 1, 2026
456b8bd
Improve split border colour clarity
alex-rawlings-yyc Jun 1, 2026
2833e40
Add useArcSplitHandler hook
alex-rawlings-yyc Jun 1, 2026
338010d
Update/add tests
alex-rawlings-yyc Jun 1, 2026
8fc5fce
Removed `--omit=optional` from Install extension dependencies step fr…
alex-rawlings-yyc Jun 1, 2026
e5c4d87
Refactor phrase lookup to use id-keyed map for O(1) access
alex-rawlings-yyc Jun 1, 2026
b9fdedd
Add clarifying comment about `--omit-optional` omission
alex-rawlings-yyc Jun 1, 2026
173630b
Minor adjustments, test coverage improvements
alex-rawlings-yyc Jun 2, 2026
424a4b2
Minor improvements
alex-rawlings-yyc Jun 2, 2026
2db7e0f
Improve handling of inter-row arcs, other arc-related bugs and nits
alex-rawlings-yyc Jun 2, 2026
87635c6
Consolidate duplicated code
alex-rawlings-yyc Jun 2, 2026
4b01deb
Add usePhraseHoverState test suite
alex-rawlings-yyc Jun 2, 2026
0d562b5
Add PhraseStripContext
alex-rawlings-yyc Jun 2, 2026
c810afc
Add PhraseStrip component and bundle props
alex-rawlings-yyc Jun 2, 2026
7e709cd
Set static control pill location relative to phrase box, clean up com…
alex-rawlings-yyc Jun 2, 2026
924187e
Add regions
alex-rawlings-yyc Jun 2, 2026
9f69484
Add tokenDocOrder prop, fix Set mutation, correct test assertions, ex…
alex-rawlings-yyc Jun 2, 2026
8a5c442
Delete empty PhraseAnalysis automatically, tokenDocOrder only conside…
alex-rawlings-yyc Jun 2, 2026
f95f09a
Eliminate unnecessary abstraction, simplify computeAllArcPaths
alex-rawlings-yyc Jun 3, 2026
c9f810c
Move edit/unlink controls to new confirm bar, fix edit mode bug, re-d…
alex-rawlings-yyc Jun 3, 2026
6869fdd
Fix SegmentView split button not working, improve gap calculation, cl…
alex-rawlings-yyc Jun 3, 2026
58e3c98
Prevent 1-token phrases being created from edit mode
alex-rawlings-yyc Jun 3, 2026
eb0944c
Minor adjustments
alex-rawlings-yyc Jun 3, 2026
27c9a39
Rework arc drawing and related things
alex-rawlings-yyc Jun 3, 2026
678f7e5
Cleanup
alex-rawlings-yyc Jun 3, 2026
b27dcd1
Minor adjustments
alex-rawlings-yyc Jun 3, 2026
66842a4
Minor adjustments
alex-rawlings-yyc Jun 4, 2026
1270d2a
Hardenings for #91 (#93)
imnasnainaec Jun 4, 2026
b3b0f3a
Do minor phrase-arc cleanup (#95)
imnasnainaec Jun 4, 2026
74feda8
Fix onFocusPhrase breaking PhraseBox memoization
alex-rawlings-yyc Jun 4, 2026
ceba5f7
Improve visibility in light mode
alex-rawlings-yyc Jun 4, 2026
6fdf023
Add toggles for phrase controls and options dropdown, subdivide compo…
alex-rawlings-yyc Jun 4, 2026
eaf0908
Fix punctuation rendering issues
alex-rawlings-yyc Jun 4, 2026
b4c67d1
Make SegmentView background clickable, shore up test coverage
alex-rawlings-yyc Jun 4, 2026
94c35ee
Reposition options dropdown on resize
alex-rawlings-yyc Jun 4, 2026
094be47
Minor adjustments
alex-rawlings-yyc Jun 4, 2026
577fac8
Minor adjustments
alex-rawlings-yyc Jun 5, 2026
d114540
Improve AGENTS.md
alex-rawlings-yyc Jun 5, 2026
72294f5
Rename window* in ContinuousView to renderWindow*, improve render win…
alex-rawlings-yyc Jun 5, 2026
abf1ee2
Standardize spelling
alex-rawlings-yyc Jun 5, 2026
e18d413
Add REVIEW.md
alex-rawlings-yyc Jun 5, 2026
fd1097b
Suggested cleanup on #91 (#99)
imnasnainaec Jun 5, 2026
88816da
Fix arc misalignment issue when out-of-segment links hidden
alex-rawlings-yyc Jun 5, 2026
ecf4eb0
Add animation to make things smoother when out-of-segment links are h…
alex-rawlings-yyc Jun 5, 2026
18033f7
Fix issue with inputs not focusing their token, add animation to Segm…
alex-rawlings-yyc Jun 5, 2026
c953ea6
Minor adjustment
alex-rawlings-yyc Jun 5, 2026
e9f4e48
Minor adjustments
alex-rawlings-yyc Jun 5, 2026
eae720d
Fix arc alignment, punctuation routing, and link-button polish; extra…
imnasnainaec Jun 10, 2026
b0ef9fc
Minor adjustments
alex-rawlings-yyc Jun 10, 2026
8fb1317
bugfix and cleanup: opacity hide, doc cleanup, helper functions (#101)
imnasnainaec Jun 11, 2026
bb9ae8d
Fix test failure
alex-rawlings-yyc Jun 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ jobs:

- name: Install extension dependencies
working-directory: extension-repo
run: npm ci --ignore-scripts --omit=optional
# Cannot --omit=optional: @swc/core and lightningcss ship native binaries as optional deps
# required by the build toolchain (SWC for transpilation, lightningcss for Tailwind CSS).
run: npm ci --ignore-scripts

- name: Install core dependencies
working-directory: paranext-core
Expand Down
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
{
"css.customData": [".vscode/tailwindcss.json"],
"css.validate": false,

"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.rulers": [100],
"editor.wordWrapColumn": 100,

"eslint.validate": ["html", "javascript", "javascriptreact", "typescript", "typescriptreact"],
"stylelint.validate": ["css", "less", "postcss", "scss", "tailwindcss"],
"tailwindCSS.validate": false,

"files.associations": {
"*.css": "tailwindcss",
Expand Down
14 changes: 12 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# AGENTS.md

This file provides guidance to AI agents when working with code in this repository.
This file provides guidance to AI agents creating and editing code in this repository.

Agents **reviewing** code should also read [REVIEW.md](REVIEW.md), which documents existing conventions that commonly trigger false-positive findings.

## Commands

Expand Down Expand Up @@ -143,7 +145,15 @@ Every function and method — exported or internal — must have a JSDoc block w
- `@returns` describing the return value (omit only for `void`/`Promise<void>`).
- `@throws` for every error condition the caller must handle; omit if the function never throws.

Type declarations (interfaces, type aliases, enums) must have a JSDoc summary on the type itself and on each field or member whose purpose is not self-evident from its name and type.
Type declarations (interfaces, type aliases, enums) must have a JSDoc summary on the type itself and on each field or member whose purpose is not self-evident from its name and type. We document each field individually rather than describing the fields in the type-level summary.

## Spelling

Use American English throughout — in code, comments, JSDoc, and documentation:

- `center` not `centre`, `color` not `colour`, `behavior` not `behaviour`
- `canceled`/`canceling` not `cancelled`/`cancelling`, `leveled`/`leveling` not `levelled`/`levelling`
- `neighboring` not `neighbouring`, `favor` not `favour`, `signaled` not `signalled`

## UX decisions

Expand Down
25 changes: 25 additions & 0 deletions REVIEW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# REVIEW.md

This file provides guidance to AI agents **reviewing** code in this repository. It documents existing conventions that commonly trigger false-positive findings, so reviewers don't flag intentional, already-handled patterns as issues.

Agents creating or editing code should follow [AGENTS.md](AGENTS.md); this file is supplementary and review-specific.

## Tailwind v4 at-rules

Tailwind v4 at-rules (`@utility`, `@apply`, `@theme`, `@config`, `@custom-variant`, `@layer`, `@source`, `@plugin`, etc.) are **already whitelisted** in [.stylelintrc.js](.stylelintrc.js)'s `scss/at-rule-no-unknown` `ignoreAtRules` list. Do **not** flag these as Stylelint violations, and do not suggest adding them to the config or adding `stylelint-disable` comments — they already pass. If you believe a lint rule is firing, run `npm run lint` and cite the actual output rather than inferring it from the rule name.

## Documentation completeness

Type declarations document each field individually rather than describing the fields in the type-level summary. When each field already carries its own JSDoc comment, the documentation is **complete** — do not flag it as inadequate, and do not ask for per-field details to be repeated or summarized in the type-level doc. The type-level summary describes the type as a whole; the per-field comments describe the fields. Only flag a field that is genuinely missing its own comment.

Before reporting any documentation as missing, open the file and confirm the JSDoc is actually absent. Do not infer missing docs from a symbol name, a type signature, or an excerpt — read the declaration.

## Keyboard navigation

Keyboard accessibility is planned but not yet implemented. Do not flag missing `tabIndex` attributes, absent `aria-*` roles, or gaps in focus management as issues — these will be addressed in a dedicated pass once the core interaction model is stable.

## Mock cleanup in tests

[jest.config.ts](jest.config.ts) sets both `resetMocks: true` and `restoreMocks: true`. This means every `jest.spyOn(...)` is automatically restored to its original implementation after each test — tests do **not** need a manual `mockRestore()` or `jest.restoreAllMocks()` in `afterEach` for spies. Do not flag spies as leaking or suggest adding cleanup for them.

Manual cleanup in `afterEach` is only required for state that `restoreMocks` cannot undo, such as plain reassignment of a global (e.g. `global.ResizeObserver = ...`). When you see an `afterEach` restoring only some things, confirm whether the rest are spies (auto-restored) before flagging an omission.
40 changes: 40 additions & 0 deletions __mocks__/lucide-react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,43 @@ export function Info(props: Readonly<{ size?: number; className?: string }>): Re
export function Trash2(props: Readonly<{ size?: number; className?: string }>): ReactElement {
return <svg data-testid="trash2-icon" {...props} />;
}

/**
* Stub for the X icon.
*
* @param props - SVG props forwarded from the component.
* @returns A ReactElement SVG element used as an X icon stub in tests.
*/
export function X(props: Readonly<{ size?: number; className?: string }>): ReactElement {
return <svg data-testid="x-icon" {...props} />;
}

/**
* Stub for the Link2 (link) icon used by the between-token link button.
*
* @param props - SVG props forwarded from the component.
* @returns A ReactElement SVG element used as a link icon stub in tests.
*/
export function Link2(props: Readonly<{ size?: number; className?: string }>): ReactElement {
return <svg data-testid="link2-icon" {...props} />;
}

/**
* Stub for the Link2Off (unlink) icon used by the between-token unlink and arc-split buttons.
*
* @param props - SVG props forwarded from the component.
* @returns A ReactElement SVG element used as an unlink icon stub in tests.
*/
export function Link2Off(props: Readonly<{ size?: number; className?: string }>): ReactElement {
return <svg data-testid="link2off-icon" {...props} />;
}

/**
* Stub for the Settings gear icon used by the view-options dropdown button.
*
* @param props - SVG props forwarded from the component.
* @returns A ReactElement SVG element used as a settings icon stub in tests.
*/
export function Settings(props: Readonly<{ size?: number; className?: string }>): ReactElement {
return <svg data-testid="settings-icon" {...props} />;
}
2 changes: 1 addition & 1 deletion __mocks__/papi-frontend-react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
/**
* Known data-provider method names exposed by this mock. Tests that call an unlisted method will
* receive a descriptive error rather than silently returning `undefined`, which mirrors the real
* PAPI behaviour where requesting an unsupported provider key is a programmer error.
* PAPI behavior where requesting an unsupported provider key is a programmer error.
*/
const KNOWN_PROJECT_DATA_METHODS = new Set(['BookUSJ']);

Expand Down
68 changes: 45 additions & 23 deletions __mocks__/platform-bible-react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* without extra transform configuration. This stub provides the subset used by the extension.
*/

import { forwardRef } from 'react';
import type { ReactElement, ReactNode } from 'react';

export interface MenuItemContainingCommand {
Expand Down Expand Up @@ -188,8 +189,8 @@ export function ScrollGroupSelector({

/**
* Stub button that passes through `children`, `onClick`, `type`, `className`, `disabled`,
* `aria-label`, and forwards them to a native `<button>` element; `variant` and `size` are
* accepted but ignored.
* `aria-label`, `aria-expanded`, `aria-haspopup`, `data-testid`, and `ref` to a native `<button>`
* element; `variant` and `size` are accepted but ignored.
*
* @param props - Component props.
* @param props.children - Button content.
Expand All @@ -199,39 +200,60 @@ export function ScrollGroupSelector({
* @param props.disabled - Whether the button is disabled.
* @param props.variant - Ignored styling variant.
* @param props.size - Ignored size variant.
* @returns A native `<button>` element with `aria-label` forwarded.
* @param props['aria-label'] - Accessible label.
* @param props['aria-expanded'] - Expanded state for popup triggers.
* @param props['aria-haspopup'] - Haspopup attribute.
* @param props['data-testid'] - Test identifier.
* @param ref - Forwarded ref to the underlying button element.
* @returns A native `<button>` element with standard attributes forwarded.
*/
export function Button({
children,
onClick,
type,
className,
disabled,
variant: _variant,
size: _size,
'aria-label': ariaLabel,
}: Readonly<{
children?: ReactNode;
onClick?: () => void;
type?: 'button' | 'submit' | 'reset';
className?: string;
disabled?: boolean;
variant?: 'default' | 'secondary' | 'destructive' | 'ghost' | 'outline' | 'link';
size?: 'default' | 'sm' | 'lg' | 'icon';
'aria-label'?: string;
}>): ReactElement {
export const Button = forwardRef<
HTMLButtonElement,
Readonly<{
children?: ReactNode;
onClick?: () => void;
type?: 'button' | 'submit' | 'reset';
className?: string;
disabled?: boolean;
variant?: 'default' | 'secondary' | 'destructive' | 'ghost' | 'outline' | 'link';
size?: 'default' | 'sm' | 'lg' | 'icon';
'aria-label'?: string;
'aria-expanded'?: boolean;
'aria-haspopup'?: boolean | 'true' | 'false' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog';
'data-testid'?: string;
}>
>(function ButtonImpl(
{
children,
onClick,
type,
className,
disabled,
variant: _variant,
size: _size,
'aria-label': ariaLabel,
'aria-expanded': ariaExpanded,
'aria-haspopup': ariaHaspopup,
'data-testid': testId,
},
ref,
) {
return (
<button
ref={ref}
type={type ?? 'button'}
onClick={onClick}
className={className}
aria-label={ariaLabel}
aria-expanded={ariaExpanded}
aria-haspopup={ariaHaspopup}
data-testid={testId}
disabled={disabled}
>
{children}
</button>
);
}
});

/**
* Stub book/chapter control that displays the current reference as text and exposes a single
Expand Down
9 changes: 8 additions & 1 deletion contributions/localizedStrings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@
"%interlinearizer_projectSettings_title%": "Interlinearizer",
"%interlinearizer_projectSettings_continuousScroll%": "Continuous Scroll",
"%interlinearizer_projectSettings_continuousScrollDescription%": "Display tokens in a continuous horizontal scroll strip instead of chapter-segmented rows",
"%interlinearizer_continuousScrollToggle%": "Continuous Scroll",
"%interlinearizer_viewOption_continuousScroll%": "Continuous Scroll",
"%interlinearizer_viewOption_hideInactiveLinkButtons%": "Hide out-of-segment link buttons",
"%interlinearizer_viewOption_simplifyPhrases%": "Show phrase controls on focus only",
"%interlinearizer_projectSettings_hideInactiveLinkButtons%": "Hide Out-of-Segment Link Buttons",
"%interlinearizer_projectSettings_hideInactiveLinkButtonsDescription%": "Hide link buttons between phrases in segments that are not currently active",
"%interlinearizer_projectSettings_simplifyPhrases%": "Show Phrase Controls on Focus Only",
"%interlinearizer_projectSettings_simplifyPhrasesDescription%": "Hide interactive controls (split, unlink, remove-token) on phrases that are not currently focused, leaving only their style change on hover",
"%interlinearizer_linkButton_crossSegmentDisabledTooltip%": "Cross-segment phrases are not yet supported. This link button is outside the current segment.",

"%interlinearizer_modal_create_title%": "Create Interlinear Project",
"%interlinearizer_modal_create_name_label%": "Name (optional)",
Expand Down
10 changes: 10 additions & 0 deletions contributions/projectSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@
"label": "%interlinearizer_projectSettings_continuousScroll%",
"description": "%interlinearizer_projectSettings_continuousScrollDescription%",
"default": true
},
"interlinearizer.hideInactiveLinkButtons": {
"label": "%interlinearizer_projectSettings_hideInactiveLinkButtons%",
"description": "%interlinearizer_projectSettings_hideInactiveLinkButtonsDescription%",
"default": false
},
"interlinearizer.simplifyPhrases": {
"label": "%interlinearizer_projectSettings_simplifyPhrases%",
"description": "%interlinearizer_projectSettings_simplifyPhrasesDescription%",
"default": false
}
}
}
Expand Down
52 changes: 22 additions & 30 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,57 +13,48 @@
"dictionaryDefinitions": [],
"dictionaries": [],
"words": [
"affordances",
"appdata",
"asyncs",
"autodocs",
"bara",
"BBCCCVVV",
"Discontiguous",
"dockbox",
"deconflict",
"deconfliction",
"deconflicts",
"discontig",
"discontiguous",
"eflomal",
"electronmon",
"elohim",
"endregion",
"eten",
"finalizer",
"Fragmenter",
"guids",
"hopkinson",
"fontsource",
"hoverable",
"iframes",
"imte",
"interlinearization",
"interlinearize",
"interlinearizer",
"interlinearizing",
"labelable",
"lightningcss",
"localstorage",
"maximizable",
"morphosyntactic",
"networkable",
"Newtonsoft",
"nodebuffer",
"nums",
"okina",
"papi",
"papis",
"paranext",
"paratext",
"pdpf",
"pdps",
"plusplus",
"proxied",
"Punct",
"recalc",
"reinitializing",
"reserialized",
"punct",
"relayout",
"sandboxed",
"scriptio",
"scrollers",
"shadcn",
"sillsdev",
"steenwyk",
"stringifiable",
"Struc",
"struc",
"Stylesheet",
"typedefs",
"unanalyzed",
"unregistering",
"unregisters",
"unhover",
"unphrased",
"unreviewed",
"unsub",
"unsubs",
Expand All @@ -72,8 +63,9 @@
"usfm",
"verseref",
"versification",
"Wordform"
"wordform",
"ZWNJ"
],
"ignoreWords": [],
"ignoreWords": ["Ελληνικά", "homme", "ʼelohim", "ʻokina"],
"import": []
}
7 changes: 3 additions & 4 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ const config: Config = {
'!src/**/*.test.{ts,tsx}',
'!src/**/*.spec.{ts,tsx}',
'!src/types/**',
'!src/components/component-types.ts',
],

/** Directory for coverage output. */
Expand Down Expand Up @@ -108,8 +107,8 @@ const config: Config = {
/** Exclude dist from module resolution to avoid Haste naming collision with root package.json. */
modulePathIgnorePatterns: ['<rootDir>/dist'],

/** Load @testing-library/jest-dom matchers for React component tests. */
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
/** Load @testing-library/jest-dom matchers and browser API stubs for React component tests. */
setupFilesAfterEnv: ['<rootDir>/jest.setup.resize-observer.js', '<rootDir>/jest.setup.ts'],

/** Use jsdom for React component tests; parser tests run fine in jsdom (no DOM use). */
testEnvironment: 'jsdom',
Expand All @@ -128,7 +127,7 @@ const config: Config = {
* ts-jest so other preprocessors can be added later without dropping TS support.
*/
transform: {
'\\.[jt]sx?$': 'ts-jest',
'\\.tsx?$': 'ts-jest',
},
};

Expand Down
5 changes: 5 additions & 0 deletions jest.setup.resize-observer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// jsdom does not implement ResizeObserver; stub it so hooks that use it don't throw.
// Plain JS to avoid TypeScript/ESLint restrictions on type assertions and class rules.
global.ResizeObserver = function ResizeObserver() {
return { observe() {}, unobserve() {}, disconnect() {} };
};
Loading
Loading