From 87499a3d670665ef8e9f1f1481aa87a9afa67377 Mon Sep 17 00:00:00 2001 From: dcgoodwin2112 Date: Mon, 25 May 2026 10:43:42 -0400 Subject: [PATCH 1/3] Refactor Storybook stories to use '@storybook/react-vite' and improve documentation - Updated imports in multiple components to use type definitions from '@storybook/react-vite'. - Enhanced story documentation for various components including HeaderTagline, Hero, LargeFileDialog, and others. - Added new stories for components like ManageColumns, MobileHeader, SearchModal, and ResourcePreview. - Improved existing stories by adding parameters and decorators for better context and layout. - Removed unused mock data and streamlined story args for clarity. - Added global type declarations for image modules to support asset imports. --- .storybook/decorators.tsx | 79 +++ .storybook/fixtures.ts | 73 +++ .storybook/main.ts | 16 + .storybook/preview.tsx | 81 ++- COMPONENTS_INVENTORY.md | 147 ++--- eslint.config.js | 16 + package-lock.json | 578 ++++++++++++++---- package.json | 4 +- scripts/generate-inventory.cjs | 16 +- .../ApiDocumentation.stories.tsx | 44 ++ .../ApiRowLimitNotice.stories.tsx | 2 +- ...umb.stories.jsx => Breadcrumb.stories.tsx} | 11 +- .../CMSTopNav/CMSTopNav.stories.jsx | 201 ------ .../CMSTopNav/CMSTopNav.stories.tsx | 128 ++++ .../DataDictionaryTable.stories.tsx | 11 +- .../DatasetDictionaryJSON.stories.tsx | 34 +- .../DatasetDictionaryPDF.stories.tsx | 15 +- .../DataTableControls.stories.tsx | 59 ++ ...ories.jsx => DataTableDensity.stories.tsx} | 14 +- .../DataTablePageResults.stories.tsx | 45 +- .../DataTableRowChanger.stories.tsx | 49 ++ .../DataTableToolbar.stories.tsx | 2 +- ... DatasetAdditionalInformation.stories.tsx} | 17 +- .../DatasetDate/DatasetDate.stories.tsx | 2 +- .../DatasetDateItem.stories.tsx | 48 ++ .../DatasetDescription.stories.tsx | 31 +- .../DatasetListItem.stories.tsx | 64 ++ .../DatasetListSubmenu.stories.tsx | 71 +++ .../DatasetListSubmenuItem.stories.tsx | 2 +- .../DatasetSearchFacets.stories.tsx | 21 +- .../DatasetSearchListItem.stories.tsx | 109 ++++ .../DatasetTableTab.stories.tsx | 2 +- .../Datatable/Datatable.stories.tsx | 79 +++ .../DatatableHeader.stories.tsx | 15 +- .../DesktopHeader/DesktopHeader.stories.tsx | 50 ++ .../DisplaySettings.stories.tsx | 24 + .../ErrorBoundary/ErrorBoundary.stories.tsx | 40 +- .../FAQAccordion/FAQAccordion.stories.tsx | 54 +- .../FilterChip/FilterChip.stories.tsx | 2 +- .../FilterDataset/FilterDataset.stories.tsx | 2 +- .../FullScreenDataTable.stories.tsx | 29 + .../HeaderNav/HeaderNav.stories.tsx | 81 +++ .../HeaderNavIconLink.stories.tsx | 56 +- .../HeaderSearch/HeaderSearch.stories.tsx | 45 +- .../HeaderSiteTitle.stories.tsx | 57 +- .../HeaderTagline/HeaderTagline.stories.tsx | 35 +- .../{Hero.stories.jsx => Hero.stories.tsx} | 23 +- .../LargeFileDialog.stories.tsx | 19 +- .../LargeFileInfo/LargeFileInfo.stories.tsx | 24 +- .../ManageColumns/ManageColumns.stories.tsx | 43 ++ src/components/MobileHeader/MobileHeader.jsx | 1 - .../MobileHeader/MobileHeader.stories.tsx | 45 ++ .../MobileMenuButton.stories.tsx | 2 +- src/components/NavBar/NavBar.stories.tsx | 57 ++ src/components/NavLink/NavLink.stories.tsx | 2 +- .../PageHeader/PageHeader.stories.tsx | 2 +- .../QueryBuilder/QueryBuilder.stories.tsx | 2 +- src/components/QueryRow/QueryRow.stories.tsx | 2 +- src/components/Resource/Resource.stories.tsx | 2 +- .../ResourceFooter/ResourceFooter.stories.tsx | 2 +- .../ResourceHeader/ResourceHeader.stories.tsx | 2 +- .../ResourceInformation.stories.tsx | 2 +- .../ResourcePreview.stories.tsx | 60 ++ .../SearchButton/SearchButton.stories.tsx | 2 +- .../SearchInput/SearchInput.stories.tsx | 2 +- ...al.stories.jsx => SearchModal.stories.tsx} | 15 +- .../SidebarNavigation.stories.tsx | 2 +- .../SitewideDataDictionaryTable.stories.tsx | 2 +- src/components/SubMenu/SubMenu.stories.tsx | 2 +- .../TopicInformation.stories.tsx | 2 +- ...tories.jsx => TransformedDate.stories.tsx} | 13 +- src/global.d.ts | 5 + src/templates/APIPage/APIPage.stories.tsx | 2 +- src/templates/Dataset/Dataset.stories.tsx | 12 +- .../DatasetList/DatasetList.stories.tsx | 14 +- .../DatasetSearch/DatasetSearch.stories.tsx | 2 +- .../FilteredResource.stories.tsx | 72 +++ src/templates/Footer/Footer.stories.tsx | 2 +- src/templates/Header/Header.stories.tsx | 62 +- .../PageNotFound/PageNotFound.stories.tsx | 2 +- .../SidebarPage/SidebarPage.stories.tsx | 2 +- .../SpecsAndLimits/SpecsAndLimits.stories.tsx | 2 +- .../StoredQueryPage.stories.tsx | 15 +- 83 files changed, 2247 insertions(+), 836 deletions(-) create mode 100644 .storybook/decorators.tsx create mode 100644 .storybook/fixtures.ts create mode 100644 eslint.config.js create mode 100644 src/components/ApiDocumentation/ApiDocumentation.stories.tsx rename src/components/Breadcrumb/{Breadcrumb.stories.jsx => Breadcrumb.stories.tsx} (87%) delete mode 100644 src/components/CMSTopNav/CMSTopNav.stories.jsx create mode 100644 src/components/CMSTopNav/CMSTopNav.stories.tsx create mode 100644 src/components/DataTableControls/DataTableControls.stories.tsx rename src/components/DataTableDensity/{DataTableDensity.stories.jsx => DataTableDensity.stories.tsx} (58%) create mode 100644 src/components/DataTableRowChanger/DataTableRowChanger.stories.tsx rename src/components/DatasetAdditionalInformation/{DatasetAdditionalInformation.stories.jsx => DatasetAdditionalInformation.stories.tsx} (60%) create mode 100644 src/components/DatasetDateItem/DatasetDateItem.stories.tsx create mode 100644 src/components/DatasetListItem/DatasetListItem.stories.tsx create mode 100644 src/components/DatasetListSubmenu/DatasetListSubmenu.stories.tsx create mode 100644 src/components/DatasetSearchListItem/DatasetSearchListItem.stories.tsx create mode 100644 src/components/Datatable/Datatable.stories.tsx create mode 100644 src/components/DesktopHeader/DesktopHeader.stories.tsx create mode 100644 src/components/DisplaySettings/DisplaySettings.stories.tsx create mode 100644 src/components/FullScreenDataTable/FullScreenDataTable.stories.tsx create mode 100644 src/components/HeaderNav/HeaderNav.stories.tsx rename src/components/Hero/{Hero.stories.jsx => Hero.stories.tsx} (76%) create mode 100644 src/components/ManageColumns/ManageColumns.stories.tsx create mode 100644 src/components/MobileHeader/MobileHeader.stories.tsx create mode 100644 src/components/NavBar/NavBar.stories.tsx create mode 100644 src/components/ResourcePreview/ResourcePreview.stories.tsx rename src/components/SearchModal/{SearchModal.stories.jsx => SearchModal.stories.tsx} (81%) rename src/components/TransformedDate/{TransformedDate.stories.jsx => TransformedDate.stories.tsx} (68%) create mode 100644 src/global.d.ts create mode 100644 src/templates/FilteredResource/FilteredResource.stories.tsx diff --git a/.storybook/decorators.tsx b/.storybook/decorators.tsx new file mode 100644 index 00000000..deb593f2 --- /dev/null +++ b/.storybook/decorators.tsx @@ -0,0 +1,79 @@ +import type { Decorator } from '@storybook/react-vite'; +import { MemoryRouter } from 'react-router-dom'; +import DataTableContext, { + DataTableContextType, +} from '../src/templates/Dataset/DataTableContext'; +import { + DataTableActionsContext, + DataTableActionsContextProps, +} from '../src/components/DatasetTableTab/DataTableActionsContext'; +import { mockResource } from '../__mocks__/mockResource'; +import { mockDistribution } from '../__mocks__/mockDistribution'; + +export const defaultDataTableContext: DataTableContextType = { + id: 'wb6u-x2ny', + resource: mockResource, + distribution: mockDistribution, + rootUrl: '/api/1', + customColumns: [], + dataDictionaryBanner: false, + datasetTableControls: false, + enableEmptyFilters: false, + relativeHomeUrlPrepend: '', +}; + +export const defaultDataTableActionsContext: DataTableActionsContextProps = { + columnOrder: mockResource.columns ?? [], + setColumnOrder: () => {}, + columnVisibility: {}, + setColumnVisibility: () => {}, + page: 1, + setPage: () => {}, + tableDensity: 'normal', + setTableDensity: () => {}, +}; + +/** + * Wraps a story in MemoryRouter + DataTableContext + DataTableActionsContext. + * Overrides are shallow-merged on top of the defaults. + */ +export const withDataTableContexts = + ( + contextOverrides: Partial = {}, + actionsOverrides: Partial = {} + ): Decorator => + (Story) => ( + + + + + + + + ); + +/** + * Same as `withDataTableContexts` but pulls per-story overrides from `args.contextOverride` + * and `args.actionsOverride`. Use this when a story needs to vary the contexts per-instance + * (e.g. Loading / Empty / Error variants). Define the args as `control: false` so they + * don't pollute the docs table. + */ +export const withDataTableContextsFromArgs: Decorator = (Story, context) => { + const args = context.args as { + contextOverride?: Partial; + actionsOverride?: Partial; + }; + return ( + + + + + + + + ); +}; diff --git a/.storybook/fixtures.ts b/.storybook/fixtures.ts new file mode 100644 index 00000000..aac536d2 --- /dev/null +++ b/.storybook/fixtures.ts @@ -0,0 +1,73 @@ +import type { NavLinkArray, OrgType, FAQItemType } from '../src/types/misc'; +import cmsLogo from '../src/assets/images/CMSgov@2x-white-O.png'; + +export const cmsOrg: OrgType = { + url: 'https://www.cms.gov', + tagline: 'The Centers for Medicare & Medicaid Services', + urlTitle: 'CMS Open Data', + logoAltText: 'CMS Logo', +}; + +export const cmsOrgWithLogo: OrgType = { + ...cmsOrg, + logoFilePath: cmsLogo, +}; + +export const mainNavLinks: NavLinkArray[] = [ + { id: 'home', label: 'Home', url: '/' }, + { id: 'datasets', label: 'Datasets', url: '/datasets' }, + { id: 'api', label: 'API Documentation', url: '/api-docs' }, + { id: 'about', label: 'About', url: '/about' }, +]; + +export const navLinksWithSubmenus: NavLinkArray[] = [ + { id: 'home', label: 'Home', url: '/' }, + { id: 'datasets', label: 'Datasets', url: '/datasets' }, + { + id: 'resources', + label: 'Resources', + url: '/resources', + submenu: [ + { id: 'api', label: 'API Documentation', url: '/api-docs' }, + { id: 'dictionary', label: 'Data Dictionary', url: '/data-dictionary' }, + { id: 'guides', label: 'User Guides', url: '/guides' }, + ], + }, + { + id: 'about', + label: 'About', + url: '/about', + submenu: [ + { id: 'mission', label: 'Our Mission', url: '/about/mission' }, + { id: 'team', label: 'Team', url: '/about/team' }, + { id: 'contact', label: 'Contact Us', url: '/about/contact' }, + ], + }, +]; + +export const topNavLinks: NavLinkArray[] = [ + { id: 'cms-main', label: 'CMS.gov', url: 'https://www.cms.gov', target: '_blank' }, + { id: 'medicare', label: 'Medicare.gov', url: 'https://www.medicare.gov', target: '_blank' }, + { id: 'medicaid', label: 'Medicaid.gov', url: 'https://www.medicaid.gov', target: '_blank' }, +]; + +export const sampleFaqs: FAQItemType[] = [ + { + id: 'faq1', + title: 'What is Open Data?', + body: 'Open data is data that can be freely used, re-used, and redistributed by anyone.', + open: false, + }, + { + id: 'faq2', + title: 'How do I access datasets?', + body: 'You can access datasets via the search or browse features on our site.', + open: false, + }, + { + id: 'faq3', + title: 'Who maintains the data?', + body: 'The Open Data team maintains and updates the datasets regularly.', + open: false, + }, +]; diff --git a/.storybook/main.ts b/.storybook/main.ts index d5e77f30..1aea14b0 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -22,6 +22,22 @@ const config: StorybookConfig = { jsxRuntime: 'automatic', }), ], + build: { + ...config.build, + rollupOptions: { + ...config.build?.rollupOptions, + output: { + ...(Array.isArray(config.build?.rollupOptions?.output) + ? {} + : config.build?.rollupOptions?.output), + manualChunks: (id: string) => { + if (id.includes('node_modules/swagger-ui') || id.includes('node_modules/swagger-client') || id.includes('@civicactions/swagger-ui-layout')) { + return 'swagger-ui'; + } + }, + }, + }, + }, }; } }; diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 5a324cf7..33290349 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -3,52 +3,54 @@ import '@cmsgov/design-system/css/index.css'; import '@cmsgov/design-system/css/core-theme.css'; import '@fortawesome/fontawesome-free/css/all.css'; import './font-awesome-overrides.css'; -import { useEffect } from 'react'; import { initialize, mswLoader } from 'msw-storybook-addon'; import { handlers } from './mswHandlers'; -// Initialize MSW for Storybook initialize({ - onUnhandledRequest: 'bypass', // Don't warn about unmocked requests + onUnhandledRequest: 'warn', }, handlers); +// Convert Pro icon classes to Free equivalents at runtime. +// Set up once at module load; the observer catches dynamically rendered icons. +const replaceIconClasses = () => { + document.querySelectorAll('.far, .fal, .fad, .fat').forEach((icon) => { + icon.classList.remove('far', 'fal', 'fad', 'fat'); + icon.classList.add('fas'); + }); + document.querySelectorAll('.fa-file-xls').forEach((icon) => { + icon.classList.replace('fa-file-xls', 'fa-file-excel'); + }); +}; + +if (typeof document !== 'undefined') { + if (document.body) { + replaceIconClasses(); + new MutationObserver(replaceIconClasses).observe(document.body, { childList: true, subtree: true }); + } else { + document.addEventListener('DOMContentLoaded', () => { + replaceIconClasses(); + new MutationObserver(replaceIconClasses).observe(document.body, { childList: true, subtree: true }); + }); + } +} + const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => ( -
+
{children}
); -/** - * Decorator to handle Font Awesome Pro to Free conversions - * - * Replaces Pro icon classes with Free equivalents at runtime. - * CSS overrides are loaded via font-awesome-overrides.css import. - */ -const FontAwesomeProToFree = (Story: any) => { - useEffect(() => { - const replaceIconClasses = () => { - // Pro weight classes → solid - document.querySelectorAll('.far, .fal, .fad, .fat').forEach((icon) => { - icon.classList.remove('far', 'fal', 'fad', 'fat'); - icon.classList.add('fas'); - }); - - // Pro icon names → Free equivalents - document.querySelectorAll('.fa-file-xls').forEach((icon) => { - icon.classList.replace('fa-file-xls', 'fa-file-excel'); - }); - }; - - replaceIconClasses(); - - // Watch for dynamically added icons - const observer = new MutationObserver(replaceIconClasses); - observer.observe(document.body, { childList: true, subtree: true }); - - return () => observer.disconnect(); - }, []); - - return ; +// Skip the Layout wrapper for stories declaring layout: 'fullscreen' so templates +// (Dataset, FilteredResource, Header, etc.) actually render full-bleed. +const layoutDecorator = (Story: React.FC, context: { parameters?: { layout?: string } }) => { + if (context.parameters?.layout === 'fullscreen') { + return ; + } + return ( + + + + ); }; const preview: Preview = { @@ -63,13 +65,6 @@ const preview: Preview = { loaders: [mswLoader], }; -preview.decorators = [ - FontAwesomeProToFree, - (Story) => ( - - - - ), -]; +preview.decorators = [layoutDecorator]; export default preview; diff --git a/COMPONENTS_INVENTORY.md b/COMPONENTS_INVENTORY.md index caa36af1..71778d38 100644 --- a/COMPONENTS_INVENTORY.md +++ b/COMPONENTS_INVENTORY.md @@ -7,94 +7,96 @@ This document provides a comprehensive inventory of all components, services, te | Type | Name | Public Export | Storybook Status | Unit Tests | |------|------|---------------|------------------|------------| | **COMPONENTS** | | | | | -| Component | [ApiDocumentation](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/ApiDocumentation) | ✅ Public | ❌ No Story | ✅ Has Tests | -| Component | [ApiRowLimitNotice](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/ApiRowLimitNotice) | ✅ Public | ✅ Has Story | ✅ Has Tests | -| Component | [Breadcrumb](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/Breadcrumb) | ✅ Public | ✅ Has Story | ✅ Has Tests | -| Component | [CMSTopNav](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/CMSTopNav) | ✅ Public | ✅ Has Story | ✅ Has Tests | -| Component | [DataDictionary](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DataDictionary) | ❌ Internal | ❌ No Story | ❌ No Tests | -| Component | [DataTableControls](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DataTableControls) | ❌ Internal | ❌ No Story | ✅ Has Tests | +| Component | [ApiDocumentation](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/ApiDocumentation) | ✅ Public | ✅ Has Story | ❌ No Tests | +| Component | [ApiRowLimitNotice](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/ApiRowLimitNotice) | ✅ Public | ✅ Has Story | ❌ No Tests | +| Component | [Breadcrumb](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/Breadcrumb) | ✅ Public | ✅ Has Story | ❌ No Tests | +| Component | [CMSTopNav](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/CMSTopNav) | ✅ Public | ✅ Has Story | ❌ No Tests | +| Component | [DataDictionary](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DataDictionary) | ❌ Internal | ✅ Has Story | ❌ No Tests | +| Component | [DataTableControls](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DataTableControls) | ❌ Internal | ✅ Has Story | ✅ Has Tests | | Component | [DataTableDensity](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DataTableDensity) | ❌ Internal | ✅ Has Story | ✅ Has Tests | -| Component | [DataTablePageResults](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DataTablePageResults) | ✅ Public | ✅ Has Story | ✅ Has Tests | -| Component | [DataTableRowChanger](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DataTableRowChanger) | ❌ Internal | ❌ No Story | ✅ Has Tests | +| Component | [DataTablePageResults](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DataTablePageResults) | ✅ Public | ✅ Has Story | ❌ No Tests | +| Component | [DataTableRowChanger](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DataTableRowChanger) | ❌ Internal | ✅ Has Story | ✅ Has Tests | | Component | [DataTableToolbar](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DataTableToolbar) | ✅ Public | ✅ Has Story | ✅ Has Tests | -| Component | [DatasetAPITab](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DatasetAPITab) | ❌ Internal | ❌ No Story | ✅ Has Tests | -| Component | [DatasetAdditionalInformation](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DatasetAdditionalInformation) | ✅ Public (buildRows) | ✅ Has Story | ✅ Has Tests | +| Component | [DatasetAPITab](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DatasetAPITab) | ❌ Internal | ❌ No Story | ❌ No Tests | +| Component | [DatasetAdditionalInformation](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DatasetAdditionalInformation) | ✅ Public (buildRows) | ✅ Has Story | ❌ No Tests | | Component | [DatasetDataDictionaryTab](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DatasetDataDictionaryTab) | ❌ Internal | ❌ No Story | ✅ Has Tests | | Component | [DatasetDate](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DatasetDate) | ✅ Public | ✅ Has Story | ✅ Has Tests | -| Component | [DatasetDateItem](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DatasetDateItem) | ✅ Public | ❌ No Story | ✅ Has Tests | +| Component | [DatasetDateItem](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DatasetDateItem) | ✅ Public | ✅ Has Story | ✅ Has Tests | | Component | [DatasetDescription](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DatasetDescription) | ❌ Internal | ✅ Has Story | ✅ Has Tests | -| Component | [DatasetListItem](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DatasetListItem) | ✅ Public | ❌ No Story | ✅ Has Tests | -| Component | [DatasetListSubmenu](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DatasetListSubmenu) | ✅ Public | ❌ No Story | ✅ Has Tests | +| Component | [DatasetListItem](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DatasetListItem) | ✅ Public | ✅ Has Story | ✅ Has Tests | +| Component | [DatasetListSubmenu](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DatasetListSubmenu) | ✅ Public | ✅ Has Story | ❌ No Tests | | Component | [DatasetListSubmenuItem](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DatasetListSubmenuItem) | ❌ Internal | ✅ Has Story | ✅ Has Tests | | Component | [DatasetOverviewTab](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DatasetOverviewTab) | ❌ Internal | ❌ No Story | ✅ Has Tests | | Component | [DatasetSearchFacets](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DatasetSearchFacets) | ✅ Public | ✅ Has Story | ✅ Has Tests | -| Component | [DatasetSearchListItem](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DatasetSearchListItem) | ✅ Public | ❌ No Story | ✅ Has Tests | +| Component | [DatasetSearchListItem](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DatasetSearchListItem) | ✅ Public | ✅ Has Story | ✅ Has Tests | | Component | [DatasetTableTab](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DatasetTableTab) | ✅ Public (as DatasetTable) | ✅ Has Story | ✅ Has Tests | -| Component | [Datatable](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/Datatable) | ✅ Public | ❌ No Story | ✅ Has Tests | +| Component | [Datatable](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/Datatable) | ✅ Public | ✅ Has Story | ✅ Has Tests | | Component | [DatatableHeader](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DatatableHeader) | ❌ Internal | ✅ Has Story | ✅ Has Tests | -| Component | [DesktopHeader](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DesktopHeader) | ❌ Internal | ❌ No Story | ✅ Has Tests | -| Component | [DisplaySettings](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DisplaySettings) | ❌ Internal | ❌ No Story | ✅ Has Tests | -| Component | [ErrorBoundary](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/ErrorBoundary) | ✅ Public | ✅ Has Story | ✅ Has Tests | -| Component | [FAQAccordion](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/FAQAccordion) | ✅ Public | ✅ Has Story | ✅ Has Tests | +| Component | [DesktopHeader](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DesktopHeader) | ❌ Internal | ✅ Has Story | ❌ No Tests | +| Component | [DisplaySettings](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DisplaySettings) | ❌ Internal | ✅ Has Story | ✅ Has Tests | +| Component | [ErrorBoundary](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/ErrorBoundary) | ✅ Public | ✅ Has Story | ❌ No Tests | +| Component | [FAQAccordion](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/FAQAccordion) | ✅ Public | ✅ Has Story | ❌ No Tests | | Component | [FilterChip](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/FilterChip) | ❌ Internal | ✅ Has Story | ✅ Has Tests | | Component | [FilterDataset](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/FilterDataset) | ❌ Internal | ✅ Has Story | ✅ Has Tests | -| Component | [FullScreenDataTable](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/FullScreenDataTable) | ❌ Internal | ❌ No Story | ✅ Has Tests | -| Component | [HeaderNav](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/HeaderNav) | ✅ Public | ❌ No Story | ✅ Has Tests | -| Component | [HeaderNavIconLink](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/HeaderNavIconLink) | ✅ Public | ✅ Has Story | ✅ Has Tests | -| Component | [HeaderSearch](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/HeaderSearch) | ✅ Public | ✅ Has Story | ✅ Has Tests | -| Component | [HeaderSiteTitle](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/HeaderSiteTitle) | ✅ Public | ✅ Has Story | ✅ Has Tests | -| Component | [HeaderTagline](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/HeaderTagline) | ✅ Public | ✅ Has Story | ✅ Has Tests | -| Component | [Hero](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/Hero) | ✅ Public | ✅ Has Story | ✅ Has Tests | +| Component | [FullScreenDataTable](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/FullScreenDataTable) | ❌ Internal | ✅ Has Story | ❌ No Tests | +| Component | [HeaderNav](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/HeaderNav) | ✅ Public | ✅ Has Story | ❌ No Tests | +| Component | [HeaderNavIconLink](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/HeaderNavIconLink) | ✅ Public | ✅ Has Story | ❌ No Tests | +| Component | [HeaderSearch](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/HeaderSearch) | ✅ Public | ✅ Has Story | ❌ No Tests | +| Component | [HeaderSiteTitle](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/HeaderSiteTitle) | ✅ Public | ✅ Has Story | ❌ No Tests | +| Component | [HeaderTagline](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/HeaderTagline) | ✅ Public | ✅ Has Story | ❌ No Tests | +| Component | [Hero](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/Hero) | ✅ Public | ✅ Has Story | ❌ No Tests | | Component | [LargeFileDialog](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/LargeFileDialog) | ❌ Internal | ✅ Has Story | ✅ Has Tests | -| Component | [LargeFileInfo](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/LargeFileInfo) | ❌ Internal | ✅ Has Story | ✅ Has Tests | -| Component | [ManageColumns](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/ManageColumns) | ❌ Internal | ❌ No Story | ✅ Has Tests | -| Component | [MobileHeader](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/MobileHeader) | ❌ Internal | ❌ No Story | ✅ Has Tests | -| Component | [MobileMenuButton](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/MobileMenuButton) | ✅ Public | ✅ Has Story | ✅ Has Tests | -| Component | [NavBar](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/NavBar) | ✅ Public | ❌ No Story | ✅ Has Tests | +| Component | [LargeFileInfo](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/LargeFileInfo) | ❌ Internal | ✅ Has Story | ❌ No Tests | +| Component | [ManageColumns](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/ManageColumns) | ❌ Internal | ✅ Has Story | ✅ Has Tests | +| Component | [MobileHeader](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/MobileHeader) | ❌ Internal | ✅ Has Story | ✅ Has Tests | +| Component | [MobileMenuButton](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/MobileMenuButton) | ✅ Public | ✅ Has Story | ❌ No Tests | +| Component | [NavBar](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/NavBar) | ✅ Public | ✅ Has Story | ❌ No Tests | | Component | [NavLink](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/NavLink) | ❌ Internal | ✅ Has Story | ✅ Has Tests | -| Component | [PageHeader](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/PageHeader) | ❌ Internal | ✅ Has Story | ✅ Has Tests | +| Component | [PageHeader](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/PageHeader) | ❌ Internal | ✅ Has Story | ❌ No Tests | | Component | [QueryBuilder](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/QueryBuilder) | ❌ Internal | ✅ Has Story | ✅ Has Tests | -| Component | [QueryRow](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/QueryRow) | ❌ Internal | ✅ Has Story | ✅ Has Tests | +| Component | [QueryRow](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/QueryRow) | ❌ Internal | ✅ Has Story | ❌ No Tests | | Component | [Resource](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/Resource) | ❌ Internal | ✅ Has Story | ✅ Has Tests | -| Component | [ResourceFooter](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/ResourceFooter) | ✅ Public | ✅ Has Story | ✅ Has Tests | -| Component | [ResourceHeader](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/ResourceHeader) | ✅ Public | ✅ Has Story | ✅ Has Tests | -| Component | [ResourceInformation](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/ResourceInformation) | ❌ Internal | ✅ Has Story | ✅ Has Tests | -| Component | [ResourcePreview](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/ResourcePreview) | ✅ Public | ❌ No Story | ✅ Has Tests | -| Component | [SearchButton](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/SearchButton) | ❌ Internal | ✅ Has Story | ✅ Has Tests | -| Component | [SearchInput](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/SearchInput) | ✅ Public | ✅ Has Story | ✅ Has Tests | -| Component | [SearchModal](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/SearchModal) | ❌ Internal | ✅ Has Story | ✅ Has Tests | -| Component | [SidebarNavigation](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/SidebarNavigation) | ✅ Public | ✅ Has Story | ✅ Has Tests | -| Component | [SitewideDataDictionaryTable](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/SitewideDataDictionaryTable) | ❌ Internal | ✅ Has Story | ✅ Has Tests | +| Component | [ResourceFooter](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/ResourceFooter) | ✅ Public | ✅ Has Story | ❌ No Tests | +| Component | [ResourceHeader](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/ResourceHeader) | ✅ Public | ✅ Has Story | ❌ No Tests | +| Component | [ResourceInformation](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/ResourceInformation) | ❌ Internal | ✅ Has Story | ❌ No Tests | +| Component | [ResourcePreview](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/ResourcePreview) | ✅ Public | ✅ Has Story | ❌ No Tests | +| Component | [SearchButton](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/SearchButton) | ❌ Internal | ✅ Has Story | ❌ No Tests | +| Component | [SearchInput](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/SearchInput) | ✅ Public | ✅ Has Story | ❌ No Tests | +| Component | [SearchModal](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/SearchModal) | ❌ Internal | ✅ Has Story | ❌ No Tests | +| Component | [SidebarNavigation](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/SidebarNavigation) | ✅ Public | ✅ Has Story | ❌ No Tests | +| Component | [SitewideDataDictionaryTable](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/SitewideDataDictionaryTable) | ❌ Internal | ✅ Has Story | ❌ No Tests | | Component | [SubMenu](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/SubMenu) | ✅ Public | ✅ Has Story | ✅ Has Tests | -| Component | [SubMenuStaticList](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/SubMenuStaticList) | ❌ Internal | ❌ No Story | ✅ Has Tests | +| Component | [SubMenuStaticList](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/SubMenuStaticList) | ❌ Internal | ❌ No Story | ❌ No Tests | | Component | [TopicInformation](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/TopicInformation) | ❌ Internal | ✅ Has Story | ✅ Has Tests | -| Component | [TransformedDate](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/TransformedDate) | ✅ Public | ✅ Has Story | ✅ Has Tests | +| Component | [TransformedDate](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/TransformedDate) | ✅ Public | ✅ Has Story | ❌ No Tests | +| Component | [useAddLoginLink](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/useAddLoginLink) | ✅ Public | ❌ No Story | ❌ No Tests | +| Component | [useScrollToTop](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/useScrollToTop) | ✅ Public | ❌ No Story | ❌ No Tests | | | | | | | | **HOOKS & CONTEXTS** | | | | | -| Context | [ACAContext](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/utilities/ACAContext.ts) | ✅ Public | ❌ No Story | ❌ No Tests | -| Context | [DataTableActionsProvider](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DatasetTableTab/DataTableActionsContext.tsx) | ✅ Public | ❌ No Story | ✅ Has Tests | +| Context | [DataTableActionsProvider](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/DatasetTableTab/DataTableActionsContext.tsx) | ✅ Public | ❌ No Story | ❌ No Tests | +| Hook | [useAddLoginLink](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/useAddLoginLink) | ✅ Public | ❌ No Story | ❌ No Tests | +| Hook | [useScrollToTop](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/useScrollToTop) | ✅ Public | ❌ No Story | ❌ No Tests | | Context | [DataTableContext](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/templates/Dataset/DataTableContext.tsx) | ✅ Public | ❌ No Story | ❌ No Tests | | Context | [HeaderContext](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/templates/Header/HeaderContext.tsx) | ✅ Public | ❌ No Story | ❌ No Tests | -| Hook | [useAddLoginLink](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/useAddLoginLink) | ✅ Public | ❌ No Story | ✅ Has Tests | -| Hook | [useScrollToTop](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/components/useScrollToTop) | ✅ Public | ❌ No Story | ✅ Has Tests | +| Context | [ACAContext](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/utilities/ACAContext.ts) | ✅ Public | ❌ No Story | ❌ No Tests | | | | | | | | **SERVICES** | | | | | -| Service | [useDatastore](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/services/useDatastore) | ✅ Public | ❌ No Story | ✅ Has Tests | -| Service | [useMetastoreDataset](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/services/useMetastoreDataset) | ✅ Public | ❌ No Story | ✅ Has Tests | -| Service | [useSearchAPI](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/services/useSearchAPI) | ✅ Public | ❌ No Story | ✅ Has Tests | +| Service | [useDatastore](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/services/useDatastore) | ✅ Public | ❌ No Story | ❌ No Tests | +| Service | [useMetastoreDataset](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/services/useMetastoreDataset) | ✅ Public | ❌ No Story | ❌ No Tests | +| Service | [useSearchAPI](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/services/useSearchAPI) | ✅ Public | ❌ No Story | ❌ No Tests | | | | | | | | **TEMPLATES** | | | | | -| Template | [APIPage](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/templates/APIPage) | ✅ Public | ✅ Has Story | ✅ Has Tests | +| Template | [APIPage](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/templates/APIPage) | ✅ Public | ✅ Has Story | ❌ No Tests | | Template | [Dataset](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/templates/Dataset) | ✅ Public | ✅ Has Story | ✅ Has Tests | -| Template | [DatasetList](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/templates/DatasetList) | ✅ Public | ✅ Has Story | ✅ Has Tests | +| Template | [DatasetList](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/templates/DatasetList) | ✅ Public | ✅ Has Story | ❌ No Tests | | Template | [DatasetSearch](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/templates/DatasetSearch) | ✅ Public | ✅ Has Story | ✅ Has Tests | -| Template | [FilteredResource](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/templates/FilteredResource) | ✅ Public | ❌ No Story | ✅ Has Tests | -| Template | [Footer](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/templates/Footer) | ✅ Public | ✅ Has Story | ✅ Has Tests | -| Template | [Header](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/templates/Header) | ✅ Public | ✅ Has Story | ✅ Has Tests | -| Template | [PageNotFound](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/templates/PageNotFound) | ✅ Public | ✅ Has Story | ✅ Has Tests | -| Template | [SidebarPage](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/templates/SidebarPage) | ✅ Public | ✅ Has Story | ✅ Has Tests | +| Template | [FilteredResource](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/templates/FilteredResource) | ✅ Public | ✅ Has Story | ❌ No Tests | +| Template | [Footer](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/templates/Footer) | ✅ Public | ✅ Has Story | ❌ No Tests | +| Template | [Header](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/templates/Header) | ✅ Public | ✅ Has Story | ❌ No Tests | +| Template | [PageNotFound](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/templates/PageNotFound) | ✅ Public | ✅ Has Story | ❌ No Tests | +| Template | [SidebarPage](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/templates/SidebarPage) | ✅ Public | ✅ Has Story | ❌ No Tests | | Template | [SpecsAndLimits](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/templates/SpecsAndLimits) | ✅ Public | ✅ Has Story | ✅ Has Tests | -| Template | [StoredQueryPage](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/templates/StoredQueryPage) | ✅ Public | ✅ Has Story | ✅ Has Tests | +| Template | [StoredQueryPage](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/templates/StoredQueryPage) | ✅ Public | ✅ Has Story | ❌ No Tests | | | | | | | | **TYPES** | | | | | | Type Definition | [dataset.ts](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/types/dataset.ts) | ❌ Internal | ❌ No Story | ❌ No Tests | @@ -102,12 +104,13 @@ This document provides a comprehensive inventory of all components, services, te | Type Definition | [search.ts](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/types/search.ts) | ❌ Internal | ❌ No Story | ❌ No Tests | | | | | | | | **UTILITIES** | | | | | -| Utility | [aca](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/utilities/aca.ts) | ✅ Public (acaToParams) | ❌ No Story | ✅ Has Tests | -| Utility | [ApiDocsSwaggerUIPlugin](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/utilities/ApiDocsSwaggerUIPlugin) | ❌ Internal | ❌ No Story | ✅ Has Tests | -| Utility | [datasetSearchReq](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/utilities/datasetSearchReq) | ❌ Internal | ❌ No Story | ✅ Has Tests | -| Utility | [format](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/utilities/format.ts) | ❌ Internal | ❌ No Story | ✅ Has Tests | -| Utility | [QueryProvider](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/utilities/QueryProvider) | ❌ Internal | ❌ No Story | ✅ Has Tests | -| Utility | [restoreFullscreenDialogScrollLock](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/utilities/restoreFullscreenDialogScrollLock.ts) | ❌ Internal | ❌ No Story | ✅ Has Tests | +| Utility | [aca](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/utilities/aca.ts) | ✅ Public (acaToParams) | ❌ No Story | ❌ No Tests | +| Utility | [ACAContext](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/utilities/ACAContext.ts) | ✅ Public | ❌ No Story | ❌ No Tests | +| Utility | [ApiDocsSwaggerUIPlugin](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/utilities/ApiDocsSwaggerUIPlugin) | ❌ Internal | ❌ No Story | ❌ No Tests | +| Utility | [datasetSearchReq](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/utilities/datasetSearchReq) | ❌ Internal | ❌ No Story | ❌ No Tests | +| Utility | [format](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/utilities/format.ts) | ❌ Internal | ❌ No Story | ❌ No Tests | +| Utility | [QueryProvider](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/utilities/QueryProvider) | ❌ Internal | ❌ No Story | ❌ No Tests | +| Utility | [restoreFullscreenDialogScrollLock](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/utilities/restoreFullscreenDialogScrollLock.ts) | ❌ Internal | ❌ No Story | ❌ No Tests | | | | | | | | **ASSETS** | | | | | | Asset | [frequencyMap](https://github.com/GetDKAN/cmsds-open-data-components/tree/main/src/assets/frequencyMap.js) | ✅ Public (commented out) | ❌ No Story | ❌ No Tests | @@ -120,19 +123,19 @@ This document provides a comprehensive inventory of all components, services, te | Category | Total | With Stories | With Tests | With Both | With Neither | |----------|-------|--------------|------------|-----------|--------------| -| Components | 62 | 41 (66%) | 61 (98%) | 41 (66%) | 1 (2%) | -| Templates | 11 | 10 (91%) | 11 (100%) | 10 (91%) | 0 (0%) | -| Services/Hooks/Contexts | 9 | 0 (0%) | 6 (67%) | 0 (0%) | 3 (33%) | -| Utilities/Types/Assets | 11 | 0 (0%) | 6 (55%) | 0 (0%) | 5 (45%) | -| **Project Total** | **93** | **51 (55%)** | **84 (90%)** | **51 (55%)** | **9 (10%)** | +| Components | 64 | 58 (91%) | 27 (42%) | 25 (39%) | 4 (6%) | +| Templates | 11 | 11 (100%) | 3 (27%) | 3 (27%) | 0 (0%) | +| Services/Hooks/Contexts | 9 | 0 (0%) | 0 (0%) | 0 (0%) | 9 (100%) | +| Utilities/Types/Assets | 12 | 0 (0%) | 0 (0%) | 0 (0%) | 12 (100%) | +| **Project Total** | **96** | **69 (72%)** | **30 (31%)** | **28 (29%)** | **25 (26%)** | --- ### Export Summary -- **Public**: 55 items (32 components, 11 templates, 3 services, 2 hooks, 4 contexts, 2 asset) +- **Public**: 58 items (34 components, 11 templates, 3 services, 2 hooks, 4 contexts, 2 asset) - **Internal**: 38 items (30 components, 0 templates, 0 services, 5 utilities, 3 types, 0 asset) --- -*Last updated: May 29, 2026* +*Last updated: May 24, 2026* *Repository: [GetDKAN/cmsds-open-data-components](https://github.com/GetDKAN/cmsds-open-data-components)* diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..adb34395 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,16 @@ +// Minimal flat config for ESLint v9+/v10. +// Scoped to Storybook story files via eslint-plugin-storybook's recommended preset. +// The legacy .eslintrc.js and package.json#eslintConfig are not read by ESLint v9+. +import storybook from 'eslint-plugin-storybook'; +import tsParser from '@typescript-eslint/parser'; + +export default [ + ...storybook.configs['flat/recommended'], + { + files: ['**/*.{ts,tsx}'], + languageOptions: { + parser: tsParser, + parserOptions: { ecmaFeatures: { jsx: true } }, + }, + }, +]; diff --git a/package-lock.json b/package-lock.json index 15dd4497..e835746c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,6 @@ "@parcel/transformer-sass": "^2.16.3", "@parcel/transformer-typescript-types": "^2.16.4", "@storybook/addon-docs": "^9.0.16", - "@storybook/builder-vite": "^9.0.16", "@storybook/react-vite": "^9.1.5", "@testing-library/dom": "^9.3.3", "@testing-library/jest-dom": "^6.6.4", @@ -55,9 +54,11 @@ "@types/react-datepicker": "^4.19.5", "@types/react-text-truncate": "^0.14.4", "@types/swagger-ui-react": "^5.18.0", + "@typescript-eslint/parser": "^8.59.4", "@vitejs/plugin-react": "^5.1.1", "core-js": "^3.27.2", "css-loader": "^6.7.3", + "eslint": "^9.39.4", "eslint-plugin-storybook": "^9.0.16", "husky": "^9.1.7", "jest": "30.0.5", @@ -2801,79 +2802,205 @@ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/config-array": { - "version": "0.23.5", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", - "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { - "@eslint/object-schema": "^3.0.5", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^10.2.4" + "minimatch": "^3.1.5" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, "node_modules/@eslint/config-helpers": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", - "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { - "@eslint/core": "^1.2.1" + "@eslint/core": "^0.17.0" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", - "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.15" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", - "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", - "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { - "@eslint/core": "^1.2.1", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@fortawesome/fontawesome-free": { @@ -2892,7 +3019,6 @@ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@humanfs/types": "^0.15.0" }, @@ -2906,7 +3032,6 @@ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", @@ -2922,7 +3047,6 @@ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=18.18.0" } @@ -2933,7 +3057,6 @@ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=12.22" }, @@ -2948,7 +3071,6 @@ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=18.18" }, @@ -8633,14 +8755,6 @@ "@types/estree": "*" } }, - "node_modules/@types/esrecurse": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -8918,6 +9032,161 @@ "dev": true, "license": "MIT" }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.4.tgz", + "integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.4.tgz", + "integrity": "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.4", + "@typescript-eslint/types": "^8.59.4", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz", + "integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz", + "integrity": "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz", + "integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz", + "integrity": "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.4", + "@typescript-eslint/tsconfig-utils": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz", + "integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@typescript-eslint/project-service": { "version": "8.59.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz", @@ -9689,7 +9958,6 @@ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -11121,8 +11389,7 @@ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/deepmerge": { "version": "4.3.1", @@ -11580,7 +11847,6 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -11589,31 +11855,33 @@ } }, "node_modules/eslint": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.3.0.tgz", - "integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.5.5", - "@eslint/core": "^1.2.1", - "@eslint/plugin-kit": "^0.7.1", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", + "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.2", - "eslint-visitor-keys": "^5.0.1", - "espree": "^11.2.0", - "esquery": "^1.7.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", @@ -11623,7 +11891,8 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -11631,7 +11900,7 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://eslint.org/donate" @@ -11663,20 +11932,17 @@ } }, "node_modules/eslint-scope": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -11695,13 +11961,76 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/eslint/node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -11719,7 +12048,6 @@ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -11730,13 +12058,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/eslint/node_modules/p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -11753,25 +12093,36 @@ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } }, "node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { - "acorn": "^8.16.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" + "eslint-visitor-keys": "^4.2.1" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -11797,7 +12148,6 @@ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -11811,7 +12161,6 @@ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -11825,7 +12174,6 @@ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -12066,8 +12414,7 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/fast-string-truncated-width": { "version": "3.0.3", @@ -12171,7 +12518,6 @@ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "flat-cache": "^4.0.0" }, @@ -12225,7 +12571,6 @@ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" @@ -12239,8 +12584,7 @@ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/focus-trap": { "version": "7.8.0", @@ -12502,7 +12846,6 @@ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -12930,7 +13273,6 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "license": "MIT", - "peer": true, "engines": { "node": ">= 4" } @@ -12941,6 +13283,33 @@ "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", "license": "MIT" }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -15395,8 +15764,7 @@ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -15417,8 +15785,7 @@ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", @@ -15439,7 +15806,6 @@ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "json-buffer": "3.0.1" } @@ -15460,7 +15826,6 @@ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -16760,7 +17125,6 @@ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -16970,6 +17334,19 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parse-entities": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", @@ -17373,7 +17750,6 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8.0" } @@ -19994,7 +20370,6 @@ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -21299,7 +21674,6 @@ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } diff --git a/package.json b/package.json index b244545a..c35226d9 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "test:coverage": "npm run test -- --coverage", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", + "lint:stories": "eslint 'src/**/*.stories.@(ts|tsx|js|jsx)' '.storybook/**/*.@(ts|tsx)'", "generate:inventory": "node scripts/generate-inventory.cjs", "generate:usage-report": "node scripts/generate-usage-report.cjs", "prepare": "husky" @@ -55,7 +56,6 @@ "@parcel/transformer-sass": "^2.16.3", "@parcel/transformer-typescript-types": "^2.16.4", "@storybook/addon-docs": "^9.0.16", - "@storybook/builder-vite": "^9.0.16", "@storybook/react-vite": "^9.1.5", "@testing-library/dom": "^9.3.3", "@testing-library/jest-dom": "^6.6.4", @@ -68,9 +68,11 @@ "@types/react-datepicker": "^4.19.5", "@types/react-text-truncate": "^0.14.4", "@types/swagger-ui-react": "^5.18.0", + "@typescript-eslint/parser": "^8.59.4", "@vitejs/plugin-react": "^5.1.1", "core-js": "^3.27.2", "css-loader": "^6.7.3", + "eslint": "^9.39.4", "eslint-plugin-storybook": "^9.0.16", "husky": "^9.1.7", "jest": "30.0.5", diff --git a/scripts/generate-inventory.cjs b/scripts/generate-inventory.cjs index 432e1313..56fc05d5 100755 --- a/scripts/generate-inventory.cjs +++ b/scripts/generate-inventory.cjs @@ -96,13 +96,21 @@ function getPublicExports() { return exports; } -// Check if a component/template has a Storybook story +// Check if a component/template has a Storybook story. +// Recurses one level into subdirectories so wrapper components like DataDictionary +// (which delegates to sub-components in nested folders) are correctly reported. function hasStory(dirPath) { try { - const files = fs.readdirSync(dirPath); - return files.some(file => - STORY_FILE_PATTERNS.some(pattern => file.endsWith(pattern)) + const entries = fs.readdirSync(dirPath, { withFileTypes: true }); + const directMatch = entries.some(e => + e.isFile() && STORY_FILE_PATTERNS.some(p => e.name.endsWith(p)) ); + if (directMatch) return true; + return entries.some(e => { + if (!e.isDirectory()) return false; + const subEntries = fs.readdirSync(path.join(dirPath, e.name)); + return subEntries.some(name => STORY_FILE_PATTERNS.some(p => name.endsWith(p))); + }); } catch (e) { return false; } diff --git a/src/components/ApiDocumentation/ApiDocumentation.stories.tsx b/src/components/ApiDocumentation/ApiDocumentation.stories.tsx new file mode 100644 index 00000000..2ed000d3 --- /dev/null +++ b/src/components/ApiDocumentation/ApiDocumentation.stories.tsx @@ -0,0 +1,44 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import ApiDocumentation from './index'; +import { createAPIPageHandlers } from '../../../.storybook/mswHandlers'; +import { mockOpenAPISpec, mockOpenAPISpecWithAuth } from '../../../__mocks__/mockOpenAPISpec'; + +const meta: Meta = { + title: 'Components/ApiDocumentation', + component: ApiDocumentation, + parameters: { + layout: 'fullscreen', + msw: { handlers: createAPIPageHandlers(mockOpenAPISpec, mockOpenAPISpecWithAuth) }, + docs: { + description: { + component: + 'Renders Swagger UI from an OpenAPI spec endpoint with the project\'s custom plugins (version stamps, row-limit notice, doc links).', + }, + }, + }, + argTypes: { + endpoint: { control: 'text', description: 'OpenAPI spec URL.' }, + docsURL: { control: 'text', description: 'URL used by the "View documentation" links inside the spec.' }, + showRowLimitNotice: { control: 'boolean', description: 'Show the API row-limit notice above operations.' }, + }, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + endpoint: '/openapi.json', + docsURL: '/api-docs', + showRowLimitNotice: false, + }, +}; + +export const WithRowLimitNotice: Story = { + args: { + endpoint: '/openapi.json', + docsURL: '/api-docs', + showRowLimitNotice: true, + }, +}; diff --git a/src/components/ApiRowLimitNotice/ApiRowLimitNotice.stories.tsx b/src/components/ApiRowLimitNotice/ApiRowLimitNotice.stories.tsx index 80a7e204..ce3c3e72 100644 --- a/src/components/ApiRowLimitNotice/ApiRowLimitNotice.stories.tsx +++ b/src/components/ApiRowLimitNotice/ApiRowLimitNotice.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import ApiRowLimitNotice from './index'; const meta: Meta = { diff --git a/src/components/Breadcrumb/Breadcrumb.stories.jsx b/src/components/Breadcrumb/Breadcrumb.stories.tsx similarity index 87% rename from src/components/Breadcrumb/Breadcrumb.stories.jsx rename to src/components/Breadcrumb/Breadcrumb.stories.tsx index 2c3fcf94..b2b5b5dd 100644 --- a/src/components/Breadcrumb/Breadcrumb.stories.jsx +++ b/src/components/Breadcrumb/Breadcrumb.stories.tsx @@ -1,8 +1,8 @@ -import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { MemoryRouter } from 'react-router-dom'; import Breadcrumb from './index'; -const meta = { +const meta: Meta = { title: 'Components/Breadcrumb', component: Breadcrumb, parameters: { @@ -26,8 +26,9 @@ The Breadcrumb component provides navigation links showing the user's current lo }; export default meta; +type Story = StoryObj; -export const Default = { +export const Default: Story = { args: { currentPage: 'Current Page', pageTrail: [ @@ -44,7 +45,7 @@ export const Default = { }, }; -export const NoTrail = { +export const NoTrail: Story = { args: { currentPage: 'Current Page', pageTrail: [], @@ -58,7 +59,7 @@ export const NoTrail = { }, }; -export const LongTrail = { +export const LongTrail: Story = { args: { currentPage: 'Current Page', pageTrail: [ diff --git a/src/components/CMSTopNav/CMSTopNav.stories.jsx b/src/components/CMSTopNav/CMSTopNav.stories.jsx deleted file mode 100644 index 8390d7aa..00000000 --- a/src/components/CMSTopNav/CMSTopNav.stories.jsx +++ /dev/null @@ -1,201 +0,0 @@ -import React from 'react'; -import { MemoryRouter } from 'react-router-dom'; -import CMSTopNav from './index'; -import logoImage from '../../assets/images/CMSGovLogo-O.png'; - -const meta = { - title: 'Components/CMSTopNav', - component: CMSTopNav, - parameters: { - layout: 'fullscreen', - docs: { - description: { - component: ` -The CMSTopNav component provides the top navigation bar for CMS websites. It displays the organization logo, tagline, and a horizontal navigation menu with links. The component is responsive and adapts to different screen sizes. - `, - }, - }, - }, - decorators: [ - (Story) => ( - - - - ), - ], - tags: ['autodocs'], -}; - -export default meta; - -export const Default = { - args: { - org: { - url: '/', - urlTitle: 'CMSDS', - logoAltText: 'CMSDS Logo', - logoFilePath: logoImage, - tagline: 'CMSDS Tagline', - }, - links: [ - { - id: '1', - label: 'Home', - url: '/', - }, - { - id: '2', - label: 'About', - url: '/about', - }, - { - id: '3', - label: 'Contact', - url: '/contact', - }, - ], - }, - parameters: { - docs: { - description: { - story: 'The default CMSTopNav component with organization information and navigation links.', - }, - }, - }, -}; - -export const WithManyLinks = { - args: { - org: { - url: '/', - urlTitle: 'CMSDS', - logoAltText: 'CMSDS Logo', - logoFilePath: logoImage, - tagline: 'CMSDS Tagline', - }, - links: [ - { - id: '1', - label: 'Home', - url: '/', - }, - { - id: '2', - label: 'About', - url: '/about', - }, - { - id: '3', - label: 'Services', - url: '/services', - }, - { - id: '4', - label: 'Resources', - url: '/resources', - }, - { - id: '5', - label: 'News', - url: '/news', - }, - { - id: '6', - label: 'Contact', - url: '/contact', - }, - ], - }, - parameters: { - docs: { - description: { - story: 'A CMSTopNav component with multiple navigation links to demonstrate horizontal scrolling behavior.', - }, - }, - }, -}; - -export const MinimalOrg = { - args: { - org: { - url: '/', - urlTitle: 'CMS', - tagline: 'CMS', - }, - links: [ - { - id: '1', - label: 'Home', - url: '/', - }, - { - id: '2', - label: 'About', - url: '/about', - }, - ], - }, - parameters: { - docs: { - description: { - story: 'A CMSTopNav component with minimal organization information (no logo) and few navigation links.', - }, - }, - }, -}; - -export const NoLinks = { - args: { - org: { - url: '/', - urlTitle: 'CMSDS', - logoAltText: 'CMSDS Logo', - logoFilePath: logoImage, - tagline: 'CMSDS Tagline', - }, - links: [], - }, - parameters: { - docs: { - description: { - story: 'A CMSTopNav component with no navigation links, showing only the organization branding.', - }, - }, - }, -}; - -export const LongTagline = { - args: { - org: { - url: '/', - urlTitle: 'CMSDS', - logoAltText: 'CMSDS Logo', - logoFilePath: logoImage, - tagline: 'CMSDS Tagline', - }, - links: [ - { - id: '1', - label: 'Home', - url: '/', - }, - { - id: '2', - label: 'About', - url: '/about', - }, - { - id: '3', - label: 'Contact', - url: '/contact', - }, - ], - }, - parameters: { - docs: { - description: { - story: 'A CMSTopNav component with a longer tagline to test text wrapping and layout behavior.', - }, - }, - }, -}; diff --git a/src/components/CMSTopNav/CMSTopNav.stories.tsx b/src/components/CMSTopNav/CMSTopNav.stories.tsx new file mode 100644 index 00000000..07336b8f --- /dev/null +++ b/src/components/CMSTopNav/CMSTopNav.stories.tsx @@ -0,0 +1,128 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { MemoryRouter } from 'react-router-dom'; +import CMSTopNav from './index'; +import logoImage from '../../assets/images/CMSGovLogo-O.png'; + +const meta: Meta = { + title: 'Components/CMSTopNav', + component: CMSTopNav, + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: ` +The CMSTopNav component provides the top navigation bar for CMS websites. It displays the organization logo, tagline, and a horizontal navigation menu with links. The component is responsive and adapts to different screen sizes. + `, + }, + }, + }, + decorators: [ + (Story) => ( + + + + ), + ], + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +const sampleOrg = { + url: '/', + urlTitle: 'CMSDS', + logoAltText: 'CMSDS Logo', + logoFilePath: logoImage, + tagline: 'CMSDS Tagline', +}; + +const defaultLinks = [ + { id: '1', label: 'Home', url: '/' }, + { id: '2', label: 'About', url: '/about' }, + { id: '3', label: 'Contact', url: '/contact' }, +]; + +export const Default: Story = { + args: { + org: sampleOrg, + links: defaultLinks, + }, + parameters: { + docs: { + description: { + story: 'The default CMSTopNav component with organization information and navigation links.', + }, + }, + }, +}; + +export const WithManyLinks: Story = { + args: { + org: sampleOrg, + links: [ + { id: '1', label: 'Home', url: '/' }, + { id: '2', label: 'About', url: '/about' }, + { id: '3', label: 'Services', url: '/services' }, + { id: '4', label: 'Resources', url: '/resources' }, + { id: '5', label: 'News', url: '/news' }, + { id: '6', label: 'Contact', url: '/contact' }, + ], + }, + parameters: { + docs: { + description: { + story: 'A CMSTopNav component with multiple navigation links to demonstrate horizontal scrolling behavior.', + }, + }, + }, +}; + +export const MinimalOrg: Story = { + args: { + org: { + url: '/', + urlTitle: 'CMS', + tagline: 'CMS', + }, + links: [ + { id: '1', label: 'Home', url: '/' }, + { id: '2', label: 'About', url: '/about' }, + ], + }, + parameters: { + docs: { + description: { + story: 'A CMSTopNav component with minimal organization information (no logo) and few navigation links.', + }, + }, + }, +}; + +export const NoLinks: Story = { + args: { + org: sampleOrg, + links: [], + }, + parameters: { + docs: { + description: { + story: 'A CMSTopNav component with no navigation links, showing only the organization branding.', + }, + }, + }, +}; + +export const LongTagline: Story = { + args: { + org: sampleOrg, + links: defaultLinks, + }, + parameters: { + docs: { + description: { + story: 'A CMSTopNav component with a longer tagline to test text wrapping and layout behavior.', + }, + }, + }, +}; diff --git a/src/components/DataDictionary/DataDictionaryTable/DataDictionaryTable.stories.tsx b/src/components/DataDictionary/DataDictionaryTable/DataDictionaryTable.stories.tsx index 76d2730f..0ecc458e 100644 --- a/src/components/DataDictionary/DataDictionaryTable/DataDictionaryTable.stories.tsx +++ b/src/components/DataDictionary/DataDictionaryTable/DataDictionaryTable.stories.tsx @@ -1,14 +1,16 @@ -import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import DataDictionaryTable from './index'; -const meta = { +const meta: Meta = { title: 'Components/DataDictionaryTable', component: DataDictionaryTable, parameters: { layout: 'padded', docs: { description: { - component: `\nThe DataDictionaryTable component displays a paginated, sortable, and filterable table for data dictionary entries.\n `, + component: ` +The DataDictionaryTable component displays a paginated, sortable, and filterable table for data dictionary entries. + `, }, }, }, @@ -22,8 +24,9 @@ const meta = { }; export default meta; +type Story = StoryObj; -export const Default = { +export const Default: Story = { args: { tableColumns: [ { accessorKey: 'field', header: 'Field' }, diff --git a/src/components/DataDictionary/DatasetDictionaryJSON/DatasetDictionaryJSON.stories.tsx b/src/components/DataDictionary/DatasetDictionaryJSON/DatasetDictionaryJSON.stories.tsx index 5eb3033f..ce3a810a 100644 --- a/src/components/DataDictionary/DatasetDictionaryJSON/DatasetDictionaryJSON.stories.tsx +++ b/src/components/DataDictionary/DatasetDictionaryJSON/DatasetDictionaryJSON.stories.tsx @@ -1,42 +1,46 @@ -import React from 'react'; -import DatasetDictionaryJSON from './index'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { QueryClientProvider } from '@tanstack/react-query'; +import DatasetDictionaryJSON from './index'; import { createStorybookQueryClient } from '../../../../.storybook/queryClient'; const queryClient = createStorybookQueryClient(); -const meta = { +const meta: Meta = { title: 'Components/DatasetDictionaryJSON', component: DatasetDictionaryJSON, decorators: [ - (Story: React.ComponentType) => ( - - - - ), - ], + (Story) => ( + + + + ), + ], parameters: { layout: 'padded', docs: { description: { - component: `\nThe DatasetDictionaryJSON component displays a filterable and paginated table for dataset dictionary entries.\n `, + component: ` +The DatasetDictionaryJSON component displays a filterable and paginated table for dataset dictionary entries. + `, }, }, }, argTypes: { - datasetDictionaryEndpoint: { control: 'string', description: 'Endpoint for data dictionary information' }, + datasetDictionaryEndpoint: { control: 'text', description: 'Endpoint for data dictionary information' }, pageSize: { control: 'number', description: 'Number of rows per page.' }, - showDownloadButton: { control: 'boolean', description: 'Whether to display the download button at the top of the page'} + showDownloadButton: { control: 'boolean', description: 'Whether to display the download button at the top of the page' }, }, tags: ['autodocs'], }; export default meta; +type Story = StoryObj; -export const Default = { +export const Default: Story = { args: { - datasetDictionaryEndpoint: "https://data.medicaid.gov/api/1/metastore/schemas/data-dictionary/items/5b71cde4-43f2-4877-9059-5830079223e9", + datasetDictionaryEndpoint: + 'https://data.medicaid.gov/api/1/metastore/schemas/data-dictionary/items/5b71cde4-43f2-4877-9059-5830079223e9', pageSize: 10, - showDownloadButton: true + showDownloadButton: true, }, }; diff --git a/src/components/DataDictionary/DatasetDictionaryPDF/DatasetDictionaryPDF.stories.tsx b/src/components/DataDictionary/DatasetDictionaryPDF/DatasetDictionaryPDF.stories.tsx index 99b53999..7bf870c7 100644 --- a/src/components/DataDictionary/DatasetDictionaryPDF/DatasetDictionaryPDF.stories.tsx +++ b/src/components/DataDictionary/DatasetDictionaryPDF/DatasetDictionaryPDF.stories.tsx @@ -1,28 +1,31 @@ -import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import DatasetDictionaryPDF from './index'; -const meta = { +const meta: Meta = { title: 'Components/DatasetDictionaryPDF', component: DatasetDictionaryPDF, parameters: { layout: 'padded', docs: { description: { - component: `\nThe DatasetDictionaryPDF component displays an iframe to load and display a pdf data dictionary file.\n `, + component: ` +The DatasetDictionaryPDF component displays an iframe to load and display a pdf data dictionary file. + `, }, }, }, argTypes: { - datasetDictionaryEndpoing: { control: 'string', description: 'Location of the PDF file' }, + datasetDictionaryEndpoint: { control: 'text', description: 'Location of the PDF file' }, }, tags: ['autodocs'], }; export default meta; +type Story = StoryObj; -export const Default = { +export const Default: Story = { args: { - datasetDictionaryEndpoint: "test" + datasetDictionaryEndpoint: 'test', }, parameters: { docs: { diff --git a/src/components/DataTableControls/DataTableControls.stories.tsx b/src/components/DataTableControls/DataTableControls.stories.tsx new file mode 100644 index 00000000..a4e701cb --- /dev/null +++ b/src/components/DataTableControls/DataTableControls.stories.tsx @@ -0,0 +1,59 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import DataTableControls from './index'; +import { withDataTableContexts } from '../../../.storybook/decorators'; + +const columnIds = ['npn', 'applicable_plan_year', 'individual_registration_completion_date']; + +const makeMockColumns = (visibility: Record = {}) => + columnIds.map((id) => ({ + id, + getIsVisible: () => visibility[id] !== false, + })); + +const meta: Meta = { + title: 'Components/DataTableControls', + component: DataTableControls, + parameters: { + layout: 'padded', + docs: { + description: { + component: + 'Bottom-of-table controls bar — surfaces a hidden-columns alert plus the ManageColumns and FullScreenDataTable buttons.', + }, + }, + }, + decorators: [withDataTableContexts()], + argTypes: { + id: { control: 'text' }, + columns: { control: false }, + defaultColumnOrder: { control: 'object' }, + isModal: { control: 'boolean' }, + }, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + id: 'wb6u-x2ny', + columns: makeMockColumns() as never, + defaultColumnOrder: columnIds, + isModal: false, + }, +}; + +export const WithHiddenColumns: Story = { + args: { + id: 'wb6u-x2ny', + columns: makeMockColumns({ applicable_plan_year: false }) as never, + defaultColumnOrder: columnIds, + isModal: false, + }, + parameters: { + docs: { + description: { story: 'Shows the "1 Columns Hidden" warning alert when any column is not visible.' }, + }, + }, +}; diff --git a/src/components/DataTableDensity/DataTableDensity.stories.jsx b/src/components/DataTableDensity/DataTableDensity.stories.tsx similarity index 58% rename from src/components/DataTableDensity/DataTableDensity.stories.jsx rename to src/components/DataTableDensity/DataTableDensity.stories.tsx index 4ba96616..95e6b136 100644 --- a/src/components/DataTableDensity/DataTableDensity.stories.jsx +++ b/src/components/DataTableDensity/DataTableDensity.stories.tsx @@ -1,14 +1,17 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import DataTableDensity from './index'; -const meta = { +const meta: Meta = { title: 'Components/DataTableDensity', component: DataTableDensity, parameters: { layout: 'padded', docs: { description: { - component: `\nThe DataTableDensity component provides a dropdown to select display density for a data table.\n `, + component: ` +The DataTableDensity component provides a dropdown to select display density for a data table. + `, }, }, }, @@ -20,10 +23,11 @@ const meta = { }; export default meta; +type Story = StoryObj; -export const Default = { +export const Default: Story = { render: (args) => { - const [tablePadding, setTablePadding] = useState('ds-u-padding-y--1'); + const [tablePadding, setTablePadding] = useState(args.tablePadding ?? 'ds-u-padding-y--1'); return ; }, args: { diff --git a/src/components/DataTablePageResults/DataTablePageResults.stories.tsx b/src/components/DataTablePageResults/DataTablePageResults.stories.tsx index 01b6b3c5..785138f7 100644 --- a/src/components/DataTablePageResults/DataTablePageResults.stories.tsx +++ b/src/components/DataTablePageResults/DataTablePageResults.stories.tsx @@ -1,14 +1,16 @@ -import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import DataTablePageResults from './DataTablePageResults'; -const meta = { +const meta: Meta = { title: 'Components/DataTablePageResults', component: DataTablePageResults, parameters: { layout: 'padded', docs: { description: { - component: `\nThe DataTablePageResults component displays the current range of visible rows and the total number of results in a data table.\n `, + component: ` +The DataTablePageResults component displays the current range of visible rows and the total number of results in a data table. + `, }, }, }, @@ -22,39 +24,20 @@ const meta = { }; export default meta; +type Story = StoryObj; -export const Default = { - args: { - totalRows: 100, - limit: 25, - offset: 0, - className: 'data-table-results', - }, +export const Default: Story = { + args: { totalRows: 100, limit: 25, offset: 0, className: 'data-table-results' }, }; -export const MiddlePage = { - args: { - totalRows: 100, - limit: 25, - offset: 25, - className: 'data-table-results', - }, +export const MiddlePage: Story = { + args: { totalRows: 100, limit: 25, offset: 25, className: 'data-table-results' }, }; -export const LastPage = { - args: { - totalRows: 100, - limit: 25, - offset: 75, - className: 'data-table-results', - }, +export const LastPage: Story = { + args: { totalRows: 100, limit: 25, offset: 75, className: 'data-table-results' }, }; -export const NoResults = { - args: { - totalRows: 0, - limit: 25, - offset: 0, - className: 'data-table-results', - }, +export const NoResults: Story = { + args: { totalRows: 0, limit: 25, offset: 0, className: 'data-table-results' }, }; diff --git a/src/components/DataTableRowChanger/DataTableRowChanger.stories.tsx b/src/components/DataTableRowChanger/DataTableRowChanger.stories.tsx new file mode 100644 index 00000000..a754ecd8 --- /dev/null +++ b/src/components/DataTableRowChanger/DataTableRowChanger.stories.tsx @@ -0,0 +1,49 @@ +import { useState, type FC } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import DataTableRowChanger from './index'; + +type Props = { + limit: number; + rowOptions: number[]; + setLimit: (value: string | number) => void; +}; + +// Wrap to sidestep a TypeScript conflict between the component's PropTypes (declares +// `limit: number | null | undefined`) and its TS prop type (`DropdownValue`). +const DataTableRowChangerWrapped: FC = (props) => ; + +const meta: Meta = { + title: 'Components/DataTableRowChanger', + component: DataTableRowChangerWrapped, + parameters: { + layout: 'padded', + docs: { + description: { + component: 'Dropdown that lets the user pick the number of rows per page in a data table.', + }, + }, + }, + argTypes: { + limit: { control: 'number', description: 'Current page size.' }, + rowOptions: { control: 'object', description: 'Allowed page sizes.' }, + setLimit: { control: false, description: 'Setter invoked when the user picks a new size.' }, + }, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: (args) => { + const [limit, setLimit] = useState(args.limit ?? 25); + return ( + setLimit(Number(value))} + /> + ); + }, + args: { limit: 25, rowOptions: [10, 25, 50, 100] }, +}; diff --git a/src/components/DataTableToolbar/DataTableToolbar.stories.tsx b/src/components/DataTableToolbar/DataTableToolbar.stories.tsx index 4e5cf07e..7285e72c 100644 --- a/src/components/DataTableToolbar/DataTableToolbar.stories.tsx +++ b/src/components/DataTableToolbar/DataTableToolbar.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import React from 'react'; import { ResourceType, ColumnType } from '../../types/dataset'; import DataTablePageResults from '../DataTablePageResults'; diff --git a/src/components/DatasetAdditionalInformation/DatasetAdditionalInformation.stories.jsx b/src/components/DatasetAdditionalInformation/DatasetAdditionalInformation.stories.tsx similarity index 60% rename from src/components/DatasetAdditionalInformation/DatasetAdditionalInformation.stories.jsx rename to src/components/DatasetAdditionalInformation/DatasetAdditionalInformation.stories.tsx index 85848c7c..2d3cdccc 100644 --- a/src/components/DatasetAdditionalInformation/DatasetAdditionalInformation.stories.jsx +++ b/src/components/DatasetAdditionalInformation/DatasetAdditionalInformation.stories.tsx @@ -1,14 +1,16 @@ -import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import DatasetAdditionalInformation from './index'; -const meta = { +const meta: Meta = { title: 'Components/DatasetAdditionalInformation', component: DatasetAdditionalInformation, parameters: { layout: 'padded', docs: { description: { - component: `\nThe DatasetAdditionalInformation component displays a table of additional metadata fields for a dataset.\n `, + component: ` +The DatasetAdditionalInformation component displays a table of additional metadata fields for a dataset. + `, }, }, }, @@ -21,14 +23,15 @@ const meta = { }; export default meta; +type Story = StoryObj; const sampleMetadataMapping = { - publisher: (value) => [{ label: 'Publisher', value }], - contact: (value) => [{ label: 'Contact', value }], - releaseDate: (value) => [{ label: 'Release Date', value }], + publisher: (value: string) => [{ label: 'Publisher', value }], + contact: (value: string) => [{ label: 'Contact', value }], + releaseDate: (value: string) => [{ label: 'Release Date', value }], }; -export const Default = { +export const Default: Story = { args: { datasetInfo: { publisher: 'CMS', diff --git a/src/components/DatasetDate/DatasetDate.stories.tsx b/src/components/DatasetDate/DatasetDate.stories.tsx index 99b975e7..b11b47e6 100644 --- a/src/components/DatasetDate/DatasetDate.stories.tsx +++ b/src/components/DatasetDate/DatasetDate.stories.tsx @@ -1,5 +1,5 @@ import React from "react"; -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { MemoryRouter } from 'react-router-dom'; import DatasetDate from "."; diff --git a/src/components/DatasetDateItem/DatasetDateItem.stories.tsx b/src/components/DatasetDateItem/DatasetDateItem.stories.tsx new file mode 100644 index 00000000..d74cacaf --- /dev/null +++ b/src/components/DatasetDateItem/DatasetDateItem.stories.tsx @@ -0,0 +1,48 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import DatasetDateItem from './index'; + +const meta: Meta = { + title: 'Components/DatasetDateItem', + component: DatasetDateItem, + parameters: { + layout: 'padded', + docs: { + description: { + component: + 'Renders a single labeled dataset date (Last Modified, Released, or Planned Update) with an optional explanatory tooltip.', + }, + }, + }, + argTypes: { + type: { + control: 'select', + options: ['modified', 'released', 'refresh'], + description: 'Which date label to display.', + }, + date: { control: 'text', description: 'Date string (parsable by Date).' }, + boldLabel: { control: 'boolean', description: 'Render the label in bold.' }, + displayTooltips: { control: 'boolean', description: 'Show the explanatory tooltip icon.' }, + }, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + type: 'modified', + date: '2025-07-18T12:00:00Z', + boldLabel: false, + displayTooltips: true, + }, +}; + +export const WithoutTooltip: Story = { + args: { + type: 'released', + date: '2024-01-15T00:00:00Z', + boldLabel: true, + displayTooltips: false, + }, +}; diff --git a/src/components/DatasetDescription/DatasetDescription.stories.tsx b/src/components/DatasetDescription/DatasetDescription.stories.tsx index 3b452ea4..c14c0fa2 100644 --- a/src/components/DatasetDescription/DatasetDescription.stories.tsx +++ b/src/components/DatasetDescription/DatasetDescription.stories.tsx @@ -1,14 +1,16 @@ -import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import DatasetDescription from './index'; -const meta = { +const meta: Meta = { title: 'Components/DatasetDescription', component: DatasetDescription, parameters: { layout: 'padded', docs: { description: { - component: `\nThe DatasetDescription component displays a sanitized HTML description for a dataset or distribution.\n `, + component: ` +The DatasetDescription component displays a sanitized HTML description for a dataset or distribution. + `, }, }, }, @@ -16,27 +18,28 @@ const meta = { distribution: { control: 'object', description: 'Distribution object.' }, dataset: { control: 'object', description: 'Dataset object.' }, resource: { control: 'object', description: 'Resource object.' }, - customDescription: { control: 'function', description: 'Custom description function.' }, - updateAriaLive: { control: 'function', description: 'Aria live update function.' }, + customDescription: { control: false, description: 'Custom description function.' }, + updateAriaLive: { control: false, description: 'Aria live update function.' }, }, tags: ['autodocs'], }; export default meta; +type Story = StoryObj; -export const Default = { +export const Default: Story = { args: { distribution: { data: { - description: 'Distribution description with HTML.' - } - }, + description: 'Distribution description with HTML.', + }, + } as never, dataset: { identifier: 'ds-001', - description: 'Fallback dataset description.' - }, - resource: {}, - customDescription: null, - updateAriaLive: null, + description: 'Fallback dataset description.', + } as never, + resource: {} as never, + customDescription: undefined, + updateAriaLive: undefined, }, }; diff --git a/src/components/DatasetListItem/DatasetListItem.stories.tsx b/src/components/DatasetListItem/DatasetListItem.stories.tsx new file mode 100644 index 00000000..345d5a0d --- /dev/null +++ b/src/components/DatasetListItem/DatasetListItem.stories.tsx @@ -0,0 +1,64 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { MemoryRouter } from 'react-router-dom'; +import DatasetListItem from './index'; + +const meta: Meta = { + title: 'Components/DatasetListItem', + component: DatasetListItem, + parameters: { + layout: 'padded', + docs: { + description: { + component: + 'A single dataset list entry used in submenu/recent-update lists. Renders the title, last-modified date, and a link to the dataset detail page.', + }, + }, + }, + decorators: [ + (Story) => ( + +
    + +
+
+ ), + ], + argTypes: { + title: { control: 'text', description: 'Dataset title.' }, + modified: { control: 'text', description: 'Last-modified date string.' }, + identifier: { control: 'text', description: 'Dataset identifier used in the detail link.' }, + paginationEnabled: { control: 'boolean', description: 'Render the item with top border (pagination-list style).' }, + dataDictionaryLinks: { control: 'boolean', description: 'Adjust column widths to make room for data-dictionary links.' }, + }, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + title: 'Medicare Provider Utilization and Payment Data', + modified: '2025-04-15T14:30:00Z', + identifier: 'medicare-provider-utilization', + paginationEnabled: true, + dataDictionaryLinks: false, + }, +}; + +export const FirstItem: Story = { + args: { + title: 'Hospital Compare National Data', + modified: '2025-03-10T09:15:00Z', + identifier: 'hospital-compare', + paginationEnabled: false, + dataDictionaryLinks: false, + }, + parameters: { + docs: { + description: { + story: 'First item in an unpaginated list — renders with a bottom border instead of a top border.', + }, + }, + }, +}; diff --git a/src/components/DatasetListSubmenu/DatasetListSubmenu.stories.tsx b/src/components/DatasetListSubmenu/DatasetListSubmenu.stories.tsx new file mode 100644 index 00000000..0336392c --- /dev/null +++ b/src/components/DatasetListSubmenu/DatasetListSubmenu.stories.tsx @@ -0,0 +1,71 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { MemoryRouter } from 'react-router-dom'; +import DatasetListSubmenu from './index'; +import { createDatasetListHandlers } from '../../../.storybook/mswHandlers'; +import { + mockApiResponse, + mockEmptyResults, +} from '../../../__mocks__/mockDatasetSearchResults'; + +const meta: Meta = { + title: 'Components/DatasetListSubmenu', + component: DatasetListSubmenu, + parameters: { + layout: 'padded', + docs: { + description: { + component: + 'A compact dataset listing for header submenus. Fetches recent datasets from `${rootUrl}/search/`, displays up to `defaultPageSize` items, and links to the full listing.', + }, + }, + msw: { handlers: createDatasetListHandlers(mockApiResponse) }, + }, + decorators: [ + (Story) => ( + +
+ +
+
+ ), + ], + argTypes: { + rootUrl: { control: 'text', description: 'Base URL for the search API endpoint.' }, + enablePagination: { control: 'boolean', description: 'Show the "Viewing X of Y" pager.' }, + defaultPageSize: { control: 'number', description: 'Max number of items to show.' }, + subLinkClasses: { control: 'text', description: 'Classes applied to each item link.' }, + }, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + rootUrl: 'https://data.cms.gov', + enablePagination: true, + defaultPageSize: 4, + }, +}; + +// Excluded from autodocs: the shared module-scope QueryClient in `withQueryProvider` +// causes this story's empty-results MSW handler to bleed into the Default render on +// the docs page (identical queryKey → React Query dedupes; either handler can win). +// View this story standalone to see the empty state correctly. +export const NoResults: Story = { + args: { + rootUrl: 'https://data.cms.gov', + enablePagination: true, + defaultPageSize: 4, + }, + tags: ['!autodocs'], + parameters: { + msw: { handlers: createDatasetListHandlers(mockEmptyResults) }, + docs: { + description: { + story: 'Empty API response — renders the "No results found" alert.', + }, + }, + }, +}; diff --git a/src/components/DatasetListSubmenuItem/DatasetListSubmenuItem.stories.tsx b/src/components/DatasetListSubmenuItem/DatasetListSubmenuItem.stories.tsx index 3b3bee6f..ee8e0cf7 100644 --- a/src/components/DatasetListSubmenuItem/DatasetListSubmenuItem.stories.tsx +++ b/src/components/DatasetListSubmenuItem/DatasetListSubmenuItem.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import DatasetListSubmenuItem from './index'; import { MemoryRouter } from 'react-router-dom'; diff --git a/src/components/DatasetSearchFacets/DatasetSearchFacets.stories.tsx b/src/components/DatasetSearchFacets/DatasetSearchFacets.stories.tsx index dd9259f6..b9d0258a 100644 --- a/src/components/DatasetSearchFacets/DatasetSearchFacets.stories.tsx +++ b/src/components/DatasetSearchFacets/DatasetSearchFacets.stories.tsx @@ -1,37 +1,40 @@ -import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import SearchFacets from './index'; -const meta = { +const meta: Meta = { title: 'Components/DatasetSearchFacets', component: SearchFacets, parameters: { layout: 'padded', docs: { description: { - component: `\nThe DatasetSearchFacets component displays a list of filterable facets for dataset search results.\n `, + component: ` +The DatasetSearchFacets component displays a list of filterable facets for dataset search results. + `, }, }, }, argTypes: { facets: { control: 'object', description: 'Array of facet objects.' }, title: { control: 'text', description: 'Title for the facet group.' }, - onClickFunction: { control: 'function', description: 'Callback for facet selection.' }, + onClickFunction: { control: false, description: 'Callback for facet selection.' }, selectedFacets: { control: 'object', description: 'Array of selected facet names.' }, }, tags: ['autodocs'], }; export default meta; +type Story = StoryObj; -export const Default = { +export const Default: Story = { args: { facets: [ - { name: 'Theme A', total: 12, type: 'theme' }, - { name: 'Theme B', total: 5, type: 'theme' }, - { name: 'Theme C', total: 0, type: 'theme' }, + { name: 'Theme A', total: '12', type: 'theme' }, + { name: 'Theme B', total: '5', type: 'theme' }, + { name: 'Theme C', total: '0', type: 'theme' }, ], title: 'Themes', - onClickFunction: (type: string, name: string) => {}, + onClickFunction: () => {}, selectedFacets: ['Theme B'], }, }; diff --git a/src/components/DatasetSearchListItem/DatasetSearchListItem.stories.tsx b/src/components/DatasetSearchListItem/DatasetSearchListItem.stories.tsx new file mode 100644 index 00000000..ce985fb7 --- /dev/null +++ b/src/components/DatasetSearchListItem/DatasetSearchListItem.stories.tsx @@ -0,0 +1,109 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { MemoryRouter } from 'react-router-dom'; +import DatasetSearchListItem from './index'; + +const meta: Meta = { + title: 'Components/DatasetSearchListItem', + component: DatasetSearchListItem, + parameters: { + layout: 'padded', + docs: { + description: { + component: + 'A search-result list entry. Renders title, modified date, description, download button (with optional large-file dialog), and quick links to Data Table, Overview, Data Dictionary, and API.', + }, + }, + }, + decorators: [ + (Story) => ( + +
    + +
