@hubspot/cms-component-library 0.3.4 → 0.3.6

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,31 +1,13 @@
1
- import { TextField, ChoiceField } from '@hubspot/cms-components/fields';
1
+ import { TextField } from '@hubspot/cms-components/fields';
2
2
  import type { ContentFieldsProps } from './types.js';
3
3
 
4
4
  const ContentFields = ({
5
5
  titleLabel = 'Title',
6
6
  titleName = 'title',
7
7
  titleDefault = 'Accordion Title',
8
- iconLabel = 'Icon',
9
- iconName = 'icon',
10
- iconDefault = 'chevron',
11
8
  }: ContentFieldsProps) => {
12
9
  return (
13
- <>
14
- <ChoiceField
15
- label={iconLabel}
16
- name={iconName}
17
- display="buttons"
18
- choices={[
19
- ['chevron', 'Chevron'],
20
- ['plus', 'Plus'],
21
- ['caret', 'Caret'],
22
- ]}
23
- preset="expand_icon"
24
- required={false}
25
- default={iconDefault}
26
- />
27
- <TextField label={titleLabel} name={titleName} default={titleDefault} />
28
- </>
10
+ <TextField label={titleLabel} name={titleName} default={titleDefault} />
29
11
  );
30
12
  };
31
13
 
@@ -0,0 +1,26 @@
1
+ import { ChoiceField } from '@hubspot/cms-components/fields';
2
+ import type { StyleFieldsProps } from './types.js';
3
+
4
+ const StyleFields = ({
5
+ iconLabel = 'Icon',
6
+ iconName = 'icon',
7
+ iconDefault = 'chevron',
8
+ }: StyleFieldsProps) => {
9
+ return (
10
+ <ChoiceField
11
+ label={iconLabel}
12
+ name={iconName}
13
+ display="buttons"
14
+ choices={[
15
+ ['caret', 'Caret'],
16
+ ['chevron', 'Chevron'],
17
+ ['plus', 'Plus'],
18
+ ]}
19
+ preset="expand_icon"
20
+ required={false}
21
+ default={iconDefault}
22
+ />
23
+ );
24
+ };
25
+
26
+ export default StyleFields;
@@ -1,5 +1,6 @@
1
1
  import styles from './index.module.scss';
2
2
  import ContentFields from './ContentFields.js';
3
+ import StyleFields from './StyleFields.js';
3
4
  import cx from '../../utils/classname.js';
4
5
  import { AccordionTitleProps } from './types.js';
5
6
  import { ChevronIcon, PlusIcon, MinusIcon, CaretIcon } from './icons.js';
@@ -39,9 +40,11 @@ const AccordionTitleComponent = ({
39
40
 
40
41
  type AccordionTitleComponentType = typeof AccordionTitleComponent & {
41
42
  ContentFields: typeof ContentFields;
43
+ StyleFields: typeof StyleFields;
42
44
  };
43
45
 
44
46
  const AccordionTitle = AccordionTitleComponent as AccordionTitleComponentType;
45
47
  AccordionTitle.ContentFields = ContentFields;
48
+ AccordionTitle.StyleFields = StyleFields;
46
49
 
47
50
  export default AccordionTitle;
@@ -11,6 +11,9 @@ export type ContentFieldsProps = {
11
11
  titleLabel?: string;
12
12
  titleName?: string;
13
13
  titleDefault?: string;
14
+ };
15
+
16
+ export type StyleFieldsProps = {
14
17
  iconLabel?: string;
15
18
  iconName?: string;
16
19
  iconDefault?: AccordionIconType;
@@ -52,7 +52,8 @@ Accordion/
52
52
  ├── AccordionTitle/
53
53
  │ ├── index.tsx # AccordionTitle component
54
54
  │ ├── types.ts # AccordionTitle TypeScript types
55
- │ ├── ContentFields.tsx # HubSpot field definitions for title/icon
55
+ │ ├── ContentFields.tsx # HubSpot field definitions for title text
56
+ │ ├── StyleFields.tsx # HubSpot field definitions for icon
56
57
  │ ├── icons.tsx # SVG icon components (Chevron, Plus, Minus)
57
58
  │ └── index.module.scss # CSS module for title styling
58
59
  ├── AccordionContent/
@@ -228,13 +229,25 @@ Configurable props for variant selection:
228
229
 
229
230
  #### AccordionTitle.ContentFields
230
231
 
231
- Configurable props for title text and icon:
232
+ Configurable props for title text:
232
233
 
233
234
  ```tsx
234
235
  <AccordionTitle.ContentFields
235
236
  titleName="title"
236
237
  titleLabel="Title"
237
238
  titleDefault="Accordion Title"
239
+ />
240
+ ```
241
+
242
+ **Fields:**
243
+ - `title`: TextField for accordion title text
244
+
245
+ #### AccordionTitle.StyleFields
246
+
247
+ Configurable props for icon style:
248
+
249
+ ```tsx
250
+ <AccordionTitle.StyleFields
238
251
  iconName="icon"
239
252
  iconLabel="Icon"
240
253
  iconDefault="chevron"
@@ -242,7 +255,6 @@ Configurable props for title text and icon:
242
255
  ```
243
256
 
244
257
  **Fields:**
245
- - `title`: TextField for accordion title text
246
258
  - `icon`: ChoiceField for selecting icon type (caret, chevron, plus)
247
259
 
248
260
  #### AccordionContent.ContentFields
@@ -275,11 +287,11 @@ import { AccordionIconType } from '@hubspot/cms-component-library/Accordion/Acco
275
287
  type FAQModuleProps = {
276
288
  style?: {
277
289
  variant?: AccordionVariant;
290
+ icon?: AccordionIconType;
278
291
  };
279
292
  accordionItems?: Array<{
280
293
  title?: string;
281
294
  content?: string;
282
- icon?: AccordionIconType;
283
295
  }>;
284
296
  };
285
297
 
@@ -288,10 +300,11 @@ export const Component = ({
288
300
  accordionItems = [],
289
301
  }: FAQModuleProps) => {
290
302
  const variant = style?.variant;
303
+ const icon = style?.icon;
291
304
 
292
305
  return (
293
306
  <Accordion>
294
- {accordionItems.map(({ title, content, icon }, index) => (
307
+ {accordionItems.map(({ title, content }, index) => (
295
308
  <AccordionItem key={index} variant={variant}>
296
309
  <AccordionTitle icon={icon}>{title}</AccordionTitle>
297
310
  <AccordionContent>{content}</AccordionContent>
@@ -326,7 +339,6 @@ import {
326
339
  const defaultItem = {
327
340
  title: 'Accordion Title',
328
341
  content: '<p>Accordion content goes here. You can add multiple lines of text.</p>',
329
- icon: 'chevron',
330
342
  };
331
343
 
332
344
  export const fields = (
@@ -342,6 +354,7 @@ export const fields = (
342
354
  </RepeatedFieldGroup>
343
355
  <FieldGroup label="Style" name="style" tab="STYLE">
344
356
  <AccordionItem.StyleFields />
357
+ <AccordionTitle.StyleFields />
345
358
  </FieldGroup>
346
359
  </ModuleFields>
347
360
  );
@@ -426,7 +439,7 @@ The Accordion component follows accessibility best practices:
426
439
  - **Gap selection**: Use any valid CSS length value (e.g., '8px', '16px', '24px', '48px') for spacing between items
427
440
  - **Dynamic rendering**: Always provide unique `key` props when mapping arrays to AccordionItems
428
441
  - **Rich content**: AccordionContent supports any HTML content including lists, paragraphs, images, and nested components
429
- - **Prefer library components**: When adding content inside AccordionContent, prefer using library components (e.g., `List`, `Heading`) over raw HTML elements for consistent theming and styling
442
+ - **Prefer library components**: When adding content inside AccordionContent, prefer using library components (e.g., `List`, `Text` with `textFeatureSet="heading"` for headings) over raw HTML elements for consistent theming and styling
430
443
  - **CSS Variables**: Override design tokens using CSS variables rather than hardcoding values
431
444
  - **Single responsibility**: Keep each accordion item focused on one topic for better UX
432
445
 
@@ -182,4 +182,4 @@ The Card component follows accessibility best practices:
182
182
 
183
183
  - **Flex**: Use for arranging cards in flexible layouts
184
184
  - **Grid**: Use for creating responsive card grids
185
- - **Heading**: Commonly used for card titles
185
+ - **Text**: Use with `textFeatureSet="heading"` for editable card titles
@@ -0,0 +1,19 @@
1
+ import { FormField } from '@hubspot/cms-components/fields';
2
+ import { ContentFieldsProps } from './types.js';
3
+
4
+ const ContentFields = ({
5
+ formIdLabel = 'Form',
6
+ formIdName = 'form',
7
+ formIdDefault = {},
8
+ }: ContentFieldsProps) => {
9
+ return (
10
+ <FormField
11
+ label={formIdLabel}
12
+ name={formIdName}
13
+ embedVersions={['v4', 'v3', 'v2']}
14
+ default={formIdDefault}
15
+ />
16
+ );
17
+ };
18
+
19
+ export default ContentFields;
@@ -0,0 +1,28 @@
1
+ // @ts-expect-error -- ?island not typed
2
+ import FormIsland from './islands/FormIsland.js?island';
3
+ // @ts-expect-error -- ?island not typed
4
+ import LegacyFormIsland from './islands/LegacyFormIsland.js?island';
5
+ import { FormProps } from './types.js';
6
+ import { Island } from '@hubspot/cms-components';
7
+ import ContentFields from './ContentFields.js';
8
+
9
+ const FormComponent = ({
10
+ formField,
11
+ formId,
12
+ formVersion,
13
+ ...rest
14
+ }: FormProps) => {
15
+ const resolvedFormId = formField != null ? formField.form_id : formId;
16
+ const FormModule = formVersion === 'v4' ? FormIsland : LegacyFormIsland;
17
+
18
+ return <Island module={FormModule} formId={resolvedFormId} {...rest} />;
19
+ };
20
+
21
+ type FormComponentType = typeof FormComponent & {
22
+ ContentFields: typeof ContentFields;
23
+ };
24
+
25
+ const Form = FormComponent as FormComponentType;
26
+ Form.ContentFields = ContentFields;
27
+
28
+ export default Form;
@@ -0,0 +1,54 @@
1
+ import { useEffect } from 'react';
2
+ import { FormProps } from '../types.js';
3
+ import { getHubID, getHSEnv } from '@hubspot/cms-components';
4
+
5
+ const getScriptSrc = (portalId: number, env: string) => {
6
+ const host = env === 'qa' ? 'js.hsformsqa.net' : 'js.hsforms.net';
7
+ return `https://${host}/forms/embed/developer/${portalId}.js`;
8
+ };
9
+
10
+ const FormIsland = ({ formId }: FormProps) => {
11
+ const portalId = getHubID();
12
+ const resolvedEnv = getHSEnv();
13
+
14
+ useEffect(() => {
15
+ if (!formId || !portalId) {
16
+ return;
17
+ }
18
+
19
+ const scriptSrc = getScriptSrc(portalId, resolvedEnv);
20
+ const existingScript = document.querySelector(`script[src="${scriptSrc}"]`);
21
+
22
+ if (existingScript) {
23
+ return;
24
+ }
25
+
26
+ const script = document.createElement('script');
27
+ script.src = scriptSrc;
28
+ document.head.appendChild(script);
29
+
30
+ return () => {
31
+ script.remove();
32
+ };
33
+ }, [formId, portalId, resolvedEnv]);
34
+
35
+ if (!formId || !portalId) {
36
+ return null;
37
+ }
38
+
39
+ /*
40
+ <script src="https://js.hsformsqa.net/forms/embed/102771136.js" defer></script>
41
+ <div class="hs-form-frame" data-env="qa" data-region="na1" data-form-id="53e5b258-4526-4012-9274-8bbe23ab2d09" data-portal-id="102771136"></div>
42
+ */
43
+
44
+ return (
45
+ <div
46
+ className="hs-form-frame"
47
+ data-form-id={formId}
48
+ data-portal-id={portalId}
49
+ data-env={resolvedEnv}
50
+ />
51
+ );
52
+ };
53
+
54
+ export default FormIsland;
@@ -0,0 +1,77 @@
1
+ import { useEffect, useId } from 'react';
2
+ import { FormProps } from '../types.js';
3
+ import { getHubID, getHSEnv } from '@hubspot/cms-components';
4
+
5
+ declare global {
6
+ interface Window {
7
+ hbspt?: {
8
+ forms: {
9
+ create: (options: {
10
+ portalId: number;
11
+ formId: string;
12
+ env: 'qa' | 'prod';
13
+ target: string;
14
+ }) => void;
15
+ };
16
+ };
17
+ }
18
+ }
19
+
20
+ const getScriptSrc = (env: string) => {
21
+ const host = env === 'qa' ? 'js.hsformsqa.net' : 'js.hsforms.net';
22
+ return `//${host}/forms/embed/v2.js`;
23
+ };
24
+
25
+ const LegacyFormIsland = ({ formId }: FormProps) => {
26
+ const portalId = getHubID();
27
+ const resolvedEnv = getHSEnv();
28
+ const rawId = useId();
29
+ const containerId = `hs-legacy-form-${rawId.replace(/:/g, '')}`;
30
+
31
+ useEffect(() => {
32
+ if (!formId || !portalId) {
33
+ return;
34
+ }
35
+
36
+ const scriptSrc = getScriptSrc(resolvedEnv);
37
+
38
+ const createForm = () => {
39
+ window.hbspt?.forms.create({
40
+ portalId,
41
+ formId,
42
+ env: resolvedEnv,
43
+ target: `#${containerId}`,
44
+ });
45
+ };
46
+
47
+ const existingScript = document.querySelector(`script[src="${scriptSrc}"]`);
48
+
49
+ if (existingScript) {
50
+ if (window.hbspt) {
51
+ createForm();
52
+ } else {
53
+ existingScript.addEventListener('load', createForm);
54
+ return () => existingScript.removeEventListener('load', createForm);
55
+ }
56
+ return;
57
+ }
58
+
59
+ const script = document.createElement('script');
60
+ script.src = scriptSrc;
61
+ script.addEventListener('load', createForm);
62
+ document.head.appendChild(script);
63
+
64
+ return () => {
65
+ script.removeEventListener('load', createForm);
66
+ script.remove();
67
+ };
68
+ }, [formId, portalId, resolvedEnv, containerId]);
69
+
70
+ if (!formId || !portalId) {
71
+ return null;
72
+ }
73
+
74
+ return <div id={containerId} />;
75
+ };
76
+
77
+ export default LegacyFormIsland;
@@ -0,0 +1,3 @@
1
+ .form {
2
+ width: var(--hscl-form-width, 100%);
3
+ }
@@ -0,0 +1,21 @@
1
+ import { FormFieldDefaults } from '@hubspot/cms-components/fields';
2
+
3
+ export type FormProps = FormPropsWithField | FormPropsWithoutField;
4
+
5
+ export type FormPropsWithoutField = {
6
+ formField?: never;
7
+ formId: string;
8
+ formVersion: 'v4' | 'v3' | 'v2' | '';
9
+ };
10
+
11
+ export type FormPropsWithField = {
12
+ formField: typeof FormFieldDefaults;
13
+ formId?: never;
14
+ formVersion?: never;
15
+ };
16
+
17
+ export type ContentFieldsProps = {
18
+ formIdLabel?: string;
19
+ formIdName?: string;
20
+ formIdDefault?: typeof FormFieldDefaults;
21
+ };
@@ -109,13 +109,13 @@ Grid/
109
109
 
110
110
  **Purpose:** Wraps grid children to control their precise placement, spanning, and alignment within the Grid container. Can render as any HTML element or custom React component while maintaining grid positioning control.
111
111
 
112
- **Key Feature:** The `as` prop is polymorphic - it accepts both HTML element strings ('div', 'section', etc.) and React component references (Button, Heading, custom components). GridItem handles grid placement while passing through all other props to the underlying component.
112
+ **Key Feature:** The `as` prop is polymorphic - it accepts both HTML element strings ('div', 'section', etc.) and React component references (Button, Text, custom components). GridItem handles grid placement while passing through all other props to the underlying component.
113
113
 
114
114
  **Props:**
115
115
  ```tsx
116
116
  {
117
117
  as?: React.ElementType; // Any HTML element or React component (default: 'div')
118
- // Examples: 'div', 'article', Button, Heading, CustomComponent
118
+ // Examples: 'div', 'article', Button, Text, CustomComponent
119
119
  gridColumn?: string; // Grid column placement (e.g., '1 / 3', 'span 2', '2')
120
120
  gridColumnMd?: string; // Grid column placement at tablet breakpoint (768px+)
121
121
  gridColumnLg?: string; // Grid column placement at desktop breakpoint (1024px+)
@@ -148,7 +148,7 @@ GridItem's `as` prop provides flexibility in rendering while maintaining grid co
148
148
  2. **React Components**: Pass component references directly
149
149
  ```tsx
150
150
  <GridItem as={Button} buttonType="primary">Click me</GridItem>
151
- <GridItem as={Heading} headingLevel="h2">Title</GridItem>
151
+ <GridItem as={Text} fieldPath="title" />
152
152
  ```
153
153
 
154
154
  3. **Prop Pass-Through**: All props beyond GridItem's own props are forwarded to the underlying component
@@ -158,10 +158,8 @@ GridItem's `as` prop provides flexibility in rendering while maintaining grid co
158
158
  Link Button
159
159
  </GridItem>
160
160
 
161
- {/* Heading-specific props are passed through */}
162
- <GridItem as={Heading} headingLevel="h2" displayAs="h1">
163
- Styled Heading
164
- </GridItem>
161
+ {/* Text-specific props are passed through */}
162
+ <GridItem as={Text} fieldPath="sectionTitle" className="custom-text" />
165
163
  ```
166
164
 
167
165
  4. **Grid Positioning**: GridItem handles all grid-specific positioning regardless of the underlying component
@@ -199,7 +197,7 @@ import Grid, { GridItem } from '@hubspot/cms-component-library/Grid';
199
197
  ```tsx
200
198
  import Grid, { GridItem } from '@hubspot/cms-component-library/Grid';
201
199
  import Button from '@hubspot/cms-component-library/Button';
202
- import Heading from '@hubspot/cms-component-library/Heading';
200
+ import Text from '@hubspot/cms-component-library/Text';
203
201
 
204
202
  <Grid templateColumns="repeat(2, 1fr)" gap="16px">
205
203
  {/* GridItem rendering as Button */}
@@ -211,13 +209,9 @@ import Heading from '@hubspot/cms-component-library/Heading';
211
209
  Click Me
212
210
  </GridItem>
213
211
 
214
- {/* GridItem rendering as Heading */}
215
- <GridItem
216
- as={Heading}
217
- headingLevel="h2"
218
- displayAs="h3"
219
- >
220
- Section Title
212
+ {/* GridItem rendering as Text (heading feature set) */}
213
+ <GridItem>
214
+ <Text fieldPath="sectionTitle" />
221
215
  </GridItem>
222
216
 
223
217
  {/* Regular content */}
@@ -255,6 +249,9 @@ import Heading from '@hubspot/cms-component-library/Heading';
255
249
  ### Complex Layout Using Props (NO custom CSS)
256
250
 
257
251
  ```tsx
252
+ import Grid, { GridItem } from '@hubspot/cms-component-library/Grid';
253
+ import Text from '@hubspot/cms-component-library/Text';
254
+
258
255
  {/* Dashboard layout - all positioning via props */}
259
256
  <Grid
260
257
  templateColumns="1fr"
@@ -267,7 +264,7 @@ import Heading from '@hubspot/cms-component-library/Heading';
267
264
  gridColumn="1"
268
265
  gridColumnMd="1 / -1"
269
266
  >
270
- <Heading headingLevel="h1">Dashboard</Heading>
267
+ <Text fieldPath="dashboardTitle" />
271
268
  </GridItem>
272
269
 
273
270
  {/* Sidebar - hidden on mobile, shown on tablet+ */}
@@ -83,7 +83,7 @@ When creating a new component in componentLibrary, ensure:
83
83
  - [ ] Semantic HTML elements used appropriately
84
84
  - [ ] ARIA roles added when needed (e.g., `role="separator"`)
85
85
  - [ ] Icon purpose properly set (SEMANTIC vs DECORATIVE)
86
- - [ ] Heading level vs display separated for proper semantics
86
+ - [ ] Use `Text` with `textFeatureSet="heading"` for editable heading fields rather than raw HTML heading elements
87
87
 
88
88
  ## Reference Examples
89
89
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/cms-component-library",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "HubSpot CMS React component library for building CMS modules",
5
5
  "license": "Apache-2.0",
6
6
  "exports": {
@@ -21,7 +21,7 @@
21
21
  },
22
22
  "type": "module",
23
23
  "dependencies": {
24
- "@hubspot/cms-components": "1.2.17",
24
+ "@hubspot/cms-components": "1.2.19",
25
25
  "sass-embedded": "^1.97.3"
26
26
  },
27
27
  "peerDependencies": {