@hubspot/cms-component-library 0.3.8 → 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.
- 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/types.ts +5 -4
- 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
|
@@ -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}
|
|
@@ -33,12 +33,13 @@ type LinkWithoutField = BaseLinkProps &
|
|
|
33
33
|
|
|
34
34
|
export type LinkProps = LinkWithField | LinkWithoutField;
|
|
35
35
|
|
|
36
|
-
export type ContentFieldsProps = {
|
|
36
|
+
export type ContentFieldsProps<TLinkName extends string = 'link'> = {
|
|
37
37
|
linkLabel?: string;
|
|
38
|
-
linkName?: string;
|
|
39
38
|
linkDefault?: typeof LinkFieldDefaults;
|
|
40
|
-
|
|
41
|
-
}
|
|
39
|
+
} & (
|
|
40
|
+
| { linkName?: never; fieldVisibility?: Partial<Record<'link', Visibility>> }
|
|
41
|
+
| { linkName: TLinkName; fieldVisibility?: Partial<Record<TLinkName, Visibility>> }
|
|
42
|
+
);
|
|
42
43
|
|
|
43
44
|
export type StyleFieldsProps = {
|
|
44
45
|
linkVariantLabel?: string;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChoiceField,
|
|
3
|
+
EmbedField,
|
|
4
|
+
ImageField,
|
|
5
|
+
VideoField,
|
|
6
|
+
} from '@hubspot/cms-components/fields';
|
|
7
|
+
import { ContentFieldsProps } from './types.js';
|
|
8
|
+
|
|
9
|
+
const ContentFields = ({
|
|
10
|
+
videoTypeLabel = 'Video type',
|
|
11
|
+
videoTypeName = 'videoType',
|
|
12
|
+
hsVideoLabel = 'HubSpot video',
|
|
13
|
+
hsVideoName = 'hubspotVideo',
|
|
14
|
+
hsVideoDefault,
|
|
15
|
+
embedVideoLabel = 'Embed video',
|
|
16
|
+
embedVideoName = 'embedVideo',
|
|
17
|
+
embedVideoDefault,
|
|
18
|
+
oembedThumbnailLabel = 'Custom thumbnail',
|
|
19
|
+
oembedThumbnailName = 'oembedThumbnail',
|
|
20
|
+
oembedThumbnailDefault,
|
|
21
|
+
}: ContentFieldsProps) => {
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
<ChoiceField
|
|
25
|
+
name={videoTypeName}
|
|
26
|
+
label={videoTypeLabel}
|
|
27
|
+
id="videoType"
|
|
28
|
+
display="radio"
|
|
29
|
+
choices={[
|
|
30
|
+
['embed', 'Embed'],
|
|
31
|
+
['hubspot_video', 'HubSpot video'],
|
|
32
|
+
]}
|
|
33
|
+
default="embed"
|
|
34
|
+
visibilityRules="ADVANCED"
|
|
35
|
+
advancedVisibility={{
|
|
36
|
+
boolean_operator: 'OR',
|
|
37
|
+
criteria: [
|
|
38
|
+
{
|
|
39
|
+
access: {
|
|
40
|
+
operator: 'HAS_ALL',
|
|
41
|
+
scopes: ['marketing-video'],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
controlling_field: 'videoType',
|
|
46
|
+
operator: 'EQUAL',
|
|
47
|
+
controlling_value_regex: 'hubspot_video',
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
}}
|
|
51
|
+
/>
|
|
52
|
+
|
|
53
|
+
<VideoField
|
|
54
|
+
name={hsVideoName}
|
|
55
|
+
label={hsVideoLabel}
|
|
56
|
+
showAdvancedOptions={true}
|
|
57
|
+
default={hsVideoDefault}
|
|
58
|
+
visibility={{
|
|
59
|
+
controlling_field: 'videoType',
|
|
60
|
+
operator: 'EQUAL',
|
|
61
|
+
controlling_value_regex: 'hubspot_video',
|
|
62
|
+
}}
|
|
63
|
+
/>
|
|
64
|
+
|
|
65
|
+
<EmbedField
|
|
66
|
+
name={embedVideoName}
|
|
67
|
+
label={embedVideoLabel}
|
|
68
|
+
id="embedVideo"
|
|
69
|
+
supportedSourceTypes={['oembed', 'html']}
|
|
70
|
+
supportedOembedTypes={['video']}
|
|
71
|
+
supportedMediaBridgeProviders={[]}
|
|
72
|
+
default={embedVideoDefault}
|
|
73
|
+
visibility={{
|
|
74
|
+
controlling_field: 'videoType',
|
|
75
|
+
operator: 'EQUAL',
|
|
76
|
+
controlling_value_regex: 'embed',
|
|
77
|
+
}}
|
|
78
|
+
/>
|
|
79
|
+
|
|
80
|
+
<ImageField
|
|
81
|
+
name={oembedThumbnailName}
|
|
82
|
+
label={oembedThumbnailLabel}
|
|
83
|
+
default={oembedThumbnailDefault}
|
|
84
|
+
responsive={true}
|
|
85
|
+
resizable={false}
|
|
86
|
+
visibilityRules="ADVANCED"
|
|
87
|
+
advancedVisibility={{
|
|
88
|
+
boolean_operator: 'AND',
|
|
89
|
+
criteria: [
|
|
90
|
+
{
|
|
91
|
+
controlling_field: 'videoType',
|
|
92
|
+
operator: 'EQUAL',
|
|
93
|
+
controlling_value_regex: 'embed',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
controlling_field: 'embedVideo',
|
|
97
|
+
operator: 'MATCHES_REGEX',
|
|
98
|
+
controlling_value_regex: '(?=.*"source_type":"oembed")',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
controlling_field: 'embedVideo',
|
|
102
|
+
operator: 'MATCHES_REGEX',
|
|
103
|
+
controlling_value_regex: '(?=.*"oembed_url":"(?!")+)',
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
}}
|
|
107
|
+
/>
|
|
108
|
+
</>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export default ContentFields;
|