@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,115 @@
1
+ import {
2
+ DimensionAPI,
3
+ OptionAPI,
4
+ DimensionConfiguration,
5
+ } from '../../types/index.js';
6
+ import { createStateManager } from './stateManager.js';
7
+ import { createChoiceFieldGenerator } from './choiceFieldGenerator.js';
8
+
9
+ const createDimensionAPI = (
10
+ stateManager: ReturnType<typeof createStateManager>,
11
+ currentDimension?: string
12
+ ): DimensionAPI => {
13
+ return {
14
+ setDimension: (dimensionKey: string): DimensionAPI => {
15
+ stateManager.setCurrentDimension(dimensionKey);
16
+ return createDimensionAPI(stateManager, dimensionKey);
17
+ },
18
+
19
+ label: (label: string): DimensionAPI => {
20
+ if (!currentDimension) {
21
+ throw new Error('Must call setDimension before setting a label');
22
+ }
23
+ stateManager.setDimensionLabel(currentDimension, label);
24
+ return createDimensionAPI(stateManager, currentDimension);
25
+ },
26
+
27
+ setOption: (optionKey: string): OptionAPI => {
28
+ if (!currentDimension) {
29
+ throw new Error('Must call setDimension before adding options');
30
+ }
31
+
32
+ stateManager.addOptionToCurrentDimension(optionKey);
33
+ return createOptionAPI(stateManager, currentDimension, optionKey);
34
+ },
35
+
36
+ createDimensionChoiceField: () => {
37
+ if (!stateManager.getCurrentDimension()) {
38
+ throw new Error(
39
+ 'Must call setDimension before createDimensionChoiceField'
40
+ );
41
+ }
42
+
43
+ const dimensionKey = stateManager.getCurrentDimension();
44
+ const dimensionTree = stateManager.getActiveConfiguration()[dimensionKey];
45
+
46
+ if (!dimensionTree || !dimensionTree.options) {
47
+ throw new Error(`No options found for dimension: ${dimensionKey}`);
48
+ }
49
+
50
+ // Create choice field generator for single dimension
51
+ const singleDimensionConfig = { [dimensionKey]: dimensionTree };
52
+ const choiceFieldGenerator = createChoiceFieldGenerator(
53
+ singleDimensionConfig,
54
+ stateManager.getDimensionLabels(),
55
+ stateManager.getOptionLabels()
56
+ );
57
+
58
+ const builtChoiceField =
59
+ choiceFieldGenerator.createDimensionChoiceField();
60
+
61
+ stateManager.setDimensionChoiceField(
62
+ dimensionKey,
63
+ builtChoiceField[dimensionKey]
64
+ );
65
+
66
+ return createDimensionAPI(stateManager, dimensionKey);
67
+ },
68
+ };
69
+ };
70
+
71
+ const createOptionAPI = (
72
+ stateManager: ReturnType<typeof createStateManager>,
73
+ dimensionKey: string,
74
+ optionKey: string
75
+ ): OptionAPI => {
76
+ // Option API extends dimension API functionality
77
+ const dimensionAPI = createDimensionAPI(stateManager, dimensionKey);
78
+
79
+ return {
80
+ ...dimensionAPI,
81
+
82
+ setProps: (styleProps: Record<string, unknown>): OptionAPI => {
83
+ stateManager.setOptionProps(dimensionKey, optionKey, styleProps);
84
+ return createOptionAPI(stateManager, dimensionKey, optionKey);
85
+ },
86
+
87
+ label: (label: string): OptionAPI => {
88
+ stateManager.setOptionLabel(dimensionKey, optionKey, label);
89
+ return createOptionAPI(stateManager, dimensionKey, optionKey);
90
+ },
91
+
92
+ setOption: (newOptionKey: string): OptionAPI => {
93
+ stateManager.addOptionToCurrentDimension(newOptionKey);
94
+ return createOptionAPI(stateManager, dimensionKey, newOptionKey);
95
+ },
96
+ };
97
+ };
98
+
99
+ export const createChainApi = (
100
+ stateManager: ReturnType<typeof createStateManager>
101
+ ) => {
102
+ const baseDimensionAPI = createDimensionAPI(stateManager);
103
+
104
+ return {
105
+ setDimension: baseDimensionAPI.setDimension,
106
+ getActiveConfiguration: (): DimensionConfiguration => {
107
+ return stateManager.getActiveConfiguration();
108
+ },
109
+ getCurrentDimension: (): string | null => {
110
+ // Note: This assumes stateManager exposes getCurrentDimension
111
+ // If not available, this can be removed or stateManager can be enhanced
112
+ return null;
113
+ },
114
+ };
115
+ };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Generates a default label from a key by converting:
3
+ * - camelCase: 'extraLarge' → 'Extra Large'
4
+ * - snake_case: 'primary_button' → 'Primary Button'
5
+ * - kebab-case: 'primary-button' → 'Primary Button'
6
+ */
7
+ export const generateDefaultLabel = (key: string): string => {
8
+ return key
9
+ .replace(/([A-Z])/g, ' $1') // camelCase: 'extraLarge' → 'extra Large'
10
+ .replace(/[_-]/g, ' ') // snake_case/kebab: 'primary_button' → 'primary button'
11
+ .replace(/\b\w/g, letter => letter.toUpperCase()) // Title case: 'extra large' → 'Extra Large'
12
+ .trim();
13
+ };
14
+
15
+ /**
16
+ * Gets the label for a dimension, using stored label or auto-generating
17
+ */
18
+ export const getDimensionLabel = (
19
+ dimensionKey: string,
20
+ dimensionLabels: Record<string, string>
21
+ ): string => {
22
+ return dimensionLabels[dimensionKey] || generateDefaultLabel(dimensionKey);
23
+ };
24
+
25
+ /**
26
+ * Gets the label for an option, using stored label or auto-generating
27
+ */
28
+ export const getOptionLabel = (
29
+ dimensionKey: string,
30
+ optionKey: string,
31
+ optionLabels: Record<string, Record<string, string>>
32
+ ): string => {
33
+ return (
34
+ optionLabels[dimensionKey]?.[optionKey] || generateDefaultLabel(optionKey)
35
+ );
36
+ };
37
+
38
+ /**
39
+ * Generates choices array with labels for a dimension
40
+ */
41
+ export const generateChoicesWithLabels = (
42
+ dimensionKey: string,
43
+ options: Record<string, unknown>,
44
+ optionLabels: Record<string, Record<string, string>>
45
+ ): [string, string][] => {
46
+ const choices: [string, string][] = [];
47
+
48
+ for (const optionKey in options) {
49
+ const optionLabel = getOptionLabel(dimensionKey, optionKey, optionLabels);
50
+ choices.push([optionKey, optionLabel]);
51
+ }
52
+
53
+ return choices;
54
+ };
55
+
56
+ /**
57
+ * Creates a label generator factory with predefined labels
58
+ * Returns functions that have access to the provided label stores
59
+ */
60
+ export const createLabelGenerator = (
61
+ dimensionLabels: Record<string, string>,
62
+ optionLabels: Record<string, Record<string, string>>
63
+ ) => {
64
+ return {
65
+ getDimensionLabel: (dimensionKey: string) =>
66
+ getDimensionLabel(dimensionKey, dimensionLabels),
67
+
68
+ getOptionLabel: (dimensionKey: string, optionKey: string) =>
69
+ getOptionLabel(dimensionKey, optionKey, optionLabels),
70
+
71
+ generateChoicesWithLabels: (
72
+ dimensionKey: string,
73
+ options: Record<string, unknown>
74
+ ) => generateChoicesWithLabels(dimensionKey, options, optionLabels),
75
+ };
76
+ };
@@ -0,0 +1,178 @@
1
+ import {
2
+ ComponentState,
3
+ DimensionChoiceField,
4
+ DimensionConfiguration,
5
+ } from '../../types/index.js';
6
+
7
+ function isNotValidName(name: string): boolean {
8
+ return /[\s-]/.test(name);
9
+ }
10
+
11
+ /**
12
+ * Creates initial component state
13
+ */
14
+ const createInitialState = (): ComponentState => ({
15
+ dimensionConfiguration: {},
16
+ userConfiguration: null,
17
+ dimensionLabels: {},
18
+ optionLabels: {},
19
+ currentDimension: null,
20
+ dimensionChoiceFields: {},
21
+ });
22
+
23
+ /**
24
+ * Creates a state manager instance with encapsulated state
25
+ */
26
+ export const createStateManager = () => {
27
+ const state = createInitialState();
28
+
29
+ /**
30
+ * Updates the working configuration and syncs user configuration
31
+ */
32
+ const updateDimensionConfiguration = (
33
+ updater: (config: DimensionConfiguration) => DimensionConfiguration
34
+ ): void => {
35
+ state.dimensionConfiguration = updater(state.dimensionConfiguration);
36
+ state.userConfiguration = { ...state.dimensionConfiguration };
37
+ };
38
+
39
+ /**
40
+ * Sets the current dimension in the state
41
+ */
42
+ const setCurrentDimension = (dimensionKey: string): void => {
43
+ state.currentDimension = dimensionKey;
44
+
45
+ if (isNotValidName(dimensionKey)) {
46
+ throw new Error(
47
+ `\nDimension name cannot contain spaces or hyphens. Please use camelCase or snake_case.\nDimension: ${dimensionKey}`
48
+ );
49
+ }
50
+
51
+ // Initialize dimension if it doesn't exist
52
+ if (!state.dimensionConfiguration[dimensionKey]) {
53
+ updateDimensionConfiguration(config => ({
54
+ ...config,
55
+ [dimensionKey]: { options: {} },
56
+ }));
57
+ }
58
+
59
+ // Initialize option labels for this dimension
60
+ if (!state.optionLabels[dimensionKey]) {
61
+ state.optionLabels[dimensionKey] = {};
62
+ }
63
+ };
64
+
65
+ /**
66
+ * Sets a dimension label
67
+ */
68
+ const setDimensionLabel = (dimensionKey: string, label: string): void => {
69
+ state.dimensionLabels[dimensionKey] = label;
70
+ };
71
+
72
+ /**
73
+ * Sets an option label
74
+ */
75
+ const setOptionLabel = (
76
+ dimensionKey: string,
77
+ optionKey: string,
78
+ label: string
79
+ ): void => {
80
+ if (!state.optionLabels[dimensionKey]) {
81
+ state.optionLabels[dimensionKey] = {};
82
+ }
83
+ state.optionLabels[dimensionKey][optionKey] = label;
84
+ };
85
+
86
+ /**
87
+ * Adds an option to the current dimension
88
+ */
89
+ const addOptionToCurrentDimension = (optionKey: string): void => {
90
+ if (!state.currentDimension) {
91
+ throw new Error('Must call setDimension before adding options');
92
+ }
93
+
94
+ if (isNotValidName(optionKey)) {
95
+ throw new Error(
96
+ `\nOption name cannot contain spaces or hyphens. Please use camelCase or snake_case.\nOption: ${optionKey}`
97
+ );
98
+ }
99
+
100
+ updateDimensionConfiguration(config => ({
101
+ ...config,
102
+ [state.currentDimension!]: {
103
+ ...config[state.currentDimension!],
104
+ options: {
105
+ ...config[state.currentDimension!].options,
106
+ [optionKey]: { props: {} },
107
+ },
108
+ },
109
+ }));
110
+ };
111
+
112
+ /**
113
+ * Sets props for a specific option
114
+ */
115
+ const setOptionProps = (
116
+ dimensionKey: string,
117
+ optionKey: string,
118
+ props: Record<string, unknown>
119
+ ): void => {
120
+ updateDimensionConfiguration(config => ({
121
+ ...config,
122
+ [dimensionKey]: {
123
+ ...config[dimensionKey],
124
+ options: {
125
+ ...config[dimensionKey].options,
126
+ [optionKey]: {
127
+ ...config[dimensionKey].options[optionKey],
128
+ props,
129
+ },
130
+ },
131
+ },
132
+ }));
133
+ };
134
+
135
+ const setDimensionChoiceField = (
136
+ dimensionKey: string,
137
+ choiceField: DimensionChoiceField
138
+ ): void => {
139
+ state.dimensionChoiceFields[dimensionKey] = choiceField;
140
+ };
141
+
142
+ const getCurrentDimension = (): string => {
143
+ return state.currentDimension || '';
144
+ };
145
+
146
+ const getDimensionLabels = (): Record<string, string> => {
147
+ return state.dimensionLabels;
148
+ };
149
+
150
+ const getOptionLabels = (): Record<string, Record<string, string>> => {
151
+ return state.optionLabels;
152
+ };
153
+
154
+ const getDimensionChoiceFields = (): Record<string, DimensionChoiceField> => {
155
+ return state.dimensionChoiceFields;
156
+ };
157
+
158
+ /**
159
+ * Gets the active configuration (user config if available, otherwise empty)
160
+ */
161
+ const getActiveConfiguration = (): DimensionConfiguration => {
162
+ return state.userConfiguration || {};
163
+ };
164
+
165
+ return {
166
+ addOptionToCurrentDimension,
167
+ setCurrentDimension,
168
+ setDimensionLabel,
169
+ setOptionLabel,
170
+ setOptionProps,
171
+ setDimensionChoiceField,
172
+ getActiveConfiguration,
173
+ getCurrentDimension,
174
+ getDimensionLabels,
175
+ getOptionLabels,
176
+ getDimensionChoiceFields,
177
+ };
178
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Combines multiple class name inputs into a single space-separated string.
3
+ * Supports strings, arrays, and objects where keys with truthy values are included.
4
+ * All whitespace is properly trimmed and empty values are excluded.
5
+ * Basically this is a replacement for the classnames library.
6
+ */
7
+ export default function cx(...args: unknown[]): string {
8
+ const classes: string[] = [];
9
+
10
+ // Process all arguments
11
+ args.flat(Infinity).forEach(arg => {
12
+ // Skip falsy values early
13
+ if (!arg) return;
14
+
15
+ // Handle string arguments
16
+ if (typeof arg === 'string') {
17
+ const trimmed = arg.trim();
18
+ if (trimmed) {
19
+ classes.push(trimmed);
20
+ }
21
+ return;
22
+ }
23
+
24
+ // Handle object arguments (className: condition pairs)
25
+ if (typeof arg === 'object' && arg !== null && !Array.isArray(arg)) {
26
+ Object.entries(arg as Record<string, unknown>).forEach(([key, value]) => {
27
+ // Only include keys with truthy values
28
+ if (value) {
29
+ const trimmed = key.trim();
30
+ if (trimmed) {
31
+ classes.push(trimmed);
32
+ }
33
+ }
34
+ });
35
+ }
36
+ });
37
+
38
+ // Join all classes with a single space
39
+ return classes.join(' ');
40
+ }
@@ -0,0 +1,44 @@
1
+ import { convertToKebabCase } from './propResolution/createCssVariables.js';
2
+ import { STYLE_COMPONENT_PROPS, BaseComponentProps } from '../types/index.js';
3
+
4
+ /**
5
+ * Creates conditional CSS classes based on which props have values.
6
+ * Only generates classes for props that are defined and not null/undefined.
7
+ *
8
+ * @param props - Component props to analyze
9
+ * @param componentName - Name of the component for class naming
10
+ * @returns Array of conditional class names
11
+ *
12
+ * @example
13
+ * createConditionalClasses({ color: 'red', fontSize: '16px' }, 'heading')
14
+ * // Returns: ['color', 'font-size']
15
+ */
16
+ export const createConditionalClasses = (
17
+ props: Partial<BaseComponentProps>,
18
+ componentName?: string | null
19
+ ): string[] => {
20
+ if (!componentName) return [];
21
+
22
+ const conditionalClasses: string[] = [];
23
+
24
+ // Iterate through passed props that have values
25
+ for (const propName of Object.keys(props)) {
26
+ const propValue = props[propName as keyof BaseComponentProps];
27
+
28
+ // Skip if prop has no value
29
+ if (propValue === undefined || propValue === null) continue;
30
+
31
+ // Only process props that are defined in STYLE_COMPONENT_PROPS
32
+ if (!(propName in STYLE_COMPONENT_PROPS)) continue;
33
+
34
+ // Skip non-primitive values (objects, arrays, functions)
35
+ if (typeof propValue !== 'string' && typeof propValue !== 'number')
36
+ continue;
37
+
38
+ // Generate the conditional class name using existing kebab case conversion
39
+ const kebabProp = convertToKebabCase(propName);
40
+ conditionalClasses.push(`hsclStyle-${kebabProp}`);
41
+ }
42
+
43
+ return conditionalClasses;
44
+ };
@@ -0,0 +1,167 @@
1
+ import React from 'react';
2
+ import {
3
+ AnyReactComponent,
4
+ ComponentPropsType,
5
+ DimensionAPI,
6
+ DimensionChoiceField,
7
+ InferPublicProps,
8
+ } from '../types/index.js';
9
+ import { resolveAllProps } from './propResolution/propResolutionUtils.js';
10
+ import { createCSSVariables } from './propResolution/createCssVariables.js';
11
+ import { createConditionalClasses } from './createConditionalClasses.js';
12
+ import cx from './classname.js';
13
+ import { createStateManager } from './chainApi/stateManager.js';
14
+ import { createChainApi } from './chainApi/index.js';
15
+
16
+ // Helper function for creating typed instances with custom props
17
+ export const createComponentInstance = <P,>(
18
+ instance: CreateHsclComponentType<AnyReactComponent>
19
+ ): CreateHsclComponentType<AnyReactComponent, P> => {
20
+ return instance.create() as CreateHsclComponentType<AnyReactComponent, P>;
21
+ };
22
+
23
+ type FieldsReturn<T> = (props: T) => React.ReactElement;
24
+
25
+ type NullableFieldsReturn<T> = FieldsReturn<T> | null | undefined;
26
+
27
+ type FieldsGetter<T> = T extends never ? undefined : NullableFieldsReturn<T>;
28
+
29
+ export type CreateHsclComponentType<
30
+ T extends AnyReactComponent,
31
+ P = InferPublicProps<T>,
32
+ CFP = never,
33
+ SFP = never
34
+ > = ((props: P) => React.ReactElement) & {
35
+ ContentFields: FieldsGetter<CFP>;
36
+ StyleFields: FieldsGetter<SFP>;
37
+
38
+ // Field methods return new instances (immutable)
39
+ withContentFields<CF>(
40
+ contentFieldsFunction: FieldsReturn<CF>
41
+ ): CreateHsclComponentType<T, P, CF, SFP>;
42
+ withStyleFields<SF>(
43
+ styleFieldsFunction: FieldsReturn<SF>
44
+ ): CreateHsclComponentType<T, P, CFP, SF>;
45
+ DimensionChoiceFields: Record<string, DimensionChoiceField>;
46
+ setDimension: (arg: string) => DimensionAPI;
47
+ create: () => CreateHsclComponentType<T, P, CFP, SFP>;
48
+ };
49
+
50
+ const createHsclComponent = <
51
+ T extends AnyReactComponent,
52
+ CFP = never,
53
+ SFP = never
54
+ >(
55
+ component: T,
56
+ contentFields: NullableFieldsReturn<CFP> = null,
57
+ styleFields: NullableFieldsReturn<SFP> = null,
58
+ stateManager = createStateManager()
59
+ ): CreateHsclComponentType<T, InferPublicProps<T>, CFP, SFP> => {
60
+ const componentName: string | null = component.hsclComponentName;
61
+ const componentCssModuleStyles = component.cssModule[componentName];
62
+
63
+ const processedComponent = (props: InferPublicProps<T>) => {
64
+ // Resolve props with 3-way separation if component has propKeys defined
65
+ const resolvedProps = resolveAllProps(stateManager, props);
66
+
67
+ const cssVars = createCSSVariables(
68
+ resolvedProps.baseResolvedProps,
69
+ componentName
70
+ );
71
+
72
+ // Generate conditional classes based on which props have values
73
+ const conditionalClasses = createConditionalClasses(
74
+ resolvedProps.baseResolvedProps,
75
+ componentName
76
+ );
77
+
78
+ // process and standardize classnames
79
+ const processedClasses = cx(
80
+ 'hscl-component',
81
+ `hscl-${componentName}`,
82
+ resolvedProps.baseResolvedProps.className,
83
+ componentCssModuleStyles,
84
+ ...conditionalClasses
85
+ );
86
+
87
+ // Build hsclInternal props for component (hidden from public)
88
+ const hsclInternal = {
89
+ hsclProcessedClasses: processedClasses,
90
+ hsclResolvedProps: resolvedProps.baseResolvedProps,
91
+ hsclCssVars: cssVars,
92
+ hsclProcessedStyles: {
93
+ ...resolvedProps.baseResolvedProps.style,
94
+ ...cssVars,
95
+ },
96
+ };
97
+
98
+ // combine with all other props
99
+ const propsToPassAlongToComponent = {
100
+ ...resolvedProps.remainingProps,
101
+ hsclInternal,
102
+ };
103
+
104
+ return React.createElement(
105
+ component,
106
+ propsToPassAlongToComponent as ComponentPropsType<T>
107
+ );
108
+ };
109
+
110
+ const chainableAPI = createChainApi(stateManager);
111
+
112
+ // Create the API methods object
113
+ const apiMethods = {
114
+ get ContentFields(): FieldsGetter<CFP> {
115
+ return contentFields as FieldsGetter<CFP>;
116
+ },
117
+
118
+ get StyleFields(): FieldsGetter<SFP> {
119
+ return styleFields as FieldsGetter<SFP>;
120
+ },
121
+
122
+ get DimensionChoiceFields() {
123
+ return stateManager.getDimensionChoiceFields() as Record<
124
+ string,
125
+ DimensionChoiceField
126
+ >;
127
+ },
128
+
129
+ // Field methods return NEW instances (immutable pattern)
130
+ withContentFields<CF>(contentFieldsFunction: FieldsReturn<CF>) {
131
+ return createHsclComponent<T, CF, SFP>(
132
+ component,
133
+ contentFieldsFunction,
134
+ styleFields,
135
+ stateManager
136
+ );
137
+ },
138
+
139
+ withStyleFields<SF>(styleFieldsFunction: FieldsReturn<SF>) {
140
+ return createHsclComponent<T, CFP, SF>(
141
+ component,
142
+ contentFields,
143
+ styleFieldsFunction,
144
+ stateManager
145
+ );
146
+ },
147
+
148
+ // Chain API methods keep mutable pattern (same stateManager)
149
+ setDimension: chainableAPI.setDimension,
150
+
151
+ create() {
152
+ return createHsclComponent<T, CFP, SFP>(
153
+ component,
154
+ contentFields,
155
+ styleFields,
156
+ createStateManager()
157
+ );
158
+ },
159
+ };
160
+
161
+ // Create a callable component function with attached properties
162
+ const callableComponent = Object.assign(processedComponent, apiMethods);
163
+
164
+ return callableComponent;
165
+ };
166
+
167
+ export default createHsclComponent;
@@ -0,0 +1,58 @@
1
+ import {
2
+ BaseComponentProps,
3
+ STYLE_COMPONENT_PROPS,
4
+ CSSVariableMap,
5
+ } from '../../types/index.js';
6
+
7
+ const shouldConvertToString = (value: string | number): boolean =>
8
+ typeof value === 'number' ||
9
+ (typeof value === 'string' && /^\d+$/.test(value));
10
+
11
+ export const convertToKebabCase = (camelCaseString: string): string =>
12
+ camelCaseString.replace(
13
+ /[A-Z]/g,
14
+ capitalLetter => `-${capitalLetter.toLowerCase()}`
15
+ );
16
+
17
+ export const convertValueToString = (value: string | number): string => {
18
+ if (typeof value === 'number') {
19
+ return `${value}`;
20
+ }
21
+ return value;
22
+ };
23
+
24
+ export const createCSSVariables = (
25
+ props: Partial<BaseComponentProps>,
26
+ componentName?: string
27
+ ): CSSVariableMap => {
28
+ const cssVariables: CSSVariableMap = {};
29
+
30
+ // Determine the prefix for CSS variable names
31
+ const variablePrefix = componentName
32
+ ? `--hscl-${componentName}`
33
+ : '--hscl-component';
34
+
35
+ // Process only style-related props using for...of for better readability
36
+ for (const propName of Object.keys(STYLE_COMPONENT_PROPS)) {
37
+ const propValue = props[propName as keyof BaseComponentProps];
38
+
39
+ // Skip undefined values
40
+ if (propValue === undefined) continue;
41
+
42
+ // Skip non-primitive values (like CSSProperties objects)
43
+ if (typeof propValue !== 'string' && typeof propValue !== 'number')
44
+ continue;
45
+
46
+ // Generate CSS variable name with component-specific or generic prefix
47
+ const variableName = `${variablePrefix}-${convertToKebabCase(propName)}`;
48
+
49
+ // Convert value to appropriate format
50
+ const finalValue = shouldConvertToString(propValue)
51
+ ? convertValueToString(propValue)
52
+ : propValue;
53
+
54
+ cssVariables[variableName] = finalValue;
55
+ }
56
+
57
+ return cssVariables;
58
+ };