diff --git a/packages/ui/src/common/Button.tsx b/packages/ui/src/common/Button.tsx new file mode 100644 index 00000000..6eddd363 --- /dev/null +++ b/packages/ui/src/common/Button.tsx @@ -0,0 +1,143 @@ +/* + * + * Copyright (c) 2025 The Ontario Institute for Cancer Research. All rights reserved + * + * This program and the accompanying materials are made available under the terms of + * the GNU Affero General Public License v3.0. You should have received a copy of the + * GNU Affero General Public License along with this program. + * If not, see . + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT + * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +/** @jsxImportSource @emotion/react */ +// This is a slightly refactored version of the stage button +import React, { ReactNode } from 'react'; +import { css } from '@emotion/react'; + +import { Theme } from '../theme'; +import { useThemeContext } from '../theme/ThemeContext'; + +type ButtonProps = { + children?: ReactNode; + disabled?: boolean; + onClick?: ( + e: React.SyntheticEvent, + ) => any | ((e: React.SyntheticEvent) => Promise); + isAsync?: boolean; + className?: string; + isLoading?: boolean; + leftIcon?: ReactNode; + width?: string; +}; + +const getButtonContainerStyles = (theme: any, width?: string) => css` + display: flex; + flex-wrap: nowrap; + align-items: center; + justify-content: center; + gap: 11px; + width: ${width || 'auto'}; + min-width: fit-content; + padding: 8px 16px; + background-color: #f7f7f7; + color: ${theme.colors.black}; + border: 1px solid #beb2b294; + border-radius: 9px; + height: 42px; + box-sizing: border-box; + cursor: pointer; + position: relative; + transition: all 0.2s ease; + + &:hover { + background-color: ${theme.colors.grey_1}; + } + + &:disabled { + cursor: not-allowed; + opacity: 0.7; + } +`; + +const getContentStyles = (theme: Theme, shouldShowLoading: boolean) => css` + display: flex; + align-items: center; + gap: 8px; + ${theme.typography.button}; + color: inherit; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + visibility: ${shouldShowLoading ? 'hidden' : 'visible'}; +`; + +const getSpinnerStyles = (shouldShowLoading: boolean) => css` + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + visibility: ${shouldShowLoading ? 'visible' : 'hidden'}; +`; + +const getIconStyles = () => css` + display: flex; + align-items: center; +`; + +/** + * This is the generic button component used throughout stage, however it has + * the styling that is specific to the current theme that is being used. + */ +const Button = React.forwardRef( + ( + { + children, + onClick = () => {}, + disabled = false, + isAsync = false, + className, + isLoading: controlledLoading, + leftIcon, + width, + }: ButtonProps, + ref, + ) => { + const [internalLoading, setInternalLoading] = React.useState(false); + + const shouldShowLoading = !!controlledLoading || (internalLoading && isAsync); + const handleClick = async (event: React.SyntheticEvent) => { + setInternalLoading(true); + await onClick(event); + setInternalLoading(false); + }; + const theme = useThemeContext(); + const { Spinner } = theme.icons; + return ( + + ); + }, +); + +export default Button; diff --git a/packages/ui/src/theme/icons/Collapse.tsx b/packages/ui/src/theme/icons/Collapse.tsx new file mode 100644 index 00000000..f6d6eca6 --- /dev/null +++ b/packages/ui/src/theme/icons/Collapse.tsx @@ -0,0 +1,49 @@ +/* + * + * Copyright (c) 2025 The Ontario Institute for Cancer Research. All rights reserved + * + * This program and the accompanying materials are made available under the terms of + * the GNU Affero General Public License v3.0. You should have received a copy of the + * GNU Affero General Public License along with this program. + * If not, see . + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT + * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +/** @jsxImportSource @emotion/react */ + +import IconProps from './IconProps'; + +const Collapse = ({ fill, width, height, style }: IconProps) => { + return ( + + + + + + + + ); +}; + +export default Collapse; diff --git a/packages/ui/src/theme/icons/Eye.tsx b/packages/ui/src/theme/icons/Eye.tsx new file mode 100644 index 00000000..20797742 --- /dev/null +++ b/packages/ui/src/theme/icons/Eye.tsx @@ -0,0 +1,46 @@ +/* + * + * Copyright (c) 2025 The Ontario Institute for Cancer Research. All rights reserved + * + * This program and the accompanying materials are made available under the terms of + * the GNU Affero General Public License v3.0. You should have received a copy of the + * GNU Affero General Public License along with this program. + * If not, see . + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT + * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +/** @jsxImportSource @emotion/react */ + +import IconProps from './IconProps'; + +const Eye = ({ fill, width, height, style }: IconProps) => { + return ( + + + + + ); +}; + +export default Eye; diff --git a/packages/ui/src/theme/icons/Spinner.tsx b/packages/ui/src/theme/icons/Spinner.tsx new file mode 100644 index 00000000..6c0de99a --- /dev/null +++ b/packages/ui/src/theme/icons/Spinner.tsx @@ -0,0 +1,54 @@ +/* + * + * Copyright (c) 2025 The Ontario Institute for Cancer Research. All rights reserved + * + * This program and the accompanying materials are made available under the terms of + * the GNU Affero General Public License v3.0. You should have received a copy of the + * GNU Affero General Public License along with this program. + * If not, see . + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT + * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +/** @jsxImportSource @emotion/react */ + +import { css, keyframes } from '@emotion/react'; +import IconProps from './IconProps'; + +// Animation +const spin = keyframes` + 100% { + transform: rotate(360deg); + } +`; + +const Spinner = ({ fill, height, width }: IconProps) => { + return ( + + + + ); +}; + +export default Spinner; diff --git a/packages/ui/src/theme/styles/icons.ts b/packages/ui/src/theme/styles/icons.ts index d09423d3..2b107574 100644 --- a/packages/ui/src/theme/styles/icons.ts +++ b/packages/ui/src/theme/styles/icons.ts @@ -1,4 +1,10 @@ import ChevronDown from '../icons/ChevronDown'; +import Spinner from '../icons/Spinner'; +import Collapse from '../icons/Collapse'; +import Eye from '../icons/Eye'; export default { ChevronDown, + Spinner, + Collapse, + Eye, }; diff --git a/packages/ui/src/theme/styles/typography.ts b/packages/ui/src/theme/styles/typography.ts index e02454de..e4df0422 100644 --- a/packages/ui/src/theme/styles/typography.ts +++ b/packages/ui/src/theme/styles/typography.ts @@ -35,11 +35,11 @@ const regular = css` const button = css` ${baseFont} - font-size: 16px; - font-weight: bold; + font-size: 20px; + font-weight: 700; font-style: normal; font-stretch: normal; - line-height: 18px; + line-height: 100%; letter-spacing: normal; `; diff --git a/packages/ui/src/viewer-table/InteractionPanel/CollapseAllButton.tsx b/packages/ui/src/viewer-table/InteractionPanel/CollapseAllButton.tsx new file mode 100644 index 00000000..604b9fba --- /dev/null +++ b/packages/ui/src/viewer-table/InteractionPanel/CollapseAllButton.tsx @@ -0,0 +1,20 @@ +import { useEffect } from 'react'; +import Button from '../../common/Button'; +import { useThemeContext } from '../../theme/ThemeContext'; + +export interface CollapseAllButtonProps { + onClick: () => void; +} + +const CollapseAllButton = ({ onClick }: CollapseAllButtonProps) => { + const theme = useThemeContext(); + const { Collapse } = theme.icons; + + return ( + + ); +}; + +export default CollapseAllButton; diff --git a/packages/ui/src/viewer-table/InteractionPanel/ExpandAllButton.tsx b/packages/ui/src/viewer-table/InteractionPanel/ExpandAllButton.tsx new file mode 100644 index 00000000..42a81ec3 --- /dev/null +++ b/packages/ui/src/viewer-table/InteractionPanel/ExpandAllButton.tsx @@ -0,0 +1,20 @@ +import { useEffect } from 'react'; +import Button from '../../common/Button'; +import { useThemeContext } from '../../theme/ThemeContext'; + +export interface ExpandAllButtonProps { + onClick: () => void; +} + +const ExpandAllButton = ({ onClick }: ExpandAllButtonProps) => { + const theme = useThemeContext(); + const { Eye } = theme.icons; + + return ( + + ); +}; + +export default ExpandAllButton; diff --git a/packages/ui/stories/common/Button.stories.tsx b/packages/ui/stories/common/Button.stories.tsx new file mode 100644 index 00000000..65578fdb --- /dev/null +++ b/packages/ui/stories/common/Button.stories.tsx @@ -0,0 +1,28 @@ +/** @jsxImportSource @emotion/react */ + +import type { Meta, StoryObj } from '@storybook/react'; +import themeDecorator from '../themeDecorator'; +import Button from '../../src/common/Button'; + +const meta = { + component: Button, + title: 'Common/Button', + tags: ['autodocs'], + decorators: [themeDecorator()], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { children: 'Click Me', onClick: () => alert('I have been clicked'), className: 'my-button', leftIcon: '👍' }, +}; +export const Disabled: Story = { + args: { children: 'Disabled', disabled: true }, +}; +export const Loading: Story = { + args: { isLoading: true, children: 'Loading...' }, +}; +export const Empty: Story = { + args: {}, +}; diff --git a/packages/ui/stories/viewer-table/interaction-panel/CollapseAllButton.stories.tsx b/packages/ui/stories/viewer-table/interaction-panel/CollapseAllButton.stories.tsx new file mode 100644 index 00000000..5e744b48 --- /dev/null +++ b/packages/ui/stories/viewer-table/interaction-panel/CollapseAllButton.stories.tsx @@ -0,0 +1,19 @@ +/** @jsxImportSource @emotion/react */ + +import type { Meta, StoryObj } from '@storybook/react'; +import themeDecorator from '../../themeDecorator'; +import CollapseAllButton from '../../../src/viewer-table/InteractionPanel/CollapseAllButton'; + +const meta = { + component: CollapseAllButton, + title: 'Viewer - Table/Interaction - Panel/CollapseAllButton', + tags: ['autodocs'], + decorators: [themeDecorator()], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { setIsCollapsed: (isCollapsed: boolean) => alert('all collapsable components are collapsed') }, +}; diff --git a/packages/ui/stories/viewer-table/interaction-panel/ExpandAllButton.stories.tsx b/packages/ui/stories/viewer-table/interaction-panel/ExpandAllButton.stories.tsx new file mode 100644 index 00000000..fff42a43 --- /dev/null +++ b/packages/ui/stories/viewer-table/interaction-panel/ExpandAllButton.stories.tsx @@ -0,0 +1,18 @@ +/** @jsxImportSource @emotion/react */ + +import type { Meta, StoryObj } from '@storybook/react'; +import themeDecorator from '../../themeDecorator'; +import ExpandAllButton from '../../../src/viewer-table/InteractionPanel/ExpandAllButton'; +const meta = { + component: ExpandAllButton, + title: 'Viewer - Table/Interaction - Panel/ExpandAllButton', + tags: ['autodocs'], + decorators: [themeDecorator()], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { setIsCollapsed: (isCollapsed: boolean) => alert('all collapsable components are expanded') }, +};