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: () => {} },
+};