@hubspot/cms-component-library 0.3.7 → 0.3.9

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 (51) hide show
  1. package/components/componentLibrary/Accordion/AccordionContent/ContentFields.tsx +6 -6
  2. package/components/componentLibrary/Accordion/AccordionContent/index.tsx +3 -1
  3. package/components/componentLibrary/Accordion/AccordionContent/types.ts +2 -3
  4. package/components/componentLibrary/Accordion/AccordionTitle/AccordionTitleBase.tsx +45 -0
  5. package/components/componentLibrary/Accordion/AccordionTitle/ContentFields.tsx +8 -3
  6. package/components/componentLibrary/Accordion/AccordionTitle/index.tsx +18 -25
  7. package/components/componentLibrary/Accordion/AccordionTitle/islands/AccordionTitleIsland.tsx +29 -0
  8. package/components/componentLibrary/Accordion/AccordionTitle/types.ts +1 -0
  9. package/components/componentLibrary/Accordion/llm.txt +20 -13
  10. package/components/componentLibrary/Button/StyleFields.tsx +8 -8
  11. package/components/componentLibrary/Button/index.module.scss +24 -27
  12. package/components/componentLibrary/Button/index.tsx +4 -4
  13. package/components/componentLibrary/Button/llm.txt +51 -64
  14. package/components/componentLibrary/Button/stories/Button.AsButton.stories.tsx +2 -2
  15. package/components/componentLibrary/Button/stories/Button.AsLink.stories.tsx +2 -2
  16. package/components/componentLibrary/Button/stories/ButtonDecorator.module.scss +19 -23
  17. package/components/componentLibrary/Button/types.ts +2 -2
  18. package/components/componentLibrary/Card/StyleFields.tsx +9 -14
  19. package/components/componentLibrary/Card/index.module.scss +7 -7
  20. package/components/componentLibrary/Card/index.tsx +8 -13
  21. package/components/componentLibrary/Card/llm.txt +22 -43
  22. package/components/componentLibrary/Card/stories/Card.stories.tsx +28 -20
  23. package/components/componentLibrary/Card/stories/CardDecorator.module.scss +28 -5
  24. package/components/componentLibrary/Card/types.ts +8 -5
  25. package/components/componentLibrary/Form/StyleFields.tsx +19 -0
  26. package/components/componentLibrary/Form/index.tsx +7 -1
  27. package/components/componentLibrary/Form/islands/FormIsland.tsx +3 -1
  28. package/components/componentLibrary/Form/islands/LegacyFormIsland.tsx +2 -1
  29. package/components/componentLibrary/Form/islands/legacyForm.module.css +251 -0
  30. package/components/componentLibrary/Form/islands/v4Form.module.css +95 -0
  31. package/components/componentLibrary/Form/llm.txt +184 -0
  32. package/components/componentLibrary/Form/types.ts +6 -0
  33. package/components/componentLibrary/Link/ContentFields.tsx +11 -3
  34. package/components/componentLibrary/Link/llm.txt +13 -0
  35. package/components/componentLibrary/Link/types.ts +6 -4
  36. package/components/componentLibrary/Logo/ContentFields.tsx +25 -0
  37. package/components/componentLibrary/Logo/index.tsx +13 -4
  38. package/components/componentLibrary/Logo/types.tsx +15 -0
  39. package/components/componentLibrary/Video/ContentFields.tsx +112 -0
  40. package/components/componentLibrary/Video/StyleFields.tsx +19 -0
  41. package/components/componentLibrary/Video/index.tsx +47 -0
  42. package/components/componentLibrary/Video/islands/HSVideoIsland.tsx +53 -0
  43. package/components/componentLibrary/Video/serverUtils.ts +41 -0
  44. package/components/componentLibrary/Video/types.ts +74 -0
  45. package/components/componentLibrary/_patterns/README.md +11 -7
  46. package/components/componentLibrary/_patterns/checklist-and-examples.md +8 -0
  47. package/components/componentLibrary/_patterns/component-structure.md +5 -1
  48. package/components/componentLibrary/_patterns/field-patterns.md +65 -0
  49. package/components/componentLibrary/_patterns/island-patterns.md +136 -0
  50. package/components/componentLibrary/utils/index.ts +1 -0
  51. package/package.json +4 -3
@@ -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
  };
@@ -0,0 +1,45 @@
1
+ import Text from '../../Text/index.js';
2
+ import styles from './index.module.scss';
3
+ import cx from '../../utils/classname.js';
4
+ import { ChevronIcon, PlusIcon, MinusIcon, CaretIcon } from './icons.js';
5
+ import type { AccordionTitleProps } from './types.js';
6
+
7
+ const AccordionTitleBase = ({
8
+ icon = 'chevron',
9
+ fieldPath,
10
+ className = '',
11
+ style = {},
12
+ children,
13
+ }: AccordionTitleProps) => {
14
+ const combinedClasses = cx(styles.accordionTitle, className);
15
+
16
+ const renderIcon = () => {
17
+ if (icon === 'chevron') return <ChevronIcon />;
18
+
19
+ if (icon === 'plus') {
20
+ return (
21
+ <>
22
+ <PlusIcon />
23
+ <MinusIcon />
24
+ </>
25
+ );
26
+ }
27
+
28
+ if (icon === 'caret') return <CaretIcon />;
29
+
30
+ return null;
31
+ };
32
+
33
+ return (
34
+ <summary className={combinedClasses} style={style}>
35
+ {fieldPath ? (
36
+ <Text fieldPath={fieldPath} className={styles.accordionTitleText} />
37
+ ) : (
38
+ <span className={styles.accordionTitleText}>{children}</span>
39
+ )}
40
+ {renderIcon()}
41
+ </summary>
42
+ );
43
+ };
44
+
45
+ export default AccordionTitleBase;
@@ -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,40 +1,33 @@
1
- import styles from './index.module.scss';
1
+ import { useEditorVariableChecks, Island } from '@hubspot/cms-components';
2
+ // @ts-expect-error -- ?island not typed
3
+ import AccordionTitleIslandModule from './islands/AccordionTitleIsland.js?island';
2
4
  import ContentFields from './ContentFields.js';
3
5
  import StyleFields from './StyleFields.js';
4
- import cx from '../../utils/classname.js';
5
6
  import { AccordionTitleProps } from './types.js';
6
- import { ChevronIcon, PlusIcon, MinusIcon, CaretIcon } from './icons.js';
7
+ import AccordionTitleBase from './AccordionTitleBase.js';
7
8
 
8
9
  const AccordionTitleComponent = ({
9
10
  icon = 'chevron',
11
+ fieldPath,
10
12
  className = '',
11
13
  style = {},
12
14
  children,
13
15
  }: AccordionTitleProps) => {
14
- const combinedClasses = cx(styles.accordionTitle, className);
15
-
16
- const renderIcon = () => {
17
- if (icon === 'chevron') return <ChevronIcon />;
18
-
19
- if (icon === 'plus') {
20
- return (
21
- <>
22
- <PlusIcon />
23
- <MinusIcon />
24
- </>
25
- );
26
- }
27
-
28
- if (icon === 'caret') return <CaretIcon />;
29
-
30
- return null;
31
- };
16
+ const editorChecks = useEditorVariableChecks();
17
+ const isInEditor = editorChecks.is_in_editor && !!fieldPath;
32
18
 
33
19
  return (
34
- <summary className={combinedClasses} style={style}>
35
- <span className={styles.accordionTitleText}>{children}</span>
36
- {renderIcon()}
37
- </summary>
20
+ <>
21
+ <AccordionTitleBase
22
+ icon={icon}
23
+ fieldPath={fieldPath}
24
+ className={className}
25
+ style={style}
26
+ >
27
+ {children}
28
+ </AccordionTitleBase>
29
+ {isInEditor && <Island module={AccordionTitleIslandModule} />}
30
+ </>
38
31
  );
39
32
  };
40
33
 
@@ -0,0 +1,29 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ const AccordionTitleIsland = () => {
4
+ const markerRef = useRef<HTMLSpanElement>(null);
5
+
6
+ useEffect(() => {
7
+ const marker = markerRef.current;
8
+ if (!marker) return;
9
+
10
+ const details = marker.closest('details');
11
+ const summary = details?.querySelector('summary');
12
+ if (!summary) return;
13
+
14
+ const handleClick = (e: MouseEvent) => {
15
+ const target = e.target as HTMLElement;
16
+ const isIconClick = target.closest('svg');
17
+ if (!isIconClick) {
18
+ e.preventDefault();
19
+ }
20
+ };
21
+
22
+ summary.addEventListener('click', handleClick);
23
+ return () => summary.removeEventListener('click', handleClick);
24
+ }, []);
25
+
26
+ return <span ref={markerRef} style={{ display: 'none' }} />;
27
+ };
28
+
29
+ export default AccordionTitleIsland;
@@ -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,10 +1,14 @@
1
- import { ChoiceField, Visibility } from '@hubspot/cms-components/fields';
1
+ import {
2
+ ChoiceField,
3
+ VariantSelectionField,
4
+ Visibility,
5
+ } from '@hubspot/cms-components/fields';
2
6
  import { StyleFieldsProps } from './types.js';
3
7
 
4
8
  const StyleFields = ({
5
9
  buttonVariantLabel = 'Button variant',
6
10
  buttonVariantName = 'buttonVariant',
7
- buttonVariantDefault = 'primary',
11
+ buttonVariantDefault = { variant_name: 'primaryButton' },
8
12
  buttonIconPositionLabel = 'Icon position',
9
13
  buttonIconPositionName = 'buttonIconPosition',
10
14
  buttonIconPositionDefault = 'right',
@@ -18,14 +22,10 @@ const StyleFields = ({
18
22
 
19
23
  return (
20
24
  <>
21
- <ChoiceField
25
+ <VariantSelectionField
22
26
  label={buttonVariantLabel}
23
27
  name={buttonVariantName}
24
- choices={[
25
- ['primary', 'Primary'],
26
- ['secondary', 'Secondary'],
27
- ['tertiary', 'Tertiary'],
28
- ]}
28
+ variantDefinitionName="button"
29
29
  default={buttonVariantDefault}
30
30
  />
31
31
  <ChoiceField
@@ -1,48 +1,45 @@
1
- // CSS variables that can be mapped to theme settings
1
+ // CSS variables provided via theme settings variant system (scoped via data-buttons-variant)
2
2
  .button {
3
3
  display: inline-flex;
4
4
  flex-direction: row;
5
5
  align-items: center;
6
6
  justify-content: center;
7
- text-decoration: none;
8
- gap: var(--hscl-button-gap, 8px);
9
- background-color: var(--hscl-button-backgroundColor);
10
- color: var(--hscl-button-color);
11
- padding: var(--hscl-button-paddingBlock) var(--hscl-button-paddingInline);
12
- border-radius: var(--hscl-button-borderRadius);
13
- border: var(--hscl-button-borderWidth) solid var(--hscl-button-borderColor);
14
- font-size: var(--hscl-button-fontSize);
15
- font-weight: var(--hscl-button-fontWeight);
7
+ gap: 8px;
8
+ background-color: var(--hs-button-backgroundColor);
9
+ color: var(--hs-button-color);
10
+ padding: 12px 24px;
11
+ border-radius: var(--hs-button-borderRadius);
12
+ border: var(--hs-button-border);
13
+ font-size: var(--hs-button-fontSize);
14
+ font-family: var(--hs-button-fontFamily);
15
+ font-style: var(--hs-button-fontStyle);
16
+ font-weight: var(--hs-button-fontWeight);
17
+ text-decoration: var(--hs-button-textDecoration);
16
18
  cursor: pointer;
17
19
  transition: all 0.2s ease;
18
- text-decoration: none;
19
20
 
20
21
  @media (prefers-reduced-motion: reduce) {
21
22
  transition: none;
22
23
  }
23
24
 
24
25
  &:hover {
25
- background-color: var(--hscl-button-backgroundColor-hover, var(--hscl-button-backgroundColor));
26
- color: var(--hscl-button-color-hover, var(--hscl-button-color));
27
- border-width: var(--hscl-button-borderWidth-hover, var(--hscl-button-borderWidth));
28
- border-color: var(--hscl-button-borderColor-hover, var(--hscl-button-borderColor));
29
- text-decoration: none;
26
+ background-color: var(--hs-button-backgroundColor-hover, var(--hs-button-backgroundColor));
27
+ color: var(--hs-button-color-hover, var(--hs-button-color));
28
+ border: var(--hs-button-border-hover, var(--hs-button-border));
30
29
  }
31
30
 
32
31
  &:focus-visible {
33
- background-color: var(--hscl-button-backgroundColor-focus, var(--hscl-button-backgroundColor));
34
- color: var(--hscl-button-color-focus, var(--hscl-button-color));
35
- border-width: var(--hscl-button-borderWidth-focus, var(--hscl-button-borderWidth));
36
- border-color: var(--hscl-button-borderColor-focus, var(--hscl-button-borderColor));
37
- outline: var(--hscl-button-outlineWidth-focus, 2px) solid var(--hscl-button-outlineColor-focus, currentColor);
38
- outline-offset: var(--hscl-button-outlineOffset-focus, 2px);
32
+ background-color: var(--hs-button-backgroundColor-focus, var(--hs-button-backgroundColor));
33
+ color: var(--hs-button-color-focus, var(--hs-button-color));
34
+ border: var(--hs-button-border-focus, var(--hs-button-border));
35
+ outline: 2px solid;
36
+ outline-offset: 2px;
39
37
  }
40
38
 
41
39
  &:active {
42
- background-color: var(--hscl-button-backgroundColor-active, var(--hscl-button-backgroundColor));
43
- color: var(--hscl-button-color-active, var(--hscl-button-color));
44
- border-width: var(--hscl-button-borderWidth-active, var(--hscl-button-borderWidth));
45
- border-color: var(--hscl-button-borderColor-active, var(--hscl-button-borderColor));
40
+ background-color: var(--hs-button-backgroundColor-active, var(--hs-button-backgroundColor));
41
+ color: var(--hs-button-color-active, var(--hs-button-color));
42
+ border: var(--hs-button-border-active, var(--hs-button-border));
46
43
  }
47
44
 
48
45
  &:disabled {
@@ -54,6 +51,6 @@
54
51
  display: inline-flex;
55
52
  align-items: center;
56
53
  justify-content: center;
57
- fill: var(--hscl-button-icon-fill);
54
+ fill: currentColor;
58
55
  }
59
56
  }
@@ -13,16 +13,16 @@ import {
13
13
 
14
14
  const ButtonComponent = ({
15
15
  buttonType = 'link',
16
- variant = 'primary',
16
+ variant = 'primaryButton',
17
17
  className = '',
18
18
  style = {},
19
19
  ...rest
20
20
  }: ButtonProps) => {
21
- const variantClass = styles[`hscl-button-${variant}`]; // !todo: not used atm but keeping for when we need to add variant system.
22
- const defaultClasses = cx(styles.button, variantClass);
21
+ const defaultClasses = cx(styles.button, className);
23
22
  const sharedProps = {
24
- className: cx(defaultClasses, className),
23
+ className: defaultClasses,
25
24
  style: style,
25
+ 'data-buttons-variant': variant,
26
26
  };
27
27
 
28
28
  const RenderWithIcon = ({