@hubspot/cms-component-library 0.1.0 → 0.3.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.
- package/components/componentLibrary/Accordion/AccordionContent/ContentFields.tsx +5 -3
- package/components/componentLibrary/Accordion/AccordionItem/StyleFields.tsx +5 -3
- package/components/componentLibrary/Accordion/AccordionItem/index.module.scss +2 -2
- package/components/componentLibrary/Accordion/AccordionItem/index.tsx +3 -3
- package/components/componentLibrary/Accordion/AccordionTitle/ContentFields.tsx +5 -3
- package/components/componentLibrary/Accordion/AccordionTitle/index.module.scss +2 -2
- package/components/componentLibrary/Accordion/stories/Accordion.stories.tsx +80 -1
- package/components/componentLibrary/Accordion/stories/AccordionDecorator.tsx +14 -14
- package/components/componentLibrary/Button/ContentFields.tsx +5 -3
- package/components/componentLibrary/Button/StyleFields.tsx +26 -4
- package/components/componentLibrary/Button/index.module.scss +22 -14
- package/components/componentLibrary/Button/index.tsx +6 -6
- package/components/componentLibrary/Button/llm.txt +5 -1
- package/components/componentLibrary/Button/stories/Button.AsButton.stories.tsx +30 -1
- package/components/componentLibrary/Button/stories/Button.AsLink.stories.tsx +38 -1
- package/components/componentLibrary/Button/stories/ButtonDecorator.tsx +1 -1
- package/components/componentLibrary/Button/types.ts +6 -1
- package/components/componentLibrary/Card/StyleFields.tsx +5 -3
- package/components/componentLibrary/Card/stories/Card.stories.tsx +46 -1
- package/components/componentLibrary/Card/stories/CardDecorator.tsx +1 -1
- package/components/componentLibrary/Divider/ContentFields.tsx +5 -3
- package/components/componentLibrary/Divider/StyleFields.tsx +5 -3
- package/components/componentLibrary/Divider/index.module.scss +6 -6
- package/components/componentLibrary/Divider/index.tsx +7 -3
- package/components/componentLibrary/Divider/stories/Divider.stories.tsx +44 -50
- package/components/componentLibrary/Divider/stories/{DividerDecorator.module.css → DividerDecorator.module.scss} +5 -4
- package/components/componentLibrary/Divider/stories/DividerDecorator.tsx +1 -1
- package/components/componentLibrary/Divider/types.ts +3 -1
- package/components/componentLibrary/Drawer/hooks/index.tsx +13 -0
- package/components/componentLibrary/Drawer/index.module.scss +94 -0
- package/components/componentLibrary/Drawer/index.tsx +131 -0
- package/components/componentLibrary/Drawer/llm.txt +416 -0
- package/components/componentLibrary/Drawer/stories/Drawer.stories.tsx +512 -0
- package/components/componentLibrary/Drawer/stories/DrawerDecorator.module.scss +8 -0
- package/components/componentLibrary/Drawer/stories/DrawerDecorator.tsx +18 -0
- package/components/componentLibrary/Drawer/types.ts +25 -0
- package/components/componentLibrary/Flex/stories/FlexDecorator.tsx +1 -1
- package/components/componentLibrary/Flex/types.ts +3 -1
- package/components/componentLibrary/Grid/stories/Grid.stories.tsx +454 -152
- package/components/componentLibrary/Grid/stories/GridDecorator.tsx +2 -2
- package/components/componentLibrary/Heading/ContentFields.tsx +5 -3
- package/components/componentLibrary/Heading/StyleFields.tsx +11 -9
- package/components/componentLibrary/Heading/index.tsx +3 -3
- package/components/componentLibrary/Heading/llm.txt +8 -8
- package/components/componentLibrary/Heading/stories/Heading.stories.tsx +3 -3
- package/components/componentLibrary/Heading/stories/HeadingDecorator.tsx +1 -1
- package/components/componentLibrary/Heading/types.ts +4 -4
- package/components/componentLibrary/Icon/ContentFields.tsx +5 -3
- package/components/componentLibrary/Icon/stories/Icon.stories.tsx +1 -1
- package/components/componentLibrary/Icon/stories/IconDecorator.tsx +1 -1
- package/components/componentLibrary/Image/ContentFields.tsx +5 -3
- package/components/componentLibrary/Image/index.tsx +4 -4
- package/components/componentLibrary/Image/llm.txt +17 -17
- package/components/componentLibrary/Image/stories/Image.stories.tsx +61 -18
- package/components/componentLibrary/Image/stories/ImageDecorator.tsx +1 -1
- package/components/componentLibrary/Image/types.ts +2 -2
- package/components/componentLibrary/LanguageSwitcher/ContentFields.tsx +18 -0
- package/components/componentLibrary/LanguageSwitcher/LanguageOptions.module.scss +37 -0
- package/components/componentLibrary/LanguageSwitcher/LanguageOptions.tsx +65 -0
- package/components/componentLibrary/LanguageSwitcher/StyleFields.tsx +48 -0
- package/components/componentLibrary/LanguageSwitcher/_dummyData.tsx +247 -0
- package/components/componentLibrary/LanguageSwitcher/assets/Globe.tsx +16 -0
- package/components/componentLibrary/LanguageSwitcher/index.module.scss +58 -0
- package/components/componentLibrary/LanguageSwitcher/index.tsx +125 -0
- package/components/componentLibrary/LanguageSwitcher/llm.txt +380 -0
- package/components/componentLibrary/LanguageSwitcher/stories/LanguageSwitcher.stories.tsx +349 -0
- package/components/componentLibrary/LanguageSwitcher/stories/LanguageSwitcherDecorator.module.scss +5 -0
- package/components/componentLibrary/LanguageSwitcher/stories/LanguageSwitcherDecorator.tsx +8 -0
- package/components/componentLibrary/LanguageSwitcher/types.ts +48 -0
- package/components/componentLibrary/LanguageSwitcher/utils.tsx +38 -0
- package/components/componentLibrary/Link/ContentFields.tsx +5 -3
- package/components/componentLibrary/Link/StyleFields.tsx +5 -3
- package/components/componentLibrary/Link/index.module.scss +10 -0
- package/components/componentLibrary/Link/index.tsx +24 -14
- package/components/componentLibrary/Link/stories/Link.stories.tsx +35 -5
- package/components/componentLibrary/Link/stories/LinkDecorator.tsx +11 -1
- package/components/componentLibrary/Link/types.ts +22 -13
- package/components/componentLibrary/List/ContentFields.tsx +5 -3
- package/components/componentLibrary/List/ListItem/ContentFields.tsx +6 -17
- package/components/componentLibrary/List/ListItem/index.module.scss +1 -13
- package/components/componentLibrary/List/ListItem/index.tsx +3 -30
- package/components/componentLibrary/List/ListItem/types.ts +1 -16
- package/components/componentLibrary/List/StyleFields.tsx +15 -18
- package/components/componentLibrary/List/index.module.scss +3 -0
- package/components/componentLibrary/List/index.tsx +5 -2
- package/components/componentLibrary/List/llm.txt +73 -103
- package/components/componentLibrary/List/stories/List.stories.tsx +56 -80
- package/components/componentLibrary/List/stories/ListDecorator.tsx +3 -6
- package/components/componentLibrary/List/types.ts +1 -3
- package/components/componentLibrary/Logo/_dummyLogoData.ts +12 -0
- package/components/componentLibrary/Logo/assets/hubspot-logo.png +0 -0
- package/components/componentLibrary/Logo/index.module.scss +22 -0
- package/components/componentLibrary/Logo/index.tsx +73 -0
- package/components/componentLibrary/Logo/llm.txt +262 -0
- package/components/componentLibrary/Logo/stories/Logo.stories.tsx +88 -0
- package/components/componentLibrary/Logo/stories/LogoDecorator.module.scss +10 -0
- package/components/componentLibrary/Logo/stories/LogoDecorator.tsx +8 -0
- package/components/componentLibrary/Logo/types.tsx +16 -0
- package/components/componentLibrary/Menu/ContentFields.tsx +16 -0
- package/components/componentLibrary/Menu/MenuItem/Chevron/index.module.scss +6 -0
- package/components/componentLibrary/Menu/MenuItem/Chevron/index.tsx +17 -0
- package/components/componentLibrary/Menu/MenuItem/index.module.scss +7 -0
- package/components/componentLibrary/Menu/MenuItem/index.tsx +266 -0
- package/components/componentLibrary/Menu/MenuItem/types.ts +17 -0
- package/components/componentLibrary/Menu/NavigationMenu/ContentFields.tsx +20 -0
- package/components/componentLibrary/Menu/NavigationMenu/index.tsx +18 -0
- package/components/componentLibrary/Menu/NavigationMenu/islands/NavigationMenuIsland.tsx +95 -0
- package/components/componentLibrary/Menu/NavigationMenu/islands/index.module.scss +100 -0
- package/components/componentLibrary/Menu/NavigationMenu/islands/types.ts +19 -0
- package/components/componentLibrary/Menu/NavigationMenu/llm.txt +197 -0
- package/components/componentLibrary/Menu/NavigationMenu/stories/NavigationMenu.stories.tsx +286 -0
- package/components/componentLibrary/Menu/NavigationMenu/stories/NavigationMenuDecorator.module.scss +15 -0
- package/components/componentLibrary/Menu/NavigationMenu/stories/NavigationMenuDecorator.tsx +12 -0
- package/components/componentLibrary/Menu/NavigationMenu/types.ts +3 -0
- package/components/componentLibrary/Menu/VerticalMenu/ContentFields.tsx +20 -0
- package/components/componentLibrary/Menu/VerticalMenu/index.tsx +18 -0
- package/components/componentLibrary/Menu/VerticalMenu/islands/index.module.scss +53 -0
- package/components/componentLibrary/Menu/VerticalMenu/islands/verticalMenuIsland.tsx +78 -0
- package/components/componentLibrary/Menu/VerticalMenu/llm.txt +177 -0
- package/components/componentLibrary/Menu/VerticalMenu/stories/VerticalMenu.stories.tsx +242 -0
- package/components/componentLibrary/Menu/VerticalMenu/stories/VerticalMenuDecorator.module.scss +19 -0
- package/components/componentLibrary/Menu/VerticalMenu/stories/VerticalMenuDecorator.tsx +12 -0
- package/components/componentLibrary/Menu/VerticalMenu/types.ts +21 -0
- package/components/componentLibrary/Menu/_dummyMenuData.js +1346 -0
- package/components/componentLibrary/Menu/types.ts +56 -0
- package/components/componentLibrary/Menu/utils/transformMenuData.ts +11 -0
- package/components/componentLibrary/_patterns/README.md +15 -17
- package/components/componentLibrary/_patterns/checklist-and-examples.md +17 -17
- package/components/componentLibrary/_patterns/component-structure.md +21 -23
- package/components/componentLibrary/_patterns/css-patterns.md +170 -18
- package/components/componentLibrary/_patterns/field-patterns.md +97 -27
- package/components/componentLibrary/_patterns/function-declaration-patterns.md +281 -0
- package/components/componentLibrary/_patterns/llm-txt.template.md +4 -2
- package/components/componentLibrary/_patterns/prop-naming-patterns.md +208 -0
- package/components/componentLibrary/_patterns/storybook-patterns.md +25 -8
- package/components/componentLibrary/_patterns/typescript-patterns.md +6 -3
- package/package.json +6 -3
- /package/components/componentLibrary/Button/stories/{ButtonDecorator.module.css → ButtonDecorator.module.scss} +0 -0
- /package/components/componentLibrary/Card/stories/{CardDecorator.module.css → CardDecorator.module.scss} +0 -0
- /package/components/componentLibrary/Flex/stories/{FlexDecorator.module.css → FlexDecorator.module.scss} +0 -0
- /package/components/componentLibrary/Grid/stories/{GridDecorator.module.css → GridDecorator.module.scss} +0 -0
- /package/components/componentLibrary/Heading/stories/{HeadingDecorator.module.css → HeadingDecorator.module.scss} +0 -0
- /package/components/componentLibrary/Icon/stories/{IconDecorator.module.css → IconDecorator.module.scss} +0 -0
- /package/components/componentLibrary/Image/stories/{ImageDecorator.module.css → ImageDecorator.module.scss} +0 -0
- /package/components/componentLibrary/Image/stories/assets/{catSmile.jpg → cat-smile.jpg} +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import MenuItem from '../../MenuItem/index.js';
|
|
3
|
+
import { VerticalMenuProps } from '../types.js';
|
|
4
|
+
import { addIdsToMenuItems } from '../../utils/transformMenuData.js';
|
|
5
|
+
import styles from './index.module.scss';
|
|
6
|
+
import cx from '../../../utils/classname.js';
|
|
7
|
+
const VerticalMenu = ({
|
|
8
|
+
maxDepth,
|
|
9
|
+
className = '',
|
|
10
|
+
style = {},
|
|
11
|
+
linkColor,
|
|
12
|
+
linkColorHover,
|
|
13
|
+
backgroundColor,
|
|
14
|
+
backgroundColorHover,
|
|
15
|
+
submenuLinkColor,
|
|
16
|
+
submenuLinkColorHover,
|
|
17
|
+
submenuBackgroundColor,
|
|
18
|
+
submenuBackgroundColorHover,
|
|
19
|
+
indentation,
|
|
20
|
+
menuData,
|
|
21
|
+
}: VerticalMenuProps) => {
|
|
22
|
+
if (!menuData?.pagesTree?.children) return null;
|
|
23
|
+
|
|
24
|
+
const menuItems = useMemo(
|
|
25
|
+
() => addIdsToMenuItems(menuData.pagesTree.children),
|
|
26
|
+
[menuData]
|
|
27
|
+
);
|
|
28
|
+
const level = 1;
|
|
29
|
+
|
|
30
|
+
const cssVariables = {
|
|
31
|
+
...(linkColor && { '--hscl-verticalMenu-color': linkColor }),
|
|
32
|
+
...(linkColorHover && {
|
|
33
|
+
'--hscl-verticalMenu-color-hover': linkColorHover,
|
|
34
|
+
}),
|
|
35
|
+
...(backgroundColor && {
|
|
36
|
+
'--hscl-verticalMenu-backgroundColor': backgroundColor,
|
|
37
|
+
}),
|
|
38
|
+
...(backgroundColorHover && {
|
|
39
|
+
'--hscl-verticalMenu-backgroundColor-hover': backgroundColorHover,
|
|
40
|
+
}),
|
|
41
|
+
...(submenuLinkColor && {
|
|
42
|
+
'--hscl-verticalMenu-submenu-color': submenuLinkColor,
|
|
43
|
+
}),
|
|
44
|
+
...(submenuLinkColorHover && {
|
|
45
|
+
'--hscl-verticalMenu-submenu-color-hover': submenuLinkColorHover,
|
|
46
|
+
}),
|
|
47
|
+
...(submenuBackgroundColor && {
|
|
48
|
+
'--hscl-verticalMenu-submenu-backgroundColor': submenuBackgroundColor,
|
|
49
|
+
}),
|
|
50
|
+
...(submenuBackgroundColorHover && {
|
|
51
|
+
'--hscl-verticalMenu-submenu-backgroundColor-hover':
|
|
52
|
+
submenuBackgroundColorHover,
|
|
53
|
+
}),
|
|
54
|
+
...(indentation && {
|
|
55
|
+
'--hscl-verticalMenu-paddingInlineStart': indentation,
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<nav
|
|
61
|
+
className={cx(styles.verticalMenu, className)}
|
|
62
|
+
style={{ ...cssVariables, ...style }}
|
|
63
|
+
>
|
|
64
|
+
<ul className={styles.menuList}>
|
|
65
|
+
{menuItems.map(item => (
|
|
66
|
+
<MenuItem
|
|
67
|
+
key={item._id}
|
|
68
|
+
item={item}
|
|
69
|
+
level={level}
|
|
70
|
+
maxDepth={maxDepth}
|
|
71
|
+
/>
|
|
72
|
+
))}
|
|
73
|
+
</ul>
|
|
74
|
+
</nav>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export default VerticalMenu;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# VerticalMenu Component
|
|
2
|
+
|
|
3
|
+
A vertical navigation menu component that renders a hierarchical list of links with support for nested submenus, customizable depth, and flexible styling options.
|
|
4
|
+
|
|
5
|
+
## Import path
|
|
6
|
+
```tsx
|
|
7
|
+
import VerticalMenu from '@hubspot/cms-component-library/VerticalMenu';
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Purpose
|
|
11
|
+
|
|
12
|
+
The VerticalMenu component provides a consistent way to render vertical navigation menus in HubSpot CMS projects. It automatically integrates with HubSpot's site menu system, supporting multi-level navigation with configurable maximum depth, customizable colors for links and backgrounds, and adjustable indentation for submenu items.
|
|
13
|
+
|
|
14
|
+
## Component Structure
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
VerticalMenu/
|
|
18
|
+
├── index.tsx # Main component wrapper (Island integration)
|
|
19
|
+
├── types.ts # TypeScript type definitions
|
|
20
|
+
├── ContentFields.tsx # HubSpot field definitions for menu selection
|
|
21
|
+
├── islands/
|
|
22
|
+
│ ├── verticalMenuIsland.tsx # Island component with rendering logic
|
|
23
|
+
│ └── index.module.scss # CSS module with design tokens
|
|
24
|
+
└── stories/
|
|
25
|
+
├── VerticalMenu.stories.tsx # Storybook examples
|
|
26
|
+
└── VerticalMenuDecorator.tsx # Storybook decorator
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Components
|
|
30
|
+
|
|
31
|
+
### VerticalMenu (Main Component)
|
|
32
|
+
|
|
33
|
+
**Purpose:** Renders a vertical navigation menu from HubSpot site menu data with support for nested submenus.
|
|
34
|
+
|
|
35
|
+
**Props:**
|
|
36
|
+
```tsx
|
|
37
|
+
{
|
|
38
|
+
menuData: SiteMenu; // Menu data from useMenu hook (required)
|
|
39
|
+
maxDepth?: number; // Maximum nesting depth (default: unlimited)
|
|
40
|
+
indentation?: string; // Submenu indentation (default: '0px')
|
|
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 VerticalMenu 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 VerticalMenu from '@hubspot/cms-component-library/VerticalMenu';
|
|
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
|
+
<VerticalMenu
|
|
72
|
+
menuData={menuData}
|
|
73
|
+
maxDepth={2}
|
|
74
|
+
indentation="20px"
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export { fields } from './fields.js';
|
|
80
|
+
|
|
81
|
+
export const meta: ModuleMeta = {
|
|
82
|
+
label: 'Vertical 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 VerticalMenu from '@hubspot/cms-component-library/VerticalMenu';
|
|
94
|
+
|
|
95
|
+
export const fields = (
|
|
96
|
+
<ModuleFields>
|
|
97
|
+
<VerticalMenu.ContentFields />
|
|
98
|
+
</ModuleFields>
|
|
99
|
+
);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**What this does:**
|
|
103
|
+
1. `VerticalMenu.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 VerticalMenu component via the `menuData` prop
|
|
107
|
+
|
|
108
|
+
## HubSpot CMS Integration
|
|
109
|
+
|
|
110
|
+
### Field Definitions
|
|
111
|
+
|
|
112
|
+
The VerticalMenu component provides field definitions for menu selection in HubSpot CMS modules.
|
|
113
|
+
|
|
114
|
+
#### ContentFields
|
|
115
|
+
|
|
116
|
+
Configurable props for customizing field labels, names, and defaults:
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
<VerticalMenu.ContentFields
|
|
120
|
+
menuLabel="Menu"
|
|
121
|
+
menuName="menu"
|
|
122
|
+
menuDefault="default"
|
|
123
|
+
/>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Fields:**
|
|
127
|
+
- `menu`: MenuField for selecting which site menu to display (default: 'default')
|
|
128
|
+
|
|
129
|
+
## Styling
|
|
130
|
+
|
|
131
|
+
### CSS Variables
|
|
132
|
+
|
|
133
|
+
The VerticalMenu component uses CSS variables for theming and customization:
|
|
134
|
+
|
|
135
|
+
**Base Menu Styles:**
|
|
136
|
+
- `--hscl-verticalMenu-width`: Menu container width (default: 100%)
|
|
137
|
+
- `--hscl-verticalMenu-gap`: Space between menu items (default: 0)
|
|
138
|
+
- `--hscl-verticalMenu-paddingInline`: Horizontal padding for links (default: 10px)
|
|
139
|
+
- `--hscl-verticalMenu-paddingBlock`: Vertical padding for links (default: 8px)
|
|
140
|
+
- `--hscl-verticalMenu-color`: Link text color (default: currentColor)
|
|
141
|
+
- `--hscl-verticalMenu-backgroundColor`: Link background color (default: transparent)
|
|
142
|
+
- `--hscl-verticalMenu-color-hover`: Link hover text color
|
|
143
|
+
- `--hscl-verticalMenu-backgroundColor-hover`: Link hover background color
|
|
144
|
+
|
|
145
|
+
**Submenu-Specific Styles:**
|
|
146
|
+
- `--hscl-verticalMenu-paddingInlineStart`: Indentation for submenu items (default: 0px)
|
|
147
|
+
- `--hscl-verticalMenu-submenu-color`: Submenu link text color
|
|
148
|
+
- `--hscl-verticalMenu-submenu-backgroundColor`: Submenu link background color
|
|
149
|
+
- `--hscl-verticalMenu-submenu-color-hover`: Submenu link hover text color
|
|
150
|
+
- `--hscl-verticalMenu-submenu-backgroundColor-hover`: Submenu link hover background color
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
## Accessibility
|
|
154
|
+
|
|
155
|
+
The VerticalMenu component follows accessibility best practices:
|
|
156
|
+
|
|
157
|
+
- **Semantic HTML**: Renders as a `<nav>` element with nested unordered lists (`<ul>`)
|
|
158
|
+
- **ARIA Roles**: List items have `role="menuitem"` for proper screen reader support
|
|
159
|
+
- **Keyboard Navigation**: Full keyboard support provided by the MenuItem component
|
|
160
|
+
- **Link Semantics**: Uses proper `<a>` elements for links with `href` attributes
|
|
161
|
+
- **External Links**: Automatically adds `target="_blank"` and `rel="noopener noreferrer"` for external links
|
|
162
|
+
- **Focus Management**: Native focus styles preserved for keyboard users
|
|
163
|
+
|
|
164
|
+
## Best Practices
|
|
165
|
+
|
|
166
|
+
- **Always use useMenu hook**: The VerticalMenu component requires menu data from the `useMenu` hook—never pass hardcoded menu structures
|
|
167
|
+
- **Control depth wisely**: Use `maxDepth` to limit nesting levels for better UX (1-3 levels recommended)
|
|
168
|
+
- **Consistent indentation**: Use consistent indentation values across your site (typically 16px, 20px, or 24px)
|
|
169
|
+
- **Color contrast**: Ensure sufficient contrast between link colors and backgrounds (WCAG AA minimum)
|
|
170
|
+
- **Hover states**: Always define hover colors for better visual feedback
|
|
171
|
+
- **Submenu styling**: Use distinct colors for submenu items to create visual hierarchy
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
## Related Components
|
|
175
|
+
|
|
176
|
+
- **NavigationMenu**: Horizontal navigation menu component for top-level navigation bars
|
|
177
|
+
- **MenuItem**: Used internally to render individual menu items with support for nested children
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import VerticalMenu from '../index.js';
|
|
3
|
+
import { VerticalMenuProps } from '../types.js';
|
|
4
|
+
import { withVerticalMenuStyles } from './VerticalMenuDecorator.js';
|
|
5
|
+
import { SBContainer, SBFocusWrapper } from '@sb-utils';
|
|
6
|
+
import { dummyMenuData } from '../../_dummyMenuData.js';
|
|
7
|
+
|
|
8
|
+
const meta: Meta<VerticalMenuProps> = {
|
|
9
|
+
title: 'Component Library/Menu/Vertical Menu',
|
|
10
|
+
component: VerticalMenu,
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: 'centered',
|
|
13
|
+
docs: {
|
|
14
|
+
description: {
|
|
15
|
+
component:
|
|
16
|
+
'The VerticalMenu component creates a vertical navigation menu with collapsible nested submenus. It supports depth control and custom indentation, making it ideal for sidebars and secondary navigation.',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
decorators: [withVerticalMenuStyles],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
type Story = StoryObj<typeof meta>;
|
|
25
|
+
|
|
26
|
+
export const Default: Story = {
|
|
27
|
+
render: () => (
|
|
28
|
+
<SBContainer addBackground width="300px">
|
|
29
|
+
<VerticalMenu menuData={dummyMenuData} />
|
|
30
|
+
</SBContainer>
|
|
31
|
+
),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const MaxDepthControl: Story = {
|
|
35
|
+
name: 'Max Depth Control',
|
|
36
|
+
render: () => (
|
|
37
|
+
<SBContainer flex direction="column" gap="extralarge">
|
|
38
|
+
<SBContainer addBackground width="300px">
|
|
39
|
+
<h4>No Depth Limit (Default)</h4>
|
|
40
|
+
<p>Shows all nested menu levels</p>
|
|
41
|
+
<VerticalMenu menuData={dummyMenuData} />
|
|
42
|
+
</SBContainer>
|
|
43
|
+
|
|
44
|
+
<SBContainer addBackground width="300px">
|
|
45
|
+
<h4>Max Depth: 1</h4>
|
|
46
|
+
<p>Only shows top-level items</p>
|
|
47
|
+
<VerticalMenu maxDepth={1} menuData={dummyMenuData} />
|
|
48
|
+
</SBContainer>
|
|
49
|
+
|
|
50
|
+
<SBContainer addBackground width="300px">
|
|
51
|
+
<h4>Max Depth: 2</h4>
|
|
52
|
+
<p>Shows up to 2 levels deep</p>
|
|
53
|
+
<VerticalMenu maxDepth={2} menuData={dummyMenuData} />
|
|
54
|
+
</SBContainer>
|
|
55
|
+
|
|
56
|
+
<SBContainer addBackground width="300px">
|
|
57
|
+
<h4>Max Depth: 3</h4>
|
|
58
|
+
<p>Shows up to 3 levels deep</p>
|
|
59
|
+
<VerticalMenu maxDepth={3} menuData={dummyMenuData} />
|
|
60
|
+
</SBContainer>
|
|
61
|
+
</SBContainer>
|
|
62
|
+
),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const IndentationOptions: Story = {
|
|
66
|
+
name: 'Indentation Options',
|
|
67
|
+
render: () => (
|
|
68
|
+
<SBContainer flex direction="column" gap="extralarge">
|
|
69
|
+
<SBContainer addBackground width="300px">
|
|
70
|
+
<h4>Default Indentation (20px)</h4>
|
|
71
|
+
<VerticalMenu menuData={dummyMenuData} />
|
|
72
|
+
</SBContainer>
|
|
73
|
+
|
|
74
|
+
<SBContainer addBackground width="300px">
|
|
75
|
+
<h4>No Indentation</h4>
|
|
76
|
+
<VerticalMenu indentation="0px" menuData={dummyMenuData} />
|
|
77
|
+
</SBContainer>
|
|
78
|
+
|
|
79
|
+
<SBContainer addBackground width="300px">
|
|
80
|
+
<h4>Small Indentation (10px)</h4>
|
|
81
|
+
<VerticalMenu indentation="10px" menuData={dummyMenuData} />
|
|
82
|
+
</SBContainer>
|
|
83
|
+
|
|
84
|
+
<SBContainer addBackground width="300px">
|
|
85
|
+
<h4>Large Indentation (40px)</h4>
|
|
86
|
+
<VerticalMenu indentation="40px" menuData={dummyMenuData} />
|
|
87
|
+
</SBContainer>
|
|
88
|
+
</SBContainer>
|
|
89
|
+
),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const ColorCustomization: Story = {
|
|
93
|
+
name: 'Color Customization',
|
|
94
|
+
render: () => (
|
|
95
|
+
<SBContainer flex direction="column" gap="extralarge">
|
|
96
|
+
<SBContainer addBackground width="300px">
|
|
97
|
+
<h4>Default Colors</h4>
|
|
98
|
+
<VerticalMenu menuData={dummyMenuData} />
|
|
99
|
+
</SBContainer>
|
|
100
|
+
|
|
101
|
+
<SBContainer addBackground width="300px">
|
|
102
|
+
<h4>Bold Link Colors</h4>
|
|
103
|
+
<VerticalMenu
|
|
104
|
+
linkColor="#1a1a1a"
|
|
105
|
+
linkColorHover="#ff5722"
|
|
106
|
+
backgroundColor="#fff3e0"
|
|
107
|
+
backgroundColorHover="#ffe0b2"
|
|
108
|
+
menuData={dummyMenuData}
|
|
109
|
+
/>
|
|
110
|
+
</SBContainer>
|
|
111
|
+
|
|
112
|
+
<SBContainer addBackground width="300px">
|
|
113
|
+
<h4>Vibrant Theme</h4>
|
|
114
|
+
<VerticalMenu
|
|
115
|
+
linkColor="#ffffff"
|
|
116
|
+
linkColorHover="#ffeb3b"
|
|
117
|
+
backgroundColor="#1976d2"
|
|
118
|
+
backgroundColorHover="#1565c0"
|
|
119
|
+
submenuLinkColor="#e3f2fd"
|
|
120
|
+
submenuLinkColorHover="#ffeb3b"
|
|
121
|
+
submenuBackgroundColor="#0d47a1"
|
|
122
|
+
submenuBackgroundColorHover="#01579b"
|
|
123
|
+
indentation="30px"
|
|
124
|
+
menuData={dummyMenuData}
|
|
125
|
+
/>
|
|
126
|
+
</SBContainer>
|
|
127
|
+
|
|
128
|
+
<SBContainer addBackground width="300px">
|
|
129
|
+
<h4>High Contrast</h4>
|
|
130
|
+
<VerticalMenu
|
|
131
|
+
linkColor="#000000"
|
|
132
|
+
linkColorHover="#d32f2f"
|
|
133
|
+
backgroundColor="#ffeb3b"
|
|
134
|
+
backgroundColorHover="#fdd835"
|
|
135
|
+
submenuLinkColor="#1a1a1a"
|
|
136
|
+
submenuLinkColorHover="#c62828"
|
|
137
|
+
submenuBackgroundColor="#fff59d"
|
|
138
|
+
submenuBackgroundColorHover="#fff176"
|
|
139
|
+
indentation="30px"
|
|
140
|
+
menuData={dummyMenuData}
|
|
141
|
+
/>
|
|
142
|
+
</SBContainer>
|
|
143
|
+
|
|
144
|
+
<SBContainer
|
|
145
|
+
flex
|
|
146
|
+
justifyContent="center"
|
|
147
|
+
style={{
|
|
148
|
+
backgroundColor: '#0a0a0a',
|
|
149
|
+
padding: '20px',
|
|
150
|
+
minHeight: '400px',
|
|
151
|
+
}}
|
|
152
|
+
>
|
|
153
|
+
<SBContainer width="300px">
|
|
154
|
+
<h4 style={{ color: '#ffffff', marginTop: 0 }}>Dark Theme</h4>
|
|
155
|
+
<VerticalMenu
|
|
156
|
+
linkColor="#e0e0e0"
|
|
157
|
+
linkColorHover="#00e676"
|
|
158
|
+
backgroundColor="#212121"
|
|
159
|
+
backgroundColorHover="#424242"
|
|
160
|
+
submenuLinkColor="#9e9e9e"
|
|
161
|
+
submenuLinkColorHover="#69f0ae"
|
|
162
|
+
submenuBackgroundColor="#1a1a1a"
|
|
163
|
+
submenuBackgroundColorHover="#2c2c2c"
|
|
164
|
+
indentation="30px"
|
|
165
|
+
menuData={dummyMenuData}
|
|
166
|
+
/>
|
|
167
|
+
</SBContainer>
|
|
168
|
+
</SBContainer>
|
|
169
|
+
</SBContainer>
|
|
170
|
+
),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export const FocusState: Story = {
|
|
174
|
+
name: 'Focus State',
|
|
175
|
+
render: () => (
|
|
176
|
+
<SBContainer flex direction="column" gap="large">
|
|
177
|
+
<SBContainer addBackground width="300px">
|
|
178
|
+
<h4>Focus State</h4>
|
|
179
|
+
<p>
|
|
180
|
+
In production, this focus outline appears when navigating with the
|
|
181
|
+
keyboard (Tab key). This Storybook demo auto-applies the focus state
|
|
182
|
+
to show how it looks.
|
|
183
|
+
</p>
|
|
184
|
+
<SBFocusWrapper>
|
|
185
|
+
<VerticalMenu menuData={dummyMenuData} />
|
|
186
|
+
</SBFocusWrapper>
|
|
187
|
+
</SBContainer>
|
|
188
|
+
</SBContainer>
|
|
189
|
+
),
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export const SidebarExample: Story = {
|
|
193
|
+
name: 'In Context',
|
|
194
|
+
render: () => (
|
|
195
|
+
<SBContainer
|
|
196
|
+
flex
|
|
197
|
+
direction="row"
|
|
198
|
+
style={{
|
|
199
|
+
minHeight: '600px',
|
|
200
|
+
border: '1px solid #cbd6e2',
|
|
201
|
+
}}
|
|
202
|
+
>
|
|
203
|
+
<SBContainer
|
|
204
|
+
width="280px"
|
|
205
|
+
style={{
|
|
206
|
+
backgroundColor: '#f5f8fa',
|
|
207
|
+
borderRight: '1px solid #cbd6e2',
|
|
208
|
+
padding: '20px 0',
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
<VerticalMenu
|
|
212
|
+
linkColor="#33475b"
|
|
213
|
+
linkColorHover="#0066cc"
|
|
214
|
+
backgroundColor="transparent"
|
|
215
|
+
backgroundColorHover="#ffffff"
|
|
216
|
+
submenuLinkColor="#516f90"
|
|
217
|
+
submenuLinkColorHover="#0066cc"
|
|
218
|
+
submenuBackgroundColor="transparent"
|
|
219
|
+
submenuBackgroundColorHover="#eaf0f6"
|
|
220
|
+
indentation="24px"
|
|
221
|
+
menuData={dummyMenuData}
|
|
222
|
+
/>
|
|
223
|
+
</SBContainer>
|
|
224
|
+
<SBContainer
|
|
225
|
+
style={{
|
|
226
|
+
flex: 1,
|
|
227
|
+
padding: '40px',
|
|
228
|
+
backgroundColor: '#ffffff',
|
|
229
|
+
}}
|
|
230
|
+
>
|
|
231
|
+
<h2>Main Content Area</h2>
|
|
232
|
+
<p>
|
|
233
|
+
This demonstrates how the vertical menu works in a sidebar layout.
|
|
234
|
+
</p>
|
|
235
|
+
<p>
|
|
236
|
+
The sidebar navigation provides easy access to different sections
|
|
237
|
+
while the main content area remains spacious.
|
|
238
|
+
</p>
|
|
239
|
+
</SBContainer>
|
|
240
|
+
</SBContainer>
|
|
241
|
+
),
|
|
242
|
+
};
|
package/components/componentLibrary/Menu/VerticalMenu/stories/VerticalMenuDecorator.module.scss
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
.verticalMenuContainer {
|
|
2
|
+
--hscl-verticalMenu-width: 100%;
|
|
3
|
+
--hscl-verticalMenu-gap: 0px;
|
|
4
|
+
--hscl-verticalMenu-paddingInline: 10px;
|
|
5
|
+
--hscl-verticalMenu-paddingBlock: 8px;
|
|
6
|
+
--hscl-verticalMenu-color: #33475b;
|
|
7
|
+
--hscl-verticalMenu-color-hover: #0066cc;
|
|
8
|
+
--hscl-verticalMenu-backgroundColor: transparent;
|
|
9
|
+
--hscl-verticalMenu-backgroundColor-hover: #f5f8fa;
|
|
10
|
+
--hscl-verticalMenu-submenu-color: #516f90;
|
|
11
|
+
--hscl-verticalMenu-submenu-color-hover: #0066cc;
|
|
12
|
+
--hscl-verticalMenu-submenu-backgroundColor: transparent;
|
|
13
|
+
--hscl-verticalMenu-submenu-backgroundColor-hover: #eaf0f6;
|
|
14
|
+
--hscl-verticalMenu-paddingInlineStart: 20px;
|
|
15
|
+
|
|
16
|
+
nav li {
|
|
17
|
+
margin-bottom: 0;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Decorator } from '@storybook/react';
|
|
2
|
+
import styles from './VerticalMenuDecorator.module.scss';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared decorator for VerticalMenu stories that applies default CSS variables
|
|
6
|
+
* and scoped styles to prevent pollution across other Storybook stories.
|
|
7
|
+
*/
|
|
8
|
+
export const withVerticalMenuStyles: Decorator = Story => (
|
|
9
|
+
<div className={styles.verticalMenuContainer}>
|
|
10
|
+
<Story />
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { CSSVariables } from '../../utils/types.js';
|
|
2
|
+
import { ContentFieldsProps } from '../types.js';
|
|
3
|
+
import { SiteMenu } from '@hubspot/cms-components';
|
|
4
|
+
|
|
5
|
+
export type VerticalMenuProps = {
|
|
6
|
+
maxDepth?: number;
|
|
7
|
+
className?: string;
|
|
8
|
+
style?: CSSVariables;
|
|
9
|
+
linkColor?: string;
|
|
10
|
+
linkColorHover?: string;
|
|
11
|
+
backgroundColor?: string;
|
|
12
|
+
backgroundColorHover?: string;
|
|
13
|
+
submenuLinkColor?: string;
|
|
14
|
+
submenuLinkColorHover?: string;
|
|
15
|
+
submenuBackgroundColor?: string;
|
|
16
|
+
submenuBackgroundColorHover?: string;
|
|
17
|
+
indentation?: string;
|
|
18
|
+
menuData: SiteMenu;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type VerticalMenuContentFieldProps = ContentFieldsProps;
|