+
+ ), + ], + argTypes: { + title: { control: 'text' }, + modified: { control: 'text' }, + description: { control: 'text' }, + downloadUrl: { control: 'text' }, + identifier: { control: 'text' }, + largeFile: { control: 'boolean' }, + paginationEnabled: { control: 'boolean' }, + dataDictionaryLinks: { control: 'boolean' }, + showTopics: { control: 'boolean' }, + showDateDetails: { control: 'boolean' }, + updateDateMonthYearOnly: { control: 'boolean' }, + distribution: { control: 'object' }, + }, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +const csvDistribution = { + identifier: 'dist-1', + data: { + title: 'CSV Download', + format: 'csv', + downloadURL: '/files/dataset-1.csv', + describedBy: '/dictionaries/dataset-1.json', + describedByType: 'application/vnd.tableschema+json', + }, +}; + +export const Default: Story = { + args: { + title: 'Medicare Provider Utilization and Payment Data', + modified: '2025-03-15T14:30:00Z', + description: + 'Provider-level data on services and procedures provided to Medicare beneficiaries, aggregated by Healthcare Common Procedure Coding System (HCPCS) code.', + downloadUrl: '/files/medicare-provider.csv', + identifier: 'medicare-provider-utilization', + largeFile: false, + paginationEnabled: true, + dataDictionaryLinks: true, + distribution: csvDistribution, + }, +}; + +export const LongDescription: Story = { + args: { + ...(Default.args as object), + description: + 'This dataset contains an extensive description that exceeds the 240-character threshold for truncation. It includes detailed information about the contents, methodology, collection process, update cadence, and intended use cases — all of which would clutter a search result page if shown in full, so the component truncates and shows a See more link to the detail page for users who want the rest.', + } as Story['args'], + parameters: { + docs: { description: { story: 'Description longer than 240 chars triggers truncation with a "See more" link.' } }, + }, +}; + +export const LargeFileWithDialog: Story = { + args: { + ...(Default.args as object), + largeFile: true, + } as Story['args'], + parameters: { + docs: { description: { story: 'When `largeFile` is true the Download button is replaced with a LargeFileDialog warning.' } }, + }, +}; + +export const NonCSVDistribution: Story = { + args: { + ...(Default.args as object), + distribution: { + identifier: 'dist-2', + data: { title: 'JSON Download', format: 'json', downloadURL: '/files/dataset.json' }, + }, + dataDictionaryLinks: false, + } as Story['args'], + parameters: { + docs: { + description: { + story: 'Non-CSV distribution — the Data Table link becomes a disabled tooltip explaining only CSVs render a table.', + }, + }, + }, +}; diff --git a/src/components/DatasetTableTab/DatasetTableTab.stories.tsx b/src/components/DatasetTableTab/DatasetTableTab.stories.tsx index f324a1d5..0802f1e4 100644 --- a/src/components/DatasetTableTab/DatasetTableTab.stories.tsx +++ b/src/components/DatasetTableTab/DatasetTableTab.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import DatasetTable from './index'; import DataTableContext from '../../templates/Dataset/DataTableContext'; import { DataTableContextType } from '../../templates/Dataset/DataTableContext'; diff --git a/src/components/Datatable/Datatable.stories.tsx b/src/components/Datatable/Datatable.stories.tsx new file mode 100644 index 00000000..472bc733 --- /dev/null +++ b/src/components/Datatable/Datatable.stories.tsx @@ -0,0 +1,79 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import DataTable from './index'; +import { mockResource } from '../../../__mocks__/mockResource'; +import { + withDataTableContextsFromArgs, + defaultDataTableContext, +} from '../../../.storybook/decorators'; +import type { DataTableContextType } from '../../templates/Dataset/DataTableContext'; + +const sampleColumns = [ + { header: 'NPN', accessor: 'npn' }, + { header: 'Plan Year', accessor: 'applicable_plan_year' }, + { header: 'Ind. Registration', accessor: 'individual_registration_completion_date' }, + { header: 'Shop Registration', accessor: 'shop_registration_completion_date' }, +]; + +type StoryArgs = React.ComponentProps & { + contextOverride?: Partial; +}; + +const meta: Meta = { + title: 'Components/Datatable', + component: DataTable, + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: ` +The Datatable component renders the data grid used inside the Dataset Data Table tab. It expects a parent +\`DataTableContext\` to supply the resource (rows, schema, pagination state) and a \`DataTableActionsContext\` +for column order/visibility and density. Most consumers use \`DatasetTableTab\` instead of this component directly; +these stories cover the table render in isolation. + `, + }, + }, + }, + decorators: [withDataTableContextsFromArgs], + args: { + columns: sampleColumns, + canResize: true, + loading: false, + isModal: false, + showDataTableToolbar: false, + }, + argTypes: { + columns: { control: 'object', description: 'Column defs ({header, accessor}).' }, + canResize: { control: 'boolean', description: 'Enable column resizing.' }, + loading: { control: 'boolean', description: 'Render the loading spinner.' }, + showDataTableToolbar: { control: 'boolean', description: 'Render the full toolbar above the table.' }, + contextOverride: { table: { disable: true } }, + }, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const Loading: Story = { + args: { + loading: true, + contextOverride: { resource: { ...defaultDataTableContext.resource!, loading: true } }, + }, + parameters: { + docs: { description: { story: 'Resource is loading — the table renders a spinner overlay.' } }, + }, +}; + +export const Empty: Story = { + args: { + contextOverride: { + resource: { ...mockResource, values: [], count: 0, totalRows: 0 }, + }, + }, + parameters: { + docs: { description: { story: 'Resource returned zero rows.' } }, + }, +}; diff --git a/src/components/DatatableHeader/DatatableHeader.stories.tsx b/src/components/DatatableHeader/DatatableHeader.stories.tsx index 258020fa..a9384f12 100644 --- a/src/components/DatatableHeader/DatatableHeader.stories.tsx +++ b/src/components/DatatableHeader/DatatableHeader.stories.tsx @@ -1,14 +1,16 @@ -import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import DataTableHeader from './index'; -const meta = { +const meta: Meta = { title: 'Components/DatatableHeader', component: DataTableHeader, parameters: { layout: 'padded', docs: { description: { - component: `\nThe DatatableHeader component displays controls and information for a data table, including download and copy link buttons.\n `, + component: ` +The DatatableHeader component displays controls and information for a data table, including download and copy link buttons. + `, }, }, }, @@ -16,7 +18,7 @@ const meta = { resource: { control: 'object', description: 'Resource object for the table.' }, downloadURL: { control: 'text', description: 'URL for filtered data download.' }, unfilteredDownloadURL: { control: 'text', description: 'URL for full data download.' }, - setPage: { control: 'function', description: 'Callback to set page.' }, + setPage: { control: false, description: 'Callback to set page.' }, showCopyLinkButton: { control: 'boolean', description: 'Show copy link button.' }, showDownloadFilteredDataButton: { control: 'boolean', description: 'Show download filtered data button.' }, showDownloadFullDataButton: { control: 'boolean', description: 'Show download full data button.' }, @@ -26,8 +28,9 @@ const meta = { }; export default meta; +type Story = StoryObj; -export const Default = { +export const Default: Story = { args: { resource: { limit: 25, @@ -36,7 +39,7 @@ export const Default = { conditions: [], setLimit: () => {}, setOffset: () => {}, - }, + } as never, downloadURL: '/download', unfilteredDownloadURL: '/download-full', setPage: () => {}, diff --git a/src/components/DesktopHeader/DesktopHeader.stories.tsx b/src/components/DesktopHeader/DesktopHeader.stories.tsx new file mode 100644 index 00000000..aa542ad0 --- /dev/null +++ b/src/components/DesktopHeader/DesktopHeader.stories.tsx @@ -0,0 +1,50 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { MemoryRouter } from 'react-router-dom'; +import DesktopHeader from './DesktopHeader'; +import { mainNavLinks, topNavLinks } from '../../../.storybook/fixtures'; + +const meta: Meta = { + title: 'Components/DesktopHeader', + component: DesktopHeader, + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: + 'Desktop variant of the legacy site header. Renders an optional CMS top-nav bar and the main site nav. Used by the responsive Header wrapper at desktop breakpoints.', + }, + }, + }, + decorators: [ + (Story) => ( + + + + ), + ], + argTypes: { + siteName: { control: 'text' }, + includeTopNav: { control: 'boolean' }, + customSearch: { control: false }, + }, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + siteName: 'CMS Open Data', + includeTopNav: true, + links: { topnav: topNavLinks, main: mainNavLinks }, + }, +}; + +export const WithoutTopNav: Story = { + args: { + siteName: 'CMS Open Data', + includeTopNav: false, + links: { topnav: topNavLinks, main: mainNavLinks }, + }, +}; diff --git a/src/components/DisplaySettings/DisplaySettings.stories.tsx b/src/components/DisplaySettings/DisplaySettings.stories.tsx new file mode 100644 index 00000000..c9e28453 --- /dev/null +++ b/src/components/DisplaySettings/DisplaySettings.stories.tsx @@ -0,0 +1,24 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import DisplaySettings from './index'; +import { withDataTableContexts } from '../../../.storybook/decorators'; + +const meta: Meta = { + title: 'Components/DisplaySettings', + component: DisplaySettings, + parameters: { + layout: 'padded', + docs: { + description: { + component: + 'Toolbar popover that toggles row-height density (expanded / normal / compact) and rows-per-page. Reads/writes the DataTableActionsContext.', + }, + }, + }, + decorators: [withDataTableContexts()], + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/src/components/ErrorBoundary/ErrorBoundary.stories.tsx b/src/components/ErrorBoundary/ErrorBoundary.stories.tsx index c7df92ec..a7e6e705 100644 --- a/src/components/ErrorBoundary/ErrorBoundary.stories.tsx +++ b/src/components/ErrorBoundary/ErrorBoundary.stories.tsx @@ -1,47 +1,45 @@ -import React from 'react'; -import type { StoryFn, StoryContext } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import ErrorBoundary from './index'; const ProblemChild = () => { throw new Error('Test error!'); }; -export default { +const meta: Meta = { title: 'Components/ErrorBoundary', component: ErrorBoundary, tags: ['autodocs'], + parameters: { + layout: 'padded', + docs: { + description: { + component: 'Catches errors in child components and displays a fallback UI.', + }, + }, + }, argTypes: { component: { control: 'boolean', description: 'Use component mode for fallback rendering.', - defaultValue: false, }, }, - parameters: { - docs: { - description: { - component: 'Catches errors in child components and displays a fallback UI.' - } - } - }, }; -export const Default = { - args: { - component: false, - }, - render: (args: { component?: boolean }) => ( +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { component: false }, + render: (args) => ( ), }; -export const ComponentMode = { - args: { - component: true, - }, - render: (args: { component?: boolean }) => ( +export const ComponentMode: Story = { + args: { component: true }, + render: (args) => ( diff --git a/src/components/FAQAccordion/FAQAccordion.stories.tsx b/src/components/FAQAccordion/FAQAccordion.stories.tsx index a197d795..f7a6e8e9 100644 --- a/src/components/FAQAccordion/FAQAccordion.stories.tsx +++ b/src/components/FAQAccordion/FAQAccordion.stories.tsx @@ -1,52 +1,30 @@ -import React from 'react'; -import type { StoryFn, StoryContext } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import FAQAccordion from './index'; -import type { FAQItemType } from '../../types/misc'; +import { sampleFaqs } from '../../../.storybook/fixtures'; -const sampleFaqs: FAQItemType[] = [ - { - id: 'faq1', - title: 'What is Open Data?', - body: 'Open data is data that can be freely used, re-used, and redistributed by anyone.', - open: false, - }, - { - id: 'faq2', - title: 'How do I access datasets?', - body: 'You can access datasets via the search or browse features on our site.', - open: false, - }, - { - id: 'faq3', - title: 'Who maintains the data?', - body: 'The Open Data team maintains and updates the datasets regularly.', - open: false, - }, -]; - -export default { +const meta: Meta = { title: 'Components/FAQAccordion', component: FAQAccordion, tags: ['autodocs'], + parameters: { + layout: 'padded', + docs: { + description: { + component: 'Displays an accordion of FAQ items with expand/collapse all functionality.', + }, + }, + }, argTypes: { faqs: { control: 'object', description: 'Array of FAQ items to display.', - defaultValue: sampleFaqs, }, }, - parameters: { - docs: { - description: { - component: 'Displays an accordion of FAQ items with expand/collapse all functionality.' - } - } - }, }; -export const Default = { - args: { - faqs: sampleFaqs, - }, - render: (args: { faqs: FAQItemType[] }) => , +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { faqs: sampleFaqs }, }; diff --git a/src/components/FilterChip/FilterChip.stories.tsx b/src/components/FilterChip/FilterChip.stories.tsx index 39847439..36427097 100644 --- a/src/components/FilterChip/FilterChip.stories.tsx +++ b/src/components/FilterChip/FilterChip.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import React from 'react'; import FilterChip from './index'; import { getOperatorLabel } from '../../templates/FilteredResource/functions'; diff --git a/src/components/FilterDataset/FilterDataset.stories.tsx b/src/components/FilterDataset/FilterDataset.stories.tsx index 9b13a7de..a62fa04d 100644 --- a/src/components/FilterDataset/FilterDataset.stories.tsx +++ b/src/components/FilterDataset/FilterDataset.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import React from 'react'; import FilterDataset from './index'; import DataTableContext, { DataTableContextType } from '../../templates/Dataset/DataTableContext'; diff --git a/src/components/FullScreenDataTable/FullScreenDataTable.stories.tsx b/src/components/FullScreenDataTable/FullScreenDataTable.stories.tsx new file mode 100644 index 00000000..ab46dbfe --- /dev/null +++ b/src/components/FullScreenDataTable/FullScreenDataTable.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import FullScreenDataTable from './index'; +import { withDataTableContexts } from '../../../.storybook/decorators'; + +const meta: Meta = { + title: 'Components/FullScreenDataTable', + component: FullScreenDataTable, + parameters: { + layout: 'padded', + docs: { + description: { + component: + 'Toolbar button that opens a full-screen Dialog containing a DatasetTable. Returns null when `isModal` is true to prevent recursive embedding.', + }, + }, + }, + decorators: [withDataTableContexts()], + argTypes: { + isModal: { control: 'boolean', description: 'When true the component renders nothing (prevents nested fullscreen).' }, + }, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { isModal: false }, +}; diff --git a/src/components/HeaderNav/HeaderNav.stories.tsx b/src/components/HeaderNav/HeaderNav.stories.tsx new file mode 100644 index 00000000..13254cfe --- /dev/null +++ b/src/components/HeaderNav/HeaderNav.stories.tsx @@ -0,0 +1,81 @@ +import { useRef } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { MemoryRouter } from 'react-router-dom'; +import HeaderContext from '../../templates/Header/HeaderContext'; +import HeaderNav from './index'; +import { + mainNavLinks, + navLinksWithSubmenus, + topNavLinks, +} from '../../../.storybook/fixtures'; + +const HeaderContextDecorator = + (overrides: Partial> = {}) => + (Story: React.FC) => { + const menuRef = useRef(null); + return ( + + {}, + menuRef, + isMobile: false, + onDark: true, + ...overrides, + }} + > +
+ +
+
+
+ ); + }; + +const meta: Meta = { + title: 'Components/HeaderNav', + component: HeaderNav, + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: + 'Site-wide main navigation rendered inside the Header template. Reads mobile/desktop state from HeaderContext and renders submenus, optional top nav links, and an optional mobile search.', + }, + }, + }, + argTypes: { + links: { control: 'object', description: 'Primary nav links (may include submenus).' }, + topNavLinks: { control: 'object', description: 'Optional secondary top-nav links.' }, + searchInMobile: { control: 'boolean', description: 'Render a HeaderSearch in mobile mode.' }, + wrapperClasses: { control: 'text', description: 'Extra classes for the nav wrapper.' }, + }, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { links: mainNavLinks }, + decorators: [HeaderContextDecorator()], +}; + +export const WithSubmenusAndTopNav: Story = { + args: { links: navLinksWithSubmenus, topNavLinks }, + decorators: [HeaderContextDecorator()], +}; + +export const MobileOpen: Story = { + args: { links: mainNavLinks, searchInMobile: true }, + decorators: [HeaderContextDecorator({ isMobile: true, mobileMenuOpen: true })], + parameters: { + docs: { + description: { + story: + 'Mobile state with the menu open. HeaderContext is overridden to simulate the responsive breakpoint without resizing the viewport.', + }, + }, + }, +}; diff --git a/src/components/HeaderNavIconLink/HeaderNavIconLink.stories.tsx b/src/components/HeaderNavIconLink/HeaderNavIconLink.stories.tsx index c8c6bdfe..e1fadd88 100644 --- a/src/components/HeaderNavIconLink/HeaderNavIconLink.stories.tsx +++ b/src/components/HeaderNavIconLink/HeaderNavIconLink.stories.tsx @@ -1,48 +1,32 @@ -import React from 'react'; -import HeaderNavIconLink, { HeaderNavIconLinkProps } from './index'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import HeaderNavIconLink from './index'; import logoImage from '../../assets/images/CMSGovLogo-O.png'; -export default { +const meta: Meta = { title: 'Components/HeaderNavIconLink', component: HeaderNavIconLink, tags: ['autodocs'], - argTypes: { - url: { - control: 'text', - description: 'Destination URL for the icon link.', - defaultValue: '/', - }, - urlTitle: { - control: 'text', - description: 'Title attribute for the link.', - defaultValue: 'CMSDS Home', - }, - logoFilePath: { - control: 'text', - description: 'Image source for the icon.', - defaultValue: logoImage, - }, - logoAltText: { - control: 'text', - description: 'Alt text for the icon image.', - defaultValue: 'CMSDS Logo', - }, - backArrow: { - control: 'boolean', - description: 'Show back arrow next to the icon.', - defaultValue: false, - }, - }, parameters: { + layout: 'padded', docs: { description: { - component: 'Displays a navigation icon link in the header, optionally with a back arrow.' - } - } + component: 'Displays a navigation icon link in the header, optionally with a back arrow.', + }, + }, + }, + argTypes: { + url: { control: 'text', description: 'Destination URL for the icon link.' }, + urlTitle: { control: 'text', description: 'Title attribute for the link.' }, + logoFilePath: { control: 'text', description: 'Image source for the icon.' }, + logoAltText: { control: 'text', description: 'Alt text for the icon image.' }, + backArrow: { control: 'boolean', description: 'Show back arrow next to the icon.' }, }, }; -export const Default = { +export default meta; +type Story = StoryObj; + +export const Default: Story = { args: { url: '/', urlTitle: 'CMSDS Home', @@ -50,10 +34,9 @@ export const Default = { logoAltText: 'CMSDS Logo', backArrow: false, }, - render: (args: HeaderNavIconLinkProps) => , }; -export const WithBackArrow = { +export const WithBackArrow: Story = { args: { url: '/', urlTitle: 'Back to CMS', @@ -61,5 +44,4 @@ export const WithBackArrow = { logoAltText: 'CMSDS Logo', backArrow: true, }, - render: (args: HeaderNavIconLinkProps) => , }; diff --git a/src/components/HeaderSearch/HeaderSearch.stories.tsx b/src/components/HeaderSearch/HeaderSearch.stories.tsx index b921fcbe..dac7cd19 100644 --- a/src/components/HeaderSearch/HeaderSearch.stories.tsx +++ b/src/components/HeaderSearch/HeaderSearch.stories.tsx @@ -1,42 +1,41 @@ -import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { MemoryRouter } from 'react-router-dom'; import HeaderSearch from './index'; -import type { StoryFn, StoryContext } from '@storybook/react'; -import type { HeaderSearchProps } from './index'; -export default { +const meta: Meta = { title: 'Components/HeaderSearch', component: HeaderSearch, tags: ['autodocs'], + parameters: { + layout: 'padded', + docs: { + description: { + component: 'Displays a search button in the header that opens a modal for dataset search.', + }, + }, + }, argTypes: { headingText: { control: 'text', description: 'Heading text for the search modal dialog.', - defaultValue: 'Dataset Search', }, }, - parameters: { - docs: { - description: { - component: 'Displays a search button in the header that opens a modal for dataset search.' - } - } - }, decorators: [ - (Story: StoryFn, context: StoryContext) => {Story(context.args, context)} + (Story) => ( + + + + ), ], }; -export const Default = { - args: { - headingText: 'Dataset Search', - }, - render: (args: HeaderSearchProps) => , +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { headingText: 'Dataset Search' }, }; -export const CustomHeading = { - args: { - headingText: 'Find Your Data', - }, - render: (args: HeaderSearchProps) => , +export const CustomHeading: Story = { + args: { headingText: 'Find Your Data' }, }; diff --git a/src/components/HeaderSiteTitle/HeaderSiteTitle.stories.tsx b/src/components/HeaderSiteTitle/HeaderSiteTitle.stories.tsx index e65a2d31..29e8e833 100644 --- a/src/components/HeaderSiteTitle/HeaderSiteTitle.stories.tsx +++ b/src/components/HeaderSiteTitle/HeaderSiteTitle.stories.tsx @@ -1,8 +1,7 @@ -import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { MemoryRouter } from 'react-router-dom'; -import HeaderSiteTitle, { HeaderSiteTitleProps } from './index'; +import HeaderSiteTitle from './index'; import { OrgType } from '../../types/misc'; -import type { StoryFn, StoryContext } from '@storybook/react'; const sampleOrg: OrgType = { url: '/', @@ -17,54 +16,48 @@ const sampleOrgWithLogo: OrgType = { logoFilePath: 'https://via.placeholder.com/150x40?text=Logo', }; -export default { +const meta: Meta = { title: 'Components/HeaderSiteTitle', component: HeaderSiteTitle, tags: ['autodocs'], + parameters: { + layout: 'padded', + docs: { + description: { + component: 'Displays the site title in the header, optionally with a logo and inverse styling.', + }, + }, + }, argTypes: { inverse: { control: 'boolean', description: 'Use inverse color scheme for the site title link.', - defaultValue: false, }, org: { control: 'object', description: 'Organization info including logo, alt text, and title.', - defaultValue: sampleOrg, }, }, - parameters: { - docs: { - description: { - component: 'Displays the site title in the header, optionally with a logo and inverse styling.' - } - } - }, decorators: [ - (Story: StoryFn, context: StoryContext) => {Story(context.args, context)} + (Story) => ( + + + + ), ], }; -export const Default = { - args: { - inverse: false, - org: sampleOrg, - }, - render: (args: HeaderSiteTitleProps) => , +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { inverse: false, org: sampleOrg }, }; -export const WithLogo = { - args: { - inverse: false, - org: sampleOrgWithLogo, - }, - render: (args: HeaderSiteTitleProps) => , +export const WithLogo: Story = { + args: { inverse: false, org: sampleOrgWithLogo }, }; -export const Inverse = { - args: { - inverse: true, - org: sampleOrg, - }, - render: (args: HeaderSiteTitleProps) => , +export const Inverse: Story = { + args: { inverse: true, org: sampleOrg }, }; diff --git a/src/components/HeaderTagline/HeaderTagline.stories.tsx b/src/components/HeaderTagline/HeaderTagline.stories.tsx index 09365ee5..b0866e1c 100644 --- a/src/components/HeaderTagline/HeaderTagline.stories.tsx +++ b/src/components/HeaderTagline/HeaderTagline.stories.tsx @@ -1,29 +1,26 @@ -import React from 'react'; -import HeaderTagline, { HeaderTaglineProps } from './index'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import HeaderTagline from './index'; -export default { +const meta: Meta = { title: 'Components/HeaderTagline', component: HeaderTagline, tags: ['autodocs'], - argTypes: { - tagline: { - control: 'text', - description: 'The tagline text to display.', - defaultValue: 'Empowering Data Transparency', - }, - }, parameters: { + layout: 'padded', docs: { description: { - component: 'Displays a tagline in the header, styled for top navigation.' - } - } - } + component: 'Displays a tagline in the header, styled for top navigation.', + }, + }, + }, + argTypes: { + tagline: { control: 'text', description: 'The tagline text to display.' }, + }, }; -export const Default = { - args: { - tagline: 'Empowering Data Transparency', - }, - render: (args: HeaderTaglineProps) => , +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { tagline: 'Empowering Data Transparency' }, }; diff --git a/src/components/Hero/Hero.stories.jsx b/src/components/Hero/Hero.stories.tsx similarity index 76% rename from src/components/Hero/Hero.stories.jsx rename to src/components/Hero/Hero.stories.tsx index fc120bed..099ebadd 100644 --- a/src/components/Hero/Hero.stories.jsx +++ b/src/components/Hero/Hero.stories.tsx @@ -1,15 +1,17 @@ -import React from 'react'; -import Hero from './index'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { MemoryRouter } from 'react-router-dom'; +import Hero from './index'; -const meta = { +const meta: Meta = { title: 'Components/Hero', component: Hero, parameters: { layout: 'padded', docs: { description: { - component: `\nThe Hero component displays a prominent header section with a title, description, and search form for datasets.\n `, + component: ` +The Hero component displays a prominent header section with a title, description, and search form for datasets. + `, }, }, }, @@ -24,7 +26,7 @@ const meta = { decorators: [ (Story) => ( - {Story()} + ), ], @@ -32,8 +34,9 @@ const meta = { }; export default meta; +type Story = StoryObj; -export const Default = { +export const Default: Story = { args: { title: 'CMSDS', description: 'Search and explore datasets.', @@ -45,8 +48,8 @@ export const Default = { parameters: { docs: { description: { - story: 'Displays the hero section with a title, description, and search form.' - } - } - } + story: 'Displays the hero section with a title, description, and search form.', + }, + }, + }, }; diff --git a/src/components/LargeFileDialog/LargeFileDialog.stories.tsx b/src/components/LargeFileDialog/LargeFileDialog.stories.tsx index a16aa0de..1d263bb6 100644 --- a/src/components/LargeFileDialog/LargeFileDialog.stories.tsx +++ b/src/components/LargeFileDialog/LargeFileDialog.stories.tsx @@ -1,7 +1,7 @@ -import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import LargeFileDialog from './index'; -const meta = { +const meta: Meta = { title: 'Components/LargeFileDialog', component: LargeFileDialog, parameters: { @@ -21,16 +21,15 @@ The LargeFileDialog component displays a button and a dialog warning users about }; export default meta; +type Story = StoryObj; -export const Default = { - args: { - downloadUrl: '/largefile.csv', - }, +export const Default: Story = { + args: { downloadUrl: '/largefile.csv' }, parameters: { docs: { description: { - story: 'Displays the large file download dialog with sample download URL.' - } - } - } + story: 'Displays the large file download dialog with sample download URL.', + }, + }, + }, }; diff --git a/src/components/LargeFileInfo/LargeFileInfo.stories.tsx b/src/components/LargeFileInfo/LargeFileInfo.stories.tsx index 93fc04b7..b006831a 100644 --- a/src/components/LargeFileInfo/LargeFileInfo.stories.tsx +++ b/src/components/LargeFileInfo/LargeFileInfo.stories.tsx @@ -1,9 +1,8 @@ -import React from 'react'; -import LargeFileInfo from './index'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { MemoryRouter } from 'react-router-dom'; -import { StoryFn } from '@storybook/react'; +import LargeFileInfo from './index'; -const meta = { +const meta: Meta = { title: 'Components/LargeFileInfo', component: LargeFileInfo, parameters: { @@ -20,7 +19,7 @@ The LargeFileInfo component displays information and recommendations for handlin className: { control: 'text', description: 'CSS class for the container.' }, }, decorators: [ - (Story: StoryFn) => ( + (Story) => ( @@ -30,16 +29,15 @@ The LargeFileInfo component displays information and recommendations for handlin }; export default meta; +type Story = StoryObj; -export const Default = { - args: { - className: '', - }, +export const Default: Story = { + args: { className: '' }, parameters: { docs: { description: { - story: 'Displays the large file info message with default styling.' - } - } - } + story: 'Displays the large file info message with default styling.', + }, + }, + }, }; diff --git a/src/components/ManageColumns/ManageColumns.stories.tsx b/src/components/ManageColumns/ManageColumns.stories.tsx new file mode 100644 index 00000000..53e40885 --- /dev/null +++ b/src/components/ManageColumns/ManageColumns.stories.tsx @@ -0,0 +1,43 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import ManageColumns from './ManageColumns'; +import { withDataTableContexts } from '../../../.storybook/decorators'; + +const columnIds = ['npn', 'applicable_plan_year', 'individual_registration_completion_date', 'shop_end_date']; + +const makeMockColumns = (visibility: Record = {}) => + columnIds.map((id) => ({ + id, + getIsVisible: () => visibility[id] !== false, + })); + +const meta: Meta = { + title: 'Components/ManageColumns', + component: ManageColumns, + parameters: { + layout: 'padded', + docs: { + description: { + component: + 'Toolbar button that opens a dialog for reordering and toggling visibility of data table columns. Reads/writes the DataTableActionsContext and persists per-dataset state to localStorage.', + }, + }, + }, + decorators: [withDataTableContexts({}, { columnOrder: columnIds })], + argTypes: { + id: { control: 'text', description: 'Dataset id (used as the localStorage key).' }, + columns: { control: false }, + defaultColumnOrder: { control: 'object' }, + }, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + id: 'wb6u-x2ny', + columns: makeMockColumns() as never, + defaultColumnOrder: columnIds, + }, +}; diff --git a/src/components/MobileHeader/MobileHeader.jsx b/src/components/MobileHeader/MobileHeader.jsx index fdd50b85..9551222f 100644 --- a/src/components/MobileHeader/MobileHeader.jsx +++ b/src/components/MobileHeader/MobileHeader.jsx @@ -6,7 +6,6 @@ import SearchModal from '../SearchModal'; import NavBar from '../NavBar/Navbar'; import cmsLogo from '../../assets/images/CMSGovLogo-O.png'; import cmsLogoWhite from '../../assets/images/CMSgov@2x-white-O.png'; -import './mobile-header.scss'; let mobileHeaderMenuClassName = "dc-c-mobile-header--menu"; const MobileHeader = ({ diff --git a/src/components/MobileHeader/MobileHeader.stories.tsx b/src/components/MobileHeader/MobileHeader.stories.tsx new file mode 100644 index 00000000..e64f358b --- /dev/null +++ b/src/components/MobileHeader/MobileHeader.stories.tsx @@ -0,0 +1,45 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { MemoryRouter } from 'react-router-dom'; +import MobileHeader from './MobileHeader'; +import { mainNavLinks, topNavLinks } from '../../../.storybook/fixtures'; + +const meta: Meta = { + title: 'Components/MobileHeader', + component: MobileHeader, + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: + 'Mobile/tablet variant of the legacy site header. Manages its own menu-open state, focus trap, and keyboard handlers. Used by the responsive Header wrapper at mobile breakpoints.', + }, + }, + }, + decorators: [ + (Story) => ( + +
+ +
+
+ ), + ], + argTypes: { + siteName: { control: 'text' }, + includeTopNav: { control: 'boolean' }, + includeSearch: { control: 'boolean' }, + }, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + siteName: 'CMS Open Data', + includeTopNav: true, + includeSearch: true, + links: { topnav: topNavLinks, main: mainNavLinks }, + }, +}; diff --git a/src/components/MobileMenuButton/MobileMenuButton.stories.tsx b/src/components/MobileMenuButton/MobileMenuButton.stories.tsx index 0dc52d76..49b80936 100644 --- a/src/components/MobileMenuButton/MobileMenuButton.stories.tsx +++ b/src/components/MobileMenuButton/MobileMenuButton.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import MobileMenuButton from './index'; import React from 'react'; diff --git a/src/components/NavBar/NavBar.stories.tsx b/src/components/NavBar/NavBar.stories.tsx new file mode 100644 index 00000000..01a96a4e --- /dev/null +++ b/src/components/NavBar/NavBar.stories.tsx @@ -0,0 +1,57 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { MemoryRouter } from 'react-router-dom'; +import NavBar from './index'; +import { mainNavLinks, navLinksWithSubmenus } from '../../../.storybook/fixtures'; + +const meta: Meta = { + title: 'Components/NavBar', + component: NavBar, + parameters: { + layout: 'padded', + docs: { + description: { + component: + 'A generic horizontal navigation menu. Renders an accessible nav with optional dropdown submenus per link.', + }, + }, + }, + decorators: [ + (Story) => ( + + + + ), + ], + argTypes: { + links: { control: 'object', description: 'Array of NavLinkArray items, with optional `submenu`.' }, + menuName: { control: 'text', description: 'Screen-reader label for the nav.' }, + menuId: { control: 'text', description: 'Used to build CSS class and aria-labelledby id.' }, + menuClasses: { control: 'text', description: 'Additional classes for the ul element.' }, + linkClasses: { control: 'text', description: 'Classes applied to each NavLink.' }, + subLinkClasses: { control: 'text', description: 'Classes applied to submenu links.' }, + wrapLabel: { control: 'boolean', description: 'When true, wraps long submenu labels.' }, + }, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + links: mainNavLinks, + menuName: 'Main navigation', + menuId: 'main-nav', + linkClasses: 'ds-c-button ds-c-button--ghost', + }, +}; + +export const WithSubmenus: Story = { + args: { + links: navLinksWithSubmenus, + menuName: 'Main navigation', + menuId: 'main-nav', + linkClasses: 'ds-c-button ds-c-button--ghost', + subLinkClasses: 'ds-c-button ds-c-button--ghost ds-u-font-weight--normal', + }, +}; diff --git a/src/components/NavLink/NavLink.stories.tsx b/src/components/NavLink/NavLink.stories.tsx index 466b884c..1e47780a 100644 --- a/src/components/NavLink/NavLink.stories.tsx +++ b/src/components/NavLink/NavLink.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import NavLink from './index'; import { MemoryRouter } from 'react-router-dom'; diff --git a/src/components/PageHeader/PageHeader.stories.tsx b/src/components/PageHeader/PageHeader.stories.tsx index 9cedb435..a5057efb 100644 --- a/src/components/PageHeader/PageHeader.stories.tsx +++ b/src/components/PageHeader/PageHeader.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import PageHeader from './index'; const meta: Meta = { diff --git a/src/components/QueryBuilder/QueryBuilder.stories.tsx b/src/components/QueryBuilder/QueryBuilder.stories.tsx index 2c615851..c8c7cbac 100644 --- a/src/components/QueryBuilder/QueryBuilder.stories.tsx +++ b/src/components/QueryBuilder/QueryBuilder.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import QueryBuilder from './index'; const meta: Meta = { diff --git a/src/components/QueryRow/QueryRow.stories.tsx b/src/components/QueryRow/QueryRow.stories.tsx index 07093118..0bd2844d 100644 --- a/src/components/QueryRow/QueryRow.stories.tsx +++ b/src/components/QueryRow/QueryRow.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import QueryRow from './index'; const meta: Meta = { diff --git a/src/components/Resource/Resource.stories.tsx b/src/components/Resource/Resource.stories.tsx index 07d41ebb..44c2ddcb 100644 --- a/src/components/Resource/Resource.stories.tsx +++ b/src/components/Resource/Resource.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import React from 'react'; import { QueryClientProvider } from '@tanstack/react-query'; import Resource from './index'; diff --git a/src/components/ResourceFooter/ResourceFooter.stories.tsx b/src/components/ResourceFooter/ResourceFooter.stories.tsx index f2873e14..0241534a 100644 --- a/src/components/ResourceFooter/ResourceFooter.stories.tsx +++ b/src/components/ResourceFooter/ResourceFooter.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import ResourceFooter from './index'; const meta: Meta = { diff --git a/src/components/ResourceHeader/ResourceHeader.stories.tsx b/src/components/ResourceHeader/ResourceHeader.stories.tsx index bd5c8417..df7b151a 100644 --- a/src/components/ResourceHeader/ResourceHeader.stories.tsx +++ b/src/components/ResourceHeader/ResourceHeader.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import ResourceHeader from './index'; const meta: Meta = { diff --git a/src/components/ResourceInformation/ResourceInformation.stories.tsx b/src/components/ResourceInformation/ResourceInformation.stories.tsx index 03a044f9..1e655d65 100644 --- a/src/components/ResourceInformation/ResourceInformation.stories.tsx +++ b/src/components/ResourceInformation/ResourceInformation.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import React from 'react'; import { QueryClientProvider } from '@tanstack/react-query'; import ResourceInformation from './index'; diff --git a/src/components/ResourcePreview/ResourcePreview.stories.tsx b/src/components/ResourcePreview/ResourcePreview.stories.tsx new file mode 100644 index 00000000..e094ebda --- /dev/null +++ b/src/components/ResourcePreview/ResourcePreview.stories.tsx @@ -0,0 +1,60 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import ResourcePreview from './index'; +import { mockResource } from '../../../__mocks__/mockResource'; +import { + withDataTableContextsFromArgs, +} from '../../../.storybook/decorators'; +import type { DataTableContextType } from '../../templates/Dataset/DataTableContext'; + +const resourceId = '32da6993-e045-59e4-823e-a5c2c56c649c'; + +type StoryArgs = React.ComponentProps & { + contextOverride?: Partial; +}; + +const meta: Meta = { + title: 'Components/ResourcePreview', + component: ResourcePreview, + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: + 'Wraps Datatable with schema-aware column preparation. Reads resource + customColumns from DataTableContext and renders a spinner until the schema is available.', + }, + }, + }, + decorators: [withDataTableContextsFromArgs], + args: { + id: resourceId, + canResize: true, + showDataTableToolbar: false, + showInfoShareContainer: false, + contextOverride: { id: resourceId }, + }, + argTypes: { + id: { control: 'text', description: 'Resource id (matches a key in resource.schema).' }, + canResize: { control: 'boolean' }, + showDataTableToolbar: { control: 'boolean' }, + showInfoShareContainer: { control: 'boolean' }, + contextOverride: { table: { disable: true } }, + }, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const Loading: Story = { + args: { + contextOverride: { + id: resourceId, + resource: { ...mockResource, loading: true, values: [], schema: {} }, + }, + }, + parameters: { + docs: { description: { story: 'Schema not yet available — renders the spinner fallback.' } }, + }, +}; diff --git a/src/components/SearchButton/SearchButton.stories.tsx b/src/components/SearchButton/SearchButton.stories.tsx index ffa90c01..745f0c8c 100644 --- a/src/components/SearchButton/SearchButton.stories.tsx +++ b/src/components/SearchButton/SearchButton.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import SearchButton from './index'; const meta: Meta = { diff --git a/src/components/SearchInput/SearchInput.stories.tsx b/src/components/SearchInput/SearchInput.stories.tsx index c3463655..7fa9638f 100644 --- a/src/components/SearchInput/SearchInput.stories.tsx +++ b/src/components/SearchInput/SearchInput.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import SearchInput from './index'; const meta: Meta = { diff --git a/src/components/SearchModal/SearchModal.stories.jsx b/src/components/SearchModal/SearchModal.stories.tsx similarity index 81% rename from src/components/SearchModal/SearchModal.stories.jsx rename to src/components/SearchModal/SearchModal.stories.tsx index 408a02ce..3b0f6742 100644 --- a/src/components/SearchModal/SearchModal.stories.jsx +++ b/src/components/SearchModal/SearchModal.stories.tsx @@ -1,9 +1,8 @@ - -import React from 'react'; -import SearchModal from './index'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { MemoryRouter } from 'react-router-dom'; +import SearchModal from './index'; -const meta = { +const meta: Meta = { title: 'Components/SearchModal', component: SearchModal, parameters: { @@ -27,8 +26,9 @@ The SearchModal component displays a modal dialog for searching datasets. It inc }; export default meta; +type Story = StoryObj; -export const Default = { +export const Default: Story = { args: { headingText: 'Dataset Search', buttonSize: null, @@ -43,16 +43,15 @@ export const Default = { }, }; -export const CustomHeading = { +export const CustomHeading: Story = { args: { headingText: 'Custom Search Heading', - buttonSize: 'medium', inversedSearchButton: false, }, parameters: { docs: { description: { - story: 'Displays the search modal with a custom heading and button size.', + story: 'Displays the search modal with a custom heading on a non-inverse button.', }, }, }, diff --git a/src/components/SidebarNavigation/SidebarNavigation.stories.tsx b/src/components/SidebarNavigation/SidebarNavigation.stories.tsx index 1bff180a..505d598d 100644 --- a/src/components/SidebarNavigation/SidebarNavigation.stories.tsx +++ b/src/components/SidebarNavigation/SidebarNavigation.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import SidebarNavigation from './index'; import { MemoryRouter } from 'react-router-dom'; diff --git a/src/components/SitewideDataDictionaryTable/SitewideDataDictionaryTable.stories.tsx b/src/components/SitewideDataDictionaryTable/SitewideDataDictionaryTable.stories.tsx index 3b0b6528..fd9a475d 100644 --- a/src/components/SitewideDataDictionaryTable/SitewideDataDictionaryTable.stories.tsx +++ b/src/components/SitewideDataDictionaryTable/SitewideDataDictionaryTable.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import SitewideDataDictionaryTable from './index'; const meta: Meta = { diff --git a/src/components/SubMenu/SubMenu.stories.tsx b/src/components/SubMenu/SubMenu.stories.tsx index 4c3e8117..8f64888a 100644 --- a/src/components/SubMenu/SubMenu.stories.tsx +++ b/src/components/SubMenu/SubMenu.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import SubMenu from './index'; import DatasetListSubmenu from '../DatasetListSubmenu'; import HeaderContext from '../../templates/Header/HeaderContext'; diff --git a/src/components/TopicInformation/TopicInformation.stories.tsx b/src/components/TopicInformation/TopicInformation.stories.tsx index 5bedc3a0..46cff10e 100644 --- a/src/components/TopicInformation/TopicInformation.stories.tsx +++ b/src/components/TopicInformation/TopicInformation.stories.tsx @@ -1,5 +1,5 @@ import React from "react"; -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { MemoryRouter } from 'react-router-dom'; import TopicInformation from "."; diff --git a/src/components/TransformedDate/TransformedDate.stories.jsx b/src/components/TransformedDate/TransformedDate.stories.tsx similarity index 68% rename from src/components/TransformedDate/TransformedDate.stories.jsx rename to src/components/TransformedDate/TransformedDate.stories.tsx index ca0ac4a0..1986f088 100644 --- a/src/components/TransformedDate/TransformedDate.stories.jsx +++ b/src/components/TransformedDate/TransformedDate.stories.tsx @@ -1,14 +1,16 @@ -import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import TransformedDate from './index'; -const meta = { +const meta: Meta = { title: 'Components/TransformedDate', component: TransformedDate, parameters: { layout: 'padded', docs: { description: { - component: `\nThe TransformedDate component displays a formatted date string using Intl.DateTimeFormat options.\n `, + component: ` +The TransformedDate component displays a formatted date string using Intl.DateTimeFormat options. + `, }, }, }, @@ -20,8 +22,9 @@ const meta = { }; export default meta; +type Story = StoryObj; -export const Default = { +export const Default: Story = { args: { date: '2025-07-18T12:00:00Z', options: { @@ -33,7 +36,7 @@ export const Default = { }, }; -export const ShortFormat = { +export const ShortFormat: Story = { args: { date: '2025-07-18T12:00:00Z', options: { diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 00000000..2c9b5d04 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,5 @@ +declare module '*.png'; +declare module '*.jpg'; +declare module '*.jpeg'; +declare module '*.gif'; +declare module '*.svg'; diff --git a/src/templates/APIPage/APIPage.stories.tsx b/src/templates/APIPage/APIPage.stories.tsx index c752ab28..490332f7 100644 --- a/src/templates/APIPage/APIPage.stories.tsx +++ b/src/templates/APIPage/APIPage.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import APIPage from './index'; import { MemoryRouter } from 'react-router-dom'; import { ACAContext } from '../../utilities/ACAContext'; diff --git a/src/templates/Dataset/Dataset.stories.tsx b/src/templates/Dataset/Dataset.stories.tsx index bdc05233..120ff59f 100644 --- a/src/templates/Dataset/Dataset.stories.tsx +++ b/src/templates/Dataset/Dataset.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import Dataset from './index'; import { MemoryRouter } from 'react-router-dom'; import { createDatasetPageHandlers } from '../../../.storybook/mswHandlers'; @@ -143,7 +143,7 @@ export const Default: Story = { }, }; -export const withoutToolbar: Story = { +export const WithoutToolbar: Story = { args: { ...defaultArgs, disableTableControls: true }, parameters: { msw: { @@ -157,7 +157,7 @@ export const withoutToolbar: Story = { }, }; -export const withDataDictionaryBanner: Story = { +export const WithDataDictionaryBanner: Story = { args: { ...defaultArgs, dataDictionaryBanner: true }, parameters: { msw: { @@ -171,7 +171,7 @@ export const withDataDictionaryBanner: Story = { }, }; -export const withoutDataDictionaryTab: Story = { +export const WithoutDataDictionaryTab: Story = { args: { ...defaultArgs, hideDataDictionary: true }, parameters: { msw: { @@ -185,7 +185,7 @@ export const withoutDataDictionaryTab: Story = { }, }; -export const withCustomDescription: Story = { +export const WithCustomDescription: Story = { args: { ...defaultArgs, customDescription: () => ( @@ -210,7 +210,7 @@ export const withCustomDescription: Story = { }, }; -export const withRowLimitNotice: Story = { +export const WithRowLimitNotice: Story = { args: { ...defaultArgs, showRowLimitNotice: true }, parameters: { msw: { diff --git a/src/templates/DatasetList/DatasetList.stories.tsx b/src/templates/DatasetList/DatasetList.stories.tsx index ce29a67e..293c06af 100644 --- a/src/templates/DatasetList/DatasetList.stories.tsx +++ b/src/templates/DatasetList/DatasetList.stories.tsx @@ -1,11 +1,11 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import DatasetList from './index'; import { MemoryRouter } from 'react-router-dom'; import { QueryClientProvider } from '@tanstack/react-query'; import { createStorybookQueryClient } from '../../../.storybook/queryClient'; import { ACAContext } from '../../utilities/ACAContext'; import { createDatasetListHandlers } from '../../../.storybook/mswHandlers'; -import { mockApiResponse } from '../../../__mocks__/mockDatasetSearchResults'; +import { mockApiResponse, mockEmptyResults } from '../../../__mocks__/mockDatasetSearchResults'; const queryClient = createStorybookQueryClient(); @@ -202,6 +202,10 @@ export const WithDataDictionaryLinks: Story = { }, }; +// Excluded from autodocs: the module-scope queryClient (defined above) is shared +// across all stories rendered on the docs page. Combined with identical query keys, +// this story's empty-results MSW handler would either bleed into other docs renders +// or be overridden by them. View this story standalone to see the empty state. export const EmptyResults: Story = { args: { rootUrl: 'https://data.cms.gov', @@ -217,10 +221,14 @@ export const EmptyResults: Story = { introText: '', dataDictionaryLinks: false, }, + tags: ['!autodocs'], parameters: { + msw: { + handlers: createDatasetListHandlers(mockEmptyResults), + }, docs: { description: { - story: 'Dataset list with no results (0 datasets returned). Shows how the component handles empty state. NOTE: This story will show mock data (25 results) due to shared MSW handlers. For true empty state testing, view this story in isolation.', + story: 'Empty API response (0 datasets returned). View this story standalone — on the docs page it shares the queryClient with other stories and can\'t render in isolation.', }, }, }, diff --git a/src/templates/DatasetSearch/DatasetSearch.stories.tsx b/src/templates/DatasetSearch/DatasetSearch.stories.tsx index 930b0b56..2ff90fcd 100644 --- a/src/templates/DatasetSearch/DatasetSearch.stories.tsx +++ b/src/templates/DatasetSearch/DatasetSearch.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import DatasetSearch from './index'; import { MemoryRouter } from 'react-router-dom'; import { QueryClientProvider } from '@tanstack/react-query'; diff --git a/src/templates/FilteredResource/FilteredResource.stories.tsx b/src/templates/FilteredResource/FilteredResource.stories.tsx new file mode 100644 index 00000000..7327258f --- /dev/null +++ b/src/templates/FilteredResource/FilteredResource.stories.tsx @@ -0,0 +1,72 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { MemoryRouter } from 'react-router-dom'; +import FilteredResource from './index'; +import { createDatasetPageHandlers } from '../../../.storybook/mswHandlers'; + +const meta: Meta = { + title: 'Templates/FilteredResource', + component: FilteredResource, + parameters: { + layout: 'fullscreen', + msw: { handlers: createDatasetPageHandlers() }, + docs: { + description: { + component: ` +The FilteredResource template renders a single dataset distribution with the full filter / sort / pagination UI. +It loads the dataset metadata via \`useMetastoreDataset\` and picks a distribution by index (\`dist_id\`). When the +identifier or dist_id is invalid, it falls back to the \`PageNotFound\` template inline. + `, + }, + }, + }, + decorators: [ + (Story) => ( + + + + ), + ], + argTypes: { + id: { control: 'text', description: 'Dataset identifier passed to useMetastoreDataset.' }, + dist_id: { control: 'text', description: 'Distribution index (number) or the literal "data".' }, + rootUrl: { control: 'text', description: 'Base URL for metastore and datastore requests.' }, + location: { control: false, description: 'Router location — supplies query params for the datastore call.' }, + customColumns: { control: 'object', description: 'Optional column overrides ({header, accessor}).' }, + customTitle: { control: 'text', description: 'Optional title to display above the distribution.' }, + customDescription: { control: false, description: 'Optional render function for the description block.' }, + setDatasetTitle: { control: false, description: 'Callback invoked with the dataset title.' }, + }, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +const defaultArgs = { + id: 'wb6u-x2ny', + dist_id: 0 as unknown as string, + rootUrl: '/api/1', + location: { search: '' } as Location, +}; + +export const Default: Story = { + args: defaultArgs, + parameters: { + docs: { + description: { + story: 'First distribution of the mock dataset rendered with filter/sort/download chrome.', + }, + }, + }, +}; + +export const InvalidDistribution: Story = { + args: { ...defaultArgs, dist_id: 99 as unknown as string }, + parameters: { + docs: { + description: { + story: 'Invalid `dist_id` — falls back to the inline "Dataset not found" message.', + }, + }, + }, +}; diff --git a/src/templates/Footer/Footer.stories.tsx b/src/templates/Footer/Footer.stories.tsx index 1e56bb8c..4f49d3b3 100644 --- a/src/templates/Footer/Footer.stories.tsx +++ b/src/templates/Footer/Footer.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import Footer from './index'; import { MemoryRouter } from 'react-router-dom'; import cmsLogo from '../../assets/images/cms-logo-footer.png'; diff --git a/src/templates/Header/Header.stories.tsx b/src/templates/Header/Header.stories.tsx index 44dba6af..76e68bbd 100644 --- a/src/templates/Header/Header.stories.tsx +++ b/src/templates/Header/Header.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import Header from './index'; import { MemoryRouter } from 'react-router-dom'; import CMSTopNav from '../../components/CMSTopNav'; @@ -6,8 +6,13 @@ import HeaderSiteTitle from '../../components/HeaderSiteTitle'; import HeaderNav from '../../components/HeaderNav'; import HeaderSearch from '../../components/HeaderSearch'; import MobileMenuButton from '../../components/MobileMenuButton'; -import cmsLogo from '../../assets/images/CMSgov@2x-white-O.png'; -import type { NavLinkArray, OrgType } from '../../types/misc'; +import { + cmsOrg as sampleOrg, + cmsOrgWithLogo as sampleOrgWithLogo, + mainNavLinks, + navLinksWithSubmenus, + topNavLinks, +} from '../../../.storybook/fixtures'; const meta: Meta = { title: 'Templates/Header', @@ -61,57 +66,6 @@ Key features: export default meta; type Story = StoryObj; -// Mock data -const sampleOrg: OrgType = { - url: 'https://www.cms.gov', - tagline: 'The Centers for Medicare & Medicaid Services', - urlTitle: 'CMS Open Data', - logoAltText: 'CMS Logo', -}; - -const sampleOrgWithLogo: OrgType = { - ...sampleOrg, - logoFilePath: cmsLogo, -}; - -const mainNavLinks: NavLinkArray[] = [ - { id: 'home', label: 'Home', url: '/' }, - { id: 'datasets', label: 'Datasets', url: '/datasets' }, - { id: 'api', label: 'API Documentation', url: '/api-docs' }, - { id: 'about', label: 'About', url: '/about' }, -]; - -const navLinksWithSubmenus: NavLinkArray[] = [ - { id: 'home', label: 'Home', url: '/' }, - { id: 'datasets', label: 'Datasets', url: '/datasets' }, - { - id: 'resources', - label: 'Resources', - url: '/resources', - submenu: [ - { id: 'api', label: 'API Documentation', url: '/api-docs' }, - { id: 'dictionary', label: 'Data Dictionary', url: '/data-dictionary' }, - { id: 'guides', label: 'User Guides', url: '/guides' }, - ], - }, - { - id: 'about', - label: 'About', - url: '/about', - submenu: [ - { id: 'mission', label: 'Our Mission', url: '/about/mission' }, - { id: 'team', label: 'Team', url: '/about/team' }, - { id: 'contact', label: 'Contact Us', url: '/about/contact' }, - ], - }, -]; - -const topNavLinks: NavLinkArray[] = [ - { id: 'cms-main', label: 'CMS.gov', url: 'https://www.cms.gov', target: '_blank' }, - { id: 'medicare', label: 'Medicare.gov', url: 'https://www.medicare.gov', target: '_blank' }, - { id: 'medicaid', label: 'Medicaid.gov', url: 'https://www.medicaid.gov', target: '_blank' }, -]; - export const Default: Story = { args: { mobileMaxWidth: 768, diff --git a/src/templates/PageNotFound/PageNotFound.stories.tsx b/src/templates/PageNotFound/PageNotFound.stories.tsx index bcbd673d..e16bab85 100644 --- a/src/templates/PageNotFound/PageNotFound.stories.tsx +++ b/src/templates/PageNotFound/PageNotFound.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import PageNotFound from './index'; const meta: Meta = { diff --git a/src/templates/SidebarPage/SidebarPage.stories.tsx b/src/templates/SidebarPage/SidebarPage.stories.tsx index ccaa6436..e53d1af7 100644 --- a/src/templates/SidebarPage/SidebarPage.stories.tsx +++ b/src/templates/SidebarPage/SidebarPage.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import SidebarPage from './index'; import { MemoryRouter } from 'react-router-dom'; diff --git a/src/templates/SpecsAndLimits/SpecsAndLimits.stories.tsx b/src/templates/SpecsAndLimits/SpecsAndLimits.stories.tsx index 503c97b9..aa0e0263 100644 --- a/src/templates/SpecsAndLimits/SpecsAndLimits.stories.tsx +++ b/src/templates/SpecsAndLimits/SpecsAndLimits.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import SpecsAndLimits from './index'; const meta: Meta = { diff --git a/src/templates/StoredQueryPage/StoredQueryPage.stories.tsx b/src/templates/StoredQueryPage/StoredQueryPage.stories.tsx index 0824b18e..1afbea1b 100644 --- a/src/templates/StoredQueryPage/StoredQueryPage.stories.tsx +++ b/src/templates/StoredQueryPage/StoredQueryPage.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import StoredQueryPage from './index'; import { MemoryRouter } from 'react-router-dom'; import { QueryClientProvider } from '@tanstack/react-query'; @@ -134,6 +134,11 @@ export const WithCustomColumns: Story = { }, }; +// Excluded from autodocs: this story uses a different datastore mock than the +// other stories, but they share a module-scope queryClient + identical queryKeys. +// On the docs page React Query dedupes the request and either this handler wins +// (corrupting other stories) or the other handlers win (showing the wrong data here). +// View standalone to see the filtered result set. export const WithQueryFilter: Story = { args: { id: 'provider-utilization-2023', @@ -145,18 +150,21 @@ export const WithQueryFilter: Story = { defaultPageSize: 25, disableTableControls: false, }, + tags: ['!autodocs'], parameters: { msw: { handlers: createStoredQueryPageHandlers(mockDatasetMetadata, mockFilteredDatastoreRecords), }, docs: { description: { - story: 'StoredQueryPage with a predefined query filter (state = "CA"). Shows only 2 California providers. The query prop contains a JSON-stringified array of filter conditions that are parsed and applied to the datastore query.', + story: 'StoredQueryPage with a predefined query filter (state = "CA"). Shows only 2 California providers. View standalone — on the docs page the shared queryClient prevents this story from rendering with its own mock.', }, }, }, }; +// Excluded from autodocs for the same reason as WithQueryFilter — different mock, +// shared queryClient. View standalone to see the 50-row mock paginate correctly. export const LargeDataset: Story = { args: { id: 'provider-utilization-2023', @@ -165,13 +173,14 @@ export const LargeDataset: Story = { defaultPageSize: 10, disableTableControls: false, }, + tags: ['!autodocs'], parameters: { msw: { handlers: createStoredQueryPageHandlers(mockDatasetMetadata, mockLargeDatastoreRecords), }, docs: { description: { - story: 'StoredQueryPage with 50 records and a page size of 10 to demonstrate pagination controls. Users can navigate between pages using the pagination toolbar.', + story: 'StoredQueryPage with 50 records and a page size of 10 to demonstrate pagination controls. View standalone — on the docs page the shared queryClient prevents this story from rendering with its own mock.', }, }, }, From 6e6f4f7cae0a3cbaefde85d61f03dfe1fbcc8833 Mon Sep 17 00:00:00 2001 From: dcgoodwin2112 Date: Tue, 26 May 2026 10:52:01 -0400 Subject: [PATCH 2/3] chore: update dependencies and enhance type definitions - Added new CMS-related packages: @cmsgov/ds-cms-gov, @cmsgov/ds-healthcare-gov, and @cmsgov/ds-medicare-gov. - Extended global type definitions to include CSS modules and inline CSS. - Updated TypeScript configuration to include all TypeScript and declaration files in the .storybook directory. --- .storybook/global.d.ts | 33 +++++++++ .storybook/mswHandlers.ts | 20 +++++- .storybook/preview.tsx | 62 ++++++++++++++++- package-lock.json | 136 ++++++++++++++------------------------ package.json | 3 + src/global.d.ts | 9 +++ tsconfig.json | 2 +- 7 files changed, 172 insertions(+), 93 deletions(-) create mode 100644 .storybook/global.d.ts diff --git a/.storybook/global.d.ts b/.storybook/global.d.ts new file mode 100644 index 00000000..b7853e27 --- /dev/null +++ b/.storybook/global.d.ts @@ -0,0 +1,33 @@ +declare module '*.css' { + const css: string; + export default css; +} + +declare module '*.css?inline' { + const css: string; + export default css; +} + +declare module '@cmsgov/design-system/css/index.css'; +declare module '@fortawesome/fontawesome-free/css/all.css'; +declare module './font-awesome-overrides.css'; + +declare module '@cmsgov/design-system/css/core-theme.css?inline' { + const css: string; + export default css; +} + +declare module '@cmsgov/ds-healthcare-gov/css/healthcare-theme.css?inline' { + const css: string; + export default css; +} + +declare module '@cmsgov/ds-medicare-gov/css/medicare-theme.css?inline' { + const css: string; + export default css; +} + +declare module '@cmsgov/ds-cms-gov/css/cmsgov-theme.css?inline' { + const css: string; + export default css; +} diff --git a/.storybook/mswHandlers.ts b/.storybook/mswHandlers.ts index 5e031ed3..eb250c45 100644 --- a/.storybook/mswHandlers.ts +++ b/.storybook/mswHandlers.ts @@ -149,12 +149,26 @@ export const createStoredQueryPageHandlers = ( /** * Creates MSW handlers for DatasetList stories. - * Mocks the search API endpoint. + * Mocks the search API endpoint. Respects `page-size` and `page` query params + * so the DatasetListSubmenu (page-size=4) and paginated DatasetList views + * render only the slice they asked for; `total` is preserved so "View all N" + * links and result counts still reflect the full dataset. */ export const createDatasetListHandlers = (searchResults: SearchResults) => [ - http.get('**/search/*', async () => { + http.get('**/search/*', async ({ request }) => { await delay(500); - return HttpResponse.json(searchResults); + const url = new URL(request.url); + const pageSize = parseInt(url.searchParams.get('page-size') ?? '', 10); + const page = parseInt(url.searchParams.get('page') ?? '', 10); + const entries = Object.entries(searchResults.results); + + if (!Number.isFinite(pageSize) || pageSize <= 0 || entries.length === 0) { + return HttpResponse.json(searchResults); + } + + const offset = Number.isFinite(page) && page > 1 ? (page - 1) * pageSize : 0; + const sliced = Object.fromEntries(entries.slice(offset, offset + pageSize)); + return HttpResponse.json({ ...searchResults, results: sliced }); }), ]; diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 33290349..d861416c 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,15 +1,46 @@ import type { Preview } from '@storybook/react-vite' import '@cmsgov/design-system/css/index.css'; -import '@cmsgov/design-system/css/core-theme.css'; import '@fortawesome/fontawesome-free/css/all.css'; import './font-awesome-overrides.css'; import { initialize, mswLoader } from 'msw-storybook-addon'; import { handlers } from './mswHandlers'; +// CMS Design System theme files — each is a single :root { --var: ... } block +// of CSS custom-property overrides on top of the shared base in `index.css`. +// `?inline` returns the file content as a string; we inject the active theme +// into a single