@hubspot/cms-component-library 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/components/componentLibrary/Accordion/AccordionContent/ContentFields.tsx +5 -3
  2. package/components/componentLibrary/Accordion/AccordionItem/StyleFields.tsx +5 -3
  3. package/components/componentLibrary/Accordion/AccordionItem/index.module.scss +2 -2
  4. package/components/componentLibrary/Accordion/AccordionItem/index.tsx +3 -3
  5. package/components/componentLibrary/Accordion/AccordionTitle/ContentFields.tsx +5 -3
  6. package/components/componentLibrary/Accordion/AccordionTitle/index.module.scss +2 -2
  7. package/components/componentLibrary/Accordion/stories/Accordion.stories.tsx +80 -1
  8. package/components/componentLibrary/Accordion/stories/AccordionDecorator.tsx +14 -14
  9. package/components/componentLibrary/Button/ContentFields.tsx +5 -3
  10. package/components/componentLibrary/Button/StyleFields.tsx +5 -3
  11. package/components/componentLibrary/Button/index.module.scss +22 -14
  12. package/components/componentLibrary/Button/index.tsx +6 -6
  13. package/components/componentLibrary/Button/stories/Button.AsButton.stories.tsx +30 -1
  14. package/components/componentLibrary/Button/stories/Button.AsLink.stories.tsx +38 -1
  15. package/components/componentLibrary/Button/stories/ButtonDecorator.tsx +1 -1
  16. package/components/componentLibrary/Card/StyleFields.tsx +5 -3
  17. package/components/componentLibrary/Card/stories/Card.stories.tsx +46 -1
  18. package/components/componentLibrary/Card/stories/CardDecorator.tsx +1 -1
  19. package/components/componentLibrary/Divider/ContentFields.tsx +5 -3
  20. package/components/componentLibrary/Divider/StyleFields.tsx +5 -3
  21. package/components/componentLibrary/Divider/index.module.scss +6 -6
  22. package/components/componentLibrary/Divider/index.tsx +7 -3
  23. package/components/componentLibrary/Divider/stories/Divider.stories.tsx +44 -50
  24. package/components/componentLibrary/Divider/stories/{DividerDecorator.module.css → DividerDecorator.module.scss} +5 -4
  25. package/components/componentLibrary/Divider/stories/DividerDecorator.tsx +1 -1
  26. package/components/componentLibrary/Divider/types.ts +3 -1
  27. package/components/componentLibrary/Drawer/hooks/index.tsx +13 -0
  28. package/components/componentLibrary/Drawer/index.module.scss +94 -0
  29. package/components/componentLibrary/Drawer/index.tsx +131 -0
  30. package/components/componentLibrary/Drawer/llm.txt +416 -0
  31. package/components/componentLibrary/Drawer/stories/Drawer.stories.tsx +512 -0
  32. package/components/componentLibrary/Drawer/stories/DrawerDecorator.module.scss +8 -0
  33. package/components/componentLibrary/Drawer/stories/DrawerDecorator.tsx +18 -0
  34. package/components/componentLibrary/Drawer/types.ts +25 -0
  35. package/components/componentLibrary/Flex/stories/FlexDecorator.tsx +1 -1
  36. package/components/componentLibrary/Flex/types.ts +3 -1
  37. package/components/componentLibrary/Grid/stories/Grid.stories.tsx +454 -152
  38. package/components/componentLibrary/Grid/stories/GridDecorator.tsx +2 -2
  39. package/components/componentLibrary/Heading/ContentFields.tsx +5 -3
  40. package/components/componentLibrary/Heading/StyleFields.tsx +11 -9
  41. package/components/componentLibrary/Heading/index.tsx +3 -3
  42. package/components/componentLibrary/Heading/llm.txt +8 -8
  43. package/components/componentLibrary/Heading/stories/Heading.stories.tsx +3 -3
  44. package/components/componentLibrary/Heading/stories/HeadingDecorator.tsx +1 -1
  45. package/components/componentLibrary/Heading/types.ts +4 -4
  46. package/components/componentLibrary/Icon/ContentFields.tsx +5 -3
  47. package/components/componentLibrary/Icon/stories/Icon.stories.tsx +1 -1
  48. package/components/componentLibrary/Icon/stories/IconDecorator.tsx +1 -1
  49. package/components/componentLibrary/Image/ContentFields.tsx +5 -3
  50. package/components/componentLibrary/Image/index.tsx +4 -4
  51. package/components/componentLibrary/Image/llm.txt +17 -17
  52. package/components/componentLibrary/Image/stories/Image.stories.tsx +61 -18
  53. package/components/componentLibrary/Image/stories/ImageDecorator.tsx +1 -1
  54. package/components/componentLibrary/Image/types.ts +2 -2
  55. package/components/componentLibrary/LanguageSwitcher/ContentFields.tsx +18 -0
  56. package/components/componentLibrary/LanguageSwitcher/LanguageOptions.module.scss +37 -0
  57. package/components/componentLibrary/LanguageSwitcher/LanguageOptions.tsx +65 -0
  58. package/components/componentLibrary/LanguageSwitcher/StyleFields.tsx +48 -0
  59. package/components/componentLibrary/LanguageSwitcher/_dummyData.tsx +247 -0
  60. package/components/componentLibrary/LanguageSwitcher/assets/Globe.tsx +16 -0
  61. package/components/componentLibrary/LanguageSwitcher/index.module.scss +58 -0
  62. package/components/componentLibrary/LanguageSwitcher/index.tsx +125 -0
  63. package/components/componentLibrary/LanguageSwitcher/llm.txt +380 -0
  64. package/components/componentLibrary/LanguageSwitcher/stories/LanguageSwitcher.stories.tsx +349 -0
  65. package/components/componentLibrary/LanguageSwitcher/stories/LanguageSwitcherDecorator.module.scss +5 -0
  66. package/components/componentLibrary/LanguageSwitcher/stories/LanguageSwitcherDecorator.tsx +8 -0
  67. package/components/componentLibrary/LanguageSwitcher/types.ts +48 -0
  68. package/components/componentLibrary/LanguageSwitcher/utils.tsx +38 -0
  69. package/components/componentLibrary/Link/ContentFields.tsx +5 -3
  70. package/components/componentLibrary/Link/StyleFields.tsx +5 -3
  71. package/components/componentLibrary/Link/index.module.scss +10 -0
  72. package/components/componentLibrary/Link/index.tsx +24 -14
  73. package/components/componentLibrary/Link/stories/Link.stories.tsx +35 -5
  74. package/components/componentLibrary/Link/stories/LinkDecorator.tsx +11 -1
  75. package/components/componentLibrary/Link/types.ts +22 -13
  76. package/components/componentLibrary/List/ContentFields.tsx +5 -3
  77. package/components/componentLibrary/List/ListItem/ContentFields.tsx +6 -17
  78. package/components/componentLibrary/List/ListItem/index.module.scss +1 -13
  79. package/components/componentLibrary/List/ListItem/index.tsx +3 -30
  80. package/components/componentLibrary/List/ListItem/types.ts +1 -16
  81. package/components/componentLibrary/List/StyleFields.tsx +15 -18
  82. package/components/componentLibrary/List/index.module.scss +3 -0
  83. package/components/componentLibrary/List/index.tsx +5 -2
  84. package/components/componentLibrary/List/llm.txt +73 -103
  85. package/components/componentLibrary/List/stories/List.stories.tsx +56 -80
  86. package/components/componentLibrary/List/stories/ListDecorator.tsx +3 -6
  87. package/components/componentLibrary/List/types.ts +1 -3
  88. package/components/componentLibrary/Logo/_dummyLogoData.ts +12 -0
  89. package/components/componentLibrary/Logo/assets/hubspot-logo.png +0 -0
  90. package/components/componentLibrary/Logo/index.module.scss +22 -0
  91. package/components/componentLibrary/Logo/index.tsx +73 -0
  92. package/components/componentLibrary/Logo/llm.txt +262 -0
  93. package/components/componentLibrary/Logo/stories/Logo.stories.tsx +88 -0
  94. package/components/componentLibrary/Logo/stories/LogoDecorator.module.scss +10 -0
  95. package/components/componentLibrary/Logo/stories/LogoDecorator.tsx +8 -0
  96. package/components/componentLibrary/Logo/types.tsx +16 -0
  97. package/components/componentLibrary/Menu/ContentFields.tsx +16 -0
  98. package/components/componentLibrary/Menu/MenuItem/Chevron/index.module.scss +6 -0
  99. package/components/componentLibrary/Menu/MenuItem/Chevron/index.tsx +17 -0
  100. package/components/componentLibrary/Menu/MenuItem/index.module.scss +7 -0
  101. package/components/componentLibrary/Menu/MenuItem/index.tsx +266 -0
  102. package/components/componentLibrary/Menu/MenuItem/types.ts +17 -0
  103. package/components/componentLibrary/Menu/NavigationMenu/ContentFields.tsx +20 -0
  104. package/components/componentLibrary/Menu/NavigationMenu/index.tsx +18 -0
  105. package/components/componentLibrary/Menu/NavigationMenu/islands/NavigationMenuIsland.tsx +95 -0
  106. package/components/componentLibrary/Menu/NavigationMenu/islands/index.module.scss +100 -0
  107. package/components/componentLibrary/Menu/NavigationMenu/islands/types.ts +19 -0
  108. package/components/componentLibrary/Menu/NavigationMenu/llm.txt +197 -0
  109. package/components/componentLibrary/Menu/NavigationMenu/stories/NavigationMenu.stories.tsx +286 -0
  110. package/components/componentLibrary/Menu/NavigationMenu/stories/NavigationMenuDecorator.module.scss +15 -0
  111. package/components/componentLibrary/Menu/NavigationMenu/stories/NavigationMenuDecorator.tsx +12 -0
  112. package/components/componentLibrary/Menu/NavigationMenu/types.ts +3 -0
  113. package/components/componentLibrary/Menu/VerticalMenu/ContentFields.tsx +20 -0
  114. package/components/componentLibrary/Menu/VerticalMenu/index.tsx +18 -0
  115. package/components/componentLibrary/Menu/VerticalMenu/islands/index.module.scss +53 -0
  116. package/components/componentLibrary/Menu/VerticalMenu/islands/verticalMenuIsland.tsx +78 -0
  117. package/components/componentLibrary/Menu/VerticalMenu/llm.txt +177 -0
  118. package/components/componentLibrary/Menu/VerticalMenu/stories/VerticalMenu.stories.tsx +242 -0
  119. package/components/componentLibrary/Menu/VerticalMenu/stories/VerticalMenuDecorator.module.scss +19 -0
  120. package/components/componentLibrary/Menu/VerticalMenu/stories/VerticalMenuDecorator.tsx +12 -0
  121. package/components/componentLibrary/Menu/VerticalMenu/types.ts +21 -0
  122. package/components/componentLibrary/Menu/_dummyMenuData.js +1346 -0
  123. package/components/componentLibrary/Menu/types.ts +56 -0
  124. package/components/componentLibrary/Menu/utils/transformMenuData.ts +11 -0
  125. package/components/componentLibrary/_patterns/README.md +15 -17
  126. package/components/componentLibrary/_patterns/checklist-and-examples.md +17 -17
  127. package/components/componentLibrary/_patterns/component-structure.md +21 -23
  128. package/components/componentLibrary/_patterns/css-patterns.md +170 -18
  129. package/components/componentLibrary/_patterns/field-patterns.md +97 -27
  130. package/components/componentLibrary/_patterns/function-declaration-patterns.md +281 -0
  131. package/components/componentLibrary/_patterns/llm-txt.template.md +4 -2
  132. package/components/componentLibrary/_patterns/prop-naming-patterns.md +208 -0
  133. package/components/componentLibrary/_patterns/storybook-patterns.md +25 -8
  134. package/components/componentLibrary/_patterns/typescript-patterns.md +6 -3
  135. package/package.json +4 -2
  136. /package/components/componentLibrary/Button/stories/{ButtonDecorator.module.css → ButtonDecorator.module.scss} +0 -0
  137. /package/components/componentLibrary/Card/stories/{CardDecorator.module.css → CardDecorator.module.scss} +0 -0
  138. /package/components/componentLibrary/Flex/stories/{FlexDecorator.module.css → FlexDecorator.module.scss} +0 -0
  139. /package/components/componentLibrary/Grid/stories/{GridDecorator.module.css → GridDecorator.module.scss} +0 -0
  140. /package/components/componentLibrary/Heading/stories/{HeadingDecorator.module.css → HeadingDecorator.module.scss} +0 -0
  141. /package/components/componentLibrary/Icon/stories/{IconDecorator.module.css → IconDecorator.module.scss} +0 -0
  142. /package/components/componentLibrary/Image/stories/{ImageDecorator.module.css → ImageDecorator.module.scss} +0 -0
  143. /package/components/componentLibrary/Image/stories/assets/{catSmile.jpg → cat-smile.jpg} +0 -0
