@hubspot/cms-component-library 0.1.0-alpha.1

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 (37) hide show
  1. package/README.md +3 -0
  2. package/cli/commands/customize.ts +145 -0
  3. package/cli/commands/help.ts +56 -0
  4. package/cli/commands/version.ts +12 -0
  5. package/cli/index.ts +42 -0
  6. package/cli/tests/commands.test.ts +128 -0
  7. package/cli/tests/get-file.test.ts +82 -0
  8. package/cli/tests/version-integration.test.ts +39 -0
  9. package/cli/utils/cli-metadata.ts +9 -0
  10. package/cli/utils/component-naming.ts +76 -0
  11. package/cli/utils/components.ts +74 -0
  12. package/cli/utils/file-operations.ts +158 -0
  13. package/cli/utils/logging.ts +13 -0
  14. package/cli/utils/prompts.ts +80 -0
  15. package/cli/utils/version.ts +33 -0
  16. package/components/componentLibrary/Button/index.module.scss +9 -0
  17. package/components/componentLibrary/Button/index.tsx +83 -0
  18. package/components/componentLibrary/Button/scaffolds/fields.tsx.template +70 -0
  19. package/components/componentLibrary/Button/scaffolds/index.ts.template +95 -0
  20. package/components/componentLibrary/Heading/index.module.scss +9 -0
  21. package/components/componentLibrary/Heading/index.tsx +34 -0
  22. package/components/componentLibrary/Heading/scaffolds/fields.tsx.template +62 -0
  23. package/components/componentLibrary/Heading/scaffolds/index.ts.template +46 -0
  24. package/components/componentLibrary/index.ts +1 -0
  25. package/components/componentLibrary/styles/_component-base.scss +246 -0
  26. package/components/componentLibrary/types/index.ts +308 -0
  27. package/components/componentLibrary/utils/chainApi/choiceFieldGenerator.tsx +64 -0
  28. package/components/componentLibrary/utils/chainApi/index.ts +115 -0
  29. package/components/componentLibrary/utils/chainApi/labelGenerator.ts +76 -0
  30. package/components/componentLibrary/utils/chainApi/stateManager.ts +178 -0
  31. package/components/componentLibrary/utils/classname.ts +40 -0
  32. package/components/componentLibrary/utils/createConditionalClasses.ts +44 -0
  33. package/components/componentLibrary/utils/createHsclComponent.tsx +167 -0
  34. package/components/componentLibrary/utils/propResolution/createCssVariables.ts +58 -0
  35. package/components/componentLibrary/utils/propResolution/propResolutionUtils.ts +113 -0
  36. package/components/componentLibrary/utils/storybook/standardArgs.ts +607 -0
  37. package/package.json +62 -0
@@ -0,0 +1 @@
1
+ export { createComponentInstance } from './utils/createHsclComponent.js';
@@ -0,0 +1,246 @@
1
+ // Auto-generated conditional styles
2
+ // DO NOT EDIT MANUALLY - Generated from STYLE_COMPONENT_PROPS
3
+ // Run: npm run generate-conditional-styles to regenerate
4
+
5
+ @mixin hscl-component-conditional($component-name) {
6
+ &:global(.hsclStyle-color) {
7
+ color: var(--hscl-#{$component-name}-color, revert-layer);
8
+ }
9
+ &:global(.hsclStyle-background) {
10
+ background: var(--hscl-#{$component-name}-background, revert-layer);
11
+ }
12
+ &:global(.hsclStyle-background-color) {
13
+ background-color: var(--hscl-#{$component-name}-background-color, revert-layer);
14
+ }
15
+ &:global(.hsclStyle-font-size) {
16
+ font-size: var(--hscl-#{$component-name}-font-size, revert-layer);
17
+ }
18
+ &:global(.hsclStyle-font-style) {
19
+ font-style: var(--hscl-#{$component-name}-font-style, revert-layer);
20
+ }
21
+ &:global(.hsclStyle-font-weight) {
22
+ font-weight: var(--hscl-#{$component-name}-font-weight, revert-layer);
23
+ }
24
+ &:global(.hsclStyle-font-family) {
25
+ font-family: var(--hscl-#{$component-name}-font-family, revert-layer);
26
+ }
27
+ &:global(.hsclStyle-line-height) {
28
+ line-height: var(--hscl-#{$component-name}-line-height, revert-layer);
29
+ }
30
+ &:global(.hsclStyle-letter-spacing) {
31
+ letter-spacing: var(--hscl-#{$component-name}-letter-spacing, revert-layer);
32
+ }
33
+ &:global(.hsclStyle-text-align) {
34
+ text-align: var(--hscl-#{$component-name}-text-align, revert-layer);
35
+ }
36
+ &:global(.hsclStyle-text-transform) {
37
+ text-transform: var(--hscl-#{$component-name}-text-transform, revert-layer);
38
+ }
39
+ &:global(.hsclStyle-text-decoration) {
40
+ text-decoration: var(--hscl-#{$component-name}-text-decoration, revert-layer);
41
+ }
42
+ &:global(.hsclStyle-cursor) {
43
+ cursor: var(--hscl-#{$component-name}-cursor, revert-layer);
44
+ }
45
+ &:global(.hsclStyle-opacity) {
46
+ opacity: var(--hscl-#{$component-name}-opacity, revert-layer);
47
+ }
48
+ &:global(.hsclStyle-box-shadow) {
49
+ box-shadow: var(--hscl-#{$component-name}-box-shadow, revert-layer);
50
+ }
51
+ &:global(.hsclStyle-transition) {
52
+ transition: var(--hscl-#{$component-name}-transition, revert-layer);
53
+ }
54
+ &:global(.hsclStyle-transition-property) {
55
+ transition-property: var(--hscl-#{$component-name}-transition-property, revert-layer);
56
+ }
57
+ &:global(.hsclStyle-transition-duration) {
58
+ transition-duration: var(--hscl-#{$component-name}-transition-duration, revert-layer);
59
+ }
60
+ &:global(.hsclStyle-transition-timing-function) {
61
+ transition-timing-function: var(--hscl-#{$component-name}-transition-timing-function, revert-layer);
62
+ }
63
+ &:global(.hsclStyle-transition-delay) {
64
+ transition-delay: var(--hscl-#{$component-name}-transition-delay, revert-layer);
65
+ }
66
+ &:global(.hsclStyle-transform) {
67
+ transform: var(--hscl-#{$component-name}-transform, revert-layer);
68
+ }
69
+ &:global(.hsclStyle-transform-origin) {
70
+ transform-origin: var(--hscl-#{$component-name}-transform-origin, revert-layer);
71
+ }
72
+ &:global(.hsclStyle-border-block-start) {
73
+ border-block-start: var(--hscl-#{$component-name}-border-block-start, revert-layer);
74
+ }
75
+ &:global(.hsclStyle-border-block-end) {
76
+ border-block-end: var(--hscl-#{$component-name}-border-block-end, revert-layer);
77
+ }
78
+ &:global(.hsclStyle-border-inline-start) {
79
+ border-inline-start: var(--hscl-#{$component-name}-border-inline-start, revert-layer);
80
+ }
81
+ &:global(.hsclStyle-border-inline-end) {
82
+ border-inline-end: var(--hscl-#{$component-name}-border-inline-end, revert-layer);
83
+ }
84
+ &:global(.hsclStyle-border) {
85
+ border: var(--hscl-#{$component-name}-border, revert-layer);
86
+ }
87
+ &:global(.hsclStyle-border-radius) {
88
+ border-radius: var(--hscl-#{$component-name}-border-radius, revert-layer);
89
+ }
90
+ &:global(.hsclStyle-outline) {
91
+ outline: var(--hscl-#{$component-name}-outline, revert-layer);
92
+ }
93
+ &:global(.hsclStyle-margin-inline) {
94
+ margin-inline: var(--hscl-#{$component-name}-margin-inline, revert-layer);
95
+ }
96
+ &:global(.hsclStyle-margin-block) {
97
+ margin-block: var(--hscl-#{$component-name}-margin-block, revert-layer);
98
+ }
99
+ &:global(.hsclStyle-margin) {
100
+ margin: var(--hscl-#{$component-name}-margin, revert-layer);
101
+ }
102
+ &:global(.hsclStyle-padding-inline) {
103
+ padding-inline: var(--hscl-#{$component-name}-padding-inline, revert-layer);
104
+ }
105
+ &:global(.hsclStyle-padding-block) {
106
+ padding-block: var(--hscl-#{$component-name}-padding-block, revert-layer);
107
+ }
108
+ &:global(.hsclStyle-padding) {
109
+ padding: var(--hscl-#{$component-name}-padding, revert-layer);
110
+ }
111
+ &:global(.hsclStyle-position) {
112
+ position: var(--hscl-#{$component-name}-position, revert-layer);
113
+ }
114
+ &:global(.hsclStyle-top) {
115
+ top: var(--hscl-#{$component-name}-top, revert-layer);
116
+ }
117
+ &:global(.hsclStyle-right) {
118
+ right: var(--hscl-#{$component-name}-right, revert-layer);
119
+ }
120
+ &:global(.hsclStyle-bottom) {
121
+ bottom: var(--hscl-#{$component-name}-bottom, revert-layer);
122
+ }
123
+ &:global(.hsclStyle-left) {
124
+ left: var(--hscl-#{$component-name}-left, revert-layer);
125
+ }
126
+ &:global(.hsclStyle-z-index) {
127
+ z-index: var(--hscl-#{$component-name}-z-index, revert-layer);
128
+ }
129
+ &:global(.hsclStyle-width) {
130
+ width: var(--hscl-#{$component-name}-width, revert-layer);
131
+ }
132
+ &:global(.hsclStyle-height) {
133
+ height: var(--hscl-#{$component-name}-height, revert-layer);
134
+ }
135
+ &:global(.hsclStyle-min-width) {
136
+ min-width: var(--hscl-#{$component-name}-min-width, revert-layer);
137
+ }
138
+ &:global(.hsclStyle-min-height) {
139
+ min-height: var(--hscl-#{$component-name}-min-height, revert-layer);
140
+ }
141
+ &:global(.hsclStyle-max-width) {
142
+ max-width: var(--hscl-#{$component-name}-max-width, revert-layer);
143
+ }
144
+ &:global(.hsclStyle-max-height) {
145
+ max-height: var(--hscl-#{$component-name}-max-height, revert-layer);
146
+ }
147
+ &:global(.hsclStyle-display) {
148
+ display: var(--hscl-#{$component-name}-display, revert-layer);
149
+ }
150
+ &:global(.hsclStyle-overflow) {
151
+ overflow: var(--hscl-#{$component-name}-overflow, revert-layer);
152
+ }
153
+ &:global(.hsclStyle-visibility) {
154
+ visibility: var(--hscl-#{$component-name}-visibility, revert-layer);
155
+ }
156
+ &:global(.hsclStyle-justify-content) {
157
+ justify-content: var(--hscl-#{$component-name}-justify-content, revert-layer);
158
+ }
159
+ &:global(.hsclStyle-align-items) {
160
+ align-items: var(--hscl-#{$component-name}-align-items, revert-layer);
161
+ }
162
+ &:global(.hsclStyle-flex-direction) {
163
+ flex-direction: var(--hscl-#{$component-name}-flex-direction, revert-layer);
164
+ }
165
+ &:global(.hsclStyle-gap) {
166
+ gap: var(--hscl-#{$component-name}-gap, revert-layer);
167
+ }
168
+ &:global(.hsclStyle-align-self) {
169
+ align-self: var(--hscl-#{$component-name}-align-self, revert-layer);
170
+ }
171
+ &:global(.hsclStyle-flex) {
172
+ flex: var(--hscl-#{$component-name}-flex, revert-layer);
173
+ }
174
+ &:global(.hsclStyle-flex-grow) {
175
+ flex-grow: var(--hscl-#{$component-name}-flex-grow, revert-layer);
176
+ }
177
+ &:global(.hsclStyle-flex-shrink) {
178
+ flex-shrink: var(--hscl-#{$component-name}-flex-shrink, revert-layer);
179
+ }
180
+ &:global(.hsclStyle-flex-basis) {
181
+ flex-basis: var(--hscl-#{$component-name}-flex-basis, revert-layer);
182
+ }
183
+ &:global(.hsclStyle-order) {
184
+ order: var(--hscl-#{$component-name}-order, revert-layer);
185
+ }
186
+ &:global(.hsclStyle-white-space) {
187
+ white-space: var(--hscl-#{$component-name}-white-space, revert-layer);
188
+ }
189
+ &:global(.hsclStyle-word-break) {
190
+ word-break: var(--hscl-#{$component-name}-word-break, revert-layer);
191
+ }
192
+ &:global(.hsclStyle-text-wrap) {
193
+ text-wrap: var(--hscl-#{$component-name}-text-wrap, revert-layer);
194
+ }
195
+ &:global(.hsclStyle-overflow-wrap) {
196
+ overflow-wrap: var(--hscl-#{$component-name}-overflow-wrap, revert-layer);
197
+ }
198
+ &:global(.hsclStyle-text-overflow) {
199
+ text-overflow: var(--hscl-#{$component-name}-text-overflow, revert-layer);
200
+ }
201
+ &:global(.hsclStyle-word-wrap) {
202
+ word-wrap: var(--hscl-#{$component-name}-word-wrap, revert-layer);
203
+ }
204
+ &:global(.hsclStyle-pointer-events) {
205
+ pointer-events: var(--hscl-#{$component-name}-pointer-events, revert-layer);
206
+ }
207
+ &:global(.hsclStyle-object-fit) {
208
+ object-fit: var(--hscl-#{$component-name}-object-fit, revert-layer);
209
+ }
210
+ &:global(.hsclStyle-aspect-ratio) {
211
+ aspect-ratio: var(--hscl-#{$component-name}-aspect-ratio, revert-layer);
212
+ }
213
+ }
214
+
215
+ @mixin hscl-component-hover-conditional($component-name) {
216
+ &:global(.hsclStyle-hover-color):hover {
217
+ color: var(--hscl-#{$component-name}-hover-color, revert-layer);
218
+ }
219
+ &:global(.hsclStyle-hover-background):hover {
220
+ background: var(--hscl-#{$component-name}-hover-background, revert-layer);
221
+ }
222
+ &:global(.hsclStyle-hover-background-color):hover {
223
+ background-color: var(--hscl-#{$component-name}-hover-background-color, revert-layer);
224
+ }
225
+ &:global(.hsclStyle-hover-border-block-start):hover {
226
+ border-block-start: var(--hscl-#{$component-name}-hover-border-block-start, revert-layer);
227
+ }
228
+ &:global(.hsclStyle-hover-border-block-end):hover {
229
+ border-block-end: var(--hscl-#{$component-name}-hover-border-block-end, revert-layer);
230
+ }
231
+ &:global(.hsclStyle-hover-border-inline-start):hover {
232
+ border-inline-start: var(--hscl-#{$component-name}-hover-border-inline-start, revert-layer);
233
+ }
234
+ &:global(.hsclStyle-hover-border-inline-end):hover {
235
+ border-inline-end: var(--hscl-#{$component-name}-hover-border-inline-end, revert-layer);
236
+ }
237
+ &:global(.hsclStyle-hover-opacity):hover {
238
+ opacity: var(--hscl-#{$component-name}-hover-opacity, revert-layer);
239
+ }
240
+ &:global(.hsclStyle-hover-transform):hover {
241
+ transform: var(--hscl-#{$component-name}-hover-transform, revert-layer);
242
+ }
243
+ &:global(.hsclStyle-hover-box-shadow):hover {
244
+ box-shadow: var(--hscl-#{$component-name}-hover-box-shadow, revert-layer);
245
+ }
246
+ }
@@ -0,0 +1,308 @@
1
+ // Style-related props that generate CSS variables
2
+
3
+ import { ChoiceFieldType } from '@hubspot/cms-components/fields';
4
+
5
+ // Typography types
6
+ export type TextAlign = 'left' | 'center' | 'right' | 'justify';
7
+ export type TextTransform = 'none' | 'uppercase' | 'lowercase' | 'capitalize';
8
+ export type FontWeight = 'lighter' | 'normal' | 'bold' | 'bolder' | number;
9
+ export type FontStyle = 'normal' | 'italic' | 'oblique';
10
+
11
+ export const STYLE_COMPONENT_PROPS = {
12
+ // CSS property props
13
+ color: String,
14
+ background: String,
15
+ backgroundColor: String,
16
+ fontSize: [String, Number] as const,
17
+ fontStyle: String as () => FontStyle,
18
+ fontWeight: String as () => FontWeight,
19
+ fontFamily: String,
20
+ lineHeight: [String, Number] as const,
21
+ letterSpacing: [String, Number] as const,
22
+ textAlign: String as () => TextAlign,
23
+ textTransform: String as () => TextTransform,
24
+ textDecoration: String,
25
+ cursor: String,
26
+ opacity: Number,
27
+ boxShadow: String,
28
+
29
+ // Transition properties
30
+ transition: String,
31
+ transitionProperty: String,
32
+ transitionDuration: String,
33
+ transitionTimingFunction: String,
34
+ transitionDelay: String,
35
+
36
+ // Transform properties
37
+ transform: String,
38
+ transformOrigin: String,
39
+
40
+ // Border props
41
+ borderBlockStart: String,
42
+ borderBlockEnd: String,
43
+ borderInlineStart: String,
44
+ borderInlineEnd: String,
45
+ border: String,
46
+ borderRadius: [String, Number] as const,
47
+ outline: String,
48
+
49
+ // Hover state props
50
+ hoverColor: String,
51
+ hoverBackground: String,
52
+ hoverBackgroundColor: String,
53
+ hoverBorderBlockStart: String,
54
+ hoverBorderBlockEnd: String,
55
+ hoverBorderInlineStart: String,
56
+ hoverBorderInlineEnd: String,
57
+ hoverOpacity: Number,
58
+ hoverTransform: String,
59
+ hoverBoxShadow: String,
60
+
61
+ // Space props (margin/padding) - flexible user-defined values
62
+ marginInline: String,
63
+ marginBlock: String,
64
+ margin: String,
65
+ paddingInline: String,
66
+ paddingBlock: String,
67
+ padding: String,
68
+
69
+ // Position props
70
+ position: String as () =>
71
+ | 'static'
72
+ | 'relative'
73
+ | 'absolute'
74
+ | 'fixed'
75
+ | 'sticky',
76
+ top: [String, Number] as const,
77
+ right: [String, Number] as const,
78
+ bottom: [String, Number] as const,
79
+ left: [String, Number] as const,
80
+ zIndex: Number,
81
+
82
+ // Size props
83
+ width: [String, Number] as const,
84
+ height: [String, Number] as const,
85
+ minWidth: [String, Number] as const,
86
+ minHeight: [String, Number] as const,
87
+ maxWidth: [String, Number] as const,
88
+ maxHeight: [String, Number] as const,
89
+
90
+ // Display props
91
+ display: String as () =>
92
+ | 'block'
93
+ | 'inline'
94
+ | 'inline-block'
95
+ | 'flex'
96
+ | 'inline-flex'
97
+ | 'grid'
98
+ | 'none',
99
+ overflow: String as () => 'visible' | 'hidden' | 'scroll' | 'auto',
100
+ visibility: String as () => 'visible' | 'hidden' | 'collapse',
101
+
102
+ // Flex layout properties
103
+ justifyContent: String as () =>
104
+ | 'flex-start'
105
+ | 'center'
106
+ | 'flex-end'
107
+ | 'space-between'
108
+ | 'space-around'
109
+ | 'space-evenly'
110
+ | 'start'
111
+ | 'end',
112
+ alignItems: String as () =>
113
+ | 'stretch'
114
+ | 'flex-start'
115
+ | 'center'
116
+ | 'flex-end'
117
+ | 'baseline'
118
+ | 'start'
119
+ | 'end',
120
+ flexDirection: String as () => 'row' | 'column',
121
+ gap: [String, Number] as const,
122
+
123
+ // Flex item properties
124
+ alignSelf: String as () =>
125
+ | 'auto'
126
+ | 'stretch'
127
+ | 'flex-start'
128
+ | 'center'
129
+ | 'flex-end'
130
+ | 'baseline'
131
+ | 'start'
132
+ | 'end',
133
+ flex: [Number] as const,
134
+ flexGrow: Number,
135
+ flexShrink: Number,
136
+ flexBasis: [String, Number] as const,
137
+ order: Number,
138
+
139
+ // Enhanced text properties
140
+ whiteSpace: String as () =>
141
+ | 'normal'
142
+ | 'nowrap'
143
+ | 'pre'
144
+ | 'pre-line'
145
+ | 'pre-wrap'
146
+ | 'break-spaces',
147
+ wordBreak: String as () => 'normal' | 'break-all' | 'keep-all' | 'break-word',
148
+ textWrap: String as () => 'wrap' | 'nowrap' | 'balance' | 'pretty' | 'stable',
149
+ overflowWrap: String as () => 'normal' | 'anywhere' | 'break-word',
150
+ textOverflow: String as () => 'clip' | 'ellipsis' | string,
151
+ wordWrap: String as () => 'normal' | 'break-word' | 'anywhere',
152
+
153
+ // Interaction properties
154
+ pointerEvents: String as () =>
155
+ | 'auto'
156
+ | 'none'
157
+ | 'visiblePainted'
158
+ | 'visibleFill'
159
+ | 'visibleStroke'
160
+ | 'visible'
161
+ | 'painted'
162
+ | 'fill'
163
+ | 'stroke'
164
+ | 'all',
165
+
166
+ // Media/object properties
167
+ objectFit: String as () =>
168
+ | 'fill'
169
+ | 'contain'
170
+ | 'cover'
171
+ | 'none'
172
+ | 'scale-down',
173
+ aspectRatio: String,
174
+ } as const;
175
+
176
+ // Helper type to convert config to proper TypeScript types
177
+ type ConvertMapToType<T> = T extends readonly [
178
+ StringConstructor,
179
+ NumberConstructor
180
+ ]
181
+ ? string | number
182
+ : T extends StringConstructor
183
+ ? string
184
+ : T extends NumberConstructor
185
+ ? number
186
+ : T extends ObjectConstructor
187
+ ? React.CSSProperties
188
+ : T extends () => infer U
189
+ ? U
190
+ : never;
191
+
192
+ // Single source of truth for all base component props
193
+ export const BASE_COMPONENT_PROPS_CONFIG = {
194
+ // Basic element props
195
+ className: String,
196
+ style: Object as () => React.CSSProperties,
197
+
198
+ // Spread in all style props
199
+ ...STYLE_COMPONENT_PROPS,
200
+
201
+ // Accessibility props
202
+ role: String,
203
+ } as const;
204
+
205
+ // Generate TypeScript type from the config
206
+ export type BaseComponentProps = {
207
+ [K in keyof typeof BASE_COMPONENT_PROPS_CONFIG]?: ConvertMapToType<
208
+ (typeof BASE_COMPONENT_PROPS_CONFIG)[K]
209
+ >;
210
+ };
211
+
212
+ // Generate runtime keys directly from the config - exported for use in utils
213
+ export const BASE_COMPONENT_PROP_KEYS = new Set(
214
+ Object.keys(BASE_COMPONENT_PROPS_CONFIG) as (keyof BaseComponentProps)[]
215
+ );
216
+ // ==============================================================================
217
+ // Helpers
218
+ // ==============================================================================
219
+
220
+ // Common type constraint for React components
221
+ export type AnyReactComponent = React.ComponentType<unknown> & {
222
+ hsclComponentName: string;
223
+ cssModule: Record<string, string>;
224
+ };
225
+
226
+ // Props that are generated by the component factory and consumed internally.
227
+ // Think of them as pre-processed props that the developer doesn't need to know about.
228
+ export type HsclInternalProps = {
229
+ hsclInternal?: {
230
+ hsclProcessedClasses?: string;
231
+ hsclCssVars?: CSSVariableMap;
232
+ hsclResolvedProps?: BaseComponentProps;
233
+ hsclProcessedStyles?: React.CSSProperties;
234
+ };
235
+ };
236
+
237
+ export type ComponentPropsType<T extends AnyReactComponent> =
238
+ React.ComponentProps<T>;
239
+
240
+ // Alias which merges BaseComponentProps with component props while excluding internal props
241
+ // This type preserves discriminated unions by distributing over union types
242
+ export type InferPublicProps<
243
+ T extends
244
+ | keyof React.JSX.IntrinsicElements
245
+ | React.JSXElementConstructor<unknown>
246
+ > = React.ComponentProps<T> extends infer U
247
+ ? U extends unknown
248
+ ? BaseComponentProps & Omit<U, keyof HsclInternalProps>
249
+ : never
250
+ : never;
251
+
252
+ export type BaseAndInternalComponentProps = BaseComponentProps &
253
+ HsclInternalProps;
254
+
255
+ export type CSSVariableMap = {
256
+ [variableName: string]: string | number;
257
+ };
258
+
259
+ // ==============================================================================
260
+ // Chainable API
261
+ // ==============================================================================
262
+
263
+ export interface DimensionOption {
264
+ props: Partial<BaseComponentProps>;
265
+ choiceFieldOptionLabel?: string;
266
+ }
267
+
268
+ export interface Dimension {
269
+ options: Record<string, DimensionOption>;
270
+ choiceFieldLabel?: string;
271
+ }
272
+
273
+ export type DimensionConfiguration = Record<string, Dimension>;
274
+
275
+ export type ChainableAPI = {
276
+ setDimension: (key: string) => DimensionAPI;
277
+ };
278
+
279
+ export interface DimensionAPI {
280
+ setOption: (key: string) => OptionAPI;
281
+ setDimension: (key: string) => DimensionAPI;
282
+ label: (label: string) => DimensionAPI;
283
+ createDimensionChoiceField: () => DimensionAPI;
284
+ }
285
+
286
+ export interface OptionAPI extends DimensionAPI {
287
+ setProps: (styleProps: Record<string, unknown>) => OptionAPI;
288
+ label: (label: string) => OptionAPI;
289
+ setOption: (key: string) => OptionAPI;
290
+ }
291
+
292
+ export type DimensionChoiceField = (
293
+ props: Omit<Partial<ChoiceFieldType>, 'choices'>
294
+ ) => React.ReactElement;
295
+
296
+ export interface ComponentState {
297
+ dimensionConfiguration: DimensionConfiguration;
298
+ userConfiguration: DimensionConfiguration | null;
299
+ dimensionLabels: Record<string, string>;
300
+ optionLabels: Record<string, Record<string, string>>;
301
+ currentDimension: string | null;
302
+ dimensionChoiceFields: Record<string, DimensionChoiceField>;
303
+ }
304
+
305
+ export type ComponentInstanceFactory<T extends AnyReactComponent> =
306
+ ChainableAPI & {
307
+ (props: InferPublicProps<T>): React.ReactElement;
308
+ };
@@ -0,0 +1,64 @@
1
+ import { ChoiceField, ChoiceFieldType } from '@hubspot/cms-components/fields';
2
+ import {
3
+ generateChoicesWithLabels,
4
+ getDimensionLabel,
5
+ } from './labelGenerator.js';
6
+ import {
7
+ DimensionChoiceField,
8
+ DimensionConfiguration,
9
+ } from '../../types/index.js';
10
+
11
+ /**
12
+ * Creates prebuilt choice field components with enhanced labeling
13
+ */
14
+ export const createDimensionChoiceField = (
15
+ configuration: DimensionConfiguration,
16
+ dimensionLabels: Record<string, string>,
17
+ optionLabels: Record<string, Record<string, string>>
18
+ ) => {
19
+ const dimensionChoiceFields: Record<string, DimensionChoiceField> = {};
20
+
21
+ for (const [dimensionKey, dimension] of Object.entries(configuration)) {
22
+ if (!dimension?.options) {
23
+ continue;
24
+ }
25
+
26
+ const dimensionLabel = getDimensionLabel(dimensionKey, dimensionLabels);
27
+ const choices = generateChoicesWithLabels(
28
+ dimensionKey,
29
+ dimension.options,
30
+ optionLabels
31
+ );
32
+
33
+ // Create prebuilt component for this dimension
34
+ dimensionChoiceFields[dimensionKey] = (props: Partial<ChoiceFieldType>) => {
35
+ const { name, label, ...restProps } = props;
36
+
37
+ return (
38
+ <ChoiceField
39
+ {...restProps}
40
+ name={name || dimensionKey}
41
+ label={label || dimensionLabel}
42
+ choices={choices}
43
+ />
44
+ );
45
+ };
46
+ }
47
+
48
+ return dimensionChoiceFields;
49
+ };
50
+
51
+ /**
52
+ * Creates a choice field generator factory with predefined configuration
53
+ * Returns functions that have access to the provided configuration and labels
54
+ */
55
+ export const createChoiceFieldGenerator = (
56
+ configuration: DimensionConfiguration,
57
+ dimensionLabels: Record<string, string>,
58
+ optionLabels: Record<string, Record<string, string>>
59
+ ) => {
60
+ return {
61
+ createDimensionChoiceField: () =>
62
+ createDimensionChoiceField(configuration, dimensionLabels, optionLabels),
63
+ };
64
+ };