@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
@@ -0,0 +1,251 @@
1
+ .legacyFormStyles {
2
+ /* Form container */
3
+ :global(.hs-form) {
4
+ padding: 32px;
5
+ border-color: #ff00ff;
6
+ border-radius: 8px;
7
+ border-style: solid;
8
+ border-width: 4px;
9
+ background: #ffe0f0;
10
+
11
+ /* Form field spacing */
12
+ :global(.hs-form-field) {
13
+ margin-block-end: 24px;
14
+ }
15
+
16
+ /* Textarea */
17
+ :global(textarea) {
18
+ position: relative;
19
+ height: 160px;
20
+ border-radius: 3px;
21
+
22
+ &::-webkit-resizer {
23
+ display: none;
24
+ }
25
+ }
26
+
27
+ /* Textarea drag icon */
28
+ :global(.hs_multi_line_field .input) {
29
+ position: relative;
30
+
31
+ &::after {
32
+ position: absolute;
33
+ right: 8px;
34
+ bottom: 8px;
35
+ content: url("data:image/svg+xml,%3Csvg width='11' height='12' viewBox='0 0 22 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cline y1='-1' x2='29.5206' y2='-1' transform='matrix(-0.666795 0.745241 -0.806754 -0.590888 19.6843 0)' stroke='%23303F59' stroke-width='2'/%3E%3Cpath d='M21.0005 9.99756L10.5005 21.9976' stroke='%23303F59' stroke-width='2'/%3E%3C/svg%3E%0A");
36
+ pointer-events: none;
37
+ }
38
+ }
39
+
40
+ /* Select dropdown - remove native appearance */
41
+ :global(select) {
42
+ -webkit-appearance: none;
43
+ -moz-appearance: none;
44
+ appearance: none;
45
+ }
46
+
47
+ /* Select dropdown icon */
48
+ :global(.hs-fieldtype-select .input) {
49
+ position: relative;
50
+
51
+ &::after {
52
+ position: absolute;
53
+ top: 50%;
54
+ right: 16px;
55
+ content: url("data:image/svg+xml,%3Csvg width='24' height='25' viewBox='0 0 24 25' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10.9407 19.5595C11.5267 20.1454 12.4782 20.1454 13.0642 19.5595L22.0642 10.5595C22.6501 9.97354 22.6501 9.02197 22.0642 8.43604C21.4782 7.8501 20.5267 7.8501 19.9407 8.43604L12.0001 16.3767L4.05947 8.44072C3.47354 7.85478 2.52197 7.85478 1.93604 8.44072C1.3501 9.02666 1.3501 9.97822 1.93604 10.5642L10.936 19.5642L10.9407 19.5595Z' fill='%2309152B'/%3E%3C/svg%3E%0A");
56
+ pointer-events: none;
57
+ transform: translateY(-50%);
58
+ }
59
+ }
60
+
61
+ /* Datepicker icon */
62
+ :global(.hs-dateinput) {
63
+ position: relative;
64
+
65
+ &::before {
66
+ position: absolute;
67
+ top: 50%;
68
+ right: 16px;
69
+ content: url("data:image/svg+xml,%3Csvg width='24' height='29' viewBox='0 0 24 29' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0_3812_12272)'%3E%3Cpath d='M8.14286 2.07136C8.14286 1.35886 7.56964 0.785645 6.85714 0.785645C6.14464 0.785645 5.57143 1.35886 5.57143 2.07136V4.21422H3.42857C1.5375 4.21422 0 5.75172 0 7.64279V8.49993V11.0714V24.7856C0 26.6767 1.5375 28.2142 3.42857 28.2142H20.5714C22.4625 28.2142 24 26.6767 24 24.7856V11.0714V8.49993V7.64279C24 5.75172 22.4625 4.21422 20.5714 4.21422H18.4286V2.07136C18.4286 1.35886 17.8554 0.785645 17.1429 0.785645C16.4304 0.785645 15.8571 1.35886 15.8571 2.07136V4.21422H8.14286V2.07136ZM2.57143 11.0714H21.4286V24.7856C21.4286 25.2571 21.0429 25.6428 20.5714 25.6428H3.42857C2.95714 25.6428 2.57143 25.2571 2.57143 24.7856V11.0714Z' fill='%2309152B'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_3812_12272'%3E%3Crect width='24' height='27.4286' fill='white' transform='translate(0 0.785645)'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E%0A");
70
+ pointer-events: none;
71
+ transform: translateY(-50%);
72
+ }
73
+ }
74
+
75
+ /* Placeholder text color */
76
+ :global(::-moz-placeholder) {
77
+ color: #ff6600;
78
+ }
79
+
80
+ :global(::placeholder) {
81
+ color: #ff6600;
82
+ }
83
+
84
+ /* Checkbox and radio list reset */
85
+ :global(.inputs-list) {
86
+ padding: 0;
87
+ margin: 0;
88
+ list-style: none;
89
+
90
+ :global(li) {
91
+ display: block;
92
+ margin-block-end: 16px;
93
+
94
+ &:last-of-type {
95
+ margin-block-end: 0;
96
+ }
97
+ }
98
+
99
+ :global(:is(input, span)) {
100
+ vertical-align: middle;
101
+ }
102
+ }
103
+
104
+ /* Help text / legend */
105
+ :global(legend) {
106
+ color: #ff6600;
107
+ margin-block-end: 8px;
108
+ }
109
+
110
+ /* Rich text within form */
111
+ :global(.hs-richtext) {
112
+ color: #9900cc;
113
+
114
+ :global(img) {
115
+ height: auto;
116
+ max-width: 100% !important;
117
+ }
118
+ }
119
+
120
+ /* Error state - input border */
121
+ :global(.hs-input.error) {
122
+ border-color: #ff0000;
123
+ }
124
+
125
+ /* Error messages */
126
+ :global(.hs-error-msg),
127
+ :global(.hs-error-msgs) {
128
+ color: #ff0000;
129
+ margin-block-start: 4px;
130
+ }
131
+
132
+ /* File upload and fieldset max width */
133
+ :global(.hs-form-field.hs-fieldtype-file .hs-input),
134
+ :global(fieldset) {
135
+ max-width: 100% !important;
136
+ }
137
+ }
138
+
139
+ /* Labels - targets both form types */
140
+ :global(:is(.hs-form, .hs-elevate-system-form)) :global(label) {
141
+ display: block;
142
+ color: #cc0066;
143
+ font-family: sans-serif;
144
+ font-size: 0.875rem;
145
+ font-weight: 600;
146
+ margin-block-end: 8px;
147
+ }
148
+
149
+ /* Text inputs, selects, textareas - targets both form types */
150
+ :global(:is(.hs-form, .hs-elevate-system-form)) :global(input[type='text']),
151
+ :global(:is(.hs-form, .hs-elevate-system-form)) :global(input[type='email']),
152
+ :global(:is(.hs-form, .hs-elevate-system-form)) :global(input[type='password']),
153
+ :global(:is(.hs-form, .hs-elevate-system-form)) :global(input[type='tel']),
154
+ :global(:is(.hs-form, .hs-elevate-system-form)) :global(input[type='number']),
155
+ :global(:is(.hs-form, .hs-elevate-system-form)) :global(input[type='search']),
156
+ :global(:is(.hs-form, .hs-elevate-system-form)) :global(select),
157
+ :global(:is(.hs-form, .hs-elevate-system-form)) :global(textarea) {
158
+ width: 100% !important;
159
+ border-radius: 6px;
160
+ border-top: 3px solid #00ccff;
161
+ border-right: 3px solid #00ccff;
162
+ border-bottom: 3px solid #00ccff;
163
+ border-left: 3px solid #00ccff;
164
+ background-color: #e0ffff;
165
+ color: #003300;
166
+ padding-block: 12px;
167
+ padding-inline: 16px;
168
+ }
169
+
170
+ /* Checkbox and radio sizing + fill - targets both form types */
171
+ :global(:is(.hs-form, .hs-elevate-system-form)) :global(input[type='checkbox']),
172
+ :global(:is(.hs-form, .hs-elevate-system-form)) :global(input[type='radio']) {
173
+ height: 24px;
174
+ width: 24px !important;
175
+ accent-color: #00cc00;
176
+ cursor: pointer;
177
+ margin-inline-end: 12px;
178
+ }
179
+
180
+ /* Submit button - targets both form types */
181
+ :global(:is(.hs-form, .hs-elevate-system-form)) :global(:is(.hs-button, input[type='submit'])) {
182
+ display: inline-block;
183
+ width: 100%;
184
+ border-color: #ff6600;
185
+ border-radius: 6px;
186
+ border-style: solid;
187
+ border-width: 3px;
188
+ background-color: #ff6600;
189
+ color: #ffffff;
190
+ cursor: pointer;
191
+ font-family: sans-serif;
192
+ font-size: 1rem;
193
+ font-weight: 600;
194
+ padding-block: 12px;
195
+ padding-inline: 24px;
196
+ text-align: center;
197
+ text-decoration: none;
198
+ transition: all 0.15s linear;
199
+ white-space: normal !important;
200
+
201
+ &:hover,
202
+ &:focus {
203
+ border-color: #cc5200;
204
+ background-color: #cc5200;
205
+ color: #ffffff;
206
+ text-decoration: none;
207
+ }
208
+ }
209
+
210
+ /* International phone field layout */
211
+ :global(.hs-input.hs-fieldtype-intl-phone) {
212
+ display: flex;
213
+ width: 100% !important;
214
+ flex-direction: row;
215
+ flex-wrap: wrap;
216
+ align-items: center;
217
+ justify-content: flex-start;
218
+ gap: 8px;
219
+
220
+ > :global(input) {
221
+ flex: 1 0 calc(70% - 8px) !important;
222
+ }
223
+
224
+ > :global(select) {
225
+ flex: 1 0 30% !important;
226
+ }
227
+
228
+ @media (width <= 600px) {
229
+ display: flex;
230
+ width: 100%;
231
+ flex-direction: column;
232
+ gap: 8px;
233
+
234
+ > :global(select),
235
+ > :global(input) {
236
+ min-width: 100%;
237
+ flex: 1 1 100%;
238
+ }
239
+
240
+ > :global(select) {
241
+ padding-inline: 16px;
242
+ }
243
+ }
244
+ }
245
+
246
+
247
+ :global(.grecaptcha-badge) {
248
+ margin-block: 0 !important;
249
+ margin-inline: auto !important;
250
+ }
251
+ }
@@ -0,0 +1,95 @@
1
+ .v4FormStyles {
2
+ --hsf-global__font-size: 16px;
3
+ --hsf-global__color: #1a0033;
4
+ --hsf-global-error__color: #ff0000;
5
+
6
+ --hsf-background__background-color: #ffe0f0;
7
+ --hsf-background__padding: 32px;
8
+ --hsf-background__border-style: solid;
9
+ --hsf-background__border-color: #ff00ff;
10
+ --hsf-background__border-radius: 8px;
11
+ --hsf-background__border-width: 4px;
12
+
13
+ --hsf-heading__font-family: inherit;
14
+ --hsf-heading__color: #9900cc;
15
+ --hsf-heading__text-shadow: 1px 1px 2px #ff00ff;
16
+ --hsf-richtext__font-family: inherit;
17
+ --hsf-richtext__font-size: 14px;
18
+ --hsf-richtext__color: #9900cc;
19
+
20
+ --hsf-field-label__font-family: inherit;
21
+ --hsf-field-label__font-size: 0.875rem;
22
+ --hsf-field-label__color: #cc0066;
23
+ --hsf-field-label-requiredindicator__color: #ff0000;
24
+ --hsf-field-description__font-family: inherit;
25
+ --hsf-field-description__color: #6600cc;
26
+ --hsf-field-footer__font-family: inherit;
27
+ --hsf-field-footer__color: #6600cc;
28
+ --hsf-erroralert__font-family: inherit;
29
+ --hsf-erroralert__color: #ff0000;
30
+
31
+ --hsf-field-input__font-family: inherit;
32
+ --hsf-field-input__background-color: #e0ffff;
33
+ --hsf-field-input__placeholder-color: #ff6600;
34
+ --hsf-field-input__border-color: #00ccff;
35
+ --hsf-field-input__border-width: 3px;
36
+ --hsf-field-input__border-style: solid;
37
+ --hsf-field-input__border-radius: 6px;
38
+ --hsf-field-input__padding: 12px 16px;
39
+ --hsf-field-input__color: #003300;
40
+
41
+ --hsf-field-textarea__font-family: inherit;
42
+ --hsf-field-textarea__color: #003300;
43
+ --hsf-field-textarea__background-color: #e0ffff;
44
+ --hsf-field-textarea__border-color: #00ccff;
45
+ --hsf-field-textarea__border-style: solid;
46
+ --hsf-field-textarea__border-radius: 6px;
47
+ --hsf-field-textarea__padding: 12px 16px;
48
+
49
+ --hsf-field-checkbox__padding: 12px;
50
+ --hsf-field-checkbox__background-color: #ffffff;
51
+ --hsf-field-checkbox__color: #00cc00;
52
+ --hsf-field-checkbox__border-color: #00ccff;
53
+ --hsf-field-checkbox__border-width: 2px;
54
+ --hsf-field-checkbox__border-style: solid;
55
+
56
+ --hsf-field-radio__padding: 12px;
57
+ --hsf-field-radio__background-color: #ffffff;
58
+ --hsf-field-radio__color: #00cc00;
59
+ --hsf-field-radio__border-color: #00ccff;
60
+ --hsf-field-radio__border-width: 2px;
61
+ --hsf-field-radio__border-style: solid;
62
+
63
+ --hsf-row__vertical-spacing: 24px;
64
+ --hsf-row__horizontal-spacing: 16px;
65
+ --hsf-module__vertical-spacing: 16px;
66
+
67
+ --hsf-button__width: 100%;
68
+ --hsf-button__font-family: inherit;
69
+ --hsf-button__font-size: 1rem;
70
+ --hsf-button__font-weight: 700;
71
+ --hsf-button__color: #ffffff;
72
+ --hsf-button__background-color: #ff6600;
73
+ --hsf-button__background-image: none;
74
+ --hsf-button__border-radius: 6px;
75
+ --hsf-button__border-width: 3px;
76
+ --hsf-button__border-style: solid;
77
+ --hsf-button__border-color: #ff6600;
78
+ --hsf-button__padding: 12px 24px;
79
+ --hsf-button__box-shadow: 0 4px 8px rgba(255, 102, 0, 0.4);
80
+ --hsf-button--hover__color: #ffffff;
81
+ --hsf-button--hover__background-color: #cc5200;
82
+ --hsf-button--hover__border-color: #cc5200;
83
+ --hsf-button--focus__color: #ffffff;
84
+ --hsf-button--focus__background-color: #cc5200;
85
+ --hsf-button--focus__border-color: #ff00ff;
86
+
87
+ --hsf-progressbar__font-family: inherit;
88
+ --hsf-progressbar__font-size: 14px;
89
+ --hsf-progressbar__color: #ffffff;
90
+ --hsf-progressbar__background-color: #00cc00;
91
+ --hsf-progressbar__background: linear-gradient(90deg, #00cc00, #00ffcc);
92
+ --hsf-progressbar__border-color: #009900;
93
+ --hsf-progressbar__border-style: solid;
94
+ --hsf-progressbar__border-width: 2px;
95
+ }
@@ -0,0 +1,184 @@
1
+ # Form Component
2
+
3
+ A form component that dynamically renders either a v4 form or a legacy form based on the form field's `embed_version` value. The component uses JavaScript islands to load and embed HubSpot forms client-side.
4
+
5
+ ## Import path
6
+ ```tsx
7
+ import Form from '@hubspot/cms-component-library/Form';
8
+ ```
9
+
10
+ ## Purpose
11
+
12
+ The Form component provides a unified interface for embedding HubSpot forms in CMS pages. It automatically determines whether to render a v4 form (using the new embed SDK) or a legacy form (using the classic `hbspt.forms.create` API) based on the `embed_version` returned by the form field. This allows module authors to simply pass the form field value through without needing to know which form version they're working with.
13
+
14
+ ## Component Structure
15
+
16
+ ```
17
+ Form/
18
+ ├── index.tsx # Main component with version resolution logic
19
+ ├── types.ts # TypeScript type definitions (discriminated union)
20
+ ├── ContentFields.tsx # HubSpot field definitions for content
21
+ ├── StyleFields.tsx # HubSpot field definitions for styling
22
+ ├── islands/
23
+ │ ├── FormIsland.tsx # V4 form island (embed SDK)
24
+ │ ├── LegacyFormIsland.tsx # Legacy form island (hbspt.forms.create)
25
+ │ ├── index.module.scss # Shared form styles
26
+ │ ├── v4Form.module.css # V4 form CSS variables
27
+ │ └── legacyForm.module.css # Legacy form styles
28
+ └── llm.txt
29
+ ```
30
+
31
+ ## Props
32
+
33
+ The Form component uses a discriminated union — you must use one of two prop patterns:
34
+
35
+ ### With form field (preferred)
36
+ ```tsx
37
+ {
38
+ formField: typeof FormFieldDefaults; // The form field value from module props
39
+ }
40
+ ```
41
+
42
+ ### With explicit form ID and version
43
+ ```tsx
44
+ {
45
+ formId: string; // The HubSpot form ID
46
+ formVersion: 'v4' | 'v3' | 'v2' | ''; // The form embed version
47
+ }
48
+ ```
49
+
50
+ ## Dynamic Version Resolution
51
+
52
+ When using the `formField` prop, the component reads `formField.embed_version` to decide which island to render:
53
+ - If `embed_version === 'v4'` → renders `FormIsland` (v4 embed SDK)
54
+ - Otherwise (missing or any other value) → renders `LegacyFormIsland` (classic embed)
55
+
56
+ When using `formId` + `formVersion` props, the `formVersion` value is used directly.
57
+
58
+ ## Usage Examples
59
+
60
+ ### Using form field (recommended)
61
+
62
+ This is the preferred approach. Pass the form field value directly and let the component resolve the version automatically.
63
+
64
+ Module file:
65
+ ```tsx
66
+ import Form from '@hubspot/cms-component-library/Form';
67
+ import { FormFieldDefaults } from '@hubspot/cms-components/fields';
68
+
69
+ type ComponentProps = {
70
+ form: typeof FormFieldDefaults;
71
+ };
72
+
73
+ export const Component = (props: ComponentProps) => {
74
+ return <Form formField={props.form} />;
75
+ };
76
+ ```
77
+
78
+ ### Using explicit form ID and version
79
+
80
+ Use this when you have a form ID from a source other than a form field.
81
+
82
+ ```tsx
83
+ import Form from '@hubspot/cms-component-library/Form';
84
+
85
+ <Form formId="53e5b258-4526-4012-9274-8bbe23ab2d09" formVersion="v4" />
86
+ ```
87
+
88
+ ## HubSpot CMS Integration
89
+
90
+ ### Field Definitions
91
+
92
+ The Form component provides field definitions for easy integration with HubSpot CMS modules.
93
+
94
+ #### ContentFields
95
+
96
+ Configurable props for customizing field labels, names, and defaults:
97
+
98
+ ```tsx
99
+ <Form.ContentFields
100
+ formIdLabel="Form"
101
+ formIdName="form"
102
+ formIdDefault={{}}
103
+ />
104
+ ```
105
+
106
+ **Fields:**
107
+ - `form`: FormField that allows the user to select a HubSpot form in the editor. Supports embed versions v4, v3, and v2.
108
+
109
+ **ContentFields Props:**
110
+ ```tsx
111
+ {
112
+ formIdLabel?: string; // Label shown in the editor (default: "Form")
113
+ formIdName?: string; // Field name (default: "form")
114
+ formIdDefault?: FormFieldDefaults; // Default field value (default: {})
115
+ }
116
+ ```
117
+
118
+ #### StyleFields
119
+
120
+ Configurable props for variant selection:
121
+
122
+ ```tsx
123
+ <Form.StyleFields
124
+ formVariantLabel="Form variant"
125
+ formVariantName="formVariant"
126
+ formVariantDefault={{ variant_name: 'primaryForm' }}
127
+ />
128
+ ```
129
+
130
+ **Fields:**
131
+ - `formVariant`: VariantSelectionField for selecting a form style variant from the `forms` variant definition.
132
+
133
+ **StyleFields Props:**
134
+ ```tsx
135
+ {
136
+ formVariantLabel?: string; // Label shown in the editor (default: "Form variant")
137
+ formVariantName?: string; // Field name (default: "formVariant")
138
+ formVariantDefault?: { variant_name: string }; // Default variant (default: { variant_name: 'primaryForm' })
139
+ }
140
+ ```
141
+
142
+ ### Module Usage Example
143
+
144
+ ```tsx
145
+ // Component file (index.tsx)
146
+ import Form from '@hubspot/cms-component-library/Form';
147
+ import { FormFieldDefaults } from '@hubspot/cms-components/fields';
148
+
149
+ type ComponentProps = {
150
+ form: typeof FormFieldDefaults;
151
+ };
152
+
153
+ export const Component = (props: ComponentProps) => {
154
+ return <Form formField={props.form} />;
155
+ };
156
+
157
+ // Fields file (fields.tsx)
158
+ import { ModuleFields, FieldGroup } from '@hubspot/cms-components/fields';
159
+ import Form from '@hubspot/cms-component-library/Form';
160
+
161
+ export const fields = (
162
+ <ModuleFields>
163
+ <Form.ContentFields />
164
+ <FieldGroup label="Design" name="groupDesign" tab="STYLE">
165
+ <Form.StyleFields />
166
+ </FieldGroup>
167
+ </ModuleFields>
168
+ );
169
+ ```
170
+
171
+ ## Islands
172
+
173
+ The Form component renders as a JavaScript island. Both form types require client-side JavaScript:
174
+
175
+ - **FormIsland (v4)**: Loads the v4 embed SDK script (`js.hsforms.net/forms/embed/developer/{portalId}.js`) and renders a `div` with `data-form-id` and `data-portal-id` attributes.
176
+ - **LegacyFormIsland**: Loads the legacy embed script (`js.hsforms.net/forms/embed/v2.js`) and calls `window.hbspt.forms.create()` to render the form into a target container.
177
+
178
+ Both islands automatically resolve the portal ID and environment (QA vs prod) from the CMS context.
179
+
180
+ ## Best Practices
181
+
182
+ - **Prefer `formField` over `formId` + `formVersion`**: Passing the form field value directly ensures the version is always correct and keeps the module code simple.
183
+ - **Form version is determined by the form itself**: The `embed_version` property on the form field value is set by HubSpot based on the form's configuration. Module authors should not hardcode a version.
184
+ - **Styling via variant definitions**: Use `Form.StyleFields` with variant definitions to allow theme-level form styling rather than hardcoding CSS values.
@@ -19,3 +19,9 @@ export type ContentFieldsProps = {
19
19
  formIdName?: string;
20
20
  formIdDefault?: typeof FormFieldDefaults;
21
21
  };
22
+
23
+ export type StyleFieldsProps = {
24
+ formVariantLabel?: string;
25
+ formVariantName?: string;
26
+ formVariantDefault?: { variant_name: string };
27
+ };
@@ -1,7 +1,7 @@
1
1
  import { LinkField } from '@hubspot/cms-components/fields';
2
2
  import { ContentFieldsProps } from './types.js';
3
3
 
4
- const ContentFields = ({
4
+ const ContentFields = <TLinkName extends string = 'link'>({
5
5
  linkLabel = 'Link',
6
6
  linkName = 'link',
7
7
  linkDefault = {
@@ -12,7 +12,7 @@ const ContentFields = ({
12
12
  },
13
13
  },
14
14
  fieldVisibility,
15
- }: ContentFieldsProps) => {
15
+ }: ContentFieldsProps<TLinkName>) => {
16
16
  return (
17
17
  <LinkField
18
18
  label={linkLabel}
@@ -1,25 +1,18 @@
1
- import { ChoiceField } from '@hubspot/cms-components/fields';
1
+ import { VariantSelectionField } from '@hubspot/cms-components/fields';
2
2
  import { StyleFieldsProps } from './types.js';
3
3
 
4
- // !todo: may not need later, but keeping for now in case we have variant system for links
5
4
  const StyleFields = ({
6
- linkVariantLabel = 'Link variant',
7
- linkVariantName = 'linkVariant',
8
- linkVariantDefault = 'primary',
5
+ variantLabel = 'Link variant',
6
+ variantName = 'linkVariant',
7
+ variantDefault = { variant_name: 'primaryLink' },
9
8
  }: StyleFieldsProps) => {
10
9
  return (
11
- <>
12
- <ChoiceField
13
- label={linkVariantLabel}
14
- name={linkVariantName}
15
- choices={[
16
- ['primary', 'Primary'],
17
- ['secondary', 'Secondary'],
18
- ['tertiary', 'Tertiary'],
19
- ]}
20
- default={linkVariantDefault}
21
- />
22
- </>
10
+ <VariantSelectionField
11
+ label={variantLabel}
12
+ name={variantName}
13
+ variantDefinitionName="link"
14
+ default={variantDefault}
15
+ />
23
16
  );
24
17
  };
25
18
 
@@ -1,21 +1,21 @@
1
1
  .link {
2
- display: var(--hscl-link-display, inline-block);
3
- color: var(--hscl-link-color);
4
- text-decoration: var(--hscl-link-textDecoration, underline);
2
+ display: inline-block;
3
+ color: var(--hs-link-color);
4
+ text-decoration: var(--hs-link-textDecoration);
5
5
  cursor: pointer;
6
6
 
7
7
  &:hover {
8
- color: var(--hscl-link-color-hover);
9
- text-decoration: var(--hscl-link-textDecoration-hover, underline);
8
+ color: var(--hs-link-color-hover);
9
+ text-decoration: var(--hs-link-textDecoration-hover);
10
10
  }
11
11
 
12
12
  &:focus-visible {
13
- outline: var(--hscl-link-outlineWidth-focus, 2px) solid var(--hscl-link-outlineColor-focus, currentColor);
14
- outline-offset: var(--hscl-link-outlineOffset-focus, 2px);
13
+ outline: 2px solid currentColor;
14
+ outline-offset: 2px;
15
15
  }
16
16
 
17
17
  &:active {
18
- color: var(--hscl-link-color-active, var(--hscl-link-color));
19
- text-decoration: var(--hscl-link-textDecoration-active, var(--hscl-link-textDecoration, underline));
18
+ color: var(--hs-link-color-active, var(--hs-link-color));
19
+ text-decoration: var(--hs-link-textDecoration-active, var(--hs-link-textDecoration));
20
20
  }
21
21
  }
@@ -2,7 +2,6 @@ import styles from './index.module.scss';
2
2
  import ContentFields from './ContentFields.js';
3
3
  import StyleFields from './StyleFields.js';
4
4
  import cx from '../utils/classname.js';
5
- import type { CSSVariables } from '../utils/types.js';
6
5
  import { LinkProps, BaseLinkProps, LinkHTMLProps } from './types.js';
7
6
  import {
8
7
  getLinkRel,
@@ -32,7 +31,7 @@ const getLinkValues = (props: Omit<LinkProps, keyof BaseLinkProps>) => {
32
31
  };
33
32
 
34
33
  const LinkComponent = ({
35
- variant = 'primary',
34
+ variant = 'primaryLink',
36
35
  className = '',
37
36
  style = {},
38
37
  children,
@@ -43,18 +42,14 @@ const LinkComponent = ({
43
42
  const defaultClasses = styles.link;
44
43
  const combinedClasses = cx(defaultClasses, className);
45
44
 
46
- const cssVariables: CSSVariables = {
47
- '--hscl-link-color': `var(--hscl-link-color-${variant})`,
48
- '--hscl-link-color-hover': `var(--hscl-link-color-hover-${variant})`,
49
- };
50
-
51
45
  return (
52
46
  <a
53
47
  className={combinedClasses}
54
- style={{ ...cssVariables, ...style }}
48
+ style={style}
55
49
  href={href}
56
50
  target={target}
57
51
  rel={rel}
52
+ data-link-variant={variant}
58
53
  {...anchorProps}
59
54
  >
60
55
  {children}