From a5e5f29675f282304f38dbce6edfcaafb7158577 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Thu, 25 Jul 2024 17:24:39 +0200 Subject: [PATCH 1/6] Re-export all components that have stories told about them --- src/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/index.tsx b/src/index.tsx index 18882c7..0bc3565 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,3 +4,7 @@ import { type Variant, residueVariants} from './toggles/variants' export { ResiduesHeader }; export type { Variant }; export { residueVariants }; +export {CopyToClipBoardIcon } from './CopyToClipBoardIcon'; +export {ResiduesSelect, PickIn3D} from './toggles' +export { useChunked } from './useChunked' +export { NGLStage, NGLComponent, NGLResidues, SimpleViewer } from "./molviewer"; \ No newline at end of file From b959ff71a3b45109b9c4f27978a5703c15d7e124 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Thu, 25 Jul 2024 17:30:30 +0200 Subject: [PATCH 2/6] Replace eslint with biome --- .eslintrc.cjs | 18 --- .github/workflows/lint.yml | 2 +- .ladle/components.tsx | 1 - CONTRIBUTING.md | 12 ++ biome.json | 116 +++++++++++++++++ package.json | 7 +- pnpm-lock.yaml | 119 +++++++++++++----- ...es.tsx => CopyToClipBoardIcon.stories.tsx} | 0 src/molviewer.tsx | 3 +- tsconfig.app.json | 2 - 10 files changed, 224 insertions(+), 56 deletions(-) delete mode 100644 .eslintrc.cjs create mode 100644 biome.json rename src/{CopyToClipbBoardIcon.stories.tsx => CopyToClipBoardIcon.stories.tsx} (100%) diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index d6c9537..0000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - root: true, - env: { browser: true, es2020: true }, - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:react-hooks/recommended', - ], - ignorePatterns: ['dist', '.eslintrc.cjs'], - parser: '@typescript-eslint/parser', - plugins: ['react-refresh'], - rules: { - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - }, -} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 31f770c..22e5712 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -22,6 +22,6 @@ jobs: - name: Install dependencies run: pnpm install - name: Lint - run: pnpm lint + run: pnpm biome ci --reporter=github - name: Build run: pnpm build diff --git a/.ladle/components.tsx b/.ladle/components.tsx index 3b585ad..719a242 100644 --- a/.ladle/components.tsx +++ b/.ladle/components.tsx @@ -6,7 +6,6 @@ import type { GlobalProvider } from "@ladle/react"; export const Provider: GlobalProvider = ({ children, globalState, - storyMeta, }) => { // Make components that use `className="dark:underline"` dark mode compatible const theme = globalState.theme; diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4f40b93..8c27c1a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,6 +27,18 @@ We use [ladle](https://ladle.dev/) to develop components. To start the developme pnpm dev ``` +To lint use + +```bash +pnpm lint +``` + +To format use + +```bash +pnpm format +``` + ## Build package ```bash diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..92a0b09 --- /dev/null +++ b/biome.json @@ -0,0 +1,116 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", + "organizeImports": { "enabled": true }, + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": false, + "complexity": { + "noBannedTypes": "error", + "noExtraBooleanCast": "error", + "noMultipleSpacesInRegularExpressionLiterals": "error", + "noUselessCatch": "error", + "noUselessTypeConstraint": "error", + "noWith": "error" + }, + "correctness": { + "noConstAssign": "error", + "noConstantCondition": "error", + "noEmptyCharacterClassInRegex": "error", + "noEmptyPattern": "error", + "noGlobalObjectCalls": "error", + "noInnerDeclarations": "error", + "noInvalidConstructorSuper": "error", + "noNewSymbol": "error", + "noNonoctalDecimalEscape": "error", + "noPrecisionLoss": "error", + "noSelfAssign": "error", + "noSetterReturn": "error", + "noSwitchDeclarations": "error", + "noUndeclaredVariables": "error", + "noUnreachable": "error", + "noUnreachableSuper": "error", + "noUnsafeFinally": "error", + "noUnsafeOptionalChaining": "error", + "noUnusedLabels": "error", + "noUnusedVariables": "error", + "useArrayLiterals": "off", + "useExhaustiveDependencies": "warn", + "useHookAtTopLevel": "error", + "useIsNan": "error", + "useValidForDirection": "error", + "useYield": "error" + }, + "style": { "noNamespace": "error", "useAsConstAssertion": "error" }, + "suspicious": { + "noAsyncPromiseExecutor": "error", + "noCatchAssign": "error", + "noClassAssign": "error", + "noCompareNegZero": "error", + "noControlCharactersInRegex": "error", + "noDebugger": "error", + "noDuplicateCase": "error", + "noDuplicateClassMembers": "error", + "noDuplicateObjectKeys": "error", + "noDuplicateParameters": "error", + "noEmptyBlockStatements": "off", + "noExplicitAny": "error", + "noExtraNonNullAssertion": "error", + "noFallthroughSwitchClause": "error", + "noFunctionAssign": "error", + "noGlobalAssign": "error", + "noImportAssign": "error", + "noMisleadingCharacterClass": "error", + "noMisleadingInstantiator": "error", + "noPrototypeBuiltins": "error", + "noRedeclare": "error", + "noShadowRestrictedNames": "error", + "noUnsafeDeclarationMerging": "error", + "noUnsafeNegation": "error", + "useGetterReturn": "error", + "useValidTypeof": "error" + } + }, + "ignore": ["**/dist", "**/.eslintrc.cjs"] + }, + "overrides": [ + { + "include": ["*.ts", "*.tsx", "*.mts", "*.cts"], + "linter": { + "rules": { + "correctness": { + "noConstAssign": "off", + "noGlobalObjectCalls": "off", + "noInvalidConstructorSuper": "off", + "noInvalidNewBuiltin": "off", + "noNewSymbol": "off", + "noSetterReturn": "off", + "noUndeclaredVariables": "off", + "noUnreachable": "off", + "noUnreachableSuper": "off" + }, + "style": { + "noArguments": "error", + "noVar": "error", + "useConst": "error" + }, + "suspicious": { + "noDuplicateClassMembers": "off", + "noDuplicateObjectKeys": "off", + "noDuplicateParameters": "off", + "noFunctionAssign": "off", + "noImportAssign": "off", + "noRedeclare": "off", + "noUnsafeNegation": "off", + "useGetterReturn": "off" + } + } + } + } + ] +} diff --git a/package.json b/package.json index 7e8d6ee..8c09e1b 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "build": "tsc -b && vite build", "build:docs": "ladle build", "preview": "ladle preview", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "lint": "biome lint", + "format": "biome check --write", "prepublishOnly": "pnpm build" }, "dependencies": { @@ -31,6 +32,7 @@ "react-dom": "^18.3.1" }, "devDependencies": { + "@biomejs/biome": "1.8.3", "@ladle/react": "^4.1.0", "@tailwindcss/typography": "^0.5.13", "@types/node": "^20.14.12", @@ -40,9 +42,6 @@ "@typescript-eslint/parser": "^7.15.0", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.19", - "eslint": "^8.57.0", - "eslint-plugin-react-hooks": "^4.6.2", - "eslint-plugin-react-refresh": "^0.4.7", "postcss": "^8.4.40", "tailwindcss": "^3.4.6", "typescript": "^5.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf4e098..dc2e9c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,6 +24,9 @@ importers: specifier: ^2.4.0 version: 2.4.0 devDependencies: + '@biomejs/biome': + specifier: 1.8.3 + version: 1.8.3 '@ladle/react': specifier: ^4.1.0 version: 4.1.0(@types/node@20.14.12)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.4) @@ -51,15 +54,6 @@ importers: autoprefixer: specifier: ^10.4.19 version: 10.4.19(postcss@8.4.40) - eslint: - specifier: ^8.57.0 - version: 8.57.0 - eslint-plugin-react-hooks: - specifier: ^4.6.2 - version: 4.6.2(eslint@8.57.0) - eslint-plugin-react-refresh: - specifier: ^0.4.7 - version: 0.4.9(eslint@8.57.0) postcss: specifier: ^8.4.40 version: 8.4.40 @@ -193,6 +187,59 @@ packages: resolution: {integrity: sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==} engines: {node: '>=6.9.0'} + '@biomejs/biome@1.8.3': + resolution: {integrity: sha512-/uUV3MV+vyAczO+vKrPdOW0Iaet7UnJMU4bNMinggGJTAnBPjCoLEYcyYtYHNnUNYlv4xZMH6hVIQCAozq8d5w==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@1.8.3': + resolution: {integrity: sha512-9DYOjclFpKrH/m1Oz75SSExR8VKvNSSsLnVIqdnKexj6NwmiMlKk94Wa1kZEdv6MCOHGHgyyoV57Cw8WzL5n3A==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@1.8.3': + resolution: {integrity: sha512-UeW44L/AtbmOF7KXLCoM+9PSgPo0IDcyEUfIoOXYeANaNXXf9mLUwV1GeF2OWjyic5zj6CnAJ9uzk2LT3v/wAw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@1.8.3': + resolution: {integrity: sha512-9yjUfOFN7wrYsXt/T/gEWfvVxKlnh3yBpnScw98IF+oOeCYb5/b/+K7YNqKROV2i1DlMjg9g/EcN9wvj+NkMuQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@1.8.3': + resolution: {integrity: sha512-fed2ji8s+I/m8upWpTJGanqiJ0rnlHOK3DdxsyVLZQ8ClY6qLuPc9uehCREBifRJLl/iJyQpHIRufLDeotsPtw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@1.8.3': + resolution: {integrity: sha512-UHrGJX7PrKMKzPGoEsooKC9jXJMa28TUSMjcIlbDnIO4EAavCoVmNQaIuUSH0Ls2mpGMwUIf+aZJv657zfWWjA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@1.8.3': + resolution: {integrity: sha512-I8G2QmuE1teISyT8ie1HXsjFRz9L1m5n83U1O6m30Kw+kPMPSKjag6QGUn+sXT8V+XWIZxFFBoTDEDZW2KPDDw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@1.8.3': + resolution: {integrity: sha512-J+Hu9WvrBevfy06eU1Na0lpc7uR9tibm9maHynLIoAjLZpQU3IW+OKHUtyL8p6/3pT2Ju5t5emReeIS2SAxhkQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@1.8.3': + resolution: {integrity: sha512-/PJ59vA1pnQeKahemaQf4Nyj7IKUvGQSc3Ze1uIGi+Wvr1xF7rGobSrAAG01T/gUDG21vkDsZYM03NAmPiVkqg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + '@bundled-es-modules/cookie@2.0.0': resolution: {integrity: sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==} @@ -1403,17 +1450,6 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - eslint-plugin-react-hooks@4.6.2: - resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} - engines: {node: '>=10'} - peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 - - eslint-plugin-react-refresh@0.4.9: - resolution: {integrity: sha512-QK49YrBAo5CLNLseZ7sZgvgTy21E6NEw22eZqc4teZfH8pxV3yXc9XXOYfUI6JNpw7mfHNkAeWtBxrTyykB6HA==} - peerDependencies: - eslint: '>=7' - eslint-scope@7.2.2: resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3368,6 +3404,41 @@ snapshots: '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 + '@biomejs/biome@1.8.3': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 1.8.3 + '@biomejs/cli-darwin-x64': 1.8.3 + '@biomejs/cli-linux-arm64': 1.8.3 + '@biomejs/cli-linux-arm64-musl': 1.8.3 + '@biomejs/cli-linux-x64': 1.8.3 + '@biomejs/cli-linux-x64-musl': 1.8.3 + '@biomejs/cli-win32-arm64': 1.8.3 + '@biomejs/cli-win32-x64': 1.8.3 + + '@biomejs/cli-darwin-arm64@1.8.3': + optional: true + + '@biomejs/cli-darwin-x64@1.8.3': + optional: true + + '@biomejs/cli-linux-arm64-musl@1.8.3': + optional: true + + '@biomejs/cli-linux-arm64@1.8.3': + optional: true + + '@biomejs/cli-linux-x64-musl@1.8.3': + optional: true + + '@biomejs/cli-linux-x64@1.8.3': + optional: true + + '@biomejs/cli-win32-arm64@1.8.3': + optional: true + + '@biomejs/cli-win32-x64@1.8.3': + optional: true + '@bundled-es-modules/cookie@2.0.0': dependencies: cookie: 0.5.0 @@ -4679,14 +4750,6 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-plugin-react-hooks@4.6.2(eslint@8.57.0): - dependencies: - eslint: 8.57.0 - - eslint-plugin-react-refresh@0.4.9(eslint@8.57.0): - dependencies: - eslint: 8.57.0 - eslint-scope@7.2.2: dependencies: esrecurse: 4.3.0 diff --git a/src/CopyToClipbBoardIcon.stories.tsx b/src/CopyToClipBoardIcon.stories.tsx similarity index 100% rename from src/CopyToClipbBoardIcon.stories.tsx rename to src/CopyToClipBoardIcon.stories.tsx diff --git a/src/molviewer.tsx b/src/molviewer.tsx index 01f9b76..a4fbcb4 100644 --- a/src/molviewer.tsx +++ b/src/molviewer.tsx @@ -75,6 +75,7 @@ export function NGLResidues({ return "not all"; }, [residues, chain]); + // biome-ignore lint/correctness/useExhaustiveDependencies: to not (re)create new representation when selection changes, keep it out of dep list useEffect(() => { component.addRepresentation(representation, { name, @@ -88,8 +89,6 @@ export function NGLResidues({ repr.dispose(); } }; - // to not (re)create new representation when selection changes, keep it out of dep list - // eslint-disable-next-line react-hooks/exhaustive-deps }, [stage, component, color, opacity, name, representation]); useEffect(() => { diff --git a/tsconfig.app.json b/tsconfig.app.json index d739292..ef6be00 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -8,7 +8,6 @@ "module": "ESNext", "skipLibCheck": true, - /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, @@ -17,7 +16,6 @@ "noEmit": true, "jsx": "react-jsx", - /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, From df3674ebae8475cc83a7a9bd0dee3a77fae27433 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Thu, 25 Jul 2024 17:33:46 +0200 Subject: [PATCH 3/6] Run Format --- .ladle/components.tsx | 25 +- .ladle/config.mjs | 8 +- biome.json | 2 +- package.json | 142 ++- postcss.config.js | 10 +- src/CopyToClipBoardIcon.stories.tsx | 4 +- src/CopyToClipBoardIcon.tsx | 32 +- src/MdxLayout.tsx | 14 +- src/ResiduesSelect.stories.tsx | 488 ++++----- src/cn.ts | 4 +- src/index.tsx | 12 +- src/molviewer--residues.stories.tsx | 70 +- src/molviewer--simpleviewer.stories.tsx | 6 +- src/molviewer--surface.stories.tsx | 251 ++--- src/molviewer.tsx | 1211 +++++++++++------------ src/structure.ts | 4 +- src/toggles.tsx | 566 +++++------ src/toggles/ResidueHeader.stories.tsx | 17 +- src/toggles/ResidueHeader.tsx | 63 +- src/toggles/variants.ts | 10 +- src/useChunked.stories.tsx | 26 +- src/useChunked.ts | 24 +- tailwind.config.js | 24 +- tsconfig.app.json | 42 +- tsconfig.json | 18 +- tsconfig.node.json | 22 +- vite.config.ts | 60 +- 27 files changed, 1577 insertions(+), 1578 deletions(-) diff --git a/.ladle/components.tsx b/.ladle/components.tsx index 719a242..05a0408 100644 --- a/.ladle/components.tsx +++ b/.ladle/components.tsx @@ -1,20 +1,13 @@ import React from "react"; -import "../src/index.css" +import "../src/index.css"; import type { GlobalProvider } from "@ladle/react"; -export const Provider: GlobalProvider = ({ - children, - globalState, -}) => { - // Make components that use `className="dark:underline"` dark mode compatible - const theme = globalState.theme; - if (theme === 'dark') { - return ( -
- {children} -
- ) - } - return children; -}; \ No newline at end of file +export const Provider: GlobalProvider = ({ children, globalState }) => { + // Make components that use `className="dark:underline"` dark mode compatible + const theme = globalState.theme; + if (theme === "dark") { + return
{children}
; + } + return children; +}; diff --git a/.ladle/config.mjs b/.ladle/config.mjs index 76a5058..bfb2b86 100644 --- a/.ladle/config.mjs +++ b/.ladle/config.mjs @@ -1,6 +1,6 @@ /** @type {import('@ladle/react').UserConfig} */ export default { - defaultStory: "index--readme", - outDir: "docs", - base: process.env.CI ? "/haddock3-ui/" : "/", -}; \ No newline at end of file + defaultStory: "index--readme", + outDir: "docs", + base: process.env.CI ? "/haddock3-ui/" : "/", +}; diff --git a/biome.json b/biome.json index 92a0b09..8992704 100644 --- a/biome.json +++ b/biome.json @@ -3,7 +3,7 @@ "organizeImports": { "enabled": true }, "vcs": { "enabled": true, - "clientKind": "git", + "clientKind": "git", "useIgnoreFile": true }, "linter": { diff --git a/package.json b/package.json index 8c09e1b..bcac58b 100644 --- a/package.json +++ b/package.json @@ -1,74 +1,72 @@ { - "name": "@i-vresse/haddock3-ui", - "version": "0.1.4", - "type": "module", - "private": false, - "sideEffects": false, - "license": "Apache-2.0", - "homepage": "https://github.com/i-VRESSE/haddock3-ui#readme", - "repository": { - "type": "git", - "url": "git+https://github.com/i-VRESSE/haddock3-ui.git" - }, - "bugs": { - "url": "https://github.com/i-VRESSE/haddock3-ui/issues" - }, - "scripts": { - "dev": "ladle serve", - "build": "tsc -b && vite build", - "build:docs": "ladle build", - "preview": "ladle preview", - "lint": "biome lint", - "format": "biome check --write", - "prepublishOnly": "pnpm build" - }, - "dependencies": { - "clsx": "^2.1.1", - "ngl": "^2.3.1", - "tailwind-merge": "^2.4.0" - }, - "peerDependencies": { - "react": "^18.3.1", - "react-dom": "^18.3.1" - }, - "devDependencies": { - "@biomejs/biome": "1.8.3", - "@ladle/react": "^4.1.0", - "@tailwindcss/typography": "^0.5.13", - "@types/node": "^20.14.12", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@typescript-eslint/eslint-plugin": "^7.15.0", - "@typescript-eslint/parser": "^7.15.0", - "@vitejs/plugin-react": "^4.3.1", - "autoprefixer": "^10.4.19", - "postcss": "^8.4.40", - "tailwindcss": "^3.4.6", - "typescript": "^5.2.2", - "vite": "^5.3.4", - "vite-plugin-dts": "^3.9.1" - }, - "files": [ - "dist" - ], - "module": "dist/index.js", - "types": "./dist/src/index.d.ts", - "exports": { - ".": { - "module": "./dist/index.mjs", - "import": { - "types": "./dist/src/index.d.ts", - "default": "./dist/index.js" - }, - "default": "./dist/index.js" - }, - "./package.json": "./package.json" - }, - "packageManager": "pnpm@9.6.0", - "publishConfig": { - "access": "public" - }, - "engines": { - "node": ">=20" - } + "name": "@i-vresse/haddock3-ui", + "version": "0.1.4", + "type": "module", + "private": false, + "sideEffects": false, + "license": "Apache-2.0", + "homepage": "https://github.com/i-VRESSE/haddock3-ui#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/i-VRESSE/haddock3-ui.git" + }, + "bugs": { + "url": "https://github.com/i-VRESSE/haddock3-ui/issues" + }, + "scripts": { + "dev": "ladle serve", + "build": "tsc -b && vite build", + "build:docs": "ladle build", + "preview": "ladle preview", + "lint": "biome lint", + "format": "biome check --write", + "prepublishOnly": "pnpm build" + }, + "dependencies": { + "clsx": "^2.1.1", + "ngl": "^2.3.1", + "tailwind-merge": "^2.4.0" + }, + "peerDependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@biomejs/biome": "1.8.3", + "@ladle/react": "^4.1.0", + "@tailwindcss/typography": "^0.5.13", + "@types/node": "^20.14.12", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^7.15.0", + "@typescript-eslint/parser": "^7.15.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.40", + "tailwindcss": "^3.4.6", + "typescript": "^5.2.2", + "vite": "^5.3.4", + "vite-plugin-dts": "^3.9.1" + }, + "files": ["dist"], + "module": "dist/index.js", + "types": "./dist/src/index.d.ts", + "exports": { + ".": { + "module": "./dist/index.mjs", + "import": { + "types": "./dist/src/index.d.ts", + "default": "./dist/index.js" + }, + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "packageManager": "pnpm@9.6.0", + "publishConfig": { + "access": "public" + }, + "engines": { + "node": ">=20" + } } diff --git a/postcss.config.js b/postcss.config.js index 2e7af2b..7b75c83 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,6 +1,6 @@ export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/src/CopyToClipBoardIcon.stories.tsx b/src/CopyToClipBoardIcon.stories.tsx index 3bd37ed..4c54ec4 100644 --- a/src/CopyToClipBoardIcon.stories.tsx +++ b/src/CopyToClipBoardIcon.stories.tsx @@ -2,6 +2,4 @@ import type { Story } from "@ladle/react"; import { CopyToClipBoardIcon } from "./CopyToClipBoardIcon.js"; -export const Default: Story = () => ( - -); \ No newline at end of file +export const Default: Story = () => ; diff --git a/src/CopyToClipBoardIcon.tsx b/src/CopyToClipBoardIcon.tsx index 5478cd9..4bb35ce 100644 --- a/src/CopyToClipBoardIcon.tsx +++ b/src/CopyToClipBoardIcon.tsx @@ -1,18 +1,18 @@ export function CopyToClipBoardIcon() { - return ( - - - - ); + return ( + + + + ); } diff --git a/src/MdxLayout.tsx b/src/MdxLayout.tsx index 4bf2dc6..5386451 100644 --- a/src/MdxLayout.tsx +++ b/src/MdxLayout.tsx @@ -1,13 +1,17 @@ /** * Use me as a layout for your MDX files. - * + * * For example: - * + * * ```tsx * import {Layout} from "../MdxLayout"; * export default Layout * `` */ -export function Layout({children}: {children: React.ReactNode}) { - return
{children}
; -} \ No newline at end of file +export function Layout({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/src/ResiduesSelect.stories.tsx b/src/ResiduesSelect.stories.tsx index 16e9e20..be3a867 100644 --- a/src/ResiduesSelect.stories.tsx +++ b/src/ResiduesSelect.stories.tsx @@ -1,263 +1,271 @@ -import { useLadleContext, type Story, action } from "@ladle/react"; -import { ResidueNeighbourSelection, ResidueSelection, ResiduesSelect } from "./toggles"; +import { type Story, action, useLadleContext } from "@ladle/react"; import { useState } from "react"; +import { + ResidueNeighbourSelection, + ResidueSelection, + ResiduesSelect, +} from "./toggles"; export const Surface: Story = () => { - const { globalState} = useLadleContext(); - return ( - -)}; + const { globalState } = useLadleContext(); + return ( + + ); +}; export const ActiveOnly: Story = () => { - const { globalState} = useLadleContext(); - const [selected, setSelected] = useState({ - act: [3], - pass: [], - neighbours: [], - }); + const { globalState } = useLadleContext(); + const [selected, setSelected] = useState({ + act: [3], + pass: [], + neighbours: [], + }); - const onChange = (selected: ResidueSelection) => { - setSelected({ - ...selected, - neighbours: [], - }); - }; + const onChange = (selected: ResidueSelection) => { + setSelected({ + ...selected, + neighbours: [], + }); + }; - return ( - - ) -} + return ( + + ); +}; export const PassiveOnly: Story = () => { - const { globalState} = useLadleContext(); - const [selected, setSelected] = useState({ - act: [], - pass: [3], - neighbours: [], - }); + const { globalState } = useLadleContext(); + const [selected, setSelected] = useState({ + act: [], + pass: [3], + neighbours: [], + }); - const onChange = (selected: ResidueSelection) => { - setSelected({ - ...selected, - neighbours: [], - }); - }; + const onChange = (selected: ResidueSelection) => { + setSelected({ + ...selected, + neighbours: [], + }); + }; - return ( - - ) -} + return ( + + ); +}; export const ActiveAndNeigbours: Story = () => { - const { globalState} = useLadleContext(); - const [selected, setSelected] = useState({ - act: [3], - pass: [], - neighbours: [1, 2], - }); + const { globalState } = useLadleContext(); + const [selected, setSelected] = useState({ + act: [3], + pass: [], + neighbours: [1, 2], + }); - const onChange = (selected: ResidueSelection) => { - // Use inverse of selected as fake neighbours computation - const neighbours = [1, 2, 3].filter((resno) => !selected.act.includes(resno) && !selected.pass.includes(resno)) - setSelected({ - ...selected, - neighbours - }); - }; + const onChange = (selected: ResidueSelection) => { + // Use inverse of selected as fake neighbours computation + const neighbours = [1, 2, 3].filter( + (resno) => + !selected.act.includes(resno) && !selected.pass.includes(resno), + ); + setSelected({ + ...selected, + neighbours, + }); + }; - return ( - - ) -} + return ( + + ); +}; export const ActiveDisabledAndPassive = () => ( -
Unwanted combination
-) +
Unwanted combination
+); export const ActiveAndPassive: Story = () => { - const { globalState} = useLadleContext(); - const [selected, setSelected] = useState({ - act: [3], - pass: [1], - neighbours: [], - }); + const { globalState } = useLadleContext(); + const [selected, setSelected] = useState({ + act: [3], + pass: [1], + neighbours: [], + }); - const onChange = (selected: ResidueSelection) => { - setSelected({ - ...selected, - neighbours:[] - }); - }; + const onChange = (selected: ResidueSelection) => { + setSelected({ + ...selected, + neighbours: [], + }); + }; - return ( - - ) -} + return ( + + ); +}; export const LongList: Story = () => { - const { globalState} = useLadleContext(); - return ( - ({ - resno: i + 42, - resname: "XXX", - seq: "X", - surface: true, - }))} - selected={{ - act: [43, 111], - pass: [51, 78], - neighbours: [], - }} - onChange={action("onChange")} - onHover={action("onHover")} - theme={globalState.theme === "dark" ? "dark" : "light"} - /> - ) -} \ No newline at end of file + const { globalState } = useLadleContext(); + return ( + ({ + resno: i + 42, + resname: "XXX", + seq: "X", + surface: true, + }))} + selected={{ + act: [43, 111], + pass: [51, 78], + neighbours: [], + }} + onChange={action("onChange")} + onHover={action("onHover")} + theme={globalState.theme === "dark" ? "dark" : "light"} + /> + ); +}; diff --git a/src/cn.ts b/src/cn.ts index a5ef193..ac680b3 100644 --- a/src/cn.ts +++ b/src/cn.ts @@ -1,6 +1,6 @@ -import { clsx, type ClassValue } from "clsx"; +import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); + return twMerge(clsx(inputs)); } diff --git a/src/index.tsx b/src/index.tsx index 0bc3565..81e859f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,10 +1,10 @@ /* eslint-disable react-refresh/only-export-components */ -import { ResiduesHeader } from './toggles/ResidueHeader'; -import { type Variant, residueVariants} from './toggles/variants' +import { ResiduesHeader } from "./toggles/ResidueHeader"; +import { type Variant, residueVariants } from "./toggles/variants"; export { ResiduesHeader }; export type { Variant }; export { residueVariants }; -export {CopyToClipBoardIcon } from './CopyToClipBoardIcon'; -export {ResiduesSelect, PickIn3D} from './toggles' -export { useChunked } from './useChunked' -export { NGLStage, NGLComponent, NGLResidues, SimpleViewer } from "./molviewer"; \ No newline at end of file +export { CopyToClipBoardIcon } from "./CopyToClipBoardIcon"; +export { ResiduesSelect, PickIn3D } from "./toggles"; +export { useChunked } from "./useChunked"; +export { NGLStage, NGLComponent, NGLResidues, SimpleViewer } from "./molviewer"; diff --git a/src/molviewer--residues.stories.tsx b/src/molviewer--residues.stories.tsx index 1cb44e7..56e91ce 100644 --- a/src/molviewer--residues.stories.tsx +++ b/src/molviewer--residues.stories.tsx @@ -1,41 +1,43 @@ -import { StructureRepresentationType } from "ngl"; import { Story } from "@ladle/react"; +import { StructureRepresentationType } from "ngl"; -import { NGLStage, NGLComponent, NGLResidues } from "./molviewer"; +import { NGLComponent, NGLResidues, NGLStage } from "./molviewer"; import { structure } from "./structure"; -function ResiduesViewer( - props: Parameters[0], - ) { - return ( -
- - - - - - - When switching representation the selected residues should remain - selected. - -
- ); - } - - export const Default: Story<{representation: StructureRepresentationType}> = ({representation}) => { - return ; +function ResiduesViewer(props: Parameters[0]) { + return ( +
+ + + + + + + When switching representation the selected residues should remain + selected. + +
+ ); } + +export const Default: Story<{ + representation: StructureRepresentationType; +}> = ({ representation }) => { + return ( + + ); +}; Default.args = { - representation: "ball+stick" -} + representation: "ball+stick", +}; Default.argTypes = { - representation: { - options: ["licorice", "ball+stick", "spacefill"], - control: { type: "radio" }, - defaultValue: "ball+stick" - } -} \ No newline at end of file + representation: { + options: ["licorice", "ball+stick", "spacefill"], + control: { type: "radio" }, + defaultValue: "ball+stick", + }, +}; diff --git a/src/molviewer--simpleviewer.stories.tsx b/src/molviewer--simpleviewer.stories.tsx index 423fce8..a8a81a2 100644 --- a/src/molviewer--simpleviewer.stories.tsx +++ b/src/molviewer--simpleviewer.stories.tsx @@ -5,8 +5,4 @@ import { structure } from "./structure"; // TODO make dark mode aware -export const Default: Story = () => ( - - ); +export const Default: Story = () => ; diff --git a/src/molviewer--surface.stories.tsx b/src/molviewer--surface.stories.tsx index 72c0f26..dc82b10 100644 --- a/src/molviewer--surface.stories.tsx +++ b/src/molviewer--surface.stories.tsx @@ -1,149 +1,152 @@ -import { NGLSurface, NGLStage, NGLComponent } from "./molviewer"; +import { NGLComponent, NGLStage, NGLSurface } from "./molviewer"; -import { structure } from "./structure"; import { useState } from "react"; +import { structure } from "./structure"; import { ActPass, PickIn3D } from "./toggles"; function SurfaceViewer( - props: Parameters[0] & - Omit[0], "children"> + props: Parameters[0] & + Omit[0], "children">, ) { - const { onHover, onMouseLeave, onPick, ...surfaceprops } = props; - return ( -
- - - - - -
    -
  • Active: {surfaceprops.activeColor}
  • -
  • Passive: {surfaceprops.passiveColor}
  • -
  • Neighbours: {surfaceprops.neighboursColor}
  • -
  • Default: {surfaceprops.defaultColor}
  • -
  • Highlight: {surfaceprops.highlightColor}
  • -
