@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.
- package/components/componentLibrary/Accordion/AccordionTitle/AccordionTitleBase.tsx +45 -0
- package/components/componentLibrary/Accordion/AccordionTitle/index.tsx +17 -30
- package/components/componentLibrary/Accordion/AccordionTitle/islands/AccordionTitleIsland.tsx +29 -0
- package/components/componentLibrary/Button/StyleFields.tsx +8 -8
- package/components/componentLibrary/Button/index.module.scss +24 -27
- package/components/componentLibrary/Button/index.tsx +4 -4
- package/components/componentLibrary/Button/llm.txt +51 -64
- package/components/componentLibrary/Button/stories/Button.AsButton.stories.tsx +2 -2
- package/components/componentLibrary/Button/stories/Button.AsLink.stories.tsx +2 -2
- package/components/componentLibrary/Button/stories/ButtonDecorator.module.scss +19 -23
- package/components/componentLibrary/Button/types.ts +2 -2
- package/components/componentLibrary/Card/StyleFields.tsx +9 -14
- package/components/componentLibrary/Card/index.module.scss +7 -7
- package/components/componentLibrary/Card/index.tsx +8 -13
- package/components/componentLibrary/Card/llm.txt +22 -43
- package/components/componentLibrary/Card/stories/Card.stories.tsx +28 -20
- package/components/componentLibrary/Card/stories/CardDecorator.module.scss +28 -5
- package/components/componentLibrary/Card/types.ts +8 -5
- package/components/componentLibrary/Form/StyleFields.tsx +19 -0
- package/components/componentLibrary/Form/index.tsx +7 -1
- package/components/componentLibrary/Form/islands/FormIsland.tsx +3 -1
- package/components/componentLibrary/Form/islands/LegacyFormIsland.tsx +2 -1
- package/components/componentLibrary/Form/islands/legacyForm.module.css +251 -0
- package/components/componentLibrary/Form/islands/v4Form.module.css +95 -0
- package/components/componentLibrary/Form/llm.txt +184 -0
- package/components/componentLibrary/Form/types.ts +6 -0
- package/components/componentLibrary/Link/ContentFields.tsx +2 -2
- package/components/componentLibrary/Link/StyleFields.tsx +10 -17
- package/components/componentLibrary/Link/index.module.scss +9 -9
- package/components/componentLibrary/Link/index.tsx +3 -8
- package/components/componentLibrary/Link/llm.txt +29 -85
- package/components/componentLibrary/Link/stories/Link.stories.tsx +4 -11
- package/components/componentLibrary/Link/stories/LinkDecorator.module.scss +15 -0
- package/components/componentLibrary/Link/stories/LinkDecorator.tsx +2 -11
- package/components/componentLibrary/Link/types.ts +11 -8
- package/components/componentLibrary/Video/ContentFields.tsx +112 -0
- package/components/componentLibrary/Video/StyleFields.tsx +19 -0
- package/components/componentLibrary/Video/index.tsx +47 -0
- package/components/componentLibrary/Video/islands/HSVideoIsland.tsx +53 -0
- package/components/componentLibrary/Video/serverUtils.ts +41 -0
- package/components/componentLibrary/Video/types.ts +74 -0
- package/components/componentLibrary/_patterns/README.md +11 -7
- package/components/componentLibrary/_patterns/checklist-and-examples.md +8 -0
- package/components/componentLibrary/_patterns/component-structure.md +5 -1
- package/components/componentLibrary/_patterns/field-patterns.md +46 -0
- package/components/componentLibrary/_patterns/island-patterns.md +136 -0
- package/components/componentLibrary/utils/index.ts +1 -0
- 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. **[
|
|
37
|
-
3. **[
|
|
38
|
-
4. **[
|
|
39
|
-
5. **[
|
|
40
|
-
6. **[
|
|
41
|
-
7. **[
|
|
42
|
-
8. **[
|
|
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.
|
|
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.
|
|
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",
|