diff --git a/packages/ui/src/common/Accordion/Accordion.tsx b/packages/ui/src/common/Accordion/Accordion.tsx index 8dd91877..06bd6277 100644 --- a/packages/ui/src/common/Accordion/Accordion.tsx +++ b/packages/ui/src/common/Accordion/Accordion.tsx @@ -22,19 +22,19 @@ /** @jsxImportSource @emotion/react */ import { css } from '@emotion/react'; -import { ReactNode, useMemo, useState } from 'react'; +import { ReactNode, useEffect, useMemo, useState } from 'react'; import AccordionItem from './AccordionItem'; export type AccordionData = { title: string; - openOnInit: boolean; description?: string; content: ReactNode; schemaName: string; }; export type AccordionProps = { accordionItems: Array; + collapseAll: boolean; }; export type AccordionOpenState = { @@ -93,10 +93,16 @@ const accordionStyle = css` display: flex; flex-direction: column; gap: 24px; + cursor: pointer; `; -const Accordion = ({ accordionItems }: AccordionProps) => { - const [openStates, setOpenStates] = useState(accordionItems.map((item) => item.openOnInit)); +const Accordion = ({ accordionItems, collapseAll }: AccordionProps) => { + const [openStates, setOpenStates] = useState(accordionItems.map(() => collapseAll)); + + useEffect(() => { + setOpenStates(accordionItems.map(() => collapseAll)); + }, [collapseAll]); + const handleToggle = (index: number) => { setOpenStates((prev) => prev.map((isOpen, i) => (i === index ? !isOpen : isOpen))); }; diff --git a/packages/ui/src/common/Accordion/AccordionItem.tsx b/packages/ui/src/common/Accordion/AccordionItem.tsx index bdb5d7f3..c0f49bdf 100644 --- a/packages/ui/src/common/Accordion/AccordionItem.tsx +++ b/packages/ui/src/common/Accordion/AccordionItem.tsx @@ -47,11 +47,11 @@ const accordionItemStyle = (theme: Theme) => css` overflow: hidden; background-color: #ffffff; box-shadow: - 0 2px 6px rgba(70, 63, 63, 0.05), + 0 2px 6px rgba(70, 63, 63, 0.3), 0 0 0 0.3px ${theme.colors.black}; &:hover { box-shadow: - 0 2px 6px rgba(70, 63, 63, 0.15), + 0 2px 6px rgba(70, 63, 63, 0.5), 0 0 0 0.3px ${theme.colors.black}; } transition: all 0.3s ease; @@ -71,34 +71,35 @@ const accordionItemButtonStyle = (theme: Theme) => css` display: flex; border: none; align-items: center; - color: ${theme.colors.accent_dark}; cursor: pointer; - ${theme.typography?.button}; - text-align: left; background: transparent; - padding: 8px 0; - flex: 1; + padding: 8px 0px; +`; +const titleStyle = (theme: Theme) => css` + ${theme.typography?.subtitleSecondary}; + color: ${theme.colors.accent_dark}; + text-align: left; `; const chevronStyle = (isOpen: boolean) => css` transform: ${isOpen ? 'rotate(0deg)' : 'rotate(-90deg)'}; transition: transform 0.2s ease; margin-right: 12px; - flex-shrink: 0; `; const contentContainerStyle = css` display: flex; flex-direction: column; align-items: flex-start; - flex: 1; `; const titleRowStyle = css` display: flex; + gap: 2px; align-items: center; - width: 100%; margin-bottom: 10px; + flex-direction: auto; + flex-wrap: wrap; `; const hashIconStyle = (theme: Theme) => css` @@ -107,27 +108,19 @@ const hashIconStyle = (theme: Theme) => css` background: transparent; border: none; cursor: pointer; - padding: 0; - svg { border-bottom: 2px solid ${theme.colors.secondary}; } - &:hover { opacity: 1; } `; const descriptionWrapperStyle = (theme: Theme) => css` - ${theme.typography?.label2}; - color: ${theme.colors.grey_5}; + ${theme.typography.paragraphSmall}; + color: ${theme.colors.black}; overflow-wrap: break-word; - width: 100%; -`; - -const downloadButtonContainerStyle = css` - flex-shrink: 0; - margin-right: 8px; + margin-left: 16px; `; const accordionCollapseStyle = (isOpen: boolean) => css` @@ -137,7 +130,7 @@ const accordionCollapseStyle = (isOpen: boolean) => css` `; const accordionItemContentStyle = css` - padding: 30px; + padding: 0px 30px 30px 30px; `; const contentInnerContainerStyle = (theme: Theme) => css` @@ -148,16 +141,15 @@ const contentInnerContainerStyle = (theme: Theme) => css` const handleInitialHashCheck = ( windowLocationHash: string, - accordionData: AccordionData, openState: AccordionOpenState, indexString: string, accordionRef: RefObject, ) => { if (window.location.hash === windowLocationHash) { - if (!accordionData.openOnInit) { - openState.toggle(); - } - accordionRef.current?.id === indexString ? accordionRef.current.scrollIntoView({ behavior: 'smooth' }) : null; + openState.toggle(); + accordionRef.current?.id === indexString ? + accordionRef.current.scrollIntoView({ block: 'center', behavior: 'smooth' }) + : null; } }; @@ -167,8 +159,10 @@ const hashOnClick = ( setClipboardContents: (currentSchema: string) => void, ) => { event.stopPropagation(); - window.location.hash = windowLocationHash; - setClipboardContents(window.location.href); + event.preventDefault(); + setClipboardContents( + `${window.location.origin}${window.location.pathname}${window.location.search}${windowLocationHash}`, + ); }; const AccordionItem = ({ index, accordionData, openState }: AccordionItemProps) => { @@ -182,7 +176,9 @@ const AccordionItem = ({ index, accordionData, openState }: AccordionItemProps) const windowLocationHash = `#${index}`; useEffect(() => { - handleInitialHashCheck(windowLocationHash, accordionData, openState, indexString, accordionRef); + setTimeout(() => { + handleInitialHashCheck(windowLocationHash, openState, indexString, accordionRef); + }, 100); }, []); return ( @@ -190,15 +186,15 @@ const AccordionItem = ({ index, accordionData, openState }: AccordionItemProps)
- + {title} + + {description} +
- - {description} -
-
+
{/* Mock props for the dictionary since we haven't implemented the download per schema yet */} css` display: flex; align-items: center; gap: 8px; - ${theme.typography.button}; + ${theme.typography.subtitleSecondary}; color: inherit; white-space: nowrap; overflow: hidden; diff --git a/packages/ui/src/common/Dropdown/Dropdown.tsx b/packages/ui/src/common/Dropdown/Dropdown.tsx index cf3b2a75..5d24e656 100644 --- a/packages/ui/src/common/Dropdown/Dropdown.tsx +++ b/packages/ui/src/common/Dropdown/Dropdown.tsx @@ -68,12 +68,13 @@ const chevronStyle = (open: boolean) => css` `; const dropDownTitleStyle = (theme: Theme) => css` - ${theme.typography?.button}; + ${theme.typography?.subtitleSecondary}; color: ${theme.colors.accent_dark}; `; const dropdownMenuStyle = (theme: Theme) => css` - ${theme.typography?.button}; + all: unset; + ${theme.typography?.subtitleSecondary}; position: absolute; top: calc(100% + 5px); width: 100%; @@ -82,6 +83,9 @@ const dropdownMenuStyle = (theme: Theme) => css` padding-top: 5px; border-radius: 9px; padding-bottom: 5px; + z-index: 100; + max-height: 150px; + overflow-y: auto; `; type MenuItem = { @@ -128,7 +132,7 @@ const Dropdown = ({ menuItems = [], title, leftIcon, disabled = false }: DropDow const renderMenuItems = () => { return menuItems.map(({ label, action }) => ( - + setOpen(false)}> {label} )); @@ -136,15 +140,22 @@ const Dropdown = ({ menuItems = [], title, leftIcon, disabled = false }: DropDow return (
-
-
- {leftIcon} - {title} - -
- - {open && !disabled &&
{renderMenuItems()}
} -
+ + {open && !disabled && ( + + {renderMenuItems()} + + )}
); }; diff --git a/packages/ui/src/common/Dropdown/DropdownItem.tsx b/packages/ui/src/common/Dropdown/DropdownItem.tsx index d479130d..047b2d87 100644 --- a/packages/ui/src/common/Dropdown/DropdownItem.tsx +++ b/packages/ui/src/common/Dropdown/DropdownItem.tsx @@ -22,13 +22,15 @@ /** @jsxImportSource @emotion/react */ import { css, SerializedStyles } from '@emotion/react'; -import { ReactNode } from 'react'; +import React, { ReactNode } from 'react'; + import type { Theme } from '../../theme'; import { useThemeContext } from '../../theme/ThemeContext'; type DropDownItemProps = { action?: string | (() => void); children: ReactNode; + onItemClick?: () => void; customStyles?: { hover?: SerializedStyles; base?: SerializedStyles; @@ -38,8 +40,7 @@ type DropDownItemProps = { const styledListItemStyle = (theme: Theme, customStyles?: any) => css` display: flex; min-height: 100%; - padding-bottom: 5px; - height: 100%; + padding: 10px; align-items: center; border-radius: 9px; justify-content: center; @@ -52,18 +53,31 @@ const styledListItemStyle = (theme: Theme, customStyles?: any) => css` ${customStyles?.base} `; -const DropDownItem = ({ children, action, customStyles }: DropDownItemProps) => { +const DropDownItem = ({ children, action, onItemClick, customStyles }: DropDownItemProps) => { const theme = useThemeContext(); - const content =
{children}
; + + const handleClick = () => { + if (typeof action === 'function') { + action(); + } + if (onItemClick) { + onItemClick(); + } + }; + if (typeof action === 'function') { return ( -
+
  • {children} -
  • + ); } - return content; + return ( +
  • + {children} +
  • + ); }; export default DropDownItem; diff --git a/packages/ui/src/common/ReadMoreText.tsx b/packages/ui/src/common/ReadMoreText.tsx index 876574e9..16a3131e 100644 --- a/packages/ui/src/common/ReadMoreText.tsx +++ b/packages/ui/src/common/ReadMoreText.tsx @@ -37,16 +37,16 @@ export type ReadMoreTextProps = { }; const defaultWrapperStyle = (theme: Theme) => css` - ${theme.typography?.label2}; - color: ${theme.colors.grey_5}; + ${theme.typography.paragraphSmall}; + color: ${theme.colors.black}; padding: 4px 8px; word-wrap: break-word; overflow-wrap: break-word; `; const linkStyle = (theme: Theme) => css` - ${theme.typography?.label2}; - color: ${theme.colors.accent_dark}; + ${theme.typography.captionBold}; + color: ${theme.colors.black}; cursor: pointer; display: inline-flex; align-items: center; diff --git a/packages/ui/src/theme/styles/typography.ts b/packages/ui/src/theme/styles/typography.ts index e4df0422..63f8332f 100644 --- a/packages/ui/src/theme/styles/typography.ts +++ b/packages/ui/src/theme/styles/typography.ts @@ -23,6 +23,69 @@ const baseFont = css` font-family: 'Lato', sans-serif; `; +const baseFontSecondary = css` + font-family: 'Open Sans', sans-serif; +`; + +const hero = css` + ${baseFont} + font-size: 40px; + font-weight: 400; + font-style: normal; + font-stretch: normal; + line-height: 100%; + letter-spacing: 0%; +`; + +const title = css` + ${baseFont} + font-size: 26px; + font-weight: 400; + font-style: normal; + font-stretch: normal; + line-height: 100%; + letter-spacing: 0%; +`; + +const subtitle = css` + ${baseFont} + font-size: 30px; + font-weight: 400; + font-style: normal; + font-stretch: normal; + line-height: 100%; + letter-spacing: 0%; +`; + +const subtitleSecondary = css` + ${baseFont} + font-size: 20px; + font-weight: 700; + font-style: normal; + font-stretch: normal; + line-height: 100%; + letter-spacing: 0%; +`; + +const introText = css` + ${baseFontSecondary} + font-size: 18px; + font-weight: 400; + font-style: normal; + font-stretch: normal; + line-height: 100%; + letter-spacing: 0%; +`; +const introTextBold = css` + ${baseFontSecondary} + font-size: 18px; + font-weight: 700; + font-style: normal; + font-stretch: normal; + line-height: 100%; + letter-spacing: 0%; +`; + const regular = css` ${baseFont} font-size: inherit; @@ -33,83 +96,99 @@ const regular = css` letter-spacing: inherit; `; -const button = css` - ${baseFont} - font-size: 20px; +const paragraph = css` + ${baseFontSecondary} + font-size: 26px; + font-weight: 400; + font-style: normal; + font-stretch: normal; + line-height: 100%; + letter-spacing: 0%; +`; + +const paragraphBold = css` + ${baseFontSecondary} + font-size: 26px; font-weight: 700; font-style: normal; font-stretch: normal; line-height: 100%; - letter-spacing: normal; + letter-spacing: 0%; `; -const heading = css` - ${baseFont} - font-size: 18px; - font-weight: bold; +const paragraphSmall = css` + ${baseFontSecondary} + font-size: 16px; + font-weight: 400; font-style: normal; font-stretch: normal; - line-height: 30px; - letter-spacing: normal; + line-height: 100%; + letter-spacing: 0%; `; -const subheading = css` - ${baseFont} +const paragraphSmallBold = css` + ${baseFontSecondary} font-size: 16px; - font-weight: bold; + font-weight: 700; font-style: normal; font-stretch: normal; - line-height: 24px; - letter-spacing: normal; + line-height: 100%; + letter-spacing: 0%; `; -const subheading2 = css` - ${baseFont} - font-size: 14px; - font-weight: bold; +const data = css` + ${baseFontSecondary} + font-size: 13px; + font-weight: 400; font-style: normal; font-stretch: normal; - line-height: 16px; - letter-spacing: normal; + line-height: 100%; + letter-spacing: 0%; `; -const label = css` - ${baseFont} - font-size: 12px; - font-weight: bold; +const dataBold = css` + ${baseFontSecondary} + font-size: 13px; + font-weight: 700; font-style: normal; font-stretch: normal; - line-height: 16px; - letter-spacing: normal; + line-height: 100%; + letter-spacing: 0%; `; -const label2 = css` - ${baseFont} - font-size: 10px; - font-weight: normal; +const caption = css` + ${baseFontSecondary} + font-size: 11px; + font-weight: 400; font-style: normal; font-stretch: normal; - line-height: 14px; - letter-spacing: normal; + line-height: 100%; + letter-spacing: 0%; `; -const data = css` - ${baseFont} - font-size: 13px; - font-weight: normal; +const captionBold = css` + ${baseFontSecondary} + font-size: 11px; + font-weight: 700; font-style: normal; font-stretch: normal; - line-height: 16px; - letter-spacing: normal; + line-height: 100%; + letter-spacing: 0%; `; export default { + hero, + title, + subtitle, + subtitleSecondary, + introText, regular, - heading, - subheading, - subheading2, - label, - label2, + paragraph, + paragraphBold, + paragraphSmall, + paragraphSmallBold, data, - button, + dataBold, + caption, + captionBold, }; diff --git a/packages/ui/src/viewer-table/DataDictionaryPage.tsx b/packages/ui/src/viewer-table/DataDictionaryPage.tsx new file mode 100644 index 00000000..d684baac --- /dev/null +++ b/packages/ui/src/viewer-table/DataDictionaryPage.tsx @@ -0,0 +1,131 @@ +/* + * + * 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 { Dictionary, Schema, SchemaField } from '@overture-stack/lectern-dictionary'; +import React, { useState } from 'react'; +import Accordion, { AccordionData } from '../common/Accordion/Accordion'; +import SchemaTable from './DataTable/SchemaTable/SchemaTable'; +import DictionaryHeader from './DictionaryHeader'; +import { FilterOptions } from './InteractionPanel/AttributeFilterDropdown'; +import InteractionPanel from './InteractionPanel/InteractionPanel'; + +const interactionPanelSpacing = css` + margin-bottom: 24px; +`; + +export type DataDictionaryPageProps = { + dictionaries: Dictionary[]; + dictionaryIndex?: number; + lecternUrl?: string; + onVersionChange?: (index: number) => void; +}; + +const DataDictionaryPage = ({ + dictionaries, + dictionaryIndex = 0, + lecternUrl = '', + onVersionChange, +}: DataDictionaryPageProps) => { + const [filters, setFilters] = useState([]); + const [isCollapsed, setIsCollapsed] = useState(true); + + const dictionary = dictionaries[dictionaryIndex]; + + const handleSelect = (schemaIndex: number) => { + // All we need to do is hook up to the accordion's scroll into view, since that is already implemented + }; + + const handleVersionChange = (index: number) => { + if (onVersionChange) { + onVersionChange(index); + } + }; + + const displayData = (data: Dictionary[], filters: FilterOptions[], dictionaryIndex: number) => { + const currentDictionary = data?.[dictionaryIndex]; + // If the filter is not active or we just have nothing to filter, return the original data + if (!filters?.length) { + return currentDictionary; + } + return { + ...currentDictionary, + schemas: currentDictionary?.schemas?.map((schema: Schema) => ({ + ...schema, + fields: schema.fields.filter((field: SchemaField) => { + // we are going to filter via the constraints that are given + if (filters?.includes('Required')) { + const restrictions = field?.restrictions ?? []; + if ('required' in restrictions && typeof restrictions !== 'function') { + return restrictions.required === true; + } + return false; + } + return filters?.includes('All Fields'); + }), + })), + }; + }; + + const filteredDictionary = displayData(dictionaries, filters, dictionaryIndex); + + const accordionItems: AccordionData[] = + filteredDictionary?.schemas?.map((schema) => ({ + title: schema.name, + description: schema.description, + content: , + schemaName: 'schemaName', + })) || []; + + return ( +
    +
    + +
    + +
    +
    + +
    + ); +}; + +export default DataDictionaryPage; diff --git a/packages/ui/src/viewer-table/DataTable/SchemaTable/Columns/Fields.tsx b/packages/ui/src/viewer-table/DataTable/SchemaTable/Columns/Fields.tsx index 3276327c..73d3e4d5 100644 --- a/packages/ui/src/viewer-table/DataTable/SchemaTable/Columns/Fields.tsx +++ b/packages/ui/src/viewer-table/DataTable/SchemaTable/Columns/Fields.tsx @@ -20,12 +20,7 @@ /** @jsxImportSource @emotion/react */ import { css } from '@emotion/react'; -import { - DictionaryMeta, - type ForeignKeyRestriction, - SchemaField, - SchemaRestrictions, -} from '@overture-stack/lectern-dictionary'; +import { DictionaryMeta, SchemaField, SchemaRestrictions } from '@overture-stack/lectern-dictionary'; import { Row } from '@tanstack/react-table'; import { useMemo, useState } from 'react'; @@ -36,14 +31,13 @@ import { useThemeContext } from '../../../../theme/ThemeContext'; const fieldContainerStyle = css` display: flex; flex-direction: column; - gap: 3px; + gap: 10px; `; const fieldNameStyle = (theme: Theme) => css` - ${theme.typography.label} + ${theme.typography.introText} display: flex; align-items: center; - gap: 2px; `; export type FieldExamplesProps = { @@ -137,7 +131,8 @@ export const FieldsColumn = ({ fieldRow }: FieldColumnProps) => { return (
    - {fieldName} {Array.isArray(uniqueKey) && uniqueKey.length === 1 && Primary Key} + {fieldName}{' '} + {Array.isArray(uniqueKey) && uniqueKey.length === 1 && Primary Key} {Array.isArray(uniqueKey) && Compound Key} {foreignKey && Foreign Key} diff --git a/packages/ui/src/viewer-table/DataTable/TableHeader.tsx b/packages/ui/src/viewer-table/DataTable/TableHeader.tsx index c24e2701..53fec68e 100644 --- a/packages/ui/src/viewer-table/DataTable/TableHeader.tsx +++ b/packages/ui/src/viewer-table/DataTable/TableHeader.tsx @@ -28,7 +28,7 @@ import { Theme } from '../../theme'; import { useThemeContext } from '../../theme/ThemeContext'; const thStyle = (theme: Theme, index: number) => css` - ${theme.typography.heading}; + ${theme.typography.subtitleSecondary}; background: #e5edf3; text-align: left; padding: 12px; @@ -40,6 +40,13 @@ const thStyle = (theme: Theme, index: number) => css` background-color: #e5edf3; `} border: 1px solid #DCDDE1; + ${index === 0 && + ` + position: sticky; + left: 0; + background-color: #e5edf3; + `} + border: 1px solid #DCDDE1; `; export type TableHeaderProps = { diff --git a/packages/ui/src/viewer-table/DataTable/TableRow.tsx b/packages/ui/src/viewer-table/DataTable/TableRow.tsx index ae6c5f89..1f97e5e0 100644 --- a/packages/ui/src/viewer-table/DataTable/TableRow.tsx +++ b/packages/ui/src/viewer-table/DataTable/TableRow.tsx @@ -22,6 +22,7 @@ /** @jsxImportSource @emotion/react */ import { css } from '@emotion/react'; + import { Row, flexRender } from '@tanstack/react-table'; import ReadMoreText from '../../common/ReadMoreText'; @@ -33,10 +34,12 @@ const rowStyle = (index: number) => css` `; const tdStyle = (theme: Theme, cellIndex: number, rowIndex: number) => css` - ${theme.typography.data} + ${theme.typography.paragraphSmall} padding: 12px; max-width: 30vw; - vertical-align: top; + white-space: pre-wrap; + overflow-wrap: break-word; + word-break: break-word; ${cellIndex === 0 && ` position: sticky; @@ -58,15 +61,7 @@ const TableRow = ({ row, index }: TableRowProps) => { {row.getVisibleCells().map((cell, cellIndex) => { return ( - css` - ${theme.typography.data} - white-space: pre-wrap; - `} - maxLines={4} - > + {flexRender(cell.column.columnDef.cell, cell.getContext())} diff --git a/packages/ui/src/viewer-table/DictionaryHeader.tsx b/packages/ui/src/viewer-table/DictionaryHeader.tsx index 39a3aaf2..97349e35 100644 --- a/packages/ui/src/viewer-table/DictionaryHeader.tsx +++ b/packages/ui/src/viewer-table/DictionaryHeader.tsx @@ -21,10 +21,9 @@ /** @jsxImportSource @emotion/react */ import { css } from '@emotion/react'; -import { useState } from 'react'; +import ReadMoreText from '../common/ReadMoreText'; import type { Theme } from '../theme'; import { useThemeContext } from '../theme/ThemeContext'; -import colours from './styles/colours'; export type DictionaryHeaderProps = { name: string; @@ -32,37 +31,25 @@ export type DictionaryHeaderProps = { version?: string; }; -const getChevronStyle = (isExpanded: boolean) => css` - margin-left: 4px; - ${isExpanded && `transform: rotate(180deg);`} -`; - -const linkStyle = (theme: Theme) => css` - ${theme.typography?.subheading} +const descriptionWrapperStyle = (theme: Theme) => css` + ${theme.typography.paragraphSmall} color: white; - cursor: pointer; - display: inline-flex; - align-items: center; - - &:hover { - text-decoration: underline; + padding: 0; + button { + ${theme.typography.paragraphSmallBold} + color: white; + margin-top: 4px; + &:hover { + text-decoration: underline; + } + svg { + fill: white; + } } `; -// Was unable to find the appropriate font size for the version numbering in the current design system, that matches -// Figma mockup so we are using something that is somewhat close with a hard coded font size - -const descriptionStyle = (theme: Theme) => css` - ${theme.typography?.data} - font-size: 16px; - color: white; - margin: 0; - display: inline; -`; - const containerStyle = (theme: Theme) => css` - background-color: ${colours.accent1_1}; - ${theme.typography.heading} + background-color: ${theme.colors.accent_dark}; display: flex; margin-bottom: 1rem; padding: 2.5rem; @@ -81,25 +68,16 @@ const titleColumnStyle = css` margin-right: 2rem; `; -// Was unable to find the appropriate font size for the title in the current design system, that matches -// Figma mockup so it is HARDCODED for now - -const titleStyle = css` - font-weight: 700; - font-size: 30px; +const titleStyle = (theme: Theme) => css` + ${theme.typography.subtitle} color: white; - line-height: 100%; margin: 0; margin-bottom: 0.5rem; `; -// Was unable to find the appropriate font size for the version numbering in the current design system, that matches -// Figma mockup so we are using something that is somewhat close - const versionStyle = (theme: Theme) => css` - ${theme.typography.data} + ${theme.typography.paragraphSmall} color: white; - font-size: 17px; `; const descriptionColumnStyle = css` @@ -109,40 +87,26 @@ const descriptionColumnStyle = css` justify-content: center; `; -const DESCRIPTION_LENGTH_THRESHOLD = 140; // Chosen to display ~2-3 lines of text before truncation based on typical container width - const DictionaryHeader = ({ name, description, version }: DictionaryHeaderProps) => { const theme = useThemeContext(); - const { ChevronDown } = theme.icons; - const [isExpanded, setIsExpanded] = useState(false); - - // Determine if the description is long enough to need a toggle, based off of how many characters we want to show by default - // according to the figma styling - const needsToggle = description && description.length > DESCRIPTION_LENGTH_THRESHOLD; - // We want to show all the text if it is not long or if it is already expanded via state variable - const showFull = isExpanded || !needsToggle; - // Based off of showFull, we determine the text to show, either its the full description or a truncated version - const textToShow = showFull ? description : description.slice(0, DESCRIPTION_LENGTH_THRESHOLD) + '... '; return (
    -

    {name}

    +

    {name}

    {version && {version}}
    {description && (
    -
    - {textToShow} - {needsToggle && ( - setIsExpanded((prev) => !prev)}> - {' '} - {isExpanded ? 'Read less' : 'Show more'} - - - )} -
    + + {description} +
    )}
    diff --git a/packages/ui/src/viewer-table/InteractionPanel/CollapseAllButton.tsx b/packages/ui/src/viewer-table/InteractionPanel/CollapseAllButton.tsx index d1e8d16e..d23e1f64 100644 --- a/packages/ui/src/viewer-table/InteractionPanel/CollapseAllButton.tsx +++ b/packages/ui/src/viewer-table/InteractionPanel/CollapseAllButton.tsx @@ -32,7 +32,7 @@ const CollapseAllButton = ({ onClick, disabled }: CollapseAllButtonProps) => { const { Collapse } = theme.icons; return ( - ); diff --git a/packages/ui/src/viewer-table/InteractionPanel/ExpandAllButton.tsx b/packages/ui/src/viewer-table/InteractionPanel/ExpandAllButton.tsx index e64da34a..0d69a7bf 100644 --- a/packages/ui/src/viewer-table/InteractionPanel/ExpandAllButton.tsx +++ b/packages/ui/src/viewer-table/InteractionPanel/ExpandAllButton.tsx @@ -33,7 +33,7 @@ const ExpandAllButton = ({ onClick, disabled }: ExpandAllButtonProps) => { const { Eye } = theme.icons; return ( - ); diff --git a/packages/ui/src/viewer-table/InteractionPanel/InteractionPanel.tsx b/packages/ui/src/viewer-table/InteractionPanel/InteractionPanel.tsx index d0a8bebb..b295d263 100644 --- a/packages/ui/src/viewer-table/InteractionPanel/InteractionPanel.tsx +++ b/packages/ui/src/viewer-table/InteractionPanel/InteractionPanel.tsx @@ -60,10 +60,7 @@ const panelStyles = (theme: Theme, styles?: SerializedStyles) => css` background-color: ${theme.colors.white}; min-height: 70px; flex-wrap: nowrap; - overflow-x: auto; - overflow-y: visible; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - position: relative; ${styles} `; diff --git a/packages/ui/src/viewer-table/styles/colours.ts b/packages/ui/src/viewer-table/styles/colours.ts deleted file mode 100644 index fa0dcc06..00000000 --- a/packages/ui/src/viewer-table/styles/colours.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * - * 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. - * - */ - -const primary = { - primary1: '#00A88F', - primary2: '#00C4A7', - primary3: '#00DDBE', - primary4: '#40E6CF', - primary5: '#99F1E5', - primary6: '#CCF8F2', - primary7: '#E5FBF8', -}; - -const secondary = { - secondary1: '#0B75A2', - secondary2: '#109ED9', - secondary3: '#4BC6F0', - secondary4: '#66CEF2', - secondary5: '#AEE5F8', - secondary6: '#D2F1FB', - secondary7: '#EDF9FD', -}; - -const greyscale = { - grey1: '#282A35', - grey2: '#5E6068', - grey3: '#AEAFB3', - grey4: '#DFDFE1', - grey5: '#F2F3F5', - grey6: '#F2F5F8', -}; - -const accent = { - accent1_1: '#003055', - accent1_2: '#04518C', - accent1_3: '#4F85AE', - accent1_4: '#9BB9D1', - accent1_5: '#C0D3E2', - accent1_6: '#E5EDF3', -}; - -const accent2 = { - accent2_1: '#9E005D', - accent2_2: '#B74A89', - accent2_3: '#C772A3', - accent2_4: '#E2B7D0', - accent2_5: '#EDD2E1', - accent2_6: '#F7ECF3', -}; - -const accent3 = { - accent3_1: '#CFD509', - accent3_2: '#D9DE3A', - accent3_3: '#E4E775', - accent3_4: '#F0F2B0', - accent3_5: '#F5F7CE', - accent3_6: '#FBFBEB', -}; - -const gradient = { - gradientStart: '#45A0D4', - gradientEnd: '#6EC9D0', -}; - -export default { - ...primary, - ...secondary, - ...greyscale, - ...accent, - ...accent2, - ...accent3, - ...gradient, -}; diff --git a/packages/ui/src/viewer-table/styles/typography.ts b/packages/ui/src/viewer-table/styles/typography.ts deleted file mode 100644 index 3c565687..00000000 --- a/packages/ui/src/viewer-table/styles/typography.ts +++ /dev/null @@ -1,184 +0,0 @@ -/* - * - * 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 { css } from '@emotion/react'; - -export const Lato = { - Hero: css` - font-family: 'Lato', sans-serif; - font-size: 40px; - font-weight: 400; - line-height: 100%; - `, - Title: css` - font-family: 'Lato', sans-serif; - font-size: 26px; - font-weight: 400; - line-height: 100%; - `, - Subtitle: css` - font-family: 'Lato', sans-serif; - font-size: 30px; - font-weight: 400; - line-height: 100%; - `, - Subtitle2: css` - font-family: 'Lato', sans-serif; - font-size: 20px; - font-weight: 400; - line-height: 100%; - `, - Intro_Text: css` - font-family: 'Lato', sans-serif; - font-size: 18px; - font-weight: 400; - line-height: 100%; - `, - Paragraph: css` - font-family: 'Lato', sans-serif; - font-size: 26px; - font-weight: 400; - line-height: 100%; - `, - Paragraph_bold: css` - font-family: 'Lato', sans-serif; - font-size: 26px; - font-weight: 700; - line-height: 100%; - `, - Paragraph_small: css` - font-family: 'Lato', sans-serif; - font-size: 16px; - font-weight: 400; - line-height: 100%; - `, - Paragraph_bold_small: css` - font-family: 'Lato', sans-serif; - font-size: 16px; - font-weight: 700; - line-height: 100%; - `, - Data: css` - font-family: 'Lato', sans-serif; - font-size: 13px; - font-weight: 400; - line-height: 100%; - `, - Data_bold: css` - font-family: 'Lato', sans-serif; - font-size: 13px; - font-weight: 700; - line-height: 100%; - `, - Caption: css` - font-family: 'Lato', sans-serif; - font-size: 11px; - font-weight: 400; - line-height: 100%; - `, - Caption_bold: css` - font-family: 'Lato', sans-serif; - font-size: 11px; - font-weight: 700; - line-height: 100%; - `, -}; - -export const Open_Sans = { - Hero: css` - font-family: 'Open Sans', sans-serif; - font-size: 40px; - font-weight: 400; - line-height: 100%; - `, - Title: css` - font-family: 'Open Sans', sans-serif; - font-size: 26px; - font-weight: 400; - line-height: 100%; - `, - Subtitle: css` - font-family: 'Open Sans', sans-serif; - font-size: 30px; - font-weight: 400; - line-height: 100%; - `, - Subtitle2: css` - font-family: 'Open Sans', sans-serif; - font-size: 20px; - font-weight: 400; - line-height: 100%; - `, - Intro_Text: css` - font-family: 'Open Sans', sans-serif; - font-size: 18px; - font-weight: 400; - line-height: 100%; - `, - Paragraph: css` - font-family: 'Open Sans', sans-serif; - font-size: 26px; - font-weight: 400; - line-height: 100%; - `, - Paragraph_bold: css` - font-family: 'Open Sans', sans-serif; - font-size: 26px; - font-weight: 700; - line-height: 100%; - `, - Paragraph_small: css` - font-family: 'Open Sans', sans-serif; - font-size: 16px; - font-weight: 400; - line-height: 100%; - `, - Paragraph_bold_small: css` - font-family: 'Open Sans', sans-serif; - font-size: 16px; - font-weight: 700; - line-height: 100%; - `, - Data: css` - font-family: 'Open Sans', sans-serif; - font-size: 13px; - font-weight: 400; - line-height: 100%; - `, - Data_bold: css` - font-family: 'Open Sans', sans-serif; - font-size: 13px; - font-weight: 700; - line-height: 100%; - `, - Caption: css` - font-family: 'Open Sans', sans-serif; - font-size: 11px; - font-weight: 400; - line-height: 100%; - `, - Caption_bold: css` - font-family: 'Open Sans', sans-serif; - font-size: 11px; - font-weight: 700; - line-height: 100%; - `, -}; diff --git a/packages/ui/stories/viewer-table/DataDictionaryPage.stories.tsx b/packages/ui/stories/viewer-table/DataDictionaryPage.stories.tsx new file mode 100644 index 00000000..a1e61663 --- /dev/null +++ b/packages/ui/stories/viewer-table/DataDictionaryPage.stories.tsx @@ -0,0 +1,96 @@ +/** @jsxImportSource @emotion/react */ + +import { Dictionary, replaceReferences } from '@overture-stack/lectern-dictionary'; +import type { Meta, StoryObj } from '@storybook/react'; +import React, { useState } from 'react'; +import DataDictionaryPage from '../../src/viewer-table/DataDictionaryPage'; +import PCGL from '../fixtures/pcgl.json'; +import Advanced from '../fixtures/TorontoMapleLeafs.json'; +import themeDecorator from '../themeDecorator'; + +const pcglDictionary: Dictionary = replaceReferences(PCGL as Dictionary); +const advancedDictionary: Dictionary = replaceReferences(Advanced as Dictionary); + +// Wrapper component to manage state for version switching +const DataDictionaryPageWrapper = ({ + dictionaries, + initialDictionaryIndex = 0, + lecternUrl = 'localhost:3031', +}: { + dictionaries: Dictionary[]; + initialDictionaryIndex?: number; + lecternUrl?: string; +}) => { + const [dictionaryIndex, setDictionaryIndex] = useState(initialDictionaryIndex); + + const handleVersionChange = (index: number) => { + console.log('Version changed to index:', index); + setDictionaryIndex(index); + }; + + return ( + + ); +}; + +const meta = { + component: DataDictionaryPageWrapper, + title: 'Viewer - Table/Data Dictionary Page', + tags: ['autodocs'], + decorators: [themeDecorator()], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const PCGLDictionary: Story = { + args: { + dictionaries: [pcglDictionary], + initialDictionaryIndex: 0, + lecternUrl: 'localhost:3031', + }, +}; + +export const MultipleVersions: Story = { + args: { + dictionaries: [ + { ...pcglDictionary, version: '1.0', name: `${pcglDictionary.name} v1.0` }, + { ...pcglDictionary, version: '1.1', name: `${pcglDictionary.name} v1.1` }, + { ...pcglDictionary, version: '2.0', name: `${pcglDictionary.name} v2.0` }, + { ...pcglDictionary, version: '2.1', name: `${pcglDictionary.name} v2.1` }, + { ...pcglDictionary, version: '3.0', name: `${pcglDictionary.name} v3.0` }, + ], + initialDictionaryIndex: 2, + lecternUrl: 'localhost:3031', + }, +}; + +export const AdvancedDictionary: Story = { + args: { + dictionaries: [ + { ...advancedDictionary, version: '1.0', name: `${advancedDictionary.name} v1.0` }, + { ...advancedDictionary, version: '1.5', name: `${advancedDictionary.name} v1.5` }, + { ...advancedDictionary, version: '2.0', name: `${advancedDictionary.name} v2.0` }, + ], + initialDictionaryIndex: 1, + lecternUrl: 'localhost:3031', + }, +}; + +export const MixedDictionaries: Story = { + args: { + dictionaries: [ + { ...pcglDictionary, version: '1.0' }, + { ...advancedDictionary, version: '1.0' }, + { ...pcglDictionary, version: '2.0' }, + { ...advancedDictionary, version: '2.0' }, + ], + initialDictionaryIndex: 0, + lecternUrl: 'localhost:3031', + }, +};