-
- ); + const { onHover, onMouseLeave, onPick, ...surfaceprops } = props; + return ( +
+ + + + + +
    +
  • Active: {surfaceprops.activeColor}
  • +
  • Passive: {surfaceprops.passiveColor}
  • +
  • Neighbours: {surfaceprops.neighboursColor}
  • +
  • Default: {surfaceprops.defaultColor}
  • +
  • Highlight: {surfaceprops.highlightColor}
  • +
+
+ ); } export const NoSelection = () => ( - + ); export const WithSelection = () => ( - + ); export const WithHighlight = () => ( - + ); export const Pickable = () => { - const [picked, setPicked] = useState<[string,number,string,string] | undefined>(undefined); - return ( - <> -
-

- (Click on surface to select residue, only works when ngl show tooltip) -

-

last picked: {JSON.stringify(picked)}

-
- - setPicked([chain, resno, comp, resname]), - }} - /> - - ); + const [picked, setPicked] = useState< + [string, number, string, string] | undefined + >(undefined); + return ( + <> +
+

+ (Click on surface to select residue, only works when ngl show tooltip) +

+

last picked: {JSON.stringify(picked)}

+
+ + setPicked([chain, resno, comp, resname]), + }} + /> + + ); }; export const PickableActiveOrPassive = () => { - const [what, setWhat] = useState("act"); - const [picked, setPicked] = useState<[string,number,string,string, string] | undefined>(undefined); - return ( - <> -
-

- (Click on surface to select residue, only works when ngl show tooltip) -

- setWhat(newWhat)} - /> -

last picked: {JSON.stringify(picked)}

-
- - setPicked([chain, resno, comp, resname, what]), - }} - /> - - ); + const [what, setWhat] = useState("act"); + const [picked, setPicked] = useState< + [string, number, string, string, string] | undefined + >(undefined); + return ( + <> +
+

+ (Click on surface to select residue, only works when ngl show tooltip) +

+ setWhat(newWhat)} /> +

