From 89feff7502659d29f7259f844626260f0df4a15a Mon Sep 17 00:00:00 2001 From: Stephen Chen Date: Mon, 23 Mar 2026 10:58:08 -0600 Subject: [PATCH 01/26] Commit --- package-lock.json | 26 ++++---------------------- package.json | 2 +- 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2dc9cea6..d772b1d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@electron/notarize": "^2.3.1", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", - "@fingerprintjs/fingerprintjs": "^5.0.1", + "@fingerprintjs/fingerprintjs": "^5.1.0", "@fontsource/roboto": "^5.2.6", "@fortawesome/fontawesome-svg-core": "^7.0.1", "@fortawesome/free-regular-svg-icons": "^7.0.1", @@ -1723,9 +1723,9 @@ } }, "node_modules/@fingerprintjs/fingerprintjs": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@fingerprintjs/fingerprintjs/-/fingerprintjs-5.0.1.tgz", - "integrity": "sha512-KbaeE/rk2WL8MfpRP6jTI4lSr42SJPjvkyrjP3QU6uUDkOMWWYC2Ts1sNSYcegHC8avzOoYTHBj+2fTqvZWQBA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fingerprintjs/fingerprintjs/-/fingerprintjs-5.1.0.tgz", + "integrity": "sha512-8h/CscV3xQ4KSLyXbSK8YFpZ5AaezzHfkl82mn8NJIEWNi1zLfbZSIu7MGGtx4pqa10oejhEk4u0MNutuE63Fw==", "license": "MIT" }, "node_modules/@floating-ui/core": { @@ -15882,24 +15882,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 5b7cf9a8..3ab54f38 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@electron/notarize": "^2.3.1", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", - "@fingerprintjs/fingerprintjs": "^5.0.1", + "@fingerprintjs/fingerprintjs": "^5.1.0", "@fontsource/roboto": "^5.2.6", "@fortawesome/fontawesome-svg-core": "^7.0.1", "@fortawesome/free-regular-svg-icons": "^7.0.1", From 8b05aafd778dd6a336049b82dcd07732ceefdaf0 Mon Sep 17 00:00:00 2001 From: Stephen Chen Date: Mon, 23 Mar 2026 13:27:49 -0600 Subject: [PATCH 02/26] REF fix --- src/renderer/src/components/Sheet/PassageCard.tsx | 4 ++-- src/renderer/src/components/Sheet/PassageRef.tsx | 15 ++++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/renderer/src/components/Sheet/PassageCard.tsx b/src/renderer/src/components/Sheet/PassageCard.tsx index 53565def..bcd7be47 100644 --- a/src/renderer/src/components/Sheet/PassageCard.tsx +++ b/src/renderer/src/components/Sheet/PassageCard.tsx @@ -68,7 +68,7 @@ export function PassageCard(props: IProps) { ) : ( @@ -89,7 +89,7 @@ export function PassageCard(props: IProps) { )} diff --git a/src/renderer/src/components/Sheet/PassageRef.tsx b/src/renderer/src/components/Sheet/PassageRef.tsx index e5d7bc59..18c24652 100644 --- a/src/renderer/src/components/Sheet/PassageRef.tsx +++ b/src/renderer/src/components/Sheet/PassageRef.tsx @@ -9,11 +9,16 @@ import { planSheetSelector } from '../../selector'; interface PassageRefProps { psgType: PassageTypeEnum; book?: string; - ref?: string; + passageRef?: string; comment?: string; } -export function PassageRef({ psgType, book, ref, comment }: PassageRefProps) { +export function PassageRef({ + psgType, + book, + passageRef, + comment, +}: PassageRefProps) { const ctx = useContext(PlanContext); const t: IPlanSheetStrings = useSelector(planSheetSelector, shallowEqual); const bookMap = useSelector((state: IState) => state.books.map); @@ -33,10 +38,10 @@ export function PassageRef({ psgType, book, ref, comment }: PassageRefProps) { return ( {psgType === PassageTypeEnum.PASSAGE ? ( - `${fullBookName} ${ref}` - ) : ref ? ( + `${fullBookName} ${passageRef}` + ) : passageRef ? ( <> - + {psgType === PassageTypeEnum.CHAPTERNUMBER && comment ? ` ${comment}` : null} From 2c681e69f63fe565d00085250023d1d1c3698226 Mon Sep 17 00:00:00 2001 From: Stephen Chen Date: Mon, 23 Mar 2026 14:50:06 -0600 Subject: [PATCH 03/26] Verse Marking fixed --- .../PassageDetailMarkVersesIsMobile.test.tsx | 24 ++++++++++++++ .../PassageDetailMarkVersesIsMobile.tsx | 9 ++++++ src/renderer/src/crud/useWaveSurfer.tsx | 31 +++++++++++++++++-- src/renderer/src/routes/PassageDetail.tsx | 10 ++++-- 4 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.test.tsx create mode 100644 src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx diff --git a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.test.tsx b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.test.tsx new file mode 100644 index 00000000..11857610 --- /dev/null +++ b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.test.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import PassageDetailMarkVersesIsMobile from './PassageDetailMarkVersesIsMobile'; + +const mockDesktop = jest.fn(() =>
); + +jest.mock('../../PassageDetailMarkVerses', () => ({ + __esModule: true, + default: (props: { width: number }) => mockDesktop(props), +})); + +describe('PassageDetailMarkVersesIsMobile', () => { + beforeEach(() => { + mockDesktop.mockClear(); + }); + + it('forwards props to desktop component', () => { + render(); + + expect(mockDesktop).toHaveBeenCalled(); + expect(mockDesktop.mock.calls[0][0]).toEqual({ width: 320 }); + expect(screen.getByTestId('desktop-mark-verses')).toBeInTheDocument(); + }); +}); diff --git a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx new file mode 100644 index 00000000..092d74d8 --- /dev/null +++ b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx @@ -0,0 +1,9 @@ +import PassageDetailMarkVerses, { + MarkVersesProps, +} from '../../PassageDetailMarkVerses'; + +const PassageDetailMarkVersesIsMobile = ({ width }: MarkVersesProps) => ( + +); + +export default PassageDetailMarkVersesIsMobile; diff --git a/src/renderer/src/crud/useWaveSurfer.tsx b/src/renderer/src/crud/useWaveSurfer.tsx index 105daa7e..a2765dc9 100644 --- a/src/renderer/src/crud/useWaveSurfer.tsx +++ b/src/renderer/src/crud/useWaveSurfer.tsx @@ -17,7 +17,7 @@ import { convertToWav } from '../utils/wav'; import { useGlobal } from '../context/useGlobal'; import { maxZoom } from '../components/WSAudioPlayerZoom'; import WaveSurfer from 'wavesurfer.js'; -import { NamedRegions } from '../utils'; +import { NamedRegions, useMobile } from '../utils'; const noop = () => {}; @@ -47,6 +47,7 @@ export function useWaveSurfer( verses?: string, hasSegmentUndo?: boolean ) { + const { isMobile } = useMobile(); const [errorReporter] = useGlobal('errorReporter'); const progressRef = useRef(0); const [Regions, setRegions] = useState(); @@ -73,6 +74,11 @@ export function useWaveSurfer( const loadingRef = useRef(false); const recordingRef = useRef(false); const currentBlobUrlRef = useRef(undefined); + const lastWaveformTapTimeRef = useRef(0); + const lastWaveformTapProgressRef = useRef(undefined); + + const MOBILE_DOUBLE_TAP_MS = 350; + const MOBILE_DOUBLE_TAP_POSITION_SLOP = 0.75; // Create plugins outside of useMemo to ensure they're stable const regionsPlugin = useMemo(() => { @@ -308,10 +314,31 @@ export function useWaveSurfer( wavesurfer.on('click', (/*relativeX: number, relativeY: number*/) => { if (singleRegionOnly) { wsRemoveCurrentRegion(); + return; + } + if (isMobile) { + const now = Date.now(); + const currentProgress = progress(); + const isDoubleTap = + now - lastWaveformTapTimeRef.current <= MOBILE_DOUBLE_TAP_MS && + lastWaveformTapProgressRef.current !== undefined && + Math.abs( + currentProgress - lastWaveformTapProgressRef.current + ) <= MOBILE_DOUBLE_TAP_POSITION_SLOP; + + if (isDoubleTap) { + lastWaveformTapTimeRef.current = 0; + lastWaveformTapProgressRef.current = undefined; + wsAddRegion(); + return; + } + + lastWaveformTapTimeRef.current = now; + lastWaveformTapProgressRef.current = currentProgress; } }); wavesurfer.on('dblclick', (/*relativeX: number, relativeY: number*/) => { - if (!singleRegionOnly) { + if (!singleRegionOnly && !isMobile) { wsAddRegion(); } }); diff --git a/src/renderer/src/routes/PassageDetail.tsx b/src/renderer/src/routes/PassageDetail.tsx index da8e33b9..c9d35ba7 100644 --- a/src/renderer/src/routes/PassageDetail.tsx +++ b/src/renderer/src/routes/PassageDetail.tsx @@ -25,6 +25,7 @@ import PassageDetailPlayer from '../components/PassageDetail/PassageDetailPlayer import PassageDetailRecord from '../components/PassageDetail/PassageDetailRecord'; import PassageDetailItem from '../components/PassageDetail/PassageDetailItem'; import PassageDetailMarkVerses from '../components/PassageDetail/PassageDetailMarkVerses'; +import PassageDetailMarkVersesIsMobile from '../components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile'; import PassageDetailTranscribe from '../components/PassageDetail/PassageDetailTranscribe'; import PassageDetailChooser from '../components/PassageDetail/PassageDetailChooser'; import ConsultantCheck from '../components/PassageDetail/ConsultantCheck'; @@ -347,9 +348,12 @@ const PassageDetailGrids = () => { size={{ xs: 12 }} > - {tool === ToolSlug.Verses && ( - - )} + {tool === ToolSlug.Verses && + (isMobile ? ( + + ) : ( + + ))} {tool === ToolSlug.Transcribe && ( Date: Mon, 23 Mar 2026 15:12:51 -0600 Subject: [PATCH 04/26] cypress fix --- .../PassageDetailMarkVersesIsMobile.cy.tsx | 17 +++++++++++++ .../PassageDetailMarkVersesIsMobile.test.tsx | 24 ------------------- 2 files changed, 17 insertions(+), 24 deletions(-) create mode 100644 src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.cy.tsx delete mode 100644 src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.test.tsx diff --git a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.cy.tsx b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.cy.tsx new file mode 100644 index 00000000..2e31e980 --- /dev/null +++ b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.cy.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import PassageDetailMarkVerses from '../../PassageDetailMarkVerses'; + +type DesktopProps = { + width: number; +}; + +type Props = DesktopProps & { + DesktopComponent?: React.ComponentType; +}; + +export default function PassageDetailMarkVersesIsMobile({ + DesktopComponent = PassageDetailMarkVerses, + ...props +}: Props) { + return ; +} \ No newline at end of file diff --git a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.test.tsx b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.test.tsx deleted file mode 100644 index 11857610..00000000 --- a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import PassageDetailMarkVersesIsMobile from './PassageDetailMarkVersesIsMobile'; - -const mockDesktop = jest.fn(() =>
); - -jest.mock('../../PassageDetailMarkVerses', () => ({ - __esModule: true, - default: (props: { width: number }) => mockDesktop(props), -})); - -describe('PassageDetailMarkVersesIsMobile', () => { - beforeEach(() => { - mockDesktop.mockClear(); - }); - - it('forwards props to desktop component', () => { - render(); - - expect(mockDesktop).toHaveBeenCalled(); - expect(mockDesktop.mock.calls[0][0]).toEqual({ width: 320 }); - expect(screen.getByTestId('desktop-mark-verses')).toBeInTheDocument(); - }); -}); From dd80b2ec7e71e7fda1e2bccbbe7efa41c12dfb4c Mon Sep 17 00:00:00 2001 From: Stephen Chen Date: Tue, 24 Mar 2026 08:30:10 -0600 Subject: [PATCH 05/26] half of table works --- .../MarkVerses/MarkVersesTableIsMobile.tsx | 140 +++ .../PassageDetailMarkVersesIsMobile.cy.tsx | 61 +- .../PassageDetailMarkVersesIsMobile.test.tsx | 279 ++++++ .../PassageDetailMarkVersesIsMobile.tsx | 803 +++++++++++++++++- 4 files changed, 1265 insertions(+), 18 deletions(-) create mode 100644 src/renderer/src/components/PassageDetail/mobile/MarkVerses/MarkVersesTableIsMobile.tsx create mode 100644 src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.test.tsx diff --git a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/MarkVersesTableIsMobile.tsx b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/MarkVersesTableIsMobile.tsx new file mode 100644 index 00000000..9fd575e4 --- /dev/null +++ b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/MarkVersesTableIsMobile.tsx @@ -0,0 +1,140 @@ +import { + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Typography, +} from '@mui/material'; +import type { ChangeEvent, ClipboardEvent } from 'react'; +import type { ICell, ICellChange } from './PassageDetailMarkVersesIsMobile'; + +interface MarkVersesTableIsMobileProps { + data: ICell[][]; + onCellsChanged: (changes: Array) => void; + onParsePaste: (clipboard: string) => any[]; +} + +enum ColName { + Limits, + Ref, +} + +export default function MarkVersesTableIsMobile({ + data, + onCellsChanged, + onParsePaste, +}: MarkVersesTableIsMobileProps) { + const rows = data.slice(1); + const header = data[0] ?? []; + + const handleReferenceChange = (rowIndex: number, value: string) => { + onCellsChanged([ + { + cell: null, + row: rowIndex + 1, + col: ColName.Ref, + value, + }, + ]); + }; + + const handleReferencePaste = ( + event: ClipboardEvent, + rowIndex: number + ) => { + const clipboard = event.clipboardData.getData('text'); + const parsed = onParsePaste(clipboard); + + if (!parsed.length) return; + + event.preventDefault(); + + const changes = parsed.map((entry: string[] | string, offset: number) => ({ + cell: null, + row: rowIndex + offset + 1, + col: ColName.Ref, + value: Array.isArray(entry) ? entry[0] : entry, + })); + + onCellsChanged(changes); + }; + + const handleInputChange = + (rowIndex: number) => + (event: ChangeEvent) => { + handleReferenceChange(rowIndex, event.target.value); + }; + + const handlePaste = + (rowIndex: number) => + (event: ClipboardEvent) => { + handleReferencePaste(event, rowIndex); + }; + + return ( + + + + + + {header[ColName.Limits]?.value ?? 'Start-Stop'} + + {header[ColName.Ref]?.value ?? 'Reference'} + + + + + {rows.map((row, index) => { + const limits = row[ColName.Limits] as ICell; + const reference = row[ColName.Ref] as ICell; + const current = limits.className?.includes('cur'); + const invalid = reference.className?.includes('Err'); + + return ( + + + + {limits.value || '-'} + + + + + + + + ); + })} + +
+
+ ); +} \ No newline at end of file diff --git a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.cy.tsx b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.cy.tsx index 2e31e980..874c55c1 100644 --- a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.cy.tsx +++ b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.cy.tsx @@ -1,17 +1,54 @@ import React from 'react'; -import PassageDetailMarkVerses from '../../PassageDetailMarkVerses'; +import { ThemeProvider, createTheme } from '@mui/material/styles'; +import MarkVersesTableIsMobile from './MarkVersesTableIsMobile'; -type DesktopProps = { - width: number; -}; +const sampleData = [ + [ + { value: 'Start-Stop', readOnly: true }, + { value: 'Reference', readOnly: true }, + ], + [ + { value: '0.0-10.0', className: 'lim cur' }, + { value: '2:11', className: 'ref' }, + ], + [ + { value: '10.1-18.9', className: 'lim' }, + { value: '2:12', className: 'ref Err' }, + ], +]; + +const mountTable = () => { + const onCellsChanged = cy.stub(); + const onParsePaste = cy.stub().returns([]); + + cy.wrap(onCellsChanged).as('onCellsChanged'); + cy.wrap(onParsePaste).as('onParsePaste'); -type Props = DesktopProps & { - DesktopComponent?: React.ComponentType; + cy.mount( + + + + ); }; -export default function PassageDetailMarkVersesIsMobile({ - DesktopComponent = PassageDetailMarkVerses, - ...props -}: Props) { - return ; -} \ No newline at end of file +describe('MarkVersesTableIsMobile', () => { + it('renders marker timestamps in a mobile table', () => { + mountTable(); + + cy.contains('Start-Stop').should('be.visible'); + cy.contains('Reference').should('be.visible'); + cy.contains('0.0-10.0').should('be.visible'); + cy.contains('10.1-18.9').should('be.visible'); + }); + + it('updates the selected reference cell', () => { + mountTable(); + + cy.get('input[aria-label="verse-reference-1"]').clear().type('2:15'); + cy.get('@onCellsChanged').should('have.been.called'); + }); +}); \ No newline at end of file diff --git a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.test.tsx b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.test.tsx new file mode 100644 index 00000000..8bab8989 --- /dev/null +++ b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.test.tsx @@ -0,0 +1,279 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import React from 'react'; +import { act } from 'react'; +import { cleanup, render, screen, waitFor } from '@testing-library/react'; +import Coordinator from '@orbit/coordinator'; +import { UnsavedProvider } from '../../../../context/UnsavedContext'; +import { useGetGlobal, useGlobal } from '../../../../context/useGlobal'; +import { AlertSeverity } from '../../../../hoc/SnackBar'; +import { + MediaFileD, + OrgWorkflowStepD, + PassageD, + SectionResourceD, +} from '../../../../model'; +import { RoleNames } from '../../../../model/roleNames'; +import { memory } from '../../../../schema'; +import PassageDetailMarkVersesIsMobile, { + MarkVersesProps, +} from './PassageDetailMarkVersesIsMobile'; +import { DetailPlayerProps } from '../../PassageDetailPlayer'; + +jest.mock('../../../../context/useGlobal', () => ({ + useGlobal: jest.fn(), + useGetGlobal: jest.fn(), +})); + +interface IRow { + id: string; + sequenceNum: number; + version: number; + mediafile: MediaFileD; + playItem: string; + artifactName: string; + artifactType: string; + artifactCategory: string; + done: boolean; + editAction: React.JSX.Element | null; + resource: SectionResourceD | null; + passageId: string; + isVernacular: boolean; + isResource: boolean; + isComment: boolean; + isKeyTerm: boolean; + isText: boolean; + sourceVersion: number; +} + +const mockMemory = memory; +const mockMediafileId = 'm1'; +const mockPassageId = 'p1'; +const mockCurrentStep = 'step1'; +const mockSetCurrentStep = jest.fn(); +let mockPlayerAction: ((segment: string, init: boolean) => void) | undefined; +const mockRowData: IRow[] = []; + +const passageAttributes = { + sequencenum: 1, + book: 'LUK', + reference: '1:1-4', + title: '', + state: 'noMedia', + dateCreated: '2024-05-08T15:37:36.284Z', + dateUpdated: '2024-05-08T15:37:36.284Z', +}; + +const mockPassage = { + id: mockPassageId, + type: 'passage', + attributes: { ...passageAttributes }, + relationships: { + lastModifiedByUser: { + data: { type: 'user', id: 'u1' }, + }, + }, +} as PassageD; + +const mockOrgWorkflowStep = { + id: 'step1', + type: 'orgworkflowstep', + attributes: { + process: 'obt', + name: 'markVerse', + sequencenum: 1, + tool: '{}', + permissions: '{}', + dateCreated: '2024-05-08T15:37:36.284Z', + dateUpdated: '2024-05-08T15:37:36.284Z', + }, + relationships: { + lastModifiedByUser: { + data: { type: 'user', id: 'u1' }, + }, + }, +} as OrgWorkflowStepD; + +jest.mock('../../../../context/usePassageDetailContext', () => () => ({ + mediafileId: mockMediafileId, + passage: mockPassage, + currentstep: mockCurrentStep, + currentSegment: '', + setCurrentStep: mockSetCurrentStep, + orgWorkflowSteps: [mockOrgWorkflowStep], + setupLocate: jest.fn(), + rowData: mockRowData, + section: '', + gotoNextStep: jest.fn(), + setStepComplete: jest.fn(), +})); + +jest.mock('../../../../utils/useStepPermission', () => ({ + useStepPermissions: () => ({ + canDoSectionStep: jest.fn(() => true), + }), +})); + +jest.mock('../../PassageDetailPlayer', () => { + const MockedPassageDetailPlayer = ({ onSegment }: DetailPlayerProps) => { + mockPlayerAction = onSegment; + return
PassageDetailPlayer
; + }; + MockedPassageDetailPlayer.displayName = 'PassageDetailPlayer'; + return MockedPassageDetailPlayer; +}); + +jest.mock('../../../../utils/logErrorService', () => jest.fn()); +jest.mock('../../../../context/GlobalContext', () => ({ + useGlobal: (arg: string) => + arg === 'memory' ? [mockMemory, jest.fn()] : [{}, jest.fn()], + useGetGlobal: () => () => false, +})); + +jest.mock('react-redux', () => ({ + useSelector: () => ({ + availableOnClipboard: 'Available on Clipboard', + cancel: 'Cancel', + canceling: 'Canceling', + cantCopy: "Can't Copy", + clipboard: 'Clipboard', + clipboardCopy: 'Copy to Clipboard', + markVerses: 'Mark Verses', + noData: 'No Data {0}', + pasteFormat: 'Paste Format', + reference: 'Reference', + saveVerseMarkup: 'Save Verse Markup', + startStop: 'Start-Stop', + badReferences: 'ERROR: Markup contains bad references', + btNotUpdated: + 'WARNING: Since back translation recordings already exist, back translation segments will not be updated to line up with verse changes.', + issues: 'The verse markup has issues. Do you want to continue?', + missingReferences: 'Warning: Verses in passage not included: ({0})', + noReferences: 'Warning: Some audio segments will not be included in verses', + noSegments: 'ERROR: Some verses have no segment: ({0})', + outsideReferences: 'ERROR: Some verses are outside passage: ({0})', + }), + shallowEqual: jest.fn(), +})); + +const mockCoordinator = new Coordinator(); +const mockErrorReporter = { + notify: jest.fn(), + _notify: jest.fn(), + leaveBreadcrumb: jest.fn(), + addOnError: jest.fn(), + removeOnError: jest.fn(), + addOnSession: jest.fn(), + removeOnSession: jest.fn(), + startSession: jest.fn(), + pauseSession: jest.fn(), + resumeSession: jest.fn(), + stopSession: jest.fn(), + getContext: jest.fn(), + setContext: jest.fn(), + addContext: jest.fn(), + clearContext: jest.fn(), + setUser: jest.fn(), + clearUser: jest.fn(), + addMetadata: jest.fn(), + clearMetadata: jest.fn(), + addFeatureFlag: jest.fn(), + clearFeatureFlag: jest.fn(), + addFeatureFlags: jest.fn(), + clearFeatureFlags: jest.fn(), + getSession: jest.fn(), + _logger: { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, +} as any; + +const mockGlobalState = { + coordinator: mockCoordinator, + errorReporter: mockErrorReporter, + fingerprint: 'test-fingerprint', + memory: mockMemory, + lang: 'en', + latestVersion: '1.0.0', + loadComplete: true, + offlineOnly: false, + organization: 'test-org', + releaseDate: '2024-01-01', + user: 'test-user', + alertOpen: false, + autoOpenAddMedia: false, + changed: false, + connected: true, + dataChangeCount: 0, + developer: null, + enableOffsite: false, + home: false, + importexportBusy: false, + orbitRetries: 0, + orgRole: undefined as RoleNames | undefined, + plan: '', + progress: 0, + project: '', + projectsLoaded: [], + projType: '', + remoteBusy: false, + saveResult: undefined as string | undefined, + snackAlert: undefined as AlertSeverity | undefined, + snackMessage: <>, + offline: false, +}; + +(useGlobal as jest.Mock).mockImplementation((key: string) => { + if (key === 'memory') return [mockMemory, jest.fn()]; + if (key === 'lang') return ['en', jest.fn()]; + if (key === 'user') return ['test-user', jest.fn()]; + if (key === 'organization') return ['test-org', jest.fn()]; + if (key === 'snackMessage') return [<>, jest.fn()]; + if (key === 'snackAlert') return [undefined, jest.fn()]; + if (key === 'plan') return ['', jest.fn()]; + if (key === 'progress') return [0, jest.fn()]; + return [undefined, jest.fn()]; +}); + +(useGetGlobal as jest.Mock).mockImplementation( + (key: string) => mockGlobalState[key as keyof typeof mockGlobalState] +); + +const runTest = (props: MarkVersesProps) => + render( + + + + ); + +afterEach(() => { + mockPassage.attributes = { ...passageAttributes } as any; + cleanup(); + jest.clearAllMocks(); +}); + +test('updates timestamp rows when the player emits verse markers', async () => { + runTest({ width: 375 }); + + await waitFor(() => { + expect(screen.getByLabelText('verse-reference-1')).toHaveValue('1:1'); + }); + + act(() => { + mockPlayerAction?.( + '{"regions":"[{\\"start\\":0,\\"end\\":10},{\\"start\\":10,\\"end\\":20},{\\"start\\":20,\\"end\\":69}]"}', + false + ); + }); + + await waitFor(() => { + expect(screen.getByText('0.0-10.0')).toBeInTheDocument(); + }); + + expect(screen.getByText('10.0-20.0')).toBeInTheDocument(); + expect(screen.getByText('20.0-69.0')).toBeInTheDocument(); + expect(screen.getByLabelText('verse-reference-1')).toHaveValue('1:1'); + expect(screen.getByLabelText('verse-reference-2')).toHaveValue('1:2'); + expect(screen.getByLabelText('verse-reference-3')).toHaveValue('1:3'); +}); diff --git a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx index 092d74d8..97ad85ed 100644 --- a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx +++ b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx @@ -1,9 +1,800 @@ -import PassageDetailMarkVerses, { - MarkVersesProps, -} from '../../PassageDetailMarkVerses'; +import { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { Box, Paper, SxProps, Typography } from '@mui/material'; +import { shallowEqual, useSelector } from 'react-redux'; +import { useGlobal } from '../../../../context/useGlobal'; +import usePassageDetailContext from '../../../../context/usePassageDetailContext'; +import { UnsavedContext } from '../../../../context/UnsavedContext'; +import { ActionRow } from '../../../../control/ActionRow'; +import { AltButton } from '../../../../control/AltButton'; +import { GrowingSpacer } from '../../../../control/GrowingSpacer'; +import { PriButton } from '../../../../control/PriButton'; +import { passageTypeFromRef } from '../../../../control/passageTypeFromRef'; +import { findRecord } from '../../../../crud/tryFindRecord'; +import { parseRef } from '../../../../crud/passage'; +import { ArtifactTypeSlug } from '../../../../crud/artifactTypeSlug'; +import { useArtifactType } from '../../../../crud/useArtifactType'; +import { usePlanType } from '../../../../crud/usePlanType'; +import { IRegion } from '../../../../crud/useWavesurferRegions'; +import { useSnackBar } from '../../../../hoc/SnackBar'; +import { + ISharedStrings, + ITranscriptionTabStrings, + IVerseStrings, + MediaFileD, + Passage, +} from '../../../../model'; +import { PassageTypeEnum } from '../../../../model/passageType'; +import { + sharedSelector, + transcriptionTabSelector, + verseSelector, +} from '../../../../selector'; +import { cleanClipboard } from '../../../../utils/cleanClipboard'; +import { + getSortedRegions, + NamedRegions, + updateSegments, +} from '../../../../utils/namedSegments'; +import { refMatch } from '../../../../utils/refMatch'; +import { useStepPermissions } from '../../../../utils/useStepPermission'; +import Confirm from '../../../AlertDialog'; +import PassageDetailPlayer from '../../PassageDetailPlayer'; +import { useProjectSegmentSave } from '../../Internalization/useProjectSegmentSave'; +import MarkVersesTableIsMobile from './MarkVersesTableIsMobile'; -const PassageDetailMarkVersesIsMobile = ({ width }: MarkVersesProps) => ( - +const verseToolId = 'VerseTool'; +const paperProps = { p: 2, m: 'auto', width: 'calc(100% - 32px)' } as SxProps; +const readOnlys = [true, false]; +const widths = [150, 150]; +const cClass = ['lim', 'ref']; + +type IVrs = [string, number[]]; + +export interface ICell { + value: any; + readOnly?: boolean; + width?: number; + className?: string; +} + +export interface ICellChange { + cell: any; + row: number; + col: number; + value: string | null; +} + +enum ColName { + Limits, + Ref, +} + +export interface MarkVersesProps { + width: number; +} + +export default function PassageDetailMarkVersesIsMobile({ + width, +}: MarkVersesProps) { + const { + mediafileId, + section, + passage, + currentstep, + currentSegment, + setStepComplete, + gotoNextStep, + rowData, + } = usePassageDetailContext(); + const [memory] = useGlobal('memory'); + const [, setComplete] = useGlobal('progress'); + const [plan] = useGlobal('plan'); + const [data, setDatax] = useState([]); + const [issues, setIssues] = useState([]); + const [confirm, setConfirm] = useState(''); + const [numSegments, setNumSegments] = useState(0); + const [pastedSegments, setPastedSegments] = useState(''); + const [engVrs, setEngVrs] = useState>(new Map()); + const savingRef = useRef(false); + const canceling = useRef(false); + const dataRef = useRef([]); + const segmentsRef = useRef('{}'); + const passageRefs = useRef([]); + const resettingSegmentsRef = useRef(false); + const { canDoSectionStep } = useStepPermissions(); + const hasPermission = canDoSectionStep(currentstep, section); + const { localizedArtifactType } = useArtifactType(); + const t = useSelector(verseSelector, shallowEqual) as IVerseStrings; + const ts = useSelector(sharedSelector, shallowEqual) as ISharedStrings; + const tt = useSelector( + transcriptionTabSelector, + shallowEqual + ) as ITranscriptionTabStrings; + const { + toolChanged, + toolsChanged, + isChanged, + saveRequested, + startSave, + saveCompleted, + clearRequested, + clearCompleted, + checkSavedFn, + waitForSave, + } = useContext(UnsavedContext).state; + const projectSegmentSave = useProjectSegmentSave(); + const { showMessage } = useSnackBar(); + const planType = usePlanType(); + + const isFlat = useMemo(() => planType(plan)?.flat, [plan, planType]); + + const passType = useMemo( + () => passageTypeFromRef(passage?.attributes?.reference, isFlat), + [isFlat, passage] + ); + + useEffect(() => { + import('../../../../assets/eng-vrs').then((module) => { + setEngVrs(new Map(module.default as IVrs[])); + }); + }, []); + + const rowCells = useCallback( + (row: string[], first = false) => + row.map( + (value, index) => + ({ + value, + width: widths[index], + readOnly: first || readOnlys[index], + className: first + ? 'cTitle' + : cClass[index] + + (index === ColName.Ref && value && !refMatch(value) + ? ' Err' + : ''), + }) as ICell + ), + [] + ); + + const emptyTable = () => [rowCells([t.startStop, t.reference], true)]; + + const setData = (newData: ICell[][]) => { + setDatax(newData); + dataRef.current = newData; + }; + + useEffect(() => { + if (dataRef.current.length === 0) { + setData(emptyTable()); + } +}, [t.reference, t.startStop]); + + const tableSignature = (tableData: ICell[][]) => + JSON.stringify( + tableData.map((row) => + row.map((cell) => ({ + value: cell.value ?? '', + className: cell.className ?? '', + readOnly: cell.readOnly ?? false, + })) + ) + ); + + const media = useMemo( + () => findRecord(memory, 'mediafile', mediafileId) as MediaFileD, + [mediafileId, memory] + ); + + const hasBtRecordings = useMemo(() => { + const btType = localizedArtifactType( + ArtifactTypeSlug.PhraseBackTranslation + ); + return rowData.some((row) => row.artifactType === btType); + }, [localizedArtifactType, rowData]); + + const setupData = (items: string[]) => { + passageRefs.current = items; + const newData = emptyTable(); + items.forEach((item) => { + newData.push(rowCells(['', item])); + }); + setData(newData); + if (segmentsRef.current) handleSegment(segmentsRef.current, true); + }; + + const getRefs = useCallback( + (value: string, book: string) => { + const normalized = value + .replace(/[–—]/g, '-') + .replace(/(\d+)\.(\d+)/g, '$1:$2') + .trim(); + + const psg = { + attributes: { + reference: normalized, + book, + }, + } as Passage; + + parseRef(psg); + + const { startChapter, startVerse, endChapter, endVerse } = psg.attributes; + + if (!startChapter || !startVerse) return []; + + const finalChapter = endChapter ?? startChapter; + const finalVerse = endVerse ?? startVerse; + const refs: string[] = []; + + if (startChapter === finalChapter) { + for (let verse = startVerse; verse <= finalVerse; verse += 1) { + refs.push(`${startChapter}:${verse}`); + } + return refs; + } + + for (let chapter = startChapter; chapter <= finalChapter; chapter += 1) { + const fromVerse = chapter === startChapter ? startVerse : 1; + const toVerse = + chapter === finalChapter + ? finalVerse + : (engVrs.get(book) ?? [])[chapter - 1]; + + if (!toVerse) continue; + + for (let verse = fromVerse; verse <= toVerse; verse += 1) { + refs.push(`${chapter}:${verse}`); + } + } + + return refs; + }, + [engVrs] + ); + + const getPassageRefs = useCallback( + (psg?: Passage) => { + if (!psg?.attributes?.book) return []; + + if (psg.attributes.reference) { + const refsFromReference = getRefs( + psg.attributes.reference, + psg.attributes.book + ); + if (refsFromReference.length > 0) return refsFromReference; + } + + const { + book, + startChapter, + startVerse, + endChapter, + endVerse, + } = psg.attributes; + + if (!startChapter || !startVerse) return []; + + const finalChapter = endChapter ?? startChapter; + const finalVerse = endVerse ?? startVerse; + const refs: string[] = []; + + if (startChapter === finalChapter) { + for (let verse = startVerse; verse <= finalVerse; verse += 1) { + refs.push(`${startChapter}:${verse}`); + } + return refs; + } + + for (let chapter = startChapter; chapter <= finalChapter; chapter += 1) { + const fromVerse = chapter === startChapter ? startVerse : 1; + const toVerse = + chapter === finalChapter + ? finalVerse + : (engVrs.get(book) ?? [])[chapter - 1]; + + if (!toVerse) continue; + + for (let verse = fromVerse; verse <= toVerse; verse += 1) { + refs.push(`${chapter}:${verse}`); + } + } + + return refs; + }, + [engVrs, getRefs] ); -export default PassageDetailMarkVersesIsMobile; + useEffect(() => { + const refs = getPassageRefs(passage); + + console.log('passage attributes:', passage?.attributes); + console.log('expanded refs from passage:', refs); + + if (refs.length > 0) { + setupData(refs); + } else if (dataRef.current.length === 0) { + setData(emptyTable()); + } + // setupData is intentionally local to keep the mobile render simple. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getPassageRefs, passage]); + + const handleComplete = (complete: boolean) => { + waitForSave(undefined, 200).finally(async () => { + await setStepComplete(currentstep, complete); + if (complete) gotoNextStep(); + }); + }; + + const writeResources = async () => { + if (!savingRef.current && media) { + savingRef.current = true; + let segments = updateSegments( + NamedRegions.Transcription, + updateSegments( + NamedRegions.Verse, + media.attributes?.segments, + segmentsRef.current + ), + segmentsRef.current + ); + if (!hasBtRecordings) { + segments = updateSegments( + NamedRegions.BackTranslation, + segments, + segmentsRef.current + ); + } + segments = updateSegments(NamedRegions.TRTask, segments, ''); + projectSegmentSave({ media, segments }) + .then(() => { + saveCompleted(verseToolId); + }) + .catch((err) => { + saveCompleted(verseToolId, err.message); + }) + .finally(() => { + savingRef.current = false; + canceling.current = false; + setComplete(0); + handleComplete(true); + }); + } + }; + + const collectRefs = useCallback( + (tableData: ICell[][]) => { + const refs: string[] = []; + tableData + .filter((_, index) => index > 0) + .forEach((row) => { + const value = (row[ColName.Ref] as ICell).value; + if (refMatch(value)) { + refs.push(...getRefs(value, passage.attributes.book)); + } + }); + return refs; + }, + [getRefs, passage.attributes.book] + ); + +const formatTime = (value: number) => { + const minutes = Math.floor(value / 60); + const seconds = value - minutes * 60; + return `${minutes}:${seconds.toFixed(1).padStart(4, '0')}`; +}; + +const parseFormattedTime = (value: string) => { + const trimmed = value.trim(); + + if (trimmed.includes(':')) { + const [minPart, secPart] = trimmed.split(':'); + const minutes = parseInt(minPart, 10); + const seconds = parseFloat(secPart); + + if (Number.isNaN(minutes) || Number.isNaN(seconds)) return NaN; + return minutes * 60 + seconds; + } + + return parseFloat(trimmed); +}; + +const formLim = ({ start, end }: IRegion) => + `${formatTime(start)}-${formatTime(end)}`; + + + + const resetSegments = (regions: IRegion[]) => { + const segments = JSON.stringify({ regions }); + setTimeout(() => { + resettingSegmentsRef.current = true; + setPastedSegments(segments); + }, 40); + }; + + const handleSegment = useCallback( + (segments: string, init: boolean) => { + segmentsRef.current = segments; + + console.log('passage reference:', passage?.attributes?.reference); + console.log('passageRefs.current:', passageRefs.current); + + if (resettingSegmentsRef.current) { + resettingSegmentsRef.current = false; + return; + } + if (!hasPermission && !init) { + toolChanged(verseToolId, false); + return; + } + const regions = getSortedRegions(segments); + const previousData = + dataRef.current.length > 0 ? dataRef.current : emptyTable(); + + setNumSegments(regions.length); + + const newData = [rowCells([t.startStop, t.reference], true)]; + const currentLength = previousData.length; + let reset = false; + + regions.forEach((region, index) => { + const previousRow = + index + 1 < currentLength + ? (previousData[index + 1] as ICell[]) + : undefined; + const previousReference = previousRow?.[ColName.Ref] as + | ICell + | undefined; + let nextReference = `${previousReference?.value ?? ''}`; + + if (!nextReference && passageRefs.current[index]) { + nextReference = passageRefs.current[index]; + } + if (region.label && init) { + const refsSoFar = collectRefs(newData); + if (!refsSoFar.includes(region.label)) { + nextReference = region.label; + } + } else if (region.label !== nextReference) { + region.label = nextReference; + reset = true; + } + + const row = rowCells([formLim(region), nextReference]); + const limits = row[ColName.Limits] as ICell; + const reference = row[ColName.Ref] as ICell; + if (formLim(region) === currentSegment.trim()) { + limits.className = `${limits.className ?? 'lim'} cur`; + } + if (!refMatch(nextReference)) { + reference.className = nextReference ? 'ref Err' : 'ref'; + } + newData.push(row); + }); + + const refs = collectRefs(newData); + previousData.slice(newData.length).forEach((existingRow) => { + const reference = existingRow[ColName.Ref] as ICell; + if (reference.value !== '' && !refs.includes(reference.value)) { + newData.push(rowCells(['', `${reference.value ?? ''}`])); + } + }); + + const change = + numSegments !== regions.length || + tableSignature(previousData) !== tableSignature(newData); + + if (change) { + setData(newData); + if (reset) resetSegments(regions); + if (!init && !isChanged(verseToolId)) toolChanged(verseToolId); + } + }, + [ + collectRefs, + currentSegment, + hasPermission, + isChanged, + numSegments, + rowCells, + t.reference, + t.startStop, + toolChanged, + ] + ); + + const setSegments = () => { + const regions: IRegion[] = []; + dataRef.current.forEach((row, index) => { + if (index === 0) return; + const limits = `${row[ColName.Limits].value}`.split('-'); + if (limits.length === 2) { + regions.push({ + start: parseFloat(limits[0]), + end: parseFloat(limits[1]), + label: row[ColName.Ref].value, + }); + } + }); + resetSegments(regions); + }; + + const handleCellsChanged = (changes: Array) => { + const newData = dataRef.current.map((row) => + row.map((cell) => ({ + ...cell, + })) + ); + + let changed = false; + + changes.forEach((change) => { + const value = change.value?.trim() ?? ''; + const row = newData[change.row]; + if (!row) return; + + const cell = row[change.col] as ICell | undefined; + if (!cell) return; + + if (value !== cell.value) { + changed = true; + + if (change.col === ColName.Ref) { + row[change.col] = { + ...cell, + value, + className: `ref${value && !refMatch(value) ? ' Err' : ''}`, + }; + } else { + row[change.col] = { + ...cell, + value, + }; + } + } + }); + + if (changed) { + setData(newData); + setSegments(); + toolChanged(verseToolId); + } + }; + + const handleParsePaste = (clipboard: string) => { + const rawData = cleanClipboard(clipboard); + if (rawData.length === 0) { + showMessage(tt.noData.replace('{0}', t.clipboard)); + return []; + } + const rawWidth = (rawData[0] as string[]).length; + if (![1, 2].includes(rawWidth)) { + showMessage(t.pasteFormat); + return []; + } + + if (rawWidth === 1) { + toolChanged(verseToolId); + return rawData; + } + + showMessage('TODO: multi-column paste not implemented'); + return []; + }; + + const handleCopy = () => { + const content = dataRef.current + .filter((_, index) => index > 0) + .map( + (row) => + `${(row[ColName.Limits] as ICell).value}\t${ + (row[ColName.Ref] as ICell).value + }` + ) + .join('\n'); + + if (!content.length) { + showMessage(tt.noData.replace('{0}', t.markVerses)); + return; + } + + navigator.clipboard + .writeText(content) + .then(() => { + showMessage(tt.availableOnClipboard); + }) + .catch(() => { + showMessage(ts.cantCopy); + }); + }; + + useEffect(() => { + if (saveRequested(verseToolId) && !savingRef.current) { + writeResources(); + } else if (clearRequested(verseToolId)) { + clearCompleted(verseToolId); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [toolsChanged]); + + const checkRefs = () => { + const refs = collectRefs(dataRef.current); + const noSegRefs = dataRef.current + .filter((_, index) => index > 0) + .filter( + (row) => + (row[ColName.Ref] as ICell).value && + !(row[ColName.Limits] as ICell).value + ) + .map((row) => (row[ColName.Ref] as ICell).value); + const noRefSegs = dataRef.current + .filter((_, index) => index > 0) + .some( + (row) => + !(row[ColName.Ref] as ICell).value && + (row[ColName.Limits] as ICell).value + ); + const matchAll = refs.every((ref) => refMatch(ref)); + const refSet = new Set(passageRefs.current); + const outsideRefs = new Set(); + + refs.forEach((ref) => { + if (refSet.has(ref)) { + refSet.delete(ref); + } else if (refMatch(ref)) { + outsideRefs.add(ref); + } + }); + + const nextIssues: string[] = []; + if (!matchAll) nextIssues.push(t.badReferences); + if (noSegRefs.length > 0) { + nextIssues.push(t.noSegments.replace('{0}', noSegRefs.join(', '))); + } + if (refSet.size > 0) { + nextIssues.push( + t.missingReferences.replace('{0}', Array.from(refSet).sort().join(', ')) + ); + } + if (outsideRefs.size > 0) { + nextIssues.push( + t.outsideReferences.replace('{0}', Array.from(outsideRefs).join(', ')) + ); + } + if (noRefSegs) nextIssues.push(t.noReferences); + if (hasBtRecordings) nextIssues.push(t.btNotUpdated); + return nextIssues; + }; + + const handleCancel = () => { + if (savingRef.current) { + showMessage(t.canceling); + canceling.current = true; + return; + } + checkSavedFn(() => { + toolChanged(verseToolId, false); + if (hasPermission) handleComplete(true); + }); + }; + + const resetSave = () => { + setConfirm(''); + setIssues([]); + }; + + const handleNoIssueSave = () => { + if (!hasPermission) return handleCancel(); + if (!saveRequested(verseToolId)) { + startSave(verseToolId); + } + resetSave(); + }; + + const handleSaveMarkup = () => { + const nextIssues = checkRefs(); + if (nextIssues.length > 0) { + setIssues(nextIssues); + setConfirm(t.issues); + } else { + handleNoIssueSave(); + } + }; + + if (!mediafileId) { + return ( + + + {ts.noAudio} + + + ); + } + + if (passType === PassageTypeEnum.NOTE) { + return ( + + + {ts.notSupported} + + + ); + } + + console.log('MARK VERSES table data:', data); + + return ( + + + + row.map((cell) => ({ + ...cell, + readOnly: true, + })) + ) + } + onCellsChanged={handleCellsChanged} + onParsePaste={handleParsePaste} + /> + + + {ts.clipboardCopy} + + + + {t.saveVerseMarkup} + + + {ts.cancel} + + + {confirm && ( + + {issues.map((issue, index) => ( +
  • {issue}
  • + ))} + + } + text={confirm} + noResponse={resetSave} + yesResponse={handleNoIssueSave} + /> + )} +
    + ); +} \ No newline at end of file From d7ab96894743c39cf1f7cb312c5484948004ef91 Mon Sep 17 00:00:00 2001 From: Stephen Chen Date: Tue, 24 Mar 2026 11:49:16 -0600 Subject: [PATCH 06/26] table fix --- .../src/business/player/usePlayerLogic.ts | 5 +- .../PassageDetailMarkVersesIsMobile.tsx | 162 +++++++++--------- 2 files changed, 83 insertions(+), 84 deletions(-) diff --git a/src/renderer/src/business/player/usePlayerLogic.ts b/src/renderer/src/business/player/usePlayerLogic.ts index 16c97411..f121ed05 100644 --- a/src/renderer/src/business/player/usePlayerLogic.ts +++ b/src/renderer/src/business/player/usePlayerLogic.ts @@ -55,7 +55,10 @@ export const usePlayerLogic = (props: PlayerLogicProps) => { const loadSegments = () => { const segs = mediafileRef.current?.attributes?.segments || '{}'; if (allowSegment) { - segmentsRef.current = getSegments(allowSegment, segs); + segmentsRef.current = + suggestedSegments && suggestedSegments.length > 0 + ? suggestedSegments + : getSegments(allowSegment, segs); setSegmentToWhole(); } setDefaultSegments(segmentsRef.current); diff --git a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx index 97ad85ed..40de3c40 100644 --- a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx +++ b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx @@ -50,6 +50,7 @@ import { useProjectSegmentSave } from '../../Internalization/useProjectSegmentSa import MarkVersesTableIsMobile from './MarkVersesTableIsMobile'; const verseToolId = 'VerseTool'; +const emptySegments = JSON.stringify({ regions: [] }); const paperProps = { p: 2, m: 'auto', width: 'calc(100% - 32px)' } as SxProps; const readOnlys = [true, false]; const widths = [150, 150]; @@ -100,7 +101,7 @@ export default function PassageDetailMarkVersesIsMobile({ const [issues, setIssues] = useState([]); const [confirm, setConfirm] = useState(''); const [numSegments, setNumSegments] = useState(0); - const [pastedSegments, setPastedSegments] = useState(''); + const [pastedSegments, setPastedSegments] = useState(emptySegments); const [engVrs, setEngVrs] = useState>(new Map()); const savingRef = useRef(false); const canceling = useRef(false); @@ -146,6 +147,12 @@ export default function PassageDetailMarkVersesIsMobile({ }); }, []); + useEffect(() => { + segmentsRef.current = emptySegments; + setNumSegments(0); + setPastedSegments(emptySegments); + }, [mediafileId]); + const rowCells = useCallback( (row: string[], first = false) => row.map( @@ -165,7 +172,10 @@ export default function PassageDetailMarkVersesIsMobile({ [] ); - const emptyTable = () => [rowCells([t.startStop, t.reference], true)]; + const emptyTable = useCallback( + () => [rowCells([t.startStop, t.reference], true)], + [rowCells, t.reference, t.startStop] + ); const setData = (newData: ICell[][]) => { setDatax(newData); @@ -173,10 +183,10 @@ export default function PassageDetailMarkVersesIsMobile({ }; useEffect(() => { - if (dataRef.current.length === 0) { - setData(emptyTable()); - } -}, [t.reference, t.startStop]); + if (dataRef.current.length === 0) { + setData(emptyTable()); + } + }, [emptyTable]); const tableSignature = (tableData: ICell[][]) => JSON.stringify( @@ -215,8 +225,9 @@ export default function PassageDetailMarkVersesIsMobile({ (value: string, book: string) => { const normalized = value .replace(/[–—]/g, '-') - .replace(/(\d+)\.(\d+)/g, '$1:$2') - .trim(); + .replace(/\s+/g, ' ') + .trim() + .replace(/^[^\d]*/, ''); const psg = { attributes: { @@ -262,63 +273,52 @@ export default function PassageDetailMarkVersesIsMobile({ ); const getPassageRefs = useCallback( - (psg?: Passage) => { - if (!psg?.attributes?.book) return []; + (psg?: Passage) => { + if (!psg?.attributes) return []; - if (psg.attributes.reference) { - const refsFromReference = getRefs( - psg.attributes.reference, - psg.attributes.book - ); - if (refsFromReference.length > 0) return refsFromReference; - } - - const { - book, - startChapter, - startVerse, - endChapter, - endVerse, - } = psg.attributes; + const book = psg.attributes.book ?? ''; + if (psg.attributes.reference) { + const refsFromReference = getRefs(psg.attributes.reference, book); + if (refsFromReference.length > 0) return refsFromReference; + } - if (!startChapter || !startVerse) return []; + const { startChapter, startVerse, endChapter, endVerse } = psg.attributes; + if (!startChapter || !startVerse) return []; - const finalChapter = endChapter ?? startChapter; - const finalVerse = endVerse ?? startVerse; - const refs: string[] = []; + const finalChapter = endChapter ?? startChapter; + const finalVerse = endVerse ?? startVerse; + const refs: string[] = []; - if (startChapter === finalChapter) { - for (let verse = startVerse; verse <= finalVerse; verse += 1) { - refs.push(`${startChapter}:${verse}`); + if (startChapter === finalChapter) { + for (let verse = startVerse; verse <= finalVerse; verse += 1) { + refs.push(`${startChapter}:${verse}`); + } + return refs; } - return refs; - } - for (let chapter = startChapter; chapter <= finalChapter; chapter += 1) { - const fromVerse = chapter === startChapter ? startVerse : 1; - const toVerse = - chapter === finalChapter - ? finalVerse - : (engVrs.get(book) ?? [])[chapter - 1]; + if (!book) return []; + + for (let chapter = startChapter; chapter <= finalChapter; chapter += 1) { + const fromVerse = chapter === startChapter ? startVerse : 1; + const toVerse = + chapter === finalChapter + ? finalVerse + : (engVrs.get(book) ?? [])[chapter - 1]; - if (!toVerse) continue; + if (!toVerse) continue; - for (let verse = fromVerse; verse <= toVerse; verse += 1) { - refs.push(`${chapter}:${verse}`); + for (let verse = fromVerse; verse <= toVerse; verse += 1) { + refs.push(`${chapter}:${verse}`); + } } - } - return refs; - }, - [engVrs, getRefs] -); + return refs; + }, + [engVrs, getRefs] + ); useEffect(() => { const refs = getPassageRefs(passage); - - console.log('passage attributes:', passage?.attributes); - console.log('expanded refs from passage:', refs); - if (refs.length > 0) { setupData(refs); } else if (dataRef.current.length === 0) { @@ -387,31 +387,16 @@ export default function PassageDetailMarkVersesIsMobile({ [getRefs, passage.attributes.book] ); -const formatTime = (value: number) => { - const minutes = Math.floor(value / 60); - const seconds = value - minutes * 60; - return `${minutes}:${seconds.toFixed(1).padStart(4, '0')}`; -}; - -const parseFormattedTime = (value: string) => { - const trimmed = value.trim(); - - if (trimmed.includes(':')) { - const [minPart, secPart] = trimmed.split(':'); - const minutes = parseInt(minPart, 10); - const seconds = parseFloat(secPart); - - if (Number.isNaN(minutes) || Number.isNaN(seconds)) return NaN; - return minutes * 60 + seconds; - } - - return parseFloat(trimmed); -}; - -const formLim = ({ start, end }: IRegion) => - `${formatTime(start)}-${formatTime(end)}`; + const formatTime = (value: number) => { + const minutes = Math.floor(value / 60); + const seconds = value - minutes * 60; + return `${minutes}:${seconds.toFixed(1).padStart(4, '0')}`; + }; - + const formLim = useCallback( + ({ start, end }: IRegion) => `${formatTime(start)}-${formatTime(end)}`, + [] + ); const resetSegments = (regions: IRegion[]) => { const segments = JSON.stringify({ regions }); @@ -425,9 +410,6 @@ const formLim = ({ start, end }: IRegion) => (segments: string, init: boolean) => { segmentsRef.current = segments; - console.log('passage reference:', passage?.attributes?.reference); - console.log('passageRefs.current:', passageRefs.current); - if (resettingSegmentsRef.current) { resettingSegmentsRef.current = false; return; @@ -437,8 +419,18 @@ const formLim = ({ start, end }: IRegion) => return; } const regions = getSortedRegions(segments); + const autoRefs = + passageRefs.current.length > 0 + ? passageRefs.current + : getPassageRefs(passage); const previousData = - dataRef.current.length > 0 ? dataRef.current : emptyTable(); + dataRef.current.length > 0 + ? dataRef.current + : [emptyTable()[0], ...autoRefs.map((ref) => rowCells(['', ref]))]; + + if (passageRefs.current.length === 0 && autoRefs.length > 0) { + passageRefs.current = autoRefs; + } setNumSegments(regions.length); @@ -456,8 +448,8 @@ const formLim = ({ start, end }: IRegion) => | undefined; let nextReference = `${previousReference?.value ?? ''}`; - if (!nextReference && passageRefs.current[index]) { - nextReference = passageRefs.current[index]; + if (!nextReference && autoRefs[index]) { + nextReference = autoRefs[index]; } if (region.label && init) { const refsSoFar = collectRefs(newData); @@ -504,6 +496,10 @@ const formLim = ({ start, end }: IRegion) => currentSegment, hasPermission, isChanged, + getPassageRefs, + emptyTable, + formLim, + passage, numSegments, rowCells, t.reference, @@ -797,4 +793,4 @@ const formLim = ({ start, end }: IRegion) => )} ); -} \ No newline at end of file +} From ab9048f5c9c440e42d0a75e3a1eccf9fd74832c9 Mon Sep 17 00:00:00 2001 From: Stephen Chen Date: Tue, 24 Mar 2026 12:33:39 -0600 Subject: [PATCH 07/26] table fix --- .../PassageDetailMarkVersesIsMobile.test.tsx | 31 +++++++++- .../PassageDetailMarkVersesIsMobile.tsx | 60 +++++++++++++++---- 2 files changed, 77 insertions(+), 14 deletions(-) diff --git a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.test.tsx b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.test.tsx index 8bab8989..c24ef035 100644 --- a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.test.tsx +++ b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.test.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { act } from 'react'; import { cleanup, render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import Coordinator from '@orbit/coordinator'; import { UnsavedProvider } from '../../../../context/UnsavedContext'; import { useGetGlobal, useGlobal } from '../../../../context/useGlobal'; @@ -50,6 +51,7 @@ const mockMediafileId = 'm1'; const mockPassageId = 'p1'; const mockCurrentStep = 'step1'; const mockSetCurrentStep = jest.fn(); +const mockSetCurrentSegment = jest.fn(); let mockPlayerAction: ((segment: string, init: boolean) => void) | undefined; const mockRowData: IRow[] = []; @@ -98,6 +100,7 @@ jest.mock('../../../../context/usePassageDetailContext', () => () => ({ passage: mockPassage, currentstep: mockCurrentStep, currentSegment: '', + setCurrentSegment: mockSetCurrentSegment, setCurrentStep: mockSetCurrentStep, orgWorkflowSteps: [mockOrgWorkflowStep], setupLocate: jest.fn(), @@ -268,12 +271,34 @@ test('updates timestamp rows when the player emits verse markers', async () => { }); await waitFor(() => { - expect(screen.getByText('0.0-10.0')).toBeInTheDocument(); + expect(screen.getByText('0:00.0-0:10.0')).toBeInTheDocument(); }); - expect(screen.getByText('10.0-20.0')).toBeInTheDocument(); - expect(screen.getByText('20.0-69.0')).toBeInTheDocument(); + expect(screen.getByText('0:10.0-0:20.0')).toBeInTheDocument(); + expect(screen.getByText('0:20.0-1:09.0')).toBeInTheDocument(); expect(screen.getByLabelText('verse-reference-1')).toHaveValue('1:1'); expect(screen.getByLabelText('verse-reference-2')).toHaveValue('1:2'); expect(screen.getByLabelText('verse-reference-3')).toHaveValue('1:3'); }); + +test('highlights the matching waveform region when a row is edited', async () => { + const user = userEvent.setup(); + + runTest({ width: 375 }); + + act(() => { + mockPlayerAction?.( + '{"regions":"[{\\"start\\":0,\\"end\\":10},{\\"start\\":10,\\"end\\":20},{\\"start\\":20,\\"end\\":69}]"}', + false + ); + }); + + const secondReference = await screen.findByLabelText('verse-reference-2'); + await user.clear(secondReference); + await user.type(secondReference, '1:2a'); + + expect(mockSetCurrentSegment).toHaveBeenLastCalledWith( + expect.objectContaining({ start: 10, end: 20 }), + 1 + ); +}); diff --git a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx index 40de3c40..871cbf2d 100644 --- a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx +++ b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx @@ -90,6 +90,7 @@ export default function PassageDetailMarkVersesIsMobile({ passage, currentstep, currentSegment, + setCurrentSegment, setStepComplete, gotoNextStep, rowData, @@ -393,11 +394,34 @@ export default function PassageDetailMarkVersesIsMobile({ return `${minutes}:${seconds.toFixed(1).padStart(4, '0')}`; }; + const parseFormattedTime = (value: string) => { + const trimmed = value.trim(); + if (!trimmed) return NaN; + if (trimmed.includes(':')) { + const [minPart, secPart] = trimmed.split(':'); + const minutes = parseInt(minPart, 10); + const seconds = parseFloat(secPart); + if (Number.isNaN(minutes) || Number.isNaN(seconds)) return NaN; + return minutes * 60 + seconds; + } + return parseFloat(trimmed); + }; + const formLim = useCallback( ({ start, end }: IRegion) => `${formatTime(start)}-${formatTime(end)}`, [] ); + const getSegmentFromRow = useCallback((row?: ICell[]) => { + if (!row) return undefined; + const limits = `${row[ColName.Limits]?.value ?? ''}`.split('-'); + if (limits.length !== 2) return undefined; + const start = parseFormattedTime(limits[0]); + const end = parseFormattedTime(limits[1]); + if (Number.isNaN(start) || Number.isNaN(end)) return undefined; + return { start, end } as IRegion; + }, []); + const resetSegments = (regions: IRegion[]) => { const segments = JSON.stringify({ regions }); setTimeout(() => { @@ -512,14 +536,12 @@ export default function PassageDetailMarkVersesIsMobile({ const regions: IRegion[] = []; dataRef.current.forEach((row, index) => { if (index === 0) return; - const limits = `${row[ColName.Limits].value}`.split('-'); - if (limits.length === 2) { - regions.push({ - start: parseFloat(limits[0]), - end: parseFloat(limits[1]), - label: row[ColName.Ref].value, - }); - } + const segment = getSegmentFromRow(row); + if (!segment) return; + regions.push({ + ...segment, + label: row[ColName.Ref].value, + }); }); resetSegments(regions); }; @@ -532,6 +554,7 @@ export default function PassageDetailMarkVersesIsMobile({ ); let changed = false; + let activeRowIndex = -1; changes.forEach((change) => { const value = change.value?.trim() ?? ''; @@ -543,6 +566,7 @@ export default function PassageDetailMarkVersesIsMobile({ if (value !== cell.value) { changed = true; + activeRowIndex = change.row; if (change.col === ColName.Ref) { row[change.col] = { @@ -560,8 +584,25 @@ export default function PassageDetailMarkVersesIsMobile({ }); if (changed) { + newData.forEach((row, index) => { + if (index === 0) return; + const limits = row[ColName.Limits] as ICell; + limits.className = limits.className?.replace(/\s*cur\b/g, '') || 'lim'; + }); + + const activeRow = + activeRowIndex > 0 ? (newData[activeRowIndex] as ICell[]) : undefined; + const activeSegment = getSegmentFromRow(activeRow); + if (activeRow && activeSegment) { + const limits = activeRow[ColName.Limits] as ICell; + limits.className = `${limits.className ?? 'lim'} cur`.trim(); + } + setData(newData); setSegments(); + if (activeSegment) { + setCurrentSegment(activeSegment, activeRowIndex - 1); + } toolChanged(verseToolId); } }; @@ -725,9 +766,6 @@ export default function PassageDetailMarkVersesIsMobile({ ); } - - console.log('MARK VERSES table data:', data); - return ( Date: Tue, 24 Mar 2026 12:35:21 -0600 Subject: [PATCH 08/26] remove highlight --- .../mobile/MarkVerses/MarkVersesTableIsMobile.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/MarkVersesTableIsMobile.tsx b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/MarkVersesTableIsMobile.tsx index 9fd575e4..073ab35a 100644 --- a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/MarkVersesTableIsMobile.tsx +++ b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/MarkVersesTableIsMobile.tsx @@ -96,14 +96,10 @@ export default function MarkVersesTableIsMobile({ {rows.map((row, index) => { const limits = row[ColName.Limits] as ICell; const reference = row[ColName.Ref] as ICell; - const current = limits.className?.includes('cur'); const invalid = reference.className?.includes('Err'); return ( - + ); -} \ No newline at end of file +} From dc9ce36298d044d266c3076f6b347e88662d6147 Mon Sep 17 00:00:00 2001 From: Stephen Chen Date: Tue, 24 Mar 2026 12:36:37 -0600 Subject: [PATCH 09/26] fix --- .../PassageDetail/mobile/MarkVerses/MarkVersesTableIsMobile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/MarkVersesTableIsMobile.tsx b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/MarkVersesTableIsMobile.tsx index 073ab35a..cde9d6fe 100644 --- a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/MarkVersesTableIsMobile.tsx +++ b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/MarkVersesTableIsMobile.tsx @@ -75,7 +75,7 @@ export default function MarkVersesTableIsMobile({ handleReferencePaste(event, rowIndex); }; - return ( + return ( Date: Tue, 24 Mar 2026 14:20:36 -0600 Subject: [PATCH 10/26] editreferences and reset --- .../PassageDetailMarkVersesIsMobile.test.tsx | 65 +++++++++++++- .../PassageDetailMarkVersesIsMobile.tsx | 86 ++++++++++++++++++- src/renderer/src/store/localization/model.tsx | 3 + .../src/store/localization/reducers.tsx | 3 + 4 files changed, 151 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.test.tsx b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.test.tsx index c24ef035..8da8b5a9 100644 --- a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.test.tsx +++ b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.test.tsx @@ -140,10 +140,13 @@ jest.mock('react-redux', () => ({ cantCopy: "Can't Copy", clipboard: 'Clipboard', clipboardCopy: 'Copy to Clipboard', + doneEditingReference: 'Done Editing', + editReference: 'Edit Reference', markVerses: 'Mark Verses', noData: 'No Data {0}', pasteFormat: 'Paste Format', reference: 'Reference', + reset: 'Reset', saveVerseMarkup: 'Save Verse Markup', startStop: 'Start-Stop', badReferences: 'ERROR: Markup contains bad references', @@ -293,12 +296,66 @@ test('highlights the matching waveform region when a row is edited', async () => ); }); + await user.click(screen.getByRole('button', { name: 'Edit Reference' })); const secondReference = await screen.findByLabelText('verse-reference-2'); await user.clear(secondReference); await user.type(secondReference, '1:2a'); - expect(mockSetCurrentSegment).toHaveBeenLastCalledWith( - expect.objectContaining({ start: 10, end: 20 }), - 1 - ); + await waitFor(() => { + expect(mockSetCurrentSegment).toHaveBeenLastCalledWith( + expect.objectContaining({ start: 10, end: 20 }), + 1 + ); + }); +}); + +test('locks reference inputs until edit reference is enabled', async () => { + const user = userEvent.setup(); + + runTest({ width: 375 }); + + const firstReference = await screen.findByLabelText('verse-reference-1'); + expect(firstReference).toBeDisabled(); + + await user.click(screen.getByRole('button', { name: 'Edit Reference' })); + expect(firstReference).not.toBeDisabled(); + + await user.clear(firstReference); + await user.type(firstReference, '1:1a'); + expect(firstReference).toHaveValue('1:1a'); + + await user.click(screen.getByRole('button', { name: 'Done Editing' })); + expect(screen.getByLabelText('verse-reference-1')).toBeDisabled(); +}); + +test('reset clears markers and restores the original reference table', async () => { + const user = userEvent.setup(); + + runTest({ width: 375 }); + + act(() => { + mockPlayerAction?.( + '{"regions":"[{\\"start\\":0,\\"end\\":10},{\\"start\\":10,\\"end\\":20},{\\"start\\":20,\\"end\\":69}]"}', + false + ); + }); + + await screen.findByText('0:00.0-0:10.0'); + + await user.click(screen.getByRole('button', { name: 'Edit Reference' })); + const secondReference = screen.getByLabelText('verse-reference-2'); + await user.clear(secondReference); + await user.type(secondReference, '2:10'); + expect(secondReference).toHaveValue('2:10'); + + await user.click(screen.getByRole('button', { name: 'Reset' })); + + await waitFor(() => { + expect(screen.queryByText('0:00.0-0:10.0')).not.toBeInTheDocument(); + }); + + expect(screen.getByLabelText('verse-reference-1')).toHaveValue('1:1'); + expect(screen.getByLabelText('verse-reference-2')).toHaveValue('1:2'); + expect(screen.getByLabelText('verse-reference-3')).toHaveValue('1:3'); + expect(screen.getByLabelText('verse-reference-1')).toBeDisabled(); }); diff --git a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx index 871cbf2d..39ed8c2e 100644 --- a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx +++ b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx @@ -6,7 +6,7 @@ import { useRef, useState, } from 'react'; -import { Box, Paper, SxProps, Typography } from '@mui/material'; +import { Box, Button, Paper, SxProps, Typography } from '@mui/material'; import { shallowEqual, useSelector } from 'react-redux'; import { useGlobal } from '../../../../context/useGlobal'; import usePassageDetailContext from '../../../../context/usePassageDetailContext'; @@ -104,6 +104,7 @@ export default function PassageDetailMarkVersesIsMobile({ const [numSegments, setNumSegments] = useState(0); const [pastedSegments, setPastedSegments] = useState(emptySegments); const [engVrs, setEngVrs] = useState>(new Map()); + const [isReferenceEditing, setIsReferenceEditing] = useState(false); const savingRef = useRef(false); const canceling = useRef(false); const dataRef = useRef([]); @@ -654,6 +655,40 @@ export default function PassageDetailMarkVersesIsMobile({ }); }; + const handleToggleReferenceEditing = () => { + setIsReferenceEditing((value) => !value); + }; + + const handleResetMarkup = () => { + const refs = + passageRefs.current.length > 0 + ? passageRefs.current + : getPassageRefs(passage); + const newData = emptyTable(); + + refs.forEach((ref) => { + newData.push(rowCells(['', ref])); + }); + + const hadChanges = + numSegments > 0 || + tableSignature(dataRef.current) !== tableSignature(newData); + + passageRefs.current = refs; + segmentsRef.current = emptySegments; + setNumSegments(0); + setData(newData); + setCurrentSegment(undefined, -1); + setIsReferenceEditing(false); + setConfirm(''); + setIssues([]); + resetSegments([]); + + if (hadChanges) { + toolChanged(verseToolId); + } + }; + useEffect(() => { if (saveRequested(verseToolId) && !savingRef.current) { writeResources(); @@ -766,6 +801,11 @@ export default function PassageDetailMarkVersesIsMobile({ ); } + + const editReferenceLabel = 'Edit References'; + const doneEditingReferenceLabel = 'Done Editing'; + const resetLabel = t.reset || 'Reset'; + return ( + + + + + + + + row.map((cell, colIndex) => ({ + ...cell, + readOnly: + rowIndex === 0 || + colIndex === ColName.Limits || + !isReferenceEditing, + })) + ) : data.map((row) => row.map((cell) => ({ ...cell, diff --git a/src/renderer/src/store/localization/model.tsx b/src/renderer/src/store/localization/model.tsx index 3c8cf06d..5fcb5048 100644 --- a/src/renderer/src/store/localization/model.tsx +++ b/src/renderer/src/store/localization/model.tsx @@ -1722,6 +1722,8 @@ export interface IVerseStrings extends Localize.LocalizedStringsMethods { "btNotUpdated": string; "canceling": string; "clipboard": string; + "doneEditingReference": string; + "editReference": string; "issues": string; "markVerses": string; "missingReferences": string; @@ -1730,6 +1732,7 @@ export interface IVerseStrings extends Localize.LocalizedStringsMethods { "outsideReferences": string; "pasteFormat": string; "reference": string; + "reset": string; "saveVerseMarkup": string; "startStop": string; }; diff --git a/src/renderer/src/store/localization/reducers.tsx b/src/renderer/src/store/localization/reducers.tsx index 759cea35..cf45451a 100644 --- a/src/renderer/src/store/localization/reducers.tsx +++ b/src/renderer/src/store/localization/reducers.tsx @@ -1819,6 +1819,8 @@ const initialState = { "btNotUpdated": "WARNING: Since back translation recordings already exist, back translation segments will not be updated to line up with verse changes.", "canceling": "Canceling verse markup", "clipboard": "clipboard", + "doneEditingReference": "Done Editing", + "editReference": "Edit Reference", "issues": "The verse markup has issues. Do you want to continue?", "markVerses": "Mark Verses", "missingReferences": "Warning: Verses in passage not included: ({0})", @@ -1827,6 +1829,7 @@ const initialState = { "outsideReferences": "ERROR: Some verses are outside passage: ({0})", "pasteFormat": "Invalid number of columns on clipboard.", "reference": "Reference", + "reset": "Reset", "saveVerseMarkup": "Save Verse Markup", "startStop": "Start --> Stop", } From 2f65942ee26cf2106864a9052efab46ea961a61e Mon Sep 17 00:00:00 2001 From: Stephen Chen Date: Tue, 24 Mar 2026 14:23:17 -0600 Subject: [PATCH 11/26] cleanup --- .../PassageDetailMarkVersesIsMobile.tsx | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx index 39ed8c2e..662f8c60 100644 --- a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx +++ b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx @@ -11,10 +11,6 @@ import { shallowEqual, useSelector } from 'react-redux'; import { useGlobal } from '../../../../context/useGlobal'; import usePassageDetailContext from '../../../../context/usePassageDetailContext'; import { UnsavedContext } from '../../../../context/UnsavedContext'; -import { ActionRow } from '../../../../control/ActionRow'; -import { AltButton } from '../../../../control/AltButton'; -import { GrowingSpacer } from '../../../../control/GrowingSpacer'; -import { PriButton } from '../../../../control/PriButton'; import { passageTypeFromRef } from '../../../../control/passageTypeFromRef'; import { findRecord } from '../../../../crud/tryFindRecord'; import { parseRef } from '../../../../crud/passage'; @@ -872,31 +868,6 @@ export default function PassageDetailMarkVersesIsMobile({ onCellsChanged={handleCellsChanged} onParsePaste={handleParsePaste} /> - - - {ts.clipboardCopy} - - - - {t.saveVerseMarkup} - - - {ts.cancel} - - {confirm && ( Date: Tue, 24 Mar 2026 15:22:57 -0600 Subject: [PATCH 12/26] table highlights --- .../MarkVerses/MarkVersesTableIsMobile.tsx | 19 ++++- .../PassageDetailMarkVersesIsMobile.test.tsx | 5 ++ .../PassageDetailMarkVersesIsMobile.tsx | 4 +- .../src/crud/useWavesurferRegions.tsx | 73 ++++++++++++------- 4 files changed, 71 insertions(+), 30 deletions(-) diff --git a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/MarkVersesTableIsMobile.tsx b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/MarkVersesTableIsMobile.tsx index cde9d6fe..58b043aa 100644 --- a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/MarkVersesTableIsMobile.tsx +++ b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/MarkVersesTableIsMobile.tsx @@ -10,6 +10,7 @@ import { Typography, } from '@mui/material'; import type { ChangeEvent, ClipboardEvent } from 'react'; +import { getSegmentRegionColor } from '../../../../crud/useWavesurferRegions'; import type { ICell, ICellChange } from './PassageDetailMarkVersesIsMobile'; interface MarkVersesTableIsMobileProps { @@ -97,10 +98,22 @@ export default function MarkVersesTableIsMobile({ const limits = row[ColName.Limits] as ICell; const reference = row[ColName.Ref] as ICell; const invalid = reference.className?.includes('Err'); + const rowColor = limits.value + ? getSegmentRegionColor(index, 0.24) + : 'transparent'; return ( - - + + - + runTest({ width: 375 }); + await waitFor(() => { + expect(screen.getByLabelText('verse-reference-1')).toHaveValue('1:1'); + }); + act(() => { mockPlayerAction?.( '{"regions":"[{\\"start\\":0,\\"end\\":10},{\\"start\\":10,\\"end\\":20},{\\"start\\":20,\\"end\\":69}]"}', @@ -296,6 +300,7 @@ test('highlights the matching waveform region when a row is edited', async () => ); }); + await screen.findByText('0:10.0-0:20.0'); await user.click(screen.getByRole('button', { name: 'Edit Reference' })); const secondReference = await screen.findByLabelText('verse-reference-2'); await user.clear(secondReference); diff --git a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx index 662f8c60..af633a51 100644 --- a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx +++ b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx @@ -798,8 +798,8 @@ export default function PassageDetailMarkVersesIsMobile({ ); } - const editReferenceLabel = 'Edit References'; - const doneEditingReferenceLabel = 'Done Editing'; + const editReferenceLabel = t.editReference || 'Edit Reference'; + const doneEditingReferenceLabel = t.doneEditingReference || 'Done Editing'; const resetLabel = t.reset || 'Reset'; return ( diff --git a/src/renderer/src/crud/useWavesurferRegions.tsx b/src/renderer/src/crud/useWavesurferRegions.tsx index 84c95aea..eae73453 100644 --- a/src/renderer/src/crud/useWavesurferRegions.tsx +++ b/src/renderer/src/crud/useWavesurferRegions.tsx @@ -54,6 +54,32 @@ export const parseRegions = (regionstr: string) => { segs.regions.sort((a: IRegion, b: IRegion) => a.start - b.start); return segs as IRegions; }; + +const segmentPalette = [ + [244, 67, 54], + [33, 150, 243], + [76, 175, 80], + [255, 152, 0], + [156, 39, 176], + [0, 188, 212], + [233, 30, 99], + [139, 195, 74], + [255, 87, 34], + [63, 81, 181], +]; + +export const getSegmentRegionColor = (index: number, alpha: number = 0.28) => { + const [red, green, blue] = segmentPalette[index % segmentPalette.length]; + return `rgba(${red}, ${green}, ${blue}, ${alpha})`; +}; + +const withRegionColorAlpha = (color: string, alpha: number) => { + const match = color.match( + /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*[\d.]+)?\s*\)/ + ); + if (!match) return color; + return `rgba(${match[1]}, ${match[2]}, ${match[3]}, ${alpha})`; +}; export function useWaveSurferRegions( singleRegionOnly: boolean, defaultRegionIndex: number, @@ -101,7 +127,7 @@ export function useWaveSurferRegions( const finishHandlerRef = useRef<(() => void) | undefined>(undefined); const CLICK_DEBOUNCE_MS = 100; // Minimum time between clicks - const CURRENT_REGION_COLOR = (theme.palette as any).custom.currentRegion; // Green color for current region + const CURRENT_REGION_BORDER = (theme.palette as any).custom.currentRegion; const NEXT_BORDER_COLOR = 'red'; const Regions = () => regionsRef.current; @@ -213,13 +239,16 @@ export function useWaveSurferRegions( // Set new current region color and remember its current color if (r) { - currentRegionOriginalColorRef.current = r.color || randomColor(0.1); - r.setOptions({ color: CURRENT_REGION_COLOR }); + currentRegionOriginalColorRef.current = + r.color || getSegmentRegionColor(0); + r.setOptions({ + color: withRegionColorAlpha(currentRegionOriginalColorRef.current, 0.48), + }); if ( !singleRegionRef.current && (!isAtEnd(r.end) || numRegions() === 1) ) { - setRegionEndBorderColor(r, NEXT_BORDER_COLOR); + setRegionEndBorderColor(r, CURRENT_REGION_BORDER); } } else { currentRegionOriginalColorRef.current = ''; @@ -330,6 +359,7 @@ export function useWaveSurferRegions( 500 ) .then(() => { + setPrevNext(getSortedIds()); setCurrentRegion( singleRegionRef.current ? r : findRegion(progress(), true) ); @@ -356,6 +386,7 @@ export function useWaveSurferRegions( 200 ).then(() => { if (destroyingRef.current) return; + setPrevNext(getSortedIds()); onRegion(numRegions(), true); setCurrentRegion(findRegion(progress(), true)); }); @@ -705,12 +736,20 @@ export function useWaveSurferRegions( if (!wsRef.current || sortedIds.length === 0 || singleRegionRef.current) return; let prev: Region | undefined = undefined; - sortedIds.forEach(function (id) { + sortedIds.forEach(function (id, index) { const r = region(id); if (r && prev) { setAttribute(prev, 'nextRegion', r); setAttribute(r, 'prevRegion', prev); } + if (r) { + const baseColor = getSegmentRegionColor(index); + if (currentRegionRef.current?.id === r.id) { + currentRegionOriginalColorRef.current = baseColor; + } else { + r.setOptions({ color: baseColor }); + } + } prev = r; }); }; @@ -767,7 +806,7 @@ export function useWaveSurferRegions( regarray.forEach(function (region: any) { region.start = roundToFiveDecimals(region.start); region.end = roundToFiveDecimals(region.end); - region.color = randomColor(0.1); + region.color = getSegmentRegionColor(regarray.indexOf(region)); region.drag = false; region.content = region.label; const r = Regions()?.addRegion(region); @@ -807,7 +846,7 @@ export function useWaveSurferRegions( start: split, end: ret.end, drag: false, - color: randomColor(0.1), + color: getSegmentRegionColor(0), }; const sortedIds: string[] = getSortedIds(); //need to get sorted ids before adding the new region const newRegion = Regions()?.addRegion(region); @@ -825,7 +864,7 @@ export function useWaveSurferRegions( start: 0, end: split, drag: false, - color: randomColor(0.1), + color: getSegmentRegionColor(0), }; const firstRegion = Regions()?.addRegion(region); newSorted.push(firstRegion?.id ?? 'fr'); @@ -892,7 +931,7 @@ export function useWaveSurferRegions( }; const wsAddRegion = () => { - return wsSplitRegion(currentRegion(), progress()); + return wsSplitRegion(findRegion(progress(), true), progress()); }; const wsRemoveCurrentRegion = () => { @@ -1036,22 +1075,6 @@ export function useWaveSurferRegions( loopingRef.current = loop; return loop; }; - /** - * Random RGBA color. - */ - function randomColor(seed: number) { - return ( - 'rgba(' + - [ - ~~(Math.random() * 255), - ~~(Math.random() * 255), - ~~(Math.random() * 255), - seed || 1, - ] + - ')' - ); - } - const roundToFiveDecimals = (n: number) => Math.round(n * 100000) / 100000; function roundToTenths(n: number) { return Math.round(n * 10) / 10; From 014b8b5fa8a8a08430e40384492a89f5d1d99efb Mon Sep 17 00:00:00 2001 From: Stephen Chen Date: Tue, 24 Mar 2026 17:05:20 -0600 Subject: [PATCH 13/26] reset fix --- .../mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx index af633a51..a1ad6f78 100644 --- a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx +++ b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx @@ -101,6 +101,7 @@ export default function PassageDetailMarkVersesIsMobile({ const [pastedSegments, setPastedSegments] = useState(emptySegments); const [engVrs, setEngVrs] = useState>(new Map()); const [isReferenceEditing, setIsReferenceEditing] = useState(false); + const [playerResetKey, setPlayerResetKey] = useState(0); const savingRef = useRef(false); const canceling = useRef(false); const dataRef = useRef([]); @@ -679,6 +680,7 @@ export default function PassageDetailMarkVersesIsMobile({ setConfirm(''); setIssues([]); resetSegments([]); + setPlayerResetKey((value) => value + 1); if (hadChanges) { toolChanged(verseToolId); @@ -805,6 +807,7 @@ export default function PassageDetailMarkVersesIsMobile({ return ( Date: Thu, 26 Mar 2026 11:18:40 -0600 Subject: [PATCH 14/26] SplitVerse kinda works --- .../MarkVerses/EditReferenceDropdown.tsx | 264 ++++++++++++ .../MarkVerses/MarkVersesTableIsMobile.tsx | 3 + .../PassageDetailMarkVersesIsMobile.test.tsx | 85 ++++ .../PassageDetailMarkVersesIsMobile.tsx | 386 +++++++++++++++++- src/renderer/src/store/localization/model.tsx | 1 + .../src/store/localization/reducers.tsx | 1 + src/renderer/src/utils/refMatch.ts | 2 +- 7 files changed, 724 insertions(+), 18 deletions(-) create mode 100644 src/renderer/src/components/PassageDetail/mobile/MarkVerses/EditReferenceDropdown.tsx diff --git a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/EditReferenceDropdown.tsx b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/EditReferenceDropdown.tsx new file mode 100644 index 00000000..4c87b8c0 --- /dev/null +++ b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/EditReferenceDropdown.tsx @@ -0,0 +1,264 @@ +import { + Box, + Button, + Checkbox, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControlLabel, + NativeSelect, + Typography, +} from '@mui/material'; +import type { ChangeEvent } from 'react'; +import { useEffect, useState } from 'react'; + +const suffixOptions = ['', 'a', 'b', 'c', 'd', 'e']; +const selectSx = { + minWidth: 64, + '& .MuiNativeSelect-select': { + fontSize: 40, + lineHeight: 1.1, + textAlign: 'center', + pr: 3, + }, + '& .MuiNativeSelect-icon': { + fontSize: 24, + right: 0, + }, +}; +const verseSelectSx = { + ...selectSx, + '& .MuiNativeSelect-select': { + ...selectSx['& .MuiNativeSelect-select'], + fontSize: 28, + }, +}; +const suffixOptionStyle = { + fontSize: 50, +}; +const verseOptionStyle = { + fontSize: 48, +}; + +export interface EditReferenceValue { + splitVerse: boolean; + canSplit: boolean; + startChapter: number; + startVerse: number; + startSuffix: string; + endChapter: number; + endVerse: number; + endSuffix: string; +} + +interface EditReferenceDropdownProps { + open: boolean; + limits: string; + maxVerse: number; + verseOptions: number[]; + title: string; + cancelLabel: string; + saveLabel: string; + splitVerseLabel: string; + value: EditReferenceValue; + onCancel: () => void; + onSave: (value: EditReferenceValue) => void; +} + +export default function EditReferenceDropdown({ + open, + limits, + maxVerse, + verseOptions, + title, + cancelLabel, + saveLabel, + splitVerseLabel, + value, + onCancel, + onSave, +}: EditReferenceDropdownProps) { + const [draft, setDraft] = useState(value); + const verseNumberOptions = Array.from( + new Set([...verseOptions, draft.startVerse, draft.endVerse, maxVerse]) + ).sort((left, right) => left - right); + + useEffect(() => { + setDraft(value); + }, [value]); + + const handleSplitChange = ( + event: ChangeEvent, + checked: boolean + ) => { + setDraft((current) => ({ + ...current, + splitVerse: checked, + })); + }; + + const handleSuffixChange = + (key: 'startSuffix' | 'endSuffix') => + (event: ChangeEvent) => { + const nextSuffix = event.target.value.toLowerCase(); + setDraft((current) => ({ + ...current, + [key]: nextSuffix, + })); + }; + + const handleVerseChange = + (key: 'startVerse' | 'endVerse') => + (event: ChangeEvent) => { + const nextVerse = parseInt(event.target.value, 10); + if (Number.isNaN(nextVerse)) return; + + setDraft((current) => ({ + ...current, + [key]: nextVerse, + })); + }; + + const displayEndChapter = draft.endChapter; + const displayEndVerse = draft.endVerse; + const canEditEndSuffix = draft.canSplit || draft.splitVerse; + + return ( + + + {`${title} ${limits}`} + + + + + + {`${draft.startChapter}:`} + + {verseNumberOptions.map((option) => ( + + ))} + + + + {suffixOptions.map((option) => ( + + ))} + + + - + + + {`${displayEndChapter}:`} + + {verseNumberOptions.map((option) => ( + + ))} + + + + {suffixOptions.map((option) => ( + + ))} + + + + + + } + label={splitVerseLabel} + /> + + + + + + + ); +} diff --git a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/MarkVersesTableIsMobile.tsx b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/MarkVersesTableIsMobile.tsx index 58b043aa..944d4a03 100644 --- a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/MarkVersesTableIsMobile.tsx +++ b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/MarkVersesTableIsMobile.tsx @@ -17,6 +17,7 @@ interface MarkVersesTableIsMobileProps { data: ICell[][]; onCellsChanged: (changes: Array) => void; onParsePaste: (clipboard: string) => any[]; + onRowSelect?: (rowIndex: number) => void; } enum ColName { @@ -28,6 +29,7 @@ export default function MarkVersesTableIsMobile({ data, onCellsChanged, onParsePaste, + onRowSelect, }: MarkVersesTableIsMobileProps) { const rows = data.slice(1); const header = data[0] ?? []; @@ -105,6 +107,7 @@ export default function MarkVersesTableIsMobile({ return ( onRowSelect?.(index + 1)} sx={{ backgroundColor: rowColor }} > ({ reference: 'Reference', reset: 'Reset', saveVerseMarkup: 'Save Verse Markup', + splitVerse: 'Split Verse', startStop: 'Start-Stop', badReferences: 'ERROR: Markup contains bad references', btNotUpdated: @@ -157,6 +158,7 @@ jest.mock('react-redux', () => ({ noReferences: 'Warning: Some audio segments will not be included in verses', noSegments: 'ERROR: Some verses have no segment: ({0})', outsideReferences: 'ERROR: Some verses are outside passage: ({0})', + save: 'Save', }), shallowEqual: jest.fn(), })); @@ -333,6 +335,89 @@ test('locks reference inputs until edit reference is enabled', async () => { expect(screen.getByLabelText('verse-reference-1')).toBeDisabled(); }); +test('opens and cancels the split verse dialog', async () => { + const user = userEvent.setup(); + + runTest({ width: 375 }); + + act(() => { + mockPlayerAction?.( + '{"regions":"[{\\"start\\":0,\\"end\\":10},{\\"start\\":10,\\"end\\":20},{\\"start\\":20,\\"end\\":69}]"}', + false + ); + }); + + await screen.findByText('0:00.0-0:10.0'); + + await user.click(screen.getByRole('button', { name: 'Split Verse' })); + + expect( + screen.getByRole('heading', { name: 'Edit Reference for 0:00.0-0:10.0' }) + ).toBeInTheDocument(); + expect(screen.getByLabelText('end verse number')).toBeDisabled(); + expect(screen.getAllByRole('option', { name: '4' })).toHaveLength(2); + expect(screen.queryAllByRole('option', { name: '5' })).toHaveLength(0); + expect(screen.getByLabelText('start verse suffix')).toBeInTheDocument(); + expect(screen.getByLabelText('end verse suffix')).toBeInTheDocument(); + expect(screen.getAllByRole('option', { name: 'e' })).toHaveLength(2); + + await user.click(screen.getByRole('button', { name: 'Cancel' })); + expect( + screen.queryByRole('heading', { + name: 'Edit Reference for 0:00.0-0:10.0', + }) + ).not.toBeInTheDocument(); +}); + +test('saves a split verse range and shifts following references up', async () => { + const user = userEvent.setup(); + + runTest({ width: 375 }); + + act(() => { + mockPlayerAction?.( + '{"regions":"[{\\"start\\":0,\\"end\\":10},{\\"start\\":10,\\"end\\":20},{\\"start\\":20,\\"end\\":69}]"}', + false + ); + }); + + await screen.findByText('0:00.0-0:10.0'); + + await user.click(screen.getByRole('button', { name: 'Split Verse' })); + await user.click(screen.getByRole('checkbox', { name: 'Split Verse' })); + expect(screen.getByLabelText('end verse number')).not.toBeDisabled(); + await user.selectOptions(screen.getByLabelText('start verse suffix'), 'a'); + await user.selectOptions(screen.getByLabelText('end verse suffix'), 'e'); + await user.click(screen.getByRole('button', { name: 'Save' })); + + expect(screen.getByLabelText('verse-reference-1')).toHaveValue('1:1a-2e'); + expect(screen.getByLabelText('verse-reference-2')).toHaveValue('1:3'); + expect(screen.getByLabelText('verse-reference-3')).toHaveValue('1:4'); +}); + +test('saving a suffix on the second line updates that line instead of creating a range', async () => { + const user = userEvent.setup(); + + runTest({ width: 375 }); + + act(() => { + mockPlayerAction?.( + '{"regions":"[{\\"start\\":0,\\"end\\":10},{\\"start\\":10,\\"end\\":20},{\\"start\\":20,\\"end\\":69}]"}', + false + ); + }); + + await screen.findByText('0:00.0-0:10.0'); + + await user.click(screen.getByRole('button', { name: 'Split Verse' })); + await user.selectOptions(screen.getByLabelText('end verse suffix'), 'e'); + await user.click(screen.getByRole('button', { name: 'Save' })); + + expect(screen.getByLabelText('verse-reference-1')).toHaveValue('1:1'); + expect(screen.getByLabelText('verse-reference-2')).toHaveValue('1:2e'); + expect(screen.getByLabelText('verse-reference-1')).not.toHaveValue('1:1-2e'); +}); + test('reset clears markers and restores the original reference table', async () => { const user = userEvent.setup(); diff --git a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx index a1ad6f78..46e93cb0 100644 --- a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx +++ b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx @@ -43,6 +43,9 @@ import { useStepPermissions } from '../../../../utils/useStepPermission'; import Confirm from '../../../AlertDialog'; import PassageDetailPlayer from '../../PassageDetailPlayer'; import { useProjectSegmentSave } from '../../Internalization/useProjectSegmentSave'; +import EditReferenceDropdown, { + EditReferenceValue, +} from './EditReferenceDropdown'; import MarkVersesTableIsMobile from './MarkVersesTableIsMobile'; const verseToolId = 'VerseTool'; @@ -73,6 +76,20 @@ enum ColName { Ref, } +interface IParsedReference { + chapter: number; + verse: number; + suffix: string; +} + +interface IEditReferenceDialogState extends EditReferenceValue { + rowIndex: number; + limits: string; + existingSplit: boolean; + maxVerse: number; + verseOptions: number[]; +} + export interface MarkVersesProps { width: number; } @@ -101,6 +118,8 @@ export default function PassageDetailMarkVersesIsMobile({ const [pastedSegments, setPastedSegments] = useState(emptySegments); const [engVrs, setEngVrs] = useState>(new Map()); const [isReferenceEditing, setIsReferenceEditing] = useState(false); + const [editReferenceDialog, setEditReferenceDialog] = + useState(); const [playerResetKey, setPlayerResetKey] = useState(0); const savingRef = useRef(false); const canceling = useRef(false); @@ -420,6 +439,324 @@ export default function PassageDetailMarkVersesIsMobile({ return { start, end } as IRegion; }, []); + const cloneTableData = useCallback( + (tableData: ICell[][]) => + tableData.map((row) => + row.map((cell) => ({ + ...cell, + })) + ), + [] + ); + + const setActiveRowHighlight = useCallback( + (tableData: ICell[][], rowIndex: number) => { + tableData.forEach((row, index) => { + if (index === 0) return; + const limits = row[ColName.Limits] as ICell; + limits.className = limits.className?.replace(/\s*cur\b/g, '') || 'lim'; + }); + + if (rowIndex > 0) { + const activeRow = tableData[rowIndex] as ICell[] | undefined; + const limits = activeRow?.[ColName.Limits] as ICell | undefined; + if (limits) { + limits.className = `${limits.className ?? 'lim'} cur`.trim(); + } + } + }, + [] + ); + + const buildReferenceCell = useCallback((value: string, cell: ICell) => { + return { + ...cell, + value, + className: `ref${value && !refMatch(value) ? ' Err' : ''}`, + }; + }, []); + + const parseReferencePart = useCallback( + (value: string, fallbackChapter: number) => { + const match = /^(?:(\d+):)?(\d+)([a-e]?)$/i.exec(value.trim()); + if (!match) return undefined; + return { + chapter: match[1] ? parseInt(match[1], 10) : fallbackChapter, + verse: parseInt(match[2], 10), + suffix: (match[3] ?? '').toLowerCase(), + } as IParsedReference; + }, + [] + ); + + const parseReferenceValue = useCallback( + (value: string) => { + const normalized = value + .replace(/[–—]/g, '-') + .replace(/\s+/g, '') + .trim(); + if (!normalized) return undefined; + + const [startText, endText] = normalized.split('-', 2); + const start = parseReferencePart(startText, 0); + if (!start) return undefined; + const end = endText + ? parseReferencePart(endText, start.chapter) + : start; + if (!end) return undefined; + return { start, end }; + }, + [parseReferencePart] + ); + + const formatReferenceValue = useCallback( + ({ + startChapter, + startVerse, + startSuffix, + endChapter, + endVerse, + endSuffix, + splitVerse, + }: EditReferenceValue) => { + const startLabel = `${startChapter}:${startVerse}${startSuffix}`; + const sameVerse = + startChapter === endChapter && startVerse === endVerse; + + if (!splitVerse || sameVerse) { + if (endSuffix && endSuffix !== startSuffix) { + return `${startLabel}-${endVerse}${endSuffix}`; + } + return startLabel; + } + + if (startChapter === endChapter) { + return `${startLabel}-${endVerse}${endSuffix}`; + } + + return `${startLabel}-${endChapter}:${endVerse}${endSuffix}`; + }, + [] + ); + + const getHighestVerseInput = useCallback( + (tableData: ICell[][]) => { + const highestVerse = tableData.reduce((maxVerse, row, index) => { + if (index === 0) return maxVerse; + const parsedReference = parseReferenceValue( + `${row[ColName.Ref]?.value ?? ''}` + ); + if (!parsedReference) return maxVerse; + return Math.max( + maxVerse, + parsedReference.start.verse, + parsedReference.end.verse + ); + }, 0); + + return highestVerse > 0 ? highestVerse : 1; + }, + [parseReferenceValue] + ); + + const getVerseOptionsFromInputs = useCallback( + (tableData: ICell[][]) => { + const verseNumbers = tableData.reduce((allVerses, row, index) => { + if (index === 0) return allVerses; + const parsedReference = parseReferenceValue( + `${row[ColName.Ref]?.value ?? ''}` + ); + if (!parsedReference) return allVerses; + allVerses.push(parsedReference.start.verse, parsedReference.end.verse); + return allVerses; + }, []); + + const uniqueSortedVerses = Array.from(new Set(verseNumbers)).sort( + (left, right) => left - right + ); + + return uniqueSortedVerses.length > 0 ? uniqueSortedVerses : [1]; + }, + [parseReferenceValue] + ); + + const findActiveRowIndex = useCallback(() => { + const highlightedIndex = dataRef.current.findIndex( + (row, index) => + index > 0 && + ((row[ColName.Limits] as ICell).className ?? '').includes('cur') + ); + if (highlightedIndex > 0) return highlightedIndex; + + const firstSegmentedIndex = dataRef.current.findIndex( + (row, index) => index > 0 && Boolean((row[ColName.Limits] as ICell).value) + ); + return firstSegmentedIndex > 0 ? firstSegmentedIndex : -1; + }, []); + + const buildEditReferenceDialogState = useCallback( + (rowIndex: number) => { + const row = dataRef.current[rowIndex] as ICell[] | undefined; + if (!row) return undefined; + + const currentValue = `${row[ColName.Ref]?.value ?? ''}`; + const fallbackValue = + passageRefs.current[rowIndex - 1] || currentValue || '1:1'; + const currentRef = + parseReferenceValue(currentValue) || parseReferenceValue(fallbackValue); + if (!currentRef) return undefined; + + const nextRow = dataRef.current[rowIndex + 1] as ICell[] | undefined; + const nextValue = `${nextRow?.[ColName.Ref]?.value ?? ''}`; + const nextRef = parseReferenceValue(nextValue); + const existingSplit = + currentRef.start.chapter !== currentRef.end.chapter || + currentRef.start.verse !== currentRef.end.verse; + const canSplit = + existingSplit || + Boolean(nextRef) || + Boolean(`${nextRow?.[ColName.Ref]?.value ?? ''}`.trim()) || + Boolean(`${nextRow?.[ColName.Limits]?.value ?? ''}`.trim()); + + return { + rowIndex, + limits: `${row[ColName.Limits]?.value ?? ''}`, + canSplit, + splitVerse: existingSplit, + existingSplit, + maxVerse: Math.max( + getHighestVerseInput(dataRef.current), + currentRef.start.verse, + currentRef.end.verse, + nextRef?.start.verse ?? 0 + ), + verseOptions: getVerseOptionsFromInputs(dataRef.current), + startChapter: currentRef.start.chapter, + startVerse: currentRef.start.verse, + startSuffix: currentRef.start.suffix, + endChapter: existingSplit + ? currentRef.end.chapter + : (nextRef?.start.chapter ?? currentRef.end.chapter), + endVerse: existingSplit + ? currentRef.end.verse + : (nextRef?.start.verse ?? currentRef.end.verse), + endSuffix: existingSplit + ? currentRef.end.suffix + : (nextRef?.start.suffix ?? currentRef.end.suffix), + } as IEditReferenceDialogState; + }, + [getHighestVerseInput, getVerseOptionsFromInputs, parseReferenceValue] + ); + + const handleSelectRow = useCallback( + (rowIndex: number) => { + const row = dataRef.current[rowIndex] as ICell[] | undefined; + const activeSegment = getSegmentFromRow(row); + if (!row || !activeSegment) return; + + const newData = cloneTableData(dataRef.current); + setActiveRowHighlight(newData, rowIndex); + setData(newData); + setCurrentSegment(activeSegment, rowIndex - 1); + }, + [cloneTableData, getSegmentFromRow, setCurrentSegment, setData, setActiveRowHighlight] + ); + + const handleOpenSplitVerseDialog = useCallback(() => { + const rowIndex = findActiveRowIndex(); + if (rowIndex < 1) return; + const nextDialog = buildEditReferenceDialogState(rowIndex); + if (nextDialog) { + setEditReferenceDialog(nextDialog); + } + }, [buildEditReferenceDialogState, findActiveRowIndex]); + + const handleCloseSplitVerseDialog = () => { + setEditReferenceDialog(undefined); + }; + + const handleSaveSplitVerseDialog = (value: EditReferenceValue) => { + if (!editReferenceDialog) return; + + const newData = cloneTableData(dataRef.current); + const row = newData[editReferenceDialog.rowIndex] as ICell[] | undefined; + if (!row) return; + + if (!value.splitVerse) { + row[ColName.Ref] = buildReferenceCell( + `${value.startChapter}:${value.startVerse}${value.startSuffix}`, + row[ColName.Ref] as ICell + ); + + if ( + editReferenceDialog.canSplit && + !editReferenceDialog.existingSplit && + newData[editReferenceDialog.rowIndex + 1] + ) { + const nextRow = newData[editReferenceDialog.rowIndex + 1] as ICell[]; + nextRow[ColName.Ref] = buildReferenceCell( + `${value.endChapter}:${value.endVerse}${value.endSuffix}`, + nextRow[ColName.Ref] as ICell + ); + } + } else { + const referenceValues = newData.map( + (existingRow) => `${existingRow[ColName.Ref]?.value ?? ''}` + ); + const nextReference = formatReferenceValue(value); + const targetRowIndex = newData.findIndex((existingRow, index) => { + if (index <= editReferenceDialog.rowIndex) return false; + const parsedReference = parseReferenceValue( + `${existingRow[ColName.Ref]?.value ?? ''}` + ); + return ( + parsedReference?.start.chapter === value.endChapter && + parsedReference.start.verse === value.endVerse + ); + }); + const rowsConsumed = + targetRowIndex > editReferenceDialog.rowIndex + ? targetRowIndex - editReferenceDialog.rowIndex + : 1; + row[ColName.Ref] = buildReferenceCell( + nextReference, + row[ColName.Ref] as ICell + ); + + if (value.canSplit) { + for ( + let index = editReferenceDialog.rowIndex + 1; + index < newData.length; + index += 1 + ) { + const shiftedValue = referenceValues[index + rowsConsumed] ?? ''; + const referenceCell = newData[index]?.[ColName.Ref] as + | ICell + | undefined; + if (!referenceCell) continue; + newData[index][ColName.Ref] = buildReferenceCell( + shiftedValue, + referenceCell + ); + } + } + } + + setActiveRowHighlight(newData, editReferenceDialog.rowIndex); + setData(newData); + setSegments(); + + const activeSegment = getSegmentFromRow( + newData[editReferenceDialog.rowIndex] as ICell[] + ); + if (activeSegment) { + setCurrentSegment(activeSegment, editReferenceDialog.rowIndex - 1); + } + + toolChanged(verseToolId); + setEditReferenceDialog(undefined); + }; + const resetSegments = (regions: IRegion[]) => { const segments = JSON.stringify({ regions }); setTimeout(() => { @@ -545,11 +882,7 @@ export default function PassageDetailMarkVersesIsMobile({ }; const handleCellsChanged = (changes: Array) => { - const newData = dataRef.current.map((row) => - row.map((cell) => ({ - ...cell, - })) - ); + const newData = cloneTableData(dataRef.current); let changed = false; let activeRowIndex = -1; @@ -582,19 +915,10 @@ export default function PassageDetailMarkVersesIsMobile({ }); if (changed) { - newData.forEach((row, index) => { - if (index === 0) return; - const limits = row[ColName.Limits] as ICell; - limits.className = limits.className?.replace(/\s*cur\b/g, '') || 'lim'; - }); - + setActiveRowHighlight(newData, activeRowIndex); const activeRow = activeRowIndex > 0 ? (newData[activeRowIndex] as ICell[]) : undefined; const activeSegment = getSegmentFromRow(activeRow); - if (activeRow && activeSegment) { - const limits = activeRow[ColName.Limits] as ICell; - limits.className = `${limits.className ?? 'lim'} cur`.trim(); - } setData(newData); setSegments(); @@ -677,6 +1001,7 @@ export default function PassageDetailMarkVersesIsMobile({ setData(newData); setCurrentSegment(undefined, -1); setIsReferenceEditing(false); + setEditReferenceDialog(undefined); setConfirm(''); setIssues([]); resetSegments([]); @@ -802,7 +1127,10 @@ export default function PassageDetailMarkVersesIsMobile({ const editReferenceLabel = t.editReference || 'Edit Reference'; const doneEditingReferenceLabel = t.doneEditingReference || 'Done Editing'; + const splitVerseLabel = t.splitVerse || 'Split Verse'; const resetLabel = t.reset || 'Reset'; + const saveLabel = ts.save || 'Save'; + const cancelLabel = ts.cancel || 'Cancel'; return ( @@ -825,8 +1153,6 @@ export default function PassageDetailMarkVersesIsMobile({ flexWrap: 'wrap', }} > - - + + {undoState && ( + + + + )} Date: Thu, 26 Mar 2026 11:51:13 -0600 Subject: [PATCH 18/26] Remove Milliseconds --- .../mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx index a5d9e1f2..af5f467e 100644 --- a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx +++ b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.tsx @@ -412,8 +412,8 @@ export default function PassageDetailMarkVersesIsMobile({ const formatTime = (value: number) => { const minutes = Math.floor(value / 60); - const seconds = value - minutes * 60; - return `${minutes}:${seconds.toFixed(1).padStart(4, '0')}`; + const seconds = Math.floor(value - minutes * 60); + return `${minutes}:${seconds.toString().padStart(2, '0')}`; }; const parseFormattedTime = (value: string) => { From a502517182f479a54c59cef7727f9c094dce962b Mon Sep 17 00:00:00 2001 From: Stephen Chen Date: Thu, 26 Mar 2026 11:51:47 -0600 Subject: [PATCH 19/26] Remove Milliseconds --- .../PassageDetailMarkVersesIsMobile.test.tsx | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.test.tsx b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.test.tsx index 5a28c22d..89238cc7 100644 --- a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.test.tsx +++ b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/PassageDetailMarkVersesIsMobile.test.tsx @@ -276,11 +276,11 @@ test('updates timestamp rows when the player emits verse markers', async () => { }); await waitFor(() => { - expect(screen.getByText('0:00.0-0:10.0')).toBeInTheDocument(); + expect(screen.getByText('0:00-0:10')).toBeInTheDocument(); }); - expect(screen.getByText('0:10.0-0:20.0')).toBeInTheDocument(); - expect(screen.getByText('0:20.0-1:09.0')).toBeInTheDocument(); + expect(screen.getByText('0:10-0:20')).toBeInTheDocument(); + expect(screen.getByText('0:20-1:09')).toBeInTheDocument(); expect(screen.getByLabelText('verse-reference-1')).toHaveValue('1:1'); expect(screen.getByLabelText('verse-reference-2')).toHaveValue('1:2'); expect(screen.getByLabelText('verse-reference-3')).toHaveValue('1:3'); @@ -302,7 +302,7 @@ test('highlights the matching waveform region when a row is edited', async () => ); }); - await screen.findByText('0:10.0-0:20.0'); + await screen.findByText('0:10-0:20'); await user.click(screen.getByRole('button', { name: 'Edit Reference' })); const secondReference = await screen.findByLabelText('verse-reference-2'); await user.clear(secondReference); @@ -347,12 +347,12 @@ test('opens and cancels the split verse dialog', async () => { ); }); - await screen.findByText('0:00.0-0:10.0'); + await screen.findByText('0:00-0:10'); await user.click(screen.getByRole('button', { name: 'Split Verse' })); expect( - screen.getByRole('heading', { name: 'Edit Reference for 0:00.0-0:10.0' }) + screen.getByRole('heading', { name: 'Edit Reference for 0:00-0:10' }) ).toBeInTheDocument(); expect(screen.getByLabelText('end verse number')).toBeDisabled(); expect(screen.getAllByRole('option', { name: '4' })).toHaveLength(2); @@ -364,7 +364,7 @@ test('opens and cancels the split verse dialog', async () => { await user.click(screen.getByRole('button', { name: 'Cancel' })); expect( screen.queryByRole('heading', { - name: 'Edit Reference for 0:00.0-0:10.0', + name: 'Edit Reference for 0:00-0:10', }) ).not.toBeInTheDocument(); }); @@ -381,7 +381,7 @@ test('saves a split verse range and shifts following references up', async () => ); }); - await screen.findByText('0:00.0-0:10.0'); + await screen.findByText('0:00-0:10'); await user.click(screen.getByRole('button', { name: 'Split Verse' })); await user.click(await screen.findByRole('checkbox')); @@ -407,7 +407,7 @@ test('saving a suffix on the second line updates that line instead of creating a ); }); - await screen.findByText('0:00.0-0:10.0'); + await screen.findByText('0:00-0:10'); await user.click(screen.getByRole('button', { name: 'Split Verse' })); await user.selectOptions(screen.getByLabelText('end verse suffix'), 'e'); @@ -430,9 +430,9 @@ test('split uses the selected left and right verses rather than the dialog row', ); }); - await screen.findByText('0:20.0-1:09.0'); + await screen.findByText('0:20-1:09'); - await user.click(screen.getByText('0:20.0-1:09.0')); + await user.click(screen.getByText('0:20-1:09')); await user.click(screen.getByRole('button', { name: 'Split Verse' })); await user.selectOptions(screen.getByLabelText('start verse number'), '2'); await user.click(screen.getByRole('checkbox', { name: 'Split Verse' })); @@ -456,7 +456,7 @@ test('shows undo after dialog save and restores the previous table', async () => ); }); - await screen.findByText('0:00.0-0:10.0'); + await screen.findByText('0:00-0:10'); await user.click(screen.getByRole('button', { name: 'Split Verse' })); await user.click(await screen.findByRole('checkbox')); @@ -487,7 +487,7 @@ test('reset clears markers and restores the original reference table', async () ); }); - await screen.findByText('0:00.0-0:10.0'); + await screen.findByText('0:00-0:10'); await user.click(screen.getByRole('button', { name: 'Edit Reference' })); const secondReference = screen.getByLabelText('verse-reference-2'); @@ -498,7 +498,7 @@ test('reset clears markers and restores the original reference table', async () await user.click(screen.getByRole('button', { name: 'Reset' })); await waitFor(() => { - expect(screen.queryByText('0:00.0-0:10.0')).not.toBeInTheDocument(); + expect(screen.queryByText('0:00-0:10')).not.toBeInTheDocument(); }); expect(screen.getByLabelText('verse-reference-1')).toHaveValue('1:1'); From 34719bf70cd7893858a3781da74436f168f3f159 Mon Sep 17 00:00:00 2001 From: Stephen Chen Date: Thu, 26 Mar 2026 16:04:49 -0600 Subject: [PATCH 20/26] C --- package-lock.json | 529 ++++++++++++++++++++-------------------------- package.json | 1 + 2 files changed, 233 insertions(+), 297 deletions(-) diff --git a/package-lock.json b/package-lock.json index d772b1d1..98fecb2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -95,6 +95,7 @@ "@types/wavesurfer.js": "^6.0.12", "@upstash/context7-mcp": "^2.1.2", "@vitejs/plugin-react": "^5.1.1", + "baseline-browser-mapping": "^2.10.11", "dotenv": "^17.0.0", "electron": "^39.2.6", "electron-builder": "^26.0.12", @@ -859,9 +860,9 @@ } }, "node_modules/@electron/rebuild/node_modules/node-abi": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.26.0.tgz", - "integrity": "sha512-8QwIZqikRvDIkXS2S93LjzhsSPJuIbfaMETWH+Bx8oOT9Sa9UsUtBFQlc3gBNd1+QINjaTloitXr1W3dQLi9Iw==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.28.0.tgz", + "integrity": "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g==", "dev": true, "license": "MIT", "dependencies": { @@ -903,6 +904,16 @@ "node": ">=16.4" } }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/@electron/universal/node_modules/minimatch": { "version": "9.0.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", @@ -919,16 +930,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@electron/universal/node_modules/minimatch/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/@electron/windows-sign": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", @@ -1540,9 +1541,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1582,15 +1583,15 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1636,20 +1637,20 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -1686,9 +1687,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", - "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "engines": { @@ -1838,9 +1839,9 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.9", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", - "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", "dev": true, "license": "MIT", "engines": { @@ -3854,20 +3855,20 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.49.0.tgz", - "integrity": "sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.49.0", - "@typescript-eslint/type-utils": "8.49.0", - "@typescript-eslint/utils": "8.49.0", - "@typescript-eslint/visitor-keys": "8.49.0", - "ignore": "^7.0.0", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3877,8 +3878,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.49.0", - "eslint": "^8.57.0 || ^9.0.0", + "@typescript-eslint/parser": "^8.57.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, @@ -3893,17 +3894,17 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.49.0.tgz", - "integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.49.0", - "@typescript-eslint/types": "8.49.0", - "@typescript-eslint/typescript-estree": "8.49.0", - "@typescript-eslint/visitor-keys": "8.49.0", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3913,20 +3914,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.49.0.tgz", - "integrity": "sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.49.0", - "@typescript-eslint/types": "^8.49.0", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3940,14 +3941,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.49.0.tgz", - "integrity": "sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.49.0", - "@typescript-eslint/visitor-keys": "8.49.0" + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3958,9 +3959,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.49.0.tgz", - "integrity": "sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", "dev": true, "license": "MIT", "engines": { @@ -3975,17 +3976,17 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.49.0.tgz", - "integrity": "sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.49.0", - "@typescript-eslint/typescript-estree": "8.49.0", - "@typescript-eslint/utils": "8.49.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3995,14 +3996,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.49.0.tgz", - "integrity": "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", "dev": true, "license": "MIT", "engines": { @@ -4014,21 +4015,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.49.0.tgz", - "integrity": "sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.49.0", - "@typescript-eslint/tsconfig-utils": "8.49.0", - "@typescript-eslint/types": "8.49.0", - "@typescript-eslint/visitor-keys": "8.49.0", - "debug": "^4.3.4", - "minimatch": "^9.0.4", - "semver": "^7.6.0", + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4041,36 +4042,10 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -4081,16 +4056,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.49.0.tgz", - "integrity": "sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.49.0", - "@typescript-eslint/types": "8.49.0", - "@typescript-eslint/typescript-estree": "8.49.0" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4100,19 +4075,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.49.0.tgz", - "integrity": "sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.49.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.57.2", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4122,6 +4097,19 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@uiw/color-convert": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/@uiw/color-convert/-/color-convert-2.9.2.tgz", @@ -4356,9 +4344,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -4512,9 +4500,9 @@ "license": "MIT" }, "node_modules/app-builder-lib": { - "version": "26.7.0", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.7.0.tgz", - "integrity": "sha512-/UgCD8VrO79Wv8aBNpjMfsS1pIUfIPURoRn0Ik6tMe5avdZF+vQgl/juJgipcMmH3YS0BD573lCdCHyoi84USg==", + "version": "26.8.2", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.8.2.tgz", + "integrity": "sha512-z3ptLzJwNl35fyR0wxv4qWOfZuU36VysYHnbs8PDtf8S0QzIl2OWimdDFVmCxYMkIV1k/RT9CeTgcP7oUznFOw==", "dev": true, "license": "MIT", "dependencies": { @@ -4529,7 +4517,7 @@ "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", "async-exit-hook": "^2.0.1", - "builder-util": "26.4.1", + "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chromium-pickle-js": "^0.2.0", "ci-info": "4.3.1", @@ -4537,7 +4525,7 @@ "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", - "electron-publish": "26.6.0", + "electron-publish": "26.8.1", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", @@ -4559,8 +4547,8 @@ "node": ">=14.0.0" }, "peerDependencies": { - "dmg-builder": "26.7.0", - "electron-builder-squirrel-windows": "26.7.0" + "dmg-builder": "26.8.2", + "electron-builder-squirrel-windows": "26.8.2" } }, "node_modules/app-builder-lib/node_modules/@electron/get": { @@ -5196,13 +5184,16 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.6.tgz", - "integrity": "sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==", + "version": "2.10.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.11.tgz", + "integrity": "sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/basic-ftp": { @@ -5418,9 +5409,9 @@ "license": "MIT" }, "node_modules/builder-util": { - "version": "26.4.1", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.4.1.tgz", - "integrity": "sha512-FlgH43XZ50w3UtS1RVGDWOz8v9qMXPC7upMtKMtBEnYdt1OVoS61NYhKm/4x+cIaWqJTXua0+VVPI+fSPGXNIw==", + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.8.1.tgz", + "integrity": "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==", "dev": true, "license": "MIT", "dependencies": { @@ -5537,6 +5528,16 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/cacache/node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -5559,17 +5560,14 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/cacache/node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } + "license": "ISC" }, - "node_modules/cacache/node_modules/glob/node_modules/minimatch": { + "node_modules/cacache/node_modules/minimatch": { "version": "9.0.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", @@ -5585,59 +5583,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/cacache/node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "extraneous": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/cacache/node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "extraneous": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacache/node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "extraneous": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", @@ -6200,9 +6145,9 @@ } }, "node_modules/cosmiconfig/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "license": "ISC", "engines": { "node": ">= 6" @@ -6588,14 +6533,14 @@ } }, "node_modules/dmg-builder": { - "version": "26.7.0", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.7.0.tgz", - "integrity": "sha512-uOOBA3f+kW3o4KpSoMQ6SNpdXU7WtxlJRb9vCZgOvqhTz4b3GjcoWKstdisizNZLsylhTMv8TLHFPFW0Uxsj/g==", + "version": "26.8.2", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.2.tgz", + "integrity": "sha512-DaWI+p4DOqiFVZFMovdGYammBOyJAiHHFWUTQ0Z7gNc0twfdIN0LvyJ+vFsgZEDR1fjgbpCj690IVtbYIsZObQ==", "dev": true, "license": "MIT", "dependencies": { - "app-builder-lib": "26.7.0", - "builder-util": "26.4.1", + "app-builder-lib": "26.8.2", + "builder-util": "26.8.1", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" @@ -6793,18 +6738,18 @@ } }, "node_modules/electron-builder": { - "version": "26.7.0", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.7.0.tgz", - "integrity": "sha512-LoXbCvSFxLesPneQ/fM7FB4OheIDA2tjqCdUkKlObV5ZKGhYgi5VHPHO/6UUOUodAlg7SrkPx7BZJPby+Vrtbg==", + "version": "26.8.2", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.8.2.tgz", + "integrity": "sha512-ieiiXPdgH3qrG6lcvy2mtnI5iEmAopmLuVRMSJ5j40weU0tgpNx0OAk9J5X5nnO0j9+KIkxHzwFZVUDk1U3aGw==", "dev": true, "license": "MIT", "dependencies": { - "app-builder-lib": "26.7.0", - "builder-util": "26.4.1", + "app-builder-lib": "26.8.2", + "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "ci-info": "^4.2.0", - "dmg-builder": "26.7.0", + "dmg-builder": "26.8.2", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", @@ -6819,15 +6764,15 @@ } }, "node_modules/electron-builder-squirrel-windows": { - "version": "26.7.0", - "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.7.0.tgz", - "integrity": "sha512-3EqkQK+q0kGshdPSKEPb2p5F75TENMKu6Fe5aTdeaPfdzFK4Yjp5L0d6S7K8iyvqIsGQ/ei4bnpyX9wt+kVCKQ==", + "version": "26.8.2", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.8.2.tgz", + "integrity": "sha512-kXhajX6DzdIQcTlctVTKoG1oO39JhWcTG0lH7ZEJ4FzPaKJy7KFNfNJUd5BoEmLjv5GlrRZpEOYnniD+LcwNJA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "app-builder-lib": "26.7.0", - "builder-util": "26.4.1", + "app-builder-lib": "26.8.2", + "builder-util": "26.8.1", "electron-winstaller": "5.4.0" } }, @@ -6861,14 +6806,14 @@ } }, "node_modules/electron-publish": { - "version": "26.6.0", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.6.0.tgz", - "integrity": "sha512-LsyHMMqbvJ2vsOvuWJ19OezgF2ANdCiHpIucDHNiLhuI+/F3eW98ouzWSRmXXi82ZOPZXC07jnIravY4YYwCLQ==", + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.8.1.tgz", + "integrity": "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==", "dev": true, "license": "MIT", "dependencies": { "@types/fs-extra": "^9.0.11", - "builder-util": "26.4.1", + "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "form-data": "^4.0.5", @@ -7423,25 +7368,25 @@ } }, "node_modules/eslint": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", - "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", + "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.1", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", + "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", @@ -7460,7 +7405,7 @@ "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -7902,13 +7847,13 @@ } }, "node_modules/express-rate-limit": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", - "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", + "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", "dev": true, "license": "MIT", "dependencies": { - "ip-address": "10.0.1" + "ip-address": "10.1.0" }, "engines": { "node": ">= 16" @@ -7920,16 +7865,6 @@ "express": ">= 4.11" } }, - "node_modules/express-rate-limit/node_modules/ip-address": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", - "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/express/node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -8186,9 +8121,9 @@ } }, "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -8291,9 +8226,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -8962,9 +8897,9 @@ "license": "MIT" }, "node_modules/hono": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.2.tgz", - "integrity": "sha512-gJnaDHXKDayjt8ue0n8Gs0A007yKXj4Xzb8+cNjZeYsSzzwKc0Lr+OZgYwVfB0pHfUs17EPoLvrOsEaJ9mj+Tg==", + "version": "4.12.9", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz", + "integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==", "dev": true, "license": "MIT", "engines": { @@ -11294,9 +11229,9 @@ } }, "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -11386,9 +11321,9 @@ } }, "node_modules/minimatch/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11449,11 +11384,11 @@ } }, "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "minipass": "^3.0.0" }, @@ -12365,9 +12300,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -13705,9 +13640,9 @@ "license": "MIT" }, "node_modules/sanitize-filename": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", - "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", "dev": true, "license": "WTFPL OR ISC", "dependencies": { @@ -14684,9 +14619,9 @@ } }, "node_modules/tar": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz", - "integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==", + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -14946,9 +14881,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -15157,16 +15092,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.49.0.tgz", - "integrity": "sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.2.tgz", + "integrity": "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.49.0", - "@typescript-eslint/parser": "8.49.0", - "@typescript-eslint/typescript-estree": "8.49.0", - "@typescript-eslint/utils": "8.49.0" + "@typescript-eslint/eslint-plugin": "8.57.2", + "@typescript-eslint/parser": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -15176,7 +15111,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, @@ -15200,9 +15135,9 @@ } }, "node_modules/undici": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", - "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", + "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 3ab54f38..c66a6407 100644 --- a/package.json +++ b/package.json @@ -155,6 +155,7 @@ "@types/wavesurfer.js": "^6.0.12", "@upstash/context7-mcp": "^2.1.2", "@vitejs/plugin-react": "^5.1.1", + "baseline-browser-mapping": "^2.10.11", "dotenv": "^17.0.0", "electron": "^39.2.6", "electron-builder": "^26.0.12", From f0171e60b1c0e04ddb1061ecb2c348fe96506be5 Mon Sep 17 00:00:00 2001 From: Stephen Chen Date: Fri, 27 Mar 2026 14:32:57 -0600 Subject: [PATCH 21/26] c --- src/renderer/src/routes/PassageDetail.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/renderer/src/routes/PassageDetail.tsx b/src/renderer/src/routes/PassageDetail.tsx index c9d35ba7..41c11bb2 100644 --- a/src/renderer/src/routes/PassageDetail.tsx +++ b/src/renderer/src/routes/PassageDetail.tsx @@ -80,7 +80,6 @@ const PassageDetailGrids = () => { const { isMobile } = useMobile(); const scrollbarWidthRef = React.useRef(0); - // Calculate scrollbar width dynamically const getScrollbarWidth = () => { // Create a temporary div to measure scrollbar width From 45201904a50b039d3cd8e4b06d06c6a1e7b37fa9 Mon Sep 17 00:00:00 2001 From: Stephen Chen Date: Fri, 27 Mar 2026 14:54:37 -0600 Subject: [PATCH 22/26] c --- .../MarkVerses/EditReferenceDropdown.tsx | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/EditReferenceDropdown.tsx b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/EditReferenceDropdown.tsx index 4c87b8c0..19629900 100644 --- a/src/renderer/src/components/PassageDetail/mobile/MarkVerses/EditReferenceDropdown.tsx +++ b/src/renderer/src/components/PassageDetail/mobile/MarkVerses/EditReferenceDropdown.tsx @@ -223,13 +223,13 @@ export default function EditReferenceDropdown({ ))} - + {suffixOptions.map((option) => (