├── .babelrc ├── .editorconfig ├── .env.development ├── .env.test ├── .github ├── dependabot.yml └── workflows │ ├── jest_tests.yml │ ├── publish-npm-manual.yml │ └── publish-npm.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── .storybook ├── main.js └── preview.js ├── Dockerfile ├── README.md ├── babel.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── app.tsx ├── assets │ ├── images │ │ └── periodictable.png │ ├── img │ │ └── network │ │ │ ├── acceptDeleteIcon.png │ │ │ ├── addNodeIcon.png │ │ │ ├── backIcon.png │ │ │ ├── connectIcon.png │ │ │ ├── cross.png │ │ │ ├── cross2.png │ │ │ ├── deleteIcon.png │ │ │ ├── downArrow.png │ │ │ ├── editIcon.png │ │ │ ├── leftArrow.png │ │ │ ├── minus.png │ │ │ ├── plus.png │ │ │ ├── rightArrow.png │ │ │ ├── upArrow.png │ │ │ └── zoomExtends.png │ └── mixins.less ├── components │ ├── crystal-toolkit │ │ ├── CameraContextProvider │ │ │ ├── CameraContextProvider.tsx │ │ │ ├── camera-reducer.ts │ │ │ └── index.tsx │ │ ├── CrystalToolkitScene │ │ │ ├── CrystalToolkitScene.less │ │ │ ├── CrystalToolkitScene.test.tsx │ │ │ ├── CrystalToolkitScene.tsx │ │ │ └── index.tsx │ │ ├── Download │ │ │ ├── Download.tsx │ │ │ └── index.tsx │ │ ├── DynamicCrystalToolkitScene │ │ │ ├── DynamicCrystalToolkitScene.tsx │ │ │ └── index.tsx │ │ ├── graph.component.tsx │ │ ├── scene │ │ │ ├── RadiusTubeBufferGeometry.ts │ │ │ ├── Scene.ts │ │ │ ├── animation-helper.ts │ │ │ ├── bezier-scene.ts │ │ │ ├── constants.ts │ │ │ ├── debug-helper.ts │ │ │ ├── download-event.ts │ │ │ ├── glass.png │ │ │ ├── inset-helper.ts │ │ │ ├── mike.ts │ │ │ ├── simple-scene.ts │ │ │ ├── surface-scene.ts │ │ │ ├── three_builder.ts │ │ │ └── tooltip-helper.ts │ │ ├── utils.ts │ │ └── vis.less │ ├── data-display │ │ ├── ActiveFilterButtons │ │ │ ├── ActiveFilterButtons.css │ │ │ ├── ActiveFilterButtons.tsx │ │ │ └── index.tsx │ │ ├── ArrayChips │ │ │ ├── ArrayChips.tsx │ │ │ └── index.tsx │ │ ├── ButtonBar │ │ │ ├── ButtonBar.css │ │ │ ├── ButtonBar.tsx │ │ │ └── index.tsx │ │ ├── DataBlock │ │ │ ├── DataBlock.css │ │ │ ├── DataBlock.test.tsx │ │ │ ├── DataBlock.tsx │ │ │ └── index.tsx │ │ ├── DataCard │ │ │ ├── DataCard.css │ │ │ ├── DataCard.tsx │ │ │ └── index.tsx │ │ ├── DataTable │ │ │ ├── ColumnsMenu │ │ │ │ ├── ColumnsMenu.tsx │ │ │ │ └── index.tsx │ │ │ ├── DataTable.css │ │ │ ├── DataTable.tsx │ │ │ └── index.tsx │ │ ├── DownloadButton │ │ │ ├── DownloadButton.tsx │ │ │ └── index.tsx │ │ ├── DownloadDropdown │ │ │ ├── DownloadDropdown.tsx │ │ │ └── index.tsx │ │ ├── Drawer │ │ │ ├── Drawer.css │ │ │ ├── Drawer.tsx │ │ │ ├── DrawerContextProvider.tsx │ │ │ ├── DrawerTrigger.tsx │ │ │ └── index.tsx │ │ ├── Enlargeable │ │ │ ├── Enlargeable.css │ │ │ ├── Enlargeable.tsx │ │ │ └── index.tsx │ │ ├── Formula │ │ │ ├── Formula.test.tsx │ │ │ ├── Formula.tsx │ │ │ └── index.tsx │ │ ├── JsonView │ │ │ ├── JsonView.tsx │ │ │ └── index.tsx │ │ ├── Markdown │ │ │ ├── Markdown.tsx │ │ │ └── index.tsx │ │ ├── Modal │ │ │ ├── Modal.css │ │ │ ├── Modal.tsx │ │ │ ├── ModalCloseButton │ │ │ │ ├── ModalCloseButton.css │ │ │ │ ├── ModalCloseButton.tsx │ │ │ │ └── index.tsx │ │ │ ├── ModalContextProvider.tsx │ │ │ ├── ModalTrigger.tsx │ │ │ └── index.tsx │ │ ├── Paginator │ │ │ ├── Paginator.css │ │ │ ├── Paginator.tsx │ │ │ └── index.tsx │ │ ├── SearchUI │ │ │ ├── SearchUI.test.tsx │ │ │ ├── SearchUIContainer │ │ │ │ ├── MatscholarSearchUIContainer.tsx │ │ │ │ ├── SearchUIContainer.css │ │ │ │ ├── SearchUIContainer.tsx │ │ │ │ └── index.tsx │ │ │ ├── SearchUIContextProvider │ │ │ │ ├── MatscholarSearchUIContextProvider.tsx │ │ │ │ ├── SearchUIContextProvider.tsx │ │ │ │ └── index.tsx │ │ │ ├── SearchUIDataCards │ │ │ │ ├── SearchUIDataCards.tsx │ │ │ │ └── index.tsx │ │ │ ├── SearchUIDataHeader │ │ │ │ ├── SearchUIDataHeader.tsx │ │ │ │ └── index.tsx │ │ │ ├── SearchUIDataTable │ │ │ │ ├── SearchUIDataTable.tsx │ │ │ │ └── index.tsx │ │ │ ├── SearchUIDataView │ │ │ │ ├── SearchUIDataView.tsx │ │ │ │ └── index.tsx │ │ │ ├── SearchUIFilters │ │ │ │ ├── SearchUIFilters.tsx │ │ │ │ └── index.tsx │ │ │ ├── SearchUIGrid │ │ │ │ ├── SearchUIGrid.tsx │ │ │ │ └── index.tsx │ │ │ ├── SearchUISearchBar │ │ │ │ ├── SearchUISearchBar.tsx │ │ │ │ └── index.tsx │ │ │ ├── SearchUISynthesisRecipeCards │ │ │ │ ├── SearchUISynthesisRecipeCards.tsx │ │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ ├── types.tsx │ │ │ └── utils.tsx │ │ ├── SortDropdown │ │ │ ├── SortDropdown.tsx │ │ │ └── index.tsx │ │ ├── SynthesisRecipeCard │ │ │ ├── SynthesisRecipeCard.css │ │ │ ├── SynthesisRecipeCard.tsx │ │ │ └── index.tsx │ │ └── Tooltip │ │ │ ├── Tooltip.css │ │ │ ├── Tooltip.tsx │ │ │ └── index.tsx │ ├── data-entry │ │ ├── CheckboxList │ │ │ ├── CheckboxList.tsx │ │ │ └── index.tsx │ │ ├── DualRangeSlider │ │ │ ├── DualRangeSlider.test.tsx │ │ │ ├── DualRangeSlider.tsx │ │ │ └── index.tsx │ │ ├── FilterField │ │ │ ├── FilterField.css │ │ │ ├── FilterField.tsx │ │ │ └── index.tsx │ │ ├── GlobalSearchBar │ │ │ ├── GlobalSearchBar.tsx │ │ │ └── index.tsx │ │ ├── MaterialsInput │ │ │ ├── FormulaAutocomplete │ │ │ │ ├── FormulaAutocomplete.tsx │ │ │ │ └── index.tsx │ │ │ ├── InputHelp │ │ │ │ ├── InputHelp.tsx │ │ │ │ └── index.tsx │ │ │ ├── MaterialsInput.css │ │ │ ├── MaterialsInput.test.tsx │ │ │ ├── MaterialsInput.tsx │ │ │ ├── MaterialsInputBox │ │ │ │ ├── MaterialsInputBox.tsx │ │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ └── utils.tsx │ │ ├── RangeSlider │ │ │ ├── RangeSlider.css │ │ │ ├── RangeSlider.tsx │ │ │ └── index.tsx │ │ ├── Select │ │ │ ├── Select.css │ │ │ ├── Select.tsx │ │ │ └── index.tsx │ │ ├── Switch │ │ │ ├── Switch.css │ │ │ ├── Switch.tsx │ │ │ └── index.tsx │ │ ├── TextInput │ │ │ ├── TextInput.tsx │ │ │ └── index.tsx │ │ ├── ThreeStateBooleanSelect │ │ │ ├── ThreeStateBooleanSelect.tsx │ │ │ └── index.tsx │ │ └── utils.tsx │ ├── navigation │ │ ├── Dropdown │ │ │ ├── Dropdown.tsx │ │ │ └── index.tsx │ │ ├── Link │ │ │ ├── Link.tsx │ │ │ └── index.tsx │ │ ├── Navbar │ │ │ ├── Navbar.css │ │ │ ├── Navbar.tsx │ │ │ └── index.tsx │ │ ├── NavbarDropdown │ │ │ ├── NavbarDropdown.tsx │ │ │ └── index.tsx │ │ ├── NotificationDropdown │ │ │ ├── Bell.tsx │ │ │ ├── NotificationDropdown.tsx │ │ │ └── index.tsx │ │ ├── Scrollspy │ │ │ ├── Scrollspy.test.tsx │ │ │ ├── Scrollspy.tsx │ │ │ └── index.tsx │ │ ├── Sidebar │ │ │ ├── Sidebar.less │ │ │ ├── Sidebar.tsx │ │ │ └── index.tsx │ │ └── Tabs │ │ │ ├── Tabs.tsx │ │ │ └── index.tsx │ ├── periodic-table │ │ ├── PeriodicTable │ │ │ └── PeriodicTableSpacer │ │ │ │ ├── PeriodicTableSpacer.tsx │ │ │ │ └── index.tsx │ │ ├── PeriodicTableFormulaButtons │ │ │ ├── PeriodicTableFormulaButtons.tsx │ │ │ └── index.tsx │ │ ├── PeriodicTableModeSwitcher │ │ │ ├── PeriodicTableModeSwitcher.css │ │ │ ├── PeriodicTableModeSwitcher.tsx │ │ │ └── index.tsx │ │ ├── PeriodicTablePluginWrapper │ │ │ ├── PeriodicTablePluginWrapper.tsx │ │ │ └── index.tsx │ │ ├── periodic-element │ │ │ ├── periodic-element.component.tsx │ │ │ ├── periodic-element.detailed.less │ │ │ ├── periodic-element.module.less │ │ │ ├── periodic-element.spec.tsx │ │ │ ├── standalone-periodic-component.spec.tsx │ │ │ ├── standalone-periodic-component.tsx │ │ │ └── standalone-periodic-element.less │ │ ├── periodic-filter │ │ │ ├── filter-definitions.ts │ │ │ ├── table-filter-with-context.spec.tsx │ │ │ ├── table-filter.less │ │ │ ├── table-filter.tsx │ │ │ └── table-fiter.spec.tsx │ │ ├── periodic-table-component │ │ │ ├── periodic-table-heatmap.spec.ts │ │ │ ├── periodic-table.component.tsx │ │ │ ├── periodic-table.module.less │ │ │ └── periodic-table.spec.tsx │ │ ├── periodic-table-data │ │ │ ├── table-v2.ts │ │ │ ├── table.spec.ts │ │ │ └── table.ts │ │ ├── periodic-table-state │ │ │ ├── periodic-selection-context.tsx │ │ │ ├── table-store.test.ts │ │ │ └── table-store.ts │ │ ├── table-state.spec.tsx │ │ ├── table-state.tsx │ │ └── table-variable.less │ └── publications │ │ ├── BibCard │ │ ├── BibCard.css │ │ ├── BibCard.test.tsx │ │ ├── BibCard.tsx │ │ └── index.tsx │ │ ├── BibFilter │ │ ├── BibFilter.css │ │ ├── BibFilter.test.tsx │ │ ├── BibFilter.tsx │ │ └── index.tsx │ │ ├── BibjsonCard │ │ ├── BibjsonCard.tsx │ │ └── index.tsx │ │ ├── BibtexButton │ │ ├── BibtexButton.tsx │ │ └── index.tsx │ │ ├── CrossrefCard │ │ ├── CrossrefCard.tsx │ │ └── index.tsx │ │ ├── OpenAccessButton │ │ ├── OpenAccessButton.css │ │ ├── OpenAccessButton.tsx │ │ ├── index.tsx │ │ └── oab_color.png │ │ └── PublicationButton │ │ ├── PublicationButton.css │ │ ├── PublicationButton.tsx │ │ └── index.tsx ├── constants │ ├── pointGroups.ts │ └── spaceGroups.ts ├── declarations.d.ts ├── index.html ├── index.ts ├── jest-setup.ts ├── mocks │ ├── constants │ │ ├── autocomplete.ts │ │ ├── materialsById.ts │ │ ├── materialsByStability.ts │ │ ├── materialsByVolume.ts │ │ ├── materialsColumns.json │ │ ├── materialsFilterGroups.json │ │ ├── materialsUnfiltered.ts │ │ └── mp-papers.json │ ├── fileMock.js │ ├── handlers.ts │ └── server.ts ├── pages │ ├── BatteryExplorer │ │ ├── BatteryExplorer.tsx │ │ ├── columns.json │ │ ├── filterGroups.json │ │ └── index.tsx │ ├── CatalystExplorer │ │ ├── CatalystExplorer.tsx │ │ ├── columns.json │ │ ├── filterGroups.json │ │ └── index.tsx │ ├── CrystalStructureViewer │ │ ├── CrystalStructureViewer.tsx │ │ └── index.tsx │ ├── MPContribsSearch │ │ ├── MPContribsSearch.tsx │ │ ├── columns.json │ │ ├── filterGroups.json │ │ └── index.tsx │ ├── MaterialsDetail │ │ ├── MaterialsDetail.tsx │ │ └── index.tsx │ ├── MaterialsExplorer │ │ ├── MaterialsExplorer.tsx │ │ ├── columns.json │ │ ├── filterGroups.json │ │ └── index.tsx │ ├── MatscholarMaterialsExplorer │ │ ├── MatscholarMaterialsExplorer.tsx │ │ ├── filterGroups.json │ │ └── index.tsx │ ├── MofExplorer │ │ ├── MofExplorer.tsx │ │ ├── columns.json │ │ ├── filterGroups.json │ │ └── index.tsx │ ├── MoleculesExplorer │ │ ├── MoleculesExplorer.tsx │ │ ├── columns.json │ │ ├── filterGroups.json │ │ └── index.tsx │ ├── Publications │ │ ├── Publications.tsx │ │ └── index.tsx │ ├── Sandbox │ │ ├── Sandbox.tsx │ │ ├── crossref.json │ │ └── index.tsx │ ├── SynthesisExplorer │ │ ├── SynthesisExplorer.tsx │ │ ├── columns.json │ │ ├── filterGroups.json │ │ └── index.tsx │ └── XasApp │ │ ├── XasApp.tsx │ │ ├── columns.json │ │ ├── filterGroups.json │ │ └── index.tsx ├── scripts │ ├── extract-element.sh │ ├── script.py │ └── table_final.json ├── stories │ ├── constants.ts │ ├── constants │ │ ├── columns.json │ │ ├── filterGroups.json │ │ ├── materialsRecords.json │ │ ├── matscholarFilterGroups.json │ │ ├── mofColumns.json │ │ └── mofFilterGroups.json │ ├── crystal-toolkit │ │ ├── CrystalToolkitScene.stories.tsx │ │ └── ReactGraphComponent.stories.tsx │ ├── data-display │ │ ├── DataBlock.stories.tsx │ │ ├── DataTable.stories.tsx │ │ ├── Drawer.stories.tsx │ │ ├── Formula.stories.tsx │ │ ├── JsonView.stories.tsx │ │ ├── Markdown.stories.tsx │ │ ├── Modal.stories.tsx │ │ └── Tooltip.stories.tsx │ ├── data-entry │ │ ├── DualRangeSlider.stories.tsx │ │ ├── MaterialsInput.stories.tsx │ │ ├── PeriodicTable.stories.tsx │ │ ├── RangeSlider.stories.tsx │ │ ├── Select.stories.tsx │ │ ├── Switch.stories.tsx │ │ └── ThreeStateBooleanSelect.stories.tsx │ ├── introduction │ │ ├── DashUsage.stories.mdx │ │ └── Intro.stories.mdx │ ├── navigation │ │ ├── Dropdown.stories.tsx │ │ ├── Link.stories.tsx │ │ ├── Navbar.stories.tsx │ │ ├── Scrollspy.stories.tsx │ │ └── Tabs.stories.tsx │ ├── publications │ │ ├── BibCard.stories.tsx │ │ ├── BibFilter.stories.tsx │ │ ├── BibjsonCard.stories.tsx │ │ ├── BibtexButton.stories.tsx │ │ ├── CrossrefCard.stories.tsx │ │ ├── OpenAccessButton.stories.tsx │ │ └── PublicationButton.stories.tsx │ ├── search │ │ ├── BuildingSearchUI.stories.mdx │ │ ├── Columns.stories.mdx │ │ ├── ConditionalRowStyles.stories.mdx │ │ ├── FilterGroups.stories.mdx │ │ ├── SearchBarInputTypes.stories.mdx │ │ ├── SearchUI.stories.tsx │ │ ├── SearchUIDataHeader.stories.tsx │ │ ├── SearchUIDataTable.stories.tsx │ │ ├── SearchUIDataView.stories.tsx │ │ ├── SearchUIFilters.stories.tsx │ │ ├── SearchUIGrid.stories.tsx │ │ └── SearchUISearchBar.stories.tsx │ ├── stories.css │ └── type-components.tsx ├── styles.less └── utils │ ├── hooks.ts │ ├── matgen.ts │ ├── navigation.tsx │ ├── publications.ts │ ├── table.tsx │ └── utils.ts └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"], 3 | "plugins": [ 4 | "@babel/transform-runtime" 5 | ] 6 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | DEBUG=true 2 | REACT_APP_BASE_URL="http://localhost:8000" 3 | REACT_APP_AUTOCOMPLETE_URL="http://localhost:8000/materials/formula_autocomplete/" 4 | REACT_APP_API_KEY=$MP_API_KEY -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | DEBUG=true 2 | REACT_APP_BASE_URL="https://api.materialsproject.org/" 3 | REACT_APP_AUTOCOMPLETE_URL="https://api.materialsproject.org/materials/formula_autocomplete/" 4 | REACT_APP_API_KEY=$MP_API_KEY -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: '/' 5 | schedule: 6 | interval: weekly 7 | time: '08:00' 8 | timezone: US/Pacific 9 | open-pull-requests-limit: 10 10 | -------------------------------------------------------------------------------- /.github/workflows/publish-npm-manual.yml: -------------------------------------------------------------------------------- 1 | name: publish-npm-manual 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | logLevel: 6 | description: 'Log level' 7 | required: true 8 | default: 'warning' 9 | type: choice 10 | options: 11 | - info 12 | - warning 13 | - debug 14 | tags: 15 | description: 'manual publish' 16 | required: false 17 | type: boolean 18 | jobs: 19 | publish: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v2 24 | with: 25 | ref: main 26 | - name: Set up Python 3.7 27 | uses: actions/setup-python@v1 28 | with: 29 | python-version: 3.7 30 | - uses: actions/setup-node@v1 31 | env: 32 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 33 | with: 34 | node-version: '12.x' 35 | registry-url: https://registry.npmjs.org/ 36 | scope: '@mat-github-ci' 37 | - name: Install dependencies 38 | run: | 39 | npm i 40 | - name: Build project 41 | run: | 42 | npm run build-publish 43 | - name: Publish to NPM 44 | run: | 45 | echo Publishing to test repo $NODE_AUTH_TOKEN 46 | npm publish 47 | env: 48 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 49 | -------------------------------------------------------------------------------- /.github/workflows/publish-npm.yml: -------------------------------------------------------------------------------- 1 | name: publish-npm 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v2 12 | with: 13 | ref: main 14 | - name: Set up Python 3.7 15 | uses: actions/setup-python@v1 16 | with: 17 | python-version: 3.7 18 | - uses: actions/setup-node@v1 19 | env: 20 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 21 | with: 22 | node-version: '12.x' 23 | registry-url: https://registry.npmjs.org/ 24 | scope: '@mat-github-ci' 25 | - name: Install dependencies 26 | run: | 27 | npm i 28 | - name: Build project 29 | run: | 30 | npm run build-publish 31 | - name: Publish to NPM 32 | run: | 33 | echo Publishing to test repo $NODE_AUTH_TOKEN 34 | npm publish 35 | env: 36 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .idea 3 | 4 | .cache/ 5 | coverage/ 6 | dist*/ 7 | !dist/index.html 8 | node_modules/ 9 | *.log 10 | 11 | # OS generated files 12 | .DS_Store 13 | .DS_Store? 14 | ._* 15 | .Spotlight-V100 16 | .Trashes 17 | ehthumbs.db 18 | Thumbs.db 19 | 20 | # Storybook files 21 | out* 22 | storybook-static 23 | 24 | # Rollup artifacts 25 | rollup.build.* 26 | index.js 27 | index.js.map 28 | index.less 29 | index.css 30 | index.css.map 31 | **/wip-components/** 32 | 33 | # Unused files used only for reference 34 | archive/ 35 | 36 | # VSCode configs 37 | .vscode/ 38 | 39 | # Assets used for local development only 40 | src/assets/fonts 41 | src/assets/styles.css 42 | src/assets/fonts.css -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | #TODO(chab) whitelist instead, in files array 2 | 3 | .idea 4 | .storybook 5 | .cache/ 6 | .github 7 | .cache 8 | coverage/ 9 | node_modules/ 10 | *.log 11 | 12 | # OS generated files 13 | .DS_Store 14 | .DS_Store? 15 | ._* 16 | .Spotlight-V100 17 | .Trashes 18 | ehthumbs.db 19 | Thumbs.db 20 | 21 | # Storybook files 22 | out* 23 | storybook-static 24 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "semi": true, 7 | "bracketSpacing": true, 8 | "trailingComma": "none" 9 | } 10 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], 3 | addons: [ 4 | '@storybook/addon-links', 5 | '@storybook/addon-essentials', 6 | '@storybook/addon-interactions' 7 | ], 8 | framework: '@storybook/react', 9 | core: { 10 | builder: '@storybook/builder-webpack5' 11 | }, 12 | webpackFinal: async (config) => { 13 | config.module.rules.push({ 14 | test: /\.less$/, 15 | use: [ 16 | 'style-loader', 17 | { 18 | loader: 'css-loader', 19 | options: { 20 | modules: false 21 | } 22 | }, 23 | { 24 | loader: 'less-loader', 25 | options: { 26 | lessOptions: { 27 | javascriptEnabled: true 28 | } 29 | } 30 | } 31 | ] 32 | }); 33 | return config; 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import '../node_modules/bulma/css/bulma.min.css'; 2 | import '../src/styles.less'; 3 | import '../src/assets/fonts.css'; 4 | import '../src//stories/stories.css'; 5 | 6 | export const parameters = { 7 | controls: { expanded: true, sort: 'alpha' }, 8 | options: { 9 | storySort: { 10 | order: [ 11 | 'Introduction', 12 | ['MP React Components', 'Usage with Dash'], 13 | 'Search UI', 14 | [ 15 | 'Building a Search UI', 16 | 'SearchUIContainer', 17 | ['Fully Featured', 'With MP Contribs Data', 'Matscholar Alpha'], 18 | 'SearchUISearchBar', 19 | 'SearchUIFilters', 20 | 'SearchUIDataHeader', 21 | 'SearchUIDataTable', 22 | 'SearchUIDataView', 23 | 'SearchUIGrid', 24 | 'Columns', 25 | 'Filters', 26 | 'Conditional Row Styles', 27 | 'Search Bar Input Types' 28 | ], 29 | 'Data-Entry', 30 | [ 31 | 'MaterialsInput', 32 | 'PeriodicTable', 33 | 'RangeSlider', 34 | 'DualRangeSlider', 35 | 'Select', 36 | 'ThreeStateBooleanSelect', 37 | 'Switch' 38 | ], 39 | 'Data-Display', 40 | 'Publications', 41 | ['BibCard', 'CrossrefCard', 'BibjsonCard', 'PublicationCard', 'BibtexCard', 'BibFilter'], 42 | 'Crystal Toolkit', 43 | ['CrystalToolkitScene', 'ReactGraphComponent'], 44 | 'Navigation' 45 | ] 46 | } 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.14.1 2 | # Setup the working directory 3 | RUN mkdir /srv/github-actions-app 4 | WORKDIR /srv/github-actions-app 5 | # Send over the dependency definitions to the container 6 | COPY package.json package-lock.json ./ 7 | RUN npm i 8 | # Copy the whitelisted files 9 | COPY . . 10 | 11 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { presets: ['@babel/preset-env'] }; 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({ path: './.env.test' }); 2 | 3 | module.exports = { 4 | preset: 'ts-jest/presets/js-with-ts', 5 | testEnvironment: 'jsdom', 6 | collectCoverage: true, 7 | /** 8 | * Tell jest to ignore transforming most node_modules 9 | * except for the ones that match the module strings listed here. 10 | * This is necessary for node modules that distribute their source code as uncompiled JS. 11 | */ 12 | transformIgnorePatterns: [ 13 | 'node_modules/(?!(three|unist-.*|hast-.*|rehype-slug|remark-rehype|react-markdown|vfile.*|unified|bail|is-plain-obj|trough|remark-parse|mdast-.*|micromark.*|decode-named-character-reference|unist-.*|character-entities|property-information|space-separated-tokens|comma-separated-tokens)/)' 14 | ], 15 | modulePaths: [''], 16 | moduleDirectories: ['node_modules', ''], 17 | moduleNameMapper: { 18 | '\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 19 | '/src/mocks/fileMock.js', 20 | '\\.(css|less)$': '/src/mocks/fileMock.js' 21 | }, 22 | coverageReporters: ['json', 'html'], 23 | setupFilesAfterEnv: ['./src/jest-setup.ts'], 24 | globals: { 25 | 'ts-jest': { 26 | tsConfig: './tsconfig.json', 27 | isolatedModules: true 28 | } 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | import pkg from './package.json'; 3 | import styles from 'rollup-plugin-styles'; 4 | //import urlPlugin from '@rollup/plugin-url'; we use image instead 5 | import resolve from 'rollup-plugin-node-resolve'; 6 | import image from '@rollup/plugin-image'; 7 | import localResolve from 'rollup-plugin-local-resolve'; 8 | import replace from 'rollup-plugin-replace'; 9 | // import { terser } from 'rollup-plugin-terser'; 10 | 11 | export default { 12 | input: 'src/index.ts', 13 | output: [ 14 | { 15 | file: pkg.main, 16 | format: 'cjs', 17 | sourcemap: true 18 | }, 19 | { 20 | file: pkg.module, 21 | format: 'es', 22 | sourcemap: true 23 | } 24 | ], 25 | external: (p) => { 26 | if ( 27 | [ 28 | ...Object.keys(pkg.dependencies || {}), 29 | ...Object.keys(pkg.peerDependencies || {}), 30 | 'prop-types' 31 | ].indexOf(p) > -1 32 | ) { 33 | return true; 34 | } 35 | // prevent duplicate import of three 36 | // prevent packages that have css separately bundled to fail 37 | return /^three/.test(p) || /^@trendmicro/.test(p) || /^react-toastify/.test(p); 38 | }, 39 | plugins: [ 40 | styles(), 41 | image(), 42 | localResolve(), 43 | resolve(), 44 | typescript({ 45 | tsconfigDefaults: {}, 46 | tsconfig: 'tsconfig.json', 47 | tsconfigOverride: {}, 48 | sourceMap: false, 49 | verbosity: 1 // overrides for debugging 50 | }), 51 | replace({ 52 | 'process.env.NODE_ENV': JSON.stringify('production') 53 | }) 54 | //terser() // TODO(chab) we might want to not mimify it 55 | ] 56 | }; 57 | -------------------------------------------------------------------------------- /src/assets/images/periodictable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mp-react-components/73f1a1b38ccb4021ece5dcf38d472e95d96521dc/src/assets/images/periodictable.png -------------------------------------------------------------------------------- /src/assets/img/network/acceptDeleteIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mp-react-components/73f1a1b38ccb4021ece5dcf38d472e95d96521dc/src/assets/img/network/acceptDeleteIcon.png -------------------------------------------------------------------------------- /src/assets/img/network/addNodeIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mp-react-components/73f1a1b38ccb4021ece5dcf38d472e95d96521dc/src/assets/img/network/addNodeIcon.png -------------------------------------------------------------------------------- /src/assets/img/network/backIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mp-react-components/73f1a1b38ccb4021ece5dcf38d472e95d96521dc/src/assets/img/network/backIcon.png -------------------------------------------------------------------------------- /src/assets/img/network/connectIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mp-react-components/73f1a1b38ccb4021ece5dcf38d472e95d96521dc/src/assets/img/network/connectIcon.png -------------------------------------------------------------------------------- /src/assets/img/network/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mp-react-components/73f1a1b38ccb4021ece5dcf38d472e95d96521dc/src/assets/img/network/cross.png -------------------------------------------------------------------------------- /src/assets/img/network/cross2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mp-react-components/73f1a1b38ccb4021ece5dcf38d472e95d96521dc/src/assets/img/network/cross2.png -------------------------------------------------------------------------------- /src/assets/img/network/deleteIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mp-react-components/73f1a1b38ccb4021ece5dcf38d472e95d96521dc/src/assets/img/network/deleteIcon.png -------------------------------------------------------------------------------- /src/assets/img/network/downArrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mp-react-components/73f1a1b38ccb4021ece5dcf38d472e95d96521dc/src/assets/img/network/downArrow.png -------------------------------------------------------------------------------- /src/assets/img/network/editIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mp-react-components/73f1a1b38ccb4021ece5dcf38d472e95d96521dc/src/assets/img/network/editIcon.png -------------------------------------------------------------------------------- /src/assets/img/network/leftArrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mp-react-components/73f1a1b38ccb4021ece5dcf38d472e95d96521dc/src/assets/img/network/leftArrow.png -------------------------------------------------------------------------------- /src/assets/img/network/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mp-react-components/73f1a1b38ccb4021ece5dcf38d472e95d96521dc/src/assets/img/network/minus.png -------------------------------------------------------------------------------- /src/assets/img/network/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mp-react-components/73f1a1b38ccb4021ece5dcf38d472e95d96521dc/src/assets/img/network/plus.png -------------------------------------------------------------------------------- /src/assets/img/network/rightArrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mp-react-components/73f1a1b38ccb4021ece5dcf38d472e95d96521dc/src/assets/img/network/rightArrow.png -------------------------------------------------------------------------------- /src/assets/img/network/upArrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mp-react-components/73f1a1b38ccb4021ece5dcf38d472e95d96521dc/src/assets/img/network/upArrow.png -------------------------------------------------------------------------------- /src/assets/img/network/zoomExtends.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mp-react-components/73f1a1b38ccb4021ece5dcf38d472e95d96521dc/src/assets/img/network/zoomExtends.png -------------------------------------------------------------------------------- /src/components/crystal-toolkit/CameraContextProvider/CameraContextProvider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Dispatch, useMemo, useReducer } from 'react'; 3 | import { 4 | CameraActionPayload, 5 | cameraReducer, 6 | CameraReducerAction, 7 | CameraState, 8 | initialState, 9 | } from './camera-reducer'; 10 | import { Action } from '../utils'; 11 | 12 | export interface ICameraContext { 13 | state: CameraState | null; 14 | dispatch: Dispatch> | null; 15 | } 16 | 17 | export const CameraContext = React.createContext(null); 18 | 19 | /** 20 | * 21 | * Use CameraContextProvider to coordinate multiple 3D Scene 22 | * 23 | */ 24 | export function CameraContextProvider(props: any) { 25 | // type of dispatch is React.Dispatch> 26 | const [state, dispatch] = useReducer(cameraReducer, initialState); 27 | const store = useMemo(() => ({ state, dispatch }), [state, dispatch]); 28 | return {{ ...props.children }}; 29 | } 30 | -------------------------------------------------------------------------------- /src/components/crystal-toolkit/CameraContextProvider/camera-reducer.ts: -------------------------------------------------------------------------------- 1 | import { Quaternion, Vector3 } from 'three'; 2 | import { Action } from '../utils'; 3 | 4 | export interface CameraState { 5 | quaternion?: Quaternion; 6 | position?: Vector3; 7 | zoom?: number; 8 | /** 9 | * The id of the scene component that most recently set 10 | * the camera state values. 11 | * e.g. "1" 12 | */ 13 | setByComponentId?: string; 14 | /** 15 | * whether to follow the camera 16 | * (what does this mean?) 17 | */ 18 | following?: boolean; 19 | } 20 | 21 | export interface CameraActionPayload { 22 | quaternion?: Quaternion; 23 | position?: Vector3; 24 | zoom?: number; 25 | /** 26 | * The id of the component that is initiating 27 | * the camera change. Sets the state value for setByComponentId. 28 | * e.g. "1" 29 | */ 30 | componentId?: string; 31 | following?: boolean; 32 | } 33 | 34 | export enum CameraReducerAction { 35 | NEW_POSITION = 'follow_camera', 36 | STOP_FOLLOWING = 'stop_following', 37 | START_FOLLOWING = 'start_following', 38 | } 39 | 40 | export const initialState = { 41 | following: true, 42 | }; 43 | 44 | export function cameraReducer( 45 | state: CameraState, 46 | { type, payload }: Action 47 | ): CameraState { 48 | // we expect the new position/orientation, and the ID of the component 49 | // (to avoid resetting the position ) 50 | switch (type) { 51 | case CameraReducerAction.NEW_POSITION: 52 | return { 53 | quaternion: payload.quaternion!.clone(), 54 | position: payload.position!.clone(), 55 | zoom: payload.zoom, 56 | setByComponentId: payload.componentId, 57 | following: state.following, 58 | }; 59 | case CameraReducerAction.STOP_FOLLOWING: 60 | return { ...state, following: false }; 61 | case CameraReducerAction.START_FOLLOWING: 62 | return { ...state, following: true }; 63 | default: 64 | console.error('Unknown action, return current state. Action', type, payload); 65 | } 66 | return state; 67 | } 68 | -------------------------------------------------------------------------------- /src/components/crystal-toolkit/CameraContextProvider/index.tsx: -------------------------------------------------------------------------------- 1 | export { CameraContextProvider, CameraContext, ICameraContext } from './CameraContextProvider'; 2 | -------------------------------------------------------------------------------- /src/components/crystal-toolkit/CrystalToolkitScene/CrystalToolkitScene.less: -------------------------------------------------------------------------------- 1 | .mpc-scene { 2 | position: relative; 3 | } 4 | 5 | .tooltiptext { 6 | background-color: black; 7 | color: #fff; 8 | text-align: center; 9 | border-radius: 6px; 10 | padding: 5px; 11 | } 12 | 13 | .show-pointer { 14 | cursor: pointer !important; 15 | } 16 | 17 | .mpc-scene-square { 18 | position: relative; 19 | } 20 | 21 | .mpc-scene-square:before { 22 | content: ''; 23 | display: block; 24 | height: 0; 25 | width: 0; 26 | float: left; 27 | padding-bottom: 100%; 28 | } 29 | 30 | .mpc-scene-square::after { 31 | clear: left; 32 | content: ' '; 33 | display: table; 34 | } 35 | 36 | /** 37 | * Prevent the structure layout from growing beyond the height of the modal. 38 | * Setting max-width with vh instead of simply setting max-height so we don't interfere 39 | * with the structure's styles that force it to render with 1:1 ratio. 40 | */ 41 | .mpc-scene-square-wrapper { 42 | width: 100% !important; 43 | max-width: calc(95vh - 3rem); 44 | height: auto !important; 45 | margin: auto; 46 | } 47 | 48 | .mpc-scene .mpc-button-bar { 49 | position: absolute; 50 | right: 0; 51 | z-index: 3; 52 | } 53 | 54 | .mpc-scene-settings-panel { 55 | position: absolute; 56 | right: 4.5rem; 57 | max-height: 100%; 58 | max-width: 450px; 59 | overflow: auto; 60 | border: 1px solid #ccc; 61 | border-radius: 6px; 62 | padding: 1.25rem; 63 | background-color: #fff; 64 | opacity: 0.8; 65 | z-index: 2; 66 | } 67 | 68 | .mpc-scene-bottom-panel { 69 | position: absolute; 70 | right: 0; 71 | bottom: 0; 72 | z-index: 1; 73 | } 74 | 75 | /** 76 | * Include this explicitly so that toggling the settings panel works without bulma 77 | */ 78 | .mpc-scene-settings-panel.is-hidden { 79 | display: none; 80 | } 81 | 82 | /** 83 | * Include this explicitly so that file dropdown looks okay without bulma 84 | */ 85 | .mpc-scene .dropdown-content { 86 | position: absolute; 87 | right: 0; 88 | min-width: 100px; 89 | } 90 | -------------------------------------------------------------------------------- /src/components/crystal-toolkit/CrystalToolkitScene/CrystalToolkitScene.test.tsx: -------------------------------------------------------------------------------- 1 | import { mount } from 'enzyme'; 2 | import * as React from 'react'; 3 | import { CrystalToolkitScene } from './CrystalToolkitScene'; 4 | import { s2 as scene } from '../scene/simple-scene'; 5 | import { MOUNT_NODE_CLASS, Renderer } from '../scene/constants'; 6 | import Scene from '../scene/Scene'; 7 | 8 | const spy = jest.spyOn(Scene.prototype, 'renderScene'); 9 | const RENDERSCENE_CALLS_BY_REACT_RENDERING = 1; // goal is to reach 1 and stay there :) 10 | 11 | // When we run test, three.js is bundled differently, and we encounter again the bug 12 | // where we have 2 different instances of three 13 | describe('', () => { 14 | it('should be rendered', () => { 15 | const wrapper = renderElement(); 16 | expect(wrapper.find(`.${MOUNT_NODE_CLASS}`).length).toBe(1); 17 | expect(wrapper.find(`.mpc-scene-square`).length).toBe(1); 18 | 19 | // Note(chab) we call renderScene when we mount, due to the react effect 20 | // those are the three call sites (constructor / toggleVis / inlet ) 21 | expect(spy).toBeCalledTimes(1 * RENDERSCENE_CALLS_BY_REACT_RENDERING); 22 | 23 | // fails because SVGRender will import a different instance of Three 24 | // expect(wrapper.find('path').length).toBe(6); 25 | }); 26 | 27 | it('should re-render if we change the size of the screen', () => { 28 | const wrapper = renderElement(); 29 | wrapper.setProps({ size: 400 }); 30 | expect(spy).toBeCalledTimes(2 * RENDERSCENE_CALLS_BY_REACT_RENDERING); 31 | }); 32 | }); 33 | 34 | function renderElement() { 35 | // we use mount to test the rendering of the underlying elements 36 | return mount( 37 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/components/crystal-toolkit/CrystalToolkitScene/index.tsx: -------------------------------------------------------------------------------- 1 | export { CrystalToolkitScene } from './CrystalToolkitScene'; 2 | -------------------------------------------------------------------------------- /src/components/crystal-toolkit/Download/Download.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { toByteArray } from 'base64-js'; 3 | 4 | /** 5 | * The Download component opens a download dialog when the data property (dict of filename, content, and type) changes. 6 | */ 7 | 8 | interface DataInput { 9 | filename: string; 10 | content: any; 11 | isBase64?: boolean; 12 | isDataURL?: boolean; 13 | mimeType?: string; 14 | } 15 | 16 | interface Props { 17 | /** 18 | * The ID used to identify this component in Dash callbacks. 19 | */ 20 | id: string; 21 | 22 | /** 23 | * When set, a download is invoked using a Blob. 24 | */ 25 | data?: DataInput; 26 | 27 | /** 28 | * Set to true if data.content is a base64 string 29 | */ 30 | isBase64?: boolean; 31 | 32 | /** 33 | * Set to true if data.content is a data url 34 | */ 35 | isDataURL?: boolean; 36 | 37 | /** 38 | * Default value for mimeType. 39 | */ 40 | mimeType?: string; 41 | 42 | /** 43 | * Dash-assigned callback that should be called to report property changes 44 | * to Dash, to make them available for callbacks. 45 | */ 46 | setProps?: (value: any) => any; 47 | } 48 | 49 | export const Download: React.FC = ({ 50 | mimeType = 'text/plain', 51 | isBase64 = false, 52 | ...otherProps 53 | }) => { 54 | const props = { mimeType, isBase64, ...otherProps }; 55 | useEffect(() => { 56 | if (props.data) { 57 | const mimeType = props.data.mimeType ? props.data.mimeType : props.mimeType; 58 | const isDataURL = props.data.isDataURL ? props.data.isDataURL : props.isDataURL; 59 | const isBase64 = props.data.isBase64 ? props.data.isBase64 : props.isBase64; 60 | let content = props.data.content; 61 | if (isDataURL) content = toByteArray(props.data.content.split(',')[1]); 62 | if (isBase64 && !isDataURL) content = toByteArray(props.data.content); 63 | // Construct the blob. 64 | const blob = new Blob([content], { type: mimeType }); 65 | const filename = props.data.filename; 66 | const a = document.createElement('a'); 67 | document.body.appendChild(a); 68 | const url = window.URL.createObjectURL(blob); 69 | a.href = url; 70 | a.download = filename; 71 | a.click(); 72 | setTimeout(() => { 73 | window.URL.revokeObjectURL(url); 74 | document.body.removeChild(a); 75 | }, 0); 76 | } 77 | }, [props.data]); 78 | 79 | return null; 80 | }; 81 | -------------------------------------------------------------------------------- /src/components/crystal-toolkit/Download/index.tsx: -------------------------------------------------------------------------------- 1 | export { Download } from './Download'; 2 | -------------------------------------------------------------------------------- /src/components/crystal-toolkit/DynamicCrystalToolkitScene/DynamicCrystalToolkitScene.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import { CrystalToolkitScene } from '../CrystalToolkitScene'; 3 | import { AnimationStyle, Renderer } from '../scene/constants'; 4 | 5 | /** 6 | * Component for generating a CrystalToolkitScene dynamically from user-supplied JSON input 7 | */ 8 | export const DynamicCrystalToolkitScene: React.FC = () => { 9 | const [sceneData, setSceneData] = useState(null); 10 | const [showScene, setShowScene] = useState(false); 11 | const inputRef = useRef(null); 12 | const emptyObject = {}; 13 | 14 | function show() { 15 | if (inputRef && inputRef.current) { 16 | const cleanJsonString = inputRef.current.value.replace( 17 | /(['"])?([a-z0-9A-Z_]+)(['"])?:/g, 18 | '"$2": ' 19 | ); 20 | const cleanerJsonString = cleanJsonString.replace("'", '"'); 21 | setSceneData(JSON.parse(cleanerJsonString)); 22 | } else { 23 | setSceneData(null); 24 | } 25 | setShowScene(true); 26 | } 27 | 28 | function remove() { 29 | setSceneData(null); 30 | setShowScene(false); 31 | } 32 | 33 | return ( 34 |
35 | 36 | 37 | 38 | {showScene && sceneData ? ( 39 | 48 | ) : null} 49 |
50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /src/components/crystal-toolkit/DynamicCrystalToolkitScene/index.tsx: -------------------------------------------------------------------------------- 1 | export { DynamicCrystalToolkitScene } from './DynamicCrystalToolkitScene'; 2 | -------------------------------------------------------------------------------- /src/components/crystal-toolkit/graph.component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import PropTypes, { InferProps } from 'prop-types'; 3 | import Graph from 'react-graph-vis'; 4 | import './vis.less'; 5 | 6 | /** 7 | * Render linked data in a force-directed graph. 8 | * This was an experimental component and is not being used anywhere at the moment. 9 | */ 10 | export default function ReactGraphComponent( 11 | props: InferProps 12 | ) { 13 | const network = useRef({ edges: null, nodes: null, fit: () => {} }); 14 | 15 | //NOTE(chab) not 100% sure of the original intent :) 16 | // but this will fit the network AFTER the rendering 17 | useEffect(() => { 18 | if (props.graph && (props.graph as any).nodes && (props.graph as any).edges) { 19 | network.current.edges = (props.graph as any).edges; 20 | network.current.nodes = (props.graph as any).nodes; 21 | network.current.fit(); 22 | } 23 | }, [(props.graph! as any).nodes, (props.graph! as any).sedges]); 24 | 25 | // the API here is weird.. either we just pass the graph, and the downstream component takes care of it 26 | // either we update imperatively the graph 27 | 28 | return ( 29 | (network.current = network)} 33 | /> 34 | ); 35 | } 36 | 37 | ReactGraphComponent.propTypes = { 38 | /** 39 | * The ID used to identify this component in Dash callbacks 40 | */ 41 | id: PropTypes.string, 42 | 43 | /** 44 | * A graph that will be displayed when this component is rendered 45 | */ 46 | graph: PropTypes.object, 47 | 48 | /** 49 | * Display options for the graph 50 | */ 51 | options: PropTypes.object, 52 | 53 | /** 54 | * Dash-assigned callback that should be called whenever any of the 55 | * properties change 56 | */ 57 | setProps: PropTypes.func 58 | }; 59 | -------------------------------------------------------------------------------- /src/components/crystal-toolkit/scene/download-event.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * A very simple and naive singleton event bus. 4 | * Unclear if this is still necessary. Can likely delete this whole file. 5 | * 6 | */ 7 | 8 | import { Subject, Subscription } from 'rxjs'; 9 | import { ExportType } from './constants'; 10 | interface DownloadRequestEvent { 11 | filename: string; 12 | filetype: ExportType; 13 | } 14 | 15 | const eventBus: Subject = new Subject(); 16 | 17 | export function triggerDownloadRequest(downloadRequest: DownloadRequestEvent) { 18 | eventBus.next(downloadRequest); 19 | } 20 | 21 | export function subscribe(cb: (event: DownloadRequestEvent) => void): Subscription { 22 | return eventBus.asObservable().subscribe((event) => cb(event)); 23 | } 24 | -------------------------------------------------------------------------------- /src/components/crystal-toolkit/scene/glass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mp-react-components/73f1a1b38ccb4021ece5dcf38d472e95d96521dc/src/components/crystal-toolkit/scene/glass.png -------------------------------------------------------------------------------- /src/components/crystal-toolkit/scene/tooltip-helper.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'; 3 | import { rgb } from 'd3-color'; 4 | 5 | export class TooltipHelper { 6 | private tooltipedJsonObject: any | null = null; 7 | private tooltipedThreeObject: THREE.Object3D | null = null; 8 | public readonly tooltip; 9 | 10 | constructor() { 11 | const label = document.createElement('div'); 12 | label.className = 'tooltiptext'; 13 | const hoverLabel = document.createElement('span'); 14 | hoverLabel.className = ''; 15 | label.appendChild(hoverLabel); 16 | const labelObject = new CSS2DObject(label); 17 | this.tooltip = labelObject; 18 | this.moveOffscreen(); 19 | } 20 | 21 | public updateTooltip(point, jsonObject: any, sceneObject: THREE.Object3D) { 22 | if (!(this.tooltipedJsonObject === jsonObject)) { 23 | sceneObject.children.forEach(c => { 24 | if (c instanceof THREE.Mesh) { 25 | const color = rgb(jsonObject.color).brighter(1); 26 | (c.material as THREE.MeshStandardMaterial).color = new THREE.Color(color.formatHex()); 27 | } 28 | }); 29 | this.tooltipedJsonObject = jsonObject; 30 | this.tooltipedThreeObject = sceneObject; 31 | } 32 | this.tooltip.position.x = point.x; 33 | this.tooltip.position.y = point.y; 34 | this.tooltip.position.z = point.z; 35 | // TODO(chab) support markdown ? 36 | this.tooltip.element.textContent = jsonObject.tooltip; 37 | } 38 | 39 | /** 40 | * 41 | * Return true if the tooltip was removed 42 | */ 43 | public hideTooltipIfNeeded(): boolean { 44 | if (this.tooltipedThreeObject) { 45 | this.tooltipedThreeObject.children.forEach(c => { 46 | if (c instanceof THREE.Mesh) { 47 | (c.material as THREE.MeshStandardMaterial).color = new THREE.Color( 48 | this.tooltipedJsonObject!.color 49 | ); 50 | } 51 | }); 52 | this.tooltipedThreeObject = null; 53 | this.tooltipedJsonObject = null; 54 | this.moveOffscreen(); 55 | return true; 56 | } 57 | return false; 58 | } 59 | 60 | private moveOffscreen() { 61 | this.tooltip.translateX(Number.MAX_SAFE_INTEGER); 62 | this.tooltip.translateY(Number.MAX_SAFE_INTEGER); 63 | this.tooltip.translateZ(Number.MAX_SAFE_INTEGER); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/components/data-display/ActiveFilterButtons/ActiveFilterButtons.css: -------------------------------------------------------------------------------- 1 | .mpc-active-filter-button { 2 | display: inline-block; 3 | margin-right: 0.5rem; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/data-display/ActiveFilterButtons/ActiveFilterButtons.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React, { useState } from 'react'; 3 | import * as d3 from 'd3'; 4 | import { FaTimes, FaTimesCircle } from 'react-icons/fa'; 5 | import { Formula } from '../Formula'; 6 | import { validateFormula } from '../../data-entry/MaterialsInput/utils'; 7 | import { ActiveFilter } from '../SearchUI/types'; 8 | import { formatPointGroup } from '../../data-entry/utils'; 9 | import './ActiveFilterButtons.css'; 10 | 11 | interface Props { 12 | className?: string; 13 | filters: ActiveFilter[]; 14 | onClick: (params: string[]) => any; 15 | } 16 | 17 | const formatValue = (af: ActiveFilter) => { 18 | if ( 19 | af.hasOwnProperty('defaultValue') && 20 | Array.isArray(af.value) && 21 | af.value.length === 2 && 22 | !isNaN(af.value[0]) 23 | ) { 24 | const displayMin = d3.format(',')(af.value[0]); 25 | const displayMax = d3.format(',')(af.value[1]); 26 | if (af.defaultValue[0] !== 0 && af.value[0] === af.defaultValue[0]) { 27 | return `${displayMax} or less`; 28 | } else if (af.value[1] === af.defaultValue[1]) { 29 | return `${displayMin} or more`; 30 | } else { 31 | return `${displayMin} to ${displayMax}`; 32 | } 33 | } else if (Array.isArray(af.value)) { 34 | return af.value.join(', '); 35 | } else if (af.name === 'Point Group') { 36 | return formatPointGroup(af.value); 37 | } else if (validateFormula(af.value.toString())) { 38 | return {af.value.toString()}; 39 | } else { 40 | return af.value.toString(); 41 | } 42 | }; 43 | 44 | export const ActiveFilterButtons: React.FC = (props) => { 45 | return ( 46 |
47 | {props.filters.map((f, i) => ( 48 |
49 | 55 |
56 | ))} 57 |
58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /src/components/data-display/ActiveFilterButtons/index.tsx: -------------------------------------------------------------------------------- 1 | export { ActiveFilterButtons } from './ActiveFilterButtons'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/ArrayChips/index.tsx: -------------------------------------------------------------------------------- 1 | export { ArrayChips } from './ArrayChips'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/ButtonBar/ButtonBar.css: -------------------------------------------------------------------------------- 1 | .mpc-button-bar { 2 | display: flex; 3 | flex-direction: column; 4 | position: relative; 5 | float: right; 6 | z-index: 2; 7 | } 8 | 9 | .mpc-button-bar > * { 10 | margin: 0 !important; 11 | width: 100%; 12 | } 13 | 14 | .mpc-button-bar .dropdown, 15 | .mpc-button-bar .dropdown-trigger, 16 | .mpc-button-bar .dropdown-trigger .button { 17 | width: 100%; 18 | } 19 | 20 | .mpc-button-bar > *:not(:last-child):not(:focus), 21 | .mpc-button-bar > *:not(:last-child):not(:focus) .button { 22 | border-bottom: none; 23 | } 24 | 25 | .mpc-button-bar > *:not(:last-child):not(:first-child), 26 | .mpc-button-bar > *:not(:last-child):not(:first-child) .button { 27 | border-radius: 0; 28 | } 29 | 30 | .mpc-button-bar > *:first-child:not(:last-child), 31 | .mpc-button-bar > *:first-child:not(:last-child) .button { 32 | border-bottom-left-radius: 0; 33 | border-bottom-right-radius: 0; 34 | } 35 | 36 | .mpc-button-bar > *:last-child:not(:first-child), 37 | .mpc-button-bar > *:last-child:not(:first-child) .button { 38 | border-top-left-radius: 0; 39 | border-top-right-radius: 0; 40 | } 41 | -------------------------------------------------------------------------------- /src/components/data-display/ButtonBar/ButtonBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import classNames from 'classnames'; 3 | import './ButtonBar.css'; 4 | 5 | interface ButtonBarProps { 6 | /** 7 | * The ID used to identify this component in Dash callbacks. 8 | */ 9 | id?: string; 10 | /** 11 | * Class name to apply to the top level of the component 12 | */ 13 | className?: string; 14 | /** 15 | * Dash-assigned callback that should be called to report property changes 16 | * to Dash, to make them available for callbacks. 17 | */ 18 | setProps?: (value: any) => any; 19 | } 20 | 21 | /** 22 | * Wrap around buttons to make a right-floating vertical bar of buttons 23 | */ 24 | export const ButtonBar: React.FC = (props) => { 25 | return ( 26 |
27 | {props.children} 28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/data-display/ButtonBar/index.tsx: -------------------------------------------------------------------------------- 1 | export { ButtonBar } from './ButtonBar'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/DataBlock/index.tsx: -------------------------------------------------------------------------------- 1 | export { DataBlock } from './DataBlock'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/DataCard/DataCard.css: -------------------------------------------------------------------------------- 1 | .mpc-data-card { 2 | display: inline-flex; 3 | } 4 | 5 | .mpc-data-card-right { 6 | flex: 1; 7 | margin-left: 1rem; 8 | } 9 | 10 | .mpc-data-card-right-bottom > * { 11 | flex: 1; 12 | display: grid; 13 | grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); 14 | column-gap: 1rem; 15 | row-gap: 1rem; 16 | } 17 | 18 | .mpc-data-card-right-bottom p:first-child { 19 | font-weight: 600; 20 | } 21 | -------------------------------------------------------------------------------- /src/components/data-display/DataCard/DataCard.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React, { ReactNode } from 'react'; 3 | import './DataCard.css'; 4 | 5 | interface KeyLabelPair { 6 | key: string; 7 | label: string; 8 | } 9 | 10 | interface Props { 11 | id?: string; 12 | setProps?: (value: any) => any; 13 | className?: string; 14 | data: object; 15 | levelOneKey?: string; 16 | levelTwoKey?: string; 17 | levelThreeKeys?: KeyLabelPair[]; 18 | leftComponent?: ReactNode; 19 | } 20 | 21 | export const DataCard: React.FC = (props) => { 22 | return ( 23 |
24 |
{props.leftComponent}
25 |
26 | {props.levelOneKey &&

{props.data[props.levelOneKey]}

} 27 | {props.levelTwoKey &&

{props.data[props.levelTwoKey]}

} 28 |
29 |
30 | {props.levelThreeKeys && props.levelThreeKeys[0] && ( 31 |
32 |

{props.levelThreeKeys[0].label}

33 |

{props.data[props.levelThreeKeys[0].key] || '-'}

34 |
35 | )} 36 | {props.levelThreeKeys && props.levelThreeKeys[1] && ( 37 |
38 |

{props.levelThreeKeys[1].label}

39 |

{props.data[props.levelThreeKeys[1].key] || '-'}

40 |
41 | )} 42 |
43 |
44 | {props.levelThreeKeys && props.levelThreeKeys[2] && ( 45 |
46 |

{props.levelThreeKeys[2].label}

47 |

{props.data[props.levelThreeKeys[2].key] || '-'}

48 |
49 | )} 50 | {props.levelThreeKeys && props.levelThreeKeys[3] && ( 51 |
52 |

{props.levelThreeKeys[3].label}

53 |

{props.data[props.levelThreeKeys[3].key] || '-'}

54 |
55 | )} 56 |
57 |
58 |
59 |
60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /src/components/data-display/DataCard/index.tsx: -------------------------------------------------------------------------------- 1 | export { DataCard } from './DataCard'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/DataTable/ColumnsMenu/index.tsx: -------------------------------------------------------------------------------- 1 | export { ColumnsMenu } from './ColumnsMenu'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/DataTable/DataTable.css: -------------------------------------------------------------------------------- 1 | .mpc-data-table { 2 | background-color: #fff; 3 | border-radius: 6px; 4 | } 5 | 6 | .mpc-data-table .rdt_Table, 7 | .mpc-data-table .rdt_TableRow { 8 | background: none; 9 | } 10 | 11 | .mpc-data-table .mpc-data-table-header { 12 | padding: 1rem 1rem 0 1rem; 13 | } 14 | 15 | .mpc-data-table .mpc-paginator { 16 | padding: 1rem; 17 | border-top: 1px solid rgba(0, 0, 0, 0.12); 18 | } 19 | 20 | .mpc-data-table .react-data-table-outer-container { 21 | position: relative; 22 | height: 100%; 23 | display: flex; 24 | flex-direction: column; 25 | } 26 | 27 | .mpc-data-table .react-data-table-container { 28 | width: 100%; 29 | min-height: 0; 30 | flex: 1; 31 | } 32 | 33 | .mpc-data-table .react-data-table { 34 | overflow: auto; 35 | max-height: 100%; 36 | } 37 | 38 | .mpc-data-table .rdt_TableHead { 39 | position: sticky; 40 | top: 0; 41 | z-index: 1; 42 | box-shadow: #bbb 0px 0px 4px; 43 | } 44 | 45 | .mpc-data-table .react-data-table .column-header-right { 46 | text-align: right; 47 | } 48 | 49 | .mpc-data-table .react-data-table .column-header-center { 50 | text-align: center; 51 | } 52 | 53 | .mpc-data-table .react-data-table .column-units { 54 | font-weight: normal; 55 | } 56 | 57 | .mpc-data-table .mpc-data-table-footer { 58 | padding: 0 1rem 1rem 1rem; 59 | font-size: 0.85rem; 60 | } 61 | -------------------------------------------------------------------------------- /src/components/data-display/DataTable/index.tsx: -------------------------------------------------------------------------------- 1 | export { DataTable } from './DataTable'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/DownloadButton/DownloadButton.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React, { useEffect, useState } from 'react'; 3 | import { downloadAs, DownloadType } from '../../data-entry/utils'; 4 | 5 | interface Props { 6 | id?: string; 7 | setProps?: (value: any) => any; 8 | className?: string; 9 | data: any; 10 | filename?: string; 11 | filetype?: DownloadType; 12 | tooltip?: string; 13 | } 14 | 15 | export const DownloadButton: React.FC = ({ 16 | filename = 'export', 17 | filetype = 'json', 18 | ...otherProps 19 | }) => { 20 | const props = { filename, filetype, ...otherProps }; 21 | const [data, setData] = useState(props.data); 22 | 23 | useEffect(() => { 24 | setData(props.data); 25 | }, [props.data]); 26 | 27 | return ( 28 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/data-display/DownloadButton/index.tsx: -------------------------------------------------------------------------------- 1 | export { DownloadButton } from './DownloadButton'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/DownloadDropdown/index.tsx: -------------------------------------------------------------------------------- 1 | export { DownloadDropdown } from './DownloadDropdown'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/Drawer/Drawer.css: -------------------------------------------------------------------------------- 1 | .mpc-drawer { 2 | background-color: #fff; 3 | width: 400px; 4 | height: 100%; 5 | padding: 1.25rem; 6 | position: absolute; 7 | right: 0; 8 | top: 0; 9 | box-shadow: 0px 0px 3px -5px #dbdbdb, 0 15px 15px; 10 | transform: translate(105%, 0); 11 | transition: 0.5s; 12 | } 13 | 14 | .mpc-drawer.is-active { 15 | transform: translate(0, 0); 16 | } 17 | -------------------------------------------------------------------------------- /src/components/data-display/Drawer/Drawer.tsx: -------------------------------------------------------------------------------- 1 | import React, { Dispatch, SetStateAction, useEffect, useState } from 'react'; 2 | import classNames from 'classnames'; 3 | import './Drawer.css'; 4 | import { ModalCloseButton } from '../Modal/ModalCloseButton'; 5 | import { useDrawerContext } from './DrawerContextProvider'; 6 | 7 | export interface EnlargeableProps { 8 | /** 9 | * A unique ID to use to open and close the drawer in its `DrawerContextProvider`. 10 | * This id should be passed to the `forDrawerId` prop in a `DrawerTrigger` 11 | * that sits inside the same `DrawerContextProvider`. 12 | * Also used to identify this component in Dash callbacks. 13 | */ 14 | id: string; 15 | /** 16 | * Dash-assigned callback that should be called to report property changes 17 | * to Dash, to make them available for callbacks. 18 | */ 19 | setProps?: (value: any) => any; 20 | /** 21 | * Additional class to apply to drawer 22 | */ 23 | className?: string; 24 | } 25 | 26 | /** 27 | * Render a right-side drawer that can be opened and closed. 28 | * A `Drawer` must be used inside of a `DrawerContextProvider` and must have a 29 | * corresponding `DrawerTrigger` within the same context. 30 | * The `id` of a drawer should be passed to the `forDrawerId` prop of a `DrawerTrigger` to open/close the drawer. 31 | */ 32 | export const Drawer: React.FC = (props) => { 33 | const { activeDrawer, setActiveDrawer } = useDrawerContext(); 34 | 35 | return ( 36 |
42 | setActiveDrawer(null)} /> 43 | {props.children} 44 |
45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/components/data-display/Drawer/DrawerContextProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | interface DrawerState { 4 | activeDrawer: string | null; 5 | setActiveDrawer: (value: any) => any; 6 | } 7 | 8 | const DrawerContext = React.createContext(undefined); 9 | 10 | /** 11 | * Wrap a `DrawerTrigger` component and a `Drawer` component inside a `DrawerContextProvider` to render an element (trigger) that 12 | * will open up a modal. Apply props to the `DrawerContextProvider`. 13 | */ 14 | export const DrawerContextProvider: React.FC = (props) => { 15 | const [activeDrawer, setActiveDrawer] = useState(null); 16 | 17 | return ( 18 | 19 | {props.children} 20 | 21 | ); 22 | }; 23 | 24 | /** 25 | * Custom hook for consuming the DrawerContext 26 | * Must only be used by child components of DrawerContextProvider 27 | */ 28 | export const useDrawerContext = () => { 29 | const context = React.useContext(DrawerContext); 30 | if (context === undefined) { 31 | throw new Error('useDrawerContext must be used within a DrawerContextProvider'); 32 | } 33 | return context; 34 | }; 35 | -------------------------------------------------------------------------------- /src/components/data-display/Drawer/DrawerTrigger.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React, { ReactNode, useState } from 'react'; 3 | import { useDrawerContext } from './DrawerContextProvider'; 4 | 5 | interface Props { 6 | /** 7 | * The ID used to identify this component in Dash callbacks 8 | */ 9 | id?: string; 10 | 11 | /** 12 | * Dash-assigned callback that should be called whenever any of the 13 | * properties change 14 | */ 15 | setProps?: (value: any) => any; 16 | 17 | /** 18 | * Class name applied to the drawer trigger span. 19 | * The "mpc-drawer-trigger" class is added automatically 20 | */ 21 | className?: string; 22 | 23 | /** 24 | * The ID of the drawer that this trigger should open. 25 | */ 26 | forDrawerId: string; 27 | } 28 | 29 | /** 30 | * Render a trigger that opens a ModalContent that is within the same ModalContextProvider 31 | */ 32 | export const DrawerTrigger: React.FC = (props) => { 33 | const { activeDrawer, setActiveDrawer } = useDrawerContext(); 34 | 35 | return ( 36 | { 40 | if (activeDrawer === props.forDrawerId) { 41 | setActiveDrawer(null); 42 | } else { 43 | setActiveDrawer(props.forDrawerId); 44 | } 45 | }} 46 | > 47 | {props.children} 48 | 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /src/components/data-display/Drawer/index.tsx: -------------------------------------------------------------------------------- 1 | export { Drawer } from './Drawer'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/Enlargeable/Enlargeable.css: -------------------------------------------------------------------------------- 1 | .mpc-enlarge-button { 2 | float: right; 3 | z-index: 2; 4 | } 5 | 6 | .modal.is-active .modal-content.is-large { 7 | width: 95vw; 8 | max-height: 95vh; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/data-display/Enlargeable/index.tsx: -------------------------------------------------------------------------------- 1 | export { Enlargeable } from './Enlargeable'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/Formula/Formula.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, cleanup } from '@testing-library/react'; 3 | import { Formula } from '.'; 4 | 5 | afterEach(() => cleanup()); 6 | 7 | describe('', () => { 8 | it('should render formula parts individually', () => { 9 | render(Li4Ti5O12); 10 | expect(screen.getByText('Li')).toBeInTheDocument(); 11 | expect(screen.getByText('4')).toBeInTheDocument(); 12 | expect(screen.getByText('Ti')).toBeInTheDocument(); 13 | expect(screen.getByText('5')).toBeInTheDocument(); 14 | expect(screen.getByText('O')).toBeInTheDocument(); 15 | /** Multi-digit numbers should be rendered digit by digit */ 16 | expect(screen.getByText('1')).toBeInTheDocument(); 17 | expect(screen.getByText('2')).toBeInTheDocument(); 18 | }); 19 | it('should render elements as spans', () => { 20 | render(Li4Ti5O12); 21 | expect(screen.getByText('Li').tagName).toBe('SPAN'); 22 | expect(screen.getByText('Ti').tagName).toBe('SPAN'); 23 | expect(screen.getByText('O').tagName).toBe('SPAN'); 24 | }); 25 | it('should render numbers as subscripts', () => { 26 | render(Li4Ti5O12); 27 | expect(screen.getByText('4').tagName).toBe('SUB'); 28 | expect(screen.getByText('5').tagName).toBe('SUB'); 29 | expect(screen.getByText('1').tagName).toBe('SUB'); 30 | expect(screen.getByText('2').tagName).toBe('SUB'); 31 | }); 32 | it('should render non-elements as subscripts', () => { 33 | render(LiFexMn2-xO4); 34 | expect(screen.getAllByText('x')[0].tagName).toBe('SUB'); 35 | expect(screen.getByText('2').tagName).toBe('SUB'); 36 | expect(screen.getByText('-').tagName).toBe('SUB'); 37 | expect(screen.getAllByText('x')[1].tagName).toBe('SUB'); 38 | expect(screen.getByText('4').tagName).toBe('SUB'); 39 | }); 40 | it('should render decimals as subscripts', () => { 41 | render(Y0.95VO4); 42 | expect(screen.getByText('0').tagName).toBe('SUB'); 43 | expect(screen.getByText('.').tagName).toBe('SUB'); 44 | expect(screen.getByText('9').tagName).toBe('SUB'); 45 | expect(screen.getByText('5').tagName).toBe('SUB'); 46 | expect(screen.getByText('4').tagName).toBe('SUB'); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/components/data-display/Formula/Formula.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React from 'react'; 3 | import { ELEMENTS_REGEX, ELEMENTS_SPLIT_REGEX } from '../../data-entry/MaterialsInput/utils'; 4 | 5 | export interface FormulaProps { 6 | /** 7 | * The ID used to identify this component in Dash callbacks 8 | */ 9 | id?: string; 10 | /** 11 | * Dash-assigned callback that should be called whenever any of the 12 | * properties change 13 | */ 14 | setProps?: (value: any) => any; 15 | /** 16 | * Class name(s) to append to the component's default class (`mpc-formula`) 17 | */ 18 | className?: string; 19 | /** 20 | * Formula string to format 21 | */ 22 | children: string; 23 | } 24 | 25 | const formulaItem = (str: string) => { 26 | if (!str.match(/\(|\)|\*/g) && !str.match(ELEMENTS_REGEX)) { 27 | return {str}; 28 | } else { 29 | return {str}; 30 | } 31 | }; 32 | 33 | /** 34 | * Render a formula string with proper subscripts 35 | */ 36 | export const Formula: React.FC = (props) => { 37 | let formula: React.ReactNode; 38 | const splitFormula = props.children.match(ELEMENTS_SPLIT_REGEX); 39 | formula = ( 40 | 41 | {splitFormula?.map((s, i) => ( 42 | {formulaItem(s)} 43 | ))} 44 | 45 | ); 46 | 47 | return ( 48 | 53 | {formula} 54 | 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /src/components/data-display/Formula/index.tsx: -------------------------------------------------------------------------------- 1 | export { Formula } from './Formula'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/JsonView/index.tsx: -------------------------------------------------------------------------------- 1 | export { JsonView } from './JsonView'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/Markdown/index.tsx: -------------------------------------------------------------------------------- 1 | export { Markdown } from './Markdown'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/Modal/Modal.css: -------------------------------------------------------------------------------- 1 | .mpc-modal.modal { 2 | display: flex; 3 | bottom: 100%; 4 | opacity: 0; 5 | transition: 0.5s; 6 | } 7 | 8 | .mpc-modal.modal.is-active { 9 | bottom: 0; 10 | opacity: 1; 11 | } 12 | 13 | .mpc-modal.modal .modal-background { 14 | position: fixed; 15 | top: 0; 16 | opacity: 0; 17 | pointer-events: none; 18 | transition: 0.5s; 19 | } 20 | 21 | .mpc-modal.modal.is-active .modal-background { 22 | opacity: 1; 23 | pointer-events: all; 24 | } 25 | 26 | .mpc-modal.modal-content { 27 | background: #fff; 28 | border-radius: 6px; 29 | } 30 | -------------------------------------------------------------------------------- /src/components/data-display/Modal/Modal.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React, { ReactNode, useState } from 'react'; 3 | import './Modal.css'; 4 | import { ModalCloseButton } from './ModalCloseButton'; 5 | import { useModalContext } from './ModalContextProvider'; 6 | 7 | export interface ModalProps { 8 | /** 9 | * The ID used to identify this component in Dash callbacks 10 | */ 11 | id?: string; 12 | 13 | /** 14 | * Dash-assigned callback that should be called whenever any of the 15 | * properties change 16 | */ 17 | setProps?: (value: any) => any; 18 | 19 | /** 20 | * Class name applied to modal content div. 21 | * The "mpc-modal" and "modal" classes are added automatically 22 | */ 23 | className?: string; 24 | } 25 | 26 | /** 27 | * Render modal that can be opened by a ModalTrigger within its same ModalContextProvider 28 | */ 29 | export const Modal: React.FC = (props) => { 30 | const { active, setActive, forceAction } = useModalContext(); 31 | return ( 32 |
38 |
{ 41 | if (forceAction) { 42 | return; 43 | } else { 44 | setActive(false); 45 | } 46 | }} 47 | >
48 |
49 | {!forceAction && setActive(false)} />} 50 | {props.children} 51 |
52 |
53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /src/components/data-display/Modal/ModalCloseButton/ModalCloseButton.css: -------------------------------------------------------------------------------- 1 | .mpc-modal-close.modal-close { 2 | position: absolute; 3 | top: 1rem; 4 | right: 1rem; 5 | background: rgba(10, 10, 10, 0.2); 6 | } 7 | -------------------------------------------------------------------------------- /src/components/data-display/Modal/ModalCloseButton/ModalCloseButton.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React, { ReactNode, useState } from 'react'; 3 | import './ModalCloseButton.css'; 4 | 5 | interface Props { 6 | /** 7 | * The ID used to identify this component in Dash callbacks 8 | */ 9 | id?: string; 10 | /** 11 | * Dash-assigned callback that should be called whenever any of the 12 | * properties change 13 | */ 14 | setProps?: (value: any) => any; 15 | /** 16 | * Class name applied to the modal close button. 17 | * The "modal-close" and "mpc-modal-close" classes are added automatically. 18 | */ 19 | className?: string; 20 | /** 21 | * Function to handle closing the modal 22 | */ 23 | onClick?: () => any; 24 | } 25 | 26 | /** 27 | * Render an "x" button at the top right of the modal 28 | */ 29 | export const ModalCloseButton: React.FC = (props) => { 30 | return ( 31 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/data-display/Modal/ModalCloseButton/index.tsx: -------------------------------------------------------------------------------- 1 | export { ModalCloseButton } from './ModalCloseButton'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/Modal/ModalTrigger.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React, { ReactNode, useState } from 'react'; 3 | import './Modal.css'; 4 | import { useModalContext } from './ModalContextProvider'; 5 | 6 | interface Props { 7 | /** 8 | * The ID used to identify this component in Dash callbacks 9 | */ 10 | id?: string; 11 | 12 | /** 13 | * Dash-assigned callback that should be called whenever any of the 14 | * properties change 15 | */ 16 | setProps?: (value: any) => any; 17 | 18 | /** 19 | * Class name applied to the modal trigger span. 20 | * The "mpc-modal-trigger" class is added automatically 21 | */ 22 | className?: string; 23 | } 24 | 25 | /** 26 | * Render a trigger that opens a ModalContent that is within the same ModalContextProvider 27 | */ 28 | export const ModalTrigger: React.FC = (props) => { 29 | const { active, setActive } = useModalContext(); 30 | return ( 31 | setActive(!active)} 35 | > 36 | {props.children} 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/data-display/Modal/index.tsx: -------------------------------------------------------------------------------- 1 | export { Modal } from './Modal'; 2 | export { ModalTrigger } from './ModalTrigger'; 3 | export { ModalContextProvider } from './ModalContextProvider'; 4 | -------------------------------------------------------------------------------- /src/components/data-display/Paginator/Paginator.css: -------------------------------------------------------------------------------- 1 | .mpc-paginator .pagination { 2 | display: inline-flex; 3 | } 4 | 5 | .mpc-paginator { 6 | display: flex; 7 | justify-content: space-between; 8 | } 9 | 10 | .mpc-paginator .dropdown { 11 | margin-right: 0.5rem; 12 | text-align: left; 13 | } 14 | 15 | @media screen and (min-width: 1152px) { 16 | .mpc-paginator .pagination { 17 | min-width: 450px; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/data-display/Paginator/index.tsx: -------------------------------------------------------------------------------- 1 | export { Paginator } from './Paginator'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/SearchUI/SearchUIContainer/MatscholarSearchUIContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SearchUIContextProvider } from '../SearchUIContextProvider'; 3 | import { SearchUIContainerProps, SearchUIViewType } from '../types'; 4 | import { BrowserRouter as Router, Route } from 'react-router-dom'; 5 | import classNames from 'classnames'; 6 | import { QueryParamProvider } from 'use-query-params'; 7 | import './SearchUIContainer.css'; 8 | import { MatscholarSearchUIContextProvider } from '../SearchUIContextProvider/MatscholarSearchUIContextProvider'; 9 | 10 | /** 11 | * Alternate version of the SearchUIContainer which uses MatscholarSearchUIContextProvider 12 | * for alpha version support for Matscholar queries. 13 | */ 14 | export const MatscholarSearchUIContainer: React.FC = ({ 15 | view = SearchUIViewType.TABLE, 16 | apiEndpoint = '', 17 | apiEndpointParams = {}, 18 | resultLabel = 'result', 19 | hasSortMenu = true, 20 | sortFields = [], 21 | sortKey = '_sort_fields', 22 | limitKey = '_limit', 23 | skipKey = '_skip', 24 | fieldsKey = '_fields', 25 | totalKey = 'meta.total_doc', 26 | conditionalRowStyles = [], 27 | results = [], 28 | selectedRows = [], 29 | setProps = () => null, 30 | debounce = 1000, 31 | ...otherProps 32 | }) => { 33 | const props = { 34 | view, 35 | apiEndpoint, 36 | apiEndpointParams, 37 | resultLabel, 38 | hasSortMenu, 39 | sortFields, 40 | sortKey, 41 | limitKey, 42 | skipKey, 43 | fieldsKey, 44 | totalKey, 45 | conditionalRowStyles, 46 | results, 47 | selectedRows, 48 | setProps, 49 | debounce, 50 | ...otherProps 51 | }; 52 | return ( 53 |
54 | 55 | 56 | 57 | {props.children} 58 | 59 | 60 | 61 |
62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /src/components/data-display/SearchUI/SearchUIContainer/SearchUIContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SearchUIContextProvider } from '../SearchUIContextProvider'; 3 | import { SearchUIContainerProps, SearchUIViewType } from '../types'; 4 | import { BrowserRouter as Router, Route } from 'react-router-dom'; 5 | import classNames from 'classnames'; 6 | import { QueryParamProvider } from 'use-query-params'; 7 | import './SearchUIContainer.css'; 8 | 9 | /** 10 | * A component for building a customizable, integrated search interface that can fetch and filter data from a REST API. 11 | * This component generates a state context which can be shared by its inner components. 12 | */ 13 | export const SearchUIContainer: React.FC = ({ 14 | view = SearchUIViewType.TABLE, 15 | apiEndpoint = '', 16 | apiEndpointParams = {}, 17 | resultLabel = 'result', 18 | hasSortMenu = true, 19 | sortFields = [], 20 | sortKey = '_sort_fields', 21 | limitKey = '_limit', 22 | skipKey = '_skip', 23 | fieldsKey = '_fields', 24 | totalKey = 'meta.total_doc', 25 | conditionalRowStyles = [], 26 | results = [], 27 | selectedRows = [], 28 | setProps = () => null, 29 | debounce = 1000, 30 | ...otherProps 31 | }) => { 32 | const props = { 33 | view, 34 | apiEndpoint, 35 | apiEndpointParams, 36 | resultLabel, 37 | hasSortMenu, 38 | sortFields, 39 | sortKey, 40 | limitKey, 41 | skipKey, 42 | fieldsKey, 43 | totalKey, 44 | conditionalRowStyles, 45 | results, 46 | selectedRows, 47 | setProps, 48 | debounce, 49 | ...otherProps 50 | }; 51 | return ( 52 |
53 | 54 | 55 | {props.children} 56 | 57 | 58 |
59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /src/components/data-display/SearchUI/SearchUIContainer/index.tsx: -------------------------------------------------------------------------------- 1 | export { SearchUIContainer } from './SearchUIContainer'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/SearchUI/SearchUIContextProvider/index.tsx: -------------------------------------------------------------------------------- 1 | export { 2 | SearchUIContextProvider, 3 | useSearchUIContext, 4 | useSearchUIContextActions 5 | } from './SearchUIContextProvider'; 6 | -------------------------------------------------------------------------------- /src/components/data-display/SearchUI/SearchUIDataCards/SearchUIDataCards.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import { useSearchUIContext, useSearchUIContextActions } from '../SearchUIContextProvider'; 3 | import { Paginator } from '../../Paginator'; 4 | import { DataCard } from '../../DataCard'; 5 | 6 | /** 7 | * NOT IMPLEMENTED 8 | * Component for rendering SearchUI results in the cards view 9 | * Will use the DataCard component to render results as a grid 10 | * of cards where each shows an image and a few select data properties. 11 | */ 12 | export const SearchUIDataCards: React.FC = () => { 13 | const { state, query } = useSearchUIContext(); 14 | const actions = useSearchUIContextActions(); 15 | const handlePageChange = (page: number) => { 16 | actions.setPage(page); 17 | }; 18 | 19 | const CustomPaginator = () => ( 20 | 26 | ); 27 | 28 | return ( 29 |
30 | 31 |
32 | {state.results!.map((d, i) => ( 33 | 42 | 45 | 46 | } 47 | /> 48 | ))} 49 |
50 | 51 |
52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /src/components/data-display/SearchUI/SearchUIDataCards/index.tsx: -------------------------------------------------------------------------------- 1 | export { SearchUIDataCards } from './SearchUIDataCards'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/SearchUI/SearchUIDataHeader/index.tsx: -------------------------------------------------------------------------------- 1 | export { SearchUIDataHeader } from './SearchUIDataHeader'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/SearchUI/SearchUIDataTable/index.tsx: -------------------------------------------------------------------------------- 1 | export { SearchUIDataTable } from './SearchUIDataTable'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/SearchUI/SearchUIDataView/SearchUIDataView.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import { useSearchUIContext, useSearchUIContextActions } from '../SearchUIContextProvider'; 3 | import { searchUIViewsMap } from '../types'; 4 | import { FaExclamationTriangle } from 'react-icons/fa'; 5 | 6 | /** 7 | * Component for rendering SearchUI data in a certain view dynamically 8 | * based on the current view state, error state, and number 9 | * of results. The view is determined by the `SearchUIContainer`'s `view` prop. 10 | */ 11 | export const SearchUIDataView: React.FC = () => { 12 | const { state, query } = useSearchUIContext(); 13 | 14 | const getDataView = () => { 15 | if (state.error) { 16 | return ( 17 |
18 |

19 | There was an error with your search. 20 |

21 |

22 | You may have entered an invalid search value. Otherwise, the API may be temporarily 23 | unavailable. 24 |

25 |
26 | ); 27 | } else if (!state.results || state.results.length === 0) { 28 | return ( 29 |
30 |

No records match your search criteria

31 |
32 | ); 33 | } else { 34 | const SearchUIViewComponent = searchUIViewsMap[state.view!]; 35 | return ; 36 | } 37 | }; 38 | 39 | return
{getDataView()}
; 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/data-display/SearchUI/SearchUIDataView/index.tsx: -------------------------------------------------------------------------------- 1 | export { SearchUIDataView } from './SearchUIDataView'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/SearchUI/SearchUIFilters/index.tsx: -------------------------------------------------------------------------------- 1 | export { SearchUIFilters } from './SearchUIFilters'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/SearchUI/SearchUIGrid/SearchUIGrid.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SearchUIFilters } from '../SearchUIFilters'; 3 | import { SearchUIDataHeader } from '../SearchUIDataHeader'; 4 | import { SearchUIDataView } from '../SearchUIDataView'; 5 | 6 | /** 7 | * A component that combines the filters, data header, and data view 8 | * of a `SearchUI` into a common grid layout. 9 | * Note that this must be used within a `SearchUIContainer`. 10 | */ 11 | export const SearchUIGrid: React.FC = (props) => { 12 | return ( 13 |
14 |
15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/data-display/SearchUI/SearchUIGrid/index.tsx: -------------------------------------------------------------------------------- 1 | export { SearchUIGrid } from './SearchUIGrid'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/SearchUI/SearchUISearchBar/index.tsx: -------------------------------------------------------------------------------- 1 | export { SearchUISearchBar } from './SearchUISearchBar'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/SearchUI/SearchUISynthesisRecipeCards/SearchUISynthesisRecipeCards.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSearchUIContext, useSearchUIContextActions } from '../SearchUIContextProvider'; 3 | import { Paginator } from '../../Paginator'; 4 | import { SynthesisRecipeCard } from '../../SynthesisRecipeCard'; 5 | 6 | /** 7 | * 8 | */ 9 | export const SearchUISynthesisRecipeCards: React.FC = () => { 10 | const { state, query } = useSearchUIContext(); 11 | const actions = useSearchUIContextActions(); 12 | const handlePageChange = (page: number) => { 13 | actions.setPage(page); 14 | }; 15 | 16 | const CustomPaginator = ({ isTop = false }) => ( 17 | 25 | ); 26 | 27 | return ( 28 |
29 |
30 | {state.results!.map((d, i) => ( 31 | // Custom result cards 32 | 33 | ))} 34 |
35 | 36 |
37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/data-display/SearchUI/SearchUISynthesisRecipeCards/index.tsx: -------------------------------------------------------------------------------- 1 | export { SearchUISynthesisRecipeCards } from './SearchUISynthesisRecipeCards'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/SearchUI/index.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materialsproject/mp-react-components/73f1a1b38ccb4021ece5dcf38d472e95d96521dc/src/components/data-display/SearchUI/index.tsx -------------------------------------------------------------------------------- /src/components/data-display/SortDropdown/index.tsx: -------------------------------------------------------------------------------- 1 | export { SortDropdown } from './SortDropdown'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/SynthesisRecipeCard/SynthesisRecipeCard.css: -------------------------------------------------------------------------------- 1 | .mpc-synthesis-card-type { 2 | position: relative; 3 | top: -2rem; 4 | left: -0.5rem; 5 | height: 0.2rem; 6 | } 7 | 8 | .mpc-synthesis-card-equation { 9 | margin-bottom: 0.5rem; 10 | font-weight: bold; 11 | } 12 | 13 | .mpc-synthesis-card-paragraph > *:first-child { 14 | margin-bottom: 0.5rem; 15 | border-left: 4px solid #b5b5b5; 16 | padding-left: 0.5rem; 17 | } 18 | 19 | .mpc-synthesis-card .mpc-data-block-footer { 20 | font-size: 0.85rem; 21 | } 22 | 23 | .mpc-synthesis-card-material-label { 24 | font-weight: bold; 25 | } 26 | 27 | .mpc-synthesis-card-material-label { 28 | font-weight: bold; 29 | } 30 | 31 | .mpc-synthesis-card-highlight-hit { 32 | background: yellow; 33 | } 34 | 35 | .mpc-synthesis-card-operations { 36 | font-size: smaller; 37 | } 38 | 39 | .mpc-synthesis-card-collapse-operations { 40 | cursor: pointer; 41 | font-size: smaller; 42 | color: #3960e3; 43 | } 44 | 45 | .mpc-synthesis-card-collapse-operations:hover { 46 | text-decoration: underline; 47 | } 48 | 49 | .mpc-synthesis-card-collapse { 50 | margin-top: 0.5rem; 51 | } 52 | 53 | .mpc-synthesis-card-collapse-chevron { 54 | -moz-transition: all 400ms ease; 55 | -webkit-transition: all 400ms ease; 56 | transition: all 400ms ease; 57 | } 58 | 59 | .mpc-synthesis-card-collapse-opened .mpc-synthesis-card-collapse-chevron { 60 | -ms-transform: rotate(180deg); 61 | -moz-transform: rotate(180deg); 62 | -webkit-transform: rotate(180deg); 63 | transform: rotate(180deg); 64 | } 65 | 66 | .mpc-synthesis-card .mpc-formula { 67 | font-weight: 600; 68 | } 69 | 70 | @media screen and (min-width: 769px) and (max-width: 1116px) { 71 | .mpc-synthesis-card .mpc-data-block-item { 72 | min-width: auto !important; 73 | max-width: none !important; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/components/data-display/SynthesisRecipeCard/index.tsx: -------------------------------------------------------------------------------- 1 | export { SynthesisRecipeCard } from './SynthesisRecipeCard'; 2 | -------------------------------------------------------------------------------- /src/components/data-display/Tooltip/Tooltip.css: -------------------------------------------------------------------------------- 1 | .mpc-tooltip { 2 | font-weight: normal; 3 | text-align: left; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/data-display/Tooltip/index.tsx: -------------------------------------------------------------------------------- 1 | export { Tooltip } from './Tooltip'; 2 | -------------------------------------------------------------------------------- /src/components/data-entry/CheckboxList/CheckboxList.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | 3 | interface Option { 4 | value: any; 5 | label: string | number; 6 | checked?: boolean; 7 | } 8 | 9 | interface Props { 10 | options: Option[]; 11 | values?: any[]; 12 | onChange?: (value: any) => void; 13 | } 14 | 15 | const getOptions = (options: Option[], values?: any[]) => { 16 | return options.map((d) => { 17 | if (values) { 18 | d.checked = values.indexOf(d.value) > -1 ? true : false; 19 | } else { 20 | d.checked = false; 21 | } 22 | return d; 23 | }); 24 | }; 25 | 26 | const getCheckedValues = (ops: Option[]) => { 27 | const checkedValues: any[] = []; 28 | ops.forEach((d) => { 29 | if (d.checked) { 30 | checkedValues.push(d.value); 31 | } 32 | }); 33 | 34 | return checkedValues; 35 | }; 36 | 37 | export const CheckboxList: React.FC = (props) => { 38 | const [options, setOptions] = useState(getOptions(props.options, props.values)); 39 | 40 | const handleChange = (index: number) => { 41 | let newOptions = [...options]; 42 | newOptions[index].checked = !newOptions[index].checked; 43 | const newValues = getCheckedValues(newOptions); 44 | if (props.onChange) props.onChange(newValues); 45 | setOptions(newOptions); 46 | }; 47 | 48 | useEffect(() => { 49 | setOptions(getOptions(options, props.values)); 50 | }, [props.values]); 51 | 52 | return ( 53 |
54 | {options.map((d, i) => ( 55 |
56 | 65 |
66 | ))} 67 |
68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /src/components/data-entry/CheckboxList/index.tsx: -------------------------------------------------------------------------------- 1 | export { CheckboxList } from './CheckboxList'; 2 | -------------------------------------------------------------------------------- /src/components/data-entry/DualRangeSlider/index.tsx: -------------------------------------------------------------------------------- 1 | export { DualRangeSlider } from './DualRangeSlider'; 2 | -------------------------------------------------------------------------------- /src/components/data-entry/FilterField/FilterField.css: -------------------------------------------------------------------------------- 1 | .mpc-filter-label { 2 | font-weight: 700; 3 | margin-bottom: 0.5rem; 4 | } 5 | 6 | .mpc-units { 7 | font-weight: normal; 8 | font-size: 0.85rem; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/data-entry/FilterField/index.tsx: -------------------------------------------------------------------------------- 1 | export { FilterField } from './FilterField'; 2 | -------------------------------------------------------------------------------- /src/components/data-entry/GlobalSearchBar/GlobalSearchBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { linkOnClick } from '../../../utils/navigation'; 3 | import { MaterialsInput, MaterialsInputType } from '../MaterialsInput'; 4 | import { PeriodicTableMode } from '../MaterialsInput/MaterialsInput'; 5 | 6 | /** 7 | * A specific version of the MaterialsInput component used within the SearchUI component 8 | * for performing top level searches by mp-id, formula, or elements. 9 | * The input value is parsed into its appropriate search inputType upon submission. 10 | */ 11 | 12 | interface Props { 13 | redirectRoute: string; 14 | hidePeriodicTable?: boolean; 15 | autocompleteFormulaUrl?: string; 16 | apiKey?: string; 17 | placeholder?: string; 18 | } 19 | 20 | export const GlobalSearchBar: React.FC = (props) => { 21 | const [searchValue, setSearchValue] = useState(''); 22 | const [searchInputType, setSearchInputType] = useState(MaterialsInputType.ELEMENTS); 23 | 24 | const handleSubmit = (e: React.FormEvent | React.MouseEvent, value?: string) => { 25 | let query = new URLSearchParams(); 26 | query.set(searchInputType, searchValue); 27 | const href = props.redirectRoute + '?' + query.toString(); 28 | linkOnClick(e, href); 29 | }; 30 | 31 | return ( 32 | setSearchValue(v)} 36 | onInputTypeChange={(inputType) => setSearchInputType(inputType)} 37 | onSubmit={handleSubmit} 38 | periodicTableMode={PeriodicTableMode.TOGGLE} 39 | hidePeriodicTable={props.hidePeriodicTable} 40 | autocompleteFormulaUrl={props.autocompleteFormulaUrl} 41 | autocompleteApiKey={props.apiKey} 42 | placeholder={props.placeholder} 43 | /> 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/components/data-entry/GlobalSearchBar/index.tsx: -------------------------------------------------------------------------------- 1 | export { GlobalSearchBar } from './GlobalSearchBar'; 2 | -------------------------------------------------------------------------------- /src/components/data-entry/MaterialsInput/FormulaAutocomplete/index.tsx: -------------------------------------------------------------------------------- 1 | export { FormulaAutocomplete } from './FormulaAutocomplete'; 2 | -------------------------------------------------------------------------------- /src/components/data-entry/MaterialsInput/InputHelp/InputHelp.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React from 'react'; 3 | 4 | export interface InputHelpItem { 5 | label?: string | null; 6 | examples?: string[] | null; 7 | } 8 | 9 | interface Props { 10 | items: InputHelpItem[]; 11 | show?: boolean; 12 | onChange?: (value: string) => void; 13 | } 14 | 15 | /** 16 | * Interactive help menu to display below `MaterialsInput` 17 | */ 18 | export const InputHelp: React.FC = (props) => { 19 | return ( 20 |
26 | {props.items.map((item, i) => ( 27 |
28 | {item.examples && ( 29 |
30 | {item.label && {item.label}:} 31 |
32 | {item.examples.map((example, k) => ( 33 | { 37 | if (props.onChange) props.onChange(example); 38 | }} 39 | > 40 | {example} 41 | 42 | ))} 43 |
44 |
45 | )} 46 | {!item.examples && item.label &&
{item.label}
} 47 |
48 | ))} 49 |
50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /src/components/data-entry/MaterialsInput/InputHelp/index.tsx: -------------------------------------------------------------------------------- 1 | export { InputHelp } from './InputHelp'; 2 | -------------------------------------------------------------------------------- /src/components/data-entry/MaterialsInput/MaterialsInput.css: -------------------------------------------------------------------------------- 1 | .mpc-materials-input { 2 | position: relative; 3 | } 4 | 5 | .mpc-materials-input .autocomplete { 6 | display: block; 7 | position: absolute; 8 | top: 100%; 9 | left: 0; 10 | width: 100%; 11 | padding: 0; 12 | border: solid 1px #eee; 13 | border-radius: 0 0 5px 5px; 14 | } 15 | 16 | .mpc-materials-input .autocomplete .dropdown-content { 17 | border-radius: 0 0 5px 5px; 18 | } 19 | 20 | .mpc-materials-input .input-help-menu { 21 | display: block; 22 | position: absolute; 23 | top: 100%; 24 | left: 0; 25 | z-index: 2; 26 | width: 100%; 27 | background: #fff; 28 | padding: 1rem; 29 | border: solid 1px #eee; 30 | border-radius: 0 0 5px 5px; 31 | max-height: 200px; 32 | overflow: auto; 33 | } 34 | 35 | .mpc-materials-input .input-help-menu > div:not(:last-child) { 36 | margin-bottom: 0.5rem; 37 | } 38 | 39 | .mpc-materials-input .input-help-menu .tags { 40 | display: inline-flex; 41 | } 42 | 43 | .mpc-materials-input .autocomplete-label { 44 | font-size: 0.8em; 45 | font-style: italic; 46 | padding: 0 0 0.5em 1em; 47 | } 48 | 49 | .mpc-materials-input .input[type='search'] { 50 | box-shadow: none; 51 | } 52 | 53 | .mpc-materials-input .control [data-tooltip] { 54 | border-left: none; 55 | } 56 | 57 | .mpc-materials-input .control [data-tooltip]:hover { 58 | border-left: 1px solid #b5b5b5; 59 | } 60 | 61 | .mpc-materials-input .mpc-materials-input-error { 62 | color: #d72638; 63 | } 64 | 65 | .mpc-materials-input .control .button { 66 | height: 100%; 67 | } 68 | 69 | .mpc-materials-input .input-help-button { 70 | border-left: none; 71 | } 72 | 73 | .mpc-materials-input .input-help-button:hover, 74 | .mpc-materials-input .input-help-button:focus { 75 | border-color: #dbdbdb; 76 | } 77 | 78 | /* .mpc-materials-input .table-transition-wrapper-small { 79 | width: 600px; 80 | margin: auto; 81 | position: absolute; 82 | z-index: 500; 83 | top: 1.75rem; 84 | left: 50%; 85 | transform: translate(-50%, 0); 86 | } 87 | 88 | .mpc-materials-input .table-legend-container.box { 89 | border: 1px solid #ddd; 90 | border-top: none; 91 | border-radius: 0 0 6px 6px; 92 | background: #fff; 93 | box-shadow: 0 0.5em 1em -0.125em rgb(10 10 10 / 10%), 0 4px 4px 1px #777; 94 | }*/ 95 | -------------------------------------------------------------------------------- /src/components/data-entry/MaterialsInput/MaterialsInputBox/index.tsx: -------------------------------------------------------------------------------- 1 | export { MaterialsInputBox } from './MaterialsInputBox'; 2 | -------------------------------------------------------------------------------- /src/components/data-entry/MaterialsInput/index.tsx: -------------------------------------------------------------------------------- 1 | export { MaterialsInput, MaterialsInputType, MaterialsInputProps } from './MaterialsInput'; 2 | -------------------------------------------------------------------------------- /src/components/data-entry/RangeSlider/RangeSlider.css: -------------------------------------------------------------------------------- 1 | .mpc-range-slider:not(.no-ticks) { 2 | padding-bottom: 1em; 3 | } 4 | 5 | .mpc-range-slider .level { 6 | margin-bottom: 0; 7 | } 8 | 9 | .mpc-dual-range-slider .input { 10 | flex-basis: 75px; 11 | min-width: 75px; 12 | } 13 | 14 | .mpc-range-slider .input { 15 | width: 75px; 16 | } 17 | 18 | .mpc-range-slider .slider { 19 | padding: 0 0.75em; 20 | margin-top: 0.25rem; 21 | } 22 | 23 | .mpc-range-slider .slider-track { 24 | height: 1.5em; 25 | display: flex; 26 | justify-content: center; 27 | } 28 | 29 | .mpc-range-slider .slider-track-inner { 30 | height: 5px; 31 | width: 100%; 32 | align-self: center; 33 | } 34 | 35 | .mpc-range-slider .button.is-slider { 36 | padding: 0; 37 | height: 1.5em; 38 | width: 1.5em; 39 | } 40 | 41 | .mpc-range-slider .button.is-slider > .inner-slider-button { 42 | height: 1em; 43 | width: 5px; 44 | background-color: #ccc; 45 | } 46 | 47 | .mpc-range-slider .button.is-slider.is-dragged > .inner-slider-button { 48 | background-color: #3273dc; 49 | } 50 | 51 | .mpc-range-slider .slider-tick-mark { 52 | height: 0.4em; 53 | width: 1px; 54 | margin-top: 7px !important; 55 | } 56 | 57 | .mpc-range-slider .slider-tick-value { 58 | position: absolute; 59 | top: 2em; 60 | font-size: 0.8em; 61 | transform: translate(-50%, 0); 62 | } 63 | -------------------------------------------------------------------------------- /src/components/data-entry/RangeSlider/index.tsx: -------------------------------------------------------------------------------- 1 | export { RangeSlider } from './RangeSlider'; 2 | -------------------------------------------------------------------------------- /src/components/data-entry/Select/Select.css: -------------------------------------------------------------------------------- 1 | .react-select-outer-container.is-open { 2 | position: relative; 3 | z-index: 100; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/data-entry/Select/index.tsx: -------------------------------------------------------------------------------- 1 | export { Select } from './Select'; 2 | -------------------------------------------------------------------------------- /src/components/data-entry/Switch/Switch.css: -------------------------------------------------------------------------------- 1 | .mpc-switch { 2 | display: inline-block; 3 | line-height: 1.5; 4 | } 5 | 6 | .mpc-switch-icon { 7 | font-size: 2rem; 8 | vertical-align: bottom; 9 | } 10 | 11 | .mpc-switch-label { 12 | margin-left: 0.5rem; 13 | vertical-align: text-bottom; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/data-entry/Switch/Switch.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React from 'react'; 3 | import { FaToggleOff, FaToggleOn } from 'react-icons/fa'; 4 | import './Switch.css'; 5 | 6 | export interface SwitchProps { 7 | /** 8 | * The ID used to identify this component in Dash callbacks 9 | */ 10 | id?: string; 11 | /** 12 | * Dash-assigned callback that should be called whenever any of the 13 | * properties change 14 | */ 15 | setProps?: (value: any) => any; 16 | /** 17 | * Class name(s) to append to the component's default class. 18 | */ 19 | className?: string; 20 | /** 21 | * Value of the input, either true or false. 22 | */ 23 | value?: boolean; 24 | /** 25 | * Whether to show a label to the right of the switch. 26 | */ 27 | hasLabel?: boolean; 28 | /** 29 | * Text to show when the switch is on. 30 | * @default 'On' 31 | */ 32 | truthyLabel?: string; 33 | /** 34 | * Text to show when the switch is off. 35 | * @default 'Off' 36 | */ 37 | falsyLabel?: string; 38 | onChange?: (value: boolean) => any; 39 | } 40 | 41 | /** 42 | * Simple boolean switch 43 | */ 44 | export const Switch: React.FC = ({ 45 | value = false, 46 | truthyLabel = 'On', 47 | falsyLabel = 'Off', 48 | ...otherProps 49 | }) => { 50 | const props = { value, truthyLabel, falsyLabel, ...otherProps }; 51 | const handleClick = () => { 52 | const newValue = !props.value; 53 | if (props.onChange) { 54 | props.onChange(newValue); 55 | } 56 | if (props.setProps) { 57 | props.setProps({ value: newValue }); 58 | } 59 | }; 60 | 61 | return ( 62 |
63 | {props.value ? ( 64 | 65 | ) : ( 66 | 67 | )} 68 | {props.hasLabel && ( 69 | 70 | {props.value ? props.truthyLabel : props.falsyLabel} 71 | 72 | )} 73 |
74 | ); 75 | }; 76 | -------------------------------------------------------------------------------- /src/components/data-entry/Switch/index.tsx: -------------------------------------------------------------------------------- 1 | export { Switch } from './Switch'; 2 | -------------------------------------------------------------------------------- /src/components/data-entry/TextInput/TextInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useDebounce } from '../../../utils/hooks'; 3 | 4 | interface Props { 5 | value: any; 6 | debounce?: number; 7 | onChange: (value: any) => any; 8 | } 9 | 10 | export const TextInput: React.FC = (props) => { 11 | const [inputValue, setInputValue] = useState(props.value); 12 | const debouncedInputValue = props.debounce ? useDebounce(inputValue, props.debounce) : inputValue; 13 | 14 | const handleChange = (e) => { 15 | setInputValue(e.target.value); 16 | }; 17 | 18 | /** 19 | * This effect is triggered after the debouncedInputValue is set 20 | * The debouncedInputValue is set with inputValue after the specified debounce time 21 | * If no debounce prop is supplied, there is no debounce and debouncedInputValue is exactly the same as inputValue 22 | * Triggers the onChange event prop for the value prop 23 | */ 24 | useEffect(() => { 25 | props.onChange(debouncedInputValue); 26 | }, [debouncedInputValue]); 27 | 28 | /** 29 | * This effect is triggered when the value prop is changed directly from outside this component 30 | * Here inputValue is set, triggering debouncedInputValue to get set after the debounce timer 31 | */ 32 | useEffect(() => { 33 | setInputValue(props.value); 34 | }, [props.value]); 35 | 36 | return ; 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/data-entry/TextInput/index.tsx: -------------------------------------------------------------------------------- 1 | export { TextInput } from './TextInput'; 2 | -------------------------------------------------------------------------------- /src/components/data-entry/ThreeStateBooleanSelect/ThreeStateBooleanSelect.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import { Select } from '../Select'; 3 | import { SelectOption } from '../Select/Select'; 4 | 5 | export interface ThreeStateBooleanSelectProps { 6 | /** 7 | * List of two options: one for `true` and one for `false`. 8 | * The third `undefined` option is included automatically with the label "Any." 9 | * Each option should be an object with a `label` and a `value` where the value is either true or false. 10 | */ 11 | options: SelectOption[]; 12 | /** 13 | * The current or initial selected value. 14 | * Must be `true`, `false`, or `undefined`. 15 | */ 16 | value?: boolean; 17 | /** 18 | * Function to handle what happens when an option is selected. 19 | */ 20 | onChange?: (value: any) => void; 21 | } 22 | 23 | /** 24 | * A select component that handles values that can be true, false, or undefined. 25 | * The undefined option will automatically render with the label "Any." 26 | */ 27 | export const ThreeStateBooleanSelect: React.FC = ({ 28 | options, 29 | value, 30 | onChange 31 | }) => { 32 | const threeOptions = [options[0], options[1], { label: 'Any', value: undefined }]; 33 | const selected = threeOptions.find((option) => option.value === value); 34 | return ; 12 | 13 | export const Basic = Template.bind({}); 14 | Basic.args = { 15 | isClearable: true, 16 | value: 'NM', 17 | options: [ 18 | { 19 | label: 'Ferromagnetic', 20 | value: 'FM' 21 | }, 22 | { 23 | label: 'Non-magnetic', 24 | value: 'NM' 25 | }, 26 | { 27 | label: 'Ferrimagnetic', 28 | value: 'FiM' 29 | }, 30 | { 31 | label: 'Antiferromagnetic', 32 | value: 'AFM' 33 | }, 34 | { 35 | label: 'Unknown', 36 | value: 'Unknown' 37 | } 38 | ] 39 | }; 40 | 41 | export const Controlled: Story> = (args) => { 42 | const [state, setState] = useState({ value: 1 }); 43 | return ( 44 |