diff --git a/.eslintrc.yml b/.eslintrc.yml index 2adb6f0..37308f8 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,8 +1,7 @@ extends: + - cheminfo-typescript - cheminfo-react - plugin:storybook/recommended rules: react/jsx-no-bind: off consistent-return: off -env: - jest: true diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 2e2007c..71e0af2 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -10,3 +10,5 @@ jobs: nodejs: # Documentation: https://github.com/zakodium/workflows#nodejs-ci uses: zakodium/workflows/.github/workflows/nodejs.yml@nodejs-v1 + with: + lint-check-types: true diff --git a/.storybook/main.mjs b/.storybook/main.mjs index 92c2ad1..ceeeece 100644 --- a/.storybook/main.mjs +++ b/.storybook/main.mjs @@ -1,5 +1,5 @@ export default { - stories: ['../stories/**/*.stories.jsx'], + stories: ['../stories/**/*.stories.tsx'], addons: [ '@storybook/addon-storysource', diff --git a/babel.config.js b/babel.config.js deleted file mode 100644 index 0d88e04..0000000 --- a/babel.config.js +++ /dev/null @@ -1,17 +0,0 @@ -const config = { - presets: [ - [ - '@babel/preset-react', - { - runtime: 'automatic', - }, - ], - ], - plugins: [], -}; - -if (process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'cjs') { - config.plugins.push('@babel/transform-modules-commonjs'); -} - -module.exports = config; diff --git a/base.d.ts b/base.d.ts index a4b6222..f008f0f 100644 --- a/base.d.ts +++ b/base.d.ts @@ -1,24 +1,12 @@ -import { - IIdcodeSvgRendererProps as IdcodeProps, - IMolfileSvgRendererProps as MolfileProps, - ISmilesSvgRendererProps as SmilesProps, -} from './types'; - -export interface IIdcodeSvgRendererProps extends IdcodeProps { - OCL: any; -} -export function IdcodeSvgRenderer( - props: IIdcodeSvgRendererProps, -): JSX.Element; - -export interface IMolfileSvgRendererProps extends MolfileProps { - OCL: any; -} -export function MolfileSvgRenderer( - props: IMolfileSvgRendererProps, -): JSX.Element; - -export interface ISmilesSvgRendererProps extends SmilesProps { - OCL: any; -} -export function SmilesSvgRenderer(props: ISmilesSvgRendererProps): JSX.Element; +export { + type BaseIdcodeSvgRendererProps, + default as BaseIdcodeSvgRenderer, +} from './lib/components/IdcodeSvgRenderer.d.ts'; +export { + type BaseMolfileSvgRendererProps, + default as BaseMolfileSvgRenderer, +} from './lib/components/MolfileSvgRenderer.d.ts'; +export { + type BaseSmilesSvgRendererProps, + default as BaseSmilesSvgRenderer, +} from './lib/components/SmilesSvgRenderer.d.ts'; diff --git a/core.d.ts b/core.d.ts index d37caaf..216f28a 100644 --- a/core.d.ts +++ b/core.d.ts @@ -1 +1 @@ -export * from './minimal'; +export * from './minimal.d.ts'; diff --git a/full.d.ts b/full.d.ts index 9dbf2ab..5a3257b 100644 --- a/full.d.ts +++ b/full.d.ts @@ -1,2 +1,5 @@ export * from './core'; -export { IStructureEditorProps, StructureEditor } from './types'; +export { + type StructureEditorProps, + default as StructureEditor, +} from './src/components/StructureEditor'; diff --git a/minimal.d.ts b/minimal.d.ts index 9d393ee..d6f6646 100644 --- a/minimal.d.ts +++ b/minimal.d.ts @@ -1,9 +1,9 @@ export { - IBaseSvgRendererProps, - IIdcodeSvgRendererProps, - IMolfileSvgRendererProps, - ISmilesSvgRendererProps, + type BaseSvgRendererProps, + type IdcodeSvgRendererProps, IdcodeSvgRenderer, + type MolfileSvgRendererProps, MolfileSvgRenderer, + type SmilesSvgRendererProps, SmilesSvgRenderer, -} from './types'; +} from './lib/index.d.ts'; diff --git a/package.json b/package.json index ece55b5..eb5fad3 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "ocl", "openchemlib" ], + "type": "module", "files": [ "lib", "lib-cjs", @@ -22,8 +23,7 @@ "full.cjs", "minimal.d.ts", "minimal.mjs", - "minimal.cjs", - "types.d.ts" + "minimal.cjs" ], "main": "minimal.cjs", "module": "minimal.mjs", @@ -34,6 +34,11 @@ "require": "./minimal.cjs", "default": "./minimal.mjs" }, + "./base": { + "types": "./base.d.ts", + "require": "./base.cjs", + "default": "./base.mjs" + }, "./minimal": { "types": "./minimal.d.ts", "require": "./minimal.cjs", @@ -59,17 +64,18 @@ "author": "Michaƫl Zasso ", "scripts": { "build-storybook": "storybook build", - "compile": "babel src --out-dir lib --ignore src/**/__tests__ --quiet --source-maps", - "postcompile": "echo '{\"type\":\"module\"}' > lib/package.json", - "compile-cjs": "cross-env NODE_ENV=cjs babel src --out-dir lib-cjs --ignore src/**/__tests__ --quiet --source-maps", + "check-types": "tsc --noEmit", + "compile": "tsc -p tsconfig.esm.json", + "compile-cjs": "tsc -p tsconfig.cjs.json", + "postcompile-cjs": "echo '{\"type\":\"commonjs\"}' > lib-cjs/package.json", "dev": "storybook dev -p 6006", "eslint": "eslint src stories", "eslint-fix": "npm run eslint -- --fix", "prepack": "npm run compile && npm run compile-cjs", "prettier": "prettier --check src", "prettier-write": "prettier --write src", - "test": "npm run test-only && npm run eslint && npm run prettier", - "test-only": "cross-env NODE_ENV=test jest" + "test": "npm run test-only && npm run check-types && npm run eslint && npm run prettier", + "test-only": "vitest run" }, "prettier": { "arrowParens": "always", @@ -85,29 +91,25 @@ "react-dom": ">=18" }, "devDependencies": { - "@babel/cli": "^7.25.7", - "@babel/core": "^7.25.7", - "@babel/plugin-transform-modules-commonjs": "^7.25.7", - "@babel/preset-react": "^7.25.7", "@storybook/addon-essentials": "^8.3.5", "@storybook/addon-links": "^8.3.5", "@storybook/addon-storysource": "^8.3.5", "@storybook/react": "^8.3.5", "@storybook/react-vite": "^8.3.5", + "@types/react-test-renderer": "^18.3.0", "@vitejs/plugin-react": "^4.3.2", - "babel-jest": "^29.7.0", - "babel-loader": "^9.2.1", - "cross-env": "^7.0.3", "eslint": "^8.56.0", "eslint-config-cheminfo-react": "^10.1.0", + "eslint-config-cheminfo-typescript": "^13.0.0", "eslint-plugin-storybook": "^0.9.0", - "jest": "^29.7.0", "openchemlib": "^8.15.0", "prettier": "^3.3.3", "react": "^18.3.1", "react-dom": "^18.3.1", "react-test-renderer": "^18.3.1", "storybook": "^8.3.5", - "vite": "^5.4.8" + "typescript": "^5.6.3", + "vite": "^5.4.8", + "vitest": "^2.1.2" } } diff --git a/src/components/ErrorRenderer.jsx b/src/components/ErrorRenderer.tsx similarity index 61% rename from src/components/ErrorRenderer.jsx rename to src/components/ErrorRenderer.tsx index 4c41515..4e609a0 100644 --- a/src/components/ErrorRenderer.jsx +++ b/src/components/ErrorRenderer.tsx @@ -1,4 +1,16 @@ -export function ErrorRenderer(props) { +import type { ComponentType } from 'react'; + +import type { ErrorComponentProps } from './types.js'; + +interface ErrorRendererProps { + width?: number; + height?: number; + value: string; + error: Error; + ErrorComponent: ComponentType; +} + +export function ErrorRenderer(props: ErrorRendererProps) { const { width = 300, height = 150, value, error, ErrorComponent } = props; return (
@@ -12,7 +24,11 @@ export function ErrorRenderer(props) { ); } -export function DefaultErrorRenderer(props) { +export function DefaultErrorRenderer(props: { + width: number; + height: number; + message: string; +}) { return ( + ); } diff --git a/src/components/MolfileSvgRenderer.jsx b/src/components/MolfileSvgRenderer.tsx similarity index 54% rename from src/components/MolfileSvgRenderer.jsx rename to src/components/MolfileSvgRenderer.tsx index d02d56f..9d6ca81 100644 --- a/src/components/MolfileSvgRenderer.jsx +++ b/src/components/MolfileSvgRenderer.tsx @@ -1,11 +1,21 @@ -import { memo } from 'react'; +import type OCL from 'openchemlib/minimal'; +import { memo, type ReactElement } from 'react'; import { useHandleMemoError } from '../hooks/useHandleMemoError.js'; import { DefaultErrorRenderer, ErrorRenderer } from './ErrorRenderer.js'; import SvgRenderer from './SvgRenderer.js'; +import type { BaseSvgRendererProps } from './types.js'; -function MolfileSvgRenderer(props) { +export interface MolfileSvgRendererProps extends BaseSvgRendererProps { + molfile: string; +} + +export interface BaseMolfileSvgRendererProps extends MolfileSvgRendererProps { + OCL: typeof OCL; +} + +function MolfileSvgRenderer(props: BaseMolfileSvgRendererProps): ReactElement { const { OCL, molfile, @@ -32,8 +42,15 @@ function MolfileSvgRenderer(props) { export default memo(MolfileSvgRenderer); -function DefaultMolfileErrorComponent(props) { +function DefaultMolfileErrorComponent(props: { + width: number; + height: number; +}) { return ( - + ); } diff --git a/src/components/SmilesSvgRenderer.jsx b/src/components/SmilesSvgRenderer.tsx similarity index 51% rename from src/components/SmilesSvgRenderer.jsx rename to src/components/SmilesSvgRenderer.tsx index 00b0ceb..d0a6bf0 100644 --- a/src/components/SmilesSvgRenderer.jsx +++ b/src/components/SmilesSvgRenderer.tsx @@ -1,11 +1,23 @@ -import { memo } from 'react'; +import type OCL from 'openchemlib/minimal'; +import { memo, type ReactElement } from 'react'; import { useHandleMemoError } from '../hooks/useHandleMemoError.js'; import { DefaultErrorRenderer, ErrorRenderer } from './ErrorRenderer.js'; import SvgRenderer from './SvgRenderer.js'; +import type { BaseSvgRendererProps } from './types.js'; -function SmilesSvgRenderer(props) { +export interface SmilesSvgRendererProps extends BaseSvgRendererProps { + smiles: string; +} + +export interface BaseSmilesSvgRendererProps extends SmilesSvgRendererProps { + OCL: typeof OCL; +} + +function BaseSmilesSvgRenderer( + props: BaseSmilesSvgRendererProps, +): ReactElement { const { OCL, smiles, @@ -30,10 +42,14 @@ function SmilesSvgRenderer(props) { return ; } -export default memo(SmilesSvgRenderer); +export default memo(BaseSmilesSvgRenderer); -function DefaultSmilesErrorComponent(props) { +function DefaultSmilesErrorComponent(props: { width: number; height: number }) { return ( - + ); } diff --git a/src/components/StructureEditor.jsx b/src/components/StructureEditor.tsx similarity index 69% rename from src/components/StructureEditor.jsx rename to src/components/StructureEditor.tsx index 33ac2d0..c1e3906 100644 --- a/src/components/StructureEditor.jsx +++ b/src/components/StructureEditor.tsx @@ -1,7 +1,29 @@ import OCL from 'openchemlib/full'; -import { useEffect, useRef } from 'react'; +import { type ReactElement, useEffect, useRef } from 'react'; -function StructureEditor(props) { +export interface StructureEditorProps { + width?: number; + height?: number; + initialMolfile?: string; + initialIDCode?: string; + fragment?: boolean; + svgMenu?: boolean; + onChange?: (molfile: string, molecule: OCL.Molecule, idCode: string) => void; + onAtomEnter?: (atomId: number) => void; + onAtomLeave?: (atomId: number) => void; + onBondEnter?: (bondId: number) => void; + onBondLeave?: (bondId: number) => void; +} + +interface CallbacksRef { + onChange?: OCL.ChangeListenerCallback; + onAtomHighlight?: OCL.AtomHighlightCallback; + onBondHighlight?: OCL.BondHighlightCallback; +} + +export default function StructureEditor( + props: StructureEditorProps, +): ReactElement { const { width = 675, height = 450, @@ -16,18 +38,24 @@ function StructureEditor(props) { onBondLeave, } = props; - const domRef = useRef(); - const editorRef = useRef({ editor: null, hadFirstChange: false }); - const callbacksRef = useRef({}); + const domRef = useRef(null); + const editorRef = useRef<{ + editor: OCL.StructureEditor | null; + hadFirstChange: boolean; + }>({ editor: null, hadFirstChange: false }); + const callbacksRef = useRef({}); useEffect(() => { + if (!domRef.current) return; + domRef.current.innerHTML = ''; // GWT doesn't play well with the shadow DOM. This hack allows to load an // OCL editor inside a shadow root. const root = domRef.current.getRootNode(); - let originalGetElementById; + let originalGetElementById: typeof document.getElementById | undefined; if (root instanceof ShadowRoot) { + // eslint-disable-next-line @typescript-eslint/unbound-method originalGetElementById = document.getElementById; document.getElementById = root.getElementById.bind(root); } @@ -35,7 +63,7 @@ function StructureEditor(props) { try { editor = new OCL.StructureEditor(domRef.current, svgMenu, 1); } finally { - if (root instanceof ShadowRoot) { + if (originalGetElementById) { document.getElementById = originalGetElementById; } } @@ -69,7 +97,7 @@ function StructureEditor(props) { callbacksRef.current.onChange = () => { if (!editorRef.current.hadFirstChange) { editorRef.current.hadFirstChange = true; - } else if (onChange) { + } else if (onChange && editorRef.current.editor) { const molfile = editorRef.current.editor.getMolFileV3(); const molecule = editorRef.current.editor.getMolecule(); const idCode = editorRef.current.editor.getIDCode(); @@ -100,5 +128,3 @@ function StructureEditor(props) { return
; } - -export default StructureEditor; diff --git a/src/components/SvgRenderer.jsx b/src/components/SvgRenderer.tsx similarity index 66% rename from src/components/SvgRenderer.jsx rename to src/components/SvgRenderer.tsx index 06bdb3b..ec5bea5 100644 --- a/src/components/SvgRenderer.jsx +++ b/src/components/SvgRenderer.tsx @@ -1,6 +1,20 @@ -import { useEffect, useId, useMemo, useRef } from 'react'; +import type OCL from 'openchemlib/minimal'; +import { + type MouseEvent as ReactMouseEvent, + type RefObject, + useEffect, + useId, + useMemo, + useRef, +} from 'react'; + +import type { BaseSvgRendererProps } from './types.js'; + +interface SvgRendererProps extends BaseSvgRendererProps { + mol: OCL.Molecule; +} -export default function SvgRenderer(props) { +export default function SvgRenderer(props: SvgRendererProps) { const { width = 300, height = 150, @@ -33,7 +47,7 @@ export default function SvgRenderer(props) { const reactId = useId().replace(/:/g, '-'); const internalId = `react-ocl${reactId}`; - const ref = useRef(null); + const ref = useRef(null); const id = idFromProps || internalId; @@ -96,29 +110,47 @@ export default function SvgRenderer(props) { ); } -function useEvents(ref, start, onEnter, onLeave, onClick) { +function useEvents( + ref: RefObject, + start: string, + onEnter: BaseSvgRendererProps['onAtomEnter'], + onLeave: BaseSvgRendererProps['onAtomLeave'], + onClick: BaseSvgRendererProps['onAtomClick'], +) { useEffect(() => { const svg = ref.current; if (!svg) return; - const handleEnter = (event) => { + const handleEnter = (event: MouseEvent) => { if (!onEnter) return; - const { target } = event; + const target = event.target as SVGElement; if (target.className.baseVal === 'event' && target.id.startsWith(start)) { - onEnter(Number(target.id.replace(start, '')), event); + // TODO: This is wrong and should be fixed. + onEnter( + Number(target.id.replace(start, '')), + event as unknown as ReactMouseEvent, + ); } }; - const handleLeave = (event) => { + const handleLeave = (event: MouseEvent) => { if (!onLeave) return; - const { target } = event; + const target = event.target as SVGElement; if (target.className.baseVal === 'event' && target.id.startsWith(start)) { - onLeave(Number(target.id.replace(start, '')), event); + // TODO: This is wrong and should be fixed. + onLeave( + Number(target.id.replace(start, '')), + event as unknown as ReactMouseEvent, + ); } }; - const handleClick = (event) => { + const handleClick = (event: MouseEvent) => { if (!onClick) return; - const { target } = event; + const target = event.target as SVGElement; if (target.className.baseVal === 'event' && target.id.startsWith(start)) { - onClick(Number(target.id.replace(start, '')), event); + // TODO: This is wrong and should be fixed. + onClick( + Number(target.id.replace(start, '')), + event as unknown as ReactMouseEvent, + ); } }; svg.addEventListener('mouseover', handleEnter); @@ -133,30 +165,36 @@ function useEvents(ref, start, onEnter, onLeave, onClick) { } function useHighlight( - ref, - start, - highlight, - highlightColor, - highlightOpacity, - attribute, + ref: RefObject, + start: string, + highlight: number[] | undefined, + highlightColor: string, + highlightOpacity: number, + attribute: string, ) { useEffect(() => { const svg = ref.current; if (!svg) return; const elements = svg.querySelectorAll(`[id^="${start}"]`); - for (const element of elements) { + elements.forEach((element) => { const elementId = Number(element.id.replace(start, '')); - if (highlight && highlight.includes(elementId)) { - element.setAttribute('opacity', highlightOpacity); + if (highlight?.includes(elementId)) { + element.setAttribute('opacity', String(highlightOpacity)); element.setAttribute(attribute, highlightColor); } else { - element.setAttribute('opacity', 0); + element.setAttribute('opacity', '0'); } - } + }); }); } -function getSVG(mol, width, height, id, serializedOptions) { +function getSVG( + mol: OCL.Molecule, + width: number, + height: number, + id: string, + serializedOptions: string, +) { const options = JSON.parse(serializedOptions); const { @@ -170,6 +208,7 @@ function getSVG(mol, width, height, id, serializedOptions) { let svg = mol.toSVG(width, height, id, svgOptions); if (label) { + // @ts-expect-error We know it will match. const [minX, minY, realWidth, realHeight] = svg .match(/viewBox="([^"]*)"/)[1] .split(' ') diff --git a/src/components/__tests__/SvgRenderer.jsx b/src/components/__tests__/SvgRenderer.test.tsx similarity index 96% rename from src/components/__tests__/SvgRenderer.jsx rename to src/components/__tests__/SvgRenderer.test.tsx index 541da90..66b913d 100644 --- a/src/components/__tests__/SvgRenderer.jsx +++ b/src/components/__tests__/SvgRenderer.test.tsx @@ -1,8 +1,10 @@ import OCL from 'openchemlib/minimal'; import renderer from 'react-test-renderer'; +import { expect, test } from 'vitest'; import MolfileSvgRenderer from '../MolfileSvgRenderer.js'; import SmilesSvgRenderer from '../SmilesSvgRenderer.js'; +import type { ErrorComponentProps } from '../types.js'; test('Molecule renders smiles with custom id', () => { const component = renderer.create( @@ -99,7 +101,7 @@ test('Syntax error in SMILES - custom renderer', () => { ( + ErrorComponent={(props: ErrorComponentProps) => (
{props.value} {props.error.message} diff --git a/src/components/__tests__/__snapshots__/SvgRenderer.jsx.snap b/src/components/__tests__/__snapshots__/SvgRenderer.test.jsx.snap similarity index 99% rename from src/components/__tests__/__snapshots__/SvgRenderer.jsx.snap rename to src/components/__tests__/__snapshots__/SvgRenderer.test.jsx.snap index bfd06c4..106af49 100644 --- a/src/components/__tests__/__snapshots__/SvgRenderer.jsx.snap +++ b/src/components/__tests__/__snapshots__/SvgRenderer.test.jsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`Molecule renders molfile with default id 1`] = ` text {font-family: sans-serif;} #mol1 { pointer-events:none; } #mol1 .event { pointer-events:all; } line { stroke-linecap:round; } polygon { stroke-linejoin:round; } + N + O + O + O + O + H + H + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +", + } + } + height="800px" + id="mol1" + style={ + { + "userSelect": "none", + } + } + version="1.1" + viewBox="0 0 1200 800" + width="1200px" + xmlns="http://www.w3.org/2000/svg" +/> +`; + +exports[`Molecule renders smiles with custom id 1`] = ` + text {font-family: sans-serif;} #mol1 { pointer-events:none; } #mol1 .event { pointer-events:all; } line { stroke-linecap:round; } polygon { stroke-linejoin:round; } + O + + + + + + + + + + +", + } + } + height="800px" + id="mol1" + style={ + { + "userSelect": "none", + } + } + version="1.1" + viewBox="0 0 1200 800" + width="1200px" + xmlns="http://www.w3.org/2000/svg" +/> +`; + +exports[`Syntax error in SMILES - custom renderer 1`] = ` +
+
+ + BAD + + + Class$S13: SmilesParser: unknown element label found. Position:1 + +
+
+`; + +exports[`Syntax error in SMILES - default renderer 1`] = ` +
+ + + + Invalid SMILES + + + +
+`; diff --git a/src/components/types.ts b/src/components/types.ts new file mode 100644 index 0000000..4387bb8 --- /dev/null +++ b/src/components/types.ts @@ -0,0 +1,39 @@ +import type { IMoleculeToSVGOptions } from 'openchemlib/minimal'; +import type { ComponentType, MouseEvent } from 'react'; + +export interface ErrorComponentProps { + width: number; + height: number; + value: string; + error: Error; +} + +export interface BaseSvgRendererProps extends IMoleculeToSVGOptions { + width?: number; + height?: number; + id?: string; + + ErrorComponent?: ComponentType; + + atomHighlight?: number[]; + atomHighlightOpacity?: number; + atomHighlightColor?: string; + onAtomEnter?: (atomId: number, event: MouseEvent) => void; + onAtomLeave?: (atomId: number, event: MouseEvent) => void; + onAtomClick?: (atomId: number, event: MouseEvent) => void; + + bondHighlight?: number[]; + bondHighlightOpacity?: number; + bondHighlightColor?: string; + onBondEnter?: (bondId: number, event: MouseEvent) => void; + onBondLeave?: (bondId: number, event: MouseEvent) => void; + onBondClick?: (bondId: number, event: MouseEvent) => void; + + autoCrop?: boolean; + autoCropMargin?: number; + + labelFontFamily?: string; + labelFontSize?: number; + labelColor?: string; + label?: string; +} diff --git a/src/hooks/useHandleMemoError.js b/src/hooks/useHandleMemoError.ts similarity index 51% rename from src/hooks/useHandleMemoError.js rename to src/hooks/useHandleMemoError.ts index a34784a..a7ae26e 100644 --- a/src/hooks/useHandleMemoError.js +++ b/src/hooks/useHandleMemoError.ts @@ -1,11 +1,14 @@ import { useMemo } from 'react'; -export function useHandleMemoError(cb, deps) { - const [hasError, result] = useMemo(() => { +export function useHandleMemoError( + cb: () => T, + deps: unknown[], +): [Error, null] | [null, T] { + const [hasError, result] = useMemo<[false, T] | [true, Error]>(() => { try { return [false, cb()]; } catch (error) { - return [true, error]; + return [true, error as Error]; } // eslint-disable-next-line react-hooks/exhaustive-deps }, deps); diff --git a/src/index.jsx b/src/index.jsx deleted file mode 100644 index 0299cec..0000000 --- a/src/index.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import OCL from 'openchemlib/minimal'; - -import BaseIdcodeSvgRenderer from './components/IdcodeSvgRenderer.js'; -import BaseMolfileSvgRenderer from './components/MolfileSvgRenderer.js'; -import BaseSmilesSvgRenderer from './components/SmilesSvgRenderer.js'; - -export function SmilesSvgRenderer(props) { - return ; -} - -export function MolfileSvgRenderer(props) { - return ; -} - -export function IdcodeSvgRenderer(props) { - return ; -} diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..d099444 --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,31 @@ +import OCL from 'openchemlib/minimal'; + +import BaseIdcodeSvgRenderer, { + type IdcodeSvgRendererProps, +} from './components/IdcodeSvgRenderer.js'; +import BaseMolfileSvgRenderer, { + type MolfileSvgRendererProps, +} from './components/MolfileSvgRenderer.js'; +import BaseSmilesSvgRenderer, { + type SmilesSvgRendererProps, +} from './components/SmilesSvgRenderer.js'; + +export type { + IdcodeSvgRendererProps, + MolfileSvgRendererProps, + SmilesSvgRendererProps, +}; + +export type { BaseSvgRendererProps } from './components/types.js'; + +export function SmilesSvgRenderer(props: SmilesSvgRendererProps) { + return ; +} + +export function MolfileSvgRenderer(props: MolfileSvgRendererProps) { + return ; +} + +export function IdcodeSvgRenderer(props: IdcodeSvgRendererProps) { + return ; +} diff --git a/stories/.eslintrc.yml b/stories/.eslintrc.yml deleted file mode 100644 index 78dc437..0000000 --- a/stories/.eslintrc.yml +++ /dev/null @@ -1,2 +0,0 @@ -rules: - no-undef: off diff --git a/stories/data.js b/stories/data.ts similarity index 100% rename from stories/data.js rename to stories/data.ts diff --git a/stories/highlight.stories.jsx b/stories/highlight.stories.tsx similarity index 90% rename from stories/highlight.stories.jsx rename to stories/highlight.stories.tsx index 2aaa1ec..d3de923 100644 --- a/stories/highlight.stories.jsx +++ b/stories/highlight.stories.tsx @@ -1,8 +1,8 @@ import { useState } from 'react'; -import { MolfileSvgRenderer } from '../src/index'; +import { MolfileSvgRenderer } from '../src/index.js'; -import { molfileV2000 } from './data'; +import { molfileV2000 } from './data.js'; export default { title: 'Highlighting', @@ -33,7 +33,7 @@ export default { }, }; -export function Fixed(args) { +export function Fixed(args: any) { return ; } Fixed.storyName = 'Fixed highlight'; @@ -52,7 +52,7 @@ Fixed.argTypes = { }, }; -export function Hover(args) { +export function Hover(args: any) { const [currentAtom, setCurrentAtom] = useState(null); const [currentBond, setCurrentBond] = useState(null); return ( diff --git a/stories/structure-editor.stories.jsx b/stories/structure-editor.stories.tsx similarity index 80% rename from stories/structure-editor.stories.jsx rename to stories/structure-editor.stories.tsx index a7daef6..f32677a 100644 --- a/stories/structure-editor.stories.jsx +++ b/stories/structure-editor.stories.tsx @@ -1,6 +1,6 @@ import { useCallback, useState } from 'react'; -import StructureEditor from '../src/components/StructureEditor'; +import StructureEditor from '../src/components/StructureEditor.js'; export default { title: 'StructureEditor', @@ -40,11 +40,21 @@ Actelion Java MolfileCreator 1.0 M END `; -export function FromMolfile({ svgMenu, fragment, width, height }) { +export function FromMolfile({ + svgMenu, + fragment, + width, + height, +}: { + svgMenu: boolean; + fragment: boolean; + width: number; + height: number; +}) { const [molfile, setMolfile] = useState(initialMolfile); - const [previous, setPrevious] = useState(null); + const [previous, setPrevious] = useState(null); const cb = useCallback( - (newMolfile) => { + (newMolfile: string) => { setMolfile(newMolfile); setPrevious(molfile); }, @@ -73,11 +83,21 @@ export function FromMolfile({ svgMenu, fragment, width, height }) { ); } -export function FromIDCode({ svgMenu, fragment, width, height }) { +export function FromIDCode({ + svgMenu, + fragment, + width, + height, +}: { + svgMenu: boolean; + fragment: boolean; + width: number; + height: number; +}) { const [idCode, setIDCode] = useState(initialIDCode); - const [previous, setPrevious] = useState(null); + const [previous, setPrevious] = useState(null); const cb = useCallback( - (netMolfile, molecule, newIDCode) => { + (netMolfile: unknown, molecule: unknown, newIDCode: string) => { setIDCode(newIDCode); setPrevious(idCode); }, diff --git a/stories/svg-renderers/common-args.js b/stories/svg-renderers/common-args.ts similarity index 100% rename from stories/svg-renderers/common-args.js rename to stories/svg-renderers/common-args.ts diff --git a/stories/svg-renderers/idcode.stories.jsx b/stories/svg-renderers/idcode.stories.tsx similarity index 67% rename from stories/svg-renderers/idcode.stories.jsx rename to stories/svg-renderers/idcode.stories.tsx index 9baa373..d88ae1a 100644 --- a/stories/svg-renderers/idcode.stories.jsx +++ b/stories/svg-renderers/idcode.stories.tsx @@ -1,7 +1,7 @@ -import { IdcodeSvgRenderer } from '../../src/index'; -import { idcode } from '../data'; +import { IdcodeSvgRenderer } from '../../src/index.js'; +import { idcode } from '../data.js'; -import { commonArgs, commonArgTypes } from './common-args'; +import { commonArgs, commonArgTypes } from './common-args.js'; export default { title: 'SVG renderers/IdcodeSvgRenderer', @@ -14,7 +14,7 @@ export default { }, }; -export function Idcode(args) { +export function Idcode(args: any) { return ; } Idcode.storyName = 'ID code'; @@ -22,7 +22,7 @@ Idcode.args = { idcode: idcode.idCode, }; -export function WithCoordinates(args) { +export function WithCoordinates(args: any) { return ; } WithCoordinates.storyName = 'ID code (with coordinates)'; diff --git a/stories/svg-renderers/molfile.stories.jsx b/stories/svg-renderers/molfile.stories.tsx similarity index 71% rename from stories/svg-renderers/molfile.stories.jsx rename to stories/svg-renderers/molfile.stories.tsx index 84d8a7f..3356298 100644 --- a/stories/svg-renderers/molfile.stories.jsx +++ b/stories/svg-renderers/molfile.stories.tsx @@ -1,7 +1,7 @@ -import { MolfileSvgRenderer } from '../../src/index'; -import { molfileV2000, molfileV3000 } from '../data'; +import { MolfileSvgRenderer } from '../../src/index.js'; +import { molfileV2000, molfileV3000 } from '../data.js'; -import { commonArgs, commonArgTypes } from './common-args'; +import { commonArgs, commonArgTypes } from './common-args.js'; export default { title: 'SVG renderers/MolfileSvgRenderer', @@ -22,7 +22,7 @@ export default { }, }; -export function V2000(args) { +export function V2000(args: any) { return ; } V2000.storyName = 'Molfile (V2000)'; @@ -30,7 +30,7 @@ V2000.args = { molfile: molfileV2000, }; -export function V3000(args) { +export function V3000(args: any) { return ; } V3000.storyName = 'Molfile (V3000)'; diff --git a/stories/svg-renderers/smiles.stories.jsx b/stories/svg-renderers/smiles.stories.tsx similarity index 82% rename from stories/svg-renderers/smiles.stories.jsx rename to stories/svg-renderers/smiles.stories.tsx index 16862d6..9cfdfa2 100644 --- a/stories/svg-renderers/smiles.stories.jsx +++ b/stories/svg-renderers/smiles.stories.tsx @@ -1,6 +1,6 @@ -import { SmilesSvgRenderer } from '../../src/index'; +import { SmilesSvgRenderer } from '../../src/index.js'; -import { commonArgs, commonArgTypes } from './common-args'; +import { commonArgs, commonArgTypes } from './common-args.js'; export default { title: 'SVG renderers/SmilesSvgRenderer', @@ -22,12 +22,12 @@ export default { }, }; -export function Smiles(args) { +export function Smiles(args: any) { return ; } Smiles.storyName = 'SMILES'; -function ErrorComponent(props) { +function ErrorComponent(props: any) { return (
{props.value}
diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json new file mode 100644 index 0000000..2ceb092 --- /dev/null +++ b/tsconfig.cjs.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.esm.json", + "compilerOptions": { + "outDir": "lib-cjs", + "module": "CommonJS", + "moduleResolution": "Node", + "verbatimModuleSyntax": false, + "esModuleInterop": true, + "declaration": false, + "declarationMap": false + } +} diff --git a/tsconfig.esm.json b/tsconfig.esm.json new file mode 100644 index 0000000..0cd9220 --- /dev/null +++ b/tsconfig.esm.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["stories", "**/__tests__", "*.d.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8059d7a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "declaration": true, + "jsx": "react-jsx", + "lib": ["dom", "esnext"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "resolveJsonModule": true, + "verbatimModuleSyntax": true, + "outDir": "lib", + "removeComments": false, + "sourceMap": true, + "declarationMap": true, + "strict": true, + "target": "es2022", + "skipLibCheck": true + }, + "include": ["src/**/*", "stories/**/*", "*.d.ts"], + "exclude": ["node_modules"] +} diff --git a/types.d.ts b/types.d.ts deleted file mode 100644 index cb9b380..0000000 --- a/types.d.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { IMoleculeToSVGOptions, Molecule } from 'openchemlib/minimal'; -import { ComponentType, MouseEvent } from 'react'; - -// Minimal and core APIs - -export interface IErrorComponentProps { - value: string; - error: Error; -} - -export interface IBaseSvgRendererProps extends IMoleculeToSVGOptions { - width?: number; - height?: number; - id?: string; - - ErrorComponent?: ComponentType; - - atomHighlight?: number[]; - atomHighlightOpacity?: number; - atomHighlightColor?: string; - onAtomEnter?: (atomId: number, event: MouseEvent) => void; - onAtomLeave?: (atomId: number, event: MouseEvent) => void; - onAtomClick?: (atomId: number, event: MouseEvent) => void; - - bondHighlight?: number[]; - bondHighlightOpacity?: number; - bondHighlightColor?: string; - onBondEnter?: (bondId: number, event: MouseEvent) => void; - onBondLeave?: (bondId: number, event: MouseEvent) => void; - onBondClick?: (bondId: number, event: MouseEvent) => void; - - autoCrop?: boolean; - autoCropMargin?: number; - - labelFontFamily?: string; - labelFontSize?: number; - labelColor?: string; - label?: string; -} - -export interface ISmilesSvgRendererProps extends IBaseSvgRendererProps { - smiles: string; -} -export function SmilesSvgRenderer(props: ISmilesSvgRendererProps): JSX.Element; - -export interface IMolfileSvgRendererProps extends IBaseSvgRendererProps { - molfile: string; -} -export function MolfileSvgRenderer( - props: IMolfileSvgRendererProps, -): JSX.Element; - -export interface IIdcodeSvgRendererProps extends IBaseSvgRendererProps { - idcode: string; - coordinates?: string; -} -export function IdcodeSvgRenderer(props: IIdcodeSvgRendererProps): JSX.Element; - -// Full API - -export interface IStructureEditorProps { - width?: number; - height?: number; - initialMolfile?: string; - initialIDCode?: string; - fragment?: boolean; - svgMenu?: boolean; - onChange?: (molfile: string, molecule: Molecule, idCode: string) => void; - onAtomEnter?: (atomId: number) => void; - onAtomLeave?: (atomId: number) => void; - onBondEnter?: (bondId: number) => void; - onBondLeave?: (bondId: number) => void; -} -export function StructureEditor(props: IStructureEditorProps): JSX.Element;