From a2101bd8b0d8fb4e6a4997a05f663b540d4be658 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Tue, 3 Feb 2026 10:55:17 +0100 Subject: [PATCH 01/15] refactor(ranges): publication string * Export as > Preview publication string (modal) become Copy publication string * Display publication string (button) become Publication string menu * Publication string divider * Configure (modal) * Show / Hide (toggle button) * Configuration modal of publication string * Apply and copy are separated actions * copy is a floating button in preview container * "Apply and close" is an action of dialog footer (right align) --- src/component/1d/FloatPublicationString.tsx | 40 +---------- src/component/elements/ToolbarPopoverItem.tsx | 19 +++++- .../hooks/use_publication_strings.ts | 39 +++++++++++ .../modal/PublicationStringModal.tsx | 31 ++++++--- .../panels/RangesPanel/RangesHeader.tsx | 68 ++++++++++++++++--- 5 files changed, 140 insertions(+), 57 deletions(-) create mode 100644 src/component/hooks/use_publication_strings.ts diff --git a/src/component/1d/FloatPublicationString.tsx b/src/component/1d/FloatPublicationString.tsx index ae2f5ac406..eb164d2a6b 100644 --- a/src/component/1d/FloatPublicationString.tsx +++ b/src/component/1d/FloatPublicationString.tsx @@ -1,23 +1,19 @@ import styled from '@emotion/styled'; import type { BoundingBox } from '@zakodium/nmrium-core'; -import { rangesToACS } from 'nmr-processing'; import { useEffect, useState } from 'react'; import { BsArrowsMove } from 'react-icons/bs'; import { FaTimes } from 'react-icons/fa'; import { Rnd } from 'react-rnd'; -import { isSpectrum1D } from '../../data/data1d/Spectrum1D/isSpectrum1D.js'; import { useChartData } from '../context/ChartContext.js'; import { useDispatch } from '../context/DispatchContext.js'; import { useGlobal } from '../context/GlobalContext.js'; import type { ActionsButtonsPopoverProps } from '../elements/ActionsButtonsPopover.js'; import { ActionsButtonsPopover } from '../elements/ActionsButtonsPopover.js'; -import { useActiveNucleusTab } from '../hooks/useActiveNucleusTab.ts'; -import { usePanelPreferences } from '../hooks/usePanelPreferences.js'; import { useSVGUnitConverter } from '../hooks/useSVGUnitConverter.js'; -import useSpectraByActiveNucleus from '../hooks/useSpectraPerNucleus.js'; import { useTextMetrics } from '../hooks/useTextMetrics.ts'; import { useCheckExportStatus } from '../hooks/useViewportSize.js'; +import { usePublicationStrings } from '../hooks/use_publication_strings.ts'; const ReactRnd = styled(Rnd)` border: 1px solid transparent; @@ -305,40 +301,8 @@ function DraggablePublicationString(props: DraggablePublicationStringProps) { ); } -function usePublicationString() { - const spectra = useSpectraByActiveNucleus(); - const activeTab = useActiveNucleusTab(); - const rangesPreferences = usePanelPreferences('ranges', activeTab); - - const output: Record = {}; - - for (const spectrum of spectra) { - if (!isSpectrum1D(spectrum)) { - continue; - } - const { id: spectrumKey, info, ranges } = spectrum; - - if (!Array.isArray(ranges?.values) || ranges.values.length === 0) { - continue; - } - - const { originFrequency: observedFrequency, nucleus } = info; - - const value = rangesToACS(ranges.values, { - nucleus, // '19f' - deltaFormat: rangesPreferences.deltaPPM.format, - couplingFormat: rangesPreferences.coupling.format, - observedFrequency, //400 - }); - - output[spectrumKey] = value; - } - - return output; -} - export function FloatPublicationString() { - const publicationString = usePublicationString(); + const publicationString = usePublicationStrings(); const { view: { ranges }, } = useChartData(); diff --git a/src/component/elements/ToolbarPopoverItem.tsx b/src/component/elements/ToolbarPopoverItem.tsx index 7ae7013e52..4c536010e2 100644 --- a/src/component/elements/ToolbarPopoverItem.tsx +++ b/src/component/elements/ToolbarPopoverItem.tsx @@ -1,5 +1,5 @@ -import type { MenuItemProps } from '@blueprintjs/core'; -import { Menu, MenuItem, Tooltip } from '@blueprintjs/core'; +import type { MenuDividerProps, MenuItemProps } from '@blueprintjs/core'; +import { Menu, MenuDivider, MenuItem, Tooltip } from '@blueprintjs/core'; import type { ToolbarItemProps, ToolbarPopoverItemProps, @@ -9,10 +9,16 @@ import { Toolbar, TooltipHelpContent } from 'react-science/ui'; export interface ToolbarPopoverMenuItem extends MenuItemProps, Pick { + menuItemType?: 'item'; data?: T; tooltip?: string | TooltipItem; } +interface ToolbarPopoverMenuDividerProps extends MenuDividerProps { + menuItemType: 'divider'; + key: string; +} + interface CustomToolbarPopoverItemProps extends Omit, @@ -21,7 +27,7 @@ interface CustomToolbarPopoverItemProps 'tooltip' | 'icon' | 'tooltipProps' | 'active' | 'id' > { itemProps?: Omit; - options: Array>; + options: Array | ToolbarPopoverMenuDividerProps>; onClick?: (data?: T) => void; } @@ -49,12 +55,19 @@ export function ToolbarPopoverItem( content={ {options.map((option) => { + if (option.menuItemType === 'divider') { + const { menuItemType, key, children, ...dividerProps } = option; + + return ; + } + const { data, text, tooltip = '', tooltipProps, disabled, + menuItemType, ...otherOptions } = option; return ( diff --git a/src/component/hooks/use_publication_strings.ts b/src/component/hooks/use_publication_strings.ts new file mode 100644 index 0000000000..ccb87faa1d --- /dev/null +++ b/src/component/hooks/use_publication_strings.ts @@ -0,0 +1,39 @@ +import { rangesToACS } from 'nmr-processing'; + +import { isSpectrum1D } from '../../data/data1d/Spectrum1D/index.ts'; + +import { useActiveNucleusTab } from './useActiveNucleusTab.ts'; +import { usePanelPreferences } from './usePanelPreferences.ts'; +import useSpectraByActiveNucleus from './useSpectraPerNucleus.ts'; + +export function usePublicationStrings() { + const spectra = useSpectraByActiveNucleus(); + const activeTab = useActiveNucleusTab(); + const rangesPreferences = usePanelPreferences('ranges', activeTab); + + const output: Record = {}; + + for (const spectrum of spectra) { + if (!isSpectrum1D(spectrum)) { + continue; + } + const { id: spectrumKey, info, ranges } = spectrum; + + if (!Array.isArray(ranges?.values) || ranges.values.length === 0) { + continue; + } + + const { originFrequency: observedFrequency, nucleus } = info; + + const value = rangesToACS(ranges.values, { + nucleus, // '19f' + deltaFormat: rangesPreferences.deltaPPM.format, + couplingFormat: rangesPreferences.coupling.format, + observedFrequency, //400 + }); + + output[spectrumKey] = value; + } + + return output; +} diff --git a/src/component/modal/PublicationStringModal.tsx b/src/component/modal/PublicationStringModal.tsx index 4c141aed50..3a9e539826 100644 --- a/src/component/modal/PublicationStringModal.tsx +++ b/src/component/modal/PublicationStringModal.tsx @@ -4,7 +4,6 @@ import { yupResolver } from '@hookform/resolvers/yup'; import type { ACSExportOptions } from '@zakodium/nmrium-core'; import { rangesToACS } from 'nmr-processing'; import { useForm, useWatch } from 'react-hook-form'; -import { FaCopy } from 'react-icons/fa'; import * as Yup from 'yup'; import { isSpectrum1D } from '../../data/data1d/Spectrum1D/isSpectrum1D.js'; @@ -150,12 +149,15 @@ function InnerPublicationStringModal(props: InnerPublicationStringModalProps) { ...(format !== 'D' ? { format, couplingFormat } : { format: '' }), }); - function handleCopy() { + function onCopy() { + onCopyClick(value); + } + + function onSave() { dispatch({ type: 'CHANGE_EXPORT_ACS_SETTINGS', payload: { options: options as ACSExportOptions, nucleus }, }); - onCopyClick(value); onClose(); } @@ -195,14 +197,27 @@ function InnerPublicationStringModal(props: InnerPublicationStringModalProps) { {!value ? ( ) : ( - // eslint-disable-next-line react/no-danger -
+ <> + + {/* eslint-disable-next-line react/no-danger */} +
+ )} - - + } + /> ); } + +const CopyPreviewButton = styled(Button)` + float: right; + margin-left: 5px; + margin-bottom: 5px; +`; diff --git a/src/component/panels/RangesPanel/RangesHeader.tsx b/src/component/panels/RangesPanel/RangesHeader.tsx index c56002d49f..349f676e6a 100644 --- a/src/component/panels/RangesPanel/RangesHeader.tsx +++ b/src/component/panels/RangesPanel/RangesHeader.tsx @@ -10,12 +10,15 @@ import { FaChartBar, FaCopy, FaDownload, + FaEye, + FaEyeSlash, FaFileExport, FaSitemap, FaUnlink, } from 'react-icons/fa'; import { ImLink } from 'react-icons/im'; import { LuMessageSquareText } from 'react-icons/lu'; +import { assert, assertUnreachable } from 'react-science/ui'; import { ClipboardFallbackModal } from '../../../utils/clipboard/clipboardComponents.js'; import { useClipboard } from '../../../utils/clipboard/clipboardHooks.js'; @@ -24,8 +27,10 @@ import { useToaster } from '../../context/ToasterContext.js'; import { useAlert } from '../../elements/Alert.js'; import type { ToolbarPopoverMenuItem } from '../../elements/ToolbarPopoverItem.js'; import { ToolbarPopoverItem } from '../../elements/ToolbarPopoverItem.js'; +import { useActiveSpectrum } from '../../hooks/useActiveSpectrum.ts'; import { useActiveSpectrumRangesViewState } from '../../hooks/useActiveSpectrumRangesViewState.js'; import { useDialogToggle } from '../../hooks/useDialogToggle.js'; +import { usePublicationStrings } from '../../hooks/use_publication_strings.ts'; import { PublicationStringModal } from '../../modal/PublicationStringModal.js'; import ChangeSumModal from '../../modal/changeSum/ChangeSumModal.js'; import { booleanToString } from '../../utility/booleanToString.js'; @@ -42,7 +47,7 @@ type ExportItem = ToolbarPopoverMenuItem; const EXPORT_MENU: ExportItem[] = [ { icon: , - text: 'Preview publication string', + text: 'Copy publication string', data: { id: 'publicationString', }, @@ -209,11 +214,17 @@ function RangesHeader(props: RangesHeaderProps) { saveAs({ blob, name, extension: '.tsv' }); } + const publicationStrings = usePublicationStrings(); + const currentSpectra = useActiveSpectrum(); + function exportHandler(data?: ExportData) { switch (data?.id) { - case 'publicationString': - openDialog('publicationStringModal'); + case 'publicationString': { + assert(currentSpectra?.id); + const publicationString = publicationStrings[currentSpectra?.id]; + saveToClipboardHandler(publicationString); break; + } case 'rangesToTSV': handleRangesToTSV(); break; @@ -312,11 +323,52 @@ function RangesHeader(props: RangesHeaderProps) { active: showAssignmentsLabels, }, { - disabled: !hasRanges, - icon: , - tooltip: `${booleanToString(!showPublicationString, { trueLabel: 'Display' })} publication string`, - onClick: handleShowPublicationString, - active: showPublicationString, + component: ( + + disabled={!hasRanges} + icon={} + tooltip="Publication string" + options={[ + { + menuItemType: 'divider', + key: 'divider-publication-string', + title: 'Publication string', + }, + { + icon: 'cog', + text: 'Configure', + data: { + id: 'publicationString', + }, + }, + { + icon: !showPublicationString ? : , + text: booleanToString(!showPublicationString, { + trueLabel: 'Show', + falseLabel: 'Hide', + }), + data: { + id: 'toggle', + }, + active: showPublicationString, + }, + ]} + onClick={(data) => { + assert(data?.id); + + switch (data?.id) { + case 'toggle': + handleShowPublicationString(); + break; + case 'publicationString': + openDialog('publicationStringModal'); + break; + default: + assertUnreachable(data?.id); + } + }} + /> + ), }, { disabled: !hasRanges, From 2af28caf90e3f9cecd0dafb040959f4d921f394b Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Tue, 3 Feb 2026 12:15:41 +0100 Subject: [PATCH 02/15] fix(ranges): publication string preview iso with chart feat: add checkbox to control publication string visibility in configuration dialog --- src/component/elements/ToolbarPopoverItem.tsx | 4 +- src/component/hooks/use_acs_settings.ts | 19 ++++++ .../hooks/use_publication_strings.ts | 62 ++++++++++++----- .../modal/PublicationStringModal.tsx | 67 ++++++++----------- .../panels/RangesPanel/RangesHeader.tsx | 18 ++++- 5 files changed, 108 insertions(+), 62 deletions(-) create mode 100644 src/component/hooks/use_acs_settings.ts diff --git a/src/component/elements/ToolbarPopoverItem.tsx b/src/component/elements/ToolbarPopoverItem.tsx index 4c536010e2..779dbcece3 100644 --- a/src/component/elements/ToolbarPopoverItem.tsx +++ b/src/component/elements/ToolbarPopoverItem.tsx @@ -14,7 +14,7 @@ export interface ToolbarPopoverMenuItem tooltip?: string | TooltipItem; } -interface ToolbarPopoverMenuDividerProps extends MenuDividerProps { +export interface ToolbarPopoverMenuDivider extends MenuDividerProps { menuItemType: 'divider'; key: string; } @@ -27,7 +27,7 @@ interface CustomToolbarPopoverItemProps 'tooltip' | 'icon' | 'tooltipProps' | 'active' | 'id' > { itemProps?: Omit; - options: Array | ToolbarPopoverMenuDividerProps>; + options: Array | ToolbarPopoverMenuDivider>; onClick?: (data?: T) => void; } diff --git a/src/component/hooks/use_acs_settings.ts b/src/component/hooks/use_acs_settings.ts new file mode 100644 index 0000000000..df70496386 --- /dev/null +++ b/src/component/hooks/use_acs_settings.ts @@ -0,0 +1,19 @@ +import type { ACSExportOptions } from '@zakodium/nmrium-core'; + +import { usePreferences } from '../context/PreferencesContext.tsx'; + +import { useActiveNucleusTab } from './useActiveNucleusTab.ts'; + +const defaultOptions: ACSExportOptions = { + signalKind: 'signal', + ascending: true, + format: 'IMJA', + couplingFormat: '0.00', + deltaFormat: '0.00', +}; + +export function useACSSettings() { + const nucleus = useActiveNucleusTab(); + const { current } = usePreferences(); + return current.acsExportSettings[nucleus] || defaultOptions; +} diff --git a/src/component/hooks/use_publication_strings.ts b/src/component/hooks/use_publication_strings.ts index ccb87faa1d..9fce42d4df 100644 --- a/src/component/hooks/use_publication_strings.ts +++ b/src/component/hooks/use_publication_strings.ts @@ -1,39 +1,67 @@ +import type { ACSExportOptions, Spectrum } from '@zakodium/nmrium-core'; +import type { RangesToACSOptions } from 'nmr-processing'; import { rangesToACS } from 'nmr-processing'; +import { assert } from 'react-science/ui'; import { isSpectrum1D } from '../../data/data1d/Spectrum1D/index.ts'; -import { useActiveNucleusTab } from './useActiveNucleusTab.ts'; -import { usePanelPreferences } from './usePanelPreferences.ts'; import useSpectraByActiveNucleus from './useSpectraPerNucleus.ts'; +import { useACSSettings } from './use_acs_settings.ts'; export function usePublicationStrings() { const spectra = useSpectraByActiveNucleus(); - const activeTab = useActiveNucleusTab(); - const rangesPreferences = usePanelPreferences('ranges', activeTab); + const acs = useACSSettings(); const output: Record = {}; for (const spectrum of spectra) { - if (!isSpectrum1D(spectrum)) { - continue; - } - const { id: spectrumKey, info, ranges } = spectrum; + if (!isSpectrum1D(spectrum)) continue; + + const { id, ranges } = spectrum; if (!Array.isArray(ranges?.values) || ranges.values.length === 0) { continue; } - const { originFrequency: observedFrequency, nucleus } = info; + output[id] = buildPublicationString({ spectrum, acs }); + } + + return output; +} + +interface BuildPublicationStringOptions { + spectrum: Spectrum; + acs: ACSExportOptions; +} +export function buildPublicationString(options: BuildPublicationStringOptions) { + const { spectrum, acs } = options; + assert(isSpectrum1D(spectrum)); + + const { + info, + ranges: { values }, + } = spectrum; + const { originFrequency, nucleus } = info; + const { signalKind, format, couplingFormat, ...otherOptions } = acs; + + const ranges = + signalKind === 'all' + ? values + : values.filter((range) => + range.signals?.some((signal) => signal.kind === 'signal'), + ); - const value = rangesToACS(ranges.values, { - nucleus, // '19f' - deltaFormat: rangesPreferences.deltaPPM.format, - couplingFormat: rangesPreferences.coupling.format, - observedFrequency, //400 - }); + const rangesToACSOptions: RangesToACSOptions = { + nucleus, + observedFrequency: originFrequency, + ...otherOptions, + format: '', + }; - output[spectrumKey] = value; + if (format !== 'D') { + rangesToACSOptions.format = format; + rangesToACSOptions.couplingFormat = couplingFormat; } - return output; + return rangesToACS(ranges, rangesToACSOptions); } diff --git a/src/component/modal/PublicationStringModal.tsx b/src/component/modal/PublicationStringModal.tsx index 3a9e539826..7a55374233 100644 --- a/src/component/modal/PublicationStringModal.tsx +++ b/src/component/modal/PublicationStringModal.tsx @@ -1,8 +1,7 @@ -import { Button, Dialog, DialogFooter } from '@blueprintjs/core'; +import { Button, Checkbox, Dialog, DialogFooter } from '@blueprintjs/core'; import styled from '@emotion/styled'; import { yupResolver } from '@hookform/resolvers/yup'; import type { ACSExportOptions } from '@zakodium/nmrium-core'; -import { rangesToACS } from 'nmr-processing'; import { useForm, useWatch } from 'react-hook-form'; import * as Yup from 'yup'; @@ -15,8 +14,9 @@ import type { LabelStyle } from '../elements/Label.js'; import Label from '../elements/Label.js'; import { Select2Controller } from '../elements/Select2Controller.tsx'; import { StyledDialogBody } from '../elements/StyledDialogBody.js'; -import { useActiveNucleusTab } from '../hooks/useActiveNucleusTab.ts'; import useSpectrum from '../hooks/useSpectrum.js'; +import { useACSSettings } from '../hooks/use_acs_settings.ts'; +import { buildPublicationString } from '../hooks/use_publication_strings.ts'; const Body = styled.div` border: 1px solid #e9e9e9; @@ -84,6 +84,9 @@ const exportFormats: Array> = [ interface InnerPublicationStringModalProps { onClose: () => void; onCopyClick: (text: string) => void; + + isPublicationStringShown: boolean; + togglePublicationStringVisibility: () => void; } interface PublicationStringModalProps extends InnerPublicationStringModalProps { @@ -98,22 +101,13 @@ export function PublicationStringModal(props: PublicationStringModalProps) { return ; } -const defaultOptions = { - signalKind: 'signal', - ascending: true, - format: 'IMJA', - couplingFormat: '0.00', - deltaFormat: '0.00', -}; - -function useACSSettings() { - const nucleus = useActiveNucleusTab(); - const { current } = usePreferences(); - return current.acsExportSettings[nucleus] || defaultOptions; -} - function InnerPublicationStringModal(props: InnerPublicationStringModalProps) { - const { onClose, onCopyClick } = props; + const { + onClose, + onCopyClick, + isPublicationStringShown, + togglePublicationStringVisibility, + } = props; const spectrum = useSpectrum(); const { dispatch } = usePreferences(); const currentACSOptions = useACSSettings(); @@ -126,33 +120,16 @@ function InnerPublicationStringModal(props: InnerPublicationStringModalProps) { if (!spectrum || !isSpectrum1D(spectrum)) return null; - const { - info, - ranges: { values }, - } = spectrum; - - const { originFrequency: observedFrequency, nucleus } = info; - - const { signalKind, format, couplingFormat, ...otherOptions } = options; - - const ranges = - signalKind === 'all' - ? values - : values.filter((range) => - range.signals?.some((signal) => signal.kind === 'signal'), - ); - - const value = rangesToACS(ranges, { - nucleus, - observedFrequency, - ...otherOptions, - ...(format !== 'D' ? { format, couplingFormat } : { format: '' }), + const value = buildPublicationString({ + spectrum, + acs: { ...currentACSOptions, ...options }, }); function onCopy() { onCopyClick(value); } + const nucleus = spectrum.info.nucleus; function onSave() { dispatch({ type: 'CHANGE_EXPORT_ACS_SETTINGS', @@ -211,7 +188,13 @@ function InnerPublicationStringModal(props: InnerPublicationStringModalProps) { Apply and close } - /> + > + + ); } @@ -221,3 +204,7 @@ const CopyPreviewButton = styled(Button)` margin-left: 5px; margin-bottom: 5px; `; + +const PublicationStringCheckbox = styled(Checkbox)` + display: inline-block; +`; diff --git a/src/component/panels/RangesPanel/RangesHeader.tsx b/src/component/panels/RangesPanel/RangesHeader.tsx index 349f676e6a..7863ef881f 100644 --- a/src/component/panels/RangesPanel/RangesHeader.tsx +++ b/src/component/panels/RangesPanel/RangesHeader.tsx @@ -25,7 +25,10 @@ import { useClipboard } from '../../../utils/clipboard/clipboardHooks.js'; import { useDispatch } from '../../context/DispatchContext.js'; import { useToaster } from '../../context/ToasterContext.js'; import { useAlert } from '../../elements/Alert.js'; -import type { ToolbarPopoverMenuItem } from '../../elements/ToolbarPopoverItem.js'; +import type { + ToolbarPopoverMenuDivider, + ToolbarPopoverMenuItem, +} from '../../elements/ToolbarPopoverItem.js'; import { ToolbarPopoverItem } from '../../elements/ToolbarPopoverItem.js'; import { useActiveSpectrum } from '../../hooks/useActiveSpectrum.ts'; import { useActiveSpectrumRangesViewState } from '../../hooks/useActiveSpectrumRangesViewState.js'; @@ -42,9 +45,16 @@ type ExportRangesType = 'publicationString' | 'rangesToTSV'; interface ExportData { id: ExportRangesType; } -type ExportItem = ToolbarPopoverMenuItem; +type ExportItem = + | ToolbarPopoverMenuItem + | ToolbarPopoverMenuDivider; const EXPORT_MENU: ExportItem[] = [ + { + key: 'export-divider', + menuItemType: 'divider', + title: 'Export', + }, { icon: , text: 'Copy publication string', @@ -242,7 +252,7 @@ function RangesHeader(props: RangesHeaderProps) { disabled={!hasRanges} icon={} - tooltip="Export as" + tooltip="Export" options={EXPORT_MENU} onClick={exportHandler} /> @@ -385,6 +395,8 @@ function RangesHeader(props: RangesHeaderProps) { isOpen={dialog.publicationStringModal} onCopyClick={saveToClipboardHandler} onClose={closeDialog} + isPublicationStringShown={showPublicationString} + togglePublicationStringVisibility={handleShowPublicationString} /> Date: Tue, 3 Feb 2026 14:47:21 +0100 Subject: [PATCH 03/15] refactor(ranges): publication string form use tanstack form --- package-lock.json | 8 +- package.json | 2 +- .../modal/PublicationStringModal.tsx | 215 +++++++++--------- 3 files changed, 115 insertions(+), 110 deletions(-) diff --git a/package-lock.json b/package-lock.json index c9a30ef32d..9db0eac005 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,7 +59,7 @@ "react-ocl-nmr": "^4.1.1", "react-plot": "^3.1.2", "react-rnd": "^10.5.2", - "react-science": "^19.5.1", + "react-science": "^19.7.0", "react-table": "^7.8.0", "smart-array-filter": "^5.0.0", "yup": "^1.7.1", @@ -10642,9 +10642,9 @@ } }, "node_modules/react-science": { - "version": "19.5.1", - "resolved": "https://registry.npmjs.org/react-science/-/react-science-19.5.1.tgz", - "integrity": "sha512-pLKU2o3mmg27v/2WkLfX3e6F+eW15lxCXn3zNIj3Sw5wGmixJeXXWDrFO9WcKekHF1GyVznH7Ruh7sCfWvqj4w==", + "version": "19.7.0", + "resolved": "https://registry.npmjs.org/react-science/-/react-science-19.7.0.tgz", + "integrity": "sha512-FnNWsnNVciVlFD3OYd75jGYFHLkjAwvTSSI8WES1U6zJnEkCOFeZ7c0/nLoK+RLraIERf9/MolIHa7N6yjGzQg==", "license": "MIT", "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "^1.7.7", diff --git a/package.json b/package.json index e02ab2b8d9..ff7ce7766d 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,7 @@ "react-ocl-nmr": "^4.1.1", "react-plot": "^3.1.2", "react-rnd": "^10.5.2", - "react-science": "^19.5.1", + "react-science": "^19.7.0", "react-table": "^7.8.0", "smart-array-filter": "^5.0.0", "yup": "^1.7.1", diff --git a/src/component/modal/PublicationStringModal.tsx b/src/component/modal/PublicationStringModal.tsx index 7a55374233..e7c6b49f55 100644 --- a/src/component/modal/PublicationStringModal.tsx +++ b/src/component/modal/PublicationStringModal.tsx @@ -1,18 +1,13 @@ import { Button, Checkbox, Dialog, DialogFooter } from '@blueprintjs/core'; import styled from '@emotion/styled'; -import { yupResolver } from '@hookform/resolvers/yup'; -import type { ACSExportOptions } from '@zakodium/nmrium-core'; -import { useForm, useWatch } from 'react-hook-form'; -import * as Yup from 'yup'; +import type { ACSExportOptions, Spectrum1D } from '@zakodium/nmrium-core'; +import type { FormEvent } from 'react'; +import { Form, assert, useForm } from 'react-science/ui'; +import { z } from 'zod'; import { isSpectrum1D } from '../../data/data1d/Spectrum1D/isSpectrum1D.js'; import { usePreferences } from '../context/PreferencesContext.tsx'; -import { CheckController } from '../elements/CheckController.tsx'; import { EmptyText } from '../elements/EmptyText.js'; -import { Input2Controller } from '../elements/Input2Controller.tsx'; -import type { LabelStyle } from '../elements/Label.js'; -import Label from '../elements/Label.js'; -import { Select2Controller } from '../elements/Select2Controller.tsx'; import { StyledDialogBody } from '../elements/StyledDialogBody.js'; import useSpectrum from '../hooks/useSpectrum.js'; import { useACSSettings } from '../hooks/use_acs_settings.ts'; @@ -23,20 +18,9 @@ const Body = styled.div` min-height: 180px; padding: 5px; width: 100%; + margin-top: 15px; `; -const labelStyle: LabelStyle = { - label: { - color: '#232323', - width: '150px', - }, - wrapper: { - display: 'flex', - justifyContent: 'flex-start', - }, - container: { padding: '5px 0' }, -}; - interface SelectItem { label: string; value: T; @@ -46,14 +30,12 @@ type ExportFormatType = 'IMJA' | 'IMJ' | 'D'; type ExportSignalKind = ACSExportOptions['signalKind']; -const validationSchema = Yup.object().shape({ - signalKind: Yup.string() - .oneOf(['all', 'signal'] as ExportSignalKind[]) - .required(), - ascending: Yup.boolean().required(), - format: Yup.string().required(), - couplingFormat: Yup.string().required(), - deltaFormat: Yup.string().required(), +const validationSchema = z.object({ + signalKind: z.enum(['all', 'signal']), + ascending: z.boolean(), + format: z.enum(['IMJA', 'IMJ', 'D']), + couplingFormat: z.string(), + deltaFormat: z.string(), }); const exportOptions: Array> = [ @@ -111,94 +93,95 @@ function InnerPublicationStringModal(props: InnerPublicationStringModalProps) { const spectrum = useSpectrum(); const { dispatch } = usePreferences(); const currentACSOptions = useACSSettings(); - const { control } = useForm({ + const form = useForm({ defaultValues: currentACSOptions, - resolver: yupResolver(validationSchema), + validators: { onChange: validationSchema }, + onSubmit: ({ value }) => { + assert(spectrum && isSpectrum1D(spectrum)); + const nucleus = spectrum.info.nucleus; + + dispatch({ + type: 'CHANGE_EXPORT_ACS_SETTINGS', + payload: { options: value, nucleus }, + }); + onClose(); + }, }); - const options = useWatch({ control }); - if (!spectrum || !isSpectrum1D(spectrum)) return null; - const value = buildPublicationString({ - spectrum, - acs: { ...currentACSOptions, ...options }, - }); - - function onCopy() { - onCopyClick(value); - } - - const nucleus = spectrum.info.nucleus; - function onSave() { - dispatch({ - type: 'CHANGE_EXPORT_ACS_SETTINGS', - payload: { options: options as ACSExportOptions, nucleus }, - }); - onClose(); + function onSubmit(event: FormEvent) { + event.preventDefault(); + void form.handleSubmit(event); } return ( - - - - - - - - - - {!value ? ( - - ) : ( - <> - - {/* eslint-disable-next-line react/no-danger */} -
- - )} - - - - Apply and close - - } + + - - -
+
+ + + + {(field) => ( + + )} + + + {(field) => ( + + )} + + + {(field) => } + + + {(field) => } + + + {(field) => } + + + + + s.values}> + {(acs) => ( + + )} + + + + + Apply and close + + } + > + + +
+ + ); } +const DialogBodyStyled = styled(StyledDialogBody)` + margin-top: -15px; +`; + const CopyPreviewButton = styled(Button)` float: right; margin-left: 5px; @@ -208,3 +191,25 @@ const CopyPreviewButton = styled(Button)` const PublicationStringCheckbox = styled(Checkbox)` display: inline-block; `; + +interface PublicationStringPreviewProps { + spectrum: Spectrum1D; + acs: ACSExportOptions; + onCopy: (value: string) => void; +} + +function PublicationStringPreview(props: PublicationStringPreviewProps) { + const { spectrum, acs, onCopy } = props; + + const value = buildPublicationString({ spectrum, acs }); + + if (!value) return ; + + return ( + <> + onCopy(value)} icon="duplicate" /> + {/*eslint-disable-next-line react/no-danger*/} +
+ + ); +} From 2bf8b899f6614b85b24f92c5b1ce6f31db2a7de3 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:21:35 +0100 Subject: [PATCH 04/15] wip: add textStyle group Needs to update ACSExportOptions type from core --- .../modal/PublicationStringModal.tsx | 74 +++++++++++-------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/src/component/modal/PublicationStringModal.tsx b/src/component/modal/PublicationStringModal.tsx index e7c6b49f55..323473a335 100644 --- a/src/component/modal/PublicationStringModal.tsx +++ b/src/component/modal/PublicationStringModal.tsx @@ -2,7 +2,13 @@ import { Button, Checkbox, Dialog, DialogFooter } from '@blueprintjs/core'; import styled from '@emotion/styled'; import type { ACSExportOptions, Spectrum1D } from '@zakodium/nmrium-core'; import type { FormEvent } from 'react'; -import { Form, assert, useForm } from 'react-science/ui'; +import { + FieldGroupSVGTextStyleFields, + Form, + assert, + svgTextStyleFieldsSchema, + useForm, +} from 'react-science/ui'; import { z } from 'zod'; import { isSpectrum1D } from '../../data/data1d/Spectrum1D/isSpectrum1D.js'; @@ -36,6 +42,7 @@ const validationSchema = z.object({ format: z.enum(['IMJA', 'IMJ', 'D']), couplingFormat: z.string(), deltaFormat: z.string(), + textStyle: svgTextStyleFieldsSchema, }); const exportOptions: Array> = [ @@ -94,7 +101,15 @@ function InnerPublicationStringModal(props: InnerPublicationStringModalProps) { const { dispatch } = usePreferences(); const currentACSOptions = useACSSettings(); const form = useForm({ - defaultValues: currentACSOptions, + defaultValues: { + ...currentACSOptions, + textStyle: { + fill: '#000000', + fontSize: '16', + fontStyle: 'normal', + fontWeight: 'normal', + } as z.input, + }, validators: { onChange: validationSchema }, onSubmit: ({ value }) => { assert(spectrum && isSpectrum1D(spectrum)); @@ -124,28 +139,33 @@ function InnerPublicationStringModal(props: InnerPublicationStringModalProps) { style={{ minWidth: 600 }} >
- - - - {(field) => ( - - )} - - - {(field) => ( - - )} - - - {(field) => } - - - {(field) => } - - - {(field) => } - - + + + {(field) => ( + + )} + + + {(field) => ( + + )} + + + {(field) => } + + + {(field) => } + + + {(field) => } + + + s.values}> @@ -158,7 +178,7 @@ function InnerPublicationStringModal(props: InnerPublicationStringModalProps) { )} - + @@ -178,10 +198,6 @@ function InnerPublicationStringModal(props: InnerPublicationStringModalProps) { ); } -const DialogBodyStyled = styled(StyledDialogBody)` - margin-top: -15px; -`; - const CopyPreviewButton = styled(Button)` float: right; margin-left: 5px; From cbf4e68e1adff11205d9ee7a1b6773184ef07109 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Tue, 3 Feb 2026 17:06:40 +0100 Subject: [PATCH 05/15] feat: integrate `textStyle` from `ACSExportOptions` core type --- src/component/hooks/use_acs_settings.ts | 6 +++++ .../modal/PublicationStringModal.tsx | 26 +++++++------------ .../workspaces/workspaceDefaultProperties.ts | 11 +++++++- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/component/hooks/use_acs_settings.ts b/src/component/hooks/use_acs_settings.ts index df70496386..29d12948c2 100644 --- a/src/component/hooks/use_acs_settings.ts +++ b/src/component/hooks/use_acs_settings.ts @@ -10,6 +10,12 @@ const defaultOptions: ACSExportOptions = { format: 'IMJA', couplingFormat: '0.00', deltaFormat: '0.00', + textStyle: { + fill: '#000000', + fontSize: 16, + fontStyle: 'normal', + fontWeight: 'normal', + }, }; export function useACSSettings() { diff --git a/src/component/modal/PublicationStringModal.tsx b/src/component/modal/PublicationStringModal.tsx index 323473a335..f3714fd680 100644 --- a/src/component/modal/PublicationStringModal.tsx +++ b/src/component/modal/PublicationStringModal.tsx @@ -39,7 +39,7 @@ type ExportSignalKind = ACSExportOptions['signalKind']; const validationSchema = z.object({ signalKind: z.enum(['all', 'signal']), ascending: z.boolean(), - format: z.enum(['IMJA', 'IMJ', 'D']), + format: z.string(), couplingFormat: z.string(), deltaFormat: z.string(), textStyle: svgTextStyleFieldsSchema, @@ -101,23 +101,16 @@ function InnerPublicationStringModal(props: InnerPublicationStringModalProps) { const { dispatch } = usePreferences(); const currentACSOptions = useACSSettings(); const form = useForm({ - defaultValues: { - ...currentACSOptions, - textStyle: { - fill: '#000000', - fontSize: '16', - fontStyle: 'normal', - fontWeight: 'normal', - } as z.input, - }, + defaultValues: validationSchema.encode(currentACSOptions), validators: { onChange: validationSchema }, onSubmit: ({ value }) => { assert(spectrum && isSpectrum1D(spectrum)); const nucleus = spectrum.info.nucleus; + const options = validationSchema.parse(value); dispatch({ type: 'CHANGE_EXPORT_ACS_SETTINGS', - payload: { options: value, nucleus }, + payload: { options, nucleus }, }); onClose(); }, @@ -169,9 +162,9 @@ function InnerPublicationStringModal(props: InnerPublicationStringModalProps) { s.values}> - {(acs) => ( + {(values) => ( @@ -210,13 +203,14 @@ const PublicationStringCheckbox = styled(Checkbox)` interface PublicationStringPreviewProps { spectrum: Spectrum1D; - acs: ACSExportOptions; + values: z.input; onCopy: (value: string) => void; } function PublicationStringPreview(props: PublicationStringPreviewProps) { - const { spectrum, acs, onCopy } = props; + const { spectrum, values, onCopy } = props; + const acs = validationSchema.parse(values); const value = buildPublicationString({ spectrum, acs }); if (!value) return ; @@ -224,7 +218,7 @@ function PublicationStringPreview(props: PublicationStringPreviewProps) { return ( <> onCopy(value)} icon="duplicate" /> - {/*eslint-disable-next-line react/no-danger*/} + {/* eslint-disable-next-line react/no-danger */}
); diff --git a/src/component/workspaces/workspaceDefaultProperties.ts b/src/component/workspaces/workspaceDefaultProperties.ts index 047914beec..cd6d5a4905 100644 --- a/src/component/workspaces/workspaceDefaultProperties.ts +++ b/src/component/workspaces/workspaceDefaultProperties.ts @@ -1,8 +1,15 @@ -import type { WorkspacePreferences } from '@zakodium/nmrium-core'; +import type { TextStyle, WorkspacePreferences } from '@zakodium/nmrium-core'; import { Filters1D } from 'nmr-processing'; import { color2D } from '../../data/data2d/Spectrum2D/get2DColor.js'; +const defaultTextStyle: TextStyle = { + fill: '#000000', + fontSize: 16, + fontStyle: 'normal', + fontWeight: 'normal', +}; + export const workspaceDefaultProperties: Required = { display: { general: { @@ -267,6 +274,7 @@ export const workspaceDefaultProperties: Required = { format: 'D', couplingFormat: '0.0', deltaFormat: '0.0', + textStyle: { ...defaultTextStyle }, }, '1H': { signalKind: 'signal', @@ -274,6 +282,7 @@ export const workspaceDefaultProperties: Required = { format: 'IMJA', couplingFormat: '0.00', deltaFormat: '0.00', + textStyle: { ...defaultTextStyle }, }, }, defaultMoleculeSettings: { From 9a7cef39388bcb485cb84c707090650e762c9209 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:44:25 +0100 Subject: [PATCH 06/15] chore: use dedicated `vitest.config.ts` file test: add type check equality with `TextStyle` and `z.output` --- src/__tests__/lib_interoperability.test-d.ts | 12 +++++++ tsconfig.json | 3 +- vite.config.ts | 3 -- vitest.config.ts | 33 ++++++++++++++++++++ 4 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 src/__tests__/lib_interoperability.test-d.ts create mode 100644 vitest.config.ts diff --git a/src/__tests__/lib_interoperability.test-d.ts b/src/__tests__/lib_interoperability.test-d.ts new file mode 100644 index 0000000000..0cc458a4c7 --- /dev/null +++ b/src/__tests__/lib_interoperability.test-d.ts @@ -0,0 +1,12 @@ +import type { TextStyle } from '@zakodium/nmrium-core'; +import type { svgTextStyleFieldsSchema } from 'react-science/ui'; +import { describe, expectTypeOf, it } from 'vitest'; +import type { z } from 'zod'; + +describe('interoperability between nmrium-core and react-science', () => { + it('TextStyle should match `z.output`', () => { + expectTypeOf().toEqualTypeOf< + z.output + >(); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 76a7a728e7..a16f1017b2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,7 @@ "src", "test-e2e", "knip.config.ts", - "vite.config.ts" + "vite.config.ts", + "vitest.config.ts" ] } diff --git a/vite.config.ts b/vite.config.ts index fd567bd80f..50935d43e8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -53,9 +53,6 @@ export default () => { : undefined, alias: resolveAliases, }, - test: { - include: ['./src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - }, }); }; diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000000..6cd2566a8e --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,33 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { defaultClientConditions } from 'vite'; +import { defineConfig } from 'vitest/config'; + +export default () => { + const isMonorepo = checkMonorepo(); + + return defineConfig({ + base: './', + ssr: { + resolve: { + conditions: isMonorepo + ? ['nmrium-internal', ...defaultClientConditions] + : undefined, + }, + }, + test: { + include: [ + './src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}', + './src/**/*.test-d.ts', + ], + }, + }); +}; + +function checkMonorepo() { + const monorepoPkg = path.join(import.meta.dirname, '..', 'package.json'); + if (!fs.existsSync(monorepoPkg)) return false; + const pkg = JSON.parse(fs.readFileSync(monorepoPkg, 'utf8')); + return pkg.name === '@zakodium/nmrium-monorepo'; +} From 95d7d6b5f83b931c230d91113631852aa7d91195 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:50:59 +0100 Subject: [PATCH 07/15] fix: vitest typecheck config --- vitest.config.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/vitest.config.ts b/vitest.config.ts index 6cd2566a8e..10932562f6 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -17,10 +17,11 @@ export default () => { }, }, test: { - include: [ - './src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}', - './src/**/*.test-d.ts', - ], + include: ['./src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + typecheck: { + enabled: true, + include: ['./src/**/*.test-d.ts'], + }, }, }); }; From 66cd66b958bfb6b631fd4e975657b2507f2571f6 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:10:48 +0100 Subject: [PATCH 08/15] fix: update react-science and remove unneeded default values --- package.json | 2 +- src/__tests__/lib_interoperability.test-d.ts | 9 +++------ src/component/hooks/use_acs_settings.ts | 7 +------ src/component/workspaces/workspaceDefaultProperties.ts | 7 +------ 4 files changed, 6 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index ff7ce7766d..5b8f0fc625 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,7 @@ "react-ocl-nmr": "^4.1.1", "react-plot": "^3.1.2", "react-rnd": "^10.5.2", - "react-science": "^19.7.0", + "react-science": "^19.7.1", "react-table": "^7.8.0", "smart-array-filter": "^5.0.0", "yup": "^1.7.1", diff --git a/src/__tests__/lib_interoperability.test-d.ts b/src/__tests__/lib_interoperability.test-d.ts index 0cc458a4c7..a51bcd3571 100644 --- a/src/__tests__/lib_interoperability.test-d.ts +++ b/src/__tests__/lib_interoperability.test-d.ts @@ -1,12 +1,9 @@ import type { TextStyle } from '@zakodium/nmrium-core'; -import type { svgTextStyleFieldsSchema } from 'react-science/ui'; +import type { SVGStyledTextUserConfig } from 'react-science/ui'; import { describe, expectTypeOf, it } from 'vitest'; -import type { z } from 'zod'; describe('interoperability between nmrium-core and react-science', () => { - it('TextStyle should match `z.output`', () => { - expectTypeOf().toEqualTypeOf< - z.output - >(); + it('TextStyle should match `SVGStyledTextUserConfig`', () => { + expectTypeOf().toEqualTypeOf(); }); }); diff --git a/src/component/hooks/use_acs_settings.ts b/src/component/hooks/use_acs_settings.ts index 29d12948c2..47dc2502c0 100644 --- a/src/component/hooks/use_acs_settings.ts +++ b/src/component/hooks/use_acs_settings.ts @@ -10,12 +10,7 @@ const defaultOptions: ACSExportOptions = { format: 'IMJA', couplingFormat: '0.00', deltaFormat: '0.00', - textStyle: { - fill: '#000000', - fontSize: 16, - fontStyle: 'normal', - fontWeight: 'normal', - }, + textStyle: {}, }; export function useACSSettings() { diff --git a/src/component/workspaces/workspaceDefaultProperties.ts b/src/component/workspaces/workspaceDefaultProperties.ts index cd6d5a4905..8395bdf84c 100644 --- a/src/component/workspaces/workspaceDefaultProperties.ts +++ b/src/component/workspaces/workspaceDefaultProperties.ts @@ -3,12 +3,7 @@ import { Filters1D } from 'nmr-processing'; import { color2D } from '../../data/data2d/Spectrum2D/get2DColor.js'; -const defaultTextStyle: TextStyle = { - fill: '#000000', - fontSize: 16, - fontStyle: 'normal', - fontWeight: 'normal', -}; +const defaultTextStyle: TextStyle = {}; export const workspaceDefaultProperties: Required = { display: { From e9e4bc40fd6241d07545d0aa5e7eb4240ed03e38 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:33:47 +0100 Subject: [PATCH 09/15] chore: move `lib_interoperability.test-d.ts` to core --- src/__tests__/lib_interoperability.test-d.ts | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 src/__tests__/lib_interoperability.test-d.ts diff --git a/src/__tests__/lib_interoperability.test-d.ts b/src/__tests__/lib_interoperability.test-d.ts deleted file mode 100644 index a51bcd3571..0000000000 --- a/src/__tests__/lib_interoperability.test-d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { TextStyle } from '@zakodium/nmrium-core'; -import type { SVGStyledTextUserConfig } from 'react-science/ui'; -import { describe, expectTypeOf, it } from 'vitest'; - -describe('interoperability between nmrium-core and react-science', () => { - it('TextStyle should match `SVGStyledTextUserConfig`', () => { - expectTypeOf().toEqualTypeOf(); - }); -}); From 4e04ff95de721b3e4068e58887523add2b67989e Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:41:57 +0100 Subject: [PATCH 10/15] fix: update core --- package-lock.json | 58 +++++++++++++++++++++++------------------------ package.json | 6 ++--- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9db0eac005..1aaa6b55d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,9 +15,9 @@ "@emotion/styled": "^11.14.1", "@hookform/resolvers": "^5.2.2", "@tanstack/react-form": "^1.27.7", - "@zakodium/nmr-types": "^0.5.0", - "@zakodium/nmrium-core": "^0.6.0", - "@zakodium/nmrium-core-plugins": "^0.6.29", + "@zakodium/nmr-types": "^0.5.1", + "@zakodium/nmrium-core": "^0.6.1", + "@zakodium/nmrium-core-plugins": "^0.6.33", "@zakodium/pdnd-esm": "^1.0.2", "@zip.js/zip.js": "^2.8.15", "cheminfo-font": "^1.13.1", @@ -59,7 +59,7 @@ "react-ocl-nmr": "^4.1.1", "react-plot": "^3.1.2", "react-rnd": "^10.5.2", - "react-science": "^19.7.0", + "react-science": "^19.7.1", "react-table": "^7.8.0", "smart-array-filter": "^5.0.0", "yup": "^1.7.1", @@ -2214,9 +2214,9 @@ } }, "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", + "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3970,20 +3970,20 @@ } }, "node_modules/@zakodium/nmr-types": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@zakodium/nmr-types/-/nmr-types-0.5.0.tgz", - "integrity": "sha512-GjxJ8JQJ3DOzKY/ucxvCJ0X3nlGl8Qk9J6Kng4XTm+rZSKgwj2Xu98IDSZvpJKxr9v0bS0reXfmvNRvSe+fPgg==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@zakodium/nmr-types/-/nmr-types-0.5.1.tgz", + "integrity": "sha512-thBE0P2fpTKyMHXGVggYyb8VMkqPXN/7+t6L6rGjwqfI403dkuRkv7oiy3e8xixkNUbJ98R6DwRuAyc4Xi3I/w==", "license": "CC-BY-NC-SA-4.0", "dependencies": { "ml-peak-shape-generator": "^4.2.0", "ml-signal-processing": "^2.1.0", - "ml-spectra-processing": "^14.18.1" + "ml-spectra-processing": "^14.18.2" } }, "node_modules/@zakodium/nmrium-core": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@zakodium/nmrium-core/-/nmrium-core-0.6.0.tgz", - "integrity": "sha512-pPzwsfH4PdxGNYWM5ouNFk2dVUhd85bnWNOhgFIrqpWqKwt1W2Mxy56E4LEIgg4iDwCZaAT2Eq4Ga8UOk8buHg==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@zakodium/nmrium-core/-/nmrium-core-0.6.2.tgz", + "integrity": "sha512-YU+OV96lxVS/kGUiOp7I9vGJNxTV4A7BpoHi7abPJ0/pVe9AaUU8va/6YAQWLLM/IVDcERWMvu7mLIEvv9Rxvg==", "license": "CC-BY-NC-SA-4.0", "dependencies": { "cheminfo-types": "^1.8.1", @@ -3996,13 +3996,13 @@ } }, "node_modules/@zakodium/nmrium-core-plugins": { - "version": "0.6.29", - "resolved": "https://registry.npmjs.org/@zakodium/nmrium-core-plugins/-/nmrium-core-plugins-0.6.29.tgz", - "integrity": "sha512-TXwBm7/PdxGA+GNULn2MScge9T14ovGkdToCyyCQwAObsfNGm3CTANJxax9pfvQHqf4u0mTgl8AbN5Pse5qVbA==", + "version": "0.6.34", + "resolved": "https://registry.npmjs.org/@zakodium/nmrium-core-plugins/-/nmrium-core-plugins-0.6.34.tgz", + "integrity": "sha512-yp2Y+oqtQBbHroWT6NNFXohpLuVEeYNwhKXu3Mx3bOveyERQ+ql12VqqaeBEtHvGNqRSk4JZPpzjkrC1mqTUpw==", "license": "CC-BY-NC-SA-4.0", "dependencies": { "@date-fns/utc": "^2.1.1", - "@zakodium/nmrium-core": "^0.6.0", + "@zakodium/nmrium-core": "^0.6.2", "cheminfo-types": "^1.8.1", "convert-to-jcamp": "^6.0.0", "date-fns": "^4.1.0", @@ -4013,9 +4013,9 @@ "linear-sum-assignment": "^1.0.9", "lodash.merge": "^4.6.2", "ml-spectra-processing": "^14.18.2", - "nmr-processing": "^22.1.0", - "openchemlib": "^9.18.2", - "openchemlib-utils": "^8.8.1", + "nmr-processing": "^22.4.0", + "openchemlib": "^9.19.0", + "openchemlib-utils": "^8.12.1", "sdf-parser": "^7.0.4" } }, @@ -9360,9 +9360,9 @@ } }, "node_modules/nmr-processing": { - "version": "22.1.0", - "resolved": "https://registry.npmjs.org/nmr-processing/-/nmr-processing-22.1.0.tgz", - "integrity": "sha512-l/IFlmY6akhsIolRMMWnG4Mpp7KMqwidZJR7NEWNkySmKYI8NhVMNBiCbc3e3QSu4yVyhl0hEU7ToHobpDgpPg==", + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/nmr-processing/-/nmr-processing-22.4.0.tgz", + "integrity": "sha512-+CAGAl70rgoN6QL+WwN+aUq7lH1NR3VoXjRdDGdZiTfo7h2LrtfO7rD+e0yraW+mr39ZrjmvaSU4dyNJNNqs5g==", "license": "CC-BY-NC-SA-4.0", "dependencies": { "binary-search": "^1.3.6", @@ -9394,8 +9394,8 @@ "multiplet-analysis": "^2.1.5", "nmr-correlation": "^3.0.0", "numeral": "^2.0.6", - "openchemlib": "^9.18.2", - "openchemlib-utils": "^8.8.1", + "openchemlib": "^9.19.0", + "openchemlib-utils": "^8.12.1", "spectrum-generator": "^8.1.1" } }, @@ -10642,9 +10642,9 @@ } }, "node_modules/react-science": { - "version": "19.7.0", - "resolved": "https://registry.npmjs.org/react-science/-/react-science-19.7.0.tgz", - "integrity": "sha512-FnNWsnNVciVlFD3OYd75jGYFHLkjAwvTSSI8WES1U6zJnEkCOFeZ7c0/nLoK+RLraIERf9/MolIHa7N6yjGzQg==", + "version": "19.7.1", + "resolved": "https://registry.npmjs.org/react-science/-/react-science-19.7.1.tgz", + "integrity": "sha512-DP+DwUdhWliOMq5GZH/jsrC5RvP3WN3Aq4FHrlmaJVD10A2NLiaY4yJSu8IKzRp7+pJyNvmoMu5TifGP9x4VLQ==", "license": "MIT", "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "^1.7.7", diff --git a/package.json b/package.json index 5b8f0fc625..dabaf38567 100644 --- a/package.json +++ b/package.json @@ -69,9 +69,9 @@ "@emotion/styled": "^11.14.1", "@hookform/resolvers": "^5.2.2", "@tanstack/react-form": "^1.27.7", - "@zakodium/nmr-types": "^0.5.0", - "@zakodium/nmrium-core": "^0.6.0", - "@zakodium/nmrium-core-plugins": "^0.6.29", + "@zakodium/nmr-types": "^0.5.1", + "@zakodium/nmrium-core": "^0.6.1", + "@zakodium/nmrium-core-plugins": "^0.6.33", "@zakodium/pdnd-esm": "^1.0.2", "@zip.js/zip.js": "^2.8.15", "cheminfo-font": "^1.13.1", From 2fae86f527f2ff2506f0514a9bcb7327c19ebffd Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:18:15 +0100 Subject: [PATCH 11/15] refactor: restore test options in vite config and add `ssr.resolve.conditions` needed for tests run inside monorepo remove vitest config --- tsconfig.json | 3 +-- vite.config.ts | 10 ++++++++++ vitest.config.ts | 34 ---------------------------------- 3 files changed, 11 insertions(+), 36 deletions(-) delete mode 100644 vitest.config.ts diff --git a/tsconfig.json b/tsconfig.json index a16f1017b2..76a7a728e7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,6 @@ "src", "test-e2e", "knip.config.ts", - "vite.config.ts", - "vitest.config.ts" + "vite.config.ts" ] } diff --git a/vite.config.ts b/vite.config.ts index 50935d43e8..de1497c96e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -53,6 +53,16 @@ export default () => { : undefined, alias: resolveAliases, }, + ssr: { + resolve: { + conditions: isMonorepo + ? ['nmrium-internal', ...defaultClientConditions] + : undefined, + }, + }, + test: { + include: ['./src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + }, }); }; diff --git a/vitest.config.ts b/vitest.config.ts deleted file mode 100644 index 10932562f6..0000000000 --- a/vitest.config.ts +++ /dev/null @@ -1,34 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; - -import { defaultClientConditions } from 'vite'; -import { defineConfig } from 'vitest/config'; - -export default () => { - const isMonorepo = checkMonorepo(); - - return defineConfig({ - base: './', - ssr: { - resolve: { - conditions: isMonorepo - ? ['nmrium-internal', ...defaultClientConditions] - : undefined, - }, - }, - test: { - include: ['./src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - typecheck: { - enabled: true, - include: ['./src/**/*.test-d.ts'], - }, - }, - }); -}; - -function checkMonorepo() { - const monorepoPkg = path.join(import.meta.dirname, '..', 'package.json'); - if (!fs.existsSync(monorepoPkg)) return false; - const pkg = JSON.parse(fs.readFileSync(monorepoPkg, 'utf8')); - return pkg.name === '@zakodium/nmrium-monorepo'; -} From b91b280703a730514f5d7ad153c444ba90b614a2 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:31:08 +0100 Subject: [PATCH 12/15] feat: forward style options to PublicationText --- src/component/1d/FloatPublicationString.tsx | 61 +++++++++++++++---- src/component/1d/peaks/PeakAnnotations.tsx | 2 +- src/component/1d/ranges/Ranges.tsx | 2 +- src/component/2d/YAxis.tsx | 2 +- src/component/2d/zones/SignalsGuideLines.tsx | 2 +- src/component/hooks/useTextMetrics.ts | 15 ++++- src/component/hooks/use_acs_settings.ts | 9 ++- .../hooks/use_publication_strings.ts | 4 +- .../modal/PublicationStringModal.tsx | 4 +- 9 files changed, 78 insertions(+), 23 deletions(-) diff --git a/src/component/1d/FloatPublicationString.tsx b/src/component/1d/FloatPublicationString.tsx index eb164d2a6b..7e67fb9f93 100644 --- a/src/component/1d/FloatPublicationString.tsx +++ b/src/component/1d/FloatPublicationString.tsx @@ -1,10 +1,12 @@ import styled from '@emotion/styled'; -import type { BoundingBox } from '@zakodium/nmrium-core'; -import { useEffect, useState } from 'react'; +import type { BoundingBox, TextStyle } from '@zakodium/nmrium-core'; +import { useEffect, useMemo, useState } from 'react'; import { BsArrowsMove } from 'react-icons/bs'; import { FaTimes } from 'react-icons/fa'; import { Rnd } from 'react-rnd'; +import { SVGStyledText } from 'react-science/ui'; +import { isSpectrum1D } from '../../data/data1d/Spectrum1D/index.ts'; import { useChartData } from '../context/ChartContext.js'; import { useDispatch } from '../context/DispatchContext.js'; import { useGlobal } from '../context/GlobalContext.js'; @@ -13,6 +15,7 @@ import { ActionsButtonsPopover } from '../elements/ActionsButtonsPopover.js'; import { useSVGUnitConverter } from '../hooks/useSVGUnitConverter.js'; import { useTextMetrics } from '../hooks/useTextMetrics.ts'; import { useCheckExportStatus } from '../hooks/useViewportSize.js'; +import { useACSSettings } from '../hooks/use_acs_settings.ts'; import { usePublicationStrings } from '../hooks/use_publication_strings.ts'; const ReactRnd = styled(Rnd)` @@ -32,11 +35,17 @@ interface UseWrapSVGTextParams { text: string; width: number; fontSize: number; + fontStyle: string | undefined; + fontWeight: string | undefined; } function useWrapSVGText(params: UseWrapSVGTextParams) { - const { text, width, fontSize } = params; - const { getTextWidth } = useTextMetrics(fontSize); + const { text, width, fontSize, fontStyle, fontWeight } = params; + const { getTextWidth } = useTextMetrics({ + labelSize: fontSize, + labelStyle: fontStyle, + labelWeight: fontWeight, + }); const formattedText = text .replaceAll(/(?.*?)<\/sup>/g, '++$1++ ') @@ -69,28 +78,33 @@ function useWrapSVGText(params: UseWrapSVGTextParams) { interface PublicationTextProps { text: string; + textStyle: TextStyle; fontSize?: number; width: number; padding?: number; } function PublicationText(props: PublicationTextProps) { - const { fontSize = 12, padding = 10, width, text } = props; + const { text, width } = props; + const textStyle = { fontSize: props.fontSize ?? 12, ...props.textStyle }; + const { fontSize = textStyle.fontSize, padding = 10 } = props; const boxWidth = width - padding * 2; const { lineHeight, lines } = useWrapSVGText({ width: boxWidth, fontSize, + fontStyle: textStyle.fontStyle, + fontWeight: textStyle.fontWeight, text, }); return ( {lines.map((line, lineIndex) => ( - {word} ; } })} - + ))} ); @@ -125,16 +139,18 @@ interface DraggablePublicationStringProps { value: string; bonding: BoundingBox; spectrumKey: string; + nucleus: string | undefined; } function DraggablePublicationString(props: DraggablePublicationStringProps) { - const { value, bonding: externalBounding, spectrumKey } = props; + const { value, bonding: externalBounding, spectrumKey, nucleus } = props; const dispatch = useDispatch(); const { viewerRef } = useGlobal(); const [bounding, setBounding] = useState(externalBounding); const [isMoveActive, setIsMoveActive] = useState(false); const { percentToPixel, pixelToPercent } = useSVGUnitConverter(); const isExportProcessStart = useCheckExportStatus(); + const acsOptions = useACSSettings(nucleus); useEffect(() => { setBounding({ ...externalBounding }); @@ -250,7 +266,11 @@ function DraggablePublicationString(props: DraggablePublicationStringProps) { if (isExportProcessStart) { return ( - + ); } @@ -294,7 +314,11 @@ function DraggablePublicationString(props: DraggablePublicationStringProps) { y={y} > - + @@ -304,9 +328,21 @@ function DraggablePublicationString(props: DraggablePublicationStringProps) { export function FloatPublicationString() { const publicationString = usePublicationStrings(); const { + data: spectra, view: { ranges }, } = useChartData(); - const options = Object.entries(ranges); + const options = useMemo(() => Object.entries(ranges), [ranges]); + const spectraToNucleusMap = useMemo(() => { + const map = new Map(); + + for (const spectrum of spectra) { + if (!isSpectrum1D(spectrum)) continue; + const { nucleus } = spectrum.info; + map.set(spectrum.id, nucleus); + } + + return map; + }, [spectra]); return options.map(([spectrumKey, viewOptions]) => { const { showPublicationString, publicationStringBounding } = viewOptions; @@ -316,6 +352,7 @@ export function FloatPublicationString() { diff --git a/src/component/1d/peaks/PeakAnnotations.tsx b/src/component/1d/peaks/PeakAnnotations.tsx index efbb1efa1f..d410f2e70c 100644 --- a/src/component/1d/peaks/PeakAnnotations.tsx +++ b/src/component/1d/peaks/PeakAnnotations.tsx @@ -55,7 +55,7 @@ function resolveYOverlaps( } function useDetectPeakOverlaps(peaks: Peak[], format: string) { - const { getTextWidth } = useTextMetrics(textSize); + const { getTextWidth } = useTextMetrics({ labelSize: textSize }); const overlapPeaksById: Record = {}; let cluster: Peak[] = []; diff --git a/src/component/1d/ranges/Ranges.tsx b/src/component/1d/ranges/Ranges.tsx index 1166c64319..23ef7b482a 100644 --- a/src/component/1d/ranges/Ranges.tsx +++ b/src/component/1d/ranges/Ranges.tsx @@ -32,7 +32,7 @@ type ProcessedRange = RangeType & { function useStackRangesAssignmentsLabels(ranges: RangeType[]) { const { scaleX } = useScaleChecked(); - const { getTextWidth } = useTextMetrics(labelSize); + const { getTextWidth } = useTextMetrics({ labelSize }); if (ranges.length === 0) return null; diff --git a/src/component/2d/YAxis.tsx b/src/component/2d/YAxis.tsx index 68201d9cad..2d3f69957e 100644 --- a/src/component/2d/YAxis.tsx +++ b/src/component/2d/YAxis.tsx @@ -25,7 +25,7 @@ function YAxis(props: YAxisProps) { const { width, height, margin } = useChartData(); const nucleusStr = useActiveNucleusTab(); const [, unit] = nucleusStr.split(','); - const { getTextWidth } = useTextMetrics(10); + const { getTextWidth } = useTextMetrics({ labelSize: 10 }); const scaleY = useScale2DY(); diff --git a/src/component/2d/zones/SignalsGuideLines.tsx b/src/component/2d/zones/SignalsGuideLines.tsx index 9749765d8c..51d3506dbb 100644 --- a/src/component/2d/zones/SignalsGuideLines.tsx +++ b/src/component/2d/zones/SignalsGuideLines.tsx @@ -67,7 +67,7 @@ function useSignalsOverlap(axis: IndicationLinesAxis, spectrum: Spectrum1D) { }); const scaleX = useScale2DX(); const scaleY = useScale2DY(); - const { getTextWidth } = useTextMetrics(labelSize); + const { getTextWidth } = useTextMetrics({ labelSize }); if (!signals) return null; diff --git a/src/component/hooks/useTextMetrics.ts b/src/component/hooks/useTextMetrics.ts index 398c5186e8..0fb43fed22 100644 --- a/src/component/hooks/useTextMetrics.ts +++ b/src/component/hooks/useTextMetrics.ts @@ -8,14 +8,25 @@ function measureTextWidth( return Math.round(context.measureText(text).width); } -export function useTextMetrics(labelSize = 12) { +interface UseTextMetricsOptions { + labelSize?: number; + labelStyle?: string; + labelWeight?: string; +} + +export function useTextMetrics(options: UseTextMetricsOptions = {}) { + const { + labelSize = 12, + labelStyle = 'normal', + labelWeight = 'normal', + } = options; const contextRef = useRef(null); if (!contextRef.current) { const canvas = document.createElement('canvas'); contextRef.current = canvas.getContext('2d'); if (contextRef.current) { - contextRef.current.font = `${labelSize}px Arial`; + contextRef.current.font = `${labelStyle} ${labelWeight} ${labelSize}px Arial`; } } diff --git a/src/component/hooks/use_acs_settings.ts b/src/component/hooks/use_acs_settings.ts index 47dc2502c0..c4f2d90dd3 100644 --- a/src/component/hooks/use_acs_settings.ts +++ b/src/component/hooks/use_acs_settings.ts @@ -13,8 +13,15 @@ const defaultOptions: ACSExportOptions = { textStyle: {}, }; -export function useACSSettings() { +export function useActiveACSSettings() { const nucleus = useActiveNucleusTab(); + + return useACSSettings(nucleus); +} + +export function useACSSettings(nucleus: string | undefined) { const { current } = usePreferences(); + if (!nucleus) return defaultOptions; + return current.acsExportSettings[nucleus] || defaultOptions; } diff --git a/src/component/hooks/use_publication_strings.ts b/src/component/hooks/use_publication_strings.ts index 9fce42d4df..57428bb21c 100644 --- a/src/component/hooks/use_publication_strings.ts +++ b/src/component/hooks/use_publication_strings.ts @@ -6,11 +6,11 @@ import { assert } from 'react-science/ui'; import { isSpectrum1D } from '../../data/data1d/Spectrum1D/index.ts'; import useSpectraByActiveNucleus from './useSpectraPerNucleus.ts'; -import { useACSSettings } from './use_acs_settings.ts'; +import { useActiveACSSettings } from './use_acs_settings.ts'; export function usePublicationStrings() { const spectra = useSpectraByActiveNucleus(); - const acs = useACSSettings(); + const acs = useActiveACSSettings(); const output: Record = {}; diff --git a/src/component/modal/PublicationStringModal.tsx b/src/component/modal/PublicationStringModal.tsx index f3714fd680..28c52002e1 100644 --- a/src/component/modal/PublicationStringModal.tsx +++ b/src/component/modal/PublicationStringModal.tsx @@ -16,7 +16,7 @@ import { usePreferences } from '../context/PreferencesContext.tsx'; import { EmptyText } from '../elements/EmptyText.js'; import { StyledDialogBody } from '../elements/StyledDialogBody.js'; import useSpectrum from '../hooks/useSpectrum.js'; -import { useACSSettings } from '../hooks/use_acs_settings.ts'; +import { useActiveACSSettings } from '../hooks/use_acs_settings.ts'; import { buildPublicationString } from '../hooks/use_publication_strings.ts'; const Body = styled.div` @@ -99,7 +99,7 @@ function InnerPublicationStringModal(props: InnerPublicationStringModalProps) { } = props; const spectrum = useSpectrum(); const { dispatch } = usePreferences(); - const currentACSOptions = useACSSettings(); + const currentACSOptions = useActiveACSSettings(); const form = useForm({ defaultValues: validationSchema.encode(currentACSOptions), validators: { onChange: validationSchema }, From acbef9703018bf26ab7460be16d058d4d1180f87 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:58:52 +0100 Subject: [PATCH 13/15] fix: patch fontSize default value to 12 Refs: https://github.com/cheminfo/nmrium/pull/3958#issuecomment-3851908205 --- .../modal/PublicationStringModal.tsx | 74 ++++++++++++------- 1 file changed, 48 insertions(+), 26 deletions(-) diff --git a/src/component/modal/PublicationStringModal.tsx b/src/component/modal/PublicationStringModal.tsx index 28c52002e1..42a648020a 100644 --- a/src/component/modal/PublicationStringModal.tsx +++ b/src/component/modal/PublicationStringModal.tsx @@ -1,7 +1,8 @@ -import { Button, Checkbox, Dialog, DialogFooter } from '@blueprintjs/core'; +import { Button, Dialog, DialogFooter } from '@blueprintjs/core'; import styled from '@emotion/styled'; import type { ACSExportOptions, Spectrum1D } from '@zakodium/nmrium-core'; import type { FormEvent } from 'react'; +import { useMemo } from 'react'; import { FieldGroupSVGTextStyleFields, Form, @@ -37,12 +38,15 @@ type ExportFormatType = 'IMJA' | 'IMJ' | 'D'; type ExportSignalKind = ACSExportOptions['signalKind']; const validationSchema = z.object({ - signalKind: z.enum(['all', 'signal']), - ascending: z.boolean(), - format: z.string(), - couplingFormat: z.string(), - deltaFormat: z.string(), - textStyle: svgTextStyleFieldsSchema, + acs: z.object({ + signalKind: z.enum(['all', 'signal']), + ascending: z.boolean(), + format: z.string(), + couplingFormat: z.string(), + deltaFormat: z.string(), + textStyle: svgTextStyleFieldsSchema, + }), + isPublicationStringShown: z.boolean(), }); const exportOptions: Array> = [ @@ -100,18 +104,37 @@ function InnerPublicationStringModal(props: InnerPublicationStringModalProps) { const spectrum = useSpectrum(); const { dispatch } = usePreferences(); const currentACSOptions = useActiveACSSettings(); + + const defaultValues = useMemo(() => { + const values = validationSchema.encode({ + acs: currentACSOptions, + isPublicationStringShown, + }); + + if (values.acs.textStyle.fontSize === undefined) { + values.acs.textStyle.fontSize = '12'; + } + + return values; + }, [currentACSOptions, isPublicationStringShown]); const form = useForm({ - defaultValues: validationSchema.encode(currentACSOptions), + defaultValues, validators: { onChange: validationSchema }, onSubmit: ({ value }) => { assert(spectrum && isSpectrum1D(spectrum)); const nucleus = spectrum.info.nucleus; - const options = validationSchema.parse(value); + const parsedValues = validationSchema.parse(value); + if (parsedValues.acs.textStyle.fontSize === 12) { + parsedValues.acs.textStyle.fontSize = undefined; + } dispatch({ type: 'CHANGE_EXPORT_ACS_SETTINGS', - payload: { options, nucleus }, + payload: { options: parsedValues.acs, nucleus }, }); + if (parsedValues.isPublicationStringShown !== isPublicationStringShown) { + togglePublicationStringVisibility(); + } onClose(); }, }); @@ -133,29 +156,29 @@ function InnerPublicationStringModal(props: InnerPublicationStringModalProps) { > - + {(field) => ( )} - + {(field) => ( )} - + {(field) => } - + {(field) => } - + {(field) => } @@ -179,11 +202,14 @@ function InnerPublicationStringModal(props: InnerPublicationStringModalProps) { } > - + + {(field) => ( + + )} + @@ -197,10 +223,6 @@ const CopyPreviewButton = styled(Button)` margin-bottom: 5px; `; -const PublicationStringCheckbox = styled(Checkbox)` - display: inline-block; -`; - interface PublicationStringPreviewProps { spectrum: Spectrum1D; values: z.input; @@ -210,7 +232,7 @@ interface PublicationStringPreviewProps { function PublicationStringPreview(props: PublicationStringPreviewProps) { const { spectrum, values, onCopy } = props; - const acs = validationSchema.parse(values); + const { acs } = validationSchema.parse(values); const value = buildPublicationString({ spectrum, acs }); if (!value) return ; From 55d165eba91290d98f08e128e8575165ca06c7e2 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Thu, 5 Feb 2026 10:38:34 +0100 Subject: [PATCH 14/15] fix: fontSize forward to renderer with default value --- src/component/1d/FloatPublicationString.tsx | 5 ++++- src/component/hooks/use_acs_settings.ts | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/component/1d/FloatPublicationString.tsx b/src/component/1d/FloatPublicationString.tsx index 7e67fb9f93..bd2d6c63b7 100644 --- a/src/component/1d/FloatPublicationString.tsx +++ b/src/component/1d/FloatPublicationString.tsx @@ -86,7 +86,10 @@ interface PublicationTextProps { function PublicationText(props: PublicationTextProps) { const { text, width } = props; - const textStyle = { fontSize: props.fontSize ?? 12, ...props.textStyle }; + const textStyle = { + ...props.textStyle, + fontSize: props.textStyle.fontSize ?? props.fontSize ?? 12, + }; const { fontSize = textStyle.fontSize, padding = 10 } = props; const boxWidth = width - padding * 2; diff --git a/src/component/hooks/use_acs_settings.ts b/src/component/hooks/use_acs_settings.ts index c4f2d90dd3..0944aff239 100644 --- a/src/component/hooks/use_acs_settings.ts +++ b/src/component/hooks/use_acs_settings.ts @@ -21,7 +21,7 @@ export function useActiveACSSettings() { export function useACSSettings(nucleus: string | undefined) { const { current } = usePreferences(); - if (!nucleus) return defaultOptions; + if (!nucleus) return structuredClone(defaultOptions); - return current.acsExportSettings[nucleus] || defaultOptions; + return current.acsExportSettings[nucleus] || structuredClone(defaultOptions); } From 8c537ff0157672fb84f2bbaf9a333f833591df82 Mon Sep 17 00:00:00 2001 From: tpoisseau <22891227+tpoisseau@users.noreply.github.com> Date: Thu, 5 Feb 2026 13:12:12 +0100 Subject: [PATCH 15/15] fix: use `defaultServerConditions` for `ssr.resolve.conditions` Refs: https://github.com/cheminfo/nmrium/pull/3958#discussion_r2768559094 --- vite.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index de1497c96e..c10f170ff2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,7 +4,7 @@ import path from 'node:path'; import react from '@vitejs/plugin-react'; import analyze from 'rollup-plugin-analyzer'; import type { AliasOptions } from 'vite'; -import { defaultClientConditions } from 'vite'; +import { defaultClientConditions, defaultServerConditions } from 'vite'; import { defineConfig } from 'vitest/config'; // https://vitejs.dev/config/ @@ -56,7 +56,7 @@ export default () => { ssr: { resolve: { conditions: isMonorepo - ? ['nmrium-internal', ...defaultClientConditions] + ? ['nmrium-internal', ...defaultServerConditions] : undefined, }, },