last picked: {JSON.stringify(picked)}

+
+ + setPicked([chain, resno, comp, resname, what]), + }} + /> + + ); }; export const Hoverable = () => { - const [hovered, setHovered] = useState<[string,number,string,string] | undefined>(undefined); - return ( - <> -
-

- (Click on surface to select residue, only works when ngl show tooltip) -

-

last hovered: {JSON.stringify(hovered)}

-
- - setHovered([chain, resno, comp, resname]), - onMouseLeave: () => setHovered(undefined), - }} - /> - - ); + const [hovered, setHovered] = useState< + [string, number, string, string] | undefined + >(undefined); + return ( + <> +
+

+ (Click on surface to select residue, only works when ngl show tooltip) +

+

last hovered: {JSON.stringify(hovered)}

+
+ + setHovered([chain, resno, comp, resname]), + onMouseLeave: () => setHovered(undefined), + }} + /> + + ); }; diff --git a/src/molviewer.tsx b/src/molviewer.tsx index a4fbcb4..542f209 100644 --- a/src/molviewer.tsx +++ b/src/molviewer.tsx @@ -1,682 +1,681 @@ +import { + ColormakerRegistry, + Component, + PickingProxy, + Stage, + Structure, + StructureComponent, + type StructureRepresentationType, +} from "ngl"; /* eslint-disable react-refresh/only-export-components */ // TODO split in more files import { - type ReactNode, - type RefCallback, - createContext, - useCallback, - useContext, - useEffect, - useId, - useState, - Component as ReactComponent, - type ErrorInfo, - useMemo, + type ErrorInfo, + Component as ReactComponent, + type ReactNode, + type RefCallback, + createContext, + useCallback, + useContext, + useEffect, + useId, + useMemo, + useState, } from "react"; -import { - ColormakerRegistry, - Component, - PickingProxy, - Stage, - Structure, - StructureComponent, - type StructureRepresentationType, -} from "ngl"; function currentBackground() { - let backgroundColor = "white"; - if (document?.documentElement?.classList.contains("dark")) { - backgroundColor = "black"; - } else if (document?.documentElement?.classList.contains("light")) { - backgroundColor = "white"; - } else if (window?.matchMedia("(prefers-color-scheme: dark)").matches) { - backgroundColor = "black"; - } - return backgroundColor; + let backgroundColor = "white"; + if (document?.documentElement?.classList.contains("dark")) { + backgroundColor = "black"; + } else if (document?.documentElement?.classList.contains("light")) { + backgroundColor = "white"; + } else if (window?.matchMedia("(prefers-color-scheme: dark)").matches) { + backgroundColor = "black"; + } + return backgroundColor; } const StageReactContext = createContext(undefined); function useStage() { - const stage = useContext(StageReactContext); - if (!stage) { - throw new Error("useStage must be used within a StageProvider"); - } - return stage; + const stage = useContext(StageReactContext); + if (!stage) { + throw new Error("useStage must be used within a StageProvider"); + } + return stage; } export function NGLResidues({ - residues, - color, - opacity = 1.0, - chain = "", - representation, + residues, + color, + opacity = 1.0, + chain = "", + representation, }: { - residues: number[]; - color: string; - opacity?: number; - chain?: string; - representation: StructureRepresentationType; + residues: number[]; + color: string; + opacity?: number; + chain?: string; + representation: StructureRepresentationType; }) { - const name = useId(); - const stage = useStage(); - const component = useComponent(); - - const selection = useMemo(() => { - const sortedResidues = [...residues].sort((a, b) => a - b); - if (sortedResidues.length) { - const newSelection = sortedResidues.join(", "); - if (chain) { - return `:${chain} and ${newSelection}`; - } else { - return newSelection; - } - } - return "not all"; - }, [residues, chain]); - - // biome-ignore lint/correctness/useExhaustiveDependencies: to not (re)create new representation when selection changes, keep it out of dep list - useEffect(() => { - component.addRepresentation(representation, { - name, - sele: selection, - color, - opacity, - }); - return () => { - const repr = stage.getRepresentationsByName(name).first; - if (repr) { - repr.dispose(); - } - }; - }, [stage, component, color, opacity, name, representation]); - - useEffect(() => { - const repr = stage.getRepresentationsByName(name).first; - if (repr) { - repr.setSelection(selection); - } - }, [selection, name, stage]); - - return null; + const name = useId(); + const stage = useStage(); + const component = useComponent(); + + const selection = useMemo(() => { + const sortedResidues = [...residues].sort((a, b) => a - b); + if (sortedResidues.length) { + const newSelection = sortedResidues.join(", "); + if (chain) { + return `:${chain} and ${newSelection}`; + } else { + return newSelection; + } + } + return "not all"; + }, [residues, chain]); + + // biome-ignore lint/correctness/useExhaustiveDependencies: to not (re)create new representation when selection changes, keep it out of dep list + useEffect(() => { + component.addRepresentation(representation, { + name, + sele: selection, + color, + opacity, + }); + return () => { + const repr = stage.getRepresentationsByName(name).first; + if (repr) { + repr.dispose(); + } + }; + }, [stage, component, color, opacity, name, representation]); + + useEffect(() => { + const repr = stage.getRepresentationsByName(name).first; + if (repr) { + repr.setSelection(selection); + } + }, [selection, name, stage]); + + return null; } function isValidStructure( - structure: Structure | undefined, + structure: Structure | undefined, ): structure is Structure { - // Removing component of structure in stage - // will modifies the structure so it contains no atoms, but the count does not reflect that - return structure !== undefined && structure.atomStore.x !== undefined; + // Removing component of structure in stage + // will modifies the structure so it contains no atoms, but the count does not reflect that + return structure !== undefined && structure.atomStore.x !== undefined; } function isStructureComponent( - component: Component, + component: Component, ): component is StructureComponent { - return (component as StructureComponent).structure !== undefined; + return (component as StructureComponent).structure !== undefined; } function stageHasValidStructure(stage: Stage, name: string): boolean { - const component = stage.getComponentsByName(name).first; - if (!component) { - return false; - } - return ( - isStructureComponent(component) && isValidStructure(component.structure) - ); + const component = stage.getComponentsByName(name).first; + if (!component) { + return false; + } + return ( + isStructureComponent(component) && isValidStructure(component.structure) + ); } const NGLComponentContext = createContext( - undefined, + undefined, ); export function useComponent() { - const component = useContext(NGLComponentContext); - if (!component) { - throw new Error( - "useNGLComponent must be used within a NGLComponentProvider", - ); - } - return component; + const component = useContext(NGLComponentContext); + if (!component) { + throw new Error( + "useNGLComponent must be used within a NGLComponentProvider", + ); + } + return component; } // List from https://github.com/nglviewer/ngl/blob/5d64dbe6769448e0f33080e9ac957a70a0973a13/src/component/structure-component.ts#L52-L79 const defaultRepresentationNames = new Set([ - "angle", - "axes", - "backbone", - "ball+stick", - "base", - "cartoon", - "contact", - "dihedral", - "dihedral-histogram", - "distance", - "dot", - "helixorient", - "hyperball", - "label", - "licorice", - "line", - "molecularsurface", - "point", - "ribbon", - "rocket", - "rope", - "slice", - "spacefill", - "surface", - "trace", - "tube", - "unitcell", - "validation", + "angle", + "axes", + "backbone", + "ball+stick", + "base", + "cartoon", + "contact", + "dihedral", + "dihedral-histogram", + "distance", + "dot", + "helixorient", + "hyperball", + "label", + "licorice", + "line", + "molecularsurface", + "point", + "ribbon", + "rocket", + "rope", + "slice", + "spacefill", + "surface", + "trace", + "tube", + "unitcell", + "validation", ]); export function NGLComponent({ - structure, - chain, - opacity = 1.0, - children, + structure, + chain, + opacity = 1.0, + children, }: { - structure: File; - chain: string; - opacity?: number; - children?: ReactNode; + structure: File; + chain: string; + opacity?: number; + children?: ReactNode; }) { - const stage = useStage(); - const [component, setComponent] = useState( - undefined, - ); - - useEffect(() => { - async function loadStructure() { - stage.getComponentsByName(structure.name).dispose(); - const newComponent = await stage.loadFile(structure); - if (!newComponent) { - return; - } - stage.defaultFileRepresentation(newComponent); - stage.autoView(); - setComponent(newComponent as StructureComponent); - } - loadStructure(); - return () => { - stage.getComponentsByName(structure.name).dispose(); - }; - }, [stage, structure]); - - useEffect(() => { - if (!component) { - return; - } - stage.eachRepresentation((repr) => { - // representations created with defaultFileRepresentation have default name - // while nested representations have unique names generated with useId hook - if ( - repr.parent.name === component.name && - defaultRepresentationNames.has(repr.name) - ) { - repr.setParameters({ opacity }); - } - }); - stage.viewer.requestRender(); - }, [opacity, component, stage]); - - useEffect(() => { - if (!component) { - return; - } - if (chain === "") { - component.setSelection(""); - } else { - component.setSelection(`:${chain}`); - } - stage.autoView(); - return () => { - if (!component) { - return; - } - if (!stageHasValidStructure(stage, component.name)) { - return; - } - const stagedComponent = stage.getComponentsByObject( - component.structure, - ).first; - if (!stagedComponent) { - return; - } - component.setSelection(""); - }; - }, [stage, chain, component]); - - return ( - <> - {component && ( - - {children} - - )} - - ); + const stage = useStage(); + const [component, setComponent] = useState( + undefined, + ); + + useEffect(() => { + async function loadStructure() { + stage.getComponentsByName(structure.name).dispose(); + const newComponent = await stage.loadFile(structure); + if (!newComponent) { + return; + } + stage.defaultFileRepresentation(newComponent); + stage.autoView(); + setComponent(newComponent as StructureComponent); + } + loadStructure(); + return () => { + stage.getComponentsByName(structure.name).dispose(); + }; + }, [stage, structure]); + + useEffect(() => { + if (!component) { + return; + } + stage.eachRepresentation((repr) => { + // representations created with defaultFileRepresentation have default name + // while nested representations have unique names generated with useId hook + if ( + repr.parent.name === component.name && + defaultRepresentationNames.has(repr.name) + ) { + repr.setParameters({ opacity }); + } + }); + stage.viewer.requestRender(); + }, [opacity, component, stage]); + + useEffect(() => { + if (!component) { + return; + } + if (chain === "") { + component.setSelection(""); + } else { + component.setSelection(`:${chain}`); + } + stage.autoView(); + return () => { + if (!component) { + return; + } + if (!stageHasValidStructure(stage, component.name)) { + return; + } + const stagedComponent = stage.getComponentsByObject( + component.structure, + ).first; + if (!stagedComponent) { + return; + } + component.setSelection(""); + }; + }, [stage, chain, component]); + + return ( + <> + {component && ( + + {children} + + )} + + ); } export function NGLStage({ - onMouseLeave = () => {}, - onPick, - onHover, - children, + onMouseLeave = () => {}, + onPick, + onHover, + children, }: { - children: ReactNode; - onPick?: ( - chain: string, - residue: number, - componentName: string, - resname: string, - ) => void; - onHover?: ( - chain: string, - residue: number, - componentName: string, - resname: string, - ) => void; - onMouseLeave?: () => void; + children: ReactNode; + onPick?: ( + chain: string, + residue: number, + componentName: string, + resname: string, + ) => void; + onHover?: ( + chain: string, + residue: number, + componentName: string, + resname: string, + ) => void; + onMouseLeave?: () => void; }) { - const [stage, setStage] = useState(); - - const stageElementRef: RefCallback = useCallback((element) => { - if (element) { - const backgroundColor = currentBackground(); - const currentStage = new Stage(element, { backgroundColor }); - setStage(currentStage); - } - }, []); - - useEffect(() => { - return (): void => { - if (stage) { - // stage.dispose(); - } - }; - }, [stage]); - - const onClick = useCallback( - (pickinProxy: PickingProxy) => { - if (onPick && pickinProxy?.atom?.resno && pickinProxy?.atom?.chainname) { - onPick( - pickinProxy.atom.chainname, - pickinProxy.atom.resno, - pickinProxy.component.name, - pickinProxy.atom.resname, - ); - } - }, - [onPick], - ); - - useEffect(() => { - if (!onClick || !stage) { - return; - } - stage.signals.clicked.add(onClick); - return () => { - if (onClick) { - stage.signals.clicked.remove(onClick); - } - }; - }, [stage, onClick]); - - const onHoverCallback = useCallback( - (pickinProxy: PickingProxy) => { - if (onHover && pickinProxy?.atom?.resno && pickinProxy?.atom?.chainname) { - onHover( - pickinProxy.atom.chainname, - pickinProxy.atom.resno, - pickinProxy.component.name, - pickinProxy.atom.resname, - ); - } - }, - [onHover], - ); - - useEffect(() => { - if (!onHoverCallback || !stage) { - return; - } - stage.signals.hovered.add(onHoverCallback); - return () => { - if (onHoverCallback) { - stage.signals.hovered.remove(onHoverCallback); - } - }; - }, [stage, onHoverCallback]); - - return ( -
-
- {stage && ( - <> -
- { - e.preventDefault(); - stage.autoView(); - }} - > - ◎ - -
- - {children} - - - )} -
- ); + const [stage, setStage] = useState(); + + const stageElementRef: RefCallback = useCallback((element) => { + if (element) { + const backgroundColor = currentBackground(); + const currentStage = new Stage(element, { backgroundColor }); + setStage(currentStage); + } + }, []); + + useEffect(() => { + return (): void => { + if (stage) { + // stage.dispose(); + } + }; + }, [stage]); + + const onClick = useCallback( + (pickinProxy: PickingProxy) => { + if (onPick && pickinProxy?.atom?.resno && pickinProxy?.atom?.chainname) { + onPick( + pickinProxy.atom.chainname, + pickinProxy.atom.resno, + pickinProxy.component.name, + pickinProxy.atom.resname, + ); + } + }, + [onPick], + ); + + useEffect(() => { + if (!onClick || !stage) { + return; + } + stage.signals.clicked.add(onClick); + return () => { + if (onClick) { + stage.signals.clicked.remove(onClick); + } + }; + }, [stage, onClick]); + + const onHoverCallback = useCallback( + (pickinProxy: PickingProxy) => { + if (onHover && pickinProxy?.atom?.resno && pickinProxy?.atom?.chainname) { + onHover( + pickinProxy.atom.chainname, + pickinProxy.atom.resno, + pickinProxy.component.name, + pickinProxy.atom.resname, + ); + } + }, + [onHover], + ); + + useEffect(() => { + if (!onHoverCallback || !stage) { + return; + } + stage.signals.hovered.add(onHoverCallback); + return () => { + if (onHoverCallback) { + stage.signals.hovered.remove(onHoverCallback); + } + }; + }, [stage, onHoverCallback]); + + return ( +
+
+ {stage && ( + <> +
+ { + e.preventDefault(); + stage.autoView(); + }} + > + ◎ + +
+ + {children} + + + )} +
+ ); } export class ErrorBoundary extends ReactComponent< - { children: ReactNode }, - { hasError: boolean } + { children: ReactNode }, + { hasError: boolean } > { - constructor(props: { children: ReactNode }) { - super(props); - this.state = { hasError: false }; - } - - static getDerivedStateFromError() { - // Update state so the next render will show the fallback UI. - return { hasError: true }; - } - override componentDidCatch(error: Error, errorInfo: ErrorInfo) { - // You can also log the error to an error reporting service - console.error(error, errorInfo); - } - - override render() { - if (this.state.hasError) { - // You can render any custom fallback UI - return

Something went wrong. See DevTools console

; - } - - return this.props.children; - } + constructor(props: { children: ReactNode }) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError() { + // Update state so the next render will show the fallback UI. + return { hasError: true }; + } + override componentDidCatch(error: Error, errorInfo: ErrorInfo) { + // You can also log the error to an error reporting service + console.error(error, errorInfo); + } + + override render() { + if (this.state.hasError) { + // You can render any custom fallback UI + return

Something went wrong. See DevTools console

; + } + + return this.props.children; + } } /** * Use single surface to display multiple colors of residue sets */ export function NGLSurface({ - active = [], - passive = [], - neighbours = [], - highlight, - activeColor = "green", - passiveColor = "yellow", - neighboursColor = "orange", - highlightColor = "red", - defaultColor = "white", + active = [], + passive = [], + neighbours = [], + highlight, + activeColor = "green", + passiveColor = "yellow", + neighboursColor = "orange", + highlightColor = "red", + defaultColor = "white", }: { - active: number[]; - passive: number[]; - neighbours: number[]; - highlight?: number | undefined; - activeColor?: string; - passiveColor?: string; - defaultColor?: string; - highlightColor?: string; - neighboursColor?: string; + active: number[]; + passive: number[]; + neighbours: number[]; + highlight?: number | undefined; + activeColor?: string; + passiveColor?: string; + defaultColor?: string; + highlightColor?: string; + neighboursColor?: string; }) { - const name = useId(); - - const schemeId = useMemo(() => { - const oldSchemeId = Object.keys(ColormakerRegistry.userSchemes).find( - (key) => key.endsWith(`|${name}`), - ); - if (oldSchemeId) { - ColormakerRegistry.removeScheme(oldSchemeId); - } - return ColormakerRegistry.addSelectionScheme( - [ - [highlightColor, `${highlight}` ?? "not all", undefined], - [activeColor, active.join(", "), undefined], - [passiveColor, passive.join(", "), undefined], - [neighboursColor, neighbours.join(", "), undefined], - [defaultColor, "*", undefined], - ], - name, - ); - }, [ - active, - activeColor, - defaultColor, - highlight, - highlightColor, - name, - neighbours, - neighboursColor, - passive, - passiveColor, - ]); - - const stage = useStage(); - const component = useComponent(); - - useEffect(() => { - component.addRepresentation("surface", { - name, - color: defaultColor, - }); - return () => { - const repr = stage.getRepresentationsByName(name).first; - if (repr) { - repr.dispose(); - } - }; - }, [name, defaultColor, stage, component]); - - useEffect(() => { - const repr = stage.getRepresentationsByName(name).first; - if (repr) { - repr.setColor(schemeId); - } - }, [schemeId, name, stage]); - - return null; + const name = useId(); + + const schemeId = useMemo(() => { + const oldSchemeId = Object.keys(ColormakerRegistry.userSchemes).find( + (key) => key.endsWith(`|${name}`), + ); + if (oldSchemeId) { + ColormakerRegistry.removeScheme(oldSchemeId); + } + return ColormakerRegistry.addSelectionScheme( + [ + [highlightColor, `${highlight}` ?? "not all", undefined], + [activeColor, active.join(", "), undefined], + [passiveColor, passive.join(", "), undefined], + [neighboursColor, neighbours.join(", "), undefined], + [defaultColor, "*", undefined], + ], + name, + ); + }, [ + active, + activeColor, + defaultColor, + highlight, + highlightColor, + name, + neighbours, + neighboursColor, + passive, + passiveColor, + ]); + + const stage = useStage(); + const component = useComponent(); + + useEffect(() => { + component.addRepresentation("surface", { + name, + color: defaultColor, + }); + return () => { + const repr = stage.getRepresentationsByName(name).first; + if (repr) { + repr.dispose(); + } + }; + }, [name, defaultColor, stage, component]); + + useEffect(() => { + const repr = stage.getRepresentationsByName(name).first; + if (repr) { + repr.setColor(schemeId); + } + }, [schemeId, name, stage]); + + return null; } export function SimpleViewer({ structure }: { structure: File }) { - return ( - - - - - - ); + return ( + + + + + + ); } export function Viewer({ - structure, - chain, - active, - passive, - renderSelectionAs = "surface", - surface, - neighbours = [], - higlightResidue, - onPick, - onHover, - onMouseLeave = () => {}, - selectionOpacity = 0.5, - theme = "light", + structure, + chain, + active, + passive, + renderSelectionAs = "surface", + surface, + neighbours = [], + higlightResidue, + onPick, + onHover, + onMouseLeave = () => {}, + selectionOpacity = 0.5, + theme = "light", }: { - structure: File; - chain: string; - active: number[]; - passive: number[]; - surface: number[]; - renderSelectionAs?: StructureRepresentationType; - neighbours?: number[]; - higlightResidue?: number | undefined; - onPick?: (chain: string, residue: number) => void; - onHover?: (chain: string, residue: number) => void; - onMouseLeave?: () => void; - selectionOpacity?: number; - theme?: "light" | "dark"; + structure: File; + chain: string; + active: number[]; + passive: number[]; + surface: number[]; + renderSelectionAs?: StructureRepresentationType; + neighbours?: number[]; + higlightResidue?: number | undefined; + onPick?: (chain: string, residue: number) => void; + onHover?: (chain: string, residue: number) => void; + onMouseLeave?: () => void; + selectionOpacity?: number; + theme?: "light" | "dark"; }) { - const isDark = theme === "dark"; - const activeColor = isDark ? "green" : "lime"; - const passiveColor = isDark ? "orange" : "yellow"; - const opacity = 0.5; - - let representations = <>; - if (renderSelectionAs === "surface") { - representations = ( - - ); - } else { - representations = ( - <> - {higlightResidue && ( - - )} - - - - - - ); - } - - return ( - - - - {representations} - - - - ); + const isDark = theme === "dark"; + const activeColor = isDark ? "green" : "lime"; + const passiveColor = isDark ? "orange" : "yellow"; + const opacity = 0.5; + + let representations = <>; + if (renderSelectionAs === "surface") { + representations = ( + + ); + } else { + representations = ( + <> + {higlightResidue && ( + + )} + + + + + + ); + } + + return ( + + + + {representations} + + + + ); } export interface Hetero { - resno: number; - resname: string; - chain: string; - description?: string; + resno: number; + resname: string; + chain: string; + description?: string; } export function LigandViewer({ - structure, - selected, - onPick, - onHover, - highlight, - onMouseLeave, - theme = "light", + structure, + selected, + onPick, + onHover, + highlight, + onMouseLeave, + theme = "light", }: { - structure: File; - selected: Hetero | undefined; - onPick: (picked: string) => void; - onHover: (hovering: string) => void; - highlight: string | undefined; - onMouseLeave?: () => void; - theme?: "light" | "dark"; + structure: File; + selected: Hetero | undefined; + onPick: (picked: string) => void; + onHover: (hovering: string) => void; + highlight: string | undefined; + onMouseLeave?: () => void; + theme?: "light" | "dark"; }) { - const isDark = theme === "dark"; - const activeColor = isDark ? "green" : "lime"; - const opacity = selected ? 0.0 : 1.0; - const representations = ( - <> - {highlight && ( - - )} - {selected && ( - - )} - - ); - - function onLigandPick( - chain: string, - residue: number, - _componentName: string, - resname: string, - ) { - - onPick(`${resname}-${chain}-${residue}`); - } - - function onLigandHover( - chain: string, - residue: number, - _componentName: string, - resname: string, - ) { - onHover(`${resname}-${chain}-${residue}`); - } - - return ( - - - - {representations} - - - - ); + const isDark = theme === "dark"; + const activeColor = isDark ? "green" : "lime"; + const opacity = selected ? 0.0 : 1.0; + const representations = ( + <> + {highlight && ( + + )} + {selected && ( + + )} + + ); + + function onLigandPick( + chain: string, + residue: number, + _componentName: string, + resname: string, + ) { + onPick(`${resname}-${chain}-${residue}`); + } + + function onLigandHover( + chain: string, + residue: number, + _componentName: string, + resname: string, + ) { + onHover(`${resname}-${chain}-${residue}`); + } + + return ( + + + + {representations} + + + + ); } diff --git a/src/structure.ts b/src/structure.ts index 02dac79..fcd4dd1 100644 --- a/src/structure.ts +++ b/src/structure.ts @@ -1,5 +1,5 @@ import structureUrl from "./assets/2oob.pdb?raw"; export const structure = new File([structureUrl], "2oob.pdb", { - type: "chemical/x-pdb", -}); \ No newline at end of file + type: "chemical/x-pdb", +}); diff --git a/src/toggles.tsx b/src/toggles.tsx index daff496..1eea791 100644 --- a/src/toggles.tsx +++ b/src/toggles.tsx @@ -1,325 +1,329 @@ /* eslint-disable react-refresh/only-export-components */ import { - type ChangeEvent, - type PropsWithChildren, - useCallback, - useId, - useMemo, - useState, + type ChangeEvent, + type PropsWithChildren, + useCallback, + useId, + useMemo, + useState, } from "react"; import { cn } from "./cn.js"; -import { useChunked } from "./useChunked"; import { ResiduesHeader } from "./toggles/ResidueHeader"; -import { residueVariants, Variant } from "./toggles/variants"; +import { Variant, residueVariants } from "./toggles/variants"; +import { useChunked } from "./useChunked"; export function useResidueChangeHandler({ - selected, - options, - onChange, - filter = () => true, + selected, + options, + onChange, + filter = () => true, }: { - options: Residue[]; - selected: ResidueSelection; - onChange: (selected: ResidueSelection) => void; - filter?: (resno: number) => boolean; + options: Residue[]; + selected: ResidueSelection; + onChange: (selected: ResidueSelection) => void; + filter?: (resno: number) => boolean; }) { - const [lastChecked, setLastChecked] = useState(null); - const handler = useCallback( - (e: ChangeEvent, index: number, actpass: ActPass) => { - const residue = parseInt(e.target.value); - const ne = e.nativeEvent as KeyboardEvent; - let newSelected: number[] = []; - if (ne.shiftKey && lastChecked !== null) { - const start = Math.min(lastChecked, index); - const end = Math.max(lastChecked, index); - newSelected = [...selected[actpass]]; - for (let i = start; i <= end; i++) { - const resno = options[i]!.resno; - if (!newSelected.includes(resno) && filter(resno)) { - newSelected.push(resno); - } - } - } else { - if (e.target.checked) { - newSelected = [...selected[actpass], residue]; - } else { - newSelected = selected[actpass].filter((r) => r !== residue); - } - } - if (actpass === "act") { - // Active should take precedence over passive. - // For example given passive is selected, - // then selecting same residue as active should remove it from passive. - const passiveWithoutAlsoActive = selected.pass.filter( - (r) => !newSelected.includes(r) - ); - onChange({ - act: newSelected, - pass: passiveWithoutAlsoActive, - }); - } else { - onChange({ - pass: newSelected, - act: selected.act, - }); - } + const [lastChecked, setLastChecked] = useState(null); + const handler = useCallback( + (e: ChangeEvent, index: number, actpass: ActPass) => { + const residue = parseInt(e.target.value); + const ne = e.nativeEvent as KeyboardEvent; + let newSelected: number[] = []; + if (ne.shiftKey && lastChecked !== null) { + const start = Math.min(lastChecked, index); + const end = Math.max(lastChecked, index); + newSelected = [...selected[actpass]]; + for (let i = start; i <= end; i++) { + const resno = options[i]!.resno; + if (!newSelected.includes(resno) && filter(resno)) { + newSelected.push(resno); + } + } + } else { + if (e.target.checked) { + newSelected = [...selected[actpass], residue]; + } else { + newSelected = selected[actpass].filter((r) => r !== residue); + } + } + if (actpass === "act") { + // Active should take precedence over passive. + // For example given passive is selected, + // then selecting same residue as active should remove it from passive. + const passiveWithoutAlsoActive = selected.pass.filter( + (r) => !newSelected.includes(r), + ); + onChange({ + act: newSelected, + pass: passiveWithoutAlsoActive, + }); + } else { + onChange({ + pass: newSelected, + act: selected.act, + }); + } - if (e.target.checked) { - setLastChecked(index); - } - }, - [filter, lastChecked, onChange, options, selected] - ); + if (e.target.checked) { + setLastChecked(index); + } + }, + [filter, lastChecked, onChange, options, selected], + ); - return handler; + return handler; } export interface Residue { - resno: number; - resname: string; - seq: string; - surface?: boolean; + resno: number; + resname: string; + seq: string; + surface?: boolean; } export function FormDescription({ children }: PropsWithChildren): JSX.Element { - return

{children}

; + return

{children}

; } export function ResidueCheckbox({ - resno, - resname, - seq, - showActive, - showPassive, - highlight, - activeChecked, - passiveChecked, - neighbourChecked, - activeDisabled, - passiveDisabled, - onHover, - onActiveChange, - onPassiveChange, - theme = "light", + resno, + resname, + seq, + showActive, + showPassive, + highlight, + activeChecked, + passiveChecked, + neighbourChecked, + activeDisabled, + passiveDisabled, + onHover, + onActiveChange, + onPassiveChange, + theme = "light", }: { - resno: number; - resname: string; - seq: string; - showActive: boolean; - showPassive: boolean; - highlight: boolean; // External component wants us to highlight this residue - activeChecked: boolean; - passiveChecked: boolean; - neighbourChecked: boolean; - activeDisabled: boolean; - passiveDisabled: boolean; - onHover: () => void; // We want external component to know we are hovering - onActiveChange: (event: ChangeEvent) => void; - onPassiveChange: (event: ChangeEvent) => void; - theme: "light" | "dark"; + resno: number; + resname: string; + seq: string; + showActive: boolean; + showPassive: boolean; + highlight: boolean; // External component wants us to highlight this residue + activeChecked: boolean; + passiveChecked: boolean; + neighbourChecked: boolean; + activeDisabled: boolean; + passiveDisabled: boolean; + onHover: () => void; // We want external component to know we are hovering + onActiveChange: (event: ChangeEvent) => void; + onPassiveChange: (event: ChangeEvent) => void; + theme: "light" | "dark"; }) { - const id = useId(); - const style = { colorScheme: theme === "dark" ? "dark" : "light" }; - let htmlFor = id + "act"; - if (showPassive && !showActive) { - htmlFor = id + "pass"; - } + const id = useId(); + const style = { colorScheme: theme === "dark" ? "dark" : "light" }; + let htmlFor = id + "act"; + if (showPassive && !showActive) { + htmlFor = id + "pass"; + } - let variant: Variant = ""; - if (passiveChecked || neighbourChecked) { - variant = "pass"; - } - if (activeChecked) { - variant = "act"; - } - if (highlight) { - variant = "highlight"; - } + let variant: Variant = ""; + if (passiveChecked || neighbourChecked) { + variant = "pass"; + } + if (activeChecked) { + variant = "act"; + } + if (highlight) { + variant = "highlight"; + } - return ( -
- - {showActive && ( - - )} - {showPassive && ( - - )} -
- ); + return ( +
+ + {showActive && ( + + )} + {showPassive && ( + + )} +
+ ); } export interface ResidueSelection { - act: number[]; - pass: number[]; + act: number[]; + pass: number[]; } export interface ResidueNeighbourSelection extends ResidueSelection { - neighbours: number[]; + neighbours: number[]; } export function ResiduesSelect({ - options, - selected, - onChange, - disabledPassive = false, - disabledActive = false, - showPassive = false, - showActive = false, - showNeighbours = false, - onHover, - highlight, - theme = "light", + options, + selected, + onChange, + disabledPassive = false, + disabledActive = false, + showPassive = false, + showActive = false, + showNeighbours = false, + onHover, + highlight, + theme = "light", }: { - options: Residue[]; - selected: ResidueNeighbourSelection; - onChange: (selected: ResidueSelection) => void; - disabledPassive?: boolean; - disabledActive?: boolean; - showPassive?: boolean; - showActive?: boolean; - showNeighbours?: boolean; - onHover: (resno: number | undefined) => void; - highlight: number | undefined; - theme: "light" | "dark"; + options: Residue[]; + selected: ResidueNeighbourSelection; + onChange: (selected: ResidueSelection) => void; + disabledPassive?: boolean; + disabledActive?: boolean; + showPassive?: boolean; + showActive?: boolean; + showNeighbours?: boolean; + onHover: (resno: number | undefined) => void; + highlight: number | undefined; + theme: "light" | "dark"; }) { - const surface = useMemo( - () => options.filter((r) => r.surface).map((r) => r.resno), - [options] - ); - const handleChange = useResidueChangeHandler({ - options, - selected, - onChange, - filter: (resno: number) => surface.includes(resno), - }); - const chunkSize = 10; - const chunks = useChunked(options, chunkSize); + const surface = useMemo( + () => options.filter((r) => r.surface).map((r) => r.resno), + [options], + ); + const handleChange = useResidueChangeHandler({ + options, + selected, + onChange, + filter: (resno: number) => surface.includes(resno), + }); + const chunkSize = 10; + const chunks = useChunked(options, chunkSize); - return ( - <> -
- - {chunks.map((chunk, cindex) => ( -
-

- {chunk[0]!.resno} -

-
onHover(undefined)}> - {chunk.map((r, index) => ( - onHover(r.resno)} - onActiveChange={(e) => - handleChange(e, cindex * chunkSize + index, "act") - } - onPassiveChange={(e) => - handleChange(e, cindex * chunkSize + index, "pass") - } - showActive={showActive} - showPassive={showPassive || showNeighbours} - neighbourChecked={selected.neighbours.includes(r.resno)} - theme={theme} - /> - ))} -
-
- ))} -
- - (Hold Shift to select a range of residues. Click residue in 3D viewer to - select.) - - - ); + return ( + <> +
+ + {chunks.map((chunk, cindex) => ( +
+

+ {chunk[0]!.resno} +

+
onHover(undefined)}> + {chunk.map((r, index) => ( + onHover(r.resno)} + onActiveChange={(e) => + handleChange(e, cindex * chunkSize + index, "act") + } + onPassiveChange={(e) => + handleChange(e, cindex * chunkSize + index, "pass") + } + showActive={showActive} + showPassive={showPassive || showNeighbours} + neighbourChecked={selected.neighbours.includes(r.resno)} + theme={theme} + /> + ))} +
+
+ ))} +
+ + (Hold Shift to select a range of residues. Click residue in 3D viewer to + select.) + + + ); } export type ActPass = "act" | "pass"; export function PickIn3D({ - value, - onChange, + value, + onChange, }: { - value: ActPass; - onChange: (value: ActPass) => void; + value: ActPass; + onChange: (value: ActPass) => void; }) { - const idAct = useId(); - const idPass = useId(); - return ( -
-
3D viewer picks
- {/* TODO make stylable from outside */} -
- onChange("act")} - /> - -
-
- onChange("pass")} - /> - -
-
- ); + const idAct = useId(); + const idPass = useId(); + return ( +
+
3D viewer picks
+ {/* TODO make stylable from outside */} +
+ onChange("act")} + /> + +
+
+ onChange("pass")} + /> + +
+
+ ); } diff --git a/src/toggles/ResidueHeader.stories.tsx b/src/toggles/ResidueHeader.stories.tsx index df56a3b..0372ccd 100644 --- a/src/toggles/ResidueHeader.stories.tsx +++ b/src/toggles/ResidueHeader.stories.tsx @@ -1,17 +1,14 @@ -import { ResiduesHeader } from "./ResidueHeader"; import type { Story } from "@ladle/react"; +import { ResiduesHeader } from "./ResidueHeader"; export const ActiveAndPassive: Story<{ - showActive: boolean; - showPassive: boolean; -}> = ({showActive, showPassive}) => ( - + showActive: boolean; + showPassive: boolean; +}> = ({ showActive, showPassive }) => ( + ); ActiveAndPassive.args = { - showActive: true, - showPassive: true, + showActive: true, + showPassive: true, }; diff --git a/src/toggles/ResidueHeader.tsx b/src/toggles/ResidueHeader.tsx index 0eb526b..6b6f2a8 100644 --- a/src/toggles/ResidueHeader.tsx +++ b/src/toggles/ResidueHeader.tsx @@ -1,35 +1,34 @@ import { cn } from "../cn"; -import { residueVariants, Variant } from "./variants"; +import { Variant, residueVariants } from "./variants"; export function ResiduesHeader({ - showActive, - showPassive, - }: { - showActive: boolean; - showPassive: boolean; - }) { - return ( -
-

 

-
-
- {/* use non breaking whitespace to prevent layout shifts */} -         -
- {showActive && } - {showPassive && } -
-
- ); - } - - export function ResidueHeaderItem({ - variant, - label, - }: { - variant: Variant; - label: string; - }) { - return
{label}
; - } - \ No newline at end of file + showActive, + showPassive, +}: { + showActive: boolean; + showPassive: boolean; +}) { + return ( +
+

 

+
+
+ {/* use non breaking whitespace to prevent layout shifts */} +         +
+ {showActive && } + {showPassive && } +
+
+ ); +} + +export function ResidueHeaderItem({ + variant, + label, +}: { + variant: Variant; + label: string; +}) { + return
{label}
; +} diff --git a/src/toggles/variants.ts b/src/toggles/variants.ts index 39fefac..43fdbf6 100644 --- a/src/toggles/variants.ts +++ b/src/toggles/variants.ts @@ -1,8 +1,8 @@ export type Variant = "act" | "pass" | "highlight" | ""; export const residueVariants: Record = { - act: "bg-green-100 dark:bg-green-700", - pass: "bg-yellow-100 dark:bg-yellow-700", - highlight: "bg-secondary dark:bg-secondary-foreground", - "": "bg-inherit dark:bg-inherit", -}; \ No newline at end of file + act: "bg-green-100 dark:bg-green-700", + pass: "bg-yellow-100 dark:bg-yellow-700", + highlight: "bg-secondary dark:bg-secondary-foreground", + "": "bg-inherit dark:bg-inherit", +}; diff --git a/src/useChunked.stories.tsx b/src/useChunked.stories.tsx index ae9e8c6..18a66fe 100644 --- a/src/useChunked.stories.tsx +++ b/src/useChunked.stories.tsx @@ -1,16 +1,16 @@ -import type { Story} from '@ladle/react' -import { useChunked } from './useChunked' +import type { Story } from "@ladle/react"; +import { useChunked } from "./useChunked"; -const all = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'] +const all = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]; export const Default: Story = () => { - const chunked = useChunked(all, 3) - return ( -
- All: -
{JSON.stringify(all)}
- Chunked: -
{JSON.stringify(chunked, undefined, 2)}
-
- ) -} \ No newline at end of file + const chunked = useChunked(all, 3); + return ( +
+ All: +
{JSON.stringify(all)}
+ Chunked: +
{JSON.stringify(chunked, undefined, 2)}
+
+ ); +}; diff --git a/src/useChunked.ts b/src/useChunked.ts index 2ca22ca..311988d 100644 --- a/src/useChunked.ts +++ b/src/useChunked.ts @@ -1,19 +1,19 @@ import { useMemo } from "react"; export function useChunked(raw: T[], chunkSize: number) { - return useMemo(() => { - const initialArray: T[][] = []; - const chunks = raw.reduce((resultArray, item, index) => { - const chunkIndex = Math.floor(index / chunkSize); + return useMemo(() => { + const initialArray: T[][] = []; + const chunks = raw.reduce((resultArray, item, index) => { + const chunkIndex = Math.floor(index / chunkSize); - if (!resultArray[chunkIndex]) { - resultArray[chunkIndex] = []; // start a new chunk - } + if (!resultArray[chunkIndex]) { + resultArray[chunkIndex] = []; // start a new chunk + } - resultArray[chunkIndex].push(item); + resultArray[chunkIndex].push(item); - return resultArray; - }, initialArray); - return chunks; - }, [raw, chunkSize]); + return resultArray; + }, initialArray); + return chunks; + }, [raw, chunkSize]); } diff --git a/tailwind.config.js b/tailwind.config.js index 4083e3e..e8f3dd6 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,15 +1,13 @@ /** @type {import('tailwindcss').Config} */ export default { - darkMode: 'selector', - content: [ - "./index.html", - "./src/**/*.{js,ts,jsx,tsx,mdx}", - ".ladle/components.tsx", - ], - theme: { - extend: {}, - }, - plugins: [ - require('@tailwindcss/typography'), - ], -} + darkMode: "selector", + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx,mdx}", + ".ladle/components.tsx", + ], + theme: { + extend: {}, + }, + plugins: [require("@tailwindcss/typography")], +}; diff --git a/tsconfig.app.json b/tsconfig.app.json index ef6be00..002e156 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -1,25 +1,25 @@ { - "compilerOptions": { - "composite": true, - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src"] + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] } diff --git a/tsconfig.json b/tsconfig.json index ea9d0cd..a5b06bf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,11 @@ { - "files": [], - "references": [ - { - "path": "./tsconfig.app.json" - }, - { - "path": "./tsconfig.node.json" - } - ] + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + } + ] } diff --git a/tsconfig.node.json b/tsconfig.node.json index 3afdd6e..e19b72a 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -1,13 +1,13 @@ { - "compilerOptions": { - "composite": true, - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true, - "strict": true, - "noEmit": true - }, - "include": ["vite.config.ts"] + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true, + "noEmit": true + }, + "include": ["vite.config.ts"] } diff --git a/vite.config.ts b/vite.config.ts index cf90caf..d9367cd 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,35 +1,35 @@ -import { defineConfig, UserConfig } from 'vite' -import { resolve } from 'node:path' -import react from '@vitejs/plugin-react' +import { resolve } from "node:path"; +import react from "@vitejs/plugin-react"; +import { UserConfig, defineConfig } from "vite"; import dts from "vite-plugin-dts"; -const build: UserConfig['build'] = { - sourcemap: true, - lib: { - // TODO dont use barrel file - entry: resolve(__dirname, 'src/index.tsx'), - formats: ['es'], - name: '@i-vresse/haddock3-ui', - fileName: 'index', - }, - rollupOptions: { - external: ['react', 'react/jsx-runtime', 'ngl', 'clsx', 'tailwind-merge'], - } -} +const build: UserConfig["build"] = { + sourcemap: true, + lib: { + // TODO dont use barrel file + entry: resolve(__dirname, "src/index.tsx"), + formats: ["es"], + name: "@i-vresse/haddock3-ui", + fileName: "index", + }, + rollupOptions: { + external: ["react", "react/jsx-runtime", "ngl", "clsx", "tailwind-merge"], + }, +}; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [ - react(), - dts({ - tsconfigPath: resolve(__dirname,'./tsconfig.app.json'), - }) - ], - // Ladle does not work in vite library mode, so we drop its config when ladle runs - // vite commands set NODE_ENV to development or production - // ladle commands do not set NODE_ENV - build: process.env.NODE_ENV === undefined ? undefined : build, - server: { - open: false - } -}) + plugins: [ + react(), + dts({ + tsconfigPath: resolve(__dirname, "./tsconfig.app.json"), + }), + ], + // Ladle does not work in vite library mode, so we drop its config when ladle runs + // vite commands set NODE_ENV to development or production + // ladle commands do not set NODE_ENV + build: process.env.NODE_ENV === undefined ? undefined : build, + server: { + open: false, + }, +}); From e12daa389e462a69a01a8ecab7a5fc82971aa717 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Thu, 25 Jul 2024 17:45:45 +0200 Subject: [PATCH 4/6] Add recommended vscode extensions --- .vscode/extensions.json | 7 +++++++ src/index.tsx | 9 ++++----- 2 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 .vscode/extensions.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..2de6454 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "biomejs.biome", + "bradlc.vscode-tailwindcss", + "yoavbls.pretty-ts-errors" + ] +} diff --git a/src/index.tsx b/src/index.tsx index 81e859f..aec0b0c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,10 +1,9 @@ /* eslint-disable react-refresh/only-export-components */ import { ResiduesHeader } from "./toggles/ResidueHeader"; import { type Variant, residueVariants } from "./toggles/variants"; -export { ResiduesHeader }; -export type { Variant }; -export { residueVariants }; export { CopyToClipBoardIcon } from "./CopyToClipBoardIcon"; -export { ResiduesSelect, PickIn3D } from "./toggles"; +export { NGLComponent, NGLResidues, NGLStage, SimpleViewer } from "./molviewer"; +export { PickIn3D, ResiduesSelect } from "./toggles"; export { useChunked } from "./useChunked"; -export { NGLStage, NGLComponent, NGLResidues, SimpleViewer } from "./molviewer"; +export { ResiduesHeader, residueVariants }; +export type { Variant }; From 29d3e64b7eae40ee2f36e48bce281534167d56de Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Thu, 25 Jul 2024 17:48:56 +0200 Subject: [PATCH 5/6] Remove unused lint comments --- src/index.tsx | 1 - src/molviewer.tsx | 4 ++-- src/toggles.tsx | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index aec0b0c..c013b0f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react-refresh/only-export-components */ import { ResiduesHeader } from "./toggles/ResidueHeader"; import { type Variant, residueVariants } from "./toggles/variants"; export { CopyToClipBoardIcon } from "./CopyToClipBoardIcon"; diff --git a/src/molviewer.tsx b/src/molviewer.tsx index 542f209..cf27223 100644 --- a/src/molviewer.tsx +++ b/src/molviewer.tsx @@ -7,8 +7,6 @@ import { StructureComponent, type StructureRepresentationType, } from "ngl"; -/* eslint-disable react-refresh/only-export-components */ -// TODO split in more files import { type ErrorInfo, Component as ReactComponent, @@ -23,6 +21,8 @@ import { useState, } from "react"; +// TODO split in more files + function currentBackground() { let backgroundColor = "white"; if (document?.documentElement?.classList.contains("dark")) { diff --git a/src/toggles.tsx b/src/toggles.tsx index 1eea791..920aefb 100644 --- a/src/toggles.tsx +++ b/src/toggles.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react-refresh/only-export-components */ import { type ChangeEvent, type PropsWithChildren, From da0993b79456a8fcb0b2f50b419c1b38363931ea Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Thu, 25 Jul 2024 17:50:19 +0200 Subject: [PATCH 6/6] Add biome badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 37dc648..7ab656d 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.12820670.svg)](https://doi.org/10.5281/zenodo.12820670) [![Research Software Directory Badge](https://img.shields.io/badge/rsd-00a3e3.svg)](https://research-software-directory.org/software/haddock3-ui) [![fair-software.eu](https://img.shields.io/badge/fair--software.eu-%E2%97%8F%20%20%E2%97%8F%20%20%E2%97%8F%20%20%E2%97%8F%20%20%E2%97%8B-yellow)](https://fair-software.eu) +[![Checked with Biome](https://img.shields.io/badge/Checked_with-Biome-60a5fa?style=flat&logo=biome)](https://biomejs.dev) The [haddock3 web application](https://github.com/i-VRESSE/haddock3-webapp) had several components that could be used outside of the web application. This package contains those components.