@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.
- package/README.md +3 -0
- package/cli/commands/customize.ts +145 -0
- package/cli/commands/help.ts +56 -0
- package/cli/commands/version.ts +12 -0
- package/cli/index.ts +42 -0
- package/cli/tests/commands.test.ts +128 -0
- package/cli/tests/get-file.test.ts +82 -0
- package/cli/tests/version-integration.test.ts +39 -0
- package/cli/utils/cli-metadata.ts +9 -0
- package/cli/utils/component-naming.ts +76 -0
- package/cli/utils/components.ts +74 -0
- package/cli/utils/file-operations.ts +158 -0
- package/cli/utils/logging.ts +13 -0
- package/cli/utils/prompts.ts +80 -0
- package/cli/utils/version.ts +33 -0
- package/components/componentLibrary/Button/index.module.scss +9 -0
- package/components/componentLibrary/Button/index.tsx +83 -0
- package/components/componentLibrary/Button/scaffolds/fields.tsx.template +70 -0
- package/components/componentLibrary/Button/scaffolds/index.ts.template +95 -0
- package/components/componentLibrary/Heading/index.module.scss +9 -0
- package/components/componentLibrary/Heading/index.tsx +34 -0
- package/components/componentLibrary/Heading/scaffolds/fields.tsx.template +62 -0
- package/components/componentLibrary/Heading/scaffolds/index.ts.template +46 -0
- package/components/componentLibrary/index.ts +1 -0
- package/components/componentLibrary/styles/_component-base.scss +246 -0
- package/components/componentLibrary/types/index.ts +308 -0
- package/components/componentLibrary/utils/chainApi/choiceFieldGenerator.tsx +64 -0
- package/components/componentLibrary/utils/chainApi/index.ts +115 -0
- package/components/componentLibrary/utils/chainApi/labelGenerator.ts +76 -0
- package/components/componentLibrary/utils/chainApi/stateManager.ts +178 -0
- package/components/componentLibrary/utils/classname.ts +40 -0
- package/components/componentLibrary/utils/createConditionalClasses.ts +44 -0
- package/components/componentLibrary/utils/createHsclComponent.tsx +167 -0
- package/components/componentLibrary/utils/propResolution/createCssVariables.ts +58 -0
- package/components/componentLibrary/utils/propResolution/propResolutionUtils.ts +113 -0
- package/components/componentLibrary/utils/storybook/standardArgs.ts +607 -0
- 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
|
+
};
|