From e58ba39a6078dae7567bbf19f368a0e946ab53a6 Mon Sep 17 00:00:00 2001 From: dannyrb Date: Thu, 4 Jul 2019 16:19:28 -0400 Subject: [PATCH] more shifting --- package.json | 3 + packages/_components/.all-contributorsrc | 135 + packages/_components/.babelrc | 18 + packages/_components/.circleci/config.yml | 85 + .../.editorconfig | 0 packages/_components/.env | 11 + packages/_components/.eslintrc | 17 + packages/_components/.gitattributes | 1 + .../.github/ISSUE_TEMPLATE/---bug-report.md | 40 + .../ISSUE_TEMPLATE/---feature-request.md | 18 + .../ISSUE_TEMPLATE/---support-question.md | 15 + packages/_components/.gitignore | 32 + packages/_components/.nvmrc | 1 + .../.prettierrc | 0 packages/_components/.vscode/extensions.json | 8 + packages/_components/.vscode/settings.json | 29 + .../LICENSE | 0 packages/_components/README.md | 165 + packages/_components/doczrc.js | 113 + packages/_components/netlify.toml | 21 + packages/_components/package.json | 127 + packages/_components/public/docz.css | 44 + packages/_components/rollup.config.js | 61 + .../src/LayoutChooser/LayoutManager.css | 22 + .../src/LayoutChooser/LayoutManager.js | 162 + .../LayoutChooser/LayoutPanelDropTarget.css | 12 + .../LayoutChooser/LayoutPanelDropTarget.js | 74 + .../src/ScrollableArea/ScrollableArea.js | 152 + .../src/ScrollableArea/ScrollableArea.styl | 69 + .../_components/src/__docs__/NameSpace.js | 17 + .../src/__docs__/assets/github.png | Bin 0 -> 37650 bytes .../_components/src/__docs__/assets/npm.png | Bin 0 -> 9155 bytes .../_components/src/__docs__/assets/react.png | Bin 0 -> 61996 bytes .../src/__docs__/assets/typescript.png | Bin 0 -> 18301 bytes .../src/__docs__/compatibility.mdx | 15 + .../src/__docs__/config/Wrapper.js | 0 .../src/__docs__/docs-settings.mdx | 13 + .../src/__docs__/getting-started.mdx | 76 + packages/_components/src/__docs__/index.html | 80 + .../_components/src/__docs__/introduction.mdx | 76 + .../src/__docs__/styling-and-theming.mdx | 56 + .../_components/src/__docs__/translating.mdx | 87 + packages/_components/src/__docs__/wrapper.js | 22 + .../_components/src/__docs__/wrapper.styl | 15 + .../components/checkbox/__docs__/checkbox.mdx | 42 + .../src/components/checkbox/checkbox.css | 56 + .../src/components/checkbox/checkbox.js | 46 + .../src/components/checkbox/index.js | 1 + .../src/components/cineDialog/CineDialog.js | 202 + .../src/components/cineDialog/CineDialog.styl | 169 + .../cineDialog/__docs__/cineDialog.mdx | 57 + .../src/components/cineDialog/index.js | 2 + packages/_components/src/components/index.js | 48 + .../languageSwitcher/LanguageSwitcher.js | 66 + .../languageSwitcher/LanguageSwitcher.styl | 4 + .../src/components/languageSwitcher/index.js | 1 + .../components/layoutButton/LayoutButton.js | 64 + .../components/layoutButton/LayoutChooser.js | 125 + .../layoutButton/LayoutChooser.styl | 46 + .../layoutButton/__docs__/layoutButton.css | 10 + .../layoutButton/__docs__/layoutButton.mdx | 49 + .../src/components/layoutButton/index.js | 2 + .../measurementTable/MeasurementTable.js | 178 + .../measurementTable/MeasurementTable.styl | 92 + .../measurementTable/MeasurementTableItem.js | 162 + .../MeasurementTableItem.styl | 61 + .../__docs__/measurementTable.mdx | 49 + .../measurementTable/__docs__/measurements.js | 93 + .../measurementTable/__docs__/timepoints.js | 14 + .../measurementTable/__docs__/warnings.js | 6 + .../src/components/measurementTable/index.js | 2 + .../src/components/overlayTrigger/Fade.js | 102 + .../src/components/overlayTrigger/Overlay.js | 102 + .../overlayTrigger/OverlayTrigger.js | 330 + .../__docs__/OverlayTrigger.mdx | 84 + .../overlayTrigger/createChainedFunction.js | 31 + .../src/components/overlayTrigger/index.js | 2 + .../src/components/quickSwitch/QuickSwitch.js | 153 + .../components/quickSwitch/QuickSwitch.styl | 209 + .../src/components/quickSwitch/SeriesList.js | 40 + .../components/quickSwitch/SeriesList.styl | 0 .../src/components/quickSwitch/StudiesItem.js | 60 + .../components/quickSwitch/StudiesItem.styl | 133 + .../src/components/quickSwitch/StudiesList.js | 37 + .../components/quickSwitch/StudiesList.styl | 0 .../quickSwitch/__docs__/quickSwitch.mdx | 61 + .../quickSwitch/__docs__/studies.js | 76 + .../src/components/quickSwitch/index.js | 1 + .../radioButtonList/RadioButtonList.css | 62 + .../radioButtonList/RadioButtonList.js | 74 + .../__docs__/radioButtonList.mdx | 74 + .../src/components/radioButtonList/index.js | 1 + .../roundedButtonGroup/RoundedButtonGroup.css | 86 + .../roundedButtonGroup/RoundedButtonGroup.js | 84 + .../__docs__/roundedButtonGroup.mdx | 51 + .../components/roundedButtonGroup/index.js | 1 + .../src/components/selectTree/InputRadio.js | 37 + .../src/components/selectTree/SelectTree.js | 182 + .../src/components/selectTree/SelectTree.styl | 106 + .../selectTree/SelectTreeBreadcrumb.js | 37 + .../selectTree/__docs__/selectTree.mdx | 42 + .../selectTree/__docs__/selectTreeItems.js | 30 + .../src/components/selectTree/index.js | 1 + .../components/simpleDialog/SimpleDialog.js | 69 + .../components/simpleDialog/SimpleDialog.styl | 108 + .../simpleDialog/__docs__/simpleDialog.mdx | 83 + .../src/components/simpleDialog/index.js | 1 + .../components/studyBrowser/DragPreview.js | 85 + .../components/studyBrowser/DragPreview.styl | 14 + .../studyBrowser/ExampleDropTarget.css | 30 + .../studyBrowser/ExampleDropTarget.js | 84 + .../components/studyBrowser/ImageThumbnail.js | 136 + .../studyBrowser/ImageThumbnail.styl | 70 + .../components/studyBrowser/StudyBrowser.js | 62 + .../components/studyBrowser/StudyBrowser.styl | 40 + .../components/studyBrowser/ThumbnailEntry.js | 113 + .../studyBrowser/ThumbnailEntry.styl | 125 + .../studyBrowser/ThumbnailEntryDragSource.js | 64 + .../studyBrowser/__docs__/exampleStudies.js | 75 + .../studyBrowser/__docs__/studyBrowser.mdx | 22 + .../__docs__/wrappedStudyBrowser.js | 40 + .../src/components/studyBrowser/index.js | 3 + .../studyList/CustomDateRangePicker.js | 99 + .../studyList/CustomDateRangePicker.styl | 84 + .../components/studyList/PaginationArea.js | 109 + .../components/studyList/PaginationArea.styl | 51 + .../src/components/studyList/StudyList.js | 428 + .../src/components/studyList/StudyList.styl | 282 + .../studyList/StudyListLoadingText.js | 12 + .../components/studyList/StudyListToolbar.js | 50 + .../studyList/StudyListToolbar.styl | 26 + .../components/studyList/__docs__/onSearch.js | 72 + .../components/studyList/__docs__/studies.js | 87 + .../studyList/__docs__/studyList.mdx | 53 + .../src/components/studyList/index.js | 1 + .../src/components/tableList/TableList.js | 37 + .../src/components/tableList/TableList.styl | 36 + .../src/components/tableList/TableListItem.js | 42 + .../components/tableList/TableListItem.styl | 46 + .../tableList/__docs__/tableList.mdx | 48 + .../src/components/tableList/index.js | 2 + .../toolbarSection/ToolbarSection.js | 65 + .../toolbarSection/ToolbarSection.styl | 4 + .../toolbarSection/__docs__/exampleButtons.js | 41 + .../__docs__/toolbarSection.docs.mdx | 39 + .../src/components/toolbarSection/index.js | 1 + .../src/components/tooltip/Tooltip.js | 66 + .../src/components/tooltip/Tooltip.styl | 92 + .../tooltip/__docs__/tooltip.noshow-mdx | 20 + .../src/components/tooltip/__tests__/.githold | 0 .../src/components/tooltip/index.js | 1 + .../userPreferencesModal/AboutModal.js | 105 + .../userPreferencesModal/AboutModal.styl | 20 + .../GeneralPreferences.js | 17 + .../HotKeysPreferences.js | 277 + .../HotKeysPreferences.styl | 14 + .../userPreferencesModal/UserPreferences.js | 118 + .../userPreferencesModal/UserPreferences.styl | 25 + .../UserPreferencesModal.js | 107 + .../UserPreferencesModal.styl | 17 + .../WindowLevelPreferences.js | 94 + .../WindowLevelPreferences.styl | 4 + .../userPreferencesModal/__docs__/about.mdx | 42 + .../__docs__/generalDefaults.js | 14 + .../__docs__/hotkeyDefaults.js | 80 + .../__docs__/userPreferences.mdx | 54 + .../__docs__/windowLevelDefaults.js | 13 + .../userPreferencesModal/hotKeysConfig.js | 100 + .../components/userPreferencesModal/index.js | 4 + .../src/design/styles/common/button.styl | 47 + .../src/design/styles/common/form.styl | 117 + .../src/design/styles/common/global.styl | 78 + .../src/design/styles/common/modal.styl | 202 + .../src/design/styles/common/navbar.styl | 57 + .../src/design/styles/common/state.styl | 25 + .../src/design/styles/common/table.styl | 15 + .../_components/src/elements/Icon/Icon.js | 15 + .../_components/src/elements/Icon/Icon.styl | 7 + .../src/elements/Icon/__docs__/icon.mdx | 84 + .../_components/src/elements/Icon/getIcon.js | 178 + .../src/elements/Icon/icons/3d-rotate.svg | 13 + .../src/elements/Icon/icons/adjust.svg | 11 + .../elements/Icon/icons/angle-double-down.svg | 11 + .../elements/Icon/icons/angle-double-up.svg | 11 + .../src/elements/Icon/icons/angle-left.svg | 11 + .../src/elements/Icon/icons/arrows-alt-h.svg | 11 + .../src/elements/Icon/icons/arrows-alt-v.svg | 11 + .../src/elements/Icon/icons/arrows.svg | 22 + .../src/elements/Icon/icons/bars.svg | 11 + .../src/elements/Icon/icons/brain.svg | 24 + .../src/elements/Icon/icons/caret-down.svg | 11 + .../src/elements/Icon/icons/caret-up.svg | 11 + .../elements/Icon/icons/check-circle-o.svg | 11 + .../src/elements/Icon/icons/check-circle.svg | 11 + .../src/elements/Icon/icons/check.svg | 11 + .../src/elements/Icon/icons/chevron-down.svg | 14 + .../src/elements/Icon/icons/circle-notch.svg | 11 + .../src/elements/Icon/icons/circle-o.svg | 11 + .../src/elements/Icon/icons/circle.svg | 11 + .../src/elements/Icon/icons/cog.svg | 11 + .../elements/Icon/icons/create-comment.svg | 12 + .../Icon/icons/create-screen-capture.svg | 15 + .../src/elements/Icon/icons/crosshairs.svg | 15 + .../src/elements/Icon/icons/cube.svg | 12 + .../src/elements/Icon/icons/database.svg | 11 + .../src/elements/Icon/icons/dot-circle.svg | 11 + .../src/elements/Icon/icons/edit.svg | 11 + .../elements/Icon/icons/ellipse-circle.svg | 11 + .../src/elements/Icon/icons/ellipse-h.svg | 11 + .../src/elements/Icon/icons/ellipse-v.svg | 11 + .../Icon/icons/exclamation-circle.svg | 13 + .../Icon/icons/exclamation-triangle.svg | 11 + .../src/elements/Icon/icons/fast-backward.svg | 10 + .../src/elements/Icon/icons/fast-forward.svg | 10 + .../src/elements/Icon/icons/info.svg | 11 + .../src/elements/Icon/icons/inline-edit.svg | 11 + .../src/elements/Icon/icons/level.svg | 13 + .../src/elements/Icon/icons/link-circles.svg | 15 + .../src/elements/Icon/icons/link.svg | 12 + .../src/elements/Icon/icons/list.svg | 11 + .../src/elements/Icon/icons/liver.svg | 16 + .../src/elements/Icon/icons/lock-alt.svg | 11 + .../src/elements/Icon/icons/lock.svg | 11 + .../src/elements/Icon/icons/lung.svg | 25 + .../Icon/icons/measure-non-target.svg | 29 + .../elements/Icon/icons/measure-target-cr.svg | 23 + .../elements/Icon/icons/measure-target-ne.svg | 16 + .../elements/Icon/icons/measure-target-un.svg | 23 + .../elements/Icon/icons/measure-target.svg | 13 + .../src/elements/Icon/icons/measure-temp.svg | 16 + .../src/elements/Icon/icons/object-group.svg | 20 + .../src/elements/Icon/icons/ohif-logo.svg | 17 + .../src/elements/Icon/icons/oval.svg | 11 + .../src/elements/Icon/icons/palette.svg | 11 + .../src/elements/Icon/icons/play.svg | 10 + .../src/elements/Icon/icons/plus.svg | 11 + .../src/elements/Icon/icons/power-off.svg | 11 + .../src/elements/Icon/icons/reset.svg | 11 + .../src/elements/Icon/icons/rotate-right.svg | 11 + .../src/elements/Icon/icons/rotate.svg | 11 + .../src/elements/Icon/icons/search-plus.svg | 14 + .../src/elements/Icon/icons/search.svg | 11 + .../src/elements/Icon/icons/soft-tissue.svg | 16 + .../src/elements/Icon/icons/sort-down.svg | 11 + .../src/elements/Icon/icons/sort-up.svg | 11 + .../src/elements/Icon/icons/sort.svg | 11 + .../src/elements/Icon/icons/square-o.svg | 11 + .../src/elements/Icon/icons/star.svg | 11 + .../src/elements/Icon/icons/step-backward.svg | 10 + .../src/elements/Icon/icons/step-forward.svg | 10 + .../src/elements/Icon/icons/stop.svg | 11 + .../src/elements/Icon/icons/sun.svg | 11 + .../src/elements/Icon/icons/th-large.svg | 12 + .../src/elements/Icon/icons/th-list.svg | 11 + .../src/elements/Icon/icons/th.svg | 11 + .../src/elements/Icon/icons/times.svg | 12 + .../src/elements/Icon/icons/trash.svg | 11 + .../src/elements/Icon/icons/user.svg | 11 + .../src/elements/Icon/icons/youtube.svg | 11 + .../_components/src/elements/Icon/index.js | 4 + packages/_components/src/elements/Svg/Svg.js | 14 + .../_components/src/elements/Svg/Svg.styl | 0 .../src/elements/Svg/__docs__/svg.mdx | 57 + .../_components/src/elements/Svg/getSvg.js | 24 + .../_components/src/elements/Svg/index.js | 2 + .../src/elements/Svg/svgs/ohif-logo-text.svg | 3 + .../Svg/svgs/ohif-logo-wrapped-text.svg | 14 + .../src/elements/form/DropdownMenu.css | 86 + .../src/elements/form/DropdownMenu.js | 133 + .../_components/src/elements/form/Label.css | 8 + .../_components/src/elements/form/Label.js | 27 + .../_components/src/elements/form/Range.css | 64 + .../_components/src/elements/form/Range.js | 40 + .../_components/src/elements/form/Select.css | 46 + .../_components/src/elements/form/Select.js | 48 + .../src/elements/form/TextArea.css | 7 + .../_components/src/elements/form/TextArea.js | 39 + .../src/elements/form/TextInput.css | 7 + .../src/elements/form/TextInput.js | 36 + .../form/__docs__/dropdownMenu.doc.mdx | 42 + .../src/elements/form/__docs__/label.doc.mdx | 32 + .../src/elements/form/__docs__/range.doc.mdx | 33 + .../elements/form/__docs__/select.docs.mdx | 39 + .../elements/form/__docs__/textArea.doc.mdx | 29 + .../elements/form/__docs__/textInput.docs.mdx | 27 + .../_components/src/elements/form/index.js | 6 + packages/_components/src/elements/index.js | 3 + packages/_components/src/index.js | 73 + packages/_components/src/test.js | 46 + .../_components/src/utils/LanguageProvider.js | 28 + .../_components/src/utils/getScrollbarSize.js | 41 + .../_components/src/utils/styleProperty.js | 57 + packages/_components/src/utils/throttled.js | 26 + .../src/utils/viewerbaseDragDropContext.js | 13 + .../src/viewer/ExpandableToolMenu.js | 125 + .../src/viewer/ExpandableToolMenu.styl | 136 + .../_components/src/viewer/PlayClipButton.js | 45 + .../_components/src/viewer/PresetToggle.js | 78 + .../src/viewer/SimpleToolbarButton.js | 31 + packages/_components/src/viewer/Toolbar.js | 151 + .../_components/src/viewer/ToolbarButton.js | 64 + .../src/viewer/ViewportErrorIndicator.js | 30 + .../src/viewer/ViewportLoadingIndicator.js | 28 + .../src/viewer/toolbar-button.styl | 33 + packages/_components/yarn.lock | 19594 ++++++++++++++++ .../src/index.js | 0 .../.babelrc | 0 .../.editorconfig | 0 .../extensions/ohif-i18n => _i18n}/.gitignore | 0 .../LICENSE | 0 .../extensions/ohif-i18n => _i18n}/README.md | 0 .../ohif-i18n => _i18n}/package.json | 0 .../ohif-i18n => _i18n}/pullTranslations.sh | 0 .../ohif-i18n => _i18n}/rollup.config.js | 0 .../ohif-i18n => _i18n}/src/config.js | 0 .../ohif-i18n => _i18n}/src/debugger.js | 0 .../ohif-i18n => _i18n}/src/index.js | 0 .../src/locales/en-US/Buttons.json | 0 .../src/locales/en-US/CineDialog.json | 0 .../src/locales/en-US/Common.json | 0 .../src/locales/en-US/Header.json | 0 .../src/locales/en-US/MeasurementTable.json | 0 .../locales/en-US/UserPreferencesModal.json | 0 .../src/locales/en-US/index.js | 0 .../src/locales/es/Buttons.json | 0 .../src/locales/es/CineDialog.json | 0 .../src/locales/es/Common.json | 0 .../src/locales/es/Header.json | 0 .../src/locales/es/MeasurementTable.json | 0 .../src/locales/es/UserPreferencesModal.json | 0 .../src/locales/es/index.js | 0 .../ohif-i18n => _i18n}/src/locales/index.js | 0 .../src/locales/nl/Buttons.json | 0 .../src/locales/nl/Common.json | 0 .../src/locales/nl/Header.json | 0 .../src/locales/nl/index.js | 0 .../writeLocaleIndexFiles.js | 0 .../extensions/ohif-i18n => _i18n}/yarn.lock | 0 .../.babelrc | 0 .../.editorconfig | 0 .../.eslintrc | 0 .../.gitignore | 0 .../.prettierrc | 0 .../.travis.yml | 0 .../LICENSE | 0 .../README.md | 0 .../package.json | 0 .../rollup.config.js | 0 .../src/ConnectedCornerstoneViewport.js | 0 .../src/OHIFCornerstoneViewport.js | 0 .../src/commandsModule.js | 0 .../src/handleSegmentationStorage.js | 0 .../src/index.js | 0 .../src/toolbarModule.js | 0 .../yarn.lock | 0 .../.babelrc | 0 .../.editorconfig | 0 .../.eslintrc | 0 .../.gitignore | 0 .../.prettierrc | 0 .../.travis.yml | 0 .../LICENSE | 0 .../package.json | 0 .../rollup.config.js | 0 .../src/DicomHtmlViewport.css | 0 .../src/DicomHtmlViewport.js | 0 .../src/OHIFDicomHtmlSopClassHandler.js | 0 .../src/OHIFDicomHtmlViewport.js | 0 .../src/TypedArrayProp.js | 0 .../src/index.js | 0 .../yarn.lock | 0 .../.babelrc | 0 .../.editorconfig | 0 .../.eslintrc | 0 .../.gitignore | 0 .../.prettierrc | 0 .../.travis.yml | 0 .../LICENSE | 0 .../README.md | 0 .../package.json | 0 .../rollup.config.js | 0 .../src/DicomMicroscopySopClassHandler.js | 0 .../src/DicomMicroscopyViewport.js | 0 .../src/index.js | 0 .../yarn.lock | 0 .../.babelrc | 0 .../.editorconfig | 0 .../.eslintrc | 0 .../.gitignore | 0 .../.prettierrc | 0 .../.travis.yml | 0 .../LICENSE | 0 .../package.json | 0 .../rollup.config.js | 0 .../src/DicomPDFViewport.js | 0 .../src/OHIFComponentPlugin.js | 0 .../src/OHIFDicomPDFSopClassHandler.js | 0 .../src/OHIFDicomPDFViewport.js | 0 .../src/TypedArrayProp.js | 0 .../src/index.js | 0 .../yarn.lock | 0 .../.babelrc | 0 packages/extension-vtk/.editorconfig | 9 + .../.eslintrc | 0 .../.gitignore | 0 packages/extension-vtk/.prettierrc | 9 + .../.travis.yml | 0 packages/extension-vtk/LICENSE | 21 + .../README.md | 0 .../package.json | 0 .../rollup.config.js | 0 .../src/ConnectedVTKViewport.js | 0 .../src/LoadingIndicator.css | 0 .../src/LoadingIndicator.js | 0 .../src/OHIFVTKViewport.js | 0 .../src/commandsModule.js | 0 .../src/handleSegmentationStorage.js | 0 .../src/index.js | 0 .../src/loadLocales.js | 0 .../src/locales/en/Buttons.json | 0 .../src/locales/en/index.js | 0 .../src/locales/es/Buttons.json | 0 .../src/locales/es/index.js | 0 .../src/locales/index.js | 0 .../src/toolbarModule.js | 0 .../src/utils/setLayoutAndViewportData.js | 0 .../src/utils/setMPRLayout.js | 0 .../src/utils/setSingleLayoutData.js | 0 .../src/utils/setViewportToVTK.js | 0 .../webpack.config.js | 0 .../yarn.lock | 0 yarn.lock | 17837 ++++++++++++++ 432 files changed, 51231 insertions(+) create mode 100644 packages/_components/.all-contributorsrc create mode 100644 packages/_components/.babelrc create mode 100644 packages/_components/.circleci/config.yml rename packages/{_viewer/extensions/ohif-cornerstone-extension => _components}/.editorconfig (100%) create mode 100644 packages/_components/.env create mode 100644 packages/_components/.eslintrc create mode 100644 packages/_components/.gitattributes create mode 100644 packages/_components/.github/ISSUE_TEMPLATE/---bug-report.md create mode 100644 packages/_components/.github/ISSUE_TEMPLATE/---feature-request.md create mode 100644 packages/_components/.github/ISSUE_TEMPLATE/---support-question.md create mode 100644 packages/_components/.gitignore create mode 100644 packages/_components/.nvmrc rename packages/{_viewer/extensions/ohif-cornerstone-extension => _components}/.prettierrc (100%) create mode 100644 packages/_components/.vscode/extensions.json create mode 100644 packages/_components/.vscode/settings.json rename packages/{_viewer/extensions/ohif-cornerstone-extension => _components}/LICENSE (100%) create mode 100644 packages/_components/README.md create mode 100644 packages/_components/doczrc.js create mode 100644 packages/_components/netlify.toml create mode 100644 packages/_components/package.json create mode 100644 packages/_components/public/docz.css create mode 100644 packages/_components/rollup.config.js create mode 100644 packages/_components/src/LayoutChooser/LayoutManager.css create mode 100644 packages/_components/src/LayoutChooser/LayoutManager.js create mode 100644 packages/_components/src/LayoutChooser/LayoutPanelDropTarget.css create mode 100644 packages/_components/src/LayoutChooser/LayoutPanelDropTarget.js create mode 100644 packages/_components/src/ScrollableArea/ScrollableArea.js create mode 100644 packages/_components/src/ScrollableArea/ScrollableArea.styl create mode 100644 packages/_components/src/__docs__/NameSpace.js create mode 100644 packages/_components/src/__docs__/assets/github.png create mode 100644 packages/_components/src/__docs__/assets/npm.png create mode 100644 packages/_components/src/__docs__/assets/react.png create mode 100644 packages/_components/src/__docs__/assets/typescript.png create mode 100644 packages/_components/src/__docs__/compatibility.mdx create mode 100644 packages/_components/src/__docs__/config/Wrapper.js create mode 100644 packages/_components/src/__docs__/docs-settings.mdx create mode 100644 packages/_components/src/__docs__/getting-started.mdx create mode 100644 packages/_components/src/__docs__/index.html create mode 100644 packages/_components/src/__docs__/introduction.mdx create mode 100644 packages/_components/src/__docs__/styling-and-theming.mdx create mode 100644 packages/_components/src/__docs__/translating.mdx create mode 100644 packages/_components/src/__docs__/wrapper.js create mode 100644 packages/_components/src/__docs__/wrapper.styl create mode 100644 packages/_components/src/components/checkbox/__docs__/checkbox.mdx create mode 100644 packages/_components/src/components/checkbox/checkbox.css create mode 100644 packages/_components/src/components/checkbox/checkbox.js create mode 100644 packages/_components/src/components/checkbox/index.js create mode 100644 packages/_components/src/components/cineDialog/CineDialog.js create mode 100644 packages/_components/src/components/cineDialog/CineDialog.styl create mode 100644 packages/_components/src/components/cineDialog/__docs__/cineDialog.mdx create mode 100644 packages/_components/src/components/cineDialog/index.js create mode 100644 packages/_components/src/components/index.js create mode 100644 packages/_components/src/components/languageSwitcher/LanguageSwitcher.js create mode 100644 packages/_components/src/components/languageSwitcher/LanguageSwitcher.styl create mode 100644 packages/_components/src/components/languageSwitcher/index.js create mode 100644 packages/_components/src/components/layoutButton/LayoutButton.js create mode 100644 packages/_components/src/components/layoutButton/LayoutChooser.js create mode 100644 packages/_components/src/components/layoutButton/LayoutChooser.styl create mode 100644 packages/_components/src/components/layoutButton/__docs__/layoutButton.css create mode 100644 packages/_components/src/components/layoutButton/__docs__/layoutButton.mdx create mode 100644 packages/_components/src/components/layoutButton/index.js create mode 100644 packages/_components/src/components/measurementTable/MeasurementTable.js create mode 100644 packages/_components/src/components/measurementTable/MeasurementTable.styl create mode 100644 packages/_components/src/components/measurementTable/MeasurementTableItem.js create mode 100644 packages/_components/src/components/measurementTable/MeasurementTableItem.styl create mode 100644 packages/_components/src/components/measurementTable/__docs__/measurementTable.mdx create mode 100644 packages/_components/src/components/measurementTable/__docs__/measurements.js create mode 100644 packages/_components/src/components/measurementTable/__docs__/timepoints.js create mode 100644 packages/_components/src/components/measurementTable/__docs__/warnings.js create mode 100644 packages/_components/src/components/measurementTable/index.js create mode 100644 packages/_components/src/components/overlayTrigger/Fade.js create mode 100644 packages/_components/src/components/overlayTrigger/Overlay.js create mode 100644 packages/_components/src/components/overlayTrigger/OverlayTrigger.js create mode 100644 packages/_components/src/components/overlayTrigger/__docs__/OverlayTrigger.mdx create mode 100644 packages/_components/src/components/overlayTrigger/createChainedFunction.js create mode 100644 packages/_components/src/components/overlayTrigger/index.js create mode 100644 packages/_components/src/components/quickSwitch/QuickSwitch.js create mode 100644 packages/_components/src/components/quickSwitch/QuickSwitch.styl create mode 100644 packages/_components/src/components/quickSwitch/SeriesList.js create mode 100644 packages/_components/src/components/quickSwitch/SeriesList.styl create mode 100644 packages/_components/src/components/quickSwitch/StudiesItem.js create mode 100644 packages/_components/src/components/quickSwitch/StudiesItem.styl create mode 100644 packages/_components/src/components/quickSwitch/StudiesList.js create mode 100644 packages/_components/src/components/quickSwitch/StudiesList.styl create mode 100644 packages/_components/src/components/quickSwitch/__docs__/quickSwitch.mdx create mode 100644 packages/_components/src/components/quickSwitch/__docs__/studies.js create mode 100644 packages/_components/src/components/quickSwitch/index.js create mode 100644 packages/_components/src/components/radioButtonList/RadioButtonList.css create mode 100644 packages/_components/src/components/radioButtonList/RadioButtonList.js create mode 100644 packages/_components/src/components/radioButtonList/__docs__/radioButtonList.mdx create mode 100644 packages/_components/src/components/radioButtonList/index.js create mode 100644 packages/_components/src/components/roundedButtonGroup/RoundedButtonGroup.css create mode 100644 packages/_components/src/components/roundedButtonGroup/RoundedButtonGroup.js create mode 100644 packages/_components/src/components/roundedButtonGroup/__docs__/roundedButtonGroup.mdx create mode 100644 packages/_components/src/components/roundedButtonGroup/index.js create mode 100644 packages/_components/src/components/selectTree/InputRadio.js create mode 100644 packages/_components/src/components/selectTree/SelectTree.js create mode 100644 packages/_components/src/components/selectTree/SelectTree.styl create mode 100644 packages/_components/src/components/selectTree/SelectTreeBreadcrumb.js create mode 100644 packages/_components/src/components/selectTree/__docs__/selectTree.mdx create mode 100644 packages/_components/src/components/selectTree/__docs__/selectTreeItems.js create mode 100644 packages/_components/src/components/selectTree/index.js create mode 100644 packages/_components/src/components/simpleDialog/SimpleDialog.js create mode 100644 packages/_components/src/components/simpleDialog/SimpleDialog.styl create mode 100644 packages/_components/src/components/simpleDialog/__docs__/simpleDialog.mdx create mode 100644 packages/_components/src/components/simpleDialog/index.js create mode 100644 packages/_components/src/components/studyBrowser/DragPreview.js create mode 100644 packages/_components/src/components/studyBrowser/DragPreview.styl create mode 100644 packages/_components/src/components/studyBrowser/ExampleDropTarget.css create mode 100644 packages/_components/src/components/studyBrowser/ExampleDropTarget.js create mode 100644 packages/_components/src/components/studyBrowser/ImageThumbnail.js create mode 100644 packages/_components/src/components/studyBrowser/ImageThumbnail.styl create mode 100644 packages/_components/src/components/studyBrowser/StudyBrowser.js create mode 100644 packages/_components/src/components/studyBrowser/StudyBrowser.styl create mode 100644 packages/_components/src/components/studyBrowser/ThumbnailEntry.js create mode 100644 packages/_components/src/components/studyBrowser/ThumbnailEntry.styl create mode 100644 packages/_components/src/components/studyBrowser/ThumbnailEntryDragSource.js create mode 100644 packages/_components/src/components/studyBrowser/__docs__/exampleStudies.js create mode 100644 packages/_components/src/components/studyBrowser/__docs__/studyBrowser.mdx create mode 100644 packages/_components/src/components/studyBrowser/__docs__/wrappedStudyBrowser.js create mode 100644 packages/_components/src/components/studyBrowser/index.js create mode 100644 packages/_components/src/components/studyList/CustomDateRangePicker.js create mode 100644 packages/_components/src/components/studyList/CustomDateRangePicker.styl create mode 100644 packages/_components/src/components/studyList/PaginationArea.js create mode 100644 packages/_components/src/components/studyList/PaginationArea.styl create mode 100644 packages/_components/src/components/studyList/StudyList.js create mode 100644 packages/_components/src/components/studyList/StudyList.styl create mode 100644 packages/_components/src/components/studyList/StudyListLoadingText.js create mode 100644 packages/_components/src/components/studyList/StudyListToolbar.js create mode 100644 packages/_components/src/components/studyList/StudyListToolbar.styl create mode 100644 packages/_components/src/components/studyList/__docs__/onSearch.js create mode 100644 packages/_components/src/components/studyList/__docs__/studies.js create mode 100644 packages/_components/src/components/studyList/__docs__/studyList.mdx create mode 100644 packages/_components/src/components/studyList/index.js create mode 100644 packages/_components/src/components/tableList/TableList.js create mode 100644 packages/_components/src/components/tableList/TableList.styl create mode 100644 packages/_components/src/components/tableList/TableListItem.js create mode 100644 packages/_components/src/components/tableList/TableListItem.styl create mode 100644 packages/_components/src/components/tableList/__docs__/tableList.mdx create mode 100644 packages/_components/src/components/tableList/index.js create mode 100644 packages/_components/src/components/toolbarSection/ToolbarSection.js create mode 100644 packages/_components/src/components/toolbarSection/ToolbarSection.styl create mode 100644 packages/_components/src/components/toolbarSection/__docs__/exampleButtons.js create mode 100644 packages/_components/src/components/toolbarSection/__docs__/toolbarSection.docs.mdx create mode 100644 packages/_components/src/components/toolbarSection/index.js create mode 100644 packages/_components/src/components/tooltip/Tooltip.js create mode 100644 packages/_components/src/components/tooltip/Tooltip.styl create mode 100644 packages/_components/src/components/tooltip/__docs__/tooltip.noshow-mdx create mode 100644 packages/_components/src/components/tooltip/__tests__/.githold create mode 100644 packages/_components/src/components/tooltip/index.js create mode 100644 packages/_components/src/components/userPreferencesModal/AboutModal.js create mode 100644 packages/_components/src/components/userPreferencesModal/AboutModal.styl create mode 100644 packages/_components/src/components/userPreferencesModal/GeneralPreferences.js create mode 100644 packages/_components/src/components/userPreferencesModal/HotKeysPreferences.js create mode 100644 packages/_components/src/components/userPreferencesModal/HotKeysPreferences.styl create mode 100644 packages/_components/src/components/userPreferencesModal/UserPreferences.js create mode 100644 packages/_components/src/components/userPreferencesModal/UserPreferences.styl create mode 100644 packages/_components/src/components/userPreferencesModal/UserPreferencesModal.js create mode 100644 packages/_components/src/components/userPreferencesModal/UserPreferencesModal.styl create mode 100644 packages/_components/src/components/userPreferencesModal/WindowLevelPreferences.js create mode 100644 packages/_components/src/components/userPreferencesModal/WindowLevelPreferences.styl create mode 100644 packages/_components/src/components/userPreferencesModal/__docs__/about.mdx create mode 100644 packages/_components/src/components/userPreferencesModal/__docs__/generalDefaults.js create mode 100644 packages/_components/src/components/userPreferencesModal/__docs__/hotkeyDefaults.js create mode 100644 packages/_components/src/components/userPreferencesModal/__docs__/userPreferences.mdx create mode 100644 packages/_components/src/components/userPreferencesModal/__docs__/windowLevelDefaults.js create mode 100644 packages/_components/src/components/userPreferencesModal/hotKeysConfig.js create mode 100644 packages/_components/src/components/userPreferencesModal/index.js create mode 100644 packages/_components/src/design/styles/common/button.styl create mode 100644 packages/_components/src/design/styles/common/form.styl create mode 100644 packages/_components/src/design/styles/common/global.styl create mode 100644 packages/_components/src/design/styles/common/modal.styl create mode 100644 packages/_components/src/design/styles/common/navbar.styl create mode 100644 packages/_components/src/design/styles/common/state.styl create mode 100644 packages/_components/src/design/styles/common/table.styl create mode 100644 packages/_components/src/elements/Icon/Icon.js create mode 100644 packages/_components/src/elements/Icon/Icon.styl create mode 100644 packages/_components/src/elements/Icon/__docs__/icon.mdx create mode 100644 packages/_components/src/elements/Icon/getIcon.js create mode 100644 packages/_components/src/elements/Icon/icons/3d-rotate.svg create mode 100644 packages/_components/src/elements/Icon/icons/adjust.svg create mode 100644 packages/_components/src/elements/Icon/icons/angle-double-down.svg create mode 100644 packages/_components/src/elements/Icon/icons/angle-double-up.svg create mode 100644 packages/_components/src/elements/Icon/icons/angle-left.svg create mode 100644 packages/_components/src/elements/Icon/icons/arrows-alt-h.svg create mode 100644 packages/_components/src/elements/Icon/icons/arrows-alt-v.svg create mode 100644 packages/_components/src/elements/Icon/icons/arrows.svg create mode 100644 packages/_components/src/elements/Icon/icons/bars.svg create mode 100644 packages/_components/src/elements/Icon/icons/brain.svg create mode 100644 packages/_components/src/elements/Icon/icons/caret-down.svg create mode 100644 packages/_components/src/elements/Icon/icons/caret-up.svg create mode 100644 packages/_components/src/elements/Icon/icons/check-circle-o.svg create mode 100644 packages/_components/src/elements/Icon/icons/check-circle.svg create mode 100644 packages/_components/src/elements/Icon/icons/check.svg create mode 100644 packages/_components/src/elements/Icon/icons/chevron-down.svg create mode 100644 packages/_components/src/elements/Icon/icons/circle-notch.svg create mode 100644 packages/_components/src/elements/Icon/icons/circle-o.svg create mode 100644 packages/_components/src/elements/Icon/icons/circle.svg create mode 100644 packages/_components/src/elements/Icon/icons/cog.svg create mode 100644 packages/_components/src/elements/Icon/icons/create-comment.svg create mode 100644 packages/_components/src/elements/Icon/icons/create-screen-capture.svg create mode 100644 packages/_components/src/elements/Icon/icons/crosshairs.svg create mode 100644 packages/_components/src/elements/Icon/icons/cube.svg create mode 100644 packages/_components/src/elements/Icon/icons/database.svg create mode 100644 packages/_components/src/elements/Icon/icons/dot-circle.svg create mode 100644 packages/_components/src/elements/Icon/icons/edit.svg create mode 100644 packages/_components/src/elements/Icon/icons/ellipse-circle.svg create mode 100644 packages/_components/src/elements/Icon/icons/ellipse-h.svg create mode 100644 packages/_components/src/elements/Icon/icons/ellipse-v.svg create mode 100644 packages/_components/src/elements/Icon/icons/exclamation-circle.svg create mode 100644 packages/_components/src/elements/Icon/icons/exclamation-triangle.svg create mode 100644 packages/_components/src/elements/Icon/icons/fast-backward.svg create mode 100644 packages/_components/src/elements/Icon/icons/fast-forward.svg create mode 100644 packages/_components/src/elements/Icon/icons/info.svg create mode 100644 packages/_components/src/elements/Icon/icons/inline-edit.svg create mode 100644 packages/_components/src/elements/Icon/icons/level.svg create mode 100644 packages/_components/src/elements/Icon/icons/link-circles.svg create mode 100644 packages/_components/src/elements/Icon/icons/link.svg create mode 100644 packages/_components/src/elements/Icon/icons/list.svg create mode 100644 packages/_components/src/elements/Icon/icons/liver.svg create mode 100644 packages/_components/src/elements/Icon/icons/lock-alt.svg create mode 100644 packages/_components/src/elements/Icon/icons/lock.svg create mode 100644 packages/_components/src/elements/Icon/icons/lung.svg create mode 100644 packages/_components/src/elements/Icon/icons/measure-non-target.svg create mode 100644 packages/_components/src/elements/Icon/icons/measure-target-cr.svg create mode 100644 packages/_components/src/elements/Icon/icons/measure-target-ne.svg create mode 100644 packages/_components/src/elements/Icon/icons/measure-target-un.svg create mode 100644 packages/_components/src/elements/Icon/icons/measure-target.svg create mode 100644 packages/_components/src/elements/Icon/icons/measure-temp.svg create mode 100644 packages/_components/src/elements/Icon/icons/object-group.svg create mode 100644 packages/_components/src/elements/Icon/icons/ohif-logo.svg create mode 100644 packages/_components/src/elements/Icon/icons/oval.svg create mode 100644 packages/_components/src/elements/Icon/icons/palette.svg create mode 100644 packages/_components/src/elements/Icon/icons/play.svg create mode 100644 packages/_components/src/elements/Icon/icons/plus.svg create mode 100644 packages/_components/src/elements/Icon/icons/power-off.svg create mode 100644 packages/_components/src/elements/Icon/icons/reset.svg create mode 100644 packages/_components/src/elements/Icon/icons/rotate-right.svg create mode 100644 packages/_components/src/elements/Icon/icons/rotate.svg create mode 100644 packages/_components/src/elements/Icon/icons/search-plus.svg create mode 100644 packages/_components/src/elements/Icon/icons/search.svg create mode 100644 packages/_components/src/elements/Icon/icons/soft-tissue.svg create mode 100644 packages/_components/src/elements/Icon/icons/sort-down.svg create mode 100644 packages/_components/src/elements/Icon/icons/sort-up.svg create mode 100644 packages/_components/src/elements/Icon/icons/sort.svg create mode 100644 packages/_components/src/elements/Icon/icons/square-o.svg create mode 100644 packages/_components/src/elements/Icon/icons/star.svg create mode 100644 packages/_components/src/elements/Icon/icons/step-backward.svg create mode 100644 packages/_components/src/elements/Icon/icons/step-forward.svg create mode 100644 packages/_components/src/elements/Icon/icons/stop.svg create mode 100644 packages/_components/src/elements/Icon/icons/sun.svg create mode 100644 packages/_components/src/elements/Icon/icons/th-large.svg create mode 100644 packages/_components/src/elements/Icon/icons/th-list.svg create mode 100644 packages/_components/src/elements/Icon/icons/th.svg create mode 100644 packages/_components/src/elements/Icon/icons/times.svg create mode 100644 packages/_components/src/elements/Icon/icons/trash.svg create mode 100644 packages/_components/src/elements/Icon/icons/user.svg create mode 100644 packages/_components/src/elements/Icon/icons/youtube.svg create mode 100644 packages/_components/src/elements/Icon/index.js create mode 100644 packages/_components/src/elements/Svg/Svg.js create mode 100644 packages/_components/src/elements/Svg/Svg.styl create mode 100644 packages/_components/src/elements/Svg/__docs__/svg.mdx create mode 100644 packages/_components/src/elements/Svg/getSvg.js create mode 100644 packages/_components/src/elements/Svg/index.js create mode 100644 packages/_components/src/elements/Svg/svgs/ohif-logo-text.svg create mode 100644 packages/_components/src/elements/Svg/svgs/ohif-logo-wrapped-text.svg create mode 100644 packages/_components/src/elements/form/DropdownMenu.css create mode 100644 packages/_components/src/elements/form/DropdownMenu.js create mode 100644 packages/_components/src/elements/form/Label.css create mode 100644 packages/_components/src/elements/form/Label.js create mode 100644 packages/_components/src/elements/form/Range.css create mode 100644 packages/_components/src/elements/form/Range.js create mode 100644 packages/_components/src/elements/form/Select.css create mode 100644 packages/_components/src/elements/form/Select.js create mode 100644 packages/_components/src/elements/form/TextArea.css create mode 100644 packages/_components/src/elements/form/TextArea.js create mode 100644 packages/_components/src/elements/form/TextInput.css create mode 100644 packages/_components/src/elements/form/TextInput.js create mode 100644 packages/_components/src/elements/form/__docs__/dropdownMenu.doc.mdx create mode 100644 packages/_components/src/elements/form/__docs__/label.doc.mdx create mode 100644 packages/_components/src/elements/form/__docs__/range.doc.mdx create mode 100644 packages/_components/src/elements/form/__docs__/select.docs.mdx create mode 100644 packages/_components/src/elements/form/__docs__/textArea.doc.mdx create mode 100644 packages/_components/src/elements/form/__docs__/textInput.docs.mdx create mode 100644 packages/_components/src/elements/form/index.js create mode 100644 packages/_components/src/elements/index.js create mode 100644 packages/_components/src/index.js create mode 100644 packages/_components/src/test.js create mode 100644 packages/_components/src/utils/LanguageProvider.js create mode 100644 packages/_components/src/utils/getScrollbarSize.js create mode 100644 packages/_components/src/utils/styleProperty.js create mode 100644 packages/_components/src/utils/throttled.js create mode 100644 packages/_components/src/utils/viewerbaseDragDropContext.js create mode 100644 packages/_components/src/viewer/ExpandableToolMenu.js create mode 100644 packages/_components/src/viewer/ExpandableToolMenu.styl create mode 100644 packages/_components/src/viewer/PlayClipButton.js create mode 100644 packages/_components/src/viewer/PresetToggle.js create mode 100644 packages/_components/src/viewer/SimpleToolbarButton.js create mode 100644 packages/_components/src/viewer/Toolbar.js create mode 100644 packages/_components/src/viewer/ToolbarButton.js create mode 100644 packages/_components/src/viewer/ViewportErrorIndicator.js create mode 100644 packages/_components/src/viewer/ViewportLoadingIndicator.js create mode 100644 packages/_components/src/viewer/toolbar-button.styl create mode 100644 packages/_components/yarn.lock rename packages/{_viewer/extensions/_ohif-example-extension => _extension-example}/src/index.js (100%) rename packages/{_viewer/extensions/ohif-cornerstone-extension => _i18n}/.babelrc (100%) rename packages/{_viewer/extensions/ohif-dicom-html-extension => _i18n}/.editorconfig (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/.gitignore (100%) rename packages/{_viewer/extensions/ohif-dicom-html-extension => _i18n}/LICENSE (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/README.md (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/package.json (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/pullTranslations.sh (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/rollup.config.js (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/src/config.js (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/src/debugger.js (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/src/index.js (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/src/locales/en-US/Buttons.json (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/src/locales/en-US/CineDialog.json (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/src/locales/en-US/Common.json (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/src/locales/en-US/Header.json (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/src/locales/en-US/MeasurementTable.json (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/src/locales/en-US/UserPreferencesModal.json (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/src/locales/en-US/index.js (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/src/locales/es/Buttons.json (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/src/locales/es/CineDialog.json (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/src/locales/es/Common.json (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/src/locales/es/Header.json (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/src/locales/es/MeasurementTable.json (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/src/locales/es/UserPreferencesModal.json (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/src/locales/es/index.js (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/src/locales/index.js (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/src/locales/nl/Buttons.json (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/src/locales/nl/Common.json (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/src/locales/nl/Header.json (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/src/locales/nl/index.js (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/writeLocaleIndexFiles.js (100%) rename packages/{_viewer/extensions/ohif-i18n => _i18n}/yarn.lock (100%) rename packages/{_viewer/extensions/ohif-dicom-html-extension => extension-cornerstone}/.babelrc (100%) rename packages/{_viewer/extensions/ohif-dicom-microscopy-extension => extension-cornerstone}/.editorconfig (100%) rename packages/{_viewer/extensions/ohif-cornerstone-extension => extension-cornerstone}/.eslintrc (100%) rename packages/{_viewer/extensions/ohif-cornerstone-extension => extension-cornerstone}/.gitignore (100%) rename packages/{_viewer/extensions/ohif-dicom-html-extension => extension-cornerstone}/.prettierrc (100%) rename packages/{_viewer/extensions/ohif-cornerstone-extension => extension-cornerstone}/.travis.yml (100%) rename packages/{_viewer/extensions/ohif-dicom-microscopy-extension => extension-cornerstone}/LICENSE (100%) rename packages/{_viewer/extensions/ohif-cornerstone-extension => extension-cornerstone}/README.md (100%) rename packages/{_viewer/extensions/ohif-cornerstone-extension => extension-cornerstone}/package.json (100%) rename packages/{_viewer/extensions/ohif-cornerstone-extension => extension-cornerstone}/rollup.config.js (100%) rename packages/{_viewer/extensions/ohif-cornerstone-extension => extension-cornerstone}/src/ConnectedCornerstoneViewport.js (100%) rename packages/{_viewer/extensions/ohif-cornerstone-extension => extension-cornerstone}/src/OHIFCornerstoneViewport.js (100%) rename packages/{_viewer/extensions/ohif-cornerstone-extension => extension-cornerstone}/src/commandsModule.js (100%) rename packages/{_viewer/extensions/ohif-cornerstone-extension => extension-cornerstone}/src/handleSegmentationStorage.js (100%) rename packages/{_viewer/extensions/ohif-cornerstone-extension => extension-cornerstone}/src/index.js (100%) rename packages/{_viewer/extensions/ohif-cornerstone-extension => extension-cornerstone}/src/toolbarModule.js (100%) rename packages/{_viewer/extensions/ohif-cornerstone-extension => extension-cornerstone}/yarn.lock (100%) rename packages/{_viewer/extensions/ohif-dicom-microscopy-extension => extension-dicom-html}/.babelrc (100%) rename packages/{_viewer/extensions/ohif-dicom-pdf-extension => extension-dicom-html}/.editorconfig (100%) rename packages/{_viewer/extensions/ohif-dicom-html-extension => extension-dicom-html}/.eslintrc (100%) rename packages/{_viewer/extensions/ohif-dicom-html-extension => extension-dicom-html}/.gitignore (100%) rename packages/{_viewer/extensions/ohif-dicom-microscopy-extension => extension-dicom-html}/.prettierrc (100%) rename packages/{_viewer/extensions/ohif-dicom-html-extension => extension-dicom-html}/.travis.yml (100%) rename packages/{_viewer/extensions/ohif-dicom-pdf-extension => extension-dicom-html}/LICENSE (100%) rename packages/{_viewer/extensions/ohif-dicom-html-extension => extension-dicom-html}/package.json (100%) rename packages/{_viewer/extensions/ohif-dicom-html-extension => extension-dicom-html}/rollup.config.js (100%) rename packages/{_viewer/extensions/ohif-dicom-html-extension => extension-dicom-html}/src/DicomHtmlViewport.css (100%) rename packages/{_viewer/extensions/ohif-dicom-html-extension => extension-dicom-html}/src/DicomHtmlViewport.js (100%) rename packages/{_viewer/extensions/ohif-dicom-html-extension => extension-dicom-html}/src/OHIFDicomHtmlSopClassHandler.js (100%) rename packages/{_viewer/extensions/ohif-dicom-html-extension => extension-dicom-html}/src/OHIFDicomHtmlViewport.js (100%) rename packages/{_viewer/extensions/ohif-dicom-html-extension => extension-dicom-html}/src/TypedArrayProp.js (100%) rename packages/{_viewer/extensions/ohif-dicom-html-extension => extension-dicom-html}/src/index.js (100%) rename packages/{_viewer/extensions/ohif-dicom-html-extension => extension-dicom-html}/yarn.lock (100%) rename packages/{_viewer/extensions/ohif-dicom-pdf-extension => extension-dicom-microscopy}/.babelrc (100%) rename packages/{_viewer/extensions/ohif-i18n => extension-dicom-microscopy}/.editorconfig (100%) rename packages/{_viewer/extensions/ohif-dicom-microscopy-extension => extension-dicom-microscopy}/.eslintrc (100%) rename packages/{_viewer/extensions/ohif-dicom-microscopy-extension => extension-dicom-microscopy}/.gitignore (100%) rename packages/{_viewer/extensions/ohif-dicom-pdf-extension => extension-dicom-microscopy}/.prettierrc (100%) rename packages/{_viewer/extensions/ohif-dicom-microscopy-extension => extension-dicom-microscopy}/.travis.yml (100%) rename packages/{_viewer/extensions/ohif-i18n => extension-dicom-microscopy}/LICENSE (100%) rename packages/{_viewer/extensions/ohif-dicom-microscopy-extension => extension-dicom-microscopy}/README.md (100%) rename packages/{_viewer/extensions/ohif-dicom-microscopy-extension => extension-dicom-microscopy}/package.json (100%) rename packages/{_viewer/extensions/ohif-dicom-microscopy-extension => extension-dicom-microscopy}/rollup.config.js (100%) rename packages/{_viewer/extensions/ohif-dicom-microscopy-extension => extension-dicom-microscopy}/src/DicomMicroscopySopClassHandler.js (100%) rename packages/{_viewer/extensions/ohif-dicom-microscopy-extension => extension-dicom-microscopy}/src/DicomMicroscopyViewport.js (100%) rename packages/{_viewer/extensions/ohif-dicom-microscopy-extension => extension-dicom-microscopy}/src/index.js (100%) rename packages/{_viewer/extensions/ohif-dicom-microscopy-extension => extension-dicom-microscopy}/yarn.lock (100%) rename packages/{_viewer/extensions/ohif-i18n => extension-dicom-pdf}/.babelrc (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-dicom-pdf}/.editorconfig (100%) rename packages/{_viewer/extensions/ohif-dicom-pdf-extension => extension-dicom-pdf}/.eslintrc (100%) rename packages/{_viewer/extensions/ohif-dicom-pdf-extension => extension-dicom-pdf}/.gitignore (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-dicom-pdf}/.prettierrc (100%) rename packages/{_viewer/extensions/ohif-dicom-pdf-extension => extension-dicom-pdf}/.travis.yml (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-dicom-pdf}/LICENSE (100%) rename packages/{_viewer/extensions/ohif-dicom-pdf-extension => extension-dicom-pdf}/package.json (100%) rename packages/{_viewer/extensions/ohif-dicom-pdf-extension => extension-dicom-pdf}/rollup.config.js (100%) rename packages/{_viewer/extensions/ohif-dicom-pdf-extension => extension-dicom-pdf}/src/DicomPDFViewport.js (100%) rename packages/{_viewer/extensions/ohif-dicom-pdf-extension => extension-dicom-pdf}/src/OHIFComponentPlugin.js (100%) rename packages/{_viewer/extensions/ohif-dicom-pdf-extension => extension-dicom-pdf}/src/OHIFDicomPDFSopClassHandler.js (100%) rename packages/{_viewer/extensions/ohif-dicom-pdf-extension => extension-dicom-pdf}/src/OHIFDicomPDFViewport.js (100%) rename packages/{_viewer/extensions/ohif-dicom-pdf-extension => extension-dicom-pdf}/src/TypedArrayProp.js (100%) rename packages/{_viewer/extensions/ohif-dicom-pdf-extension => extension-dicom-pdf}/src/index.js (100%) rename packages/{_viewer/extensions/ohif-dicom-pdf-extension => extension-dicom-pdf}/yarn.lock (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/.babelrc (100%) create mode 100644 packages/extension-vtk/.editorconfig rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/.eslintrc (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/.gitignore (100%) create mode 100644 packages/extension-vtk/.prettierrc rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/.travis.yml (100%) create mode 100644 packages/extension-vtk/LICENSE rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/README.md (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/package.json (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/rollup.config.js (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/src/ConnectedVTKViewport.js (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/src/LoadingIndicator.css (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/src/LoadingIndicator.js (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/src/OHIFVTKViewport.js (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/src/commandsModule.js (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/src/handleSegmentationStorage.js (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/src/index.js (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/src/loadLocales.js (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/src/locales/en/Buttons.json (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/src/locales/en/index.js (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/src/locales/es/Buttons.json (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/src/locales/es/index.js (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/src/locales/index.js (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/src/toolbarModule.js (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/src/utils/setLayoutAndViewportData.js (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/src/utils/setMPRLayout.js (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/src/utils/setSingleLayoutData.js (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/src/utils/setViewportToVTK.js (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/webpack.config.js (100%) rename packages/{_viewer/extensions/ohif-vtk-extension => extension-vtk}/yarn.lock (100%) create mode 100644 yarn.lock diff --git a/package.json b/package.json index e603df5d6..46da0f7d8 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,9 @@ ], "devDependencies": { "@babel/core": "^7.5.0", + "@babel/plugin-proposal-class-properties": "^7.5.0", + "@babel/plugin-syntax-dynamic-import": "^7.2.0", + "@babel/plugin-transform-runtime": "^7.5.0", "@babel/preset-env": "^7.5.0", "@babel/preset-react": "^7.0.0", "lerna": "^3.15.0" diff --git a/packages/_components/.all-contributorsrc b/packages/_components/.all-contributorsrc new file mode 100644 index 000000000..81c149133 --- /dev/null +++ b/packages/_components/.all-contributorsrc @@ -0,0 +1,135 @@ +{ + "projectName": "react-viewerbase", + "projectOwner": "OHIF", + "repoType": "github", + "repoHost": "https://github.com", + "files": [ + "README.md" + ], + "imageSize": 100, + "commit": true, + "commitConvention": "none", + "contributors": [ + { + "login": "swederik", + "name": "Erik Ziegler", + "avatar_url": "https://avatars3.githubusercontent.com/u/607793?v=4", + "profile": "https://github.com/swederik", + "contributions": [ + "code", + "maintenance" + ] + }, + { + "login": "dannyrb", + "name": "Danny Brown", + "avatar_url": "https://avatars1.githubusercontent.com/u/5797588?v=4", + "profile": "http://dannyrb.com/", + "contributions": [ + "code", + "maintenance" + ] + }, + { + "login": "galelis", + "name": "Gustavo André Lelis", + "avatar_url": "https://avatars3.githubusercontent.com/u/2378326?v=4", + "profile": "https://github.com/galelis", + "contributions": [ + "code", + "maintenance" + ] + }, + { + "login": "maltempi", + "name": "Thiago Maltempi", + "avatar_url": "https://avatars3.githubusercontent.com/u/7017182?v=4", + "profile": "https://github.com/maltempi", + "contributions": [ + "code" + ] + }, + { + "login": "EsrefDurna", + "name": "Esref Durna", + "avatar_url": "https://avatars0.githubusercontent.com/u/1230575?v=4", + "profile": "https://www.linkedin.com/in/siliconvalleynextgeneration/", + "contributions": [ + "code" + ] + }, + { + "login": "pieper", + "name": "Steve Pieper", + "avatar_url": "https://avatars0.githubusercontent.com/u/126077?v=4", + "profile": "http://www.isomics.com", + "contributions": [ + "code" + ] + }, + { + "login": "biharck", + "name": "Biharck Araujo", + "avatar_url": "https://avatars0.githubusercontent.com/u/1713255?v=4", + "profile": "http://www.biharck.com.br", + "contributions": [ + "code" + ] + }, + { + "login": "rodrigolabs", + "name": "Rodrigo Antinarelli", + "avatar_url": "https://avatars3.githubusercontent.com/u/1905961?v=4", + "profile": "https://rodrigoea.com/", + "contributions": [ + "code" + ] + }, + { + "login": "jfmedeiros1820", + "name": "João Felipe de Medeiros Moreira", + "avatar_url": "https://avatars1.githubusercontent.com/u/2211708?v=4", + "profile": "https://github.com/jfmedeiros1820", + "contributions": [ + "code" + ] + }, + { + "login": "jamesg1", + "name": "James Gosbell", + "avatar_url": "https://avatars3.githubusercontent.com/u/3621147?v=4", + "profile": "https://github.com/jamesg1", + "contributions": [ + "code" + ] + }, + { + "login": "evren217", + "name": "Evren Ozkan", + "avatar_url": "https://avatars1.githubusercontent.com/u/4920551?v=4", + "profile": "https://github.com/evren217", + "contributions": [ + "code" + ] + }, + { + "login": "zsaltzman", + "name": "Zach S.", + "avatar_url": "https://avatars1.githubusercontent.com/u/19156530?v=4", + "profile": "https://github.com/zsaltzman", + "contributions": [ + "code" + ] + }, + { + "login": "muakdogan", + "name": "Mete Ugur Akdogan", + "avatar_url": "https://avatars0.githubusercontent.com/u/19971240?v=4", + "profile": "https://github.com/muakdogan", + "contributions": [ + "code" + ] + } + ], + "contributorsPerLine": 7 +} diff --git a/packages/_components/.babelrc b/packages/_components/.babelrc new file mode 100644 index 000000000..3f85000fd --- /dev/null +++ b/packages/_components/.babelrc @@ -0,0 +1,18 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "ie": "11" + } + } + ], + "@babel/preset-react" + ], + "plugins": [ + "inline-react-svg", + "@babel/plugin-proposal-class-properties", + "@babel/plugin-transform-runtime" + ] +} diff --git a/packages/_components/.circleci/config.yml b/packages/_components/.circleci/config.yml new file mode 100644 index 000000000..cf72be118 --- /dev/null +++ b/packages/_components/.circleci/config.yml @@ -0,0 +1,85 @@ +version: 2 + +### ABOUT +# +# This configuration powers our Circleci.io integration +# +# Note: +# Netlify works independently from this configuration to +# create pull request previews and to update `https://react.ohif.org` +### + +defaults: &defaults + working_directory: ~/repo + docker: + - image: circleci/node:10.15.1 + +jobs: + build_and_test: + <<: *defaults + steps: + # Download and cache dependencies + - checkout + - restore_cache: + name: Restore Yarn Package Cache + keys: + # when lock file changes, use increasingly general patterns to restore cache + - yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }} + - yarn-packages-v1-{{ .Branch }}- + - yarn-packages-v1- + - run: + name: Install Dependencies + command: yarn install --frozen-lockfile + - save_cache: + name: Save Yarn Package Cache + paths: + - ~/.cache/yarn + key: yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }} + + # Build & Test + - run: yarn build + - run: yarn test + + # Persist :+1: + - persist_to_workspace: + root: ~/repo + paths: . + + npm_publish: + <<: *defaults + steps: + - attach_workspace: + at: ~/repo + - run: + name: Avoid hosts unknown for github + command: + mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking + no\n" > ~/.ssh/config + - run: + name: Publish using Semantic Release + command: npx semantic-release + +workflows: + version: 2 + + # PULL REQUESTS + pull_requests: + jobs: + - build_and_test: + filters: + branches: + ignore: + - master + - feature/* + - hotfix/* + + # MERGE TO MASTER + cut_release: + jobs: + - build_and_test: + filters: + branches: + only: master + - npm_publish: + requires: + - build_and_test diff --git a/packages/_viewer/extensions/ohif-cornerstone-extension/.editorconfig b/packages/_components/.editorconfig similarity index 100% rename from packages/_viewer/extensions/ohif-cornerstone-extension/.editorconfig rename to packages/_components/.editorconfig diff --git a/packages/_components/.env b/packages/_components/.env new file mode 100644 index 000000000..0708aefac --- /dev/null +++ b/packages/_components/.env @@ -0,0 +1,11 @@ +#### +## +## We need to pin WebPack's version for docz to work. See this issue: +## https://github.com/pedronauck/docz/issues/704#issuecomment-480295032 +## +## react-scripts checks that dependency versions match before running +## Our top level webpack version causes the preflight check to fail +## This prevents that check +## +#### +SKIP_PREFLIGHT_CHECK=true diff --git a/packages/_components/.eslintrc b/packages/_components/.eslintrc new file mode 100644 index 000000000..7556af95d --- /dev/null +++ b/packages/_components/.eslintrc @@ -0,0 +1,17 @@ +{ + "extends": [ + "react-app", + "eslint:recommended", + "plugin:react/recommended", + "plugin:prettier/recommended" + ], + "parser": "babel-eslint", + "env": { + "jest": true + }, + "settings": { + "react": { + "version": "detect" + } + } +} diff --git a/packages/_components/.gitattributes b/packages/_components/.gitattributes new file mode 100644 index 000000000..fcadb2cf9 --- /dev/null +++ b/packages/_components/.gitattributes @@ -0,0 +1 @@ +* text eol=lf diff --git a/packages/_components/.github/ISSUE_TEMPLATE/---bug-report.md b/packages/_components/.github/ISSUE_TEMPLATE/---bug-report.md new file mode 100644 index 000000000..974c3fd37 --- /dev/null +++ b/packages/_components/.github/ISSUE_TEMPLATE/---bug-report.md @@ -0,0 +1,40 @@ +--- +name: "\U0001F41B Bug report" +about: Create a report to help us improve +title: '' +labels: 'Bug Report :bug:' +assignees: '' + +--- + +**Before Creating an issue** + +- Are you running the latest version? +- Are you reporting to the correct repository? +- Did you search existing issues? + +**Describe the bug** + +*A clear and concise description of what the bug is.* + +**Steps To Reproduce** + +1. [First Step] +2. [Second Step] +3. ... + +``` +Please use code blocks to show formatted errors or code snippets +``` + +**Expected behavior** + +*A clear and concise description of what you expected to happen.* + + +**Environment** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Additional context** diff --git a/packages/_components/.github/ISSUE_TEMPLATE/---feature-request.md b/packages/_components/.github/ISSUE_TEMPLATE/---feature-request.md new file mode 100644 index 000000000..0231ff905 --- /dev/null +++ b/packages/_components/.github/ISSUE_TEMPLATE/---feature-request.md @@ -0,0 +1,18 @@ +--- +name: "\U0001F680 Feature request" +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +## Awesome, do you have an idea? 😍 + +If you have a **feature request, improvement or idea**, check [our official roadmap](https://github.com/OHIF/react-viewerbase/projects) to see if it is already planned! + +### 👉   [Go to Roadmap](https://github.com/OHIF/react-viewerbase/projects) + +If your feature request isn't there, continue with this issue and we can discuss it 🤟 + +Please include the reasons why you think your change should be made, and any supporting evidence that helps us asses its value and priority. diff --git a/packages/_components/.github/ISSUE_TEMPLATE/---support-question.md b/packages/_components/.github/ISSUE_TEMPLATE/---support-question.md new file mode 100644 index 000000000..e2cd7e5ff --- /dev/null +++ b/packages/_components/.github/ISSUE_TEMPLATE/---support-question.md @@ -0,0 +1,15 @@ +--- +name: "\U0001F917 Support Question" +about: "I have a question \U0001F4AC" +title: '' +labels: question +assignees: '' + +--- + +We are a small team with limited resources. Your question is much more likely to be answered if it is [a good question](https://stackoverflow.com/help/how-to-ask) + +**Description** + + +Questions can often be answered by our documentation. Unable to find an answer in our docs? We'll try to help. In the meantime, if you answer your own question, please respond with the answer here so that others may benefit as well. Better yet, open a PR to expand our docs ^_^ diff --git a/packages/_components/.gitignore b/packages/_components/.gitignore new file mode 100644 index 000000000..65d069f84 --- /dev/null +++ b/packages/_components/.gitignore @@ -0,0 +1,32 @@ + +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +node_modules +# Don't track package-lock if we intend to use yarn.lock +package-lock.json + +# builds +build +dist/ +.rpt2_cache +.docz/ +.yalc +yalc.lock + +# Log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.idea + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Can be removed after we update netlify +# to use default .docz/ output destination +example/ diff --git a/packages/_components/.nvmrc b/packages/_components/.nvmrc new file mode 100644 index 000000000..d16c3a34a --- /dev/null +++ b/packages/_components/.nvmrc @@ -0,0 +1 @@ +10.15.3 \ No newline at end of file diff --git a/packages/_viewer/extensions/ohif-cornerstone-extension/.prettierrc b/packages/_components/.prettierrc similarity index 100% rename from packages/_viewer/extensions/ohif-cornerstone-extension/.prettierrc rename to packages/_components/.prettierrc diff --git a/packages/_components/.vscode/extensions.json b/packages/_components/.vscode/extensions.json new file mode 100644 index 000000000..2280f7c91 --- /dev/null +++ b/packages/_components/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode", + "sysoev.language-stylus", + "silvenon.mdx", + "dbaeumer.vscode-eslint" + ] +} diff --git a/packages/_components/.vscode/settings.json b/packages/_components/.vscode/settings.json new file mode 100644 index 000000000..4b67a8bd3 --- /dev/null +++ b/packages/_components/.vscode/settings.json @@ -0,0 +1,29 @@ +{ + "editor.rulers": [80, 120], + + // === + // Spacing + // === + + "editor.insertSpaces": true, + "editor.tabSize": 2, + "editor.trimAutoWhitespace": true, + "files.trimTrailingWhitespace": true, + "files.eol": "\n", + "files.insertFinalNewline": true, + "files.trimFinalNewlines": true, + + // === + // Event Triggers + // === + + "editor.formatOnSave": true, + "eslint.autoFixOnSave": true, + "eslint.run": "onSave", + "eslint.validate": [ + { "language": "javascript", "autoFix": true }, + { "language": "javascriptreact", "autoFix": true } + ], + "prettier.disableLanguages": [], + "prettier.endOfLine": "lf" +} diff --git a/packages/_viewer/extensions/ohif-cornerstone-extension/LICENSE b/packages/_components/LICENSE similarity index 100% rename from packages/_viewer/extensions/ohif-cornerstone-extension/LICENSE rename to packages/_components/LICENSE diff --git a/packages/_components/README.md b/packages/_components/README.md new file mode 100644 index 000000000..98679bfab --- /dev/null +++ b/packages/_components/README.md @@ -0,0 +1,165 @@ + + +
+

react-viewerbase

+ +

React Viewerbase is a collection of components and utilities that power OHIF's zero-footprint DICOM viewer.

+
+ +
+Read The Docs | +Edit the docs + +
+ +
+ +[![CircleCI][circleci-image]][circleci-url] +[![All Contributors](https://img.shields.io/badge/all_contributors-13-orange.svg?style=flat-square)](#contributors) +[![code style: prettier][prettier-image]][prettier-url] +[![semantic-release][semantic-image]][semantic-url] + +[![NPM version][npm-version-image]][npm-url] +[![NPM downloads][npm-downloads-image]][npm-url] +[![MIT License][license-image]][license-url] + + + +This set of "Medical Imaging Viewer" React components are maintained separately +to: + +- Decouple presentation from business logic +- Test and develop components in isolation +- Provide well documented, reusable components +- Aid rapid application development for context specific viewers + +## Install + +> This component library is pre- v1.0. All realeases until a v1.0 have the +> possibility of introducing breaking changes. Please depend on an "exact" +> version in your projects to prevent issues caused by loose versioning. + +For full installation instructions, be sure to check out our +[getting started](https://react.ohif.org/getting-started#installation) guide. + +```bash +// with npm +npm i react-viewerbase --save-exact + +// with yarn +yarn add react-viewerbase --exact +``` + +## Usage + +```jsx +import React, { Component } from 'react'; +import { LayoutButton } from 'react-viewerbase'; + +class Example extends Component { + constructor(props) { + super(props); + + this.state = { + selectedCell: { + className: 'hover', + col: 1, + row: 1, + }, + }; + } + + render() { + return ( + this.setState({ selectedCell: cell })} + /> + ); + } +} +``` + +## Running Locally + +_Restore dependencies after cloning:_ + +1. `cd react-viewerbase` +2. `yarn install` + +_Develop w/ Hot Reloading:_ + +`yarn dev` + +_Build for Production:_ + +`yarn build` + +## Contributors ✨ + +Thanks goes to these wonderful people +([emoji key](https://allcontributors.org/docs/en/emoji-key)): + + + +
Erik Ziegler
Erik Ziegler

💻 🚧
Danny Brown
Danny Brown

💻 🚧
Gustavo André Lelis
Gustavo André Lelis

💻 🚧
Thiago Maltempi
Thiago Maltempi

💻
Esref Durna
Esref Durna

💻
Steve Pieper
Steve Pieper

💻
Biharck Araujo
Biharck Araujo

💻
Rodrigo Antinarelli
Rodrigo Antinarelli

💻
João Felipe de Medeiros Moreira
João Felipe de Medeiros Moreira

💻
James Gosbell
James Gosbell

💻
Evren Ozkan
Evren Ozkan

💻
Zach S.
Zach S.

💻
Mete Ugur Akdogan
Mete Ugur Akdogan

💻
+ + + +This project follows the +[all-contributors](https://github.com/all-contributors/all-contributors) +specification. Contributions of any kind welcome! + +## Issues + +_Looking to contribute? Look for the [Good First Issue][good-first-issue] +label._ + +### 🐛 Bugs + +Please file an issue for bugs, missing documentation, or unexpected behavior. + +[**See Bugs**][bugs] + +### 💡 Feature Requests + +Please file an issue to suggest new features. Vote on feature requests by adding +a 👍. This helps maintainers prioritize what to work on. + +[**See Feature Requests**][requests-feature] + +### ❓ Questions + +For questions related to using the library, please visit our support community, +or file an issue on GitHub. + +[Google Group][google-group] + +## License + +MIT © [OHIF](https://github.com/OHIF) + + + + +[all-contributors-image]: https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square +[contributing-url]: https://github.com/OHIF/react-viewerbase/blob/master/CONTRIBUTING.md +[circleci-image]: https://circleci.com/gh/OHIF/react-viewerbase.svg?style=svg +[circleci-url]: https://circleci.com/gh/OHIF/react-viewerbase +[prettier-image]: https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square +[prettier-url]: https://github.com/prettier/prettier +[npm-url]: https://npmjs.org/package/react-viewerbase +[npm-downloads-image]: https://img.shields.io/npm/dm/react-viewerbase.svg?style=flat-square +[npm-version-image]: https://img.shields.io/npm/v/react-viewerbase.svg?style=flat-square +[license-image]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square +[license-url]: LICENSE +[semantic-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg +[semantic-url]: https://github.com/semantic-release/semantic-release + +[bugs]: https://github.com/OHIF/react-viewerbase/labels/bug +[requests-feature]: https://github.com/OHIF/react-viewerbase/labels/enhancement +[good-first-issue]: https://github.com/OHIF/react-viewerbase/labels/good%20first%20issue +[google-group]: https://groups.google.com/forum/#!forum/cornerstone-platform + diff --git a/packages/_components/doczrc.js b/packages/_components/doczrc.js new file mode 100644 index 000000000..06466abd9 --- /dev/null +++ b/packages/_components/doczrc.js @@ -0,0 +1,113 @@ +/** + * Docz Configuration File: + * https://www.docz.site/docs/project-configuration + */ + +import { css } from 'docz-plugin-css'; + +export default { + dest: 'example/build', + public: '/public', + wrapper: 'src/__docs__/wrapper', + indexHtml: 'src/__docs__/index.html', + // Limited support for importing `.styl` files + codeSandbox: false, + // https://github.com/pedronauck/docz/pull/849/files + // Because we re-export using `index.js` files + notUseSpecifiers: true, + // filterComponents: files => + // files.filter(filepath => + // /\/[A-Z]\w*(\/index)?\.(js|jsx|ts|tsx)$/.test(filepath) + // ), + menu: [ + 'Introduction', + 'Getting Started', + { name: 'Elements', menu: ['Form / Select'] }, + { + name: 'Components', + menu: [ + 'CINE Dialog', + 'Layout Button', + 'Measurement Table', + 'Overlay Trigger', + 'Quick Switch', + 'Rounded Button Group', + 'Select Tree', + 'Simple Dialog', + 'Study Browser', + 'Study List', + 'Table List', + 'Toolbar Section', + 'About Modal', + 'User Preferences Modal', + ], + }, + 'Styling & Theming', + 'Translating', + 'Compatibility', + ], + // Rollup Aliases? + // https://github.com/pedronauck/docz/issues/373 + plugins: [ + // CSS + css(), + // Stylus + css({ + preprocessor: 'stylus', + cssmodules: false, + }), + ], + // `docz` uses file-loader to pull in SVGs. This kills our icons before + // They can be picked up by our `babel-plugin-inline-react-svg` dependency + // You can see where it's configured here: + // https://github.com/pedronauck/docz/blob/f72624d0aa5231dab17e1770e8d36be920f590a2/core/docz-core/src/bundler/loaders.ts#L135-L144 + // How we delete our rule: + // https://github.com/neutrinojs/webpack-chain/issues/48 + onCreateWebpackChain: config => { + config.module.rules.delete('svg'); + + // config.module + // .rule('svg') + // .test(/\.(svg)(\?.*)?$/) + // .use('file-loader') + // .loader(require.resolve('file-loader')) + // .options({ + // name: `static/img/[name].[ext]`, + // }) + }, + modifyBabelRc: babelrc => { + const newBabelRc = { + ...babelrc, + plugins: [ + ...babelrc.plugins, + require.resolve('babel-plugin-inline-react-svg'), + ], + }; + return newBabelRc; + }, +}; + +/* + * Alternative ways to extend/modify underlying webpack config + * +modifyBundlerConfig: (config) => { + config.module.rules.push({ + test: /\.scss$/, + use: ["style-loader", "css-loader", "sass-loader"] + }) + + return config +} + * + * + * +onCreateWebpackChain: config => { + config.module + .rule('css') + .test(/\.css$/) + .use('css-loader') + .loader('css-loader') + .options({ sourceMap: false, + importLoaders: 2 }) +} +*/ diff --git a/packages/_components/netlify.toml b/packages/_components/netlify.toml new file mode 100644 index 000000000..2aace3d92 --- /dev/null +++ b/packages/_components/netlify.toml @@ -0,0 +1,21 @@ +# https://www.netlify.com/docs/continuous-deployment/#deploy-contexts +# +# Global settings applied to the whole site. +# +# “base” is the directory to change to before starting build. If you set base: +# that is where we will look for package.json/.nvmrc/etc, not repo root! +# “command” is your build command. +# “publish” is the directory to publish (relative to the root of your repo). + +# COMMENT: NODE_VERSION in root `.nvmrc` takes priority +# COMMENT: Why we specify YARN_FLAGS: https://www.netlify.com/docs/build-gotchas/#yarn +[build.environment] + NODE_VERSION = "10.15.3" + YARN_VERSION = "1.15.2" + YARN_FLAGS = "--no-ignore-optional --pure-lockfile" + +# COMMENT: This a rule for Single Page Applications +[[redirects]] + from = "/*" + to = "/index.html" + status = 200 \ No newline at end of file diff --git a/packages/_components/package.json b/packages/_components/package.json new file mode 100644 index 000000000..f3ae738a1 --- /dev/null +++ b/packages/_components/package.json @@ -0,0 +1,127 @@ +{ + "name": "react-viewerbase", + "version": "0.2.17", + "description": "A set of React components for Medical Imaging Viewers", + "author": "OHIF Contributors", + "license": "MIT", + "repository": "OHIF/react-viewerbase", + "main": "dist/index.js", + "module": "dist/index.es.js", + "jsnext:main": "dist/index.es.js", + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "scripts": { + "cm": "npx git-cz", + "dev": "docz dev", + "test": "cross-env CI=1 react-scripts test --env=jsdom --transformIgnorePatterns \"node_modules/(?!react-dnd)/\"", + "test:watch": "react-scripts test --env=jsdom --transformIgnorePatterns \"node_modules/(?!react-dnd)/\"", + "build": "rollup -c", + "start": "rollup -c -w", + "lint": "eslint -c .eslintrc --fix src && prettier --single-quote --write src/**/*.{js,jsx,json,css}", + "prepare": "yarn build", + "prepublishOnly": "yarn build", + "generateStaticSite": "docz build", + "contributors:add": "all-contributors add", + "contributors:generate": "all-contributors generate" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "src/**/*.{js,jsx,json,css}": [ + "prettier --single-quote --write", + "git add" + ] + }, + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ], + "files": [ + "dist" + ], + "peerDependencies": { + "@ohif/i18n": "0.2.1", + "i18next": "^17.0.3", + "react": "^16.8.6", + "react-dom": "^16.8.6", + "react-i18next": "^10.11.0" + }, + "dependencies": { + "@babel/runtime": "7.2.0", + "@ohif/i18n": "0.2.1", + "classnames": "2.2.6", + "i18next": "^17.0.3", + "i18next-browser-languagedetector": "^3.0.1", + "lodash.debounce": "^4.0.8", + "lodash.isequal": "4.5.0", + "moment": "2.24.0", + "prop-types": "15.6.2", + "react-bootstrap-modal": "4.2.0", + "react-dates": "18.4.1", + "react-dnd": "8.0.3", + "react-dnd-touch-backend": "^0.8.3", + "react-i18next": "^10.11.0", + "react-with-direction": "1.3.0" + }, + "devDependencies": { + "@babel/core": "^7.2.2", + "@babel/plugin-external-helpers": "^7.2.0", + "@babel/plugin-proposal-class-properties": "^7.2.3", + "@babel/plugin-transform-runtime": "^7.2.0", + "@babel/preset-env": "^7.2.3", + "@babel/preset-react": "^7.0.0", + "@mdx-js/tag": "^0.20.3", + "all-contributors-cli": "6.7.0", + "autoprefixer": "^9.6.0", + "babel-eslint": "^9.0.0", + "babel-plugin-inline-react-svg": "1.1.0", + "commitizen": "3.0.7", + "cpx": "1.5.0", + "cross-env": "^5.2.0", + "cz-conventional-changelog": "2.1.0", + "docz": "1.2.0", + "docz-core": "1.2.0", + "docz-plugin-css": "0.11.0", + "docz-theme-default": "1.2.0", + "eslint": "5.12.0", + "eslint-config-prettier": "4.1.0", + "eslint-plugin-import": "^2.14.0", + "eslint-plugin-node": "^8.0.0", + "eslint-plugin-prettier": "3.0.1", + "eslint-plugin-promise": "^4.0.1", + "eslint-plugin-react": "^7.11.1", + "husky": "^1.3.1", + "lint-staged": "^8.1.0", + "lodash.clonedeep": "^4.5.0", + "prettier": "1.16.4", + "react": "16.8.6", + "react-dom": "16.8.6", + "react-powerplug": "1.0.0", + "react-scripts": "^2.1.5", + "rollup": "^0.68.2", + "rollup-plugin-babel": "^4.2.0", + "rollup-plugin-commonjs": "^9.2.0", + "rollup-plugin-node-builtins": "^2.1.2", + "rollup-plugin-node-resolve": "^4.0.0", + "rollup-plugin-peer-deps-external": "^2.2.0", + "rollup-plugin-postcss": "^1.6.3", + "rollup-plugin-url": "^2.1.0", + "semantic-release": "15.13.x", + "stylelint": "^9.9.0", + "stylelint-config-recommended": "^2.1.0", + "stylus": "^0.54.5", + "webpack": "4.28.4" + }, + "config": { + "commitizen": { + "path": "./node_modules/cz-conventional-changelog" + } + } +} diff --git a/packages/_components/public/docz.css b/packages/_components/public/docz.css new file mode 100644 index 000000000..1c45c19aa --- /dev/null +++ b/packages/_components/public/docz.css @@ -0,0 +1,44 @@ +/* Tip & Success "Call outs" */ + +p.tip { + border-left-color: #f66; +} +p.success { + border-left-color: #42b983; +} +p.tip::before { + content: '!'; + background-color: #f66; +} +p.success::before { + content: '\f00c'; + font-family: FontAwesome; + background-color: #42b983; +} +p.tip, +p.success { + padding: 12px 24px 12px 30px; + margin: 2em 0; + line-height: 1.6em; + border-left-width: 4px; + border-left-style: solid; + background-color: #f8f8f8; + position: relative; + border-bottom-right-radius: 2px; + border-top-right-radius: 2px; +} +p.tip::before, +p.success::before { + position: absolute; + top: 14px; + left: -12px; + color: #fff; + width: 20px; + height: 20px; + border-radius: 100%; + text-align: center; + line-height: 20px; + font-weight: bold; + font-family: 'Dosis', 'Source Sans Pro', 'Helvetica Neue', Arial, sans-serif; + font-size: 14px; +} diff --git a/packages/_components/rollup.config.js b/packages/_components/rollup.config.js new file mode 100644 index 000000000..0fb594827 --- /dev/null +++ b/packages/_components/rollup.config.js @@ -0,0 +1,61 @@ +import autoprefixer from 'autoprefixer'; +import babel from 'rollup-plugin-babel'; +import builtins from 'rollup-plugin-node-builtins'; +import commonjs from 'rollup-plugin-commonjs'; +import external from 'rollup-plugin-peer-deps-external'; +import pkg from './package.json'; +import postcss from 'rollup-plugin-postcss'; +import resolve from 'rollup-plugin-node-resolve'; +import url from 'rollup-plugin-url'; + +const globals = { + react: 'React', + 'react-dom': 'ReactDOM', + 'react-i18next': 'reactI18next', + '@ohif/i18n': 'i18n', +}; + +export default { + input: 'src/index.js', + output: [ + { + file: pkg.main, + format: 'umd', + name: 'react-viewerbase', + sourcemap: true, + exports: 'named', + globals, + }, + { + file: pkg.module, + format: 'es', + sourcemap: true, + globals, + }, + ], + plugins: [ + builtins(), + external(), + postcss({ + modules: false, + plugins: [autoprefixer], + }), + url(), + babel({ + exclude: 'node_modules/**', + runtimeHelpers: true, + }), + resolve({ + browser: true, + }), + commonjs({ + // https://github.com/airbnb/react-dates/issues/1183#issuecomment-392073823 + namedExports: { + 'node_modules/react-dates/index.js': [ + 'DateRangePicker', + 'isInclusivelyBeforeDay', + ], + }, + }), + ], +}; diff --git a/packages/_components/src/LayoutChooser/LayoutManager.css b/packages/_components/src/LayoutChooser/LayoutManager.css new file mode 100644 index 000000000..0b568e259 --- /dev/null +++ b/packages/_components/src/LayoutChooser/LayoutManager.css @@ -0,0 +1,22 @@ +.viewport-container { + float: left; + position: relative; + border: var(--viewport-border-thickness) solid var(--ui-border-color); +} + +.viewport-container.active { + border: var(--viewport-border-thickness) solid var(--active-color); +} + +.EmptyViewport { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + color: var(--text-secondary-color); +} + +.LayoutPanel { + width: 100%; + height: 100%; +} diff --git a/packages/_components/src/LayoutChooser/LayoutManager.js b/packages/_components/src/LayoutChooser/LayoutManager.js new file mode 100644 index 000000000..6ec9f65f0 --- /dev/null +++ b/packages/_components/src/LayoutChooser/LayoutManager.js @@ -0,0 +1,162 @@ +import './LayoutManager.css'; + +import React, { Component } from 'react'; + +import LayoutPanelDropTarget from './LayoutPanelDropTarget.js'; +import PropTypes from 'prop-types'; + +function defaultViewportPlugin(props) { + return
{JSON.stringify(props)}
; +} + +function EmptyViewport() { + return ( +
+

Please drag a stack here to view images.

+
+ ); +} + +export class LayoutManager extends Component { + static className = 'LayoutManager'; + static defaultProps = { + viewportData: [], + layout: { + viewports: [ + { + top: 0, + left: 0, + height: '100%', + width: '100%', + }, + ], + }, + activeViewportIndex: 0, + supportsDragAndDrop: true, + availablePlugins: { + defaultViewportPlugin, + }, + defaultPlugin: 'defaultViewportPlugin', + }; + + static propTypes = { + viewportData: PropTypes.array.isRequired, + supportsDragAndDrop: PropTypes.bool.isRequired, + activeViewportIndex: PropTypes.number.isRequired, + layout: PropTypes.object.isRequired, + availablePlugins: PropTypes.object.isRequired, + setViewportData: PropTypes.func, + studies: PropTypes.array, + children: PropTypes.node, + }; + + onDrop = ({ viewportIndex, item }) => { + if (this.props.setViewportData) { + this.props.setViewportData({ viewportIndex, item }); + } + }; + + getPluginComponent = plugin => { + const pluginComponent = this.props.availablePlugins[ + plugin || this.props.defaultPlugin + ]; + + if (!pluginComponent) { + throw new Error( + `No Viewport Plugin available for plugin ${plugin}. Available plugins: ${JSON.stringify( + this.props.availablePlugins + )}` + ); + } + + return pluginComponent; + }; + + getChildComponent(plugin, data, viewportIndex, children) { + if (data.displaySet) { + const PluginComponent = this.getPluginComponent(plugin); + + return ( + + ); + } + + return ; + } + + getContent(childComponent, supportsDragAndDrop, viewportIndex) { + if (supportsDragAndDrop) { + return ( + + {childComponent} + + ); + } + + return
{childComponent}
; + } + + render() { + if (!this.props.viewportData.length) { + return ''; + } + + const { supportsDragAndDrop, studies, viewportData } = this.props; + const viewports = this.props.layout.viewports; + const viewportElements = viewports.map((layout, viewportIndex) => { + const displaySet = viewportData[viewportIndex]; + const data = { + displaySet, + studies, + }; + + // Use whichever plugin is currently in use in the panel + // unless nothing is specified. If nothing is specified + // and the display set has a plugin specified, use that. + // + // TODO: Change this logic to: + // - Plugins define how capable they are of displaying a SopClass + // - When updating a panel, ensure that the currently enabled plugin + // in the viewport is capable of rendering this display set. If not + // then use the most capable available plugin + let plugin = layout.plugin; + if (!layout.plugin && displaySet && displaySet.plugin) { + plugin = displaySet.plugin; + } + + const childComponent = this.getChildComponent( + plugin, + data, + viewportIndex, + this.props.children + ); + const content = this.getContent( + childComponent, + supportsDragAndDrop, + viewportIndex + ); + + let className = 'viewport-container'; + if (this.props.activeViewportIndex === viewportIndex) { + className += ' active'; + } + + return ( +
+ {content} +
+ ); + }); + + return
{viewportElements}
; + } +} + +export default LayoutManager; diff --git a/packages/_components/src/LayoutChooser/LayoutPanelDropTarget.css b/packages/_components/src/LayoutChooser/LayoutPanelDropTarget.css new file mode 100644 index 000000000..7c629feaf --- /dev/null +++ b/packages/_components/src/LayoutChooser/LayoutPanelDropTarget.css @@ -0,0 +1,12 @@ +.LayoutPanelDropTarget { + width: 100%; + height: 100%; + + opacity: 1; + transition: 0.3s all ease; +} + +.LayoutPanelDropTarget.hovered { + opacity: 0.5; + cursor: copy; +} diff --git a/packages/_components/src/LayoutChooser/LayoutPanelDropTarget.js b/packages/_components/src/LayoutChooser/LayoutPanelDropTarget.js new file mode 100644 index 000000000..96355b519 --- /dev/null +++ b/packages/_components/src/LayoutChooser/LayoutPanelDropTarget.js @@ -0,0 +1,74 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { DropTarget } from 'react-dnd'; +import './LayoutPanelDropTarget.css'; + +// Drag sources and drop targets only interact +// if they have the same string type. +const Types = { + THUMBNAIL: 'thumbnail', +}; + +const divTarget = { + drop(props, monitor, component) { + const item = monitor.getItem(); + + if (props.onDrop) { + props.onDrop({ + viewportIndex: props.viewportIndex, + item, + }); + } + + return { + id: `LayoutPanelDropTarget-${props.viewportIndex}`, + viewportIndex: props.viewportIndex, + item, + }; + }, +}; + +// TODO: Find out why we can't move this into the Example app instead. +// It looks like the context isn't properly shared. +class LayoutPanelDropTarget extends Component { + static className = 'LayoutPanelDropTarget'; + + static defaultProps = { + isOver: false, + canDrop: false, + }; + + static propTypes = { + connectDropTarget: PropTypes.func.isRequired, + canDrop: PropTypes.bool.isRequired, + isOver: PropTypes.bool.isRequired, + viewportComponent: PropTypes.object, + }; + + render() { + const { canDrop, isOver, connectDropTarget } = this.props; + const isActive = canDrop && isOver; + + let className = LayoutPanelDropTarget.className; + + if (isActive) { + className += ' hovered'; + } else if (canDrop) { + className += ' can-drop'; + } + + return connectDropTarget( +
{this.props.children}
+ ); + } +} + +const collect = (connect, monitor) => ({ + connectDropTarget: connect.dropTarget(), + canDrop: monitor.canDrop(), + isOver: monitor.isOver(), +}); + +export default DropTarget(Types.THUMBNAIL, divTarget, collect)( + LayoutPanelDropTarget +); diff --git a/packages/_components/src/ScrollableArea/ScrollableArea.js b/packages/_components/src/ScrollableArea/ScrollableArea.js new file mode 100644 index 000000000..0a76c63e5 --- /dev/null +++ b/packages/_components/src/ScrollableArea/ScrollableArea.js @@ -0,0 +1,152 @@ +import './ScrollableArea.styl'; + +import React, { Component } from 'react'; + +import { Icon } from './../elements/Icon'; +import PropTypes from 'prop-types'; +import getScrollbarSize from '../utils/getScrollbarSize.js'; +import throttled from '../utils/throttled.js'; + +export class ScrollableArea extends Component { + static propTypes = { + children: PropTypes.node.isRequired, + class: PropTypes.string, + scrollableClass: PropTypes.string, + scrollX: PropTypes.bool, + scrollY: PropTypes.bool, + hideScrollbar: PropTypes.bool, + scrollStep: PropTypes.number, + }; + + static defaultProps = { + hideScrollbar: true, + class: 'flex-grow fit', + scrollY: true, + scrollX: false, + scrollStep: 100, + }; + + constructor(props) { + super(props); + + this.state = { + scrollAreaClasses: '', + }; + + this.scrollHandlerThrottled = throttled(150, this.scrollHandler); + } + + render() { + let scrollableClass = 'scrollable'; + if (this.props.scrollableClass) { + scrollableClass += ` ${this.props.scrollableClass}`; + } + if (this.props.scrollX) { + scrollableClass += ` scrollX`; + } + if (this.props.scrollY) { + scrollableClass += ` scrollY`; + } + + return ( +
+
{ + this.scrollableElement = element; + }} + onScroll={this.scrollHandlerThrottled} + onMouseEnter={this.scrollHandlerThrottled} + onTransitionEnd={this.scrollHandlerThrottled} + > + {this.props.children} +
+
+ {/* */} + +
+
+ +
+
+ ); + } + + componentDidMount() { + this.adjustMargins(); + this.scrollHandler(); + window.addEventListener('resize', this.adjustMargins); + } + + componentDidUpdate() { + this.adjustMargins(); + this.scrollHandler(); + } + + componentWillUnmount() { + window.removeEventListener('resize', this.adjustMargins); + } + + scrollNavDown = event => { + const { + scrollTop: currentTop, + offsetHeight: height, + scrollHeight, + } = this.scrollableElement; + + const limit = scrollHeight - height; + let scrollTop = currentTop + this.props.scrollStep; + scrollTop = scrollTop > limit ? limit : scrollTop; + this.scrollableElement.scrollTop = scrollTop; + }; + + scrollNavUp = event => { + const { scrollTop: currentTop } = this.scrollableElement; + + let scrollTop = currentTop - this.props.scrollStep; + scrollTop = scrollTop < 0 ? 0 : scrollTop; + + this.scrollableElement.scrollTop = scrollTop; + }; + + adjustMargins = () => { + if (this.props.hideScrollbar) { + const x = this.props.scrollX ? 1 : 0; + const y = this.props.scrollY ? 1 : 0; + const scrollbarSize = getScrollbarSize(); + this.scrollableElement.style.marginRight = `${0 - + scrollbarSize[0] * y}px`; + this.scrollableElement.style.marginBottom = `${0 - + scrollbarSize[1] * x}px`; + } + }; + + scrollHandler = () => { + const { + offsetHeight: height, + scrollTop: scrollTop, + scrollHeight, + } = this.scrollableElement; + let scrollAreaClasses = ''; + + // Check if can scroll up + if (scrollTop) { + scrollAreaClasses += 'canScrollUp'; + } + + // Check if can scroll down + if (scrollTop + height < scrollHeight) { + scrollAreaClasses += ' canScrollDown'; + } + + if (this.state.scrollAreaClasses !== scrollAreaClasses) { + this.setState({ + scrollAreaClasses, + }); + } + }; +} diff --git a/packages/_components/src/ScrollableArea/ScrollableArea.styl b/packages/_components/src/ScrollableArea/ScrollableArea.styl new file mode 100644 index 000000000..6c49f8197 --- /dev/null +++ b/packages/_components/src/ScrollableArea/ScrollableArea.styl @@ -0,0 +1,69 @@ +.scrollArea + overflow: hidden + position: relative + + .scrollable + max-height: inherit + overflow: hidden + zoom: 1 + + &.scrollX + overflow-x: scroll + + &.scrollY + overflow-y: scroll + + &.fit + height: 100% + width: 100% + + .scrollable + bottom: 0 + left: 0 + max-height: none + position: absolute + right: 0 + top: 0 + + .scrollNav + background-color: rgba(0, 0, 0, 0.75) + box-shadow: 0 0 10px 10px rgba(0, 0, 0, 0.75) + cursor: pointer + height: 24px + left: 10px + opacity: 0 + position: absolute + right: 10px + transition: all 0.3s ease + + .scrollNavIcon + color: var(--active-color); + display: block; + width: 18px; + height: 18px; + margin: 0 auto; + transition: color 0.3s ease; + + &:hover + background-color: rgba(0, 0, 0, 0.9) + box-shadow: 0 0 10px 10px rgba(0, 0, 0, 0.9) + + .scrollNavIcon + color: var(--hover-color) + + .scrollNavUp + border-bottom-left-radius: 12px + border-bottom-right-radius: 12px + top: 0 + transform: translateY(-24px) + + .scrollNavDown + border-top-left-radius: 12px + border-top-right-radius: 12px + bottom: 0 + transform: translateY(24px) + + &.canScrollUp .scrollNavUp, + &.canScrollDown .scrollNavDown + opacity: 1 + transform: translateY(0) diff --git a/packages/_components/src/__docs__/NameSpace.js b/packages/_components/src/__docs__/NameSpace.js new file mode 100644 index 000000000..81e1bfa34 --- /dev/null +++ b/packages/_components/src/__docs__/NameSpace.js @@ -0,0 +1,17 @@ +import React, { Fragment } from 'react'; +import { Link } from 'docz'; +import PropTypes from 'prop-types'; + +const NameSpace = ({ name }) => ( + + The namespace used on this component was:
{name}
Check the{' '} + translation docs{' '} + to see how to override it. +
+); + +NameSpace.propTypes = { + name: PropTypes.string.isRequired, +}; + +export default NameSpace; diff --git a/packages/_components/src/__docs__/assets/github.png b/packages/_components/src/__docs__/assets/github.png new file mode 100644 index 0000000000000000000000000000000000000000..bc8b2c2fe4a84bc72717d6bd712718620b34326f GIT binary patch literal 37650 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 E0cqBgWB>pF literal 0 HcmV?d00001 diff --git a/packages/_components/src/__docs__/assets/npm.png b/packages/_components/src/__docs__/assets/npm.png new file mode 100644 index 0000000000000000000000000000000000000000..f809797585e2d3677d1e42a97e164a319d611b21 GIT binary patch literal 9155 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 diff --git a/packages/_components/src/__docs__/getting-started.mdx b/packages/_components/src/__docs__/getting-started.mdx new file mode 100644 index 000000000..dd3cc7900 --- /dev/null +++ b/packages/_components/src/__docs__/getting-started.mdx @@ -0,0 +1,76 @@ +--- +name: Getting Started +route: /getting-started +--- + +# Getting Started + +## Installation + +> This component library is pre- v1.0. All realeases until a v1.0 have the +> possibility of introducing breaking changes. Please depend on an "exact" +> version in your projects to prevent issues caused by loose versioning. + +Install `react-viewerbase` from npm: + +```shell +// with npm +npm i react-viewerbase --save-exact + +// with yarn +yarn add react-viewerbase --exact +``` + +## Usage + +There is some setup required for `react-viewerbase`. We are working to eliminate +these requirements with the goal of providing ready to use components. + +### External Dependencies + +We have tried to keep the number of external dependencies limited to make setup +as easy as possible. You will, however, need to include either the default font +or the font you set for your theme: + +```js +// Google Fonts, Sanchez & Roboto +'https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700|Sanchez&display=swap'; +``` + +React Viewerbase also looks for theme CSS variabled defined on `:root`. You can +find a list of variables [on the theming page](/styling-and-theming) + +### Import & Use + +You can use components in your React app: + +```js +import React, { Component } from 'react'; +import { LayoutButton } from 'react-viewerbase'; + +class Example extends Component { + constructor(props) { + super(props); + + this.state = { + selectedCell: { + className: 'hover', + col: 1, + row: 1, + }, + }; + } + + render() { + return ( + this.setState({ selectedCell: cell })} + /> + ); + } +} +``` + +> NOTE: It is heavily recommended that you use a CSS reset or normalize. As well +> as the following style: `* { Box-sizing: Border-box }` diff --git a/packages/_components/src/__docs__/index.html b/packages/_components/src/__docs__/index.html new file mode 100644 index 000000000..263c5b2f1 --- /dev/null +++ b/packages/_components/src/__docs__/index.html @@ -0,0 +1,80 @@ + + + + + + + + + {{ title }} + + + + + + + {{ head }} + + +
+ {{ footer }} + + diff --git a/packages/_components/src/__docs__/introduction.mdx b/packages/_components/src/__docs__/introduction.mdx new file mode 100644 index 000000000..efdde28a1 --- /dev/null +++ b/packages/_components/src/__docs__/introduction.mdx @@ -0,0 +1,76 @@ +--- +name: Introduction +route: / +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { TableList, TableListItem } from './../index.js' + +# Introduction + +**React Viewerbase is a collection of components and utilities** that power +OHIF's [zero-footprint DICOM viewer](https://github.com/OHIF/Viewers) +([demo][demo-url]). We maintain them as a separate component library to: + +- Decouple presentation from business logic +- Test and develop components in isolation +- Provide well documented, reusable components +- Aid rapid application development for context specific viewers + +## Quick Example + +> This component library is pre- v1.0. All realeases until a v1.0 have the +> possibility of introducing breaking changes. Please depend on an "exact" +> version in your projects to prevent issues caused by loose versioning. + +```js +// Add 'react-viewerbase' as a dependency +yarn add react-viewerbase + +// Import and use components +import { TableList } from 'react-viewerbase' +``` + + + + {({ state, setState }) => ( + + {state.listItems.map((item, index) => { + return ( + alert('item clicked')} + > + + + ) + })} + + )} + + + +## FAQ + +... + + + + +[demo-url]: https://docs.ohif.org/demo/ + diff --git a/packages/_components/src/__docs__/styling-and-theming.mdx b/packages/_components/src/__docs__/styling-and-theming.mdx new file mode 100644 index 000000000..ce98dcde0 --- /dev/null +++ b/packages/_components/src/__docs__/styling-and-theming.mdx @@ -0,0 +1,56 @@ +--- +name: Styling & Theming +route: /styling-and-theming +--- + +# Styling & Theming + +```css +:root { + /* Interface UI Colors */ + --default-color: #9ccef9; + --hover-color: #ffffff; + --active-color: #20a5d6; + --ui-border-color: #44626f; + --ui-border-color-dark: #3c5d80; + --ui-border-color-active: #00a4d9; + --primary-background-color: #000000; + --box-background-color: #3e5975; + + --text-primary-color: #ffffff; + --text-secondary-color: #91b9cd; + --input-background-color: #2c363f; + --input-placeholder-color: #d3d3d3; + + --table-hover-color: #2c363f; + --table-text-primary-color: #ffffff; + --table-text-secondary-color: #91b9cd; + + --large-numbers-color: #6fbde2; + + --state-error: #ffcccc; + --state-error-border: #ffcccc; + --state-error-text: #ffcccc; + + /* Common palette */ + --ui-yellow: #e29e4a; + --ui-sky-blue: #6fbde2; + + /* State palette */ + --ui-state-error: #ffcccc; + --ui-state-error-border: #993333; + --ui-state-error-text: #661111; + --ui-gray-lighter: #436270; + --ui-gray-light: #516873; + --ui-gray: #263340; + --ui-gray-dark: #16202b; + --ui-gray-darker: #151a1f; + --ui-gray-darkest: #14202a; + + --calendar-day-color: #d3d3d3; + --calendar-day-border-color: #d3d3d3; + --calendar-day-active-hover-background-color: #516873; + --calendar-main-color: #263340; + --viewport-border-thickness: 1px; +} +``` diff --git a/packages/_components/src/__docs__/translating.mdx b/packages/_components/src/__docs__/translating.mdx new file mode 100644 index 000000000..44e810751 --- /dev/null +++ b/packages/_components/src/__docs__/translating.mdx @@ -0,0 +1,87 @@ +--- +name: Translating +route: /translating +--- + +# Translating + +**React Viewerbase** supports internationalization using +[i18next](https://www.i18next.com/) through the npm package +[@ohif/i18n](https://www.npmjs.com/package/@ohif/i18n), where is the main +instance of i18n containing several languages and tools. + +## How to translate components + +All you need is to use the internal custom superset of +[withTranslation HOC](https://react.i18next.com/latest/withtranslation-hoc), +available at `src/utils/LanguageProvider.js` + +e.g.: + +```js +import React from 'react'; +import { withTranslation } from '../utils/LanguageProvider.js'; + +function MyComponent({ t }) { + return

{t('my translated text')}

; +} + +export default withTranslation('MyNameSpace')(MyComponent); +``` + +--- + +## Namespaces + +For further information about namespaces and how they work, please visit +[OHIF Namespaces docs](https://docs.ohif.org/essentials/translating.html#namespaces). + +### How to get right the Name Space of a react-viewerbase component? + +Check the component's page in this site, the NameSpace information should be on +the bottom line. + +Also, a nameSpace is defined when exporting a component, this information can be +found on exports trough the code, in case of missing in docs. + +E.g. + +``` +export default withTranslation('TheNamespaceYouAreLookingFor')(TheComponentYouWantToUse); +``` + +### How to use another NameSpace inside the current NameSpace? + +i18next provides a parsing feature able to get translations strings from any +NameSpace, like this following example getting data from `InnerSpace` NameSpace: + +``` +$t(InnerSpace:Reset) +export default withTranslation('OuterSpace')(TheComponent); +``` + +--- + +## I18next Provider + +**React Viewerbase** pass down the `t` function trough the custom +[withTranslation HOC](https://github.com/OHIF/react-viewerbase/blob/master/src/utils/LanguageProvider.js). + +--- + +## Extending and Overriding languages + +[How to Extend languages with @ohif/i18n](https://docs.ohif.org/essentials/translating.html#extending-languages-in-ohifi18n) + +## Changing the language + +The language choosing method is the same implemented in +[OHIF Viewers](https://docs.ohif.org/essentials/translating.html#changing-the-language) + +> You can also change the language of the components displayed in this +> documentation by choosing an option in the +> [Docs Settings](https://react.ohif.org/docs-settings) section. + +## Debugging translations + +[How to Debug @ohif/i18n](https://docs.ohif.org/essentials/translating.html#debugging-translations) diff --git a/packages/_components/src/__docs__/wrapper.js b/packages/_components/src/__docs__/wrapper.js new file mode 100644 index 000000000..0e94c5559 --- /dev/null +++ b/packages/_components/src/__docs__/wrapper.js @@ -0,0 +1,22 @@ +import React from 'react'; + +import PropTypes from 'prop-types'; +import LanguageSwitcher from '../components/languageSwitcher'; + +import './wrapper.styl'; + +const Wrapper = ({ children }) => ( + +
+ Display components in: + +
+ {children} +
+); + +Wrapper.propTypes = { + children: PropTypes.node.isRequired, +}; + +export default Wrapper; diff --git a/packages/_components/src/__docs__/wrapper.styl b/packages/_components/src/__docs__/wrapper.styl new file mode 100644 index 000000000..b1a4dae6d --- /dev/null +++ b/packages/_components/src/__docs__/wrapper.styl @@ -0,0 +1,15 @@ +[class*="Sidebar__Menus"] + @media (min-width: 1120px) + margin-top: 80px + +.sidebarLanguageSwitcher + position: fixed + top: 155px + left 0 + z-index: 1049 + padding: 0 24px 20px + border-bottom: 1px dotted #CED4DE + background: #F5F6F7 + transition: all 200ms ease + @media (max-width: 1119px) + left -100% diff --git a/packages/_components/src/components/checkbox/__docs__/checkbox.mdx b/packages/_components/src/components/checkbox/__docs__/checkbox.mdx new file mode 100644 index 000000000..dd74a6af5 --- /dev/null +++ b/packages/_components/src/components/checkbox/__docs__/checkbox.mdx @@ -0,0 +1,42 @@ +--- +name: Checkbox +menu: Elements +route: /components/checkbox +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { Checkbox } from './../index' +import NameSpace from '../../../__docs__/NameSpace' + +# Checkbox + +## Basic usage + + + + {({ state, setState }) => ( + +
+
{JSON.stringify(state, null, 2)}
+
+
+ +
+
+ )} +
+
+ +## API + + + +## Translation Namespace + + \ No newline at end of file diff --git a/packages/_components/src/components/checkbox/checkbox.css b/packages/_components/src/components/checkbox/checkbox.css new file mode 100644 index 000000000..96311b6fd --- /dev/null +++ b/packages/_components/src/components/checkbox/checkbox.css @@ -0,0 +1,56 @@ +.ohif-check-container { + --check-button-dim: 15px; +} + +.ohif-check-container input { + position: absolute; + opacity: 0; + height: inherit; + width: inherit; + cursor: default; +} + +.ohif-check-container { + display: block; + position: relative; + padding-left: 35px; + margin-bottom: 12px; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.ohif-checkbox { + width: var(--check-button-dim); + height: var(--check-button-dim); + position: absolute; + top: 20%; + left: 5%; + cursor: pointer; + background-color: var(--ui-gray-lighter); +} + +.ohif-checkbox:hover { + background-color: var(--default-color); +} + +.ohif-checkbox:after { + content: ''; + position: absolute; + width: 70%; + height: 70%; +} + +.ohif-checkbox.ohif-checked:after { + display: block; + top: 14%; + left: 14%; + background: white; +} + +.ohif-check-label { + font-size: 12px; + font-weight: 500; +} diff --git a/packages/_components/src/components/checkbox/checkbox.js b/packages/_components/src/components/checkbox/checkbox.js new file mode 100644 index 000000000..5f1f16ea3 --- /dev/null +++ b/packages/_components/src/components/checkbox/checkbox.js @@ -0,0 +1,46 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import './checkbox.css'; + +export class Checkbox extends Component { + static propTypes = { + label: PropTypes.string.isRequired, + checked: PropTypes.bool, + }; + + constructor(props) { + super(props); + this.state = { checked: props.checked, label: props.label }; + } + + handleChange(e) { + this.setState({ checked: e.target.checked }); + } + + render() { + let checkbox; + if (this.state.checked) { + checkbox = ; + } else { + checkbox = ; + } + + return ( +
+
+ +
+
+ ); + } +} diff --git a/packages/_components/src/components/checkbox/index.js b/packages/_components/src/components/checkbox/index.js new file mode 100644 index 000000000..9d1514175 --- /dev/null +++ b/packages/_components/src/components/checkbox/index.js @@ -0,0 +1 @@ +export { Checkbox } from './checkbox.js'; diff --git a/packages/_components/src/components/cineDialog/CineDialog.js b/packages/_components/src/components/cineDialog/CineDialog.js new file mode 100644 index 000000000..854f80695 --- /dev/null +++ b/packages/_components/src/components/cineDialog/CineDialog.js @@ -0,0 +1,202 @@ +import './CineDialog.styl'; + +import React, { PureComponent } from 'react'; +import { withTranslation } from '../../utils/LanguageProvider'; +import { Icon } from './../../elements/Icon'; +import PropTypes from 'prop-types'; + +class CineDialog extends PureComponent { + constructor(props) { + super(props); + + this.state = { + cineFrameRate: props.cineFrameRate, + isPlaying: props.isPlaying, + }; + } + + static propTypes = { + /** Minimum value for range slider */ + cineMinFrameRate: PropTypes.number.isRequired, + /** Maximum value for range slider */ + cineMaxFrameRate: PropTypes.number.isRequired, + /** Increment range slider can "step" in either direction. */ + cineStepFrameRate: PropTypes.number.isRequired, + cineFrameRate: PropTypes.number.isRequired, + /** 'True' if playing, 'False' if paused. */ + isPlaying: PropTypes.bool.isRequired, + onPlayPauseChanged: PropTypes.func, + onFrameRateChanged: PropTypes.func, + onClickNextButton: PropTypes.func, + onClickBackButton: PropTypes.func, + onClickSkipToStart: PropTypes.func, + onClickSkipToEnd: PropTypes.func, + /** i18next translation function */ + t: PropTypes.func.isRequired, + }; + + static defaultProps = { + cineMinFrameRate: 1, + cineMaxFrameRate: 90, + cineStepFrameRate: 1, + cineFrameRate: 24, + isPlaying: false, + }; + + componentDidUpdate(prevProps) { + // TODO: Not sure if we should just switch this to a stateless + // fully-controlled component instead + if ( + this.props.isPlaying !== prevProps.isPlaying || + this.props.isPlaying !== this.state.isPlaying + ) { + this.setState({ + isPlaying: this.props.isPlaying, + }); + } + + if ( + this.props.cineFrameRate !== prevProps.cineFrameRate || + this.props.cineFrameRate !== this.state.cineFrameRate + ) { + this.setState({ + cineFrameRate: this.props.cineFrameRate, + }); + } + } + + handleInputChange = event => { + const target = event.target; + + let value = target.value; + + if (target.type === 'range') { + value = parseFloat(target.value); + } + + const name = target.name; + + this.setState({ + [name]: value, + }); + + if (name === 'cineFrameRate' && this.props.onFrameRateChanged) { + this.props.onFrameRateChanged(parseFloat(value)); + } + }; + + onClickPlayPause = () => { + const value = !this.state.isPlaying; + + this.setState({ + isPlaying: value, + }); + + if (this.props.onPlayPauseChanged) { + this.props.onPlayPauseChanged(value); + } + }; + + onClickNextButton = event => { + if (this.props.onClickNextButton) { + this.props.onClickNextButton(event); + } + }; + + onClickBackButton = event => { + if (this.props.onClickBackButton) { + this.props.onClickBackButton(event); + } + }; + + onClickSkipToStart = event => { + if (this.props.onClickSkipToStart) { + this.props.onClickSkipToStart(event); + } + }; + + onClickSkipToEnd = event => { + if (this.props.onClickSkipToEnd) { + this.props.onClickSkipToEnd(event); + } + }; + + // TODO: + // - Add next / previous display set buttons which just call + // onClickNextDisplaySet and onClickPreviousDisplaySet which are passed in as props. + // See https://github.com/OHIF/Viewers/blob/master/Packages/ohif-viewerbase/client/components/viewer/cineDialog/cineDialog.html#L38 + // - Add 'isEnabled' prop: https://github.com/OHIF/Viewers/blob/master/Packages/ohif-viewerbase/client/components/viewer/cineDialog/cineDialog.js#L301 + render() { + const { t } = this.props; + return ( +
+
+
+
+ + + + + +
+
+
+
+ +
+ + {this.state.cineFrameRate.toFixed(1)} {t('fps')} + +
+
+
+ ); + } +} + +const connectedComponent = withTranslation('CineDialog')(CineDialog); +export { connectedComponent as CineDialog }; +export default connectedComponent; diff --git a/packages/_components/src/components/cineDialog/CineDialog.styl b/packages/_components/src/components/cineDialog/CineDialog.styl new file mode 100644 index 000000000..76aeb4076 --- /dev/null +++ b/packages/_components/src/components/cineDialog/CineDialog.styl @@ -0,0 +1,169 @@ +.CineDialog + background: var(--ui-gray-darkest); + color: var(--text-secondary-color); + filter: drop-shadow(0 0 3px var(--ui-gray-darkest)) + border: none + border-radius: 8px + width: inherit; + + h5 + font-size: 20px + line-height: 35px + margin: 0 + + h5, label + font-weight: 400 + + .cine-navigation, .cine-controls, .cine-options + cursor: default + + .fps-section + input[type="range"] + background-color: transparent + border: 0 none + outline: 0 none + + &::-ms-tooltip + display: none + + .double-row-style + box-sizing: border-box + width: 290px + height: 70px + padding: 10px + top: 2% + left: 35% + + .cine-navigation + position: absolute + right: 16px + top: 10px + + .btn + padding: 0 4px + + i + font-size: 32px + line-height: 32px + + .cine-controls + left: 0px + + .cine-options + padding: 0px 0 + + .fps-section + width: 175px + float: left + + .fps + float: right + + + .single-row-style + box-sizing: content-box + width: 425px + height: 45px + padding: 4px 8px + top: 2% + left: 35% + + .cine-navigation + float: right + overflow: hidden + padding-right: 12px + position: relative + + .btn + padding: 0 2px + margin: 0 0 0 2px + border: 0 none + &:first-of-type + margin-left: 0 + + i + font-size: 32px + line-height: 45px + + .cine-controls + position: relative + float: left + overflow: hidden + + .btn + font-size: 22px + line-height: 45px + min-width: 28px + padding: 0 0px + margin: 0 4px + border: 0 none + + .cine-options + display: block + position: relative + width: 150px + float: left + overflow: hidden + padding: 0 + margin: 0 0 0 10px + + .fps-section + display: block + float: left + width: 80px + input[type="range"] + line-height: 45px + height: 45px + + .fps + display: block + width: 68px + float: left + padding: 0 + margin: 0 0 0 2px + text-align: center + line-height: 45px + font-size: 14px; + line-height: 1.42857143; + + .btn + color: var(--text-secondary-color); + background-color: transparent + border: 0 + border-radius: 4px + font-size: 15px + width: 45px + font-weight: normal + height: 37px + line-height: 37px + padding: 0 12px + + &:hover, &:active, &.active + color: var(--hover-color); + + &[disabled] + &:hover + color: inherit + &:active + color: var(--text-secondary-color); + + &.btn-primary, &.btn-secondary, &.btn-danger + transition: background-color 0.3s ease + + &:hover, &:active, &:focus + opacity: 0.8 + + &.btn-primary + background-color: var(--active-color); + color: var(--primary-background-color) + + &.btn-secondary + background-color: var(--text-secondary-color); + color: var(--primary-background-color) + + &.btn-danger + background-color: var(--text-destructive-color); + color: var(--text-primary-color); + + &.active + box-shadow: inset 1px 1px 2px var(--ui-gray-darkest) diff --git a/packages/_components/src/components/cineDialog/__docs__/cineDialog.mdx b/packages/_components/src/components/cineDialog/__docs__/cineDialog.mdx new file mode 100644 index 000000000..48ecfa0fd --- /dev/null +++ b/packages/_components/src/components/cineDialog/__docs__/cineDialog.mdx @@ -0,0 +1,57 @@ +--- +name: CINE Dialog +menu: Components +route: /components/cine-dialog +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { CineDialog } from './../index' +import NameSpace from '../../../__docs__/NameSpace' + +# CINE Dialog + +## Basic usage + + + + {({ state, setState }) => ( + +
+
{JSON.stringify(state, null, 2)}
+
+
+ + setState({ lastChange: 'Clicked SkipToStart' }) + } + onClickSkipToEnd={() => + setState({ lastChange: 'Clicked SkipToEnd' }) + } + onClickNextButton={() => setState({ lastChange: 'Clicked Next' })} + onClickBackButton={() => setState({ lastChange: 'Clicked Back' })} + onLoopChanged={value => setState({ isLoopEnabled: value })} + onFrameRateChanged={value => setState({ cineFrameRate: value })} + onPlayPauseChanged={() => setState({ isPlaying: !state.isPlaying })} + /> +
+
+ )} +
+
+ +## API + + + +## Translation Namespace + + diff --git a/packages/_components/src/components/cineDialog/index.js b/packages/_components/src/components/cineDialog/index.js new file mode 100644 index 000000000..5827ce588 --- /dev/null +++ b/packages/_components/src/components/cineDialog/index.js @@ -0,0 +1,2 @@ +import CineDialog from './CineDialog'; +export { CineDialog }; diff --git a/packages/_components/src/components/index.js b/packages/_components/src/components/index.js new file mode 100644 index 000000000..bc9beb74a --- /dev/null +++ b/packages/_components/src/components/index.js @@ -0,0 +1,48 @@ +import { + ExampleDropTarget, + StudyBrowser, + ThumbnailEntry, +} from './studyBrowser'; +import { LayoutButton, LayoutChooser } from './layoutButton'; +import { MeasurementTable, MeasurementTableItem } from './measurementTable'; +import { Overlay, OverlayTrigger } from './overlayTrigger'; +import { TableList, TableListItem } from './tableList'; +import { + AboutModal, + UserPreferences, + UserPreferencesModal, +} from './userPreferencesModal'; + +import { CineDialog } from './cineDialog'; +import { QuickSwitch } from './quickSwitch'; +import { RoundedButtonGroup } from './roundedButtonGroup'; +import { SelectTree } from './selectTree'; +import { SimpleDialog } from './simpleDialog'; +import { StudyList } from './studyList'; +import { ToolbarSection } from './toolbarSection'; +import { Tooltip } from './tooltip'; + +export { + CineDialog, + ExampleDropTarget, + LayoutButton, + LayoutChooser, + MeasurementTable, + MeasurementTableItem, + Overlay, + OverlayTrigger, + QuickSwitch, + RoundedButtonGroup, + SelectTree, + SimpleDialog, + StudyBrowser, + StudyList, + TableList, + TableListItem, + ThumbnailEntry, + ToolbarSection, + Tooltip, + AboutModal, + UserPreferences, + UserPreferencesModal, +}; diff --git a/packages/_components/src/components/languageSwitcher/LanguageSwitcher.js b/packages/_components/src/components/languageSwitcher/LanguageSwitcher.js new file mode 100644 index 000000000..2d17305bd --- /dev/null +++ b/packages/_components/src/components/languageSwitcher/LanguageSwitcher.js @@ -0,0 +1,66 @@ +import React, { useState, useEffect } from 'react'; +import i18n from '@ohif/i18n'; + +import './LanguageSwitcher.styl'; +import { withTranslation } from '../../utils/LanguageProvider'; + +const LanguageSwitcher = () => { + const getCurrentLanguage = (language = i18n.language) => + language.split('-')[0]; + + const [currentLanguage, setCurrentLanguage] = useState(getCurrentLanguage()); + const languages = [ + // TODO: list of available languages should come from i18n.options.resources + { + value: 'en', + label: 'English', + }, + { + value: 'es', + label: 'Spanish', + }, + ]; + + const onChange = () => { + const { value } = event.target; + const language = getCurrentLanguage(value); + setCurrentLanguage(language); + + i18n.init({ + fallbackLng: language, + lng: language, + }); + }; + + useEffect(() => { + let mounted = true; + + i18n.on('languageChanged', () => { + if (mounted) { + setCurrentLanguage(getCurrentLanguage()); + } + }); + + return () => { + mounted = false; + }; + }, []); + + return ( + + ); +}; + +export default withTranslation('UserPreferencesModal')(LanguageSwitcher); diff --git a/packages/_components/src/components/languageSwitcher/LanguageSwitcher.styl b/packages/_components/src/components/languageSwitcher/LanguageSwitcher.styl new file mode 100644 index 000000000..51284c874 --- /dev/null +++ b/packages/_components/src/components/languageSwitcher/LanguageSwitcher.styl @@ -0,0 +1,4 @@ +.language-select + color: var(--primary-background-color) + display: block + min-width: 150px diff --git a/packages/_components/src/components/languageSwitcher/index.js b/packages/_components/src/components/languageSwitcher/index.js new file mode 100644 index 000000000..31505c03b --- /dev/null +++ b/packages/_components/src/components/languageSwitcher/index.js @@ -0,0 +1 @@ +export { default } from './LanguageSwitcher'; diff --git a/packages/_components/src/components/layoutButton/LayoutButton.js b/packages/_components/src/components/layoutButton/LayoutButton.js new file mode 100644 index 000000000..fd6bdee6a --- /dev/null +++ b/packages/_components/src/components/layoutButton/LayoutButton.js @@ -0,0 +1,64 @@ +import React, { PureComponent } from 'react'; + +import { LayoutChooser } from './LayoutChooser.js'; +import PropTypes from 'prop-types'; +import ToolbarButton from '../../viewer/ToolbarButton'; + +export class LayoutButton extends PureComponent { + static defaultProps = { + dropdownVisible: false, + }; + + static propTypes = { + dropdownVisible: PropTypes.bool.isRequired, + /** Called with the selectedCell number when grid sell is selected */ + onChange: PropTypes.func, + /** The cell to show as selected */ + selectedCell: PropTypes.object, + }; + + state = { + dropdownVisible: this.props.dropdownVisible, + }; + + componentDidUpdate(prevProps) { + if (this.props.dropdownVisible !== prevProps.dropdownVisible) { + this.setState({ + dropdownVisible: this.props.dropdownVisible, + }); + } + } + + onClick = () => { + this.setState({ + dropdownVisible: !this.state.dropdownVisible, + }); + }; + + onChange = selectedCell => { + if (this.props.onChange) { + this.props.onChange(selectedCell); + } + }; + + render() { + return ( +
+ + +
+ ); + } +} + +export default LayoutButton; diff --git a/packages/_components/src/components/layoutButton/LayoutChooser.js b/packages/_components/src/components/layoutButton/LayoutChooser.js new file mode 100644 index 000000000..1603788b1 --- /dev/null +++ b/packages/_components/src/components/layoutButton/LayoutChooser.js @@ -0,0 +1,125 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import './LayoutChooser.styl'; + +/** + * Adds the 'hover' class to cells above and to the left of the current cell + * This is used to "fill in" the grid that the user will change the layout to, + * if they click on a specific table cell. + **/ + +class LayoutChooser extends PureComponent { + static propTypes = { + rows: PropTypes.number.isRequired, + columns: PropTypes.number.isRequired, + visible: PropTypes.bool.isRequired, + selectedCell: PropTypes.object, + boxSize: PropTypes.number.isRequired, + cellBorder: PropTypes.number.isRequired, + onClick: PropTypes.func, + onChange: PropTypes.func, + }; + + static defaultProps = { + rows: 3, + columns: 3, + visible: true, + boxSize: 20, + cellBorder: 1, + selectedCell: { + row: -1, + col: -1, + }, + }; + + constructor(props) { + super(props); + this.emptyCell = { + row: -1, + column: -1, + }; + this.state = { + table: [[]], + selectedCell: this.props.selectedCell, + }; + } + componentDidMount() { + this.highlightCells(this.emptyCell); + } + onClick(currentCell) { + this.setState({ + selectedCell: currentCell, + }); + this.highlightCells(currentCell); + if (this.props.onClick) { + this.props.onClick(currentCell); + } + if (this.props.onChange) { + this.props.onChange(currentCell); + } + } + isRange = (cell, parentCell) => { + return cell.row <= parentCell.row && cell.col <= parentCell.col; + }; + highlightCells = currentCell => { + let table = []; + for (let row = 0; row < this.props.rows; row++) { + let newRow = []; + for (let col = 0; col < this.props.columns; col++) { + let cell = { row: row, col: col }; + if (this.isRange(cell, currentCell)) { + cell.className = 'hover'; + } else if ( + this.isRange(currentCell, this.emptyCell) && + this.isRange(cell, this.state.selectedCell) + ) { + cell.className = 'selectedBefore'; + } + newRow.push(cell); + } + table.push(newRow); + } + this.setState({ table: table }); + }; + + render() { + let columns = this.props.columns; + const style = { + display: this.props.visible ? 'block' : 'none', + minWidth: + columns * this.props.boxSize + (columns + 5) * this.props.cellBorder, + }; + return ( +
+ + + {this.state.table.map((row, i) => { + return ( + + {row.map((cell, j) => { + return ( + + ); + })} + +
this.highlightCells(cell)} + onMouseLeave={() => this.highlightCells(this.emptyCell)} + onClick={() => this.onClick(cell)} + /> + ); + })} +
+
+ ); + } +} + +export { LayoutChooser }; diff --git a/packages/_components/src/components/layoutButton/LayoutChooser.styl b/packages/_components/src/components/layoutButton/LayoutChooser.styl new file mode 100644 index 000000000..2b5b0fc04 --- /dev/null +++ b/packages/_components/src/components/layoutButton/LayoutChooser.styl @@ -0,0 +1,46 @@ +@import './../../design/styles/common/button.styl' + +$borderColor = rgba(77, 99, 110, 0.81) + +.dropdown-menu + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + font-size: 14px; + text-align: left; + list-style: none; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0,0,0,.15); + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0,0,0,.175); + box-shadow: 0 6px 12px rgba(0,0,0,.175); + +.layoutChooser + .selectedBefore + background-color: #5cc3eb + + border: 1px solid $borderColor; + border-radius: 8px + padding: 5px 0; + position: absolute; + z-index: 5000 + + table + margin: 0 auto + border-spacing: 0; + border-collapse: collapse; + td + cursor: pointer + transition(background-color 0.1s ease) + + &:hover, &.hover // Add the hover class here to be triggered by mouseenter/mouseleave + background-color: #209ac9 diff --git a/packages/_components/src/components/layoutButton/__docs__/layoutButton.css b/packages/_components/src/components/layoutButton/__docs__/layoutButton.css new file mode 100644 index 000000000..21ec59d7e --- /dev/null +++ b/packages/_components/src/components/layoutButton/__docs__/layoutButton.css @@ -0,0 +1,10 @@ +.LayoutExample { + background-color: var(--primary-background-color); + width: 160px; + height: 160px; +} + +.GridLayout td { + padding: 5px; + border: 1px solid black; +} diff --git a/packages/_components/src/components/layoutButton/__docs__/layoutButton.mdx b/packages/_components/src/components/layoutButton/__docs__/layoutButton.mdx new file mode 100644 index 000000000..fc6a5a6ac --- /dev/null +++ b/packages/_components/src/components/layoutButton/__docs__/layoutButton.mdx @@ -0,0 +1,49 @@ +--- +name: Layout Button +menu: Components +route: /components/layout-button +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { LayoutButton } from './../index.js' +import './layoutButton.css' +import NameSpace from '../../../__docs__/NameSpace' + +# Layout Button + +## Basic usage + + + + {({ state, setState }) => ( + + {/* STATE */} +
{JSON.stringify(state, null, 2) }
+ + {/* COMPONENT */} +
+ setState({ selectedCell: cell })} + /> +
+
+ )} +
+ +
+ +## API + + + +## Translation Namespace + + diff --git a/packages/_components/src/components/layoutButton/index.js b/packages/_components/src/components/layoutButton/index.js new file mode 100644 index 000000000..b7615590a --- /dev/null +++ b/packages/_components/src/components/layoutButton/index.js @@ -0,0 +1,2 @@ +export { LayoutButton } from './LayoutButton.js'; +export { LayoutChooser } from './LayoutChooser.js'; diff --git a/packages/_components/src/components/measurementTable/MeasurementTable.js b/packages/_components/src/components/measurementTable/MeasurementTable.js new file mode 100644 index 000000000..6532e0baa --- /dev/null +++ b/packages/_components/src/components/measurementTable/MeasurementTable.js @@ -0,0 +1,178 @@ +import './MeasurementTable.styl'; + +import React, { Component } from 'react'; +import { withTranslation } from '../../utils/LanguageProvider'; + +import { Icon } from './../../elements/Icon'; +import { MeasurementTableItem } from './MeasurementTableItem.js'; +import { OverlayTrigger } from './../overlayTrigger'; +import PropTypes from 'prop-types'; +import { ScrollableArea } from './../../ScrollableArea/ScrollableArea.js'; +import { TableList } from './../tableList'; +import { Tooltip } from './../tooltip'; + +class MeasurementTable extends Component { + static propTypes = { + measurementCollection: PropTypes.array.isRequired, + timepoints: PropTypes.array.isRequired, + overallWarnings: PropTypes.object.isRequired, + readOnly: PropTypes.bool, + onItemClick: PropTypes.func, + onRelabelClick: PropTypes.func, + onDeleteClick: PropTypes.func, + onEditDescriptionClick: PropTypes.func, + selectedMeasurementNumber: PropTypes.number, + overwallWarnings: PropTypes.object, + t: PropTypes.func, + }; + + static defaultProps = { + overallWarnings: { + warningList: [], + }, + readOnly: false, + }; + + state = { + selectedKey: null, + }; + + render() { + const hasOverallWarnings = + this.props.overallWarnings.warningList.length > 0; + return ( +
+
+ {hasOverallWarnings && ( + +
+ {this.props.t('Criteria nonconformities')} +
+
+ {this.getWarningContent()} +
+ + } + > + + + + + +
+ )} + {this.getTimepointsHeader()} +
+ +
{this.getMeasurementsGroups()}
+
+
+ ); + } + + getMeasurementsGroups = () => { + return this.props.measurementCollection.map((measureGroup, index) => { + return ( + + {this.getMeasurements(measureGroup)} + + ); + }); + }; + + getMeasurements = measureGroup => { + const selectedKey = this.props.selectedMeasurementNumber + ? this.props.selectedMeasurementNumber + : this.state.selectedKey; + return measureGroup.measurements.map((measurement, index) => { + const key = measurement.measurementNumber; + const itemIndex = measurement.itemNumber || index + 1; + const itemClass = + selectedKey === key && !this.props.readOnly ? 'selected' : ''; + + return ( + + ); + }); + }; + + onItemClick = (event, measurementData) => { + if (this.props.readOnly) return; + + this.setState({ + selectedKey: measurementData.measurementNumber, + }); + + if (this.props.onItemClick) { + this.props.onItemClick(event, measurementData); + } + }; + + getCustomHeader = measureGroup => { + return ( + +
+ {this.props.t(measureGroup.groupName)} +
+ {measureGroup.maxMeasurements && ( +
+ {this.props.t('MAX')} {measureGroup.maxMeasurements} +
+ )} +
{measureGroup.measurements.length}
+
+ ); + }; + + getTimepointsHeader = () => { + return this.props.timepoints.map((timepoint, index) => { + return ( +
+
{timepoint.label}
+
{timepoint.date}
+
+ ); + }); + }; + + getWarningContent = () => { + const { warningList = '' } = this.props.overwallWarnings; + + if (Array.isArray(warningList)) { + const listedWarnings = warningList.map((warn, index) => { + return
  • {warn}
  • ; + }); + + return
      {listedWarnings}
    ; + } else { + return {warningList}; + } + }; +} + +const connectedComponent = withTranslation('MeasurementTable')( + MeasurementTable +); +export { connectedComponent as MeasurementTable }; +export default connectedComponent; diff --git a/packages/_components/src/components/measurementTable/MeasurementTable.styl b/packages/_components/src/components/measurementTable/MeasurementTable.styl new file mode 100644 index 000000000..14cf50e4d --- /dev/null +++ b/packages/_components/src/components/measurementTable/MeasurementTable.styl @@ -0,0 +1,92 @@ +.measurementTable + background-color: var(--primary-background-color) + height: 100%; + width: 100%; + display: flex; + flex-direction: column; + + .maxMeasurements + border-radius: 3px; + color: var(--primary-background-color); + font-size: 12px; + font-weight: 500; + height: 19px; + line-height: 17px; + margin-top: 22px; + margin-right: 15px; + padding: 2px 6px 0; + text-transform: uppercase; + transition: all 0.3s ease; + background-color: var(--default-color); + + .measurementTableHeader + padding-left: 45px; + + .measurementTableHeaderItem + display: inline-block; + margin-top: 9px; + margin-left: 9px; + padding-left: 9px; + width: 90px; + font-size: 14px; + line-height: 20px; + border-left: 1px solid var(--text-secondary-color); + color: var(--text-primary-color); + + .timepointLabel + color: var(--text-secondary-color); + font-size: 12px; + + .warning-status + display: block; + position: absolute; + width: 50px; + height: 40px; + padding-left: 7px; + left: 15px; + top: 7px; + + .warning-border + display: block; + border: 2px solid #e29e4a; + border-radius: 36px; + padding: 6px 7px; + width: 40px; + height: 40px; + + svg + color: #e29e4a; + width: 22px; + height: 20px; + pointer-events: inherit; + +.tooltip-warning + &.in + opacity 1; + + .tooltip-inner + padding: 0 + + .warningTitle + text-align: left + background-color: var(--ui-gray-darkest); + padding: 7px 15px; + border-top-left-radius: 7px; + border-top-right-radius: 7px; + + .warningContent + text-align center + background-color: var(--ui-gray); + border-bottom-left-radius: 7px; + border-bottom-right-radius: 7px; + + ol + margin-top: auto; + padding-inline-start: 20px; + padding-top: 10px; + padding-bottom: 10px; + padding-left: 25px; + text-align: left; + + li + padding-right: 10px; diff --git a/packages/_components/src/components/measurementTable/MeasurementTableItem.js b/packages/_components/src/components/measurementTable/MeasurementTableItem.js new file mode 100644 index 000000000..b1e42e038 --- /dev/null +++ b/packages/_components/src/components/measurementTable/MeasurementTableItem.js @@ -0,0 +1,162 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from '../../utils/LanguageProvider'; + +import { Icon } from './../../elements/Icon'; +import { OverlayTrigger } from './../overlayTrigger'; +import { Tooltip } from './../tooltip'; +import { TableListItem } from './../tableList/TableListItem.js'; + +import './MeasurementTableItem.styl'; + +class MeasurementTableItem extends Component { + static propTypes = { + measurementData: PropTypes.object.isRequired, + onItemClick: PropTypes.func.isRequired, + onRelabel: PropTypes.func, + onDelete: PropTypes.func, + onEditDescription: PropTypes.func, + itemClass: PropTypes.string, + itemIndex: PropTypes.number, + t: PropTypes.func, + }; + + render() { + const { warningTitle = '', hasWarnings } = this.props.measurementData; + + return ( + + {hasWarnings ? ( + +
    {this.props.t(warningTitle)}
    +
    {this.getWarningContent()}
    + + } + > +
    {this.getTableListItem()}
    +
    + ) : ( + {this.getTableListItem()} + )} +
    + ); + } + + getActionButton = (btnLabel, onClickCallback) => { + return ( + + ); + }; + + getTableListItem = () => { + const hasWarningClass = this.props.measurementData.hasWarnings + ? 'hasWarnings' + : ''; + + const actionButtons = []; + + if (typeof this.props.onRelabel === 'function') { + const relabelButton = this.getActionButton( + 'Relabel', + this.onRelabelClick + ); + actionButtons.push(relabelButton); + } + if (typeof this.props.onEditDescription === 'function') { + const descriptionButton = this.getActionButton( + 'Description', + this.onEditDescriptionClick + ); + actionButtons.push(descriptionButton); + } + if (typeof this.props.onDelete === 'function') { + const deleteButton = this.getActionButton('Delete', this.onDeleteClick); + actionButtons.push(deleteButton); + } + + return ( + +
    +
    + {this.props.t(this.props.measurementData.label)} +
    +
    {this.getDataDisplayText()}
    +
    {actionButtons}
    +
    +
    + ); + }; + + onItemClick = event => { + this.props.onItemClick(event, this.props.measurementData); + }; + + onRelabelClick = event => { + // Prevent onItemClick from firing + event.stopPropagation(); + + this.props.onRelabel(event, this.props.measurementData); + }; + + onEditDescriptionClick = event => { + // Prevent onItemClick from firing + event.stopPropagation(); + + this.props.onEditDescription(event, this.props.measurementData); + }; + + onDeleteClick = event => { + // Prevent onItemClick from firing + event.stopPropagation(); + + this.props.onDelete(event, this.props.measurementData); + }; + + getDataDisplayText = () => { + return this.props.measurementData.data.map((data, index) => { + return ( +
    + {data.displayText ? data.displayText : '...'} +
    + ); + }); + }; + + getWarningContent = () => { + const { warningList = '' } = this.props.measurementData; + + if (Array.isArray(warningList)) { + const listedWarnings = warningList.map((warn, index) => { + return
  • {warn}
  • ; + }); + + return
      {listedWarnings}
    ; + } else { + return {warningList}; + } + }; +} + +const connectedComponent = withTranslation('MeasurementTable')( + MeasurementTableItem +); +export { connectedComponent as MeasurementTableItem }; +export default connectedComponent; diff --git a/packages/_components/src/components/measurementTable/MeasurementTableItem.styl b/packages/_components/src/components/measurementTable/MeasurementTableItem.styl new file mode 100644 index 000000000..8f1e07bef --- /dev/null +++ b/packages/_components/src/components/measurementTable/MeasurementTableItem.styl @@ -0,0 +1,61 @@ +.measurementItem + .rowActions + background-color: var(--ui-gray-darker) + height: 0 + overflow: hidden + transition: all 0.3s ease + visibility: hidden + padding-left: 14px + + .btnAction + background-color: transparent; + border: none; + color: var(--default-color) + cursor: pointer + line-height: 35px + height: 35px + transition: all 0.3s ease + + &:hover, &:active + color: var(--text-primary-color) + + i + margin-right: 4px; + + &.selected + .rowActions + height: 35px + visibility: visible + + .measurementLocation + margin-left: 9px; + margin-top: 9px; + + .measurementDisplayText + display: inline-block; + margin-top: 9px; + margin-left: 9px; + padding-left: 9px; + width: 90px; + border-left: 1px solid var(--text-secondary-color); + color: var(--text-primary-color); + + .itemIndex + -webkit-box-sizing: initial; + -moz-box-sizing: initial; + box-sizing: initial; + + &.hasWarnings + .itemIndex + opacity: 1 + background-color: #e29e4a; + color: #fff; + + .warning-icon + display: block; + margin: 7px auto 0; + + svg + width: 22px; + height: 20px; + pointer-events: inherit; diff --git a/packages/_components/src/components/measurementTable/__docs__/measurementTable.mdx b/packages/_components/src/components/measurementTable/__docs__/measurementTable.mdx new file mode 100644 index 000000000..d85d87490 --- /dev/null +++ b/packages/_components/src/components/measurementTable/__docs__/measurementTable.mdx @@ -0,0 +1,49 @@ +--- +name: Measurement Table +menu: Components +route: /components/measurement-table +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { MeasurementTable } from './../index.js' +import NameSpace from '../../../__docs__/NameSpace' +// +import measurements from './measurements.js' +import timepoints from './timepoints.js' +import warnings from './warnings.js' + +# Measurement Table + +## Basic usage + + + + {({ state, setState }) => ( +
    + alert('onItemClick')} + onRelabelClick={() => alert('onRelabelClick')} + onDeleteClick={() => alert('onDeleteClick')} + onEditDescriptionClick={() => alert('onEditDescriptionClick')} + /> +
    + )} +
    +
    + +## API + + + +## Translation Namespace + + diff --git a/packages/_components/src/components/measurementTable/__docs__/measurements.js b/packages/_components/src/components/measurementTable/__docs__/measurements.js new file mode 100644 index 000000000..2a0507902 --- /dev/null +++ b/packages/_components/src/components/measurementTable/__docs__/measurements.js @@ -0,0 +1,93 @@ +export default [ + { + maxMeasurements: 5, + groupName: 'Targets', + measurements: [ + { + measurementId: '123', + measurementNumber: '123', + itemNumber: 1, + label: 'Chest Wall Posterior', + hasWarnings: true, + warningTitle: 'Criteria nonconformities', + isSplitLesion: false, + warningList: [ + 'All measurements should have a location', + 'Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR', + ], + data: [ + { + displayText: '25.7 x 12.9', + }, + { + displayText: '24.7 x 11.5', + }, + {}, + ], + }, + { + measurementId: '124', + measurementNumber: '124', + itemNumber: 2, + label: 'Bone Extremity', + data: [ + { + displayText: '24.7 x 11.1', + }, + { + displayText: '21.2 x 10.9', + }, + {}, + ], + }, + ], + }, + { + maxMeasurements: 3, + groupName: 'NonTargets', + measurements: [ + { + measurementId: '125', + measurementNumber: '125', + itemNumber: 1, + label: 'Chest Wall Single Site', + data: [ + { + displayText: 'MD', + }, + { + displayText: 'NM', + }, + {}, + ], + }, + { + measurementId: '126', + measurementNumber: '126', + itemNumber: 2, + label: 'Extremity Multiple Sites', + data: [ + { + displayText: 'CR', + }, + {}, + {}, + ], + }, + { + measurementId: '127', + measurementNumber: '127', + label: 'Extremity Site', + data: [ + { + displayText: 'CR', + }, + {}, + { + displayText: 'NM', + }, + ], + }, + ], + }, +]; diff --git a/packages/_components/src/components/measurementTable/__docs__/timepoints.js b/packages/_components/src/components/measurementTable/__docs__/timepoints.js new file mode 100644 index 000000000..e42b44156 --- /dev/null +++ b/packages/_components/src/components/measurementTable/__docs__/timepoints.js @@ -0,0 +1,14 @@ +export default [ + { + label: 'Follow-up 2', + date: '20-Dec-18', + }, + { + label: 'Follow-up 1', + date: '15-Jun-18', + }, + { + label: 'Baseline', + date: '10-Apr-18', + }, +]; diff --git a/packages/_components/src/components/measurementTable/__docs__/warnings.js b/packages/_components/src/components/measurementTable/__docs__/warnings.js new file mode 100644 index 000000000..a2da7f90d --- /dev/null +++ b/packages/_components/src/components/measurementTable/__docs__/warnings.js @@ -0,0 +1,6 @@ +export default { + warningList: [ + 'All measurements should have a location', + 'Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR', + ], +}; diff --git a/packages/_components/src/components/measurementTable/index.js b/packages/_components/src/components/measurementTable/index.js new file mode 100644 index 000000000..e70bd51eb --- /dev/null +++ b/packages/_components/src/components/measurementTable/index.js @@ -0,0 +1,2 @@ +export { MeasurementTable } from './MeasurementTable.js'; +export { MeasurementTableItem } from './MeasurementTableItem.js'; diff --git a/packages/_components/src/components/overlayTrigger/Fade.js b/packages/_components/src/components/overlayTrigger/Fade.js new file mode 100644 index 000000000..34a5cad79 --- /dev/null +++ b/packages/_components/src/components/overlayTrigger/Fade.js @@ -0,0 +1,102 @@ +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; +import Transition, { + ENTERED, + ENTERING, +} from 'react-transition-group/Transition'; + +const propTypes = { + /** + * Show the component; triggers the fade in or fade out animation + */ + in: PropTypes.bool, + + /** + * Wait until the first "enter" transition to mount the component (add it to the DOM) + */ + mountOnEnter: PropTypes.bool, + + /** + * Unmount the component (remove it from the DOM) when it is faded out + */ + unmountOnExit: PropTypes.bool, + + /** + * Run the fade in animation when the component mounts, if it is initially + * shown + */ + appear: PropTypes.bool, + + /** + * Duration of the fade animation in milliseconds, to ensure that finishing + * callbacks are fired even if the original browser transition end events are + * canceled + */ + timeout: PropTypes.number, + + /** + * Callback fired before the component fades in + */ + onEnter: PropTypes.func, + /** + * Callback fired after the component starts to fade in + */ + onEntering: PropTypes.func, + /** + * Callback fired after the has component faded in + */ + onEntered: PropTypes.func, + /** + * Callback fired before the component fades out + */ + onExit: PropTypes.func, + /** + * Callback fired after the component starts to fade out + */ + onExiting: PropTypes.func, + /** + * Callback fired after the component has faded out + */ + onExited: PropTypes.func, +}; + +const defaultProps = { + in: false, + timeout: 300, + mountOnEnter: false, + unmountOnExit: false, + appear: false, +}; + +const fadeStyles = { + [ENTERING]: 'in', + [ENTERED]: 'in', +}; + +class Fade extends React.Component { + render() { + const { className, children, ...props } = this.props; + + return ( + + {(status, innerProps) => + React.cloneElement(children, { + ...innerProps, + className: classNames( + 'fade', + className, + children.props.className, + fadeStyles[status] + ), + }) + } + + ); + } +} + +Fade.propTypes = propTypes; +Fade.defaultProps = defaultProps; + +export default Fade; diff --git a/packages/_components/src/components/overlayTrigger/Overlay.js b/packages/_components/src/components/overlayTrigger/Overlay.js new file mode 100644 index 000000000..e824045d5 --- /dev/null +++ b/packages/_components/src/components/overlayTrigger/Overlay.js @@ -0,0 +1,102 @@ +import classNames from 'classnames'; +import React, { cloneElement } from 'react'; +import PropTypes from 'prop-types'; +import { Overlay as BaseOverlay } from 'react-overlays'; +import elementType from 'prop-types-extra/lib/elementType'; +import { withTranslation } from '../../utils/LanguageProvider'; + +import Fade from './Fade'; + +const propTypes = { + /** + * Set the visibility of the Overlay + */ + show: PropTypes.bool, + /** + * Specify whether the overlay should trigger onHide when the user clicks outside the overlay + */ + rootClose: PropTypes.bool, + /** + * A callback invoked by the overlay when it wishes to be hidden. Required if + * `rootClose` is specified. + */ + onHide: PropTypes.func, + + /** + * Use animation + */ + animation: PropTypes.oneOfType([PropTypes.bool, elementType]), + + /** + * Callback fired before the Overlay transitions in + */ + onEnter: PropTypes.func, + + /** + * Callback fired as the Overlay begins to transition in + */ + onEntering: PropTypes.func, + + /** + * Callback fired after the Overlay finishes transitioning in + */ + onEntered: PropTypes.func, + + /** + * Callback fired right before the Overlay transitions out + */ + onExit: PropTypes.func, + + /** + * Callback fired as the Overlay begins to transition out + */ + onExiting: PropTypes.func, + + /** + * Callback fired after the Overlay finishes transitioning out + */ + onExited: PropTypes.func, + + /** + * Sets the direction of the Overlay. + */ + placement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']), +}; + +const defaultProps = { + animation: Fade, + rootClose: false, + show: false, + placement: 'right', +}; + +class Overlay extends React.Component { + render() { + const { animation, children, ...props } = this.props; + + const transition = animation === true ? Fade : animation || null; + + let child; + + if (!transition) { + child = cloneElement(children, { + className: classNames(children.props.className, 'in'), + }); + } else { + child = children; + } + + return ( + + {child} + + ); + } +} + +Overlay.propTypes = propTypes; +Overlay.defaultProps = defaultProps; + +const connectedComponent = withTranslation()(Overlay); +export { connectedComponent as Overlay }; +export default connectedComponent; diff --git a/packages/_components/src/components/overlayTrigger/OverlayTrigger.js b/packages/_components/src/components/overlayTrigger/OverlayTrigger.js new file mode 100644 index 000000000..b9216fcb3 --- /dev/null +++ b/packages/_components/src/components/overlayTrigger/OverlayTrigger.js @@ -0,0 +1,330 @@ +import contains from 'dom-helpers/query/contains'; +import React, { cloneElement } from 'react'; +import PropTypes from 'prop-types'; +import ReactDOM from 'react-dom'; +import warning from 'warning'; + +import { Overlay } from './Overlay'; + +import createChainedFunction from './createChainedFunction'; + +/** + * Check if value one is inside or equal to the of value + * + * @param {string} one + * @param {string|array} of + * @returns {boolean} + */ +function isOneOf(one, of) { + if (Array.isArray(of)) { + return of.indexOf(one) >= 0; + } + return one === of; +} + +const triggerType = PropTypes.oneOf(['click', 'hover', 'focus']); + +const propTypes = { + /** + * Specify which action or actions trigger Overlay visibility + */ + trigger: PropTypes.oneOfType([triggerType, PropTypes.arrayOf(triggerType)]), + + /** + * A millisecond delay amount to show and hide the Overlay once triggered + */ + delay: PropTypes.number, + /** + * A millisecond delay amount before showing the Overlay once triggered. + */ + delayShow: PropTypes.number, + /** + * A millisecond delay amount before hiding the Overlay once triggered. + */ + delayHide: PropTypes.number, + + // FIXME: This should be `defaultShow`. + /** + * The initial visibility state of the Overlay. For more nuanced visibility + * control, consider using the Overlay component directly. + */ + defaultOverlayShown: PropTypes.bool, + + /** + * An element or text to overlay next to the target. + */ + overlay: PropTypes.node.isRequired, + + /** + * A function to be called once the hide is triggered + */ + handleHide: PropTypes.func, + + /** + * @private + */ + onBlur: PropTypes.func, + /** + * @private + */ + onClick: PropTypes.func, + /** + * @private + */ + onFocus: PropTypes.func, + /** + * @private + */ + onMouseOut: PropTypes.func, + /** + * @private + */ + onMouseOver: PropTypes.func, + + // Overridden props from ``. + /** + * @private + */ + target: PropTypes.oneOf([null]), + /** + * @private + */ + onHide: PropTypes.oneOf([null]), + /** + * @private + */ + show: PropTypes.oneOf([null]), +}; + +const defaultProps = { + defaultOverlayShown: false, + trigger: ['hover', 'focus'], +}; + +class OverlayTrigger extends React.Component { + constructor(props, context) { + super(props, context); + + this.handleToggle = this.handleToggle.bind(this); + this.handleDelayedShow = this.handleDelayedShow.bind(this); + this.handleDelayedHide = this.handleDelayedHide.bind(this); + this.handleHide = createChainedFunction( + this.handleHide.bind(this), + props.handleHide + ); + + this.handleMouseOver = e => + this.handleMouseOverOut(this.handleDelayedShow, e, 'fromElement'); + this.handleMouseOut = e => + this.handleMouseOverOut(this.handleDelayedHide, e, 'toElement'); + + this._mountNode = null; + + this.state = { + show: props.defaultOverlayShown, + }; + } + + componentDidMount() { + this._mountNode = document.createElement('div'); + this.renderOverlay(); + } + + componentDidUpdate() { + this.renderOverlay(); + } + + componentWillUnmount() { + ReactDOM.unmountComponentAtNode(this._mountNode); + this._mountNode = null; + + clearTimeout(this._hoverShowDelay); + clearTimeout(this._hoverHideDelay); + } + + handleDelayedHide() { + if (this._hoverShowDelay != null) { + clearTimeout(this._hoverShowDelay); + this._hoverShowDelay = null; + return; + } + + if (!this.state.show || this._hoverHideDelay != null) { + return; + } + + const delay = + this.props.delayHide != null ? this.props.delayHide : this.props.delay; + + if (!delay) { + this.hide(); + return; + } + + this._hoverHideDelay = setTimeout(() => { + this._hoverHideDelay = null; + this.hide(); + }, delay); + } + + handleDelayedShow() { + if (this._hoverHideDelay != null) { + clearTimeout(this._hoverHideDelay); + this._hoverHideDelay = null; + return; + } + + if (this.state.show || this._hoverShowDelay != null) { + return; + } + + const delay = + this.props.delayShow != null ? this.props.delayShow : this.props.delay; + + if (!delay) { + this.show(); + return; + } + + this._hoverShowDelay = setTimeout(() => { + this._hoverShowDelay = null; + this.show(); + }, delay); + } + + handleHide() { + this.hide(); + } + + // Simple implementation of mouseEnter and mouseLeave. + // React's built version is broken: https://github.com/facebook/react/issues/4251 + // for cases when the trigger is disabled and mouseOut/Over can cause flicker + // moving from one child element to another. + handleMouseOverOut(handler, e, relatedNative) { + const target = e.currentTarget; + const related = e.relatedTarget || e.nativeEvent[relatedNative]; + + if ((!related || related !== target) && !contains(target, related)) { + handler(e); + } + } + + handleToggle() { + if (this.state.show) { + this.hide(); + } else { + this.show(); + } + } + + hide() { + this.setState({ show: false }); + } + + makeOverlay(overlay, props) { + return ( + + {overlay} + + ); + } + + show() { + this.setState({ show: true }); + } + + renderOverlay() { + ReactDOM.unstable_renderSubtreeIntoContainer( + this, + this._overlay, + this._mountNode + ); + } + + render() { + const { + trigger, + overlay, + children, + onBlur, + onClick, + onFocus, + onMouseOut, + onMouseOver, + ...props + } = this.props; + + delete props.delay; + delete props.delayShow; + delete props.delayHide; + delete props.defaultOverlayShown; + + const child = React.Children.only(children); + const childProps = child.props; + const triggerProps = {}; + + if (this.state.show) { + triggerProps['aria-describedby'] = overlay.props.id; + } + + // FIXME: The logic here for passing through handlers on this component is + // inconsistent. We shouldn't be passing any of these props through. + + triggerProps.onClick = createChainedFunction(childProps.onClick, onClick); + + if (isOneOf('click', trigger)) { + triggerProps.onClick = createChainedFunction( + triggerProps.onClick, + this.handleToggle + ); + } + + if (isOneOf('hover', trigger)) { + warning( + !(trigger === 'hover'), + '[react-bootstrap] Specifying only the `"hover"` trigger limits the ' + + 'visibility of the overlay to just mouse users. Consider also ' + + 'including the `"focus"` trigger so that touch and keyboard only ' + + 'users can see the overlay as well.' + ); + + triggerProps.onMouseOver = createChainedFunction( + childProps.onMouseOver, + onMouseOver, + this.handleMouseOver + ); + triggerProps.onMouseOut = createChainedFunction( + childProps.onMouseOut, + onMouseOut, + this.handleMouseOut + ); + } + + if (isOneOf('focus', trigger)) { + triggerProps.onFocus = createChainedFunction( + childProps.onFocus, + onFocus, + this.handleDelayedShow + ); + triggerProps.onBlur = createChainedFunction( + childProps.onBlur, + onBlur, + this.handleDelayedHide + ); + } + + this._overlay = this.makeOverlay(overlay, props); + + return cloneElement(child, triggerProps); + } +} + +OverlayTrigger.propTypes = propTypes; +OverlayTrigger.defaultProps = defaultProps; + +export { OverlayTrigger }; diff --git a/packages/_components/src/components/overlayTrigger/__docs__/OverlayTrigger.mdx b/packages/_components/src/components/overlayTrigger/__docs__/OverlayTrigger.mdx new file mode 100644 index 000000000..4fc48ee56 --- /dev/null +++ b/packages/_components/src/components/overlayTrigger/__docs__/OverlayTrigger.mdx @@ -0,0 +1,84 @@ +--- +name: Overlay Trigger +menu: Components +route: /components/overlay-trigger +--- + +import { Playground, Props } from 'docz' +import { Tooltip } from './../../tooltip' +import { OverlayTrigger } from './../index.js' + +# Overlay Trigger + +## Basic usage + +### On Hover + + + +
    + { + ['right', 'left', 'bottom', 'top'].map((placement) => { + return ( + + + Here I am! + + } + > + + + + ) + }) + } +
    + +
    + +### On Click + + +
    + {['right', 'left', 'bottom', 'top'].map(placement => { + return ( + + Here I am! + + } + > + + + ) + })} +
    +
    + +## API + + diff --git a/packages/_components/src/components/overlayTrigger/createChainedFunction.js b/packages/_components/src/components/overlayTrigger/createChainedFunction.js new file mode 100644 index 000000000..c5e231bc6 --- /dev/null +++ b/packages/_components/src/components/overlayTrigger/createChainedFunction.js @@ -0,0 +1,31 @@ +/** + * Safe chained function + * + * Will only create a new function if needed, + * otherwise will pass back existing functions or null. + * + * @param {function} functions to chain + * @returns {function|null} + */ +function createChainedFunction(...funcs) { + return funcs + .filter(f => f != null) + .reduce((acc, f) => { + if (typeof f !== 'function') { + throw new Error( + 'Invalid Argument Type, must only provide functions, undefined, or null.' + ); + } + + if (acc === null) { + return f; + } + + return function chainedFunction(...args) { + acc.apply(this, args); + f.apply(this, args); + }; + }, null); +} + +export default createChainedFunction; diff --git a/packages/_components/src/components/overlayTrigger/index.js b/packages/_components/src/components/overlayTrigger/index.js new file mode 100644 index 000000000..68a4d0874 --- /dev/null +++ b/packages/_components/src/components/overlayTrigger/index.js @@ -0,0 +1,2 @@ +export { Overlay } from './Overlay.js'; +export { OverlayTrigger } from './OverlayTrigger.js'; diff --git a/packages/_components/src/components/quickSwitch/QuickSwitch.js b/packages/_components/src/components/quickSwitch/QuickSwitch.js new file mode 100644 index 000000000..a29e57438 --- /dev/null +++ b/packages/_components/src/components/quickSwitch/QuickSwitch.js @@ -0,0 +1,153 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import { StudiesList } from './StudiesList.js'; +import { ScrollableArea } from './../../ScrollableArea/ScrollableArea.js'; +import { SeriesList } from './SeriesList.js'; + +import './QuickSwitch.styl'; + +export class QuickSwitch extends Component { + static propTypes = { + side: PropTypes.string, + studyListData: PropTypes.array.isRequired, + onSeriesSelected: PropTypes.func.isRequired, + seriesListData: PropTypes.array, + onStudySelected: PropTypes.func, + activeStudyInstanceUid: PropTypes.string, + activeDisplaySetInstanceUid: PropTypes.string, + }; + + constructor(props) { + super(props); + + this.state = { + seriesQuickSwitchOpen: false, + sideClass: this.props.side || '', + activeStudyInstanceUid: this.props.activeStudyInstanceUid, + activeDisplaySetInstanceUid: this.props.activeDisplaySetInstanceUid, + }; + } + + componentDidUpdate(prevProps) { + const props = this.props; + + if (props.activeStudyInstanceUid !== prevProps.activeStudyInstanceUid) { + this.setState({ + activeStudyInstanceUid: props.activeStudyInstanceUid, + }); + } + + if ( + props.activeDisplaySetInstanceUid !== + prevProps.activeDisplaySetInstanceUid + ) { + this.setState({ + activeDisplaySetInstanceUid: props.activeDisplaySetInstanceUid, + }); + } + } + + render() { + const quickSwitchClass = this.state.seriesQuickSwitchOpen + ? 'series-triggered' + : ''; + + return ( +
    +
    +
    Series
    +
    + {this.getSmallListItems()} + + + +
    +
    +
    +
    Study
    +
    + + + +
    +
    +
    + ); + } + + getSeriesItems = () => { + let seriesListData; + + if (this.props.seriesListData) { + seriesListData = this.props.seriesListData; + } else if (this.state.activeStudyInstanceUid) { + const study = this.props.studyListData.find( + study => study.studyInstanceUid === this.state.activeStudyInstanceUid + ); + + seriesListData = study.thumbnails; + } else { + seriesListData = this.props.studyListData[0].thumbnails; + } + + return seriesListData || []; + }; + + getSmallListItems = () => { + const seriesItems = this.getSeriesItems() || []; + return seriesItems.map((seriesData, index) => { + const active = + seriesData.displaySetInstanceUid === + this.state.activeDisplaySetInstanceUid; + return ( +
    + ); + }); + }; + + onStudyClick = studyDataSelected => { + if (this.props.onStudySelected) { + this.props.onStudySelected(studyDataSelected); + } + this.setState({ + activeStudyInstanceUid: studyDataSelected.studyInstanceUid, + seriesQuickSwitchOpen: true, + }); + }; + + onSeriesClick = seriesDataSelected => { + this.setState({ + activeDisplaySetInstanceUid: seriesDataSelected.displaySetInstanceUid, + }); + + this.props.onSeriesSelected(seriesDataSelected); + }; + + showSeriesSwitch = () => { + this.setState({ + seriesQuickSwitchOpen: true, + }); + }; + + hideSeriesSwitch = () => { + this.setState({ + seriesQuickSwitchOpen: false, + }); + }; +} diff --git a/packages/_components/src/components/quickSwitch/QuickSwitch.styl b/packages/_components/src/components/quickSwitch/QuickSwitch.styl new file mode 100644 index 000000000..b24314b14 --- /dev/null +++ b/packages/_components/src/components/quickSwitch/QuickSwitch.styl @@ -0,0 +1,209 @@ +.series-quick-switch + position: relative; + + &:before + box-shadow: 0 0 0 5000px rgba(0, 0, 0, 0.8) + transition: opacity 0.3s ease + opacity: 0 + content: '' + + &:hover + z-index: 50 + + &:before + display: table; + opacity: 1 + content: '' + + &:not(.left):not(.right) + left: 50% + transform: translateX(-50%) + + &.left + border-right: 1px solid var(--ui-border-color) + right: 0 + + &.right + left: 0 + + .series-switch + float: right + + .study-switch + float: left + + .study-browser + left: auto + right: 0 + transform-origin: calc(100% - 1em) calc(1em + 17px) + + .series-browser + left: 0 + right: auto + transform-origin: 1em calc(1em + 15px) + + .thumbnails-wrapper + transform-origin: 0% 0% + + .ThumbnailEntry + float: left + + .series-item + float: left + + .title-label + color: var(--text-secondary-color) + font-size: 12px + font-weight: 500 + line-height: 12px + opacity: 1 + padding-bottom: 3px + text-align: center + transition: opacity 0.3s ease + + .series-switch + padding-right: 10px; + + .series-switch, + .study-switch + float: left + position: relative + + .study-switch:hover:after + content: '' + display: block + height: 100px + left: 0 + position: absolute + top: 0 + width: 300px + + .study-browser + left: 0 + padding-top: 60px; + max-height: calc(100vh - 60px); + transform-origin: 1em calc(1em + 17px) + width: 300px + + .study-browser-list + padding: 0 10px + + .series-browser + max-height: 100vh + max-width: 720px + right: 0 + transform-origin: calc(100% - 1em) calc(1em + 15px) + + .thumbnails-wrapper + transition: transform 0.3s ease + transform-origin: 100% 0% + + .ThumbnailEntry + float: right + + .series-details + opacity: 0 + transform: translateY(-100%) + transition: transform 0.3s ease, opacity 0.3s ease + + .scrollable + padding-top: 15px + transition: padding-bottom 0.3s ease + + .study-browser-series { + overflow hidden + } + + .series-browser, + .study-browser + min-height: 120px + opacity: 0 + position: absolute + top: 0 + transform: scale(0) + transition: transform 0.3s ease , opacity 0.3s ease + z-index: 3 + + .series-box, + .study-box + height: 57px + width: 57px + + .study-box + background-color: var(--ui-gray-dark) + border: solid 2px var(--ui-border-color-dark) + border-radius: 11px + + .series-item + background-color: var(--box-background-color) + border-radius: 3px + float: right + height: 15px + margin: 2px + transition: opacity 0.3s ease + width: 15px + + &.count + color: var(--text-primary-color) + background-color: transparent + font-size: 12px + font-weight: 500 + line-height: 17px + text-align: center + + &.active + background-color: var(--active-color) + + &.series-triggered .series-browser, + .series-switch:hover .series-browser, + .study-switch:hover .study-browser + opacity: 1 + transform: scale(1) + + &.series-triggered .series-item, + .series-switch:hover .title-label + opacity: 0 + + &.series-triggered + .thumbnails-wrapper + transform: scale(0.9) translateY(80px) + + .series-browser:not(:hover) + &>.scrollable + padding-bottom: 80px + + &>.scrollNav + opacity: 0 + + .series-switch:hover .series-browser + .thumbnails-wrapper + transform: scale(1) translateY(0) + + .ThumbnailEntry .series-details + opacity: 1 + transform: translateY(0) + transition-delay: 0.3s + + .study-browser-item.active .study-item-box + box-shadow: inset 0 0 0 3px var(--active-color) + +@media screen and (max-width: 1599px) + .series-quick-switch + + .series-box, + .study-box + width: 36px + height: 36px + + .study-box + border-radius: 7px + + .series-item + height: 10px + margin: 1px + width: 10px + + &.count + font-size: 10px + font-weight: 300 + line-height: 10px diff --git a/packages/_components/src/components/quickSwitch/SeriesList.js b/packages/_components/src/components/quickSwitch/SeriesList.js new file mode 100644 index 000000000..f38d95d63 --- /dev/null +++ b/packages/_components/src/components/quickSwitch/SeriesList.js @@ -0,0 +1,40 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import { ThumbnailEntry } from './../studyBrowser'; +import './SeriesList.styl'; + +export class SeriesList extends Component { + static propTypes = { + seriesItems: PropTypes.array.isRequired, + onClick: PropTypes.func.isRequired, + activeDisplaySetInstanceUid: PropTypes.string, + }; + + render() { + return ( + +
    +
    {this.getSeriesItems()}
    +
    +
    + ); + } + + getSeriesItems = () => { + return this.props.seriesItems.map((seriesData, index) => { + return ( + this.props.onClick(seriesData)} + /> + ); + }); + }; +} diff --git a/packages/_components/src/components/quickSwitch/SeriesList.styl b/packages/_components/src/components/quickSwitch/SeriesList.styl new file mode 100644 index 000000000..e69de29bb diff --git a/packages/_components/src/components/quickSwitch/StudiesItem.js b/packages/_components/src/components/quickSwitch/StudiesItem.js new file mode 100644 index 000000000..c43fe281c --- /dev/null +++ b/packages/_components/src/components/quickSwitch/StudiesItem.js @@ -0,0 +1,60 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import './StudiesItem.styl'; + +export class StudiesItem extends Component { + static propTypes = { + onClick: PropTypes.func.isRequired, + studyData: PropTypes.object.isRequired, + active: PropTypes.bool, + }; + + render() { + const { + studyDate, + studyDescription, + modalities, + studyAvailable, + } = this.props.studyData; + const activeClass = this.props.active ? ' active' : ''; + const hasDescriptionAndDate = studyDate && studyDescription; + return ( +
    +
    +
    +
    + {modalities} +
    +
    +
    + {hasDescriptionAndDate ? ( + +
    {studyDate}
    +
    {studyDescription}
    +
    + ) : ( +
    + {studyAvailable ? ( + N/A + ) : ( + Click to load + )} +
    + )} +
    +
    +
    + ); + } + + getModalitiesStyle = () => { + return {}; + }; +} diff --git a/packages/_components/src/components/quickSwitch/StudiesItem.styl b/packages/_components/src/components/quickSwitch/StudiesItem.styl new file mode 100644 index 000000000..98c494844 --- /dev/null +++ b/packages/_components/src/components/quickSwitch/StudiesItem.styl @@ -0,0 +1,133 @@ +$boxHoverBackgroundColor = #14191E +$boxHoverBorderColor = #2d4660 +$boxActiveBorderColor = #131D29 +$boxWidth = 50px +$borderThickness = 2px +$boxBorderRadius = 8px +$nestingMargin = 6px +$spacerX = 7px +$spacerY = 12px + +.studyBrowseItem + margin-bottom: 10px; + position: relative + // required transformation to make inner fixed elements relative to this one + transform: scale(1) + + &.active + .studyItemBox + border-color: var(--active-color) + + .studyModality + color: var(--primary-background-color) + & + &:before + &:after + background-color: var(--active-color) + border-color: $boxActiveBorderColor + + &:not(.active) .studyBrowserSeries + max-height: 0 !important + + .studySeriesContainer + opacity: 0 + transform: translateY(-100%) + + .studyBrowserSeries + overflow: hidden + transition: max-height 0.3s ease + + .studySeriesContainer + opacity: 1 + transition: opacity 0.3s ease, transform 0.3s ease + transform: translateY(0) + transform-origin: 50% 0% + +.studyItemBox + border: 3px solid var(--ui-border-color-dark) + border-radius: 12px + cursor: pointer + padding: $spacerY $spacerX ($spacerY - 1) + position: relative + transition: $sidebarTransition + background-color: $boxHoverBackgroundColor + z-index: 0 + + &:hover + border-color: var(--active-color) + + &.additional + color: var(--text-secondary-color) + + .studyModality + color: var(--primary-background-color) + & + &:before + &:after + background-color: var(--active-color) + + &.additional + color: var(--text-primary-color) + font-size: 16px + font-weight: normal + height: 91px + line-height: 91px + padding: 0 + text-align: center + + .studyText + font-size: 13px + left: ($spacerX * 3) + $boxWidth + ($nestingMargin * 3) + line-height: 14px + position: absolute + right: $spacerX + top: $spacerY + .studyDate + margin-top: 8px + color: var(--text-secondary-color) + .studyDescription + margin-top: 8px + color: var(--text-primary-color) + .studyAvailability + margin-top: 24px + color: var(--text-primary-color) + + .studyModality + color: var(--text-secondary-color) + font-size: 20px + line-height: $boxWidth + margin-left: $nestingMargin * 2 + margin-top: $nestingMargin * 2 + position: relative + + .studyModalityText + height: 100% + overflow: hidden + text-align: center + text-transform: uppercase + + & + &:before + &:after + border: $borderThickness solid var(--primary-background-color) + background-color: var(--box-background-color) + border-radius: $boxBorderRadius + height: $boxWidth + ($borderThickness * 2) + transition($sidebarTransition) + width: $boxWidth + ($borderThickness * 2) + + &:before + &:after + display: block + content: '' + position: absolute + + &:before + top: -($borderThickness + $nestingMargin) + left: -($borderThickness + $nestingMargin) + z-index: -1 + + &:after + top: -($borderThickness + ($nestingMargin * 2)) + left: -($borderThickness + ($nestingMargin * 2)) + z-index: -2 diff --git a/packages/_components/src/components/quickSwitch/StudiesList.js b/packages/_components/src/components/quickSwitch/StudiesList.js new file mode 100644 index 000000000..414b46944 --- /dev/null +++ b/packages/_components/src/components/quickSwitch/StudiesList.js @@ -0,0 +1,37 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import { StudiesItem } from './StudiesItem.js'; +import './StudiesList.styl'; + +export class StudiesList extends Component { + static propTypes = { + class: PropTypes.string, + studyListData: PropTypes.array.isRequired, + onClick: PropTypes.func.isRequired, + activeStudyInstanceUid: PropTypes.string, + }; + + render() { + return ( +
    + {this.getBrowserItems()} +
    + ); + } + + getBrowserItems = () => { + return this.props.studyListData.map((studyData, index) => { + return ( + this.props.onClick(studyData)} + /> + ); + }); + }; +} diff --git a/packages/_components/src/components/quickSwitch/StudiesList.styl b/packages/_components/src/components/quickSwitch/StudiesList.styl new file mode 100644 index 000000000..e69de29bb diff --git a/packages/_components/src/components/quickSwitch/__docs__/quickSwitch.mdx b/packages/_components/src/components/quickSwitch/__docs__/quickSwitch.mdx new file mode 100644 index 000000000..c14670d9d --- /dev/null +++ b/packages/_components/src/components/quickSwitch/__docs__/quickSwitch.mdx @@ -0,0 +1,61 @@ +--- +name: Quick Switch +menu: Components +route: /components/quick-switch +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { QuickSwitch } from './../index.js' +// +import studies from './studies.js' + +# Quick Switch + +## Basic usage + + + + {({ state, setState }) => ( + { + return !!study.active; + }).thumbnails} + onSeriesSelected={(selectedSeries) => { + const { studyListData } = state; + const selectedId = selectedSeries.displaySetInstanceUid; + + studyListData.forEach(study => { + study.thumbnails.forEach(series => { + series.active = series.displaySetInstanceUid === selectedId; + }) + }); + + setState({ studyListData }); + }} + onStudySelected={(selectedStudy) => { + const { studyListData } = state; + const selectedId = selectedStudy.studyInstanceUid; + + studyListData.forEach(study => { + study.active = study.studyInstanceUid === selectedId; + }); + + setState({ studyListData }); + }} + /> + )} + + + + + +## API + + diff --git a/packages/_components/src/components/quickSwitch/__docs__/studies.js b/packages/_components/src/components/quickSwitch/__docs__/studies.js new file mode 100644 index 000000000..c4c5250d9 --- /dev/null +++ b/packages/_components/src/components/quickSwitch/__docs__/studies.js @@ -0,0 +1,76 @@ +export default [ + { + studyInstanceUid: '10001', + studyDescription: 'Anti-PD-1', + modalities: 'CT', + studyDate: '18-nov-2018', + active: false, + thumbnails: [ + { + imageSrc: + 'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/Anti-PD-1_Lung.jpg', + seriesDescription: 'Anti-PD-1_Lung', + seriesNumber: '2', + numImageFrames: 512, + displaySetInstanceUid: '10001-1', + stackPercentComplete: 30, + }, + { + imageSrc: + 'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/Anti-PD-1_MELANOMA.jpg', + seriesDescription: 'Anti-PD-1_MELANOMA', + seriesNumber: '2', + instanceNumber: '1', + numImageFrames: 256, + displaySetInstanceUid: '10001-2', + stackPercentComplete: 70, + }, + ], + }, + { + studyInstanceUid: '10002', + studyDescription: 'CPTAC', + modalities: 'CT', + studyDate: '16-aug-2017', + active: true, + thumbnails: [ + { + imageSrc: + 'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-GBM.jpg', + seriesDescription: 'CPTAC-GBM', + active: true, + seriesNumber: '2', + numImageFrames: 512, + displaySetInstanceUid: '10002-1', + stackPercentComplete: 100, + }, + { + imageSrc: + 'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-CM.jpg', + seriesDescription: 'CPTAC-CM', + seriesNumber: '2', + instanceNumber: '1', + displaySetInstanceUid: '10002-2', + numImageFrames: 256, + }, + { + imageSrc: + 'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-HNSCC.jpg', + seriesDescription: 'CPTAC-HNSCC', + seriesNumber: '2', + instanceNumber: '1', + displaySetInstanceUid: '10002-3', + numImageFrames: 256, + }, + { + imageSrc: + 'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-LSCC.jpg', + seriesDescription: 'CPTAC-LSCC', + seriesNumber: '2', + instanceNumber: '1', + displaySetInstanceUid: '10002-4', + numImageFrames: 256, + }, + ], + }, +]; diff --git a/packages/_components/src/components/quickSwitch/index.js b/packages/_components/src/components/quickSwitch/index.js new file mode 100644 index 000000000..c92ec79d5 --- /dev/null +++ b/packages/_components/src/components/quickSwitch/index.js @@ -0,0 +1 @@ +export { QuickSwitch } from './QuickSwitch.js'; diff --git a/packages/_components/src/components/radioButtonList/RadioButtonList.css b/packages/_components/src/components/radioButtonList/RadioButtonList.css new file mode 100644 index 000000000..d7c6305dc --- /dev/null +++ b/packages/_components/src/components/radioButtonList/RadioButtonList.css @@ -0,0 +1,62 @@ +.ohif-radio-button-container { + --radio-button-dim: 20px; +} + +.ohif-radio-button-container input { + position: absolute; + opacity: 0; + z-index: 10000; + cursor: pointer; + height: inherit; + width: inherit; +} + +.ohif-radio-button-container { + display: block; + position: relative; + padding-left: 35px; + margin-bottom: 12px; + cursor: pointer; + font-size: 22px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.ohif-radio-button { + width: var(--radio-button-dim); + height: var(--radio-button-dim); + position: absolute; + top: 25%; + left: 0; + background-color: var(--ui-gray-lighter); + border-radius: 50%; + z-index: 1; +} + +.ohif-radio-button:hover { + background-color: var(--default-color); + border-radius: 100%; +} + +.ohif-radio-button:after { + content: ''; + position: absolute; + display: none; + width: 70%; + height: 70%; +} + +.ohif-radio-button-container .ohif-radio-button.ohif-selected:after { + display: block; + top: 14%; + left: 14%; + border-radius: 50%; + background: white; +} + +.ohif-radio-button-label { + font-size: 12px; + font-weight: 500; +} diff --git a/packages/_components/src/components/radioButtonList/RadioButtonList.js b/packages/_components/src/components/radioButtonList/RadioButtonList.js new file mode 100644 index 000000000..0bbcf8b59 --- /dev/null +++ b/packages/_components/src/components/radioButtonList/RadioButtonList.js @@ -0,0 +1,74 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import './RadioButtonList.css'; + +export class RadioButtonList extends Component { + static className = 'RadioButtonList'; + + //TODO: Add fields to propTypes.description? + //These would be label (required), id (required), and checked (optional). + static propTypes = { + description: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, + checked: PropTypes.bool, + }) + ), + }; + + constructor(props) { + super(props); + this.state = {}; + + for (let button of props.description) { + if (button.checked) { + this.state.checked = button.id; + } + } + } + + handleChange(e) { + this.setState({ checked: e.target.value }); + } + + render() { + let buttons = this.props.description.map(button => { + let input = ( + { + this.handleChange(e); + }} + value={button.id} + /> + ); + + //needed to style the custom radio check + let inputSpan; + if (this.state.checked === button.id) { + inputSpan = ( + {input} + ); + } else { + inputSpan = {input}; + } + + return ( + + + + ); + }); + + return ( +
    +
    {buttons}
    +
    + ); + } +} diff --git a/packages/_components/src/components/radioButtonList/__docs__/radioButtonList.mdx b/packages/_components/src/components/radioButtonList/__docs__/radioButtonList.mdx new file mode 100644 index 000000000..2ad2b205d --- /dev/null +++ b/packages/_components/src/components/radioButtonList/__docs__/radioButtonList.mdx @@ -0,0 +1,74 @@ +--- +name: Radio Button List +menu: Elements +route: /components/radio-button-list +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { RadioButtonList } from './../index' +import NameSpace from '../../../__docs__/NameSpace' + +#Radio Button List + +## Basic usage + + + + {({ state, setState }) => ( + +
    +
    {JSON.stringify(state, null, 2)}
    +
    +
    + +
    +
    + )} +
    +
    + +## API + + + +## Translation Namespace + + \ No newline at end of file diff --git a/packages/_components/src/components/radioButtonList/index.js b/packages/_components/src/components/radioButtonList/index.js new file mode 100644 index 000000000..f8361a3b0 --- /dev/null +++ b/packages/_components/src/components/radioButtonList/index.js @@ -0,0 +1 @@ +export { RadioButtonList } from './RadioButtonList.js'; diff --git a/packages/_components/src/components/roundedButtonGroup/RoundedButtonGroup.css b/packages/_components/src/components/roundedButtonGroup/RoundedButtonGroup.css new file mode 100644 index 000000000..fc0e7e509 --- /dev/null +++ b/packages/_components/src/components/roundedButtonGroup/RoundedButtonGroup.css @@ -0,0 +1,86 @@ +.RoundedButtonGroup { + --height: 25px; + + position: relative; + z-index: 0; +} + +.roundedButtonWrapper { + cursor: pointer; + display: inline-block; + float: left; + margin-left: -2px; + text-decoration: none; + text-align: center; +} + +.roundedButtonWrapper.disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.RoundedButtonGroup .roundedButtonWrapper .roundedButton { + align-items: center; + background-color: var(--ui-gray-dark); + border: 2px solid var(--ui-border-color-dark); + color: var(--text-secondary-color); + display: flex; + font-size: 15px; + font-weight: 500; + justify-content: center; + height: var(--height); + line-height: var(--height); + padding: 0 22px; + position: relative; + text-transform: uppercase; + transition: var(--sidebar-transition); + z-index: 1; +} + +.roundedButtonWrapper + .roundedButton + svg + .roundedButtonWrapper + .roundedButton + span { + margin: 0 2px; +} + +.roundedButtonWrapper .roundedButton i { + line-height: 15px; + font-size: 15px; +} + +.bottomLabel { + color: var(--text-secondary-color); + font-size: 12px; + font-weight: 500; + line-height: 12px; + margin-top: 8px; +} + +.roundedButtonWrapper:first-child { + margin-left: 0; +} + +.RoundedButtonGroup .roundedButtonWrapper:first-child .roundedButton { + border-bottom-left-radius: var(--height); + border-top-left-radius: var(--height); +} + +.RoundedButtonGroup .roundedButtonWrapper:last-child .roundedButton { + border-bottom-right-radius: var(--height); + border-top-right-radius: var(--height); +} + +.roundedButtonWrapper:hover .roundedButton { + background-color: var(--box-background-color); + color: var(--ui-gray-dark); +} + +.roundedButtonWrapper.active .roundedButton { + background-color: var(--active-color); + border-color: var(--ui-border-color-active); + color: var(--ui-gray-dark); + z-index: 2; +} diff --git a/packages/_components/src/components/roundedButtonGroup/RoundedButtonGroup.js b/packages/_components/src/components/roundedButtonGroup/RoundedButtonGroup.js new file mode 100644 index 000000000..d17a65fb6 --- /dev/null +++ b/packages/_components/src/components/roundedButtonGroup/RoundedButtonGroup.js @@ -0,0 +1,84 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { Icon } from './../../elements/Icon'; +import './RoundedButtonGroup.css'; + +// TODO: Rename to Toggle? +class RoundedButtonGroup extends Component { + static className = 'RoundedButtonGroup'; + + static propTypes = { + options: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.any, + text: PropTypes.string, + icon: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({ + name: PropTypes.string.isRequired, + }), + ]), + }) + ), + value: PropTypes.string, + onValueChanged: PropTypes.func, + }; + + static defaultProps = { + options: [], + value: null, + }; + + onClickOption = value => { + let newValue = value; + if (this.props.value === value) { + newValue = null; + } + + if (this.props.onValueChanged) { + this.props.onValueChanged(newValue); + } + }; + + render() { + let className = classnames( + RoundedButtonGroup.className, + 'clearfix center-table' + ); + + const buttons = this.props.options.map((option, index) => { + const className = classnames({ + roundedButtonWrapper: true, + noselect: true, + active: this.props.value === option.value, + }); + + const optionText = option.text && {option.text}; + const iconProps = + typeof option.icon === 'string' ? { name: option.icon } : option.icon; + + const bottomLabel = option.bottomLabel && ( +
    {option.bottomLabel}
    + ); + + return ( +
    this.onClickOption(option.value)} + > +
    + {optionText} + {iconProps && } +
    + {bottomLabel} +
    + ); + }); + + return
    {buttons}
    ; + } +} + +export { RoundedButtonGroup }; diff --git a/packages/_components/src/components/roundedButtonGroup/__docs__/roundedButtonGroup.mdx b/packages/_components/src/components/roundedButtonGroup/__docs__/roundedButtonGroup.mdx new file mode 100644 index 000000000..5d00b32fc --- /dev/null +++ b/packages/_components/src/components/roundedButtonGroup/__docs__/roundedButtonGroup.mdx @@ -0,0 +1,51 @@ +--- +name: Rounded Button Group +menu: Components +route: /components/rounded-button-group +--- + +import { Playground, Props } from 'docz'; +import { State } from 'react-powerplug'; +import { RoundedButtonGroup } from './../index.js'; + +# Rounded Button Group + +A basic styled toggle switch + +## Basic usage + + + + {({ state, setState }) => ( + +
    +
    value: {JSON.stringify(state.value, null, 2)}
    +
    + setState({ value })} + /> +
    + )} +
    +
    + +## API + + diff --git a/packages/_components/src/components/roundedButtonGroup/index.js b/packages/_components/src/components/roundedButtonGroup/index.js new file mode 100644 index 000000000..e33e0490c --- /dev/null +++ b/packages/_components/src/components/roundedButtonGroup/index.js @@ -0,0 +1 @@ +export { RoundedButtonGroup } from './RoundedButtonGroup.js'; diff --git a/packages/_components/src/components/selectTree/InputRadio.js b/packages/_components/src/components/selectTree/InputRadio.js new file mode 100644 index 000000000..e4de02a06 --- /dev/null +++ b/packages/_components/src/components/selectTree/InputRadio.js @@ -0,0 +1,37 @@ +import { Component } from 'react'; +import React from 'react'; +import PropTypes from 'prop-types'; + +export default class InputRadio extends Component { + static propTypes = { + value: PropTypes.string, + label: PropTypes.string.isRequired, + itemData: PropTypes.object.isRequired, + labelClass: PropTypes.string, + id: PropTypes.string.isRequired, + onSelected: PropTypes.func.isRequired, + }; + + render() { + const labelClass = this.props.labelClass ? this.props.labelClass : ''; + return ( + + ); + } + + onSelected = evt => { + this.props.onSelected(evt, this.props.itemData); + }; +} diff --git a/packages/_components/src/components/selectTree/SelectTree.js b/packages/_components/src/components/selectTree/SelectTree.js new file mode 100644 index 000000000..7a53e46ca --- /dev/null +++ b/packages/_components/src/components/selectTree/SelectTree.js @@ -0,0 +1,182 @@ +import './SelectTree.styl'; + +import React, { Component } from 'react'; + +import { Icon } from './../../elements/Icon'; +import InputRadio from './InputRadio.js'; +import PropTypes from 'prop-types'; +import SelectTreeBreadcrumb from './SelectTreeBreadcrumb.js'; +import cloneDeep from 'lodash.clonedeep'; + +export class SelectTree extends Component { + static propTypes = { + autoFocus: PropTypes.bool, + searchEnabled: PropTypes.bool, + selectTreeFirstTitle: PropTypes.string, + selectTreeSecondTitle: PropTypes.string, + /** Called when 'componentDidUpdate' is triggered */ + onComponentChange: PropTypes.func, + /** [{ label, value, items[]}] - An array of items than can be expanded to show child items */ + items: PropTypes.array.isRequired, + /** fn(evt, item) - Called when a child item is selected; receives event and selected item */ + onSelected: PropTypes.func.isRequired, + }; + + static defaultProps = { + searchEnabled: true, + autoFocus: true, + selectTreeFirstTitle: 'First Level itens', + items: [], + }; + + constructor(props) { + super(props); + + this.state = { + searchTerm: null, + currentNode: null, + value: null, + }; + } + + render() { + const treeItems = this.getTreeItems(); + + return ( +
    +
    + {this.headerItem()} +
    + {this.state.currentNode && ( + + )} +
    +
    {treeItems}
    +
    +
    +
    +
    + ); + } + + componentDidUpdate = () => { + if (this.props.onComponentChange) { + this.props.onComponentChange(); + } + }; + + isLeafSelected = item => item && !Array.isArray(item.items); + + getLabelClass = item => { + let labelClass = 'treeLeaf'; + if (this.state.searchTerm || Array.isArray(item.items)) { + labelClass = 'treeNode'; + } + return labelClass; + }; + + getTreeItems() { + const storageKey = 'SelectTree'; + let treeItems; + + if (this.state.searchTerm) { + const filteredItems = []; + const rawItems = cloneDeep(this.props.items); + rawItems.forEach(item => { + if (Array.isArray(item.items)) { + item.items.forEach(item => { + const label = item.label.toLowerCase(); + const searchTerm = this.state.searchTerm.toLowerCase(); + if (label.indexOf(searchTerm) !== -1) { + filteredItems.push(item); + } + }); + } + }); + treeItems = filteredItems; + } else if (this.state.currentNode) { + treeItems = cloneDeep(this.state.currentNode.items); + } else { + treeItems = cloneDeep(this.props.items); + } + + return treeItems.map((item, index) => { + let itemKey = index; + if (this.state.currentNode) { + itemKey += `_${this.state.currentNode.value}`; + } + return ( + + ); + }); + } + + headerItem = () => { + let title = this.props.selectTreeFirstTitle; + if (this.state.currentNode && this.props.selectTreeSecondTitle) { + title = this.props.selectTreeSecondTitle; + } + + return ( +
    +
    {title}
    + {this.props.searchEnabled && ( +
    +
    + +
    + +
    + )} +
    + ); + }; + + searchLocations = evt => { + this.setState({ + currentNode: null, + searchTerm: evt.currentTarget.value, + }); + }; + + onSelected = (event, item) => { + if (this.isLeafSelected(item)) { + this.setState({ + searchTerm: null, + currentNode: null, + value: null, + }); + return this.props.onSelected(event, item); + } else { + this.setState({ + currentNode: item, + }); + } + }; + + onBreadcrumbSelected = () => { + this.setState({ + currentNode: null, + }); + }; +} diff --git a/packages/_components/src/components/selectTree/SelectTree.styl b/packages/_components/src/components/selectTree/SelectTree.styl new file mode 100644 index 000000000..f5ddaf63f --- /dev/null +++ b/packages/_components/src/components/selectTree/SelectTree.styl @@ -0,0 +1,106 @@ +.selectTreeRoot + text-align: initial; + width: 320px; + max-height: inherit; + font-size: 14px; + line-height: 26px; + + .treeContent + max-height: inherit; + overflow: hidden; + border: 1px solid #c4c4c4; + border-radius: 5px; + background-color: #ffffff; + display: flex; + flex-direction: column; + + .treeHeader + background-color: #c0c0c0; + border-bottom: 1px solid #c4c4c4; + color: #303030; + padding: 5px; + text-align: center; + + .wrapperText + font-weight: bold; + padding-bottom: 2px; + + .wrapperSearch + width: 100%; + + .searchIcon + position: absolute; + width: 18px; + height: 15px; + margin: 7px 10px 13px 10px; + color: #a3a3a3; + + .searchInput + width: 100%; + padding: 6px 12px 6px 35px; + background-color: #fff; + background-image: none; + border: 1px solid #9b9b9b; + border-radius: 4px; + font-size: 14px; + line-height: 1.42857143; + color: #555; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + + &:focus + outline: 0; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), + 0 0 8px rgba(102, 175, 233, 0.6); + + .wrapperText + white-space: nowrap; + cursor: pointer; + + .selectTreeBreadcrumb + .wrapperLabel + cursor: pointer; + padding: 0 12px; + height: 41px; + width: 100%; + line-height: 41px; + + .backIcon + padding-right: 10px; + + svg + width: 14px; + height: 14px; + position: relative; + top: 2px; + color: #457fb0; + + .treeOptions + overflow: auto; + height: 100%; + + .selectTreeBreadcrumb + .treeInputsWrapper .treeInputs .treeLeaf + border: none; + height: initial; + line-height: 26px; + + & > .treeInputsWrapper > .treeInputs .treeLeaf + border-bottom: 1px solid #ccc; + height: 41px; + line-height: 41px; + + .treeNode, .treeLeaf + cursor: pointer; + display: block; + overflow: hidden; + font-weight: normal; + box-shadow: 0 0 0 200px transparent; + padding: 0 12px; + + .treeNode + border-bottom: 1px solid #ccc; + height: 41px; + line-height: 41px; + + .radioLabel .radioInput + display: none; diff --git a/packages/_components/src/components/selectTree/SelectTreeBreadcrumb.js b/packages/_components/src/components/selectTree/SelectTreeBreadcrumb.js new file mode 100644 index 000000000..1bcb5ea20 --- /dev/null +++ b/packages/_components/src/components/selectTree/SelectTreeBreadcrumb.js @@ -0,0 +1,37 @@ +import { Component } from 'react'; +import React from 'react'; +import PropTypes from 'prop-types'; +import { Icon } from './../../elements/Icon'; + +export default class SelectTreeBreadcrumb extends Component { + static propTypes = { + label: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + onSelected: PropTypes.func.isRequired, + }; + + render() { + return ( +
    + +
    + ); + } +} diff --git a/packages/_components/src/components/selectTree/__docs__/selectTree.mdx b/packages/_components/src/components/selectTree/__docs__/selectTree.mdx new file mode 100644 index 000000000..68ee9882e --- /dev/null +++ b/packages/_components/src/components/selectTree/__docs__/selectTree.mdx @@ -0,0 +1,42 @@ +--- +name: Select Tree +menu: Components +route: /components/select-tree +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { SelectTree } from './../index.js' + +import selectTreeItems from './selectTreeItems.js' + +# SelectTree + +## Basic usage + + + + {({ state, setState }) => ( + +
    +                {JSON.stringify(state.items, null, 2)}
    +            
    + + setState({ itemLabelSelected: item.label }) } + selectTreeFirstTitle="Select Tree Example Header" + autoFocus={false} + /> +
    + )} + +
    + +
    + +## API + + diff --git a/packages/_components/src/components/selectTree/__docs__/selectTreeItems.js b/packages/_components/src/components/selectTree/__docs__/selectTreeItems.js new file mode 100644 index 000000000..0c7466966 --- /dev/null +++ b/packages/_components/src/components/selectTree/__docs__/selectTreeItems.js @@ -0,0 +1,30 @@ +export default [ + { + label: 'A', + value: 'A', + items: [ + { + label: 'Abdominal Wall', + value: 'AbdominalWall', + }, + { + label: 'Adrenal Left', + value: 'AdrenalLeft', + }, + ], + }, + { + label: 'B', + value: 'B', + items: [ + { + label: 'Brain', + value: 'Brain', + }, + { + label: 'Breast', + value: 'Breast', + }, + ], + }, +]; diff --git a/packages/_components/src/components/selectTree/index.js b/packages/_components/src/components/selectTree/index.js new file mode 100644 index 000000000..58dad4997 --- /dev/null +++ b/packages/_components/src/components/selectTree/index.js @@ -0,0 +1 @@ +export { SelectTree } from './SelectTree'; diff --git a/packages/_components/src/components/simpleDialog/SimpleDialog.js b/packages/_components/src/components/simpleDialog/SimpleDialog.js new file mode 100644 index 000000000..2c8ce887d --- /dev/null +++ b/packages/_components/src/components/simpleDialog/SimpleDialog.js @@ -0,0 +1,69 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import './SimpleDialog.styl'; + +class SimpleDialog extends Component { + static propTypes = { + children: PropTypes.node, + componentRef: PropTypes.any, + componentStyle: PropTypes.object, + rootClass: PropTypes.string, + isOpen: PropTypes.bool, + headerTitle: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, + onConfirm: PropTypes.func.isRequired, + }; + + static defaultProps = { + isOpen: true, + componentStyle: {}, + rootClass: '', + }; + + render() { + return ( + + {this.props.isOpen && ( +
    +
    +
    + + x + +

    {this.props.headerTitle}

    +
    +
    {this.props.children}
    +
    + + +
    +
    +
    + )} +
    + ); + } + + onClose = event => { + event.preventDefault(); + event.stopPropagation(); + this.props.onClose(); + }; + + onConfirm = event => { + event.preventDefault(); + event.stopPropagation(); + this.props.onConfirm(); + }; +} + +export { SimpleDialog }; diff --git a/packages/_components/src/components/simpleDialog/SimpleDialog.styl b/packages/_components/src/components/simpleDialog/SimpleDialog.styl new file mode 100644 index 000000000..0b0147022 --- /dev/null +++ b/packages/_components/src/components/simpleDialog/SimpleDialog.styl @@ -0,0 +1,108 @@ +@import './../../design/styles/common/button.styl' +@import './../../design/styles/common/global.styl' + +.simpleDialog + position: fixed; + top: 0px; + left: 0px; + z-index: 1000; + border: 0; + border-radius: 6px; + background-color: var(--ui-gray-darker); + + .header + border-bottom: 3px solid var(--primary-background-color); + padding: 19px 22px 17px; + position: relative; + + .title + font-size: 20px; + font-weight: 600; + line-height: 24px; + padding-right: 40px; + color: var(--text-secondary-color); + margin: 0; + + .closeBtn + -webkit-transform: translateY(-50%); + color: var(--text-secondary-color); + height: 20px; + opacity: 1; + overflow: hidden; + padding: 2px; + position: absolute; + right: 21px; + text-align: center; + text-shadow: none; + top: 50%; + transform: translateY(-50%); + transition: color 0.3s ease; + width: 20px; + + .closeIcon + color: transparent; + display: block; + font-size: 0; + height: 100%; + line-height: 0; + overflow: hidden; + position: relative; + width: 100%; + + &:after, &:before + content: ' '; + display: block; + height: 2px; + transition: background-color 0.3s ease; + width: 19px; + background-color: var(--text-secondary-color); + + &:before + left: 1px; + position: absolute; + top: 1px; + -webkit-transform: rotate(45deg); + transform: rotate(45deg); + -webkit-transform-origin: 1px 50%; + transform-origin: 1px 50%; + + &:after + right: 1px; + position: absolute; + top: 1px; + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); + -webkit-transform-origin: calc(100% - 1px) 50%; + transform-origin: calc(100% - 1px) 50%; + + .content + padding: 16px 22px 25px; + position: relative; + color: var(--text-primary-color); + + .simpleDialogSelect, .simpleDialogInput + background-color: var(--input-background-color); + color: var(--text-primary-color); + border: 0; + border-radius: 2px; + font-size: 14px; + height: 30px; + width: 100%; + line-height: 16px; + padding: 8px 9px 6px; + margin-top: 10px; + display: block; + + .simpleDialogLabelFor + font-size: 14px; + font-weight: 700; + line-height: 16px; + margin: 10px 0 0 0; + color: var(--text-primary-color); + + .footer + padding: 15px; + text-align: right; + + .btn + margin-left: 5px; diff --git a/packages/_components/src/components/simpleDialog/__docs__/simpleDialog.mdx b/packages/_components/src/components/simpleDialog/__docs__/simpleDialog.mdx new file mode 100644 index 000000000..764074b10 --- /dev/null +++ b/packages/_components/src/components/simpleDialog/__docs__/simpleDialog.mdx @@ -0,0 +1,83 @@ +--- +name: Simple Dialog +menu: Components +route: /components/simple-dialog +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { SimpleDialog } from './../index.js' + +# Simple Dialog + +Its similar to a modal but we can pass its style as props and manipulate it as +we want. Initially used to display tool data changes in our Viewport. + +## Basic usage + + + + {({ state, setState }) => ( + +
    {JSON.stringify(state, null, 2) }
    + + {/* Toggle Dialog */} + + + setState({ isDialogOpen: false })} + onConfirm={() => setState({ + isDialogOpen: false, + // If you don't want to update external state until complete, + // This is where you could grab current field values and update parent + // With setState + // + // inputValue: this.input.value, + // selectValue: this.select.value + })} + componentStyle={state.dialogStyle} + onValueChanged={(value) => setState({value}) } + > + + setState({inputValue: e.target.value})} + /> + + + +
    + )} +
    + +
    + +## API + + diff --git a/packages/_components/src/components/simpleDialog/index.js b/packages/_components/src/components/simpleDialog/index.js new file mode 100644 index 000000000..df8327865 --- /dev/null +++ b/packages/_components/src/components/simpleDialog/index.js @@ -0,0 +1 @@ +export { SimpleDialog } from './SimpleDialog.js'; diff --git a/packages/_components/src/components/studyBrowser/DragPreview.js b/packages/_components/src/components/studyBrowser/DragPreview.js new file mode 100644 index 000000000..0334e3483 --- /dev/null +++ b/packages/_components/src/components/studyBrowser/DragPreview.js @@ -0,0 +1,85 @@ +import React, { PureComponent, memo } from 'react'; +import { DragLayer } from 'react-dnd'; +import PropTypes from 'prop-types'; +import { ThumbnailEntry } from './ThumbnailEntry'; +import './DragPreview.styl'; + +let subscribedToOffsetChange = false; +let dragPreviewRef = null; + +const collector = monitor => { + if (!subscribedToOffsetChange) { + monitor.subscribeToOffsetChange(onOffsetChange(monitor)); + subscribedToOffsetChange = true; + } + + if (dragPreviewRef) { + const offset = + monitor.getSourceClientOffset() || monitor.getInitialSourceClientOffset(); + + if (offset) { + const transform = `translate(${offset.x}px, ${offset.y}px)`; + dragPreviewRef.style['transform'] = transform; + dragPreviewRef.style['-webkit-transform'] = transform; + } + } + + const item = monitor.getItem(); + let newItem = {}; + if (item) { + newItem = { + active: item.active, + altImageText: item.altImageText, + id: item.id, + imageSrc: item.imageSrc, + imageId: item.imageId, + instanceNumber: item.instanceNumber, + error: item.error, + numImageFrames: item.numImageFrames, + seriesDescription: item.seriesDescription, + seriesNumber: item.seriesNumber, + stackPercentComplete: item.stackPercentComplete, + }; + } + + return { + ...newItem, + isDragging: monitor.isDragging(), + }; +}; + +const onOffsetChange = monitor => () => { + if (!dragPreviewRef) return; + + const offset = + monitor.getSourceClientOffset() || monitor.getInitialSourceClientOffset(); + if (!offset) return; + + const transform = `translate(${offset.x}px, ${offset.y}px)`; + dragPreviewRef.style['transform'] = transform; + dragPreviewRef.style['-webkit-transform'] = transform; +}; + +const updateRef = ref => { + dragPreviewRef = ref; +}; + +class DragPreview extends PureComponent { + render() { + const { isDragging } = this.props; + if (!isDragging) return null; + return ( +
    +
    + +
    +
    + ); + } +} + +DragPreview.propTypes = { + isDragging: PropTypes.bool, +}; + +export default DragLayer(collector)(memo(DragPreview)); diff --git a/packages/_components/src/components/studyBrowser/DragPreview.styl b/packages/_components/src/components/studyBrowser/DragPreview.styl new file mode 100644 index 000000000..4866733d4 --- /dev/null +++ b/packages/_components/src/components/studyBrowser/DragPreview.styl @@ -0,0 +1,14 @@ +.DragPreview + + .source-preview { + border-radius: 5px; + background-color: rgba(0, 0, 0, 0.5); + position: fixed; + opacity: 0.5; + z-index: 9999; + left: 0; + top: 0; + transition: none; + pointer-events: none; + -webkit-touch-callout: none; + } diff --git a/packages/_components/src/components/studyBrowser/ExampleDropTarget.css b/packages/_components/src/components/studyBrowser/ExampleDropTarget.css new file mode 100644 index 000000000..3d5fc34c6 --- /dev/null +++ b/packages/_components/src/components/studyBrowser/ExampleDropTarget.css @@ -0,0 +1,30 @@ +.ExampleDropTarget { + display: flex; + justify-content: center; + flex-direction: column; + + min-height: 100px; + + border: 2px dashed black; + padding: 20px; + transition: 0.2s all ease; +} + +.ExampleDropTarget h4, +.ExampleDropTarger p { + text-align: center; + word-break: break-word; +} + +.ExampleDropTarget.can-drop { + border: 2px dashed #20a5d6; + background-color: #fafafa; + color: black; +} + +.ExampleDropTarget.hovered { + border: 2px dashed #20a5d6; + background-color: black; + color: #91b9cd; + cursor: copy; +} diff --git a/packages/_components/src/components/studyBrowser/ExampleDropTarget.js b/packages/_components/src/components/studyBrowser/ExampleDropTarget.js new file mode 100644 index 000000000..a8ce6327f --- /dev/null +++ b/packages/_components/src/components/studyBrowser/ExampleDropTarget.js @@ -0,0 +1,84 @@ +import React, { Component } from 'react'; +import { DropTarget } from 'react-dnd'; +import PropTypes from 'prop-types'; +import './ExampleDropTarget.css'; + +// Drag sources and drop targets only interact +// if they have the same string type. +const Types = { + THUMBNAIL: 'thumbnail', +}; + +const divTarget = { + drop(props, monitor, component) { + // Note: For this example we use setState, but in + // OHIF we will update the redux store instead + const item = monitor.getItem(); + + component.setState({ + item: { + id: item.id, + seriesDescription: item.seriesDescription, + }, + }); + return { id: 'ExampleDropTarget' }; + }, +}; + +// TODO: Find out why we can't move this into the Example app instead. +// It looks like the context isn't properly shared. +class CustomDropTarget extends Component { + static className = 'ExampleDropTarget'; + + state = { + item: null, + }; + + static defaultProps = { + isOver: false, + }; + + static propTypes = { + connectDropTarget: PropTypes.func.isRequired, + canDrop: PropTypes.bool.isRequired, + isOver: PropTypes.bool.isRequired, + }; + + render() { + const { canDrop, isOver, connectDropTarget } = this.props; + const isActive = canDrop && isOver; + + let className = ExampleDropTarget.className; + + if (isActive) { + className += ' hovered'; + } else if (canDrop) { + className += ' can-drop'; + } + + return connectDropTarget( +
    +

    + {isActive + ? 'Release to drop' + : 'Drag / Drop something from the Study Browser here'} +

    +

    + {this.state.item && JSON.stringify(this.state.item)} +

    +
    + ); + } +} + +const collect = (connect, monitor) => ({ + connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), +}); + +const ExampleDropTarget = DropTarget(Types.THUMBNAIL, divTarget, collect)( + CustomDropTarget +); + +export { ExampleDropTarget }; diff --git a/packages/_components/src/components/studyBrowser/ImageThumbnail.js b/packages/_components/src/components/studyBrowser/ImageThumbnail.js new file mode 100644 index 000000000..95a5e34a8 --- /dev/null +++ b/packages/_components/src/components/studyBrowser/ImageThumbnail.js @@ -0,0 +1,136 @@ +import './ImageThumbnail.styl'; + +import React, { PureComponent } from 'react'; + +import PropTypes from 'prop-types'; +import ViewportErrorIndicator from '../../viewer/ViewportErrorIndicator'; +import ViewportLoadingIndicator from '../../viewer/ViewportLoadingIndicator'; + +// TODO: How should we have this component depend on Cornerstone? +// - Passed in as a prop? +// - Set as external dependency? +// - Pass in the entire load and render function as a prop? +//import cornerstone from 'cornerstone-core'; + +/** + * Asynchronous wrapper around Cornerstone's renderToCanvas method. + * + * @param {HTMLElement} canvasElement An HTML element + * @param {Image} image A Cornerstone Image + * + * @return {Promise} A promise tracking the progress of the rendering. Resolves empty. + */ +function renderAsync(canvasElement, image) { + return new Promise((resolve, reject) => { + try { + cornerstone.renderToCanvas(canvasElement, image); + resolve(); + } catch (error) { + reject(error); + } + }); +} + +export default class ImageThumbnail extends PureComponent { + static propTypes = { + imageSrc: PropTypes.string, + imageId: PropTypes.string, + error: PropTypes.bool.isRequired, + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + stackPercentComplete: PropTypes.number.isRequired, + }; + + static defaultProps = { + error: false, + stackPercentComplete: 0, + width: 217, + height: 123, + }; + + constructor(props) { + super(props); + + this.canvas = React.createRef(); + + const renderIntoCanvas = this.props.imageId && !this.props.imageSrc; + + this.state = { + loading: renderIntoCanvas, + }; + } + + componentDidMount() { + const renderIntoCanvas = this.props.imageId && !this.props.imageSrc; + + if (renderIntoCanvas) { + const { imageId } = this.props; + const canvas = this.canvas.current; + + cornerstone.loadAndCacheImage(imageId).then( + image => { + renderAsync(canvas, image).then( + () => { + this.setState({ + loading: false, + }); + }, + error => { + // TODO: Set state? + throw new Error(error); + } + ); + }, + error => { + // TODO: Set state? + throw new Error(error); + } + ); + } + } + + render() { + let loadingOrError; + if (this.props.error) { + loadingOrError = ; + } else if (this.state.loading) { + loadingOrError = ; + } + + const showStackLoadingProgressBar = + this.props.stackPercentComplete !== undefined; + + const renderIntoCanvas = this.props.imageId && !this.props.imageSrc; + + return ( +
    +
    + {renderIntoCanvas ? ( + + ) : ( + {''} + )} +
    + {loadingOrError} + {showStackLoadingProgressBar && ( +
    +
    +
    + )} +
    + ); + } +} diff --git a/packages/_components/src/components/studyBrowser/ImageThumbnail.styl b/packages/_components/src/components/studyBrowser/ImageThumbnail.styl new file mode 100644 index 000000000..606ffb9d1 --- /dev/null +++ b/packages/_components/src/components/studyBrowser/ImageThumbnail.styl @@ -0,0 +1,70 @@ +:root { + --text-secondary-color: #91B9CD; + --active-color: #20A5D6; + --primary-background-color: black; + --ui-border-color-dark: #3C5D80 + --sidebar-transition: all 0.3s ease; +} + +.ThumbnailEntry.active .ImageThumbnail + border-color: var(--active-color); + box-shadow: none + transition: var(--sidebar-transition); + +.ImageThumbnail + background-color: var(--primary-background-color); + box-shadow: inset 0 0 0 1px var(--ui-border-color-dark); + border: 5px solid transparent + border-radius: 12px + height: 135px + margin: 0 auto + padding 5px + position: relative + transition: var(--sidebar-transition); + width: 217px + -moz-background-clip: padding + -webkit-background-clip: padding + background-clip: padding-box + +.image-thumbnail-canvas + height: 100% + overflow: hidden + display: flex + justify-content: center + + img + -webkit-user-drag: none + pointer-events: none + + +.thumbnailLoadingIndicator + display: none + pointer-events: none + color: var(--text-secondary-color); + height: 20px + width: 100% + top: 0 + left: 0 + right: 0 + bottom: 0 + margin: auto + position: absolute + + &.d-block + display: block; + + p + text-align: center + font-size: 10pt + +.image-thumbnail-progress-bar + position: relative + width: 100% + height: 3px + top: -5px + + .image-thumbnail-progress-bar-inner + height: 100% + width: 0 + border-radius: 5px + background-color: var(--active-color); diff --git a/packages/_components/src/components/studyBrowser/StudyBrowser.js b/packages/_components/src/components/studyBrowser/StudyBrowser.js new file mode 100644 index 000000000..59cda8c17 --- /dev/null +++ b/packages/_components/src/components/studyBrowser/StudyBrowser.js @@ -0,0 +1,62 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { ThumbnailEntry } from './ThumbnailEntry'; +import ThumbnailEntryDragSource from './ThumbnailEntryDragSource.js'; +import './StudyBrowser.styl'; + +class StudyBrowser extends Component { + static defaultProps = { + studies: [], + supportsDragAndDrop: true, + }; + + static propTypes = { + studies: PropTypes.array.isRequired, + supportsDragAndDrop: PropTypes.bool.isRequired, + onThumbnailClick: PropTypes.func, + onThumbnailDoubleClick: PropTypes.func, + }; + + render() { + const studies = this.props.studies; + + const thumbnails = studies.map((study, studyIndex) => { + return study.thumbnails.map((thumb, thumbIndex) => { + if (this.props.supportsDragAndDrop) { + return ( + + ); + } else { + return ( +
    + +
    + ); + } + }); + }); + + const components = thumbnails.flat(); + return ( +
    +
    {components}
    +
    + ); + } +} + +export { StudyBrowser }; diff --git a/packages/_components/src/components/studyBrowser/StudyBrowser.styl b/packages/_components/src/components/studyBrowser/StudyBrowser.styl new file mode 100644 index 000000000..c4915f826 --- /dev/null +++ b/packages/_components/src/components/studyBrowser/StudyBrowser.styl @@ -0,0 +1,40 @@ +.StudyBrowser + float: left + height: 100% + width: 100% + overflow: hidden + background-color: black + padding-bottom: 20px + padding-top: 10px + + .scrollable-study-thumbnails + height: 100% + overflow-y: auto + overflow-x: hidden + padding-bottom: 50px + padding-right: 16px + padding-left: 4px + margin-right: -16px + + display: flex; + flex-direction: column; + + &::-webkit-scrollbar + display: none + + .ThumbnailEntryContainer + margin: 0 auto; + padding-bottom: 2.5rem; + + .noselect + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer */ + -khtml-user-select: none; /* KHTML browsers (e.g. Konqueror) */ + -webkit-user-select: none; /* Chrome, Safari, and Opera */ + -webkit-touch-callout: none; /* Disable Android and iOS callouts*/ + + +.draggable + cursor: copy + cursor: -webkit-grab + cursor: -moz-grab diff --git a/packages/_components/src/components/studyBrowser/ThumbnailEntry.js b/packages/_components/src/components/studyBrowser/ThumbnailEntry.js new file mode 100644 index 000000000..8756dc542 --- /dev/null +++ b/packages/_components/src/components/studyBrowser/ThumbnailEntry.js @@ -0,0 +1,113 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import ImageThumbnail from './ImageThumbnail'; +import './ThumbnailEntry.styl'; +import classnames from 'classnames'; + +class ThumbnailEntry extends Component { + static defaultProps = { + active: false, + error: false, + stackPercentComplete: 0, + }; + + static propTypes = { + id: PropTypes.string.isRequired, + imageSrc: PropTypes.string, + imageId: PropTypes.string, + error: PropTypes.bool.isRequired, + active: PropTypes.bool.isRequired, + stackPercentComplete: PropTypes.number, + /** + altImageText will be used when no imageId or imageSrc is provided. + It will be displayed inside the
    . This is useful when it is difficult + to make a preview for a type of DICOM series (e.g. DICOM-SR) + */ + altImageText: PropTypes.string, + seriesDescription: PropTypes.string, + seriesNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + instanceNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + numImageFrames: PropTypes.number, + onDoubleClick: PropTypes.func, + onClick: PropTypes.func, + }; + + render() { + const hasInstanceNumber = this.props.instanceNumber !== undefined; + + let className = classnames('ThumbnailEntry noselect', { + active: this.props.active, + }); + const infoOnly = false; + + let contents = null; + if (this.props.imageSrc || this.props.imageId) { + contents = ( +
    + +
    + ); + } else if (this.props.altImageText) { + contents = ( +
    +

    {this.props.altImageText}

    +
    + ); + } + + return ( +
    + {contents} +
    +
    + {this.props.seriesDescription} +
    +
    +
    +
    S:
    +
    {this.props.seriesNumber}
    +
    + {hasInstanceNumber && ( +
    +
    I:
    +
    {this.props.instanceNumber}
    +
    + )} +
    +
    +
    +
    +
    {this.props.numImageFrames}
    +
    +
    +
    +
    + ); + } + + onClick = () => { + if (this.props.onClick) { + this.props.onClick(); + } + }; + + onDoubleClick = () => { + if (this.props.onDoubleClick) { + this.props.onDoubleClick(); + } + }; +} + +export { ThumbnailEntry }; diff --git a/packages/_components/src/components/studyBrowser/ThumbnailEntry.styl b/packages/_components/src/components/studyBrowser/ThumbnailEntry.styl new file mode 100644 index 000000000..f7fc4d8df --- /dev/null +++ b/packages/_components/src/components/studyBrowser/ThumbnailEntry.styl @@ -0,0 +1,125 @@ +:root { + --series-count-background-color: #678696; +} + +.ThumbnailEntry + cursor: pointer + display: table + + .p-x-1 + padding: 0 1rem + + .alt-image-text + align-items: center; + justify-content: center; + background-color: var(--primary-background-color); + box-shadow: inset 0 0 0 1px var(--ui-border-color-dark); + border: 5px solid transparent; + border-radius: 12px; + height: 135px; + margin: 0 auto; + padding: 5px; + position: relative; + transition: var(--sidebar-transition); + width: 217px; + display: flex; + + h1 + text-align: center + color: var(--text-primary-color); + + .series-details + display: flex; + flex-direction: row; + color: var(--text-primary-color); + font-size: 14px + line-height: 1.3em + margin-top: 5px + margin-left: 1rem + margin-right: 1rem + max-width: 217px + min-height: 36px + position: relative + word-wrap: break-word + + .series-description + flex-grow: 1 + + &.info-only + .series-description + display: none + + .series-information + display: flex + flex-grow: 1 + float: none + max-width: none + padding-right: 0 + + .item + flex: 1 + text-align: center + + .icon, .value + display: inline + float: none + line-height: 25px + + .value + margin-left: 0 + width: auto + + .series-information + padding-right: 4px + max-width: 50px + + .item-frames .icon + height: 18px + + .value + color: var(--text-secondary-color); + display: inline-block + float: right + font-size: 12px + margin-left: 4px + overflow: hidden + text-overflow: ellipsis + white-space: nowrap + width: calc(100% - 15px) + + .icon + color: var(--active-color); + display: inline-block + float: left + font-size: 10px + font-weight: 900 + text-align: right + width: 11px + + div + background-color: var(--series-count-background-color); + margin-top: 6px + position: relative + + &:after + background-color: var(--active-color); + box-shadow: 1px 1px rgba(0, 0, 0, .115) + left: -5px + position: absolute + top: -5px + + & + &:after + border: 1px solid var(--series-count-background-color) + content: '' + display: inline-block + height: 11px + width: 11px + +.image-thumbnail-clone + z-index: 100000 + pointer-events: none + + &:hover + border-color: var(--hover-color); + box-shadow: none diff --git a/packages/_components/src/components/studyBrowser/ThumbnailEntryDragSource.js b/packages/_components/src/components/studyBrowser/ThumbnailEntryDragSource.js new file mode 100644 index 000000000..35146a275 --- /dev/null +++ b/packages/_components/src/components/studyBrowser/ThumbnailEntryDragSource.js @@ -0,0 +1,64 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { DragSource } from 'react-dnd'; +import DragPreview from './DragPreview'; +import { ThumbnailEntry } from './ThumbnailEntry.js'; + +// Drag sources and drop targets only interact +// if they have the same string type. +const Types = { + THUMBNAIL: 'thumbnail', +}; + +const thumbnailSource = { + /*canDrag(props) { + return props.error === false; + },*/ + + beginDrag(props) { + return props; + }, + + endDrag(props, monitor) { + //const item = monitor.getItem(); + const dropResult = monitor.getDropResult(); + + if (dropResult) { + //console.log(`You dropped ${item.id} into ${dropResult.id}!`); + //console.log(item); + } + }, +}; + +class ThumbnailEntryDragSource extends Component { + static propTypes = { + connectDragSource: PropTypes.func.isRequired, + isDragging: PropTypes.bool.isRequired, + }; + + static defaultProps = { + isDragging: false, + }; + + render() { + const { connectDragSource } = this.props; + const dropEffect = 'copy'; + + return connectDragSource( +
    + + +
    , + { dropEffect } + ); + } +} + +const collect = (connect, monitor) => ({ + connectDragSource: connect.dragSource(), + isDragging: monitor.isDragging(), +}); + +export default DragSource(Types.THUMBNAIL, thumbnailSource, collect)( + ThumbnailEntryDragSource +); diff --git a/packages/_components/src/components/studyBrowser/__docs__/exampleStudies.js b/packages/_components/src/components/studyBrowser/__docs__/exampleStudies.js new file mode 100644 index 000000000..ee6a6d321 --- /dev/null +++ b/packages/_components/src/components/studyBrowser/__docs__/exampleStudies.js @@ -0,0 +1,75 @@ +export const studies = [ + { + thumbnails: [ + { + imageSrc: + 'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/Anti-PD-1_Lung.jpg', + seriesDescription: 'Anti-PD-1_Lung', + active: true, + seriesNumber: '2', + numImageFrames: 512, + stackPercentComplete: 30, + }, + { + imageSrc: + 'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/Anti-PD-1_MELANOMA.jpg', + seriesDescription: 'Anti-PD-1_MELANOMA', + seriesNumber: '2', + instanceNumber: '1', + numImageFrames: 256, + stackPercentComplete: 70, + }, + { + altImageText: 'SR', + seriesDescription: 'Imaging Measurement Report', + seriesNumber: '3', + stackPercentComplete: 100, + }, + ], + }, + { + thumbnails: [ + { + imageSrc: + 'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-GBM.jpg', + seriesDescription: 'CPTAC-GBM', + active: true, + seriesNumber: '2', + numImageFrames: 512, + stackPercentComplete: 100, + }, + { + imageSrc: + 'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-CM.jpg', + seriesDescription: 'CPTAC-CM', + seriesNumber: '2', + instanceNumber: '1', + numImageFrames: 256, + }, + { + imageSrc: + 'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-HNSCC.jpg', + seriesDescription: 'CPTAC-HNSCC', + seriesNumber: '2', + instanceNumber: '1', + numImageFrames: 256, + }, + { + imageSrc: + 'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-LSCC.jpg', + seriesDescription: 'CPTAC-LSCC', + seriesNumber: '2', + instanceNumber: '1', + numImageFrames: 256, + }, + ], + }, +]; + +export function onThumbnailClick() { + console.warn('onThumbnailClick'); +} + +export function onThumbnailDoubleClick() { + console.warn('onThumbnailDoubleClick'); +} diff --git a/packages/_components/src/components/studyBrowser/__docs__/studyBrowser.mdx b/packages/_components/src/components/studyBrowser/__docs__/studyBrowser.mdx new file mode 100644 index 000000000..f9606fe84 --- /dev/null +++ b/packages/_components/src/components/studyBrowser/__docs__/studyBrowser.mdx @@ -0,0 +1,22 @@ +--- +name: Study Browser +menu: Components +route: /components/study-browser +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { WrappedStudyBrowser } from './wrappedStudyBrowser.js' +import { StudyBrowser } from './../index.js' + +# Study Browser + +## Basic usage + + + + + +## API + + diff --git a/packages/_components/src/components/studyBrowser/__docs__/wrappedStudyBrowser.js b/packages/_components/src/components/studyBrowser/__docs__/wrappedStudyBrowser.js new file mode 100644 index 000000000..9d2c29037 --- /dev/null +++ b/packages/_components/src/components/studyBrowser/__docs__/wrappedStudyBrowser.js @@ -0,0 +1,40 @@ +import React, { Component } from 'react'; +import { DragDropContext } from 'react-dnd'; +import TouchBackend from 'react-dnd-touch-backend'; + +// +import { + studies, + onThumbnailClick, + onThumbnailDoubleClick, +} from './exampleStudies.js'; +import { ExampleDropTarget, StudyBrowser } from './../index.js'; + +class StudyBrowserContainer extends Component { + render() { + //const viewportData = [null, null, null, null]; + + return ( + + + + + ); + } +} + +// Note: +// Normally, the top level APP component is wrapped with the DragDropContext +// We wrap this component to create a simple/local example. +const WrappedStudyBrowser = DragDropContext( + TouchBackend({ enableMouseEvents: true }), + null, + true +)(StudyBrowserContainer); + +// http://react-dnd.github.io/react-dnd/docs/api/drag-drop-context +export { WrappedStudyBrowser }; diff --git a/packages/_components/src/components/studyBrowser/index.js b/packages/_components/src/components/studyBrowser/index.js new file mode 100644 index 000000000..9ed1c3194 --- /dev/null +++ b/packages/_components/src/components/studyBrowser/index.js @@ -0,0 +1,3 @@ +export { ExampleDropTarget } from './ExampleDropTarget.js'; +export { StudyBrowser } from './StudyBrowser.js'; +export { ThumbnailEntry } from './ThumbnailEntry.js'; diff --git a/packages/_components/src/components/studyList/CustomDateRangePicker.js b/packages/_components/src/components/studyList/CustomDateRangePicker.js new file mode 100644 index 000000000..b850685e1 --- /dev/null +++ b/packages/_components/src/components/studyList/CustomDateRangePicker.js @@ -0,0 +1,99 @@ +// If you want to continue using CSS stylesheets and classes... +// https://github.com/airbnb/react-dates#initialize +import 'react-dates/initialize'; +import 'react-dates/lib/css/_datepicker.css'; + +import React from 'react'; +import PropTypes from 'prop-types'; +import { DateRangePicker } from 'react-dates'; + +import './CustomDateRangePicker.styl'; + +export default class CustomDateRangePicker extends React.Component { + static propTypes = { + presets: PropTypes.arrayOf( + PropTypes.shape({ + text: PropTypes.string, + start: PropTypes.required, + end: PropTypes.required, + }) + ), + }; + + constructor(props) { + super(props); + this.state = { + startDate: this.props.startDate, + endDate: this.props.endDate, + }; + + this.renderDatePresets = this.renderDatePresets.bind(this); + this.onDatesChange = this.onDatesChange.bind(this); + } + + onDatesChange({ startDate, endDate, preset }) { + this.setState({ startDate, endDate }); + if (this.props.onDatesChange) { + this.props.onDatesChange({ startDate, endDate, preset }); + } + } + + renderDatePresets() { + const { presets } = this.props; + const { startDate, endDate } = this.state; + + return ( +
    + {presets.map(({ text, start, end }) => { + const isSelected = startDate === start && endDate === end; + + return ( + + ); + })} +
    + ); + } + + render() { + let { + autoFocus, + autoFocusEndDate, + initialStartDate, + initialEndDate, + stateDateWrapper, + onDatesChange, + startDate, + endDate, + presets, + ...dateRangePickerProps + } = this.props; + + return ( +
    + +
    + ); + } +} diff --git a/packages/_components/src/components/studyList/CustomDateRangePicker.styl b/packages/_components/src/components/studyList/CustomDateRangePicker.styl new file mode 100644 index 000000000..1112a0c90 --- /dev/null +++ b/packages/_components/src/components/studyList/CustomDateRangePicker.styl @@ -0,0 +1,84 @@ +.CalendarDay__selected_span { + background: var(--calendar-main-color); + color: var(--calendar-day-color); + border: 1px solid; + border-color: var(--calendar-day-border-color); +} + +.CalendarDay__default:hover { + color: var(--calendar-main-color); +} + +.CalendarDay__selected_span:active, .CalendarDay__selected_span:hover { + background: var(--calendar-day-active-hover-background-color); + border-color: var(--calendar-day-border-color); + color: var(--calendar-day-color); +} + +.CalendarDay__selected { + background: var(--calendar-main-color); + color: var(--calendar-day-color); + border-color: var(--calendar-day-border-color); +} + +.CalendarDay__selected:hover { + background: var(--calendar-main-color); + color: var(--calendar-day-color); + border-color: var(--calendar-day-border-color); +} + +.CalendarDay__hovered_span:hover, +.CalendarDay__hovered_span { + background: var(--calendar-main-color); + color: var(--calendar-day-color); + border-color:var(--calendar-day-color); +} + +.DateInput { + width: 97px +} + +.DateInput_input { + background-color: var(--input-background-color) + border-color: var(--input-background-color) + color: var(--input-placeholder-color) + height: 38px + font-size: 10pt +} + +.DateRangePickerInput { + background-color: var(--input-background-color) + border-color: var(--input-background-color) + color: var(--input-placeholder-color) + height: 40px +} + +.PresetDateRangePicker_panel: { + padding: 0 22px 11px 22px; +} + +.PresetDateRangePicker_button { + position: relative; + height: 100%; + textAlign: center; + background: none; + border: 2px solid var(--calendar-main-color); + color: var(--calendar-main-color); + padding: 4px 12px; + marginRight: 8; + font: inherit; + fontWeight: 700; + lineHeight: normal; + overflow: visible; + boxSizing: border-box; + cursor: pointer; + + :active: { + outline: 0; + } +} + +.PresetDateRangePicker_button__selected: { + color: var(--calendar-day-color); + background: var(--calendar-main-color); +}; diff --git a/packages/_components/src/components/studyList/PaginationArea.js b/packages/_components/src/components/studyList/PaginationArea.js new file mode 100644 index 000000000..3c4fb7997 --- /dev/null +++ b/packages/_components/src/components/studyList/PaginationArea.js @@ -0,0 +1,109 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import './PaginationArea.styl'; + +class PaginationArea extends PureComponent { + static defaultProps = { + pageOptions: [5, 10, 25, 50, 100], + rowsPerPage: 25, + currentPage: 0, + }; + + static propTypes = { + pageOptions: PropTypes.array.isRequired, + rowsPerPage: PropTypes.number.isRequired, + currentPage: PropTypes.number.isRequired, + nextPageFunc: PropTypes.func, + prevPageFunc: PropTypes.func, + onRowsPerPageChange: PropTypes.func, + recordCount: PropTypes.number.isRequired, + }; + + nextPage = () => { + this.props.nextPageFunc(this.props.currentPage); + }; + + prevPage = () => { + this.props.prevPageFunc(this.props.currentPage); + }; + + onRowsPerPageChange = event => { + this.props.onRowsPerPageChange(parseInt(event.target.value)); + }; + + renderPaginationButtons() { + return ( +
    +
    + +
    +
    + ); + } + + renderRowsPerPageDropdown() { + return ( +
    + Show + + rows per page +
    + ); + } + + render() { + return ( +
    +
    +
    +
    + {this.renderRowsPerPageDropdown()} +
    +
    +
    + {this.renderPaginationButtons()} +
    +
    +
    +
    +
    + ); + } +} + +export { PaginationArea }; diff --git a/packages/_components/src/components/studyList/PaginationArea.styl b/packages/_components/src/components/studyList/PaginationArea.styl new file mode 100644 index 000000000..9a918c00f --- /dev/null +++ b/packages/_components/src/components/studyList/PaginationArea.styl @@ -0,0 +1,51 @@ +.pagination-area + color: var(--text-secondary-color); + font-size: 13px + font-weight: normal !important + + label + font-weight: normal + + select + margin: 5px + background-color: var(--primary-background-color) + color: white + + .rows-per-page label.wrapperLabel + display: inline-table !important + margin: 0 4px + + select + margin: 0px 4px 0px 4px + width: 42px + + .page-buttons + margin: 0 + text-align: right + + label + font-weight: normal + + ul.pagination-control + margin: 0 + + li + display: table-cell + padding: 5px 2px + + button + padding: 4px 8px + background-color: var(--primary-background-color) + border-color: var(--ui-gray) + color: var(--ui-gray-darkest) + color: white + text-decoration: none + + &:hover:enabled + color: var(--active-color) + + .active + button + background-color: var(--ui-gray) + border-color: #ddd + color: white diff --git a/packages/_components/src/components/studyList/StudyList.js b/packages/_components/src/components/studyList/StudyList.js new file mode 100644 index 000000000..1f9c7ca8e --- /dev/null +++ b/packages/_components/src/components/studyList/StudyList.js @@ -0,0 +1,428 @@ +import './StudyList.styl'; + +import React, { Component } from 'react'; + +import CustomDateRangePicker from './CustomDateRangePicker.js'; +import { Icon } from './../../elements/Icon'; +import { PaginationArea } from './PaginationArea.js'; +import PropTypes from 'prop-types'; +import { StudyListLoadingText } from './StudyListLoadingText.js'; +import { StudylistToolbar } from './StudyListToolbar.js'; +import { isInclusivelyBeforeDay } from 'react-dates'; +import moment from 'moment'; +import debounce from 'lodash.debounce'; + +const today = moment(); +const lastWeek = moment().subtract(7, 'day'); +const lastMonth = moment().subtract(1, 'month'); + +class StudyList extends Component { + static propTypes = { + studies: PropTypes.array.isRequired, + onSelectItem: PropTypes.func.isRequired, + onSearch: PropTypes.func.isRequired, + currentPage: PropTypes.number, + rowsPerPage: PropTypes.number, + studyListDateFilterNumDays: PropTypes.number, + studyListFunctionsEnabled: PropTypes.bool, + defaultSort: PropTypes.shape({ + field: PropTypes.string.isRequired, + order: PropTypes.oneOf(['desc', 'asc']).isRequired, + }), + onImport: PropTypes.func, + pageOptions: PropTypes.array, + }; + + static defaultProps = { + currentPage: 0, + rowsPerPage: 25, + studyListDateFilterNumDays: 7, + }; + + static studyDatePresets = [ + { + text: 'Today', + start: today, + end: today, + }, + { + text: 'Last 7 days', + start: lastWeek, + end: today, + }, + { + text: 'Last 30 days', + start: lastMonth, + end: today, + }, + ]; + + constructor(props) { + super(props); + + const sortData = { + field: undefined, + order: undefined, + }; + + // init from props + if (props.defaultSort) { + sortData.field = props.defaultSort.field; + // todo: -1, 0, 1? + sortData.order = props.defaultSort.order; // asc, desc + } + + this.defaultStartDate = moment().subtract( + this.props.studyListDateFilterNumDays, + 'days' + ); + this.defaultEndDate = moment(); + + this.state = { + loading: false, + error: false, + searchData: { + sortData, + currentPage: this.props.currentPage, + rowsPerPage: this.props.rowsPerPage, + studyDateFrom: this.defaultStartDate, + studyDateTo: this.defaultEndDate, + }, + highlightedItem: '', + }; + + this.getChangeHandler = this.getChangeHandler.bind(this); + this.getBlurHandler = this.getBlurHandler.bind(this); + this.onInputKeydown = this.onInputKeydown.bind(this); + this.nextPage = this.nextPage.bind(this); + this.prevPage = this.prevPage.bind(this); + this.onRowsPerPageChange = this.onRowsPerPageChange.bind(this); + this.delayedSearch = debounce(this.search, 250); + } + + getChangeHandler(key) { + return event => { + this.delayedSearch.cancel(); + this.setSearchData(key, event.target.value, this.delayedSearch); + }; + } + + getBlurHandler(key) { + return event => { + this.delayedSearch.cancel(); + this.setSearchData(key, event.target.value, this.search); + }; + } + + setSearchData(key, value, callback) { + const searchData = this.state.searchData; + searchData[key] = value; + this.setState({ searchData }, callback); + } + + setSearchDataBatch(keyValues, callback) { + const searchData = this.state.searchData; + + Object.keys(keyValues).forEach(key => { + searchData[key] = keyValues[key]; + }); + + this.setState({ searchData }, callback); + } + + async onInputKeydown(event) { + if (event.key === 'Enter') { + event.preventDefault(); + event.stopPropagation(); + + this.delayedSearch.cancel(); + // reset the page because user is doing a new search + this.setSearchData('currentPage', 0, this.search); + } + } + + async search() { + try { + this.setState({ loading: true, error: false }); + await this.props.onSearch(this.state.searchData); + } catch (error) { + this.setState({ error: true }); + throw new Error(error); + } finally { + this.setState({ loading: false }); + } + } + + renderNoMachingResults() { + if (!this.props.studies.length && !this.state.error) { + return
    No matching results
    ; + } + } + + renderHasError() { + if (this.state.error) { + return ( +
    There was an error fetching studies
    + ); + } + } + + renderIsLoading() { + if (this.state.loading) { + return ( +
    + +
    + ); + } + } + + nextPage(currentPage) { + currentPage = currentPage + 1; + this.delayedSearch.cancel(); + this.setSearchData('currentPage', currentPage, this.search); + } + + prevPage(currentPage) { + currentPage = currentPage - 1; + this.delayedSearch.cancel(); + this.setSearchData('currentPage', currentPage, this.search); + } + + onRowsPerPageChange(rowsPerPage) { + this.delayedSearch.cancel(); + this.setSearchDataBatch({ rowsPerPage, currentPage: 0 }, this.search); + } + + onSortClick(field) { + return () => { + let order; + const sort = this.state.searchData.sortData; + const isSortedField = sort.field === field; + + if (isSortedField) { + if (sort.order === 'asc') { + order = 'desc'; + } else { + order = undefined; + field = undefined; + } + } else { + order = 'asc'; + } + + this.delayedSearch.cancel(); + this.setSearchData('sortData', { field, order }, this.search); + }; + } + + onHighlightItem(studyItemUid) { + this.setState({ highlightedItem: studyItemUid }); + } + + renderTableRow(study) { + return ( + { + // middle/wheel click + if (event.button === 1) { + this.props.onSelectItem(study.studyInstanceUid); + } + }} + onClick={() => { + this.onHighlightItem(study.studyInstanceUid); + this.props.onSelectItem(study.studyInstanceUid); + }} + > + + {study.patientName || '(empty)'} + + + {study.patientId} + {study.accessionNumber} + {study.studyDate} + {study.modalities} + {study.studyDescription} + + ); + } + + render() { + const tableMeta = { + patientName: { + displayText: 'Patient Name', + sort: 0, + }, + patientId: { + displayText: 'MRN', + sort: 0, + }, + accessionNumber: { + displayText: 'Accession #', + sort: 0, + }, + studyDate: { + displayText: 'Study Date', + inputType: 'date-range', + sort: 0, + }, + modalities: { + displayText: 'Modality', + sort: 0, + }, + studyDescription: { + displayText: 'Description', + sort: 0, + }, + }; + + // Apply sort + const sortedFieldName = this.state.searchData.sortData.field; + const sortedField = tableMeta[sortedFieldName]; + + if (sortedField) { + const sortOrder = this.state.searchData.sortData.order; + sortedField.sort = sortOrder === 'asc' ? 1 : 2; + } + + // Sort Icons + const sortIcons = ['sort', 'sort-up', 'sort-down']; + + return ( +
    +
    +
    Study List
    +
    + {this.props.studies.length} +
    +
    + { + + } +
    +
    +
    +
    + + + + {Object.keys(tableMeta).map((fieldName, i) => { + const field = tableMeta[fieldName]; + + return ( + + + + ); + })} + + + + {this.props.studies.map(study => { + return this.renderTableRow(study); + })} + +
    +
    + {field.displayText} + +
    + {!field.inputType && ( + + )} + {field.inputType === 'date-range' && ( +
    + + !isInclusivelyBeforeDay(day, moment()) + } + onDatesChange={({ + startDate, + endDate, + preset = false, + }) => { + if ( + startDate && + endDate && + (this.state.focusedInput === 'endDate' || + preset) + ) { + this.setSearchDataBatch( + { + studyDateFrom: startDate.toDate(), + studyDateTo: endDate.toDate(), + }, + this.search + ); + this.setState({ focusedInput: false }); + } else if (!startDate && !endDate) { + this.setSearchDataBatch( + { + studyDateFrom: null, + studyDateTo: null, + }, + this.search + ); + } + }} + focusedInput={this.state.focusedInput} + onFocusChange={focusedInput => { + this.setState({ focusedInput }); + }} + /> +
    + )} +
    + + {this.renderIsLoading()} + {this.renderHasError()} + {this.renderNoMachingResults()} + + +
    +
    + ); + } +} + +export { StudyList }; diff --git a/packages/_components/src/components/studyList/StudyList.styl b/packages/_components/src/components/studyList/StudyList.styl new file mode 100644 index 000000000..9cefa7a5d --- /dev/null +++ b/packages/_components/src/components/studyList/StudyList.styl @@ -0,0 +1,282 @@ +@import './../../design/styles/common/form.styl' +@import './../../design/styles/common/global.styl' + +$study-list-padding = 8%; +$study-list-toolbar-height = 75px; +$body-cell-height = 40px; + +$tablePaddingMediumScreen = 5px +$tablePaddingBigScreen = 3% +$tablePaddingLargeScreen = 5% + +// TODO: move this to a common mixin files +placeholder-color(c) + &::-webkit-input-placeholder + color: c + &:-moz-placeholder + color: c + &::-moz-placeholder + color: c + &:-ms-input-placeholder + color: c + +.DateRangePicker + height: 40px + margin: 0 5px 20px 5px + cursor: pointer + border: none + position: block + +.studyListToolbar + background-color: var(--ui-gray-darker) + height: $study-list-toolbar-height + margin-bottom: 2px + padding: 0 $study-list-padding + + &>div + display: inline-block + + .header + font-size: 22px + font-weight: 300 + color: var(--table-text-secondary-color) + line-height: $study-list-toolbar-height + + .studylistToolbar + height: $study-list-toolbar-height + line-height: $study-list-toolbar-height + + .studyCount + color: var(--large-numbers-color) + font-size: 40px + font-weight: 100 + line-height: $study-list-toolbar-height + +.theadBackground + height: 121px + position: absolute + width: 100% + + &:before, &:after + content: '' + display: block + left: 0 + position: absolute + width: 100% + + &:before + background-color: var(--ui-gray-darker) + height: 100% + top: 0 + z-index: 1 + + &:after + background-color: var(--ui-border-color-active) + bottom: -1px + height: 1px + z-index: 2 + +#studyListContainer + width: 100% + padding: 0 $study-list-padding + position: absolute + z-index: 2 + + .loading-text + color: var(--table-text-secondary-color) + font-size: 30px + + .notFound + color: var(--table-text-secondary-color) + font-size: 30px + font-weight: 200 + text-align: center + + table#tblStudyList + width: 100%; + max-width: 100%; + margin-bottom: 20px; + border-spacing: 0; + border-collapse: collapse; + + > tr + height: 20px + + > thead + white-space: nowrap + + tr + th + padding: 0 + border-bottom: 1px solid var(--ui-border-color-active); + width: 100%; + text-align: left; + border-top: 0; + + &.studyDate + min-width: 230px + + .display-text + display: inline-block + cursor: pointer + width: 100% + min-width: 95px + margin: 0 auto + color: var(--table-text-primary-color) + font-weight: 400 + padding: 20px + -webkit-user-select: none + -moz-user-select: none + -ms-user-select: none + user-select: none + + span + font-size: 15px + float: left + + i + margin: 0 5px + + &:hover + color: var(--active-color) + + &.active, &:active + color: var(--active-color) + + input.studylist-search + height: 40px + margin: 0 5px 20px 5px + padding: 0 20px + cursor: pointer + border: none + background-color: var(--input-background-color) + color: var(--input-placeholder-color) + font-size: 10pt + font-weight: normal + width: calc(100% - 10px) + border-radius: 4px + + transition(all 0.15s ease) + placeholder-color(var(--input-placeholder-color)) + + &.invisible + visibility: hidden + + &:active, &:hover + background-color: var(--input-background-color) + + ::-webkit-datetime-edit-year-field:not([aria-valuenow]), + ::-webkit-datetime-edit-month-field:not([aria-valuenow]), + ::-webkit-datetime-edit-day-field:not([aria-valuenow]) + color: transparent + + > tbody + tr + padding: 5px + background-color: black + + &:nth-child(even) + background-color: var(--ui-gray-darker) + + td + padding: 8px; + height: $body-cell-height + line-height: $body-cell-height + color: var(--table-text-primary-color) + font-weight: 300 + border-top: 1px solid var(--ui-gray-lighter) + border-bottom: 1px solid var(--ui-gray-lighter) + white-space: nowrap + transition(all 0.1s ease) + + &.emptyCell + color: #516873 + + &.emptyValue + color: var(--ui-gray-light) + + &:hover, &:active, &.active + background-color: var(--table-hover-color) + color: var(--text-primary-color) + + td + // This selector is necessary to override bootstrap's 'table' class + border-top: 1px solid var(--ui-gray-lighter) + border-bottom: 1px solid var(--ui-gray-lighter) + background-color: var(--table-hover-color) + +@media only screen and (max-width: 1362px) + #studyListContainer + padding: 0 $tablePaddingLargeScreen + + table#tblStudyList + thead, + tbody + tr + th, + td + &:first-child + padding-left: $tablePaddingLargeScreen + + &:last-child + padding-right: $tablePaddingLargeScreen + +@media only screen and (max-width: 1161px) + #studyListContainer + padding: 0 $tablePaddingBigWidth + + table#tblStudyList + thead, + tbody + tr + th, + td + &:first-child + padding-left: $tablePaddingBigScreen + + &:last-child + padding-right: $tablePaddingBigScreen + +@media only screen and (max-width: 1069px) + .theadBackground + height: 101px + + .studylist-pagination > .row + margin-right: 0; + + .studyListToolbar + padding: 0 $tablePaddingMediumScreen + + #studyListContainer + padding: 0 + + table#tblStudyList + thead > tr > th + + &:first-child + padding-left: $tablePaddingMediumScreen + + &:last-child + padding-right: $tablePaddingMediumScreen + + input.worklist-search + padding: 10px + + .display-text + padding: 10px 5px + + i + width: auto + + tbody > tr > td + + &:first-child + padding-left: $tablePaddingMediumScreen + + &:last-child + padding-right: $tablePaddingMediumScreen + + .worklistPagination + .row + margin-left: 0 + margin-right: 0 diff --git a/packages/_components/src/components/studyList/StudyListLoadingText.js b/packages/_components/src/components/studyList/StudyListLoadingText.js new file mode 100644 index 000000000..deab2e331 --- /dev/null +++ b/packages/_components/src/components/studyList/StudyListLoadingText.js @@ -0,0 +1,12 @@ +import { Icon } from './../../elements/Icon'; +import React from 'react'; + +function StudyListLoadingText() { + return ( +
    + Loading... +
    + ); +} + +export { StudyListLoadingText }; diff --git a/packages/_components/src/components/studyList/StudyListToolbar.js b/packages/_components/src/components/studyList/StudyListToolbar.js new file mode 100644 index 000000000..c024ea937 --- /dev/null +++ b/packages/_components/src/components/studyList/StudyListToolbar.js @@ -0,0 +1,50 @@ +import './StudyListToolbar.styl'; + +import React, { PureComponent } from 'react'; + +import { Icon } from './../../elements/Icon'; +import PropTypes from 'prop-types'; + +class StudylistToolbar extends PureComponent { + static propTypes = { + studyListFunctionsEnabled: PropTypes.bool.isRequired, + onImport: PropTypes.func, + }; + + static defaultProps = { + studyListFunctionsEnabled: true, + }; + + clear(e) { + e.target.value = null; + } + + getImportTool() { + if (this.props.onImport && this.props.studyListFunctionsEnabled) { + return ( +
    + + +
    + ); + } + } + + render() { + return
    {this.getImportTool()}
    ; + } +} + +export { StudylistToolbar }; diff --git a/packages/_components/src/components/studyList/StudyListToolbar.styl b/packages/_components/src/components/studyList/StudyListToolbar.styl new file mode 100644 index 000000000..c810bdb31 --- /dev/null +++ b/packages/_components/src/components/studyList/StudyListToolbar.styl @@ -0,0 +1,26 @@ +.studyListToolbar + .addNewStudy + margin: 0 10px + + label + font-weight: 400 + cursor: pointer + display: inline-block; + max-width: 100%; + margin-bottom: 5px; + + input + width: 0.1px; + height: 0.1px + opacity: 0 + overflow: hidden + position: absolute + z-index: -1 + + color: var(--text-secondary-color) + + &:hover + color: var(--hover-color) + + &:active + color: var(--active-color) diff --git a/packages/_components/src/components/studyList/__docs__/onSearch.js b/packages/_components/src/components/studyList/__docs__/onSearch.js new file mode 100644 index 000000000..1101e76c9 --- /dev/null +++ b/packages/_components/src/components/studyList/__docs__/onSearch.js @@ -0,0 +1,72 @@ +import moment from 'moment'; + +export default function(searchData) { + this.setState({ searchData }); + + const filter = (key, searchData, study) => { + if (key === 'studyDateFrom' && searchData[key] && study['studyDate']) { + const studyDate = moment(study['studyDate'], 'YYYYMMDD'); + return studyDate.isBetween( + searchData['studyDateFrom'], + searchData['studyDateTo'], + 'days', + '[]' + ); + } else if (searchData[key] && !study[key].includes(searchData[key])) { + return false; + } else { + return true; + } + }; + + const { field, order } = searchData.sortData; + + // just a example of local filtering + let filteredStudies = this.defaultStudies + .filter(function(study) { + const all = [ + 'patientName', + 'patientId', + 'accessionNumber', + 'modalities', + 'studyDescription', + 'studyDateFrom', + ].every(key => { + return filter(key, searchData, study); + }); + + return all; + }) + .sort(function(a, b) { + if (order === 'desc') { + if (a[field] < b[field]) { + return -1; + } + if (a[field] > b[field]) { + return 1; + } + return 0; + } else { + if (a[field] > b[field]) { + return -1; + } + if (a[field] < b[field]) { + return 1; + } + return 0; + } + }); + + // User can notice the loading icon + return new Promise(resolve => { + setTimeout(() => { + const first = searchData.currentPage * searchData.rowsPerPage; + let last = + searchData.currentPage * searchData.rowsPerPage + + searchData.rowsPerPage; + last = last >= filteredStudies.length ? filteredStudies.length : last; + this.setState({ studies: filteredStudies.slice(first, last) }); + resolve(); + }, 500); + }); +} diff --git a/packages/_components/src/components/studyList/__docs__/studies.js b/packages/_components/src/components/studyList/__docs__/studies.js new file mode 100644 index 000000000..5538d634b --- /dev/null +++ b/packages/_components/src/components/studyList/__docs__/studies.js @@ -0,0 +1,87 @@ +import moment from 'moment'; + +const studies = [ + { + studyInstanceUid: '11111.111111.111111.11111', + patientName: 'John Doe', + patientId: '1', + accessionNumber: '1234567', + studyDate: '19930201', + modalities: 'MR', + studyDescription: 'BRAIN', + }, + { + studyInstanceUid: '2222.222222.22222.22222', + patientName: 'José Silva', + patientId: '2', + accessionNumber: '7654321', + studyDate: moment().format('YYYYMMDD'), + modalities: 'CT', + studyDescription: 'PET CT STANDARD', + }, + { + studyInstanceUid: '3333.333333.33333.33333', + patientName: 'Antônio Jefferson', + patientId: '3', + accessionNumber: '732311', + studyDate: moment() + .subtract(14, 'days') + .format('YYYYMMDD'), + modalities: 'US', + studyDescription: '0', + }, + { + studyInstanceUid: '444444.44444.44444.4444', + patientName: 'Antonio da Silva', + patientId: '4', + accessionNumber: '732311', + studyDate: moment() + .subtract(1, 'months') + .format('YYYYMMDD'), + modalities: 'US', + studyDescription: '0', + }, + { + studyInstanceUid: '55555.55555.55555.55555', + patientName: 'Bezerra Souza', + patientId: '5', + accessionNumber: '5134543', + studyDate: moment() + .subtract(6, 'days') + .format('YYYYMMDD'), + modalities: 'US', + studyDescription: '0', + }, + { + studyInstanceUid: '66666.66666.66666.6666', + patientName: 'Geraldo Roger', + patientId: '6', + accessionNumber: '5315135', + studyDate: moment() + .subtract(7, 'days') + .format('YYYYMMDD'), + modalities: 'US', + studyDescription: 'US', + }, + { + studyInstanceUid: '77777.77777.77777.77777', + patientName: '', + patientId: '7', + accessionNumber: '5315136', + studyDate: moment() + .subtract(5, 'days') + .format('YYYYMMDD'), + modalities: 'US', + studyDescription: 'US', + }, +].sort(function(a, b) { + if (a.patientName < b.patientName) { + return -1; + } + if (a.patientName > b.patientName) { + return 1; + } + return 0; +}); + +export default studies; diff --git a/packages/_components/src/components/studyList/__docs__/studyList.mdx b/packages/_components/src/components/studyList/__docs__/studyList.mdx new file mode 100644 index 000000000..86c33f686 --- /dev/null +++ b/packages/_components/src/components/studyList/__docs__/studyList.mdx @@ -0,0 +1,53 @@ +--- +name: Study List +menu: Components +route: /components/study-list +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { StudyList } from './../index.js' +// Data +import defaultStudies from './studies.js' +import onSearch from './onSearch.js' +import moment from 'moment' + +# Study List + +## Basic usage + + + { + const studyDate = moment(study['studyDate'], 'YYYYMMDD'); + const startDate = moment().subtract(5, 'days'); + const endDate = moment(); + + return studyDate.isBetween(startDate, endDate, 'days', '[]'); + }).slice(0, 5) + }}> + {({ state, setState }) => ( +
    +
    + alert('Import study mock ' + e)} + onSelectItem={(studyInstanceUid) => { alert(studyInstanceUid + ' has selected! Now you can open your study.'); }} + rowsPerPage={5} + defaultSort={{ field: 'patientName', order: 'desc', }} + studyListDateFilterNumDays={7} + onSearch={onSearch.bind({ setState, defaultStudies })} + /> +
    +
    + )} +
    + +
    + +## API + + diff --git a/packages/_components/src/components/studyList/index.js b/packages/_components/src/components/studyList/index.js new file mode 100644 index 000000000..45f244092 --- /dev/null +++ b/packages/_components/src/components/studyList/index.js @@ -0,0 +1 @@ +export { StudyList } from './StudyList.js'; diff --git a/packages/_components/src/components/tableList/TableList.js b/packages/_components/src/components/tableList/TableList.js new file mode 100644 index 000000000..dab927b0a --- /dev/null +++ b/packages/_components/src/components/tableList/TableList.js @@ -0,0 +1,37 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import './TableList.styl'; + +export class TableList extends Component { + static propTypes = { + customHeader: PropTypes.node, + defaultItems: PropTypes.object, + children: PropTypes.node.isRequired, + headerTitle: PropTypes.string, + }; + + render() { + return ( +
    +
    + {this.getHeader()} +
    +
    {this.props.children}
    +
    + ); + } + + getHeader = () => { + if (this.props.customHeader) { + return this.props.customHeader; + } else { + return ( + +
    {this.props.headerTitle}
    +
    {this.props.children.length}
    +
    + ); + } + }; +} diff --git a/packages/_components/src/components/tableList/TableList.styl b/packages/_components/src/components/tableList/TableList.styl new file mode 100644 index 000000000..f62c7444d --- /dev/null +++ b/packages/_components/src/components/tableList/TableList.styl @@ -0,0 +1,36 @@ + +$headerRowHeight = 63px + +.tableList + background-color: var(--primary-background-color) + height: 100%; + width: 100%; + + .tableListHeader + background-color: var(--ui-gray-darker) + color: var(--text-secondary-color) + display: flex + height: $headerRowHeight + line-height: $headerRowHeight + margin-top: 2px + overflow: hidden + width: 100% + + .tableListHeaderTitle + color: var(--text-secondary-color) + font-size: 22px + font-weight: 300 + line-height: $headerRowHeight + padding: 0 10px + text-align: left + flex: 1; + + .numberOfItems + color: var(--ui-sky-blue) + float: right + font-weight: 300 + font-size: 40px + max-width: 54px + height: $headerRowHeight + line-height: 66px + flex: 1; diff --git a/packages/_components/src/components/tableList/TableListItem.js b/packages/_components/src/components/tableList/TableListItem.js new file mode 100644 index 000000000..719ed6e82 --- /dev/null +++ b/packages/_components/src/components/tableList/TableListItem.js @@ -0,0 +1,42 @@ +import './TableListItem.styl'; + +import { Component } from 'react'; +import { Icon } from './../../elements/Icon'; +import PropTypes from 'prop-types'; +import React from 'react'; + +export class TableListItem extends Component { + static propTypes = { + children: PropTypes.node, + itemClass: PropTypes.string, + itemIndex: PropTypes.number, + itemKey: PropTypes.oneOfType(['number', 'string']), + onItemClick: PropTypes.func.isRequired, + }; + + render() { + return ( +
    +
    + {this.props.itemIndex} + + + +
    +
    {this.props.children}
    +
    + ); + } + + onItemClick = event => { + if (this.props.onItemClick) { + event.preventDefault(); + event.stopPropagation(); + + this.props.onItemClick(event, this.props.itemKey); + } + }; +} diff --git a/packages/_components/src/components/tableList/TableListItem.styl b/packages/_components/src/components/tableList/TableListItem.styl new file mode 100644 index 000000000..a471d134e --- /dev/null +++ b/packages/_components/src/components/tableList/TableListItem.styl @@ -0,0 +1,46 @@ +.tableListItem + display: flex + margin-left: -6px + margin-top: 2px + padding-left: 6px + opacity: 0.7 + cursor: pointer; + width: calc(100% + 6px) + + &:hover + opacity 1 + + &.selected + opacity 1 + .itemIndex + color: $activeColor + + &.hasWarning + .warning + display: block + + .itemIndex + background-color: var(--ui-gray) + color: var(--text-secondary-color) + cursor: pointer + flex: 1 + max-width: 25px + transition(all 0.3s ease) + font-size: 14px + font-weight: 400 + margin-right: 5px + padding: 10px + text-align: center + -webkit-box-sizing: initial; + -moz-box-sizing: initial; + box-sizing: initial; + + .itemContent + flex: 1 + color: var(--text-secondary-color) + font-weight: 400 + margin-left: 9px; + margin-top: 9px; + + .warning-icon + display: none diff --git a/packages/_components/src/components/tableList/__docs__/tableList.mdx b/packages/_components/src/components/tableList/__docs__/tableList.mdx new file mode 100644 index 000000000..bea1c7db5 --- /dev/null +++ b/packages/_components/src/components/tableList/__docs__/tableList.mdx @@ -0,0 +1,48 @@ +--- +name: Table List +menu: Components +route: /components/table-list +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { TableList, TableListItem } from './../index.js' + +# Table List + +## Basic usage + + + + {({ state, setState }) => ( + + {state.listItems.map( (item, index) => { + return ( + alert('item clicked')} + > + + + )} + )} + + )} + + + + + +## API + + diff --git a/packages/_components/src/components/tableList/index.js b/packages/_components/src/components/tableList/index.js new file mode 100644 index 000000000..5a19d8d71 --- /dev/null +++ b/packages/_components/src/components/tableList/index.js @@ -0,0 +1,2 @@ +export { TableList } from './TableList'; +export { TableListItem } from './TableListItem'; diff --git a/packages/_components/src/components/toolbarSection/ToolbarSection.js b/packages/_components/src/components/toolbarSection/ToolbarSection.js new file mode 100644 index 000000000..e1fdbf213 --- /dev/null +++ b/packages/_components/src/components/toolbarSection/ToolbarSection.js @@ -0,0 +1,65 @@ +import './ToolbarSection.styl'; + +import React, { PureComponent } from 'react'; + +import ExpandableToolMenu from '../../viewer/ExpandableToolMenu'; +import PropTypes from 'prop-types'; +import ToolbarButton from '../../viewer/ToolbarButton'; +import classnames from 'classnames'; + +class ToolbarSection extends PureComponent { + static defaultProps = { + className: '', + }; + + static propTypes = { + buttons: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + label: PropTypes.string.isRequired, + icon: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({ + name: PropTypes.string.isRequired, + }), + ]), + /** Optional: Expandable Tool Menu */ + buttons: PropTypes.arrayOf(PropTypes.shape({})), + }) + ).isRequired, + /** Array of string button ids that should show as active */ + activeButtons: PropTypes.arrayOf(PropTypes.string).isRequired, + /** Class for toolbar section container */ + className: PropTypes.string, + }; + + render() { + const items = this.props.buttons.map((button, index) => { + if (button.buttons && Array.isArray(button.buttons)) { + return ( + + ); + } else { + return ( + + ); + } + }); + + return ( +
    + {items} +
    + ); + } +} + +export { ToolbarSection }; diff --git a/packages/_components/src/components/toolbarSection/ToolbarSection.styl b/packages/_components/src/components/toolbarSection/ToolbarSection.styl new file mode 100644 index 000000000..1e008acca --- /dev/null +++ b/packages/_components/src/components/toolbarSection/ToolbarSection.styl @@ -0,0 +1,4 @@ +.ToolbarSection + float: left; + background-color: var(--primary-background-color); + border-radius: 8px; diff --git a/packages/_components/src/components/toolbarSection/__docs__/exampleButtons.js b/packages/_components/src/components/toolbarSection/__docs__/exampleButtons.js new file mode 100644 index 000000000..74e08b6a2 --- /dev/null +++ b/packages/_components/src/components/toolbarSection/__docs__/exampleButtons.js @@ -0,0 +1,41 @@ +export default [ + { + id: 'Pan', + label: 'Pan', + icon: 'arrows', + }, + { + label: 'Zoom', + icon: 'search', + }, + { + label: 'Bidirectional', + icon: 'measure-target', + }, + { + label: 'Stack Scroll', + icon: 'bars', + }, + { + label: 'Reset', + icon: 'reset', + }, + { + label: 'Manual', + icon: 'level', + }, + { + label: 'More', + icon: 'ellipse-circle', + buttons: [ + { + label: 'Reset 2', + icon: 'reset', + }, + { + label: 'Manual 2', + icon: 'adjust', + }, + ], + }, +]; diff --git a/packages/_components/src/components/toolbarSection/__docs__/toolbarSection.docs.mdx b/packages/_components/src/components/toolbarSection/__docs__/toolbarSection.docs.mdx new file mode 100644 index 000000000..a246a2dd5 --- /dev/null +++ b/packages/_components/src/components/toolbarSection/__docs__/toolbarSection.docs.mdx @@ -0,0 +1,39 @@ +--- +name: Toolbar Section +menu: Components +route: /components/toolbar-section +--- + +import { Playground, Props } from 'docz'; +import { State } from 'react-powerplug'; +import { ToolbarSection } from './../index.js'; +import exampleButtons from './exampleButtons.js'; +import NameSpace from '../../../__docs__/NameSpace'; + +# ToolbarSection + +A basic row of buttons for a toolbar. Active command is `WwwcTool` + +## Basic usage + + + { + alert('pressed'); + }, + }} + > + {({ state, setState }) => } + + + +## API + + + +## Translation Namespace + + diff --git a/packages/_components/src/components/toolbarSection/index.js b/packages/_components/src/components/toolbarSection/index.js new file mode 100644 index 000000000..5dad83951 --- /dev/null +++ b/packages/_components/src/components/toolbarSection/index.js @@ -0,0 +1 @@ +export { ToolbarSection } from './ToolbarSection.js'; diff --git a/packages/_components/src/components/tooltip/Tooltip.js b/packages/_components/src/components/tooltip/Tooltip.js new file mode 100644 index 000000000..237d2cb22 --- /dev/null +++ b/packages/_components/src/components/tooltip/Tooltip.js @@ -0,0 +1,66 @@ +import './Tooltip.styl'; + +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; + +const propTypes = { + /** Sets the direction the Tooltip is positioned towards. */ + placement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']), + + /** The "top" position value for the Tooltip. */ + positionTop: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** The "left" position value for the Tooltip. */ + positionLeft: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + + /** The "top" position value for the Tooltip arrow. */ + arrowOffsetTop: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** The "left" position value for the Tooltip arrow. */ + arrowOffsetLeft: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), +}; + +const defaultProps = { + placement: 'right', +}; + +class Tooltip extends React.Component { + render() { + const { + placement, + positionTop, + positionLeft, + arrowOffsetTop, + arrowOffsetLeft, + className, + style, + children, + } = this.props; + + const outerStyle = { + top: positionTop, + left: positionLeft, + ...style, + }; + + const arrowStyle = { + top: arrowOffsetTop, + left: arrowOffsetLeft, + }; + + return ( +
    +
    +
    {children}
    +
    + ); + } +} + +Tooltip.propTypes = propTypes; +Tooltip.defaultProps = defaultProps; + +export { Tooltip }; diff --git a/packages/_components/src/components/tooltip/Tooltip.styl b/packages/_components/src/components/tooltip/Tooltip.styl new file mode 100644 index 000000000..ab0822ab9 --- /dev/null +++ b/packages/_components/src/components/tooltip/Tooltip.styl @@ -0,0 +1,92 @@ +@import './../../design/styles/common/global.styl' + +.tooltip + position: absolute; + z-index: 1070; + display: block; + font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 1.42857143; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + word-wrap: normal; + white-space: normal; + opacity: 0; + line-break: auto; + + &.in + opacity: .9; + + &.top + margin-top: -3px; + padding: 5px 0; + + &.right + margin-left: 3px; + padding: 0 5px; + + &.bottom + margin-top: 3px; + padding: 5px 0; + + &.left + margin-left: -3px; + padding: 0 5px; + + &.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-width: 5px 5px 0; + border-top-color: #000; + } + + &.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-width: 5px 5px 5px 0; + border-right-color: #000; + } + &.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-width: 5px 0 5px 5px; + border-left-color: #000; + } + &.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; + } + +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #fff; + text-align: center; + background-color: #000; + border-radius: 4px; +} + +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.tooltip.in + opacity: .9; diff --git a/packages/_components/src/components/tooltip/__docs__/tooltip.noshow-mdx b/packages/_components/src/components/tooltip/__docs__/tooltip.noshow-mdx new file mode 100644 index 000000000..e5adad984 --- /dev/null +++ b/packages/_components/src/components/tooltip/__docs__/tooltip.noshow-mdx @@ -0,0 +1,20 @@ +--- +name: Tooltip +menu: Components +route: /components/tooltip +--- + +import { Playground, Props } from 'docz' +import { Tooltip } from './../index.js' + +# Tooltip + +## Basic usage + + + + + +## API + + diff --git a/packages/_components/src/components/tooltip/__tests__/.githold b/packages/_components/src/components/tooltip/__tests__/.githold new file mode 100644 index 000000000..e69de29bb diff --git a/packages/_components/src/components/tooltip/index.js b/packages/_components/src/components/tooltip/index.js new file mode 100644 index 000000000..40ede0d67 --- /dev/null +++ b/packages/_components/src/components/tooltip/index.js @@ -0,0 +1 @@ +export { Tooltip } from './Tooltip.js'; diff --git a/packages/_components/src/components/userPreferencesModal/AboutModal.js b/packages/_components/src/components/userPreferencesModal/AboutModal.js new file mode 100644 index 000000000..3d1b5380a --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/AboutModal.js @@ -0,0 +1,105 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import Modal from 'react-bootstrap-modal'; +import './AboutModal.styl'; + +import 'react-bootstrap-modal/lib/css/rbm-patch.css'; + +// TODO: Is this the only component importing these? +import './../../design/styles/common/modal.styl'; + +export class AboutModal extends Component { + constructor(props) { + super(props); + } + // TODO: Make this component more generic to allow things other than W/L and hotkeys... + static propTypes = { + isOpen: PropTypes.bool.isRequired, + onCancel: PropTypes.func, + }; + + static itemsPreset = [ + { + name: 'Repository URL', + value: 'https://github.com/OHIF/Viewers/', + }, + { + name: 'Latest Master Commits', + value: 'https://github.com/OHIF/Viewers/commits/master', + }, + ]; + + static defaultProps = { + isOpen: false, + }; + + renderTableRow(item) { + return ( + + {item.name} + + + {item.value} + + + + ); + } + + render() { + return ( + + + About + + + +
    +

    Version Information

    + + + + + + + + + {AboutModal.itemsPreset.map(item => this.renderTableRow(item))} + +
    NameValue
    +
    +
    +
    + ); + } +} diff --git a/packages/_components/src/components/userPreferencesModal/AboutModal.styl b/packages/_components/src/components/userPreferencesModal/AboutModal.styl new file mode 100644 index 000000000..b808e2cca --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/AboutModal.styl @@ -0,0 +1,20 @@ +@import './../../design/styles/common/button.styl' +@import './../../design/styles/common/table.styl' + +.AboutModal + .btn + border-color: #ccc; + + .table + thead:first-child + tr:first-child + th, td + border-top: 0; + + thead, tbody, tfoot + tr + th, td + padding: 8px; + line-height: 1.42857143; + vertical-align: top; + border-top: 1px solid #ddd; diff --git a/packages/_components/src/components/userPreferencesModal/GeneralPreferences.js b/packages/_components/src/components/userPreferencesModal/GeneralPreferences.js new file mode 100644 index 000000000..51d68344e --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/GeneralPreferences.js @@ -0,0 +1,17 @@ +import React, { Component } from 'react'; +import LanguageSwitcher from '../languageSwitcher'; + +export class GeneralPreferences extends Component { + render() { + return ( +
    +
    + + +
    +
    + ); + } +} diff --git a/packages/_components/src/components/userPreferencesModal/HotKeysPreferences.js b/packages/_components/src/components/userPreferencesModal/HotKeysPreferences.js new file mode 100644 index 000000000..ea97a144b --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/HotKeysPreferences.js @@ -0,0 +1,277 @@ +import './HotKeysPreferences.styl'; +import React, { Component } from 'react'; +import { + allowedKeys, + disallowedCombinations, + specialKeys, +} from './hotKeysConfig.js'; + +import PropTypes from 'prop-types'; + +export class HotKeysPreferences extends Component { + static propTypes = { + hotKeysData: PropTypes.objectOf( + PropTypes.shape({ + keys: PropTypes.arrayOf(PropTypes.string).isRequired, + label: PropTypes.string.isRequired, + }) + ).isRequired, + onChange: PropTypes.func, + }; + + constructor(props) { + super(props); + + const hotkeyCommands = Object.keys(this.props.hotKeysData); + const localHotKeys = hotkeyCommands.map(commandName => { + const definition = this.props.hotKeysData[commandName]; + + return { + commandName, + keys: definition.keys, + label: definition.label, + }; + }); + + this.state = { + hotKeys: localHotKeys, + errorMessages: {}, + }; + + this.onInputKeyDown = this.onInputKeyDown.bind(this); + } + + /** + * Normalizes the keys used in a KeyPress event and returns an array of the + * keys pressed + * + * @param {KeyDownEvent} keyDownEvent + * @returns {string[]} + */ + getKeysPressedArray(keyDownEvent) { + const keysPressedArray = []; + const { ctrlKey, altKey, shiftKey } = keyDownEvent; + + if (ctrlKey && !altKey) { + keysPressedArray.push('CTRL'); + } + + if (shiftKey && !altKey) { + keysPressedArray.push('SHIFT'); + } + + if (altKey && !ctrlKey) { + keysPressedArray.push('ALT'); + } + + return keysPressedArray; + } + + getConflictingCommand(currentToolKey, hotKeyCommand) { + return Object.keys(this.state.hotKeys).find(tool => { + const value = this.state.hotKeys[tool].command; + return value && value === hotKeyCommand && tool !== currentToolKey; + }); + } + + /** + * + * @param {String} commandName + * @param {KeyDownEvent} keyDownEvent + * @param {Boolean} [displayPressedKey=false] + */ + updateInputText(commandName, keyDownEvent, displayPressedKey = false) { + const pressedKeys = this.getKeysPressedArray(keyDownEvent); + + if (displayPressedKey) { + const specialKeyName = specialKeys[keyDownEvent.which]; + const keyName = + specialKeyName || + keyDownEvent.key || + String.fromCharCode(keyDownEvent.keyCode); + pressedKeys.push(keyName.toUpperCase()); + } + + this.updateHotKeysState(commandName, pressedKeys.join('+')); + } + + updateHotKeysState(commandName, keys) { + const hotKeys = this.state.hotKeys; + const hotKeyIndex = this.state.hotKeys.findIndex( + x => x.commandName === commandName + ); + hotKeys[hotKeyIndex].keys[0] = keys; + this.setState({ hotKeys }); + } + + updateErrorsState(toolKey, errorMessage) { + const errorMessages = this.state.errorMessages; + errorMessages[toolKey] = errorMessage; + this.setState({ errorMessages }); + } + + onInputKeyDown(event, commandName) { + // Prevent ESC key from propagating and closing the modal + if (event.key === 'Escape') { + event.stopPropagation(); + } + + if (allowedKeys.includes(event.keyCode)) { + this.updateInputText(commandName, event, true); + } else { + this.updateInputText(commandName, event, false); + } + + event.preventDefault(); + } + + onChange(event, commandName) { + if (event.ctrlKey || event.altKey || event.shiftKey) { + return; + } + + const hotKeyIndex = this.state.hotKeys.findIndex( + x => x.commandName === commandName + ); + const hotKey = this.state.hotKeys[hotKeyIndex]; + const keys = hotKey.keys[0]; + const pressedKeys = keys.split('+'); + const lastPressedKey = pressedKeys[pressedKeys.length - 1].toUpperCase(); + + // clear the prior errors + this.setState({ errorMessages: {} }, () => { + // Check if it has a valid modifier + const isModifier = ['CTRL', 'ALT', 'SHIFT'].includes(lastPressedKey); + if (isModifier) { + this.updateHotKeysState(commandName, ''); + this.updateErrorsState( + commandName, + "It's not possible to define only modifier keys (CTRL, ALT and SHIFT) as a shortcut" + ); + return; + } + + /* + * Check if it has some conflict + */ + const conflictedCommandKey = this.getConflictingCommand( + commandName, + keys + ); + if (conflictedCommandKey) { + const conflictedCommand = this.state.hotKeys[conflictedCommandKey]; + + this.updateErrorsState( + commandName, + `"${conflictedCommand.label}" is already using the "${ + conflictedCommand.command + }" shortcut.` + ); + this.updateErrorsState(conflictedCommandKey, ''); + this.updateHotKeysState(commandName, ''); + return; + } + + /* + * Check if is a valid combination + */ + const modifierCommand = pressedKeys + .slice(0, pressedKeys.length - 1) + .join('+') + .toUpperCase(); + + const disallowedCombination = disallowedCombinations[modifierCommand]; + const hasDisallowedCombinations = disallowedCombination + ? disallowedCombination.includes(lastPressedKey) + : false; + + if (hasDisallowedCombinations) { + this.updateHotKeysState(commandName, ''); + this.updateErrorsState( + commandName, + "It's not possible to define only modifier keys (CTRL, ALT and SHIFT) as a shortcut" + ); + return; + } + }); + } + + renderRow({ commandName, label, keys }) { + return ( + + {label} + + + + + ); + } + + render() { + const halfWayThough = Math.floor(this.state.hotKeys.length / 2); + const firstHalfHotkeys = this.state.hotKeys.slice(0, halfWayThough); + const secondHalfHotkeys = this.state.hotKeys.slice( + halfWayThough, + this.state.hotKeys.length + ); + + return ( +
    + {/* */} +
    + + + + + + + + + {firstHalfHotkeys.map(hotkeyDefinition => + this.renderRow(hotkeyDefinition) + )} + +
    FunctionShortcut
    +
    + {/* */} +
    + + + + + + + + + {secondHalfHotkeys.map(hotkeyDefinition => + this.renderRow(hotkeyDefinition) + )} + +
    FunctionShortcut
    +
    +
    + ); + } +} diff --git a/packages/_components/src/components/userPreferencesModal/HotKeysPreferences.styl b/packages/_components/src/components/userPreferencesModal/HotKeysPreferences.styl new file mode 100644 index 000000000..98db2e52e --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/HotKeysPreferences.styl @@ -0,0 +1,14 @@ +.HotKeysPreferences + display: flex; + margin-right: -15px; + margin-left: -15px; + + .column + width: 50%; + padding-right: 15px; + padding-left: 15px; + + .column-full + width: 100%; + padding-right: 15px; + padding-left: 15px; diff --git a/packages/_components/src/components/userPreferencesModal/UserPreferences.js b/packages/_components/src/components/userPreferencesModal/UserPreferences.js new file mode 100644 index 000000000..584137e4c --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/UserPreferences.js @@ -0,0 +1,118 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import { HotKeysPreferences } from './HotKeysPreferences'; +import { WindowLevelPreferences } from './WindowLevelPreferences'; +import { GeneralPreferences } from './GeneralPreferences'; +import './UserPreferences.styl'; + +export class UserPreferences extends Component { + static defaultProps = { + hotKeysData: {}, + windowLevelData: {}, + generalData: {}, + }; + + // TODO: Make this more generic. Tabs should not be restricted to these entries + static propTypes = { + hotKeysData: PropTypes.object.isRequired, + windowLevelData: PropTypes.object.isRequired, + generalData: PropTypes.object.isRequired, + }; + + state = { + tabIndex: 0, + }; + + tabClick(tabIndex) { + this.setState({ tabIndex }); + } + + renderHotkeysTab() { + return ( +
    +
    + +
    +
    + ); + } + + renderWindowLevelTab() { + if (this.props.windowLevelData) { + return ( +
    +
    + +
    +
    + ); + } + } + + renderGeneralTab() { + return ( +
    +
    + +
    +
    + ); + } + + renderTabs(tabIndex) { + switch (tabIndex) { + case 0: + return this.renderHotkeysTab(); + case 1: + return this.renderWindowLevelTab(); + case 2: + return this.renderGeneralTab(); + + default: + break; + } + } + + getTabClass(tabIndex) { + return tabIndex === this.state.tabIndex ? 'nav-link active' : 'nav-link'; + } + + render() { + return ( +
    +
    +
      +
    • { + this.tabClick(0); + }} + className={this.getTabClass(0)} + > + +
    • +
    • { + this.tabClick(1); + }} + className={this.getTabClass(1)} + > + +
    • +
    • { + this.tabClick(2); + }} + className={this.getTabClass(2)} + > + +
    • +
    +
    + {this.renderTabs(this.state.tabIndex)} +
    + ); + } +} diff --git a/packages/_components/src/components/userPreferencesModal/UserPreferences.styl b/packages/_components/src/components/userPreferencesModal/UserPreferences.styl new file mode 100644 index 000000000..f62578e88 --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/UserPreferences.styl @@ -0,0 +1,25 @@ +@import './../../design/styles/common/form.styl' +@import './../../design/styles/common/navbar.styl' +@import './../../design/styles/common/state.styl' +@import './../../design/styles/common/global.styl' + +.modal-body + overflow: hidden + +.errorMessage + color: var(--state-error-text) + font-size: 10px + text-transform: uppercase; + +.form-content + border-bottom: 3px solid var(--primary-background-color) + margin-bottom: 20px + margin-left: -22px + margin-right: -22px + max-height: 70vh + overflow-y: auto + padding: 22px + min-height: 500px + +.popover + width: 300px diff --git a/packages/_components/src/components/userPreferencesModal/UserPreferencesModal.js b/packages/_components/src/components/userPreferencesModal/UserPreferencesModal.js new file mode 100644 index 000000000..0b54504dd --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/UserPreferencesModal.js @@ -0,0 +1,107 @@ +import './UserPreferencesModal.styl'; + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import Modal from 'react-bootstrap-modal'; +import { withTranslation } from '../../utils/LanguageProvider'; + +import 'react-bootstrap-modal/lib/css/rbm-patch.css'; +import cloneDeep from 'lodash.clonedeep'; +import isEqual from 'lodash.isequal'; +import { UserPreferences } from './UserPreferences'; + +// TODO: Is this the only component importing these? +import './../../design/styles/common/modal.styl'; + +class UserPreferencesModal extends Component { + // TODO: Make this component more generic to allow things other than W/L and hotkeys... + static propTypes = { + isOpen: PropTypes.bool.isRequired, + onCancel: PropTypes.func, + onSave: PropTypes.func, + onResetToDefaults: PropTypes.func, + windowLevelData: PropTypes.object, + hotKeysData: PropTypes.object, + t: PropTypes.func, + }; + + constructor(props) { + super(props); + + this.state = { + windowLevelData: cloneDeep(props.windowLevelData), + hotKeysData: cloneDeep(props.hotKeysData), + }; + } + + static defaultProps = { + isOpen: false, + }; + + save = () => { + this.props.onSave({ + windowLevelData: this.state.windowLevelData, + hotKeysData: this.state.hotKeysData, + }); + }; + + componentDidUpdate(prev, next) { + const newStateData = {}; + + if (!isEqual(prev.windowLevelData, next.windowLevelData)) { + newStateData.windowLevelData = prev.windowLevelData; + } + + if (!isEqual(prev.hotKeysData, next.hotKeysData)) { + newStateData.hotKeysData = prev.hotKeysData; + } + + if (newStateData.hotKeysData || newStateData.windowLevelData) { + this.setState(newStateData); + } + } + + render() { + return ( + + + {this.props.t('User Preferences')} + + + + + + + + {this.props.t('Cancel')} + + + + + ); + } +} + +const connectedComponent = withTranslation('UserPreferencesModal')( + UserPreferencesModal +); +export { connectedComponent as UserPreferencesModal }; +export default connectedComponent; diff --git a/packages/_components/src/components/userPreferencesModal/UserPreferencesModal.styl b/packages/_components/src/components/userPreferencesModal/UserPreferencesModal.styl new file mode 100644 index 000000000..0dea62f53 --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/UserPreferencesModal.styl @@ -0,0 +1,17 @@ +@import './../../design/styles/common/navbar.styl' +@import './../../design/styles/common/global.styl' +@import './../../design/styles/common/modal.styl' +@import './../../design/styles/common/button.styl' + +.close + float: right; + font-size: 21px; + font-weight: 700; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + opacity: .2; + +.ModalHeader + ol, ul + margin-top: 0; diff --git a/packages/_components/src/components/userPreferencesModal/WindowLevelPreferences.js b/packages/_components/src/components/userPreferencesModal/WindowLevelPreferences.js new file mode 100644 index 000000000..45e931b3e --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/WindowLevelPreferences.js @@ -0,0 +1,94 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import './WindowLevelPreferences.styl'; + +export class WindowLevelPreferences extends Component { + constructor(props) { + super(props); + + this.state = { + data: this.props.windowLevelData, + }; + } + + static propTypes = { + windowLevelData: PropTypes.object.isRequired, + onChange: PropTypes.func, + }; + + onChange(event, key, field) { + const data = this.state.data; + const entry = data[key]; + entry[field] = event.target.value; + this.setState({ data }); + + if (this.props.onChange) { + this.props.onChange(data); + } + } + + getWLPreferencesRows(key) { + const entry = this.state.data[key]; + return ( + + {key} + + + + + + + + + + + ); + } + + render() { + return ( + + + + + + + + + + + {Object.keys(this.state.data).map(key => { + return this.getWLPreferencesRows(key); + })} + +
    PresetDescriptionWindowLevel
    + ); + } +} diff --git a/packages/_components/src/components/userPreferencesModal/WindowLevelPreferences.styl b/packages/_components/src/components/userPreferencesModal/WindowLevelPreferences.styl new file mode 100644 index 000000000..78864357b --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/WindowLevelPreferences.styl @@ -0,0 +1,4 @@ +@import './UserPreferences.styl' + +.presetIndex + padding: 0px 10px 0px 10px \ No newline at end of file diff --git a/packages/_components/src/components/userPreferencesModal/__docs__/about.mdx b/packages/_components/src/components/userPreferencesModal/__docs__/about.mdx new file mode 100644 index 000000000..819648ff0 --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/__docs__/about.mdx @@ -0,0 +1,42 @@ +--- +name: About Modal +menu: Components +route: /components/about-modal +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { AboutModal } from './../index.js' + +# About Modal + +## Basic usage + + + + + {({ state, setState }) => ( + + + setState({ isOpen: false })} + /> + + )} + + + + + +## API + + diff --git a/packages/_components/src/components/userPreferencesModal/__docs__/generalDefaults.js b/packages/_components/src/components/userPreferencesModal/__docs__/generalDefaults.js new file mode 100644 index 000000000..40f7e3387 --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/__docs__/generalDefaults.js @@ -0,0 +1,14 @@ +export default { + currentLanguage: 'en', + languages: [ + { + value: 'en', + label: 'English', + }, + { + value: 'es', + label: 'Spanish', + }, + ], + onChange: language => {}, +}; diff --git a/packages/_components/src/components/userPreferencesModal/__docs__/hotkeyDefaults.js b/packages/_components/src/components/userPreferencesModal/__docs__/hotkeyDefaults.js new file mode 100644 index 000000000..1c8399977 --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/__docs__/hotkeyDefaults.js @@ -0,0 +1,80 @@ +export default { + defaultTool: { label: 'Default Tool', keys: ['ESC'], column: 0 }, + zoom: { label: 'Zoom', keys: ['Z'], column: 0 }, + wwwc: { label: 'W/L', keys: ['W'], column: 0 }, + pan: { label: 'Pan', keys: ['P'], column: 0 }, + angle: { label: 'Angle measurement', keys: ['A'], column: 0 }, + stackScroll: { label: 'Scroll stack', keys: ['S'], column: 0 }, + magnify: { label: 'Magnify', keys: ['M'], column: 0 }, + length: { label: 'Length measurement', keys: [''], column: 0 }, + annotate: { label: 'Annotate', keys: [''], column: 0 }, + dragProbe: { label: 'Pixel probe', keys: [''], column: 0 }, + ellipticalRoi: { label: 'Elliptical ROI', keys: [''], column: 0 }, + rectangleRoi: { label: 'Rectangle ROI', keys: [''], column: 0 }, + + // Viewport hotkeys + flipH: { label: 'Flip Horizontally', keys: ['H'], column: 0 }, + flipV: { label: 'Flip Vertically', keys: ['V'], column: 0 }, + rotateR: { label: 'Rotate Right', keys: ['R'], column: 0 }, + rotateL: { label: 'Rotate Left', keys: ['L'], column: 0 }, + invert: { label: 'Invert', keys: ['I'], column: 0 }, + zoomIn: { label: 'Zoom In', keys: [''], column: 0 }, + zoomOut: { label: 'Zoom Out', keys: [''], column: 0 }, + zoomToFit: { label: 'Zoom to Fit', keys: [''], column: 0 }, + resetViewport: { label: 'Reset', keys: [''], column: 0 }, + clearTools: { label: 'Clear Tools', keys: [''], column: 0 }, + + // 2nd column + + // Viewport navigation hotkeys + scrollDown: { label: 'Scroll Down', keys: ['DOWN'], column: 1 }, + scrollUp: { label: 'Scroll Up', keys: ['UP'], column: 1 }, + scrollLastImage: { label: 'Scroll to Last Image', keys: ['END'], column: 1 }, + scrollFirstImage: { + label: 'Scroll to First Image', + keys: ['HOME'], + column: 1, + }, + previousDisplaySet: { + label: 'Previous Series', + keys: ['PAGEUP'], + column: 1, + }, + nextDisplaySet: { label: 'Next Series', keys: ['PAGEDOWN'], column: 1 }, + nextPanel: { label: 'Next Image Viewport', keys: ['RIGHT'], column: 1 }, + previousPanel: { + label: 'Previous Image Viewport', + keys: ['LEFT'], + column: 1, + }, + + // Miscellaneous hotkeys + toggleOverlayTags: { + label: 'Toggle Image Info Overlay', + keys: ['O'], + column: 1, + }, + toggleCinePlay: { label: 'Play/Pause Cine', keys: ['SPACE'], column: 1 }, + toggleCineDialog: { + label: 'Show/Hide Cine Controls', + keys: [''], + column: 1, + }, + toggleDownloadDialog: { + label: 'Show/Hide Download Dialog', + keys: [''], + column: 1, + }, + + // Preset hotkeys + WLPreset0: { label: 'W/L Preset 0 (Soft Tissue)', keys: ['1'], column: 1 }, + WLPreset1: { label: 'W/L Preset 1 (Lung)', keys: ['2'], column: 1 }, + WLPreset2: { label: 'W/L Preset 2 (Liver)', keys: ['3'], column: 1 }, + WLPreset3: { label: 'W/L Preset 3 (Bone)', keys: ['4'], column: 1 }, + WLPreset4: { label: 'W/L Preset 4 (Brain)', keys: ['5'], column: 1 }, + WLPreset5: { label: 'W/L Preset 5', keys: ['6'], column: 1 }, + WLPreset6: { label: 'W/L Preset 6', keys: ['7'], column: 1 }, + WLPreset7: { label: 'W/L Preset 7', keys: ['8'], column: 1 }, + WLPreset8: { label: 'W/L Preset 8', keys: ['9'], column: 1 }, + WLPreset9: { label: 'W/L Preset 0', keys: ['0'], column: 1 }, +}; diff --git a/packages/_components/src/components/userPreferencesModal/__docs__/userPreferences.mdx b/packages/_components/src/components/userPreferencesModal/__docs__/userPreferences.mdx new file mode 100644 index 000000000..60b5ff69f --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/__docs__/userPreferences.mdx @@ -0,0 +1,54 @@ +--- +name: User Preferences Modal +menu: Components +route: /components/user-preferences-modal +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { UserPreferencesModal } from './../index.js' +import NameSpace from '../../../__docs__/NameSpace' +// +import windowLevelDefaults from './windowLevelDefaults.js' +import hotkeyDefaults from './hotkeyDefaults.js' + +# User Preferences Modal + +## Basic usage + + + + + {({ state, setState }) => ( + + + setState({ isOpen: false })} + onSave={() => alert('on save')} + onResetToDefaults={() => alert('on reset')} + /> + + )} + + + + + +## API + + + +## Translation Namespace + + diff --git a/packages/_components/src/components/userPreferencesModal/__docs__/windowLevelDefaults.js b/packages/_components/src/components/userPreferencesModal/__docs__/windowLevelDefaults.js new file mode 100644 index 000000000..ba1990c84 --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/__docs__/windowLevelDefaults.js @@ -0,0 +1,13 @@ +export default { + 0: { description: 'Soft tissue', window: 400, level: 40 }, + 1: { description: 'Lung', window: 1500, level: -600 }, + 2: { description: 'Liver', window: 150, level: 90 }, + 3: { description: 'Bone', window: 2500, level: 480 }, + 4: { description: 'Brain', window: 80, level: 40 }, + 5: { description: '', window: '', level: '' }, + 6: { description: '', window: '', level: '' }, + 7: { description: '', window: '', level: '' }, + 8: { description: '', window: '', level: '' }, + 9: { description: '', window: '', level: '' }, + 10: { description: '', window: '', level: '' }, +}; diff --git a/packages/_components/src/components/userPreferencesModal/hotKeysConfig.js b/packages/_components/src/components/userPreferencesModal/hotKeysConfig.js new file mode 100644 index 000000000..cb3cd51f8 --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/hotKeysConfig.js @@ -0,0 +1,100 @@ +const range = (start, end) => { + return new Array(end - start).fill().map((d, i) => i + start); +}; + +export const disallowedCombinations = { + '': [], + ALT: ['SPACE'], + SHIFT: [], + CTRL: [ + 'F4', + 'F5', + 'F11', + 'W', + 'R', + 'T', + 'O', + 'P', + 'A', + 'D', + 'F', + 'G', + 'H', + 'J', + 'L', + 'Z', + 'X', + 'C', + 'V', + 'B', + 'N', + 'PAGEDOWN', + 'PAGEUP', + ], + 'CTRL+SHIFT': ['Q', 'W', 'R', 'T', 'P', 'A', 'H', 'V', 'B', 'N'], +}; + +export const allowedKeys = [ + ...[8, 13, 27, 32, 46], // BACKSPACE, ENTER, ESCAPE, SPACE, DELETE + ...[12, 106, 107, 109, 110, 111], // Numpad keys + ...range(218, 220), // [\] + ...range(185, 190), // ;=,-./ + ...range(111, 131), // F1-F19 + ...range(32, 41), // arrow keys, home/end, pg dn/up + ...range(47, 58), // 0-9 + ...range(64, 91), // A-Z +]; + +export const specialKeys = { + 8: 'backspace', + 9: 'tab', + 13: 'return', + 16: 'shift', + 17: 'ctrl', + 18: 'alt', + 19: 'pause', + 20: 'capslock', + 27: 'esc', + 32: 'space', + 33: 'pageup', + 34: 'pagedown', + 35: 'end', + 36: 'home', + 37: 'left', + 38: 'up', + 39: 'right', + 40: 'down', + 45: 'insert', + 46: 'del', + 96: '0', + 97: '1', + 98: '2', + 99: '3', + 100: '4', + 101: '5', + 102: '6', + 103: '7', + 104: '8', + 105: '9', + 106: '*', + 107: '+', + 109: '-', + 110: '.', + 111: '/', + 112: 'f1', + 113: 'f2', + 114: 'f3', + 115: 'f4', + 116: 'f5', + 117: 'f6', + 118: 'f7', + 119: 'f8', + 120: 'f9', + 121: 'f10', + 122: 'f11', + 123: 'f12', + 144: 'numlock', + 145: 'scroll', + 191: '/', + 224: 'meta', +}; diff --git a/packages/_components/src/components/userPreferencesModal/index.js b/packages/_components/src/components/userPreferencesModal/index.js new file mode 100644 index 000000000..fa7580d5e --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/index.js @@ -0,0 +1,4 @@ +export { UserPreferences } from './UserPreferences.js'; +export { AboutModal } from './AboutModal.js'; +export { UserPreferencesModal } from './UserPreferencesModal.js'; +export { GeneralPreferences } from './GeneralPreferences.js'; diff --git a/packages/_components/src/design/styles/common/button.styl b/packages/_components/src/design/styles/common/button.styl new file mode 100644 index 000000000..a77e855c6 --- /dev/null +++ b/packages/_components/src/design/styles/common/button.styl @@ -0,0 +1,47 @@ +.btn-danger + color: #fff; + background-color: #d9534f; + border-color: #d43f3a; + +.btn-primary + color: #fff; + background-color: #337ab7; + border-color: #2e6da4; + +.btn-default + color: #333; + background-color: #fff; + border-color: #ccc; + +.btn + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: 400; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + vertical-align: middle; + -ms-touch-action: manipulation; + touch-action: manipulation; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; + +button.close + -webkit-appearance: none; + padding: 0; + cursor: pointer; + background: 0 0; + border: 0; + +.btn-group, .btn-group-vertical + position: relative; + display: inline-block; + vertical-align: middle; diff --git a/packages/_components/src/design/styles/common/form.styl b/packages/_components/src/design/styles/common/form.styl new file mode 100644 index 000000000..03bbdf985 --- /dev/null +++ b/packages/_components/src/design/styles/common/form.styl @@ -0,0 +1,117 @@ +.form-themed + + table + color: var(--text-primary-color) + + th, td + font-size: 15px + font-weight: normal + + th + line-height: 60px + + td + line-height: 18px + padding: 0; + + // TODO: imported this from merged-styles.css + .p-r-1 + padding-right: 1rem !important; + + label + margin-bottom: 5px + // end TODO + + label.wrapperLabel + cursor: pointer + + label.wrapperLabel:not(.checkboxLabel) + cursor: pointer + display: flex + flex-direction: column + + .wrapperText + display: block + order: -1 + transition(color 0.3s ease) + + .btn, input[type=text], input[type=password], input[type=number] + &[disabled], &.disabled + &, &:hover, &:active + background-color: var(--ui-gray-darker) + border-color: var(--ui-gray-light) + color: var(--text-primary-color) + + & + .wrapperText + color: var(--text-primary-color) + cursor: auto + + input[type=text], input[type=password], input[type=number] + background-color: var(--ui-gray) + border-color: var(--ui-border-color) + color: var(--text-primary-color) + font-weight: normal + transition(background-color 0.3s ease\, border-color 0.3s ease) + + &:active, &:focus + background-color: var(--ui-gray-dark) + border: 2px solid var(--active-color) + box-shadow: none + + & + .wrapperText + color: var(--active-color) + + .btn-primary + background-color: var(--active-color) + border-collapse: var(--ui-border-color-active) + color: var(--ui-text-color-active) + transition(background-color 0.3s ease\, border-color 0.3s ease) + + &:hover, &:active, &:focus, &:focus:active + // TODO: 0.8 opacity + background-color: var(--ui-active-color) + border-collapse: var(--ui-border-color-active) + color: var(--ui-text-color-active) + + .form-control + background-color: #FFFFFF + border: 0 + border-radius(2px) + font-size: 14px + height: 30px + line-height: 16px + padding: 8px 9px 6px + +.form-control + display: block; + width: 100%; + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + color: #555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: inset 0 1px 1px rgba(0,0,0,.075); + + &:focus + border-color: #66afe9; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102,175,233,.6); + box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102,175,233,.6); + + &::-ms-expand + border: 0; + background-color: transparent; + + &[disabled], + &[readonly], + fieldset[disabled] & + background-color: @input-bg-disabled; + opacity: 1; + + &[disabled], + fieldset[disabled] & + cursor: @cursor-disabled; diff --git a/packages/_components/src/design/styles/common/global.styl b/packages/_components/src/design/styles/common/global.styl new file mode 100644 index 000000000..cba02dac8 --- /dev/null +++ b/packages/_components/src/design/styles/common/global.styl @@ -0,0 +1,78 @@ +* + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + +.full-width + width: 100% + +.full-height + height: 100% + +.flex-h + display: flex + flex-direction: row + +.flex-v + display: flex + flex-direction: column + +.flex-grow + flex-grow: 1 + +.nowrap + white-space: nowrap + +.text-right + text-align: right; + +.text-center + text-align: center; + +.text-left + text-align: left; + +h3 + margin-top: 20px; + margin-bottom: 10px; + +pre + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + color: #333; + word-break: break-all; + word-wrap: break-word; + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 4px; + +button, input, select, textarea + font-family: inherit; + font-size: inherit; + line-height: inherit; + +.row + margin-right: -15px; + margin-left: -15px; + +.pull-left + float: left!important; + +.pull-right + float: right!important; + +.fade + opacity: 0; + -webkit-transition: opacity .15s linear; + -o-transition: opacity .15s linear; + transition: opacity .15s linear; + +.fade.in + opacity: 1; + +a + color: #337ab7; + text-decoration: none; diff --git a/packages/_components/src/design/styles/common/modal.styl b/packages/_components/src/design/styles/common/modal.styl new file mode 100644 index 000000000..d409eee32 --- /dev/null +++ b/packages/_components/src/design/styles/common/modal.styl @@ -0,0 +1,202 @@ + +.modal-open + overflow: hidden; + +.modal + overflow: hidden; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1050; + -webkit-overflow-scrolling: touch; + outline: 0; + + .modal-dialog + + .modal-content + background-color: var(--ui-gray-darker) + border-color: var(--ui-border-color) + color: var(--text-secondary-color) + border-radius(6px) + border: 0 + + .modal-header, .modal-footer + border-color: var(--ui-border-color) + + .dialog-separator, + .dialog-separator-before, + .dialog-separator-after + position: relative + height: 40px; + + .dialog-separator:before, + .dialog-separator:after, + .dialog-separator-before:before, + .dialog-separator-after:after + background-color: #000000 + box-shadow(-50px 0 0 #000000\, 50px 0 0 #000000) + content: ' ' + display: block + height: 3px + left: 0 + position: absolute + width: 100% + + .dialog-separator:before, + .dialog-separator-before:before + top: -3px + + .dialog-separator:after, + .dialog-separator-after:after + bottom: -3px + + .modal-body + color: var(--text-primary-color) + padding: 16px 22px 0px + position: relative + + ::-webkit-scrollbar { + width: 6px; + background-color: var(--ui-gray-dark); + } + + ::-webkit-scrollbar-thumb { + background-color: var(--ui-gray-light); + border-radius: 5px; + } + + .modal-header + border-bottom-width: 3px + border-bottom-style: solid + border-bottom-color: #000000 + padding: 19px 22px 17px + position: relative + + h4 + color: var(--text-secondary-color) + font-size: 20px + font-weight: 500 + line-height: 24px + padding-right: 24px + + .modal-footer + border-top: 0 + padding-top: 0 + + .card-round + background-color: var(--ui-gray-dark) + border-radius(5px) + padding: 10px + + .modal-header + position: relative + + // When fading in the modal, animate it to slide down + &.fade .modal-dialog + -webkit-transition: transform .3s ease-out; + -o-transition: transform .3s ease-out; + transition: transform .3s ease-out; + -webkit-transform: translate(0,-25%); + -ms-transform: translate(0,-25%); + -o-transform: translate(0,-25%); + transform: translate(0,-25%); + + &.in .modal-dialog + transform: translate(0,0) + +.modal-open .modal + overflow-x: hidden; + overflow-y: auto; + +.modal-dialog + position: relative; + width: auto; + margin: 10px; + +.modal-content + position: relative; + background-color: #fff; + border: 1px solid rgba(0,0,0,.2); + border-radius: 6px; + -webkit-box-shadow: 0 3px 9px rgba(0,0,0,.5); + box-shadow: 0 3px 9px rgba(0,0,0,.5); + background-clip: padding-box; + outline: 0; + +.modal-backdrop + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1050; + background-color: #fff; + + &.fade + opacity: 0; + + &.in + opacity: 0; + +.modal-header + padding: 15px; + border-bottom: 1px solid #e5e5e5; + +.modal-header .close + margin-top: -2px; + +.modal-title + margin: 0; + line-height: 1.42857143; + +.modal-body + position: relative; + padding: 15px; + +.modal-footer + padding: 15px; + text-align: right; + border-top: 1px solid #e5e5e5; + + .btn+.btn + margin-left: 5px; + margin-bottom: 0; + + .btn-group .btn+.btn + margin-left: -1px; + + .btn-block+.btn-block + margin-left: 0; + +.modal-scrollbar-measure + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; + +@media (min-width: 768px) + .modal-dialog + width: 600px; + margin: 30px auto; + + .modal-content + -webkit-box-shadow: 0 5px 15px rgba(0,0,0,.5); + box-shadow: 0 5px 15px rgba(0,0,0,.5); + +@media (min-width: 992px) + .modal-lg + width: 900px; + +.button-close, .modal-dialog button.close + color: var(--text-secondary-color) + height: 25px + opacity: 1 + overflow: hidden + text-align: center + text-shadow: none + width: 25px + font-size:25px + font-weight: lighter diff --git a/packages/_components/src/design/styles/common/navbar.styl b/packages/_components/src/design/styles/common/navbar.styl new file mode 100644 index 000000000..dda831789 --- /dev/null +++ b/packages/_components/src/design/styles/common/navbar.styl @@ -0,0 +1,57 @@ +.nav-tabs + border-bottom: 0 + margin-bottom: 3px + position: relative + z-index: 1 + + :hover + cursor: pointer + + &>li + font-size: 14px + font-weight: normal + height: 40px + line-height: 40px + margin-bottom: 0 + + &>button + display: block + width: 100% + line-height: inherit + margin: 0 + height: inherit + padding: 0 10px 0 10px + outline: none + + &:after + background-color: transparent + bottom: -3px + content: ' ' + display: block + height: 3px + transition(background-color 0.3s ease) + width: 100% + + &>button, &.active>button + &, &:hover, &:active, &:focus + color: var(--active-color) + background-color: transparent + border: 0 + + &.active>button + font-weight: bold + + &:after + background-color: var(--active-color) + +.nav + padding-left: 0; + list-style: none; + + > li + position: relative; + display: block; + + > li + float: left; + margin-bottom: -1px; diff --git a/packages/_components/src/design/styles/common/state.styl b/packages/_components/src/design/styles/common/state.styl new file mode 100644 index 000000000..8c438f741 --- /dev/null +++ b/packages/_components/src/design/styles/common/state.styl @@ -0,0 +1,25 @@ +.form-themed .state-error, .state-error + + &.wrapperLabel input + .wrapperText + color: var(--state-error-text) + + &+.tooltip + + .tooltip-inner + color: white + background-color: var(--state-error-border) + + &.top .tooltip-arrow + border-top-color: var(--state-error-border) + &.right .tooltip-arrow + border-right-color: var(--state-error-border) + &.bottom .tooltip-arrow + border-bottom-color: var(--state-error-border) + &.left .tooltip-arrow + border-left-color: var(--state-error-border) + + + &:not(.component-group) + &.form-control, .form-control + background-color: var(--state-error-border) + border-color: var(--state-error-border) diff --git a/packages/_components/src/design/styles/common/table.styl b/packages/_components/src/design/styles/common/table.styl new file mode 100644 index 000000000..26485d4e7 --- /dev/null +++ b/packages/_components/src/design/styles/common/table.styl @@ -0,0 +1,15 @@ +.table-responsive + min-height: .01%; + overflow-x: auto; + +.table + width: 100%; + max-width: 100%; + margin-bottom: 20px; + +table + border-spacing: 0; + border-collapse: collapse; + +th + text-align: left; diff --git a/packages/_components/src/elements/Icon/Icon.js b/packages/_components/src/elements/Icon/Icon.js new file mode 100644 index 000000000..5791fee68 --- /dev/null +++ b/packages/_components/src/elements/Icon/Icon.js @@ -0,0 +1,15 @@ +import './Icon.styl'; + +import PropTypes from 'prop-types'; +import getIcon from './getIcon.js'; + +const Icon = props => { + return getIcon(props.name, props); +}; + +Icon.propTypes = { + /** The string name of the icon to display */ + name: PropTypes.string.isRequired, +}; + +export default Icon; diff --git a/packages/_components/src/elements/Icon/Icon.styl b/packages/_components/src/elements/Icon/Icon.styl new file mode 100644 index 000000000..2d7949a0e --- /dev/null +++ b/packages/_components/src/elements/Icon/Icon.styl @@ -0,0 +1,7 @@ +.icon-pulse + fa-spin 1s infinite steps(8) + +@keyframes fa-spin{ + 0%{ transform:rotate(0deg) } + to{ transform:rotate(1turn) } +} diff --git a/packages/_components/src/elements/Icon/__docs__/icon.mdx b/packages/_components/src/elements/Icon/__docs__/icon.mdx new file mode 100644 index 000000000..c5b8f07a3 --- /dev/null +++ b/packages/_components/src/elements/Icon/__docs__/icon.mdx @@ -0,0 +1,84 @@ +--- +name: Icon +menu: Elements +route: /elements/icon +--- + +import { Playground, Props } from 'docz'; +import { State } from 'react-powerplug'; +import { Icon, ICONS } from './../index.js'; + +# Icon + +## Basic usage + + +
    + {Object.keys(ICONS) + .sort() + .map((name, i) => { + return ( + +
    + +
    + {name} +
    +
    +
    + ); + })} +
    +
    + +The `` component inherits the color of its parent, and sizing is based +on its parent's `font-size`. Each icon has an `aria-labelledby` to assist screen +readers. + +We use `airbnb`'s [`babel-plugin-inline-react-svg`][airbnb] to automate +optimizing SVG files with [SVGO][svgo] and to create animatable React SVG +components that can be requested asynchronously. You can configure SVGO's +plugins [by following the advice laid out here][svgo-configure] + +## API + + + +The `Icon` component takes a single prop of `name`, which indicates which Icon +should be displayed. Again, the icon responds to contextual text/font styles. + +## Attribution + +A large number of these icons are from [Font Awesome 5's free set.][fa-license] +Some attribution is likely required, depending on your usage. Sometimes as +little as a comment included side-by-side with the accompanying SVG icon. + + + + +[fa-license]: https://fontawesome.com/license +[airbnb]: https://github.com/airbnb/babel-plugin-inline-react-svg +[svgo]: https://github.com/svg/svgo/ +[svgo-configure]: https://github.com/airbnb/babel-plugin-inline-react-svg/issues/9#issuecomment-310178317) + diff --git a/packages/_components/src/elements/Icon/getIcon.js b/packages/_components/src/elements/Icon/getIcon.js new file mode 100644 index 000000000..15c85c397 --- /dev/null +++ b/packages/_components/src/elements/Icon/getIcon.js @@ -0,0 +1,178 @@ +import React from 'react'; +import adjust from './icons/adjust.svg'; +// Icons +import angleDoubleDown from './icons/angle-double-down.svg'; +import angleDoubleUp from './icons/angle-double-up.svg'; +import angleLeft from './icons/angle-left.svg'; +import arrows from './icons/arrows.svg'; +import arrowsAltH from './icons/arrows-alt-h.svg'; +import arrowsAltV from './icons/arrows-alt-v.svg'; +import bars from './icons/bars.svg'; +import brain from './icons/brain.svg'; +import caretDown from './icons/caret-down.svg'; +import caretUp from './icons/caret-up.svg'; +import check from './icons/check.svg'; +import checkCircle from './icons/check-circle.svg'; +import checkCircleO from './icons/check-circle-o.svg'; +import chevronDown from './icons/chevron-down.svg'; +import circle from './icons/circle.svg'; +import circleNotch from './icons/circle-notch.svg'; +import circleO from './icons/circle-o.svg'; +import cog from './icons/cog.svg'; +import createComment from './icons/create-comment.svg'; +import createScreenCapture from './icons/create-screen-capture.svg'; +import crosshairs from './icons/crosshairs.svg'; +import cube from './icons/cube.svg'; +import d3Rotate from './icons/3d-rotate.svg'; +import database from './icons/database.svg'; +import dotCircle from './icons/dot-circle.svg'; +import edit from './icons/edit.svg'; +import ellipseCircle from './icons/ellipse-circle.svg'; +import ellipseH from './icons/ellipse-h.svg'; +import ellipseV from './icons/ellipse-v.svg'; +import exclamationCircle from './icons/exclamation-circle.svg'; +import exclamationTriangle from './icons/exclamation-triangle.svg'; +import fastBackward from './icons/fast-backward.svg'; +import fastForward from './icons/fast-forward.svg'; +import info from './icons/info.svg'; +import inlineEdit from './icons/inline-edit.svg'; +import level from './icons/level.svg'; +import link from './icons/link.svg'; +import linkCircles from './icons/link-circles.svg'; +import list from './icons/list.svg'; +import liver from './icons/liver.svg'; +import lock from './icons/lock.svg'; +import lockAlt from './icons/lock-alt.svg'; +import lung from './icons/lung.svg'; +import measureNonTarget from './icons/measure-non-target.svg'; +import measureTarget from './icons/measure-target.svg'; +import measureTargetCr from './icons/measure-target-cr.svg'; +import measureTargetNe from './icons/measure-target-ne.svg'; +import measureTargetUn from './icons/measure-target-un.svg'; +import measureTemp from './icons/measure-temp.svg'; +import objectGroup from './icons/object-group.svg'; +import ohifLogo from './icons/ohif-logo.svg'; +import oval from './icons/oval.svg'; +import palette from './icons/palette.svg'; +import play from './icons/play.svg'; +import plus from './icons/plus.svg'; +import powerOff from './icons/power-off.svg'; +import reset from './icons/reset.svg'; +import rotate from './icons/rotate.svg'; +import rotateRight from './icons/rotate-right.svg'; +import search from './icons/search.svg'; +import searchPlus from './icons/search-plus.svg'; +import softTissue from './icons/soft-tissue.svg'; +import sort from './icons/sort.svg'; +import sortDown from './icons/sort-down.svg'; +import sortUp from './icons/sort-up.svg'; +import squareO from './icons/square-o.svg'; +import star from './icons/star.svg'; +import stepBackward from './icons/step-backward.svg'; +import stepForward from './icons/step-forward.svg'; +import stop from './icons/stop.svg'; +import sun from './icons/sun.svg'; +import th from './icons/th.svg'; +import thLarge from './icons/th-large.svg'; +import thList from './icons/th-list.svg'; +import times from './icons/times.svg'; +import trash from './icons/trash.svg'; +import user from './icons/user.svg'; +import youtube from './icons/youtube.svg'; + +const ICONS = { + user, + sort, + th, + star, + 'sort-up': sortUp, + 'sort-down': sortDown, + info, + cube, + crosshairs, + 'dot-circle': dotCircle, + 'angle-left': angleLeft, + '3d-rotate': d3Rotate, + plus, + 'chevron-down': chevronDown, + 'angle-double-down': angleDoubleDown, + 'angle-double-up': angleDoubleUp, + 'arrows-alt-h': arrowsAltH, + 'arrows-alt-v': arrowsAltV, + bars, + 'caret-down': caretDown, + 'caret-up': caretUp, + 'check-circle-o': checkCircleO, + check, + circle, + 'circle-o': circleO, + times, + 'create-comment': createComment, + 'create-screen-capture': createScreenCapture, + edit, + 'fast-backward': fastBackward, + 'fast-forward': fastForward, + 'object-group': objectGroup, + search, + 'power-off': powerOff, + 'inline-edit': inlineEdit, + list, + 'ohif-logo': ohifLogo, + lock, + play, + database, + cog, + 'circle-notch': circleNotch, + 'square-o': squareO, + 'check-circle': checkCircle, + 'lock-alt': lockAlt, + 'step-backward': stepBackward, + 'step-forward': stepForward, + stop, + 'th-large': thLarge, + 'th-list': thList, + sun, + palette, + youtube, + oval, + 'ellipse-h': ellipseH, + 'ellipse-v': ellipseV, + adjust, + level, + 'link-circles': linkCircles, + 'search-plus': searchPlus, + 'measure-non-target': measureNonTarget, + 'measure-target': measureTarget, + 'measure-target-cr': measureTargetCr, + 'measure-target-un': measureTargetUn, + 'measure-target-ne': measureTargetNe, + 'measure-temp': measureTemp, + 'ellipse-circle': ellipseCircle, + arrows, + reset, + rotate, + 'rotate-right': rotateRight, + trash, + 'exclamation-circle': exclamationCircle, + link, + 'exclamation-triangle': exclamationTriangle, + brain, + 'soft-tissue': softTissue, + lung, + liver, +}; + +/** + * Return the matching SVG Icon as a React Component. + * Results in an inlined SVG Element. If there's no match, + * return `null` + */ +export default function getIcon(key, props) { + if (!key || !ICONS[key]) { + return React.createElement('div', null, 'Missing Icon'); + } + + return React.createElement(ICONS[key], props); +} + +export { ICONS }; diff --git a/packages/_components/src/elements/Icon/icons/3d-rotate.svg b/packages/_components/src/elements/Icon/icons/3d-rotate.svg new file mode 100644 index 000000000..f9d2daadb --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/3d-rotate.svg @@ -0,0 +1,13 @@ + + 3D Rotate + + + + diff --git a/packages/_components/src/elements/Icon/icons/adjust.svg b/packages/_components/src/elements/Icon/icons/adjust.svg new file mode 100644 index 000000000..ae2747644 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/adjust.svg @@ -0,0 +1,11 @@ + + Adjust + + diff --git a/packages/_components/src/elements/Icon/icons/angle-double-down.svg b/packages/_components/src/elements/Icon/icons/angle-double-down.svg new file mode 100644 index 000000000..04d9d9fcf --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/angle-double-down.svg @@ -0,0 +1,11 @@ + + Angle Double Down + + diff --git a/packages/_components/src/elements/Icon/icons/angle-double-up.svg b/packages/_components/src/elements/Icon/icons/angle-double-up.svg new file mode 100644 index 000000000..07419bb65 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/angle-double-up.svg @@ -0,0 +1,11 @@ + + Angle Double Up + + diff --git a/packages/_components/src/elements/Icon/icons/angle-left.svg b/packages/_components/src/elements/Icon/icons/angle-left.svg new file mode 100644 index 000000000..15fb36b07 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/angle-left.svg @@ -0,0 +1,11 @@ + + Angle Left + + diff --git a/packages/_components/src/elements/Icon/icons/arrows-alt-h.svg b/packages/_components/src/elements/Icon/icons/arrows-alt-h.svg new file mode 100644 index 000000000..c96cb738c --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/arrows-alt-h.svg @@ -0,0 +1,11 @@ + + Arrows Alt H + + diff --git a/packages/_components/src/elements/Icon/icons/arrows-alt-v.svg b/packages/_components/src/elements/Icon/icons/arrows-alt-v.svg new file mode 100644 index 000000000..f1e777ddd --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/arrows-alt-v.svg @@ -0,0 +1,11 @@ + + Arrows Alt V + + diff --git a/packages/_components/src/elements/Icon/icons/arrows.svg b/packages/_components/src/elements/Icon/icons/arrows.svg new file mode 100644 index 000000000..4aca49cff --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/arrows.svg @@ -0,0 +1,22 @@ + + Arrows + + + + + + + + + diff --git a/packages/_components/src/elements/Icon/icons/bars.svg b/packages/_components/src/elements/Icon/icons/bars.svg new file mode 100644 index 000000000..3b735f90e --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/bars.svg @@ -0,0 +1,11 @@ + + Bars + + diff --git a/packages/_components/src/elements/Icon/icons/brain.svg b/packages/_components/src/elements/Icon/icons/brain.svg new file mode 100644 index 000000000..f03f67ace --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/brain.svg @@ -0,0 +1,24 @@ + + Brain + + + + + + + + + + diff --git a/packages/_components/src/elements/Icon/icons/caret-down.svg b/packages/_components/src/elements/Icon/icons/caret-down.svg new file mode 100644 index 000000000..cdebbd8f5 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/caret-down.svg @@ -0,0 +1,11 @@ + + Caret Down + + diff --git a/packages/_components/src/elements/Icon/icons/caret-up.svg b/packages/_components/src/elements/Icon/icons/caret-up.svg new file mode 100644 index 000000000..611ca870e --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/caret-up.svg @@ -0,0 +1,11 @@ + + Caret Up + + diff --git a/packages/_components/src/elements/Icon/icons/check-circle-o.svg b/packages/_components/src/elements/Icon/icons/check-circle-o.svg new file mode 100644 index 000000000..98b247319 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/check-circle-o.svg @@ -0,0 +1,11 @@ + + Check Circle + + diff --git a/packages/_components/src/elements/Icon/icons/check-circle.svg b/packages/_components/src/elements/Icon/icons/check-circle.svg new file mode 100644 index 000000000..7f54493fe --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/check-circle.svg @@ -0,0 +1,11 @@ + + Check Circle + + diff --git a/packages/_components/src/elements/Icon/icons/check.svg b/packages/_components/src/elements/Icon/icons/check.svg new file mode 100644 index 000000000..22d93a1fb --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/check.svg @@ -0,0 +1,11 @@ + + Check + + diff --git a/packages/_components/src/elements/Icon/icons/chevron-down.svg b/packages/_components/src/elements/Icon/icons/chevron-down.svg new file mode 100644 index 000000000..1c28b0512 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/chevron-down.svg @@ -0,0 +1,14 @@ + + Chevron Down + + diff --git a/packages/_components/src/elements/Icon/icons/circle-notch.svg b/packages/_components/src/elements/Icon/icons/circle-notch.svg new file mode 100644 index 000000000..1a9429bda --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/circle-notch.svg @@ -0,0 +1,11 @@ + + Circle Notch + + diff --git a/packages/_components/src/elements/Icon/icons/circle-o.svg b/packages/_components/src/elements/Icon/icons/circle-o.svg new file mode 100644 index 000000000..c921f1534 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/circle-o.svg @@ -0,0 +1,11 @@ + + Circle Outline + + diff --git a/packages/_components/src/elements/Icon/icons/circle.svg b/packages/_components/src/elements/Icon/icons/circle.svg new file mode 100644 index 000000000..99b9995b3 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/circle.svg @@ -0,0 +1,11 @@ + + Circle + + diff --git a/packages/_components/src/elements/Icon/icons/cog.svg b/packages/_components/src/elements/Icon/icons/cog.svg new file mode 100644 index 000000000..017fc0e6d --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/cog.svg @@ -0,0 +1,11 @@ + + Cog + + diff --git a/packages/_components/src/elements/Icon/icons/create-comment.svg b/packages/_components/src/elements/Icon/icons/create-comment.svg new file mode 100644 index 000000000..aec47a3ae --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/create-comment.svg @@ -0,0 +1,12 @@ + + Create Comment + + + diff --git a/packages/_components/src/elements/Icon/icons/create-screen-capture.svg b/packages/_components/src/elements/Icon/icons/create-screen-capture.svg new file mode 100644 index 000000000..cf7d8433d --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/create-screen-capture.svg @@ -0,0 +1,15 @@ + + Create Screen Capture + + + + + + diff --git a/packages/_components/src/elements/Icon/icons/crosshairs.svg b/packages/_components/src/elements/Icon/icons/crosshairs.svg new file mode 100644 index 000000000..4d53a1042 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/crosshairs.svg @@ -0,0 +1,15 @@ + + Crosshairs + + + diff --git a/packages/_components/src/elements/Icon/icons/cube.svg b/packages/_components/src/elements/Icon/icons/cube.svg new file mode 100644 index 000000000..6026cdf63 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/cube.svg @@ -0,0 +1,12 @@ + + + Cube + + diff --git a/packages/_components/src/elements/Icon/icons/database.svg b/packages/_components/src/elements/Icon/icons/database.svg new file mode 100644 index 000000000..323d566e2 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/database.svg @@ -0,0 +1,11 @@ + + Database + + diff --git a/packages/_components/src/elements/Icon/icons/dot-circle.svg b/packages/_components/src/elements/Icon/icons/dot-circle.svg new file mode 100644 index 000000000..3533dde76 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/dot-circle.svg @@ -0,0 +1,11 @@ + + Dot Circle + + diff --git a/packages/_components/src/elements/Icon/icons/edit.svg b/packages/_components/src/elements/Icon/icons/edit.svg new file mode 100644 index 000000000..c7554488d --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/edit.svg @@ -0,0 +1,11 @@ + + Edit + + diff --git a/packages/_components/src/elements/Icon/icons/ellipse-circle.svg b/packages/_components/src/elements/Icon/icons/ellipse-circle.svg new file mode 100644 index 000000000..a0c591c2b --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/ellipse-circle.svg @@ -0,0 +1,11 @@ + + Ellipse Circle + + diff --git a/packages/_components/src/elements/Icon/icons/ellipse-h.svg b/packages/_components/src/elements/Icon/icons/ellipse-h.svg new file mode 100644 index 000000000..1dbb78331 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/ellipse-h.svg @@ -0,0 +1,11 @@ + + Ellipse Horizontal + + diff --git a/packages/_components/src/elements/Icon/icons/ellipse-v.svg b/packages/_components/src/elements/Icon/icons/ellipse-v.svg new file mode 100644 index 000000000..01a0d719a --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/ellipse-v.svg @@ -0,0 +1,11 @@ + + Ellipse Vertical + + diff --git a/packages/_components/src/elements/Icon/icons/exclamation-circle.svg b/packages/_components/src/elements/Icon/icons/exclamation-circle.svg new file mode 100644 index 000000000..f3a1d4146 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/exclamation-circle.svg @@ -0,0 +1,13 @@ + + Exclamation Circle + + diff --git a/packages/_components/src/elements/Icon/icons/exclamation-triangle.svg b/packages/_components/src/elements/Icon/icons/exclamation-triangle.svg new file mode 100644 index 000000000..65da6f8e8 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/exclamation-triangle.svg @@ -0,0 +1,11 @@ + + Exclamation Triangle + + diff --git a/packages/_components/src/elements/Icon/icons/fast-backward.svg b/packages/_components/src/elements/Icon/icons/fast-backward.svg new file mode 100644 index 000000000..c1696b98f --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/fast-backward.svg @@ -0,0 +1,10 @@ + + + diff --git a/packages/_components/src/elements/Icon/icons/fast-forward.svg b/packages/_components/src/elements/Icon/icons/fast-forward.svg new file mode 100644 index 000000000..cd6bb9b7c --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/fast-forward.svg @@ -0,0 +1,10 @@ + + + diff --git a/packages/_components/src/elements/Icon/icons/info.svg b/packages/_components/src/elements/Icon/icons/info.svg new file mode 100644 index 000000000..9e802a69f --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/info.svg @@ -0,0 +1,11 @@ + + Info + + diff --git a/packages/_components/src/elements/Icon/icons/inline-edit.svg b/packages/_components/src/elements/Icon/icons/inline-edit.svg new file mode 100644 index 000000000..12c111b2c --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/inline-edit.svg @@ -0,0 +1,11 @@ + + Inline Edit + + diff --git a/packages/_components/src/elements/Icon/icons/level.svg b/packages/_components/src/elements/Icon/icons/level.svg new file mode 100644 index 000000000..c79ffc2f6 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/level.svg @@ -0,0 +1,13 @@ + + Level + + + diff --git a/packages/_components/src/elements/Icon/icons/link-circles.svg b/packages/_components/src/elements/Icon/icons/link-circles.svg new file mode 100644 index 000000000..fd980320a --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/link-circles.svg @@ -0,0 +1,15 @@ + + Link Circles + + + + + diff --git a/packages/_components/src/elements/Icon/icons/link.svg b/packages/_components/src/elements/Icon/icons/link.svg new file mode 100644 index 000000000..28faa4853 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/link.svg @@ -0,0 +1,12 @@ + + Link + + + diff --git a/packages/_components/src/elements/Icon/icons/list.svg b/packages/_components/src/elements/Icon/icons/list.svg new file mode 100644 index 000000000..15e7a6c95 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/list.svg @@ -0,0 +1,11 @@ + + List + + diff --git a/packages/_components/src/elements/Icon/icons/liver.svg b/packages/_components/src/elements/Icon/icons/liver.svg new file mode 100644 index 000000000..a099709f4 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/liver.svg @@ -0,0 +1,16 @@ + + Liver + + + + diff --git a/packages/_components/src/elements/Icon/icons/lock-alt.svg b/packages/_components/src/elements/Icon/icons/lock-alt.svg new file mode 100644 index 000000000..570a16de6 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/lock-alt.svg @@ -0,0 +1,11 @@ + + Lock + + diff --git a/packages/_components/src/elements/Icon/icons/lock.svg b/packages/_components/src/elements/Icon/icons/lock.svg new file mode 100644 index 000000000..6a43ae317 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/lock.svg @@ -0,0 +1,11 @@ + + Lock + + diff --git a/packages/_components/src/elements/Icon/icons/lung.svg b/packages/_components/src/elements/Icon/icons/lung.svg new file mode 100644 index 000000000..2de91333c --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/lung.svg @@ -0,0 +1,25 @@ + + Lung + + + + + + + diff --git a/packages/_components/src/elements/Icon/icons/measure-non-target.svg b/packages/_components/src/elements/Icon/icons/measure-non-target.svg new file mode 100644 index 000000000..6c6c55f61 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/measure-non-target.svg @@ -0,0 +1,29 @@ + + Measure Non-Target + + + + diff --git a/packages/_components/src/elements/Icon/icons/measure-target-cr.svg b/packages/_components/src/elements/Icon/icons/measure-target-cr.svg new file mode 100644 index 000000000..474136ac7 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/measure-target-cr.svg @@ -0,0 +1,23 @@ + + Measure Target CR + + CR + + + diff --git a/packages/_components/src/elements/Icon/icons/measure-target-ne.svg b/packages/_components/src/elements/Icon/icons/measure-target-ne.svg new file mode 100644 index 000000000..492a880e7 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/measure-target-ne.svg @@ -0,0 +1,16 @@ + + Measure Target NE + NE + + diff --git a/packages/_components/src/elements/Icon/icons/measure-target-un.svg b/packages/_components/src/elements/Icon/icons/measure-target-un.svg new file mode 100644 index 000000000..5ba5eb223 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/measure-target-un.svg @@ -0,0 +1,23 @@ + + Measure Target UN + + UN + + + diff --git a/packages/_components/src/elements/Icon/icons/measure-target.svg b/packages/_components/src/elements/Icon/icons/measure-target.svg new file mode 100644 index 000000000..19596906a --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/measure-target.svg @@ -0,0 +1,13 @@ + + Measure Target + + + diff --git a/packages/_components/src/elements/Icon/icons/measure-temp.svg b/packages/_components/src/elements/Icon/icons/measure-temp.svg new file mode 100644 index 000000000..fb2e2b98b --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/measure-temp.svg @@ -0,0 +1,16 @@ + + Measure Temp + + + + + + diff --git a/packages/_components/src/elements/Icon/icons/object-group.svg b/packages/_components/src/elements/Icon/icons/object-group.svg new file mode 100644 index 000000000..edd4bc189 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/object-group.svg @@ -0,0 +1,20 @@ + + Object Group + + + + + diff --git a/packages/_components/src/elements/Icon/icons/ohif-logo.svg b/packages/_components/src/elements/Icon/icons/ohif-logo.svg new file mode 100644 index 000000000..f6525ab90 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/ohif-logo.svg @@ -0,0 +1,17 @@ + + OHIF Logo + + + + + diff --git a/packages/_components/src/elements/Icon/icons/oval.svg b/packages/_components/src/elements/Icon/icons/oval.svg new file mode 100644 index 000000000..0a69e6b11 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/oval.svg @@ -0,0 +1,11 @@ + + Oval + + diff --git a/packages/_components/src/elements/Icon/icons/palette.svg b/packages/_components/src/elements/Icon/icons/palette.svg new file mode 100644 index 000000000..04f596ba1 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/palette.svg @@ -0,0 +1,11 @@ + + Pallete + + diff --git a/packages/_components/src/elements/Icon/icons/play.svg b/packages/_components/src/elements/Icon/icons/play.svg new file mode 100644 index 000000000..64ca7a5e3 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/play.svg @@ -0,0 +1,10 @@ + + + diff --git a/packages/_components/src/elements/Icon/icons/plus.svg b/packages/_components/src/elements/Icon/icons/plus.svg new file mode 100644 index 000000000..6512e1116 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/plus.svg @@ -0,0 +1,11 @@ + + Add + + diff --git a/packages/_components/src/elements/Icon/icons/power-off.svg b/packages/_components/src/elements/Icon/icons/power-off.svg new file mode 100644 index 000000000..3dd5dcf20 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/power-off.svg @@ -0,0 +1,11 @@ + + Power Off + + diff --git a/packages/_components/src/elements/Icon/icons/reset.svg b/packages/_components/src/elements/Icon/icons/reset.svg new file mode 100644 index 000000000..8b2c9aa53 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/reset.svg @@ -0,0 +1,11 @@ + + Reset + + diff --git a/packages/_components/src/elements/Icon/icons/rotate-right.svg b/packages/_components/src/elements/Icon/icons/rotate-right.svg new file mode 100644 index 000000000..106284fa0 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/rotate-right.svg @@ -0,0 +1,11 @@ + + Rotate Right + + diff --git a/packages/_components/src/elements/Icon/icons/rotate.svg b/packages/_components/src/elements/Icon/icons/rotate.svg new file mode 100644 index 000000000..2dded38c4 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/rotate.svg @@ -0,0 +1,11 @@ + + Rotate + + diff --git a/packages/_components/src/elements/Icon/icons/search-plus.svg b/packages/_components/src/elements/Icon/icons/search-plus.svg new file mode 100644 index 000000000..f130654c5 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/search-plus.svg @@ -0,0 +1,14 @@ + + Search Plus + + + + + diff --git a/packages/_components/src/elements/Icon/icons/search.svg b/packages/_components/src/elements/Icon/icons/search.svg new file mode 100644 index 000000000..a313de17d --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/search.svg @@ -0,0 +1,11 @@ + + Search + + diff --git a/packages/_components/src/elements/Icon/icons/soft-tissue.svg b/packages/_components/src/elements/Icon/icons/soft-tissue.svg new file mode 100644 index 000000000..730c69450 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/soft-tissue.svg @@ -0,0 +1,16 @@ + + Soft Tissue + + diff --git a/packages/_components/src/elements/Icon/icons/sort-down.svg b/packages/_components/src/elements/Icon/icons/sort-down.svg new file mode 100644 index 000000000..6306b4884 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/sort-down.svg @@ -0,0 +1,11 @@ + + Sort Down + + diff --git a/packages/_components/src/elements/Icon/icons/sort-up.svg b/packages/_components/src/elements/Icon/icons/sort-up.svg new file mode 100644 index 000000000..ab5e0a836 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/sort-up.svg @@ -0,0 +1,11 @@ + + Sort Up + + diff --git a/packages/_components/src/elements/Icon/icons/sort.svg b/packages/_components/src/elements/Icon/icons/sort.svg new file mode 100644 index 000000000..1ade70182 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/sort.svg @@ -0,0 +1,11 @@ + + Sort + + diff --git a/packages/_components/src/elements/Icon/icons/square-o.svg b/packages/_components/src/elements/Icon/icons/square-o.svg new file mode 100644 index 000000000..16d36444c --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/square-o.svg @@ -0,0 +1,11 @@ + + Square Outline + + diff --git a/packages/_components/src/elements/Icon/icons/star.svg b/packages/_components/src/elements/Icon/icons/star.svg new file mode 100644 index 000000000..265a0e6bb --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/star.svg @@ -0,0 +1,11 @@ + + Star + + diff --git a/packages/_components/src/elements/Icon/icons/step-backward.svg b/packages/_components/src/elements/Icon/icons/step-backward.svg new file mode 100644 index 000000000..78a15fa7d --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/step-backward.svg @@ -0,0 +1,10 @@ + + + diff --git a/packages/_components/src/elements/Icon/icons/step-forward.svg b/packages/_components/src/elements/Icon/icons/step-forward.svg new file mode 100644 index 000000000..c42eabf65 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/step-forward.svg @@ -0,0 +1,10 @@ + + + diff --git a/packages/_components/src/elements/Icon/icons/stop.svg b/packages/_components/src/elements/Icon/icons/stop.svg new file mode 100644 index 000000000..9d124d325 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/stop.svg @@ -0,0 +1,11 @@ + + Stop + + diff --git a/packages/_components/src/elements/Icon/icons/sun.svg b/packages/_components/src/elements/Icon/icons/sun.svg new file mode 100644 index 000000000..08158098d --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/sun.svg @@ -0,0 +1,11 @@ + + Sun + + diff --git a/packages/_components/src/elements/Icon/icons/th-large.svg b/packages/_components/src/elements/Icon/icons/th-large.svg new file mode 100644 index 000000000..876289bc4 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/th-large.svg @@ -0,0 +1,12 @@ + + TH Large + + diff --git a/packages/_components/src/elements/Icon/icons/th-list.svg b/packages/_components/src/elements/Icon/icons/th-list.svg new file mode 100644 index 000000000..e69919bf8 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/th-list.svg @@ -0,0 +1,11 @@ + + TH List + + diff --git a/packages/_components/src/elements/Icon/icons/th.svg b/packages/_components/src/elements/Icon/icons/th.svg new file mode 100644 index 000000000..b1df15afa --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/th.svg @@ -0,0 +1,11 @@ + + TH + + diff --git a/packages/_components/src/elements/Icon/icons/times.svg b/packages/_components/src/elements/Icon/icons/times.svg new file mode 100644 index 000000000..27f875dce --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/times.svg @@ -0,0 +1,12 @@ + + Times + + diff --git a/packages/_components/src/elements/Icon/icons/trash.svg b/packages/_components/src/elements/Icon/icons/trash.svg new file mode 100644 index 000000000..20bca7e22 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/trash.svg @@ -0,0 +1,11 @@ + + Trash + + diff --git a/packages/_components/src/elements/Icon/icons/user.svg b/packages/_components/src/elements/Icon/icons/user.svg new file mode 100644 index 000000000..c55700132 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/user.svg @@ -0,0 +1,11 @@ + + User + + diff --git a/packages/_components/src/elements/Icon/icons/youtube.svg b/packages/_components/src/elements/Icon/icons/youtube.svg new file mode 100644 index 000000000..00db81c23 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/youtube.svg @@ -0,0 +1,11 @@ + + YouTube Logo + + diff --git a/packages/_components/src/elements/Icon/index.js b/packages/_components/src/elements/Icon/index.js new file mode 100644 index 000000000..3d83dbf3e --- /dev/null +++ b/packages/_components/src/elements/Icon/index.js @@ -0,0 +1,4 @@ +import { ICONS } from './getIcon.js'; +import Icon from './Icon.js'; + +export { Icon, ICONS }; diff --git a/packages/_components/src/elements/Svg/Svg.js b/packages/_components/src/elements/Svg/Svg.js new file mode 100644 index 000000000..41ea577f5 --- /dev/null +++ b/packages/_components/src/elements/Svg/Svg.js @@ -0,0 +1,14 @@ +import './Svg.styl'; + +import PropTypes from 'prop-types'; +import getSvg from './getSvg.js'; + +const Svg = props => { + return getSvg(props.name, props); +}; + +Svg.propTypes = { + name: PropTypes.string.isRequired, +}; + +export { Svg }; diff --git a/packages/_components/src/elements/Svg/Svg.styl b/packages/_components/src/elements/Svg/Svg.styl new file mode 100644 index 000000000..e69de29bb diff --git a/packages/_components/src/elements/Svg/__docs__/svg.mdx b/packages/_components/src/elements/Svg/__docs__/svg.mdx new file mode 100644 index 000000000..6fe66a7ee --- /dev/null +++ b/packages/_components/src/elements/Svg/__docs__/svg.mdx @@ -0,0 +1,57 @@ +--- +name: Svg +menu: Elements +route: /elements/svg +--- + +import { Playground, Props } from 'docz'; +import { State } from 'react-powerplug'; +import { Svg, SVGS } from './../index.js'; + +# Svg + +## Basic usage + + +
    + {Object.keys(SVGS) + .sort() + .map((name, i) => { + return ( + +
    + +
    + {name} +
    +
    +
    + ); + })} +
    +
    + +## API + + diff --git a/packages/_components/src/elements/Svg/getSvg.js b/packages/_components/src/elements/Svg/getSvg.js new file mode 100644 index 000000000..4933fc121 --- /dev/null +++ b/packages/_components/src/elements/Svg/getSvg.js @@ -0,0 +1,24 @@ +import React from 'react'; +// Svgs +import ohifLogoText from './svgs/ohif-logo-text.svg'; +import ohifLogoWrappedText from './svgs/ohif-logo-wrapped-text.svg'; + +const SVGS = { + 'ohif-logo-wrapped-text': ohifLogoWrappedText, + 'ohif-logo-text': ohifLogoText, +}; + +/** + * Return the matching SVG as a React Component. + * Results in an inlined SVG Element. If there's no match, + * return `null` + */ +export default function getSvg(key, props) { + if (!key || !SVGS[key]) { + return React.createElement('div', null, 'Missing SVG'); + } + + return React.createElement(SVGS[key], props); +} + +export { SVGS }; diff --git a/packages/_components/src/elements/Svg/index.js b/packages/_components/src/elements/Svg/index.js new file mode 100644 index 000000000..54a598b17 --- /dev/null +++ b/packages/_components/src/elements/Svg/index.js @@ -0,0 +1,2 @@ +export { Svg } from './Svg.js'; +export { SVGS } from './getSvg.js'; diff --git a/packages/_components/src/elements/Svg/svgs/ohif-logo-text.svg b/packages/_components/src/elements/Svg/svgs/ohif-logo-text.svg new file mode 100644 index 000000000..ffc43c748 --- /dev/null +++ b/packages/_components/src/elements/Svg/svgs/ohif-logo-text.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/_components/src/elements/Svg/svgs/ohif-logo-wrapped-text.svg b/packages/_components/src/elements/Svg/svgs/ohif-logo-wrapped-text.svg new file mode 100644 index 000000000..d9c2cb6b6 --- /dev/null +++ b/packages/_components/src/elements/Svg/svgs/ohif-logo-wrapped-text.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/packages/_components/src/elements/form/DropdownMenu.css b/packages/_components/src/elements/form/DropdownMenu.css new file mode 100644 index 000000000..a20017fa4 --- /dev/null +++ b/packages/_components/src/elements/form/DropdownMenu.css @@ -0,0 +1,86 @@ +.dd-menu { + float: left; + margin-right: 10px; + cursor: pointer; + position: relative; + z-index: 999; +} + +.dd-title { + font-size: 13px; +} + +.dd-menu-toggle { + display: inline-block; +} + +.dd-caret-down { + display: inline-block; + width: 0; + height: 0; + margin-top: 0.5rem; + margin-left: 0.5rem; + border-top: 5px solid; + border-right: 5px solid transparent; + border-left: 5px solid transparent; +} + +.dd-menu-list { + position: absolute; + top: 100%; + margin-top: 10px; + background: white; + border-radius: 5px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + -ms-border-radius: 5px; + -o-border-radius: 5px; + overflow: hidden; + transition: all 300ms ease; + -webkit-transition: all 300ms ease; + -moz-transition: all 300ms ease; + -ms-transition: all 300ms ease; + -o-transition: all 300ms ease; +} + +.dd-menu-list.open { + display: inline-block; +} + +.dd-menu-list.left { + left: 0; +} + +.dd-menu-list.right { + right: 0; +} + +.dd-menu-list.center { + left: 50%; + transform: translateX(-50%); + -webkit-transform: translateX(-50%); + -moz-transform: translateX(-50%); + -ms-transform: translateX(-50%); + -o-transform: translateX(-50%); +} + +.dd-item { + display: flex; + color: var(--text-color-active); + padding: 10px 15px; + border-bottom: 1px solid #ccc; +} + +.dd-item:hover { + text-decoration: none; + background: #eee; +} + +.dd-item:last-child { + border-bottom: none; +} + +.dd-item-icon { + margin-right: 10px; + margin-top: 2px; +} diff --git a/packages/_components/src/elements/form/DropdownMenu.js b/packages/_components/src/elements/form/DropdownMenu.js new file mode 100644 index 000000000..6a40f4088 --- /dev/null +++ b/packages/_components/src/elements/form/DropdownMenu.js @@ -0,0 +1,133 @@ +import './DropdownMenu.css'; + +import React, { Component } from 'react'; + +import { Icon } from '../Icon'; +import PropTypes from 'prop-types'; + +class DropdownMenu extends Component { + state = { + open: false, + }; + + static propTypes = { + titleElement: PropTypes.node, + title: PropTypes.string, + align: PropTypes.oneOf(['left', 'center', 'right']), + /** Items to render in the select's drop down */ + list: PropTypes.arrayOf( + PropTypes.shape({ + title: PropTypes.string.isRequired, + icon: PropTypes.object, + onClick: PropTypes.func, + link: PropTypes.string, + }) + ), + }; + + getListItems = () => { + const { list, align } = this.props; + + return list.map(({ icon, title, link, onClick }, key) => { + if (link) { + return ( + this.handleOnClick(onClick)} + > + {icon && } + {title} + + ); + } else { + return ( + + ); + } + }); + }; + + renderList = () => { + const { align } = this.props; + + if (!this.state.open) { + return null; + } + + return ( +
    + {this.getListItems()} +
    + ); + }; + + handleOnClick = onClick => { + this.toggleList(); + + if (onClick) { + onClick(); + } + }; + + handleMouseClick = e => { + if (this.node.contains(e.target)) { + return; + } + + this.toggleList(); + }; + + renderTitleElement = () => { + const { titleElement, title } = this.props; + + if (titleElement) { + return titleElement; + } + + return ( + + {title} + + + ); + }; + + toggleList = () => { + const { open } = this.state; + let state = true; + + document.addEventListener('mousedown', this.handleMouseClick, false); + + if (open) { + document.removeEventListener('mousedown', this.handleMouseClick, false); + state = false; + } + + this.setState({ + open: state, + }); + }; + + render() { + return ( +
    (this.node = node)}> +
    + {this.renderTitleElement()} +
    + + {this.renderList()} +
    + ); + } +} + +export { DropdownMenu }; diff --git a/packages/_components/src/elements/form/Label.css b/packages/_components/src/elements/form/Label.css new file mode 100644 index 000000000..6d0a170b1 --- /dev/null +++ b/packages/_components/src/elements/form/Label.css @@ -0,0 +1,8 @@ +.label-ohif { + font-size: 1em; + color: #ffffff; +} + +.label-example { + background-color: var(--ui-gray-darker); +} diff --git a/packages/_components/src/elements/form/Label.js b/packages/_components/src/elements/form/Label.js new file mode 100644 index 000000000..06086fbd6 --- /dev/null +++ b/packages/_components/src/elements/form/Label.js @@ -0,0 +1,27 @@ +import './Label.css'; + +import React from 'react'; + +import PropTypes from 'prop-types'; + +class Label extends React.Component { + constructor(props) { + super(props); + this.state = { value: this.props.text }; + } + + render() { + return ( + + ); + } +} + +Label.propTypes = { + text: PropTypes.string.isRequired, + for: PropTypes.string, +}; + +export { Label }; diff --git a/packages/_components/src/elements/form/Range.css b/packages/_components/src/elements/form/Range.css new file mode 100644 index 000000000..434a8486f --- /dev/null +++ b/packages/_components/src/elements/form/Range.css @@ -0,0 +1,64 @@ +.range { + margin: 0; + width: 100%; + -webkit-appearance: none; +} + +.range:focus { + outline: none; +} + +.range::-webkit-slider-runnable-track { + width: 100%; + height: 2px; + cursor: pointer; + box-shadow: none; + background-color: var(--ui-border-color-dark); + border-radius: 0px; + border: 0px solid var(--ui-border-color-dark); +} + +.range::-moz-range-track { + width: 100%; + height: 2px; + cursor: pointer; + box-shadow: none; + background-color: var(--ui-border-color-dark); + border-radius: 0px; + border: 0px solid var(--ui-border-color-dark); +} + +.range::-webkit-slider-thumb { + box-shadow: none; + border: 0px solid var(--active-color); + box-shadow: 0px 10px 10px rgba(0, 0, 0, 0.25); + height: 21px; + width: 11px; + border-radius: 11px; + background: var(--active-color); + cursor: pointer; + -webkit-appearance: none; + margin-top: -10px; +} + +.range::-moz-range-thumb { + box-shadow: none; + border: 0px solid var(--active-color); + box-shadow: 0px 10px 10px rgba(0, 0, 0, 0.25); + height: 21px; + width: 11px; + border-radius: 11px; + background: var(--active-color); + cursor: pointer; + -webkit-appearance: none; + margin-top: -10px; +} + +.range::-moz-focus-outer { + border: 0; +} + +.range-example { + background: black; + height: 2em; +} diff --git a/packages/_components/src/elements/form/Range.js b/packages/_components/src/elements/form/Range.js new file mode 100644 index 000000000..a8647a8fa --- /dev/null +++ b/packages/_components/src/elements/form/Range.js @@ -0,0 +1,40 @@ +import './Range.css'; + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +class Range extends React.Component { + constructor(props) { + super(props); + this.state = { value: props.value }; + } + + handleChange = event => { + this.setState({ value: event.target.value }); + if (this.props.onChange) this.props.onChange(); + }; + + render() { + return ( + + ); + } +} + +Range.propTypes = { + value: PropTypes.number, + min: PropTypes.number.isRequired, + max: PropTypes.number.isRequired, + id: PropTypes.string, + onChange: PropTypes.func, +}; + +export { Range }; diff --git a/packages/_components/src/elements/form/Select.css b/packages/_components/src/elements/form/Select.css new file mode 100644 index 000000000..0ea43c5a9 --- /dev/null +++ b/packages/_components/src/elements/form/Select.css @@ -0,0 +1,46 @@ +.select-ohif { + display: block; + font-family: Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', + sans-serif; + font-size: 0.8em; + font-weight: 600; + color: #444; + line-height: 1.3; + padding: 0.6em 1.4em 0.5em 0.8em; + width: 100%; + max-width: 100%; + box-sizing: border-box; + margin: 0; + border: 1px solid #aaa; + box-shadow: 0 1px 0 1px rgba(0, 0, 0, 0.04); + border-radius: 0.5em; + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; + background-color: #fff; + background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23007CB2%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E'), + linear-gradient(to bottom, #ffffff 0%, #e5e5e5 100%); + background-repeat: no-repeat, repeat; + background-position: right 0.7em top 50%, 0 0; + background-size: 0.65em auto, 100%; +} + +.select-ohif::-ms-expand { + display: none; +} + +.select-ohif:hover { + border-color: #888; +} + +.select-ohif:focus { + border-color: #aaa; + box-shadow: 0 0 1px 3px rgba(59, 153, 252, 0.7); + box-shadow: 0 0 0 3px -moz-mac-focusring; + color: #222; + outline: none; +} + +.select-ohif option { + font-weight: normal; +} diff --git a/packages/_components/src/elements/form/Select.js b/packages/_components/src/elements/form/Select.js new file mode 100644 index 000000000..5652f9a8c --- /dev/null +++ b/packages/_components/src/elements/form/Select.js @@ -0,0 +1,48 @@ +import './Select.css'; + +import React, { Component } from 'react'; + +import PropTypes from 'prop-types'; + +class Select extends Component { + constructor(props) { + super(props); + this.state = { value: this.props.value }; + } + + handleChange = event => { + this.setState({ value: event.target.value }); + if (this.props.onChange) this.props.onChange(); + }; + + render() { + return ( + + ); + } +} + +Select.propTypes = { + options: PropTypes.arrayOf( + PropTypes.shape({ + key: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + }) + ), + value: PropTypes.string, + onChange: PropTypes.func, +}; + +export { Select }; diff --git a/packages/_components/src/elements/form/TextArea.css b/packages/_components/src/elements/form/TextArea.css new file mode 100644 index 000000000..72e8b2860 --- /dev/null +++ b/packages/_components/src/elements/form/TextArea.css @@ -0,0 +1,7 @@ +.textarea-ohif { + background-color: #b6b6b6; + border-color: #b6b6b6; + font-family: Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', + sans-serif; + font-size: 1em; +} diff --git a/packages/_components/src/elements/form/TextArea.js b/packages/_components/src/elements/form/TextArea.js new file mode 100644 index 000000000..7f132f267 --- /dev/null +++ b/packages/_components/src/elements/form/TextArea.js @@ -0,0 +1,39 @@ +import './TextArea.css'; + +import React from 'react'; +import PropTypes from 'prop-types'; + +class TextArea extends React.Component { + constructor(props) { + super(props); + this.state = { value: this.props.value }; + } + + handleChange = event => { + this.setState({ value: event.target.value }); + if (this.props.onChange) this.props.onChange(); + }; + + render() { + return ( +