diff --git a/packages/ui/src/common/Dropdown/Dropdown.tsx b/packages/ui/src/common/Dropdown/Dropdown.tsx index 3ee365cc..648d6f23 100644 --- a/packages/ui/src/common/Dropdown/Dropdown.tsx +++ b/packages/ui/src/common/Dropdown/Dropdown.tsx @@ -31,24 +31,19 @@ const dropdownButtonStyle = (theme: Theme, width?: string) => css` display: flex; flex-wrap: nowrap; align-items: center; - justify-content: space-between; + justify-content: center; gap: 11px; - min-width: ${width || '200px'}; - max-width: 400px; - width: 100%; - padding: 8px; + width: ${width || 'auto'}; + min-width: fit-content; + padding: 8px 16px; background-color: #f7f7f7; - color: ${theme.colors.black}; + color: ${theme.colors.accent_dark}; border: 1px solid #beb2b294; border-radius: 9px; - font-size: 14px; - max-height: 42px; + height: 42px; + box-sizing: border-box; cursor: pointer; - transition: background-color 0.2s ease; - - &:hover { - background-color: ${theme.colors.grey_1}; - } + transition: all 0.2s ease; `; const parentStyle = css` @@ -60,29 +55,22 @@ const chevronStyle = (open: boolean) => css` transform: ${open ? 'rotate(180deg)' : 'none'}; transition: transform 0.2s ease; `; -const dropDownTitleStyle = (theme: any) => css` - padding: 5px 10px; - font-weight: 400; - line-height: 100%; - letter-spacing: 0%; + +const dropDownTitleStyle = (theme: Theme) => css` + ${theme.typography?.button}; color: ${theme.colors.accent_dark}; `; const dropdownMenuStyle = (theme: Theme) => css` + ${theme.typography?.button}; position: absolute; top: calc(100% + 5px); - left: 0; width: 100%; background-color: #f7f7f7; - border: 1px solid ${theme.colors?.grey_1}; - border-radius: 4px; - box-shadow: - 0 1px 6px rgba(0, 0, 0, 0.1), - 0 1px 5px rgba(0, 0, 0, 0.08); - list-style: none; - padding: 4px 0; - margin: 0; - z-index: 1000; + border: 1px solid #beb2b294; + padding-top: 5px; + border-radius: 9px; + padding-bottom: 5px; `; type MenuItem = { @@ -103,8 +91,6 @@ const Dropdown = ({ menuItems = [], title, leftIcon }: DropDownProps) => { const { ChevronDown } = theme.icons; - const hasMenuItems = menuItems.length > 0; - useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { @@ -138,10 +124,11 @@ const Dropdown = ({ menuItems = [], title, leftIcon }: DropDownProps) => {
{leftIcon} {title} - +
- {open && } + {/* {open && <>{renderMenuItems()}} */} + {open &&
{renderMenuItems()}
} ); diff --git a/packages/ui/src/common/Dropdown/DropdownItem.tsx b/packages/ui/src/common/Dropdown/DropdownItem.tsx index 342aacec..49f7e635 100644 --- a/packages/ui/src/common/Dropdown/DropdownItem.tsx +++ b/packages/ui/src/common/Dropdown/DropdownItem.tsx @@ -37,21 +37,18 @@ type DropDownItemProps = { const styledListItemStyle = (theme: Theme, customStyles?: any) => css` display: flex; - max-height: 42px; min-height: 100%; + padding-bottom: 5px; height: 100%; align-items: center; - padding: 8px; + border-radius: 9px; justify-content: center; - color: ${theme.colors?.black}; - background-color: #f7f7f7; - border: 1px solid ${theme.colors.grey_1}; - text-decoration: none; + color: ${theme.colors.accent_dark}; cursor: pointer; - border: none; &:hover { - background-color: ${theme.colors.grey_2}; + background-color: ${theme.colors.grey_1}; } + ${customStyles?.base} `; @@ -60,9 +57,9 @@ const DropDownItem = ({ children, action, customStyles }: DropDownItemProps) => const content =
{children}
; if (typeof action === 'function') { return ( - +
{children} - +
); } diff --git a/packages/ui/src/theme/icons/List.tsx b/packages/ui/src/theme/icons/List.tsx new file mode 100644 index 00000000..1beeaa26 --- /dev/null +++ b/packages/ui/src/theme/icons/List.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 } from '@emotion/react'; + +import IconProps from './IconProps'; + +const List = ({ fill, width, height, style }: IconProps) => { + return ( + + + + + + + + + ); +}; + +export default List; diff --git a/packages/ui/src/theme/styles/icons.ts b/packages/ui/src/theme/styles/icons.ts index d09423d3..ca4167f4 100644 --- a/packages/ui/src/theme/styles/icons.ts +++ b/packages/ui/src/theme/styles/icons.ts @@ -1,4 +1,6 @@ import ChevronDown from '../icons/ChevronDown'; +import List from '../icons/List'; export default { ChevronDown, + List, }; 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/TableOfContentsDropdown.tsx b/packages/ui/src/viewer-table/InteractionPanel/TableOfContentsDropdown.tsx new file mode 100644 index 00000000..968f3416 --- /dev/null +++ b/packages/ui/src/viewer-table/InteractionPanel/TableOfContentsDropdown.tsx @@ -0,0 +1,53 @@ +/* + * + * 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. + * + */ + +import type { Schema } from '@overture-stack/lectern-dictionary'; +import React from 'react'; +import Dropdown from '../../common/Dropdown/Dropdown'; +import { useThemeContext } from '../../theme/ThemeContext'; + +export type TableOfContentsDropdownProps = { + schemas: Schema[]; + onSelect: (schemaIndex: number) => void; +}; + +const TableOfContentsDropdown = ({ schemas, onSelect }: TableOfContentsDropdownProps) => { + const theme = useThemeContext(); + const { List } = theme.icons; + const handleAction = (index: number) => { + const anchorId = `#${index}`; + onSelect(index); + window.location.hash = anchorId; + }; + + const menuItemsFromSchemas = schemas.map((schema, index) => ({ + label: schema.name, + action: () => { + handleAction(index); + }, + })); + + return schemas.length > 0 ? + } title="Table of Contents" menuItems={menuItemsFromSchemas} /> + : null; +}; + +export default TableOfContentsDropdown; diff --git a/packages/ui/stories/fixtures/advanced.json b/packages/ui/stories/fixtures/advanced.json new file mode 100644 index 00000000..2b77d1bc --- /dev/null +++ b/packages/ui/stories/fixtures/advanced.json @@ -0,0 +1,35 @@ +{ + "name": "Advanced", + "version": "1.0", + "schemas": [ + { + "name": "empty", + "description": "An empty schema with no fields.", + "fields": [] + }, + { + "name": "single_field", + "description": "A schema with a single field of type string.", + "fields": [{ "name": "single_string_field", "valueType": "string" }] + }, + { + "name": "multiple_fields", + "description": "A schema with multiple fields of different types.", + "fields": [ + { "name": "string_field", "valueType": "string" }, + { "name": "integer_field", "valueType": "integer" }, + { "name": "boolean_field", "valueType": "boolean" } + ] + }, + { + "name": "primitives", + "description": "Includes one field of each primitive type without any restrictions. No Frills.", + "fields": [ + { "name": "boolean_field", "valueType": "boolean" }, + { "name": "integer_field", "valueType": "integer" }, + { "name": "number_field", "valueType": "number" }, + { "name": "string_field", "valueType": "string" } + ] + } + ] +} diff --git a/packages/ui/stories/viewer-table/interaction-panel/TableOfContentDropdown.stories.tsx b/packages/ui/stories/viewer-table/interaction-panel/TableOfContentDropdown.stories.tsx new file mode 100644 index 00000000..d11b24f0 --- /dev/null +++ b/packages/ui/stories/viewer-table/interaction-panel/TableOfContentDropdown.stories.tsx @@ -0,0 +1,37 @@ +/** @jsxImportSource @emotion/react */ + +import { Schema } from '@overture-stack/lectern-dictionary'; +import type { Meta, StoryObj } from '@storybook/react'; +import Dictionary from '../../fixtures/advanced.json'; +import TableOfContentsDropdown from '../../../src/viewer-table/InteractionPanel/TableOfContentsDropdown'; +import themeDecorator from '../../themeDecorator'; + +const meta = { + component: TableOfContentsDropdown, + title: 'Viewer - Table/Interaction - Panel/Table of Contents Dropdown', + tags: ['autodocs'], + decorators: [themeDecorator()], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Using the primitiveJson as a mock schema for demonstration purposes +const schemas: Schema[] = Dictionary.schemas as Schema[]; + +// Mock functions for the story just to demonstrate interaction + +const onSelect = (schemaIndex: number) => { + alert(`Accordion has been toggled for the following schema: ${schemas[schemaIndex].name}`); +}; + +export const Default: Story = { + args: { + schemas: schemas, + onSelect, + }, +}; + +export const Empty: Story = { + args: { schemas: [], onSelect: () => {} }, +};