@@ -0,0 +1,19 @@
1
+ import { FlexJustifyContent } from '../../../Flex/types.js';
2
+ import { CSSVariables } from '../../../utils/types.js';
3
+ import { SiteMenu } from '@hubspot/cms-components';
4
+
5
+ export type NavigationMenuProps = {
6
+ navAriaLabel?: string;
7
+ justifyMenu?: FlexJustifyContent;
8
+ className?: string;
9
+ style?: CSSVariables;
10
+ linkColor?: string;
11
+ linkColorHover?: string;
12
+ backgroundColor?: string;
13
+ backgroundColorHover?: string;
14
+ submenuLinkColor?: string;
15
+ submenuLinkColorHover?: string;
16
+ submenuBackgroundColor?: string;
17
+ submenuBackgroundColorHover?: string;
18
+ menuData: SiteMenu;
19
+ };
@@ -0,0 +1,197 @@
1
+ # NavigationMenu Component
2
+
3
+ A horizontal navigation menu component that renders a top-level menu bar with dropdown submenus, keyboard navigation support, and flexible styling options.
4
+
5
+ ## Import path
6
+ ```tsx
7
+ import NavigationMenu from '@hubspot/cms-component-library/NavigationMenu';
8
+ ```
9
+
10
+ ## Purpose
11
+
12
+ The NavigationMenu component provides a horizontal navigation menu for HubSpot CMS projects. It automatically integrates with HubSpot's site menu system, supporting multi-level dropdown menus with keyboard navigation, customizable colors for links and backgrounds, and flexible horizontal alignment options.
13
+
14
+ ## Component Structure
15
+
16
+ ```
17
+ NavigationMenu/
18
+ ├── index.tsx # Main component wrapper (Island integration)
19
+ ├── ContentFields.tsx # HubSpot field definitions for menu selection
20
+ ├── islands/
21
+ │ ├── NavigationMenuIsland.tsx # Island component with rendering logic
22
+ │ ├── types.ts # TypeScript type definitions
23
+ │ └── index.module.scss # CSS module with design tokens
24
+ └── stories/
25
+ ├── NavigationMenu.stories.tsx # Storybook examples
26
+ └── NavigationMenuDecorator.tsx # Storybook decorator
27
+ ```
28
+
29
+ ## Components
30
+
31
+ ### NavigationMenu (Main Component)
32
+
33
+ **Purpose:** Renders a horizontal navigation menu from HubSpot site menu data with dropdown submenus.
34
+
35
+ **Props:**
36
+ ```tsx
37
+ {
38
+ menuData: SiteMenu; // Menu data from useMenu hook (required)
39
+ justifyMenu?: FlexJustifyContent; // Horizontal alignment (default: 'flex-start')
40
+ navAriaLabel?: string; // ARIA label for navigation (default: 'Navigation')
41
+ linkColor?: string; // Top-level link text color
42
+ linkColorHover?: string; // Top-level link hover text color
43
+ backgroundColor?: string; // Top-level link background color
44
+ backgroundColorHover?: string; // Top-level link hover background color
45
+ submenuLinkColor?: string; // Submenu link text color
46
+ submenuLinkColorHover?: string; // Submenu link hover text color
47
+ submenuBackgroundColor?: string; // Submenu link background color
48
+ submenuBackgroundColorHover?: string; // Submenu link hover background color
49
+ className?: string; // Additional CSS classes
50
+ style?: React.CSSProperties; // Inline styles with CSS variables
51
+ }
52
+ ```
53
+
54
+ ## Consuming the Component with useMenu Hook
55
+
56
+ The NavigationMenu component requires menu data from HubSpot's `useMenu` hook. This hook fetches the site menu structure based on a menu ID defined in your module fields.
57
+
58
+ ### Basic Module Implementation
59
+
60
+ ```tsx
61
+ import { ModuleMeta } from '../types/modules.js';
62
+ import NavigationMenu from '@hubspot/cms-component-library/NavigationMenu';
63
+ import { useMenu, fieldPath } from '@hubspot/cms-components';
64
+
65
+ export const Component = () => {
66
+ // Fetch menu data using the useMenu hook
67
+ // fieldPath('menu') references the menu field defined in fields.tsx
68
+ const menuData = useMenu(fieldPath('menu'));
69
+
70
+ return (
71
+ <NavigationMenu
72
+ menuData={menuData}
73
+ justifyMenu="flex-start"
74
+ navAriaLabel="Main navigation"
75
+ />
76
+ );
77
+ };
78
+
79
+ export { fields } from './fields.js';
80
+
81
+ export const meta: ModuleMeta = {
82
+ label: 'Main Navigation Menu',
83
+ content_types: [],
84
+ icon: '',
85
+ categories: ['navigation'],
86
+ };
87
+ ```
88
+
89
+ ### Module Fields Definition
90
+
91
+ ```tsx
92
+ import { ModuleFields } from '@hubspot/cms-components/fields';
93
+ import NavigationMenu from '@hubspot/cms-component-library/NavigationMenu';
94
+
95
+ export const fields = (
96
+ <ModuleFields>
97
+ <NavigationMenu.ContentFields />
98
+ </ModuleFields>
99
+ );
100
+ ```
101
+
102
+ **What this does:**
103
+ 1. `NavigationMenu.ContentFields` creates a HubSpot MenuField in your module
104
+ 2. This field allows content editors to select which site menu to display
105
+ 3. The `useMenu(fieldPath('menu'))` hook fetches the selected menu's data
106
+ 4. The menu data is passed to the NavigationMenu component via the `menuData` prop
107
+
108
+ ## Usage Examples
109
+
110
+ ### Basic Navigation Menu
111
+
112
+ ```tsx
113
+ import NavigationMenu from '@hubspot/cms-component-library/NavigationMenu';
114
+ import { useMenu, fieldPath } from '@hubspot/cms-components';
115
+
116
+ export const Component = () => {
117
+ const menuData = useMenu(fieldPath('menu'));
118
+
119
+ return <NavigationMenu menuData={menuData} />;
120
+ };
121
+ ```
122
+
123
+
124
+ ## HubSpot CMS Integration
125
+
126
+ ### Field Definitions
127
+
128
+ The NavigationMenu component provides field definitions for menu selection in HubSpot CMS modules.
129
+
130
+ #### ContentFields
131
+
132
+ Configurable props for customizing field labels, names, and defaults:
133
+
134
+ ```tsx
135
+ <NavigationMenu.ContentFields
136
+ menuLabel="Menu"
137
+ menuName="menu"
138
+ menuDefault="default"
139
+ />
140
+ ```
141
+
142
+ **Fields:**
143
+ - `menu`: MenuField for selecting which site menu to display (default: 'default')
144
+
145
+ ## Styling
146
+
147
+ ### CSS Variables
148
+
149
+ The NavigationMenu component uses CSS variables for theming and customization:
150
+
151
+ **Base Menu Styles:**
152
+ - `--hscl-navigationMenu-gap`: Space between top-level menu items
153
+ - `--hscl-navigationMenu-color`: Link text color (default: currentColor)
154
+ - `--hscl-navigationMenu-backgroundColor`: Link background color (default: transparent)
155
+ - `--hscl-navigationMenu-color-hover`: Link hover text color
156
+ - `--hscl-navigationMenu-backgroundColor-hover`: Link hover background color
157
+
158
+ **Submenu-Specific Styles:**
159
+ - `--hscl-navigationMenu-submenu-color`: Submenu link text color
160
+ - `--hscl-navigationMenu-submenu-backgroundColor`: Submenu link background color
161
+ - `--hscl-navigationMenu-submenu-color-hover`: Submenu link hover text color
162
+ - `--hscl-navigationMenu-submenu-backgroundColor-hover`: Submenu link hover background color
163
+
164
+
165
+ ## Accessibility
166
+
167
+ The NavigationMenu component follows accessibility best practices:
168
+
169
+ - **Semantic HTML**: Renders as a `<nav>` element with an unordered list (`<ul>`)
170
+ - **ARIA Labels**: Customizable `aria-label` via the `navAriaLabel` prop
171
+ - **ARIA Roles**: List items have `role="menuitem"` for proper screen reader support
172
+ - **Keyboard Navigation**: Full keyboard support with arrow keys
173
+ - `ArrowDown`: Opens submenu or moves to next item
174
+ - `ArrowUp`: Closes submenu or moves to previous item
175
+ - `ArrowRight`: Moves to next top-level item or opens nested submenu
176
+ - `ArrowLeft`: Moves to previous top-level item or returns to parent
177
+ - `Enter`/`Space`: Activates link
178
+ - `Escape`: Closes submenu
179
+ - `Home`/`End`: Jump to first/last item
180
+ - **Focus Management**: Keyboard focus properly managed across menu levels
181
+ - **External Links**: Automatically adds `target="_blank"` and `rel="noopener noreferrer"` for external links
182
+
183
+ ## Best Practices
184
+
185
+ - **Always use useMenu hook**: The NavigationMenu component requires menu data from the `useMenu` hook—never pass hardcoded menu structures
186
+ - **Consistent alignment**: Use the same `justifyMenu` value across navigation instances for visual consistency
187
+ - **ARIA labels**: Provide descriptive `navAriaLabel` values (e.g., "Main navigation", "Footer navigation")
188
+ - **Color contrast**: Ensure sufficient contrast between link colors and backgrounds (WCAG AA minimum)
189
+ - **Hover states**: Always define hover colors for better visual feedback
190
+ - **Submenu styling**: Use distinct colors for dropdown menus to differentiate them from top-level items
191
+ - **Override CSS variables**: Customize appearance using CSS variables rather than overriding class styles
192
+ - **Null checking**: Always check if menuData exists before rendering, as useMenu can return null
193
+
194
+ ## Related Components
195
+
196
+ - **VerticalMenu**: Vertical navigation menu component for sidebar navigation
197
+ - **MenuItem**: Used internally to render individual menu items with support for nested children and keyboard navigation
@@ -0,0 +1,286 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import NavigationMenu from '../index.js';
3
+ import { NavigationMenuProps } from '../islands/types.js';
4
+ import { withNavigationMenuStyles } from './NavigationMenuDecorator.js';
5
+ import { SBContainer, SBFocusWrapper } from '@sb-utils';
6
+ import { dummyMenuData } from '../../_dummyMenuData.js';
7
+
8
+ const meta: Meta<NavigationMenuProps> = {
9
+ title: 'Component Library/Menu/Navigation Menu',
10
+ component: NavigationMenu,
11
+ parameters: {
12
+ layout: 'fullwidth',
13
+ docs: {
14
+ description: {
15
+ component:
16
+ 'The NavigationMenu component creates a horizontal navigation menu with dropdown submenus. It supports keyboard navigation and is typically used in website headers.',
17
+ },
18
+ },
19
+ },
20
+ decorators: [withNavigationMenuStyles],
21
+ };
22
+
23
+ export default meta;
24
+ type Story = StoryObj<typeof meta>;
25
+
26
+ export const Default: Story = {
27
+ render: () => (
28
+ <SBContainer
29
+ addBackground
30
+ style={{ overflow: 'visible', minHeight: '300px' }}
31
+ >
32
+ <NavigationMenu navAriaLabel="Main Navigation" menuData={dummyMenuData} />
33
+ </SBContainer>
34
+ ),
35
+ };
36
+
37
+ export const JustifyContent: Story = {
38
+ name: 'Justify Content Options',
39
+ render: () => (
40
+ <SBContainer flex direction="column" gap="extralarge">
41
+ <SBContainer
42
+ addBackground
43
+ style={{ overflow: 'visible', minHeight: '250px' }}
44
+ >
45
+ <h4>Left Aligned (Default)</h4>
46
+ <NavigationMenu justifyMenu="flex-start" menuData={dummyMenuData} />
47
+ </SBContainer>
48
+
49
+ <SBContainer
50
+ addBackground
51
+ style={{ overflow: 'visible', minHeight: '250px' }}
52
+ >
53
+ <h4>Center Aligned</h4>
54
+ <NavigationMenu justifyMenu="center" menuData={dummyMenuData} />
55
+ </SBContainer>
56
+
57
+ <SBContainer
58
+ addBackground
59
+ style={{ overflow: 'visible', minHeight: '250px' }}
60
+ >
61
+ <h4>Right Aligned</h4>
62
+ <NavigationMenu justifyMenu="flex-end" menuData={dummyMenuData} />
63
+ </SBContainer>
64
+
65
+ <SBContainer
66
+ addBackground
67
+ style={{ overflow: 'visible', minHeight: '250px' }}
68
+ >
69
+ <h4>Space Between</h4>
70
+ <NavigationMenu justifyMenu="space-between" menuData={dummyMenuData} />
71
+ </SBContainer>
72
+
73
+ <SBContainer
74
+ addBackground
75
+ style={{ overflow: 'visible', minHeight: '250px' }}
76
+ >
77
+ <h4>Space Around</h4>
78
+ <NavigationMenu justifyMenu="space-around" menuData={dummyMenuData} />
79
+ </SBContainer>
80
+ </SBContainer>
81
+ ),
82
+ };
83
+
84
+ export const ColorCustomization: Story = {
85
+ name: 'Color Customization',
86
+ render: () => (
87
+ <SBContainer flex direction="column" gap="extralarge">
88
+ <SBContainer
89
+ addBackground
90
+ style={{ overflow: 'visible', minHeight: '250px' }}
91
+ >
92
+ <h4>Default Colors</h4>
93
+ <NavigationMenu menuData={dummyMenuData} />
94
+ </SBContainer>
95
+
96
+ <SBContainer
97
+ addBackground
98
+ style={{ overflow: 'visible', minHeight: '250px' }}
99
+ >
100
+ <h4>Custom Link Colors</h4>
101
+ <NavigationMenu
102
+ linkColor="#2d3e50"
103
+ linkColorHover="#ff7a59"
104
+ menuData={dummyMenuData}
105
+ />
106
+ </SBContainer>
107
+
108
+ <SBContainer
109
+ addBackground
110
+ style={{ overflow: 'visible', minHeight: '250px' }}
111
+ >
112
+ <h4>With Background Colors</h4>
113
+ <NavigationMenu
114
+ linkColor="#ffffff"
115
+ linkColorHover="#ffffff"
116
+ backgroundColor="#0066cc"
117
+ backgroundColorHover="#0052a3"
118
+ menuData={dummyMenuData}
119
+ />
120
+ </SBContainer>
121
+
122
+ <SBContainer
123
+ addBackground
124
+ style={{ overflow: 'visible', minHeight: '250px' }}
125
+ >
126
+ <h4>Custom Submenu Colors</h4>
127
+ <NavigationMenu
128
+ linkColor="#1a1a1a"
129
+ linkColorHover="#e91e63"
130
+ backgroundColor="#fff3e0"
131
+ backgroundColorHover="#ffe0b2"
132
+ submenuLinkColor="#4a148c"
133
+ submenuLinkColorHover="#00e676"
134
+ submenuBackgroundColor="#f3e5f5"
135
+ submenuBackgroundColorHover="#e1bee7"
136
+ menuData={dummyMenuData}
137
+ />
138
+ </SBContainer>
139
+
140
+ <SBContainer
141
+ style={{
142
+ backgroundColor: '#1a1a1a',
143
+ padding: '20px',
144
+ minHeight: '300px',
145
+ overflow: 'visible',
146
+ }}
147
+ >
148
+ <h4 style={{ color: '#ffffff', marginTop: 0 }}>Dark Theme</h4>
149
+ <NavigationMenu
150
+ linkColor="#e0e0e0"
151
+ linkColorHover="#ffffff"
152
+ backgroundColor="#2d2d2d"
153
+ backgroundColorHover="#3d3d3d"
154
+ submenuLinkColor="#e0e0e0"
155
+ submenuLinkColorHover="#ffffff"
156
+ submenuBackgroundColor="#2d2d2d"
157
+ submenuBackgroundColorHover="#3d3d3d"
158
+ menuData={dummyMenuData}
159
+ />
160
+ </SBContainer>
161
+ </SBContainer>
162
+ ),
163
+ };
164
+
165
+ export const FocusState: Story = {
166
+ name: 'Focus State',
167
+ render: () => (
168
+ <SBContainer flex direction="column" gap="large">
169
+ <SBContainer
170
+ addBackground
171
+ style={{ overflow: 'visible', minHeight: '250px' }}
172
+ >
173
+ <h4>Focus State</h4>
174
+ <p>
175
+ In production, this focus outline appears when navigating with the
176
+ keyboard (Tab key). This Storybook demo auto-applies the focus state
177
+ to show how it looks.
178
+ </p>
179
+ <SBFocusWrapper>
180
+ <NavigationMenu
181
+ navAriaLabel="Focused Navigation"
182
+ menuData={dummyMenuData}
183
+ />
184
+ </SBFocusWrapper>
185
+ </SBContainer>
186
+ </SBContainer>
187
+ ),
188
+ };
189
+
190
+ export const AccessibilityFeatures: Story = {
191
+ name: 'Accessibility Features',
192
+ render: () => (
193
+ <SBContainer flex direction="column" gap="large">
194
+ <SBContainer
195
+ addBackground
196
+ style={{ overflow: 'visible', minHeight: '250px' }}
197
+ >
198
+ <h4>ARIA Label</h4>
199
+ <p>
200
+ The navigation element includes an aria-label for screen readers
201
+ (inspect the element to see it).
202
+ </p>
203
+ <NavigationMenu
204
+ navAriaLabel="Main Site Navigation"
205
+ menuData={dummyMenuData}
206
+ />
207
+ </SBContainer>
208
+
209
+ <SBContainer
210
+ addBackground
211
+ style={{ overflow: 'visible', minHeight: '250px' }}
212
+ >
213
+ <h4>Custom ARIA Label</h4>
214
+ <p>
215
+ You can customize the ARIA label to describe the navigation purpose.
216
+ </p>
217
+ <NavigationMenu
218
+ navAriaLabel="Footer Navigation"
219
+ menuData={dummyMenuData}
220
+ />
221
+ </SBContainer>
222
+
223
+ <SBContainer
224
+ addBackground
225
+ style={{ overflow: 'visible', minHeight: '250px' }}
226
+ >
227
+ <h4>Keyboard Accessible</h4>
228
+ <p>
229
+ All menu items and submenus are fully keyboard accessible using Tab,
230
+ Enter, and Arrow keys.
231
+ </p>
232
+ <NavigationMenu
233
+ navAriaLabel="Accessible Navigation Demo"
234
+ menuData={dummyMenuData}
235
+ />
236
+ </SBContainer>
237
+ </SBContainer>
238
+ ),
239
+ };
240
+
241
+ export const RealWorldExample: Story = {
242
+ name: 'In Context',
243
+ render: () => (
244
+ <SBContainer
245
+ style={{
246
+ backgroundColor: '#ffffff',
247
+ borderBottom: '1px solid #cbd6e2',
248
+ padding: '0',
249
+ minHeight: '350px',
250
+ overflow: 'visible',
251
+ }}
252
+ >
253
+ <SBContainer
254
+ flex
255
+ direction="row"
256
+ alignItems="center"
257
+ justifyContent="space-between"
258
+ style={{
259
+ maxWidth: '1200px',
260
+ margin: '0 auto',
261
+ padding: '0 50px',
262
+ }}
263
+ >
264
+ <SBContainer
265
+ style={{
266
+ fontSize: '24px',
267
+ fontWeight: 'bold',
268
+ color: '#33475b',
269
+ }}
270
+ >
271
+ Acme Corp
272
+ </SBContainer>
273
+ <NavigationMenu
274
+ justifyMenu="flex-end"
275
+ linkColor="#33475b"
276
+ linkColorHover="#0066cc"
277
+ submenuLinkColor="#516f90"
278
+ submenuLinkColorHover="#0066cc"
279
+ submenuBackgroundColor="#ffffff"
280
+ submenuBackgroundColorHover="#f5f8fa"
281
+ menuData={dummyMenuData}
282
+ />
283
+ </SBContainer>
284
+ </SBContainer>
285
+ ),
286
+ };
@@ -0,0 +1,15 @@
1
+ .navigationMenuContainer {
2
+ --hscl-navigationMenu-gap: 0px;
3
+ --hscl-navigationMenu-color: #33475b;
4
+ --hscl-navigationMenu-color-hover: #0066cc;
5
+ --hscl-navigationMenu-backgroundColor: transparent;
6
+ --hscl-navigationMenu-backgroundColor-hover: #f5f8fa;
7
+ --hscl-navigationMenu-submenu-color: #33475b;
8
+ --hscl-navigationMenu-submenu-color-hover: #0066cc;
9
+ --hscl-navigationMenu-submenu-backgroundColor: #ffffff;
10
+ --hscl-navigationMenu-submenu-backgroundColor-hover: #f5f8fa;
11
+
12
+ nav li {
13
+ margin-bottom: 0;
14
+ }
15
+ }
@@ -0,0 +1,12 @@
1
+ import type { Decorator } from '@storybook/react';
2
+ import styles from './NavigationMenuDecorator.module.scss';
3
+
4
+ /**
5
+ * Shared decorator for NavigationMenu stories that applies default CSS variables
6
+ * and scoped styles to prevent pollution across other Storybook stories.
7
+ */
8
+ export const withNavigationMenuStyles: Decorator = Story => (
9
+ <div className={styles.navigationMenuContainer}>
10
+ <Story />
11
+ </div>
12
+ );
@@ -0,0 +1,3 @@
1
+ import { ContentFieldsProps } from '../types.js';
2
+
3
+ export type NavigationMenuContentFieldProps = ContentFieldsProps;
@@ -0,0 +1,20 @@
1
+ import BaseContentFields from '../ContentFields.js';
2
+ import { VerticalMenuContentFieldProps } from './types.js';
3
+
4
+ const ContentFields = ({
5
+ menuLabel,
6
+ menuName,
7
+ menuDefault,
8
+ }: VerticalMenuContentFieldProps) => {
9
+ return (
10
+ <>
11
+ <BaseContentFields
12
+ menuLabel={menuLabel}
13
+ menuName={menuName}
14
+ menuDefault={menuDefault}
15
+ />
16
+ </>
17
+ );
18
+ };
19
+
20
+ export default ContentFields;
@@ -0,0 +1,18 @@
1
+ // @ts-expect-error -- ?island not typed
2
+ import VerticalMenuIsland from './islands/verticalMenuIsland.js?island';
3
+ import { VerticalMenuProps } from './types.js';
4
+ import { Island } from '@hubspot/cms-components';
5
+ import ContentFields from './ContentFields.js';
6
+
7
+ const VerticalMenuComponent = (props: VerticalMenuProps) => {
8
+ return <Island module={VerticalMenuIsland} {...props} />;
9
+ };
10
+
11
+ type VerticalMenuComponentType = typeof VerticalMenuComponent & {
12
+ ContentFields: typeof ContentFields;
13
+ };
14
+
15
+ const VerticalMenu = VerticalMenuComponent as VerticalMenuComponentType;
16
+ VerticalMenu.ContentFields = ContentFields;
17
+
18
+ export default VerticalMenu;
@@ -0,0 +1,53 @@
1
+ .verticalMenu {
2
+ width: var(--hscl-verticalMenu-width, 100%);
3
+ }
4
+
5
+ .menuList {
6
+ list-style: none;
7
+ display: flex;
8
+ flex-direction: column;
9
+ gap: var(--hscl-verticalMenu-gap, 0);
10
+ width: 100%;
11
+ margin: 0;
12
+ padding: 0;
13
+
14
+ // base styles for all list items
15
+ li {
16
+ position: relative;
17
+
18
+ // base styles for all links and spans
19
+ a, span {
20
+ display: block;
21
+ text-decoration: none;
22
+ padding-inline: var(--hscl-verticalMenu-paddingInline, 10px);
23
+ padding-block: var(--hscl-verticalMenu-paddingBlock, 8px);
24
+ color: var(--hscl-verticalMenu-color, currentColor);
25
+ background-color: var(--hscl-verticalMenu-backgroundColor, transparent);
26
+
27
+ &:hover {
28
+ color: var(--hscl-verticalMenu-color-hover, var(--hscl-verticalMenu-color, currentColor));
29
+ background-color: var(--hscl-verticalMenu-backgroundColor-hover, var(--hscl-verticalMenu-backgroundColor, transparent));
30
+ }
31
+ }
32
+ }
33
+
34
+ // base styles for all submenu lists
35
+ ul {
36
+ list-style: none;
37
+ margin: 0;
38
+ padding: 0;
39
+
40
+ li {
41
+ a, span {
42
+ color: var(--hscl-verticalMenu-submenu-color, var(--hscl-verticalMenu-color, currentColor));
43
+ background-color: var(--hscl-verticalMenu-submenu-backgroundColor, var(--hscl-verticalMenu-backgroundColor, transparent));
44
+ padding-inline-start: calc(var(--hscl-verticalMenu-paddingInline, 10px) + (var(--hscl-menuItem-level, 1) - 1) * var(--hscl-verticalMenu-paddingInlineStart, 0px));
45
+
46
+ &:hover {
47
+ color: var(--hscl-verticalMenu-submenu-color-hover, var(--hscl-verticalMenu-submenu-color, var(--hscl-verticalMenu-color-hover, var(--hscl-verticalMenu-color, currentColor))));
48
+ background-color: var(--hscl-verticalMenu-submenu-backgroundColor-hover, var(--hscl-verticalMenu-submenu-backgroundColor, var(--hscl-verticalMenu-backgroundColor-hover, var(--hscl-verticalMenu-backgroundColor, transparent))));
49
+ }
50
+ }
51
+ }
52
+ }
53
+ }