@hubspot/cms-component-library 0.3.8 → 0.3.10

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 (48) hide show
  1. package/components/componentLibrary/Accordion/AccordionTitle/AccordionTitleBase.tsx +45 -0
  2. package/components/componentLibrary/Accordion/AccordionTitle/index.tsx +17 -30
  3. package/components/componentLibrary/Accordion/AccordionTitle/islands/AccordionTitleIsland.tsx +29 -0
  4. package/components/componentLibrary/Button/StyleFields.tsx +8 -8
  5. package/components/componentLibrary/Button/index.module.scss +24 -27
  6. package/components/componentLibrary/Button/index.tsx +4 -4
  7. package/components/componentLibrary/Button/llm.txt +51 -64
  8. package/components/componentLibrary/Button/stories/Button.AsButton.stories.tsx +2 -2
  9. package/components/componentLibrary/Button/stories/Button.AsLink.stories.tsx +2 -2
  10. package/components/componentLibrary/Button/stories/ButtonDecorator.module.scss +19 -23
  11. package/components/componentLibrary/Button/types.ts +2 -2
  12. package/components/componentLibrary/Card/StyleFields.tsx +9 -14
  13. package/components/componentLibrary/Card/index.module.scss +7 -7
  14. package/components/componentLibrary/Card/index.tsx +8 -13
  15. package/components/componentLibrary/Card/llm.txt +22 -43
  16. package/components/componentLibrary/Card/stories/Card.stories.tsx +28 -20
  17. package/components/componentLibrary/Card/stories/CardDecorator.module.scss +28 -5
  18. package/components/componentLibrary/Card/types.ts +8 -5
  19. package/components/componentLibrary/Form/StyleFields.tsx +19 -0
  20. package/components/componentLibrary/Form/index.tsx +7 -1
  21. package/components/componentLibrary/Form/islands/FormIsland.tsx +3 -1
  22. package/components/componentLibrary/Form/islands/LegacyFormIsland.tsx +2 -1
  23. package/components/componentLibrary/Form/islands/legacyForm.module.css +251 -0
  24. package/components/componentLibrary/Form/islands/v4Form.module.css +95 -0
  25. package/components/componentLibrary/Form/llm.txt +184 -0
  26. package/components/componentLibrary/Form/types.ts +6 -0
  27. package/components/componentLibrary/Link/ContentFields.tsx +2 -2
  28. package/components/componentLibrary/Link/StyleFields.tsx +10 -17
  29. package/components/componentLibrary/Link/index.module.scss +9 -9
  30. package/components/componentLibrary/Link/index.tsx +3 -8
  31. package/components/componentLibrary/Link/llm.txt +29 -85
  32. package/components/componentLibrary/Link/stories/Link.stories.tsx +4 -11
  33. package/components/componentLibrary/Link/stories/LinkDecorator.module.scss +15 -0
  34. package/components/componentLibrary/Link/stories/LinkDecorator.tsx +2 -11
  35. package/components/componentLibrary/Link/types.ts +11 -8
  36. package/components/componentLibrary/Video/ContentFields.tsx +112 -0
  37. package/components/componentLibrary/Video/StyleFields.tsx +19 -0
  38. package/components/componentLibrary/Video/index.tsx +47 -0
  39. package/components/componentLibrary/Video/islands/HSVideoIsland.tsx +53 -0
  40. package/components/componentLibrary/Video/serverUtils.ts +41 -0
  41. package/components/componentLibrary/Video/types.ts +74 -0
  42. package/components/componentLibrary/_patterns/README.md +11 -7
  43. package/components/componentLibrary/_patterns/checklist-and-examples.md +8 -0
  44. package/components/componentLibrary/_patterns/component-structure.md +5 -1
  45. package/components/componentLibrary/_patterns/field-patterns.md +46 -0
  46. package/components/componentLibrary/_patterns/island-patterns.md +136 -0
  47. package/components/componentLibrary/utils/index.ts +1 -0
  48. package/package.json +4 -3
@@ -19,6 +19,9 @@ React-related code (components, JSX-returning functions, callbacks) uses arrow f
19
19
  ### 🏷️ [Prop Naming Patterns](./prop-naming-patterns.md)
20
20
  Standardized naming conventions for component props to ensure consistency across the library.
21
21
 
22
+ ### 🏝️ [Island Patterns](./island-patterns.md)
23
+ When and how to use islands for client-side interactivity, file structure, wrapper/island split, and multiple islands.
24
+
22
25
  ### 📋 [Field Patterns](./field-patterns.md)
23
26
  Field components, property ordering, visibility rules, and nested patterns.
24
27
 
@@ -33,10 +36,11 @@ Comprehensive component creation checklist, complete reference examples, step-by
33
36
  Reference these pattern files in order when building a new component:
34
37
 
35
38
  1. **[Component Structure](./component-structure.md)** - Set up files and basic structure
36
- 2. **[Function Declaration Patterns](./function-declaration-patterns.md)** - Use arrow functions for React code
37
- 3. **[TypeScript Patterns](./typescript-patterns.md)** - Define types in `types.ts`
38
- 4. **[Prop Naming Patterns](./prop-naming-patterns.md)** - Name props consistently
39
- 5. **[CSS Patterns](./css-patterns.md)** - Create styles with proper variable naming
40
- 6. **[Field Patterns](./field-patterns.md)** - Add ContentFields and StyleFields (if needed)
41
- 7. **[Storybook Patterns](./storybook-patterns.md)** - Create stories for documentation (optional)
42
- 8. **[LLM Documentation Template](./llm-txt.template.md)** - Create `llm.txt` for AI/LLM consumption
39
+ 2. **[Island Patterns](./island-patterns.md)** - If the component needs client-side interactivity, set up the island split
40
+ 3. **[Function Declaration Patterns](./function-declaration-patterns.md)** - Use arrow functions for React code
41
+ 4. **[TypeScript Patterns](./typescript-patterns.md)** - Define types in `types.ts`
42
+ 5. **[Prop Naming Patterns](./prop-naming-patterns.md)** - Name props consistently
43
+ 6. **[CSS Patterns](./css-patterns.md)** - Create styles with proper variable naming
44
+ 7. **[Field Patterns](./field-patterns.md)** - Add ContentFields and StyleFields (if needed)
45
+ 8. **[Storybook Patterns](./storybook-patterns.md)** - Create stories for documentation (optional)
46
+ 9. **[LLM Documentation Template](./llm-txt.template.md)** - Create `llm.txt` for AI/LLM consumption
@@ -79,6 +79,14 @@ When creating a new component in componentLibrary, ensure:
79
79
  - [ ] Additional mocks created in `.storybook/mocks/` if needed
80
80
  - [ ] Mock behavior documented in argTypes descriptions
81
81
 
82
+ ### Islands (if component needs client-side interactivity)
83
+ - [ ] Interactive logic lives in `islands/ComponentNameIsland.tsx` with a default export
84
+ - [ ] Wrapper `index.tsx` imports island with `?island` suffix and `@ts-expect-error` comment
85
+ - [ ] Wrapper renders `<Island module={...} />` from `@hubspot/cms-components`
86
+ - [ ] Server-side data (if any) fetched in the wrapper and passed as props through the Island component
87
+ - [ ] Island styles live either in `../index.module.scss` (shared with wrapper) or a dedicated `islands/index.module.scss`
88
+ - [ ] Field components (ContentFields, StyleFields) attached to the wrapper, not the island
89
+
82
90
  ### Accessibility
83
91
  - [ ] Semantic HTML elements used appropriately
84
92
  - [ ] ARIA roles added when needed (e.g., `role="separator"`)
@@ -8,16 +8,20 @@ All components follow a consistent file structure:
8
8
 
9
9
  ```
10
10
  ComponentName/
11
- ├── index.tsx # Main component implementation
11
+ ├── index.tsx # Main component implementation (or island wrapper — see below)
12
12
  ├── index.module.scss # Component styles
13
13
  ├── ContentFields.tsx # Content field definitions (optional)
14
14
  ├── StyleFields.tsx # Style field definitions (optional)
15
15
  ├── types.ts # TypeScript type definitions
16
+ ├── islands/ # Client-side interactive implementations (optional — see Island Patterns)
17
+ │ └── ComponentNameIsland.tsx
16
18
  └── stories/ # Storybook stories (these can be created after the component is reviewed and merged)
17
19
  ├── ComponentName.stories.tsx
18
20
  └── ComponentNameDecorator.tsx
19
21
  ```
20
22
 
23
+ **Note:** If a component requires client-side interactivity (state, event handlers, browser APIs), the interactive logic lives in an `islands/` subdirectory and the `index.tsx` becomes a wrapper that renders `<Island module={...} />`. See [Island Patterns](./island-patterns.md) for full details.
24
+
21
25
  **Note:** Complex components with multiple variants (like Button) should have separate story files per variant (e.g., `Button.AsButton.stories.tsx`, `Button.AsLink.stories.tsx`).
22
26
 
23
27
  ## Class Name Building Pattern
@@ -222,6 +222,52 @@ Link component example:
222
222
 
223
223
  **Key Pattern:** Look up visibility by the configurable name prop: `visibility={fieldVisibility?.[linkName]}`
224
224
 
225
+ #### Type-Safe fieldVisibility with Discriminated Unions
226
+
227
+ Use a discriminated union to ensure `fieldVisibility` keys always match the runtime field name. When `linkName` is omitted, keys are locked to the default. When provided, keys must match it.
228
+
229
+ **Single-field component:**
230
+ ```typescript
231
+ // types.ts
232
+ export type ContentFieldsProps<TLinkName extends string = 'link'> = {
233
+ linkLabel?: string;
234
+ linkDefault?: typeof LinkFieldDefaults;
235
+ } & (
236
+ | { linkName?: never; fieldVisibility?: Partial<Record<'link', Visibility>> }
237
+ | { linkName: TLinkName; fieldVisibility?: Partial<Record<TLinkName, Visibility>> }
238
+ );
239
+ ```
240
+
241
+ This prevents TS from inferring `TLinkName` from `fieldVisibility` when `linkName` is omitted:
242
+ ```typescript
243
+ // linkName omitted — fieldVisibility keys must be 'link'
244
+ <Link.ContentFields
245
+ fieldVisibility={{ wrongKey: { ... } }} // TS error: 'wrongKey' is not assignable to 'link'
246
+ />
247
+
248
+ // linkName provided — fieldVisibility keys must match
249
+ <Link.ContentFields
250
+ linkName="footerLink"
251
+ fieldVisibility={{ link: { ... } }} // TS error: 'link' is not assignable to 'footerLink'
252
+ />
253
+ ```
254
+
255
+ **Multi-field component (e.g. Button):**
256
+ ```typescript
257
+ export type ContentFieldsProps<
258
+ TTextName extends string = 'buttonText',
259
+ TLinkName extends string = 'buttonLink',
260
+ > = {
261
+ buttonTextLabel?: string;
262
+ buttonLinkLabel?: string;
263
+ } & (
264
+ | { buttonTextName?: never; buttonLinkName?: never;
265
+ fieldVisibility?: Partial<Record<'buttonText' | 'buttonLink', Visibility>> }
266
+ | { buttonTextName: TTextName; buttonLinkName: TLinkName;
267
+ fieldVisibility?: Partial<Record<TTextName | TLinkName, Visibility>> }
268
+ );
269
+ ```
270
+
225
271
  ## Nested Component Fields Pattern
226
272
 
227
273
  **Pattern for nested component fields:**
@@ -0,0 +1,136 @@
1
+ # Island Patterns
2
+
3
+ This document covers when and how to use islands within component directories. Islands enable client-side interactivity (state, event handlers, browser APIs) while keeping the component's public API identical to non-island components.
4
+
5
+ ## When to Use an Island
6
+
7
+ Create an island when a component requires **client-side interactivity** — anything that needs JavaScript to run in the browser after the initial server render:
8
+
9
+ - State management (`useState`, `useReducer`)
10
+ - Event handlers (`onClick`, `onSubmit`, `onChange`)
11
+ - Browser APIs (`window`, `document`, DOM measurements)
12
+ - Effects (`useEffect`, `useLayoutEffect`)
13
+
14
+ **Examples in this library:** LanguageSwitcher (drawer toggle), Form (form submission), NavigationMenu and VerticalMenu (menu interactions).
15
+
16
+ If a component is purely presentational (no state, no handlers), it does **not** need an island.
17
+
18
+ ## File Structure
19
+
20
+ ```
21
+ ComponentName/
22
+ ├── index.tsx # Wrapper — imports island, renders <Island module={...} />
23
+ ├── index.module.scss # Styles (may be shared with island — see Key details below)
24
+ ├── ContentFields.tsx # Content field definitions
25
+ ├── StyleFields.tsx # Style field definitions
26
+ ├── types.ts # TypeScript type definitions
27
+ ├── islands/
28
+ │ └── ComponentNameIsland.tsx # Interactive implementation (default export)
29
+ └── stories/
30
+ └── ComponentName.stories.tsx
31
+ ```
32
+
33
+ The `islands/` subdirectory lives inside the component directory. The island file is named `ComponentNameIsland.tsx`.
34
+
35
+ ## The Wrapper (`index.tsx`)
36
+
37
+ The wrapper is the component's public entry point. It imports the island, wraps it in the `<Island>` component from `@hubspot/cms-components`, and attaches field components via the standard compound component pattern.
38
+
39
+ ```typescript
40
+ // @ts-expect-error -- ?island not typed
41
+ import ComponentNameIsland from './islands/ComponentNameIsland.js?island';
42
+ import { Island } from '@hubspot/cms-components';
43
+ import ContentFields from './ContentFields.js';
44
+ import StyleFields from './StyleFields.js';
45
+ import type { ComponentNameProps } from './types.js';
46
+
47
+ const ComponentNameComponent = (props: ComponentNameProps) => {
48
+ return <Island module={ComponentNameIsland} {...props} />;
49
+ };
50
+
51
+ type ComponentNameComponentType = typeof ComponentNameComponent & {
52
+ ContentFields: typeof ContentFields;
53
+ StyleFields: typeof StyleFields;
54
+ };
55
+
56
+ const ComponentName = ComponentNameComponent as ComponentNameComponentType;
57
+ ComponentName.ContentFields = ContentFields;
58
+ ComponentName.StyleFields = StyleFields;
59
+
60
+ export default ComponentName;
61
+ ```
62
+
63
+ **Key details:**
64
+
65
+ - The `?island` query parameter on the import tells the CMS build system to treat this module as an island entry point.
66
+ - `@ts-expect-error` is required because TypeScript doesn't understand the `?island` suffix.
67
+ - The wrapper can perform **server-side work** before passing props to the island. For example, LanguageSwitcher calls `useLanguageVariants()` here and passes the result as a prop.
68
+ - Field components (ContentFields, StyleFields) are attached to the **wrapper**, not the island.
69
+
70
+ ## The Island File (`islands/ComponentNameIsland.tsx`)
71
+
72
+ The island file is a standard React component with a default export. It contains all the interactive logic.
73
+
74
+ ```typescript
75
+ import styles from '../index.module.scss';
76
+ import cx from '../../utils/classname.js';
77
+ import type { ComponentNameIslandProps } from '../types.js';
78
+
79
+ const ComponentNameIsland = ({
80
+ className = '',
81
+ style = {},
82
+ ...rest
83
+ }: ComponentNameIslandProps) => {
84
+ // Client-side state, handlers, effects go here
85
+
86
+ const combinedClasses = cx(styles.componentName, className);
87
+
88
+ return (
89
+ <div className={combinedClasses} style={style}>
90
+ {/* Interactive UI */}
91
+ </div>
92
+ );
93
+ };
94
+
95
+ export default ComponentNameIsland;
96
+ ```
97
+
98
+ **Key details:**
99
+
100
+ - The island can either share the parent component's stylesheet (`../index.module.scss`) or have its own stylesheet (`./index.module.scss`) inside the `islands/` directory — pick whichever fits the component.
101
+ - The island can import and compose other library components (Button, Drawer, Divider, etc.).
102
+ - Island props may differ from the wrapper's props. Define separate types in `types.ts` (e.g., `ComponentNameProps` for the wrapper, `ComponentNameIslandProps` for the island).
103
+
104
+ ## Multiple Islands
105
+
106
+ A component can contain multiple island files, selected at render time based on props. The Form component demonstrates this:
107
+
108
+ ```typescript
109
+ // @ts-expect-error -- ?island not typed
110
+ import FormIsland from './islands/FormIsland.js?island';
111
+ // @ts-expect-error -- ?island not typed
112
+ import LegacyFormIsland from './islands/LegacyFormIsland.js?island';
113
+ import { Island } from '@hubspot/cms-components';
114
+
115
+ const FormComponent = ({ formVersion, ...rest }: FormProps) => {
116
+ const FormModule = formVersion === 'v4' ? FormIsland : LegacyFormIsland;
117
+ return <Island module={FormModule} {...rest} />;
118
+ };
119
+ ```
120
+
121
+ Each island file follows the same pattern — default export a React component.
122
+
123
+ ## Key Principle
124
+
125
+ **Modules consuming an island component don't need to know it's an island.** The wrapper fully abstracts the island boundary. Modules use island components exactly like any other component:
126
+
127
+ ```tsx
128
+ // Module usage — identical whether the component uses an island or not
129
+ <ComponentName someProp="value" />
130
+
131
+ // Fields usage — same compound component pattern
132
+ <ComponentName.ContentFields />
133
+ <ComponentName.StyleFields />
134
+ ```
135
+
136
+ This means converting a presentational component to an island component (or vice versa) doesn't require changes to any consuming modules.
@@ -0,0 +1 @@
1
+ export { fetchHSVideoServerSide } from '../Video/serverUtils.js';
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@hubspot/cms-component-library",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "HubSpot CMS React component library for building CMS modules",
5
5
  "license": "Apache-2.0",
6
6
  "exports": {
7
7
  "./VerticalMenu": "./components/componentLibrary/Menu/VerticalMenu/index.tsx",
8
8
  "./NavigationMenu": "./components/componentLibrary/Menu/NavigationMenu/index.tsx",
9
+ "./utils": "./components/componentLibrary/utils/index.ts",
9
10
  "./*": "./components/componentLibrary/*/index.tsx"
10
11
  },
11
12
  "files": [
@@ -21,7 +22,8 @@
21
22
  },
22
23
  "type": "module",
23
24
  "dependencies": {
24
- "@hubspot/cms-components": "1.2.19",
25
+ "@hubspot/cms-components": "1.2.23",
26
+ "@hubspot/video-player-core": "0.1.21",
25
27
  "sass-embedded": "^1.97.3"
26
28
  },
27
29
  "peerDependencies": {
@@ -30,7 +32,6 @@
30
32
  "devDependencies": {
31
33
  "@types/node": "^20.0.0",
32
34
  "@vitejs/plugin-react": "4.6.0",
33
- "@hubspot/cms-dev-server": "^1.0.0",
34
35
  "typescript": "^5.0.0"
35
36
  },
36
37
  "author": "content-assets",