@hubspot/cms-component-library 0.3.6 → 0.3.8

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.
@@ -1,4 +1,4 @@
1
- import { RichTextField } from '@hubspot/cms-components/fields';
1
+ import Text from '../../Text/index.js';
2
2
  import type { ContentFieldsProps } from './types.js';
3
3
 
4
4
  const ContentFields = ({
@@ -7,11 +7,11 @@ const ContentFields = ({
7
7
  contentDefault = '<p>Accordion content goes here. You can add multiple lines of text.</p>',
8
8
  }: ContentFieldsProps) => {
9
9
  return (
10
- // Todo: consider migrating over field library
11
- <RichTextField
12
- label={contentLabel}
13
- name={contentName}
14
- default={contentDefault}
10
+ <Text.ContentFields
11
+ textLabel={contentLabel}
12
+ textName={contentName}
13
+ textDefault={contentDefault}
14
+ textFeatureSet="bodyContent"
15
15
  />
16
16
  );
17
17
  };
@@ -1,9 +1,11 @@
1
+ import Text from '../../Text/index.js';
1
2
  import styles from './index.module.scss';
2
3
  import ContentFields from './ContentFields.js';
3
4
  import cx from '../../utils/classname.js';
4
5
  import { AccordionContentProps } from './types.js';
5
6
 
6
7
  const AccordionContentComponent = ({
8
+ fieldPath,
7
9
  className = '',
8
10
  style = {},
9
11
  children,
@@ -12,7 +14,7 @@ const AccordionContentComponent = ({
12
14
 
13
15
  return (
14
16
  <div className={combinedClasses} style={style}>
15
- {children}
17
+ {fieldPath ? <Text fieldPath={fieldPath} /> : children}
16
18
  </div>
17
19
  );
18
20
  };
@@ -1,6 +1,5 @@
1
- import { RichTextFieldDefaults } from '@hubspot/cms-components/fields';
2
-
3
1
  export type AccordionContentProps = {
2
+ fieldPath?: string;
4
3
  className?: string;
5
4
  style?: React.CSSProperties;
6
5
  children?: React.ReactNode;
@@ -9,5 +8,5 @@ export type AccordionContentProps = {
9
8
  export type ContentFieldsProps = {
10
9
  contentLabel?: string;
11
10
  contentName?: string;
12
- contentDefault?: typeof RichTextFieldDefaults;
11
+ contentDefault?: string;
13
12
  };
@@ -1,13 +1,18 @@
1
- import { TextField } from '@hubspot/cms-components/fields';
1
+ import Text from '../../Text/index.js';
2
2
  import type { ContentFieldsProps } from './types.js';
3
3
 
4
4
  const ContentFields = ({
5
5
  titleLabel = 'Title',
6
6
  titleName = 'title',
7
- titleDefault = 'Accordion Title',
7
+ titleDefault = '<p>Accordion Title</p>',
8
8
  }: ContentFieldsProps) => {
9
9
  return (
10
- <TextField label={titleLabel} name={titleName} default={titleDefault} />
10
+ <Text.ContentFields
11
+ textLabel={titleLabel}
12
+ textName={titleName}
13
+ textDefault={titleDefault}
14
+ textFeatureSet="heading"
15
+ />
11
16
  );
12
17
  };
13
18
 
@@ -1,3 +1,4 @@
1
+ import Text from '../../Text/index.js';
1
2
  import styles from './index.module.scss';
2
3
  import ContentFields from './ContentFields.js';
3
4
  import StyleFields from './StyleFields.js';
@@ -7,6 +8,7 @@ import { ChevronIcon, PlusIcon, MinusIcon, CaretIcon } from './icons.js';
7
8
 
8
9
  const AccordionTitleComponent = ({
9
10
  icon = 'chevron',
11
+ fieldPath,
10
12
  className = '',
11
13
  style = {},
12
14
  children,
@@ -32,7 +34,11 @@ const AccordionTitleComponent = ({
32
34
 
33
35
  return (
34
36
  <summary className={combinedClasses} style={style}>
35
- <span className={styles.accordionTitleText}>{children}</span>
37
+ {fieldPath ? (
38
+ <Text fieldPath={fieldPath} className={styles.accordionTitleText} />
39
+ ) : (
40
+ <span className={styles.accordionTitleText}>{children}</span>
41
+ )}
36
42
  {renderIcon()}
37
43
  </summary>
38
44
  );
@@ -2,6 +2,7 @@ export type AccordionIconType = 'chevron' | 'plus' | 'caret';
2
2
 
3
3
  export type AccordionTitleProps = {
4
4
  icon?: AccordionIconType;
5
+ fieldPath?: string;
5
6
  className?: string;
6
7
  style?: React.CSSProperties;
7
8
  children?: React.ReactNode;
@@ -100,28 +100,30 @@ Accordion/
100
100
 
101
101
  ### AccordionTitle (Trigger)
102
102
 
103
- **Purpose:** Clickable trigger component that renders a `<summary>` element with text and an expand/collapse icon.
103
+ **Purpose:** Clickable trigger component that renders a `<summary>` element with rich text and an expand/collapse icon. Uses the `Text` component internally for CMS inline editing support.
104
104
 
105
105
  **Props:**
106
106
  ```tsx
107
107
  {
108
108
  icon?: 'chevron' | 'plus' | 'caret'; // Icon type (default: 'chevron')
109
+ fieldPath?: string; // Path to the RichText field in HubSpot CMS (preferred for CMS modules)
109
110
  className?: string; // Additional CSS classes
110
111
  style?: React.CSSProperties; // Inline styles
111
- children?: React.ReactNode; // Title text content
112
+ children?: React.ReactNode; // Fallback title content (used when fieldPath is not provided, e.g. in Storybook)
112
113
  }
113
114
  ```
114
115
 
115
116
  ### AccordionContent (Panel)
116
117
 
117
- **Purpose:** Content panel component that renders a `<div>` containing the expandable content.
118
+ **Purpose:** Content panel component that renders a `<div>` containing the expandable content. Uses the `Text` component internally when `fieldPath` is provided for CMS inline editing support.
118
119
 
119
120
  **Props:**
120
121
  ```tsx
121
122
  {
123
+ fieldPath?: string; // Path to the RichText field in HubSpot CMS (preferred for CMS modules)
122
124
  className?: string; // Additional CSS classes
123
125
  style?: React.CSSProperties; // Inline styles
124
- children?: React.ReactNode; // Any content (text, HTML, components)
126
+ children?: React.ReactNode; // Fallback content (used when fieldPath is not provided, e.g. in Storybook)
125
127
  }
126
128
  ```
127
129
 
@@ -229,18 +231,18 @@ Configurable props for variant selection:
229
231
 
230
232
  #### AccordionTitle.ContentFields
231
233
 
232
- Configurable props for title text:
234
+ Configurable props for title text. Uses `Text.ContentFields` internally with the `heading` feature set:
233
235
 
234
236
  ```tsx
235
237
  <AccordionTitle.ContentFields
236
238
  titleName="title"
237
239
  titleLabel="Title"
238
- titleDefault="Accordion Title"
240
+ titleDefault="<p>Accordion Title</p>"
239
241
  />
240
242
  ```
241
243
 
242
244
  **Fields:**
243
- - `title`: TextField for accordion title text
245
+ - `title`: RichTextField for accordion title text (heading feature set)
244
246
 
245
247
  #### AccordionTitle.StyleFields
246
248
 
@@ -259,7 +261,7 @@ Configurable props for icon style:
259
261
 
260
262
  #### AccordionContent.ContentFields
261
263
 
262
- Configurable props for content:
264
+ Configurable props for content. Uses `Text.ContentFields` internally with the `bodyContent` feature set:
263
265
 
264
266
  ```tsx
265
267
  <AccordionContent.ContentFields
@@ -270,7 +272,7 @@ Configurable props for content:
270
272
  ```
271
273
 
272
274
  **Fields:**
273
- - `content`: RichTextField for accordion content
275
+ - `content`: RichTextField for accordion content (bodyContent feature set)
274
276
 
275
277
  ### Module Usage Example
276
278
 
@@ -304,10 +306,15 @@ export const Component = ({
304
306
 
305
307
  return (
306
308
  <Accordion>
307
- {accordionItems.map(({ title, content }, index) => (
309
+ {accordionItems.map((_item, index) => (
308
310
  <AccordionItem key={index} variant={variant}>
309
- <AccordionTitle icon={icon}>{title}</AccordionTitle>
310
- <AccordionContent>{content}</AccordionContent>
311
+ <AccordionTitle
312
+ icon={icon}
313
+ fieldPath={`accordionItems[${index}].title`}
314
+ />
315
+ <AccordionContent
316
+ fieldPath={`accordionItems[${index}].content`}
317
+ />
311
318
  </AccordionItem>
312
319
  ))}
313
320
  </Accordion>
@@ -337,7 +344,7 @@ import {
337
344
  } from '@hubspot/cms-component-library/Accordion';
338
345
 
339
346
  const defaultItem = {
340
- title: 'Accordion Title',
347
+ title: '<p>Accordion Title</p>',
341
348
  content: '<p>Accordion content goes here. You can add multiple lines of text.</p>',
342
349
  };
343
350
 
@@ -1,114 +1,23 @@
1
- import styles from './index.module.scss';
2
- import cx from '../utils/classname.js';
3
- import { getLanguageDisplayName } from './utils.js';
4
- import LanguageOptions from './LanguageOptions.js';
5
- import GlobeIcon from './assets/Globe.js';
1
+ // @ts-expect-error -- ?island not typed
2
+ import LanguageSwitcherIsland from './islands/LanguageSwitcherIsland.js?island';
6
3
  import type { LanguageSwitcherProps } from './types.js';
4
+ import { Island, useLanguageVariants } from '@hubspot/cms-components';
7
5
  import ContentFields from './ContentFields.js';
8
6
  import StyleFields from './StyleFields.js';
9
- import Button from '../Button/index.js';
10
- import { useLanguageVariants } from '@hubspot/cms-components';
11
- import { createTranslationsArrayAsObject } from './utils.js';
12
- import Drawer, { useDrawer } from '../Drawer/index.js';
13
- import { dummyTranslationsAsObject, dummyTranslations } from './_dummyData.js';
14
- import Divider from '../Divider/index.js';
15
- import type { CSSVariables } from '../utils/types.js';
16
7
 
17
8
  const LanguageSwitcherComponent = ({
18
- useDummyData = false, // !todo - remove this in PROD (NOTE: storybook still needs the _dummyData, so add the _dummyData file to storybook dir and refactor usage there)
19
- menuBackgroundColor,
20
- menuBackgroundColorHover,
21
- color,
22
- colorHover,
23
- languageSwitcherSelectText = 'Select a language',
24
- className = '',
25
- style = {},
9
+ translations: translationsProp,
10
+ ...rest
26
11
  }: LanguageSwitcherProps) => {
27
- const translations = useDummyData ? dummyTranslations : useLanguageVariants(); // !todo remove this in PROD
28
- const translationsArrayAsObject = useDummyData
29
- ? dummyTranslationsAsObject
30
- : createTranslationsArrayAsObject(translations); // !todo remove this in PROD
31
- const activeTranslation = translations?.find(item => item.isActive);
32
- const currentPageLanguage = activeTranslation?.languageCode ?? '';
33
- const currentPageLanguageDisplayName = getLanguageDisplayName({
34
- currentPageLanguage,
35
- translationsArrayAsObject,
36
- });
37
-
38
- const { isOpen, toggle, close } = useDrawer();
39
-
40
- const isInEditor = false; // !todo - update this to use useEditorVariableChecks() hook (is_in_editor property) once it's working
41
-
42
- const colorCss = color?.rgba;
43
- const menuBackgroundColorCss = menuBackgroundColor?.rgba;
44
-
45
- const cssVariables = {
46
- ...(colorCss && { '--hscl-languageSwitcher-color': colorCss }),
47
- ...(menuBackgroundColorCss && {
48
- '--hscl-languageSwitcher-backgroundColor': menuBackgroundColorCss,
49
- }),
50
- };
51
-
52
- const dividerCssVariables: CSSVariables = {
53
- '--hscl-divider-borderColor': colorCss,
54
- };
55
-
56
- const combinedClasses = cx(styles.languageSwitcher, className);
12
+ const languageVariants = useLanguageVariants();
13
+ const translations = translationsProp ?? languageVariants;
57
14
 
58
15
  return (
59
- <div style={{ ...cssVariables, ...style }} className={combinedClasses}>
60
- <Button
61
- buttonType="button"
62
- aria-expanded={isOpen}
63
- onClick={toggle}
64
- className={styles.button}
65
- style={{ color: colorCss }}
66
- >
67
- <GlobeIcon />
68
- {currentPageLanguageDisplayName}
69
- </Button>
70
- {!isInEditor && (
71
- <Drawer
72
- open={isOpen}
73
- direction="right"
74
- onOverlayClick={close}
75
- aria-label="Language selector"
76
- className={styles.drawer}
77
- style={{
78
- ...cssVariables,
79
- backgroundColor: menuBackgroundColorCss,
80
- }}
81
- >
82
- <div className={styles.sidePanelHeader}>
83
- <span className={styles.sidePanelTitle}>
84
- {languageSwitcherSelectText}
85
- </span>
86
- <Button
87
- buttonType="button"
88
- onClick={close}
89
- aria-label="Close language selector"
90
- className={styles.sidePanelCloseButton}
91
- >
92
-
93
- </Button>
94
- </div>
95
-
96
- <Divider spacing="0px" style={dividerCssVariables} />
97
-
98
- <div className={styles.sidePanelOptionsContainer}>
99
- <LanguageOptions
100
- {...{
101
- translations,
102
- menuBackgroundColor,
103
- menuBackgroundColorHover,
104
- color,
105
- colorHover,
106
- }}
107
- />
108
- </div>
109
- </Drawer>
110
- )}
111
- </div>
16
+ <Island
17
+ module={LanguageSwitcherIsland}
18
+ translations={translations}
19
+ {...rest}
20
+ />
112
21
  );
113
22
  };
114
23
 
@@ -0,0 +1,110 @@
1
+ import styles from '../index.module.scss';
2
+ import cx from '../../utils/classname.js';
3
+ import { getLanguageDisplayName } from '../utils.js';
4
+ import LanguageOptions from '../LanguageOptions.js';
5
+ import GlobeIcon from '../assets/Globe.js';
6
+ import type { LanguageSwitcherIslandProps } from '../types.js';
7
+ import Button from '../../Button/index.js';
8
+ import { createTranslationsArrayAsObject } from '../utils.js';
9
+ import Drawer, { useDrawer } from '../../Drawer/index.js';
10
+ import Divider from '../../Divider/index.js';
11
+ import type { CSSVariables } from '../../utils/types.js';
12
+
13
+ const LanguageSwitcherIsland = ({
14
+ translations,
15
+ menuBackgroundColor,
16
+ menuBackgroundColorHover,
17
+ color,
18
+ colorHover,
19
+ languageSwitcherSelectText = 'Select a language',
20
+ className = '',
21
+ style = {},
22
+ }: LanguageSwitcherIslandProps) => {
23
+ const translationsArrayAsObject = createTranslationsArrayAsObject(
24
+ translations
25
+ );
26
+ const activeTranslation = translations?.find(item => item.isActive);
27
+ const currentPageLanguage = activeTranslation?.languageCode ?? '';
28
+ const currentPageLanguageDisplayName = getLanguageDisplayName({
29
+ currentPageLanguage,
30
+ translationsArrayAsObject,
31
+ });
32
+
33
+ const { isOpen, toggle, close } = useDrawer();
34
+
35
+ const isInEditor = false;
36
+
37
+ const colorCss = color?.rgba;
38
+ const menuBackgroundColorCss = menuBackgroundColor?.rgba;
39
+
40
+ const cssVariables = {
41
+ ...(colorCss && { '--hscl-languageSwitcher-color': colorCss }),
42
+ ...(menuBackgroundColorCss && {
43
+ '--hscl-languageSwitcher-backgroundColor': menuBackgroundColorCss,
44
+ }),
45
+ };
46
+
47
+ const dividerCssVariables: CSSVariables = {
48
+ '--hscl-divider-borderColor': colorCss,
49
+ };
50
+
51
+ const combinedClasses = cx(styles.languageSwitcher, className);
52
+
53
+ return (
54
+ <div style={{ ...cssVariables, ...style }} className={combinedClasses}>
55
+ <Button
56
+ buttonType="button"
57
+ aria-expanded={isOpen}
58
+ onClick={toggle}
59
+ className={styles.button}
60
+ style={{ color: colorCss }}
61
+ >
62
+ <GlobeIcon />
63
+ {currentPageLanguageDisplayName}
64
+ </Button>
65
+ {!isInEditor && (
66
+ <Drawer
67
+ open={isOpen}
68
+ direction="right"
69
+ onOverlayClick={close}
70
+ aria-label="Language selector"
71
+ className={styles.drawer}
72
+ style={{
73
+ ...cssVariables,
74
+ backgroundColor: menuBackgroundColorCss,
75
+ }}
76
+ >
77
+ <div className={styles.sidePanelHeader}>
78
+ <span className={styles.sidePanelTitle}>
79
+ {languageSwitcherSelectText}
80
+ </span>
81
+ <Button
82
+ buttonType="button"
83
+ onClick={close}
84
+ aria-label="Close language selector"
85
+ className={styles.sidePanelCloseButton}
86
+ >
87
+
88
+ </Button>
89
+ </div>
90
+
91
+ <Divider spacing="0px" style={dividerCssVariables} />
92
+
93
+ <div className={styles.sidePanelOptionsContainer}>
94
+ <LanguageOptions
95
+ {...{
96
+ translations,
97
+ menuBackgroundColor,
98
+ menuBackgroundColorHover,
99
+ color,
100
+ colorHover,
101
+ }}
102
+ />
103
+ </div>
104
+ </Drawer>
105
+ )}
106
+ </div>
107
+ );
108
+ };
109
+
110
+ export default LanguageSwitcherIsland;
@@ -15,7 +15,7 @@ The LanguageSwitcher component provides an accessible interface for users to vie
15
15
 
16
16
  ```
17
17
  LanguageSwitcher/
18
- ├── index.tsx # Main component with render logic
18
+ ├── index.tsx # Main component (wrapper that delegates to Island)
19
19
  ├── types.ts # TypeScript type definitions
20
20
  ├── ContentFields.tsx # HubSpot field definitions for content
21
21
  ├── StyleFields.tsx # HubSpot field definitions for styling
@@ -26,6 +26,8 @@ LanguageSwitcher/
26
26
  ├── index.module.scss # CSS module with design tokens
27
27
  ├── assets/
28
28
  │ └── Globe.tsx # Globe SVG icon component
29
+ ├── islands/
30
+ │ └── LanguageSwitcherIsland.tsx # Client-side island with interactive UI
29
31
  └── stories/
30
32
  ├── LanguageSwitcher.stories.tsx # Component usage examples
31
33
  ├── LanguageSwitcherDecorator.tsx # Storybook decorator
@@ -41,82 +43,43 @@ LanguageSwitcher/
41
43
  **Props:**
42
44
  ```tsx
43
45
  {
46
+ translations?: LanguageVariant[]; // Language variants (if not provided, uses useLanguageVariants hook)
44
47
  className?: string; // Additional CSS classes
45
48
  style?: React.CSSProperties; // Inline styles
46
- textColor?: ColorFieldValue; // Text color for button and menu items
47
- textColorHover?: ColorFieldValue; // Text color for menu items on hover
49
+ color?: ColorFieldValue; // Text color for button and menu items
50
+ colorHover?: ColorFieldValue; // Text color for menu items on hover
48
51
  menuBackgroundColor?: ColorFieldValue; // Background color for the drawer
49
52
  menuBackgroundColorHover?: ColorFieldValue; // Background color for menu items on hover
50
53
  languageSwitcherSelectText?: string; // Header text in drawer (default: 'Select a language')
51
- useDummyData?: boolean; // Use dummy data for testing (development only)
52
54
  }
53
55
  ```
54
56
 
55
57
  ## Usage Examples
56
58
 
57
- ### Island Usage
59
+ ### Basic Usage
58
60
 
59
- The LanguageSwitcher component requires JavaScript for interactive state management and must be used within an Island component.
60
-
61
- #### Island Component
61
+ The LanguageSwitcher component automatically handles Island-ification internally. Simply import and use it with optional props:
62
62
 
63
63
  ```tsx
64
- // islands/LanguageSwitcherIsland.tsx
65
64
  import LanguageSwitcher from '@hubspot/cms-component-library/LanguageSwitcher';
66
65
 
67
- export default function LanguageSwitcherIsland({
68
- textColor,
69
- textColorHover,
70
- menuBackgroundColor,
71
- menuBackgroundColorHover,
72
- languageSwitcherSelectText,
73
- }) {
74
- return (
75
- <LanguageSwitcher
76
- textColor={textColor}
77
- textColorHover={textColorHover}
78
- menuBackgroundColor={menuBackgroundColor}
79
- menuBackgroundColorHover={menuBackgroundColorHover}
80
- languageSwitcherSelectText={languageSwitcherSelectText}
81
- />
82
- );
66
+ export default function MyComponent() {
67
+ return <LanguageSwitcher />;
83
68
  }
84
69
  ```
85
70
 
86
- #### Module File
87
-
88
- ```tsx
89
- import { Island } from '@hubspot/cms-components';
90
- import LanguageSwitcherIsland from './islands/LanguageSwitcherIsland?island';
91
-
92
- export const Component = ({ fieldValues }) => {
93
- return (
94
- <Island
95
- module={LanguageSwitcherIsland}
96
- textColor={fieldValues.textColor}
97
- textColorHover={fieldValues.textColorHover}
98
- menuBackgroundColor={fieldValues.menuBackgroundColor}
99
- menuBackgroundColorHover={fieldValues.menuBackgroundColorHover}
100
- languageSwitcherSelectText={fieldValues.languageSwitcherSelectText}
101
- />
102
- );
103
- };
104
- ```
71
+ When used without a `translations` prop, it automatically calls `useLanguageVariants()` to fetch language data.
105
72
 
106
- ### Basic Language Switcher
73
+ ### With Custom Header Text
107
74
 
108
75
  ```tsx
109
- import LanguageSwitcher from '@hubspot/cms-component-library/LanguageSwitcher';
110
-
111
- export default function LanguageSwitcherIsland() {
112
- return <LanguageSwitcher />;
113
- }
76
+ <LanguageSwitcher languageSwitcherSelectText="Choose your language" />
114
77
  ```
115
78
 
116
- ### With Custom Header Text
79
+ ### With Custom Translations
117
80
 
118
81
  ```tsx
119
- <LanguageSwitcher languageSwitcherSelectText="Choose your language" />
82
+ <LanguageSwitcher translations={customLanguageVariants} />
120
83
  ```
121
84
 
122
85
  ### With Custom Colors
@@ -125,8 +88,8 @@ If passing in colors directly (without using a ColorField), like the below examp
125
88
 
126
89
  ```tsx
127
90
  <LanguageSwitcher
128
- textColor={{ rgba: 'rgba(51, 71, 91, 1)' }}
129
- textColorHover={{ rgba: 'rgba(0, 102, 204, 1)' }}
91
+ color={{ rgba: 'rgba(51, 71, 91, 1)' }}
92
+ colorHover={{ rgba: 'rgba(0, 102, 204, 1)' }}
130
93
  menuBackgroundColor={{ rgba: 'rgba(255, 255, 255, 1)' }}
131
94
  menuBackgroundColorHover={{ rgba: 'rgba(245, 245, 245, 1)' }}
132
95
  />
@@ -197,21 +160,19 @@ Configurable props for color customization:
197
160
 
198
161
  ### Module Usage Example
199
162
 
200
- If passing in colors using a ColorField, like the below example, pass in the entire field value. HubSpot will transform the field value from a default of { color: hex, opacity: number } to { rbga, rgb, opacity, color, hex, css }. The LanguageSwitcher component will pull the rgba value from this final output.
163
+ If passing in colors using a ColorField, like the below example, pass in the entire field value. HubSpot will transform the field value from a default of { color: hex, opacity: number } to { rgba, rgb, opacity, color, hex, css }. The LanguageSwitcher component will pull the rgba value from this final output.
201
164
 
202
165
  ```tsx
203
- import { Island } from '@hubspot/cms-components';
204
- import LanguageSwitcherIsland from './islands/LanguageSwitcherIsland?island';
166
+ import LanguageSwitcher from '@hubspot/cms-component-library/LanguageSwitcher';
205
167
 
206
168
  export default function HeaderModule({ fieldValues }) {
207
169
  return (
208
170
  <header>
209
171
  <nav>
210
172
  {/* Other navigation items */}
211
- <Island
212
- module={LanguageSwitcherIsland}
213
- textColor={fieldValues.textColor}
214
- textColorHover={fieldValues.textColorHover}
173
+ <LanguageSwitcher
174
+ color={fieldValues.color}
175
+ colorHover={fieldValues.colorHover}
215
176
  menuBackgroundColor={fieldValues.menuBackgroundColor}
216
177
  menuBackgroundColorHover={fieldValues.menuBackgroundColorHover}
217
178
  languageSwitcherSelectText={fieldValues.languageSwitcherSelectText}
@@ -280,14 +241,14 @@ The LanguageSwitcher component follows accessibility best practices:
280
241
 
281
242
  ## Best Practices
282
243
 
283
- - **Use Islands for interactivity**: The LanguageSwitcher component is interactive and requires JavaScript, so it must be used within an Island component in modules
244
+ - **Island-ification is handled internally**: The LanguageSwitcher component automatically handles Island-ification for interactive state, so you don't need to manually wrap it
284
245
  - **Position prominently**: Place in global navigation (header or footer) where users expect language controls
285
246
  - **Use native language names**: The component automatically displays language names in their native script (e.g., "Español" for Spanish, "Français" for French)
286
247
  - **Customize header text**: Set `languageSwitcherSelectText` to match your site's primary language for consistency
287
248
  - **Color coordination**: Match `textColor` and `menuBackgroundColor` to your site's theme
288
249
  - **Color setting**: If using ColorFields, pass in the entire field value, as HubSpot transforms the user's color choice into an object with an rgba key. Otherwise, ensure the color object has an rgba key and value. The component expects an object with an rgba key.
289
250
  - **Responsive design**: The drawer automatically adjusts to full width on mobile devices (≤400px)
290
- - **Test with real translations**: While `useDummyData` prop exists for development, always test with actual HubSpot language variants in production
251
+ - **Test with real translations**: Pass real HubSpot language variants via the `translations` prop, or rely on the default `useLanguageVariants()` hook for production
291
252
  - **Provide clear visual feedback**: Use hover colors that provide sufficient contrast for user interaction
292
253
  - **Consider placement**: Position near other utility navigation items (search, account, etc.)
293
254
  - **Dark mode support**: Provide appropriate color values for dark backgrounds when needed
@@ -297,8 +258,7 @@ The LanguageSwitcher component follows accessibility best practices:
297
258
  ### Header Navigation Integration
298
259
 
299
260
  ```tsx
300
- import { Island } from '@hubspot/cms-components';
301
- import LanguageSwitcherIsland from './islands/LanguageSwitcherIsland?island';
261
+ import LanguageSwitcher from '@hubspot/cms-component-library/LanguageSwitcher';
302
262
 
303
263
  export default function SiteHeader({ fieldValues }) {
304
264
  return (
@@ -315,9 +275,8 @@ export default function SiteHeader({ fieldValues }) {
315
275
  <a href="/about">About</a>
316
276
  <a href="/services">Services</a>
317
277
  <a href="/contact">Contact</a>
318
- <Island
319
- module={LanguageSwitcherIsland}
320
- textColor={fieldValues.textColor}
278
+ <LanguageSwitcher
279
+ color={fieldValues.color}
321
280
  menuBackgroundColor={fieldValues.menuBackgroundColor}
322
281
  languageSwitcherSelectText={fieldValues.languageSwitcherSelectText}
323
282
  />
@@ -330,13 +289,14 @@ export default function SiteHeader({ fieldValues }) {
330
289
  ### Footer Placement
331
290
 
332
291
  ```tsx
292
+ import LanguageSwitcher from '@hubspot/cms-component-library/LanguageSwitcher';
293
+
333
294
  export default function SiteFooter({ fieldValues }) {
334
295
  return (
335
296
  <footer style={{ padding: '48px 24px', textAlign: 'center' }}>
336
297
  <div style={{ marginBottom: '24px' }}>
337
- <Island
338
- module={LanguageSwitcherIsland}
339
- textColor={fieldValues.footerTextColor}
298
+ <LanguageSwitcher
299
+ color={fieldValues.footerTextColor}
340
300
  menuBackgroundColor={fieldValues.menuBackgroundColor}
341
301
  languageSwitcherSelectText={fieldValues.languageSwitcherSelectText}
342
302
  />
@@ -350,6 +310,8 @@ export default function SiteFooter({ fieldValues }) {
350
310
  ### Mobile-First Navigation
351
311
 
352
312
  ```tsx
313
+ import LanguageSwitcher from '@hubspot/cms-component-library/LanguageSwitcher';
314
+
353
315
  export default function MobileNav({ fieldValues }) {
354
316
  return (
355
317
  <nav style={{
@@ -362,9 +324,8 @@ export default function MobileNav({ fieldValues }) {
362
324
  <a href="/about">About</a>
363
325
  <a href="/contact">Contact</a>
364
326
  <div style={{ borderTop: '1px solid #eee', paddingTop: '16px' }}>
365
- <Island
366
- module={LanguageSwitcherIsland}
367
- textColor={fieldValues.textColor}
327
+ <LanguageSwitcher
328
+ color={fieldValues.color}
368
329
  menuBackgroundColor={fieldValues.menuBackgroundColor}
369
330
  />
370
331
  </div>
@@ -3,6 +3,7 @@ import LanguageSwitcher from '../index.js';
3
3
  import { LanguageSwitcherProps } from '../types.js';
4
4
  import { withLanguageSwitcherStyles } from './LanguageSwitcherDecorator.js';
5
5
  import { SBContainer, SBFocusWrapper } from '@sb-utils';
6
+ import { dummyTranslations } from '../_dummyData.js';
6
7
 
7
8
  const meta: Meta<LanguageSwitcherProps> = {
8
9
  title: 'Component Library/LanguageSwitcher',
@@ -21,14 +22,6 @@ const meta: Meta<LanguageSwitcherProps> = {
21
22
  },
22
23
  decorators: [withLanguageSwitcherStyles],
23
24
  argTypes: {
24
- useDummyData: {
25
- control: 'boolean',
26
- description: 'Use dummy translation data for testing',
27
- table: {
28
- category: 'Development',
29
- defaultValue: { summary: 'false' },
30
- },
31
- },
32
25
  languageSwitcherSelectText: {
33
26
  control: 'text',
34
27
  description: 'Text displayed in the drawer header',
@@ -37,14 +30,14 @@ const meta: Meta<LanguageSwitcherProps> = {
37
30
  defaultValue: { summary: 'Select a language' },
38
31
  },
39
32
  },
40
- textColor: {
33
+ color: {
41
34
  control: 'object',
42
35
  description: 'Text color for button and menu items',
43
36
  table: {
44
37
  category: 'Visual',
45
38
  },
46
39
  },
47
- textColorHover: {
40
+ colorHover: {
48
41
  control: 'object',
49
42
  description: 'Text color for menu items on hover',
50
43
  table: {
@@ -90,7 +83,7 @@ export const Default: Story = {
90
83
  return (
91
84
  <SBContainer>
92
85
  <LanguageSwitcher
93
- useDummyData={true}
86
+ translations={dummyTranslations}
94
87
  languageSwitcherSelectText="Select a language"
95
88
  />
96
89
  </SBContainer>
@@ -140,10 +133,10 @@ export const ColorThemes: Story = {
140
133
  <p>Custom color theming with purple tones.</p>
141
134
  <SBContainer>
142
135
  <LanguageSwitcher
143
- useDummyData={true}
136
+ translations={dummyTranslations}
144
137
  languageSwitcherSelectText="Wählen Sie eine Sprache"
145
- textColor={purpleTextColor}
146
- textColorHover={purpleTextColorHover}
138
+ color={purpleTextColor}
139
+ colorHover={purpleTextColorHover}
147
140
  menuBackgroundColor={purpleMenuBgColor}
148
141
  menuBackgroundColorHover={purpleMenuBgColorHover}
149
142
  />
@@ -162,10 +155,10 @@ export const ColorThemes: Story = {
162
155
  </SBContainer>
163
156
  <SBContainer>
164
157
  <LanguageSwitcher
165
- useDummyData={true}
158
+ translations={dummyTranslations}
166
159
  languageSwitcherSelectText="Select a language"
167
- textColor={darkTextColor}
168
- textColorHover={darkTextColorHover}
160
+ color={darkTextColor}
161
+ colorHover={darkTextColorHover}
169
162
  menuBackgroundColor={darkMenuBgColor}
170
163
  menuBackgroundColorHover={darkMenuBgColorHover}
171
164
  />
@@ -195,7 +188,7 @@ export const CustomText: Story = {
195
188
  <strong>🇬🇧 English:</strong>
196
189
  </p>
197
190
  <LanguageSwitcher
198
- useDummyData={true}
191
+ translations={dummyTranslations}
199
192
  languageSwitcherSelectText="Select a language"
200
193
  />
201
194
  </SBContainer>
@@ -205,7 +198,7 @@ export const CustomText: Story = {
205
198
  <strong>🇩🇪 German:</strong>
206
199
  </p>
207
200
  <LanguageSwitcher
208
- useDummyData={true}
201
+ translations={dummyTranslations}
209
202
  languageSwitcherSelectText="Sprache wählen"
210
203
  />
211
204
  </SBContainer>
@@ -215,7 +208,7 @@ export const CustomText: Story = {
215
208
  <strong>🇪🇸 Spanish:</strong>
216
209
  </p>
217
210
  <LanguageSwitcher
218
- useDummyData={true}
211
+ translations={dummyTranslations}
219
212
  languageSwitcherSelectText="Seleccionar idioma"
220
213
  />
221
214
  </SBContainer>
@@ -225,7 +218,7 @@ export const CustomText: Story = {
225
218
  <strong>🇫🇷 French:</strong>
226
219
  </p>
227
220
  <LanguageSwitcher
228
- useDummyData={true}
221
+ translations={dummyTranslations}
229
222
  languageSwitcherSelectText="Choisir une langue"
230
223
  />
231
224
  </SBContainer>
@@ -288,9 +281,9 @@ export const InContext: Story = {
288
281
 
289
282
  <div style={{ paddingTop: '6px' }}>
290
283
  <LanguageSwitcher
291
- useDummyData={true}
292
- textColor={{ rgba: 'rgb(55, 245, 255)' }}
293
- textColorHover={{ rgba: 'rgb(255, 255, 255)' }}
284
+ translations={dummyTranslations}
285
+ color={{ rgba: 'rgb(55, 245, 255)' }}
286
+ colorHover={{ rgba: 'rgb(255, 255, 255)' }}
294
287
  menuBackgroundColor={{ rgba: 'rgba(100, 181, 246, 1)' }}
295
288
  menuBackgroundColorHover={{ rgba: 'rgba(0, 160, 240, 1)' }}
296
289
  />
@@ -325,7 +318,7 @@ export const InteractionStates: Story = {
325
318
  <h4>Hover State</h4>
326
319
  <p>Hover over the language switcher button to see the hover effect</p>
327
320
  <LanguageSwitcher
328
- useDummyData={true}
321
+ translations={dummyTranslations}
329
322
  languageSwitcherSelectText="Select a language"
330
323
  />
331
324
  </SBContainer>
@@ -339,7 +332,7 @@ export const InteractionStates: Story = {
339
332
  </p>
340
333
  <SBFocusWrapper>
341
334
  <LanguageSwitcher
342
- useDummyData={true}
335
+ translations={dummyTranslations}
343
336
  languageSwitcherSelectText="Select a language"
344
337
  />
345
338
  </SBFocusWrapper>
@@ -7,8 +7,19 @@ import type { CSSVariables } from '../utils/types.js';
7
7
 
8
8
  type ColorFieldValue = typeof ColorFieldDefaults;
9
9
 
10
+ export type LanguageSwitcherIslandProps = {
11
+ translations: LanguageVariant[];
12
+ className?: string;
13
+ style?: CSSVariables;
14
+ color?: ColorFieldValue;
15
+ colorHover?: ColorFieldValue;
16
+ menuBackgroundColorHover?: ColorFieldValue;
17
+ menuBackgroundColor?: ColorFieldValue;
18
+ languageSwitcherSelectText?: string;
19
+ };
20
+
10
21
  export type LanguageSwitcherProps = {
11
- useDummyData?: boolean; // !todo - remove this in PROD
22
+ translations?: LanguageVariant[];
12
23
  className?: string;
13
24
  style?: CSSVariables;
14
25
  color?: ColorFieldValue;
@@ -11,8 +11,16 @@ const ContentFields = ({
11
11
  href: 'https://www.hubspot.com',
12
12
  },
13
13
  },
14
+ fieldVisibility,
14
15
  }: ContentFieldsProps) => {
15
- return <LinkField label={linkLabel} name={linkName} default={linkDefault} />;
16
+ return (
17
+ <LinkField
18
+ label={linkLabel}
19
+ name={linkName}
20
+ visibility={fieldVisibility?.[linkName]}
21
+ default={linkDefault}
22
+ />
23
+ );
16
24
  };
17
25
 
18
26
  export default ContentFields;
@@ -198,6 +198,19 @@ The Link component provides field definitions for easy integration with HubSpot
198
198
  />
199
199
  ```
200
200
 
201
+ **Props:**
202
+ - `linkLabel?: string` — label for the LinkField (default: `'Link'`)
203
+ - `linkName?: string` — field name (default: `'link'`)
204
+ - `linkDefault?` — default value for the LinkField
205
+ - `fieldVisibility?: Partial<Record<string, Visibility>>` — visibility options keyed by field name (keys must match the corresponding field name prop, e.g. `linkName`). Values match the cms-components Visibility schema (e.g. `hidden_subfields`, `controlling_field_path`, `operator`)
206
+
207
+ **Example — hide the nofollow option:**
208
+ ```tsx
209
+ <Link.ContentFields
210
+ fieldVisibility={{ link: { hidden_subfields: { no_follow: true } } }}
211
+ />
212
+ ```
213
+
201
214
  **Fields:**
202
215
  - `link`: LinkField for href destination
203
216
 
@@ -1,4 +1,4 @@
1
- import { LinkFieldDefaults } from '@hubspot/cms-components/fields';
1
+ import { LinkFieldDefaults, Visibility } from '@hubspot/cms-components/fields';
2
2
  import { LinkFieldValue } from '../utils/linkField.js';
3
3
 
4
4
  export type BaseLinkProps = {
@@ -37,6 +37,7 @@ export type ContentFieldsProps = {
37
37
  linkLabel?: string;
38
38
  linkName?: string;
39
39
  linkDefault?: typeof LinkFieldDefaults;
40
+ fieldVisibility?: Partial<Record<string, Visibility>>;
40
41
  };
41
42
 
42
43
  export type StyleFieldsProps = {
@@ -0,0 +1,25 @@
1
+ import { LogoField } from '@hubspot/cms-components/fields';
2
+ import { ContentFieldsProps } from './types.js';
3
+
4
+ const ContentFields = ({
5
+ logoLabel = 'Logo',
6
+ logoName = 'img',
7
+ logoDefault = {
8
+ override_inherited_src: false,
9
+ src: null,
10
+ alt: null,
11
+ loading: 'disabled',
12
+ },
13
+ showLoading = true,
14
+ }: ContentFieldsProps) => {
15
+ return (
16
+ <LogoField
17
+ label={logoLabel}
18
+ name={logoName}
19
+ showLoading={showLoading}
20
+ default={logoDefault}
21
+ />
22
+ );
23
+ };
24
+
25
+ export default ContentFields;
@@ -1,6 +1,7 @@
1
1
  import { useMemo } from 'react';
2
2
  import { useBrandSettings } from '@hubspot/cms-components';
3
3
  import { dummyLogoData } from './_dummyLogoData.js';
4
+ import ContentFields from './ContentFields.js';
4
5
  import { LogoData, LogoProps } from './types.js';
5
6
  import styles from './index.module.scss';
6
7
  import cx from '../utils/classname.js';
@@ -18,9 +19,10 @@ export const Logo = ({
18
19
  }: LogoProps) => {
19
20
  const { primaryLogo } = useBrandSettings() ?? {};
20
21
 
21
- const logo: LogoData | undefined = useMemo(() => {
22
- return useDummyLogo ? dummyLogoData.logo : primaryLogo;
23
- }, [useDummyLogo, primaryLogo]);
22
+ const logo: LogoData | undefined = useMemo(
23
+ () => (useDummyLogo ? dummyLogoData.logo : primaryLogo),
24
+ [useDummyLogo, primaryLogo]
25
+ );
24
26
 
25
27
  if (!logo?.src) {
26
28
  return null;
@@ -70,4 +72,11 @@ export const Logo = ({
70
72
  );
71
73
  };
72
74
 
73
- export default Logo;
75
+ type LogoComponentType = typeof Logo & {
76
+ ContentFields: typeof ContentFields;
77
+ };
78
+
79
+ const LogoWithFields = Logo as LogoComponentType;
80
+ LogoWithFields.ContentFields = ContentFields;
81
+
82
+ export default LogoWithFields;
@@ -6,6 +6,21 @@ export type LogoData = {
6
6
  link?: string;
7
7
  };
8
8
 
9
+ export type ContentFieldsProps = {
10
+ logoLabel?: string;
11
+ logoName?: string;
12
+ logoDefault?: {
13
+ override_inherited_src?: boolean;
14
+ src?: string | null;
15
+ alt?: string | null;
16
+ width?: number;
17
+ height?: number;
18
+ loading?: string;
19
+ suppress_company_name?: boolean;
20
+ };
21
+ showLoading?: boolean;
22
+ };
23
+
9
24
  export type LogoProps = {
10
25
  useDummyLogo?: boolean;
11
26
  className?: string;
@@ -203,6 +203,25 @@ const SHOW_ICON_VISIBILITY_RULES: Visibility = {
203
203
  />
204
204
  ```
205
205
 
206
+ ### Field Visibility Map Pattern
207
+
208
+ For components with configurable field names, use a `fieldVisibility` prop keyed by field name. Values match the cms-components `Visibility` schema (e.g. `hidden_subfields`, `controlling_field_path`, `operator`). Keys must match the field's name prop (e.g. `linkName`).
209
+
210
+ Link component example:
211
+
212
+ ```typescript
213
+ <Link.ContentFields
214
+ fieldVisibility={{ link: { hidden_subfields: { no_follow: true } } }}
215
+ />
216
+
217
+ <Link.ContentFields
218
+ linkName="footerLink"
219
+ fieldVisibility={{ footerLink: { hidden_subfields: { no_follow: true, open_in_new_tab: true } } }}
220
+ />
221
+ ```
222
+
223
+ **Key Pattern:** Look up visibility by the configurable name prop: `visibility={fieldVisibility?.[linkName]}`
224
+
206
225
  ## Nested Component Fields Pattern
207
226
 
208
227
  **Pattern for nested component fields:**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/cms-component-library",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "HubSpot CMS React component library for building CMS modules",
5
5
  "license": "Apache-2.0",
6
6
  "exports": {