meno-core 1.0.47 → 1.0.49
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/build-astro.ts +2 -2
- package/dist/build-static.js +7 -7
- package/dist/chunks/{chunk-UUA5LEWF.js → chunk-6IVUG7FY.js} +138 -7
- package/dist/chunks/chunk-6IVUG7FY.js.map +7 -0
- package/dist/chunks/{chunk-XSWR3QLI.js → chunk-AZQYF6KE.js} +261 -130
- package/dist/chunks/chunk-AZQYF6KE.js.map +7 -0
- package/dist/chunks/{chunk-47UNLQUU.js → chunk-CHD5UCFF.js} +57 -12
- package/dist/chunks/chunk-CHD5UCFF.js.map +7 -0
- package/dist/chunks/{chunk-FGUZOYJX.js → chunk-EQYDSPBB.js} +435 -131
- package/dist/chunks/chunk-EQYDSPBB.js.map +7 -0
- package/dist/chunks/{chunk-IF3RATBY.js → chunk-H4JSCDNW.js} +2 -2
- package/dist/chunks/{chunk-KITQJYZV.js → chunk-J23ZX5AP.js} +40 -4
- package/dist/chunks/chunk-J23ZX5AP.js.map +7 -0
- package/dist/chunks/{chunk-LJFB5EBT.js → chunk-JER5NQVM.js} +5 -5
- package/dist/chunks/{chunk-ZTKHJQ2Z.js → chunk-KPU2XHOS.js} +5 -2
- package/dist/chunks/{chunk-ZTKHJQ2Z.js.map → chunk-KPU2XHOS.js.map} +2 -2
- package/dist/chunks/{chunk-BCLGRZ3U.js → chunk-LKAGAQ3M.js} +2 -2
- package/dist/chunks/{chunk-FED5MME6.js → chunk-S2CX6HFM.js} +262 -26
- package/dist/chunks/chunk-S2CX6HFM.js.map +7 -0
- package/dist/chunks/{configService-DYCUEURL.js → configService-CCA6AIDI.js} +3 -3
- package/dist/entries/server-router.js +9 -9
- package/dist/entries/server-router.js.map +2 -2
- package/dist/lib/client/index.js +64 -20
- package/dist/lib/client/index.js.map +3 -3
- package/dist/lib/server/index.js +1737 -296
- package/dist/lib/server/index.js.map +4 -4
- package/dist/lib/shared/index.js +50 -10
- package/dist/lib/shared/index.js.map +3 -3
- package/entries/server-router.tsx +6 -2
- package/lib/client/core/ComponentBuilder.test.ts +17 -0
- package/lib/client/core/ComponentBuilder.ts +25 -1
- package/lib/client/core/builders/embedBuilder.ts +15 -2
- package/lib/client/core/builders/linkNodeBuilder.ts +15 -2
- package/lib/client/core/builders/localeListBuilder.ts +17 -6
- package/lib/client/styles/StyleInjector.ts +3 -2
- package/lib/client/theme.ts +4 -4
- package/lib/server/cssGenerator.test.ts +64 -1
- package/lib/server/cssGenerator.ts +48 -9
- package/lib/server/index.ts +1 -1
- package/lib/server/jsonLoader.test.ts +0 -17
- package/lib/server/jsonLoader.ts +0 -81
- package/lib/server/providers/fileSystemCMSProvider.test.ts +163 -0
- package/lib/server/providers/fileSystemCMSProvider.ts +200 -11
- package/lib/server/routes/api/variables.ts +4 -2
- package/lib/server/routes/index.ts +1 -1
- package/lib/server/routes/pages.ts +23 -1
- package/lib/server/services/cmsService.test.ts +246 -0
- package/lib/server/services/cmsService.ts +122 -5
- package/lib/server/services/configService.ts +5 -0
- package/lib/server/ssr/attributeBuilder.ts +41 -0
- package/lib/server/ssr/htmlGenerator.test.ts +114 -2
- package/lib/server/ssr/htmlGenerator.ts +53 -6
- package/lib/server/ssr/liveReloadIntegration.test.ts +209 -0
- package/lib/server/ssr/ssrRenderer.test.ts +362 -1
- package/lib/server/ssr/ssrRenderer.ts +216 -72
- package/lib/server/utils/jsonLineMapper.test.ts +53 -1
- package/lib/server/utils/jsonLineMapper.ts +43 -3
- package/lib/server/webflow/buildWebflow.ts +343 -123
- package/lib/server/webflow/index.ts +1 -0
- package/lib/server/webflow/nodeToWebflow.test.ts +3170 -0
- package/lib/server/webflow/nodeToWebflow.ts +2141 -129
- package/lib/server/webflow/styleMapper.test.ts +389 -0
- package/lib/server/webflow/styleMapper.ts +517 -63
- package/lib/server/webflow/templateWrapper.ts +49 -0
- package/lib/server/webflow/types.ts +218 -18
- package/lib/shared/cssGeneration.test.ts +267 -1
- package/lib/shared/cssGeneration.ts +240 -18
- package/lib/shared/cssProperties.test.ts +247 -1
- package/lib/shared/cssProperties.ts +196 -6
- package/lib/shared/elementClassName.test.ts +15 -0
- package/lib/shared/elementClassName.ts +7 -3
- package/lib/shared/interfaces/contentProvider.ts +39 -6
- package/lib/shared/pathSecurity.ts +16 -0
- package/lib/shared/registry/nodeTypes/ListNodeType.ts +1 -1
- package/lib/shared/responsiveScaling.test.ts +143 -0
- package/lib/shared/responsiveScaling.ts +253 -2
- package/lib/shared/themeDefaults.test.ts +3 -3
- package/lib/shared/themeDefaults.ts +3 -3
- package/lib/shared/types/cms.ts +28 -3
- package/lib/shared/types/index.ts +2 -0
- package/lib/shared/types/variables.ts +37 -0
- package/lib/shared/utilityClassConfig.ts +3 -0
- package/lib/shared/utilityClassMapper.test.ts +123 -0
- package/lib/shared/utilityClassMapper.ts +179 -8
- package/lib/shared/validation/schemas.ts +15 -1
- package/lib/shared/validation/validators.ts +26 -1
- package/package.json +1 -1
- package/dist/chunks/chunk-47UNLQUU.js.map +0 -7
- package/dist/chunks/chunk-FED5MME6.js.map +0 -7
- package/dist/chunks/chunk-FGUZOYJX.js.map +0 -7
- package/dist/chunks/chunk-KITQJYZV.js.map +0 -7
- package/dist/chunks/chunk-UUA5LEWF.js.map +0 -7
- package/dist/chunks/chunk-XSWR3QLI.js.map +0 -7
- /package/dist/chunks/{chunk-IF3RATBY.js.map → chunk-H4JSCDNW.js.map} +0 -0
- /package/dist/chunks/{chunk-LJFB5EBT.js.map → chunk-JER5NQVM.js.map} +0 -0
- /package/dist/chunks/{chunk-BCLGRZ3U.js.map → chunk-LKAGAQ3M.js.map} +0 -0
- /package/dist/chunks/{configService-DYCUEURL.js.map → configService-CCA6AIDI.js.map} +0 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { mapStylesToWebflow, buildInstanceStyleCombo } from './styleMapper';
|
|
3
|
+
import { DEFAULT_BREAKPOINTS } from '../../shared/breakpoints';
|
|
4
|
+
import type { ResponsiveStyleObject, InteractiveStyles } from '../../shared/types/styles';
|
|
5
|
+
|
|
6
|
+
function baseOf(style: ResponsiveStyleObject) {
|
|
7
|
+
const { primaryClass } = mapStylesToWebflow(
|
|
8
|
+
'c-test',
|
|
9
|
+
style,
|
|
10
|
+
undefined,
|
|
11
|
+
DEFAULT_BREAKPOINTS
|
|
12
|
+
);
|
|
13
|
+
return primaryClass.base;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('styleMapper — shorthand expansion', () => {
|
|
17
|
+
test('1-value margin expands to all four sides', () => {
|
|
18
|
+
const css = baseOf({ base: { margin: '16px' } });
|
|
19
|
+
expect(css.margin).toBeUndefined();
|
|
20
|
+
expect(css['margin-top']).toBe('16px');
|
|
21
|
+
expect(css['margin-right']).toBe('16px');
|
|
22
|
+
expect(css['margin-bottom']).toBe('16px');
|
|
23
|
+
expect(css['margin-left']).toBe('16px');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('2-value padding expands to vertical/horizontal pairs', () => {
|
|
27
|
+
const css = baseOf({ base: { padding: '40px 80px' } });
|
|
28
|
+
expect(css.padding).toBeUndefined();
|
|
29
|
+
expect(css['padding-top']).toBe('40px');
|
|
30
|
+
expect(css['padding-right']).toBe('80px');
|
|
31
|
+
expect(css['padding-bottom']).toBe('40px');
|
|
32
|
+
expect(css['padding-left']).toBe('80px');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('3-value margin: top, horizontal, bottom', () => {
|
|
36
|
+
const css = baseOf({ base: { margin: '10px 20px 30px' } });
|
|
37
|
+
expect(css['margin-top']).toBe('10px');
|
|
38
|
+
expect(css['margin-right']).toBe('20px');
|
|
39
|
+
expect(css['margin-bottom']).toBe('30px');
|
|
40
|
+
expect(css['margin-left']).toBe('20px');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('4-value padding: top, right, bottom, left', () => {
|
|
44
|
+
const css = baseOf({ base: { padding: '1px 2px 3px 4px' } });
|
|
45
|
+
expect(css['padding-top']).toBe('1px');
|
|
46
|
+
expect(css['padding-right']).toBe('2px');
|
|
47
|
+
expect(css['padding-bottom']).toBe('3px');
|
|
48
|
+
expect(css['padding-left']).toBe('4px');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('1-value gap fills both row and column axes', () => {
|
|
52
|
+
const css = baseOf({ base: { gap: '12px' } });
|
|
53
|
+
expect(css.gap).toBeUndefined();
|
|
54
|
+
expect(css['row-gap']).toBe('12px');
|
|
55
|
+
expect(css['column-gap']).toBe('12px');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('2-value gap maps to row then column', () => {
|
|
59
|
+
const css = baseOf({ base: { gap: '8px 24px' } });
|
|
60
|
+
expect(css.gap).toBeUndefined();
|
|
61
|
+
expect(css['row-gap']).toBe('8px');
|
|
62
|
+
expect(css['column-gap']).toBe('24px');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('var(--x) inside a shorthand stays as one token (internal whitespace preserved)', () => {
|
|
66
|
+
const css = baseOf({ base: { padding: 'var(--space, 16px) 0' } });
|
|
67
|
+
expect(css['padding-top']).toBe('var(--space, 16px)');
|
|
68
|
+
expect(css['padding-right']).toBe('0px');
|
|
69
|
+
expect(css['padding-bottom']).toBe('var(--space, 16px)');
|
|
70
|
+
expect(css['padding-left']).toBe('0px');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('calc() inside a shorthand stays as one token (internal whitespace preserved)', () => {
|
|
74
|
+
const css = baseOf({ base: { margin: 'calc(1rem + 2px) auto' } });
|
|
75
|
+
expect(css['margin-top']).toBe('calc(1rem + 2px)');
|
|
76
|
+
expect(css['margin-right']).toBe('auto');
|
|
77
|
+
expect(css['margin-bottom']).toBe('calc(1rem + 2px)');
|
|
78
|
+
expect(css['margin-left']).toBe('auto');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('explicit longhand declared after shorthand wins (CSS cascade)', () => {
|
|
82
|
+
const css = baseOf({ base: { margin: '10px', marginTop: '99px' } });
|
|
83
|
+
expect(css['margin-top']).toBe('99px');
|
|
84
|
+
expect(css['margin-right']).toBe('10px');
|
|
85
|
+
expect(css['margin-bottom']).toBe('10px');
|
|
86
|
+
expect(css['margin-left']).toBe('10px');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('shorthand declared after longhand clobbers it (CSS cascade)', () => {
|
|
90
|
+
const css = baseOf({ base: { marginTop: '99px', margin: '10px' } });
|
|
91
|
+
expect(css['margin-top']).toBe('10px');
|
|
92
|
+
expect(css['margin-right']).toBe('10px');
|
|
93
|
+
expect(css['margin-bottom']).toBe('10px');
|
|
94
|
+
expect(css['margin-left']).toBe('10px');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('numeric padding (StyleObject number) expands to four px sides', () => {
|
|
98
|
+
const css = baseOf({ base: { padding: 24 } as any });
|
|
99
|
+
expect(css['padding-top']).toBe('24px');
|
|
100
|
+
expect(css['padding-right']).toBe('24px');
|
|
101
|
+
expect(css['padding-bottom']).toBe('24px');
|
|
102
|
+
expect(css['padding-left']).toBe('24px');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('malformed 5+ token shorthand passes through unchanged', () => {
|
|
106
|
+
const css = baseOf({ base: { margin: '1px 2px 3px 4px 5px' } });
|
|
107
|
+
expect(css.margin).toBe('1px 2px 3px 4px 5px');
|
|
108
|
+
expect(css['margin-top']).toBeUndefined();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('non-shorthand properties are untouched', () => {
|
|
112
|
+
const css = baseOf({ base: { color: 'red', display: 'flex' } });
|
|
113
|
+
expect(css.color).toBe('red');
|
|
114
|
+
expect(css.display).toBe('flex');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('per-side longhands declared directly are preserved', () => {
|
|
118
|
+
const css = baseOf({ base: { marginTop: '4px', paddingLeft: '8px' } });
|
|
119
|
+
expect(css['margin-top']).toBe('4px');
|
|
120
|
+
expect(css['padding-left']).toBe('8px');
|
|
121
|
+
expect(css.margin).toBeUndefined();
|
|
122
|
+
expect(css.padding).toBeUndefined();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('styleMapper — bare color tokens wrap into var()', () => {
|
|
127
|
+
test('bare token on backgroundColor becomes var(--token)', () => {
|
|
128
|
+
const css = baseOf({ base: { backgroundColor: 'primary' } });
|
|
129
|
+
expect(css['background-color']).toBe('var(--primary)');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('bare token on color becomes var(--token)', () => {
|
|
133
|
+
const css = baseOf({ base: { color: 'text-light' } });
|
|
134
|
+
expect(css.color).toBe('var(--text-light)');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('bare token on borderColor becomes var(--token)', () => {
|
|
138
|
+
const css = baseOf({ base: { borderColor: 'border' } });
|
|
139
|
+
expect(css['border-color']).toBe('var(--border)');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('hex colors pass through unchanged', () => {
|
|
143
|
+
const css = baseOf({ base: { backgroundColor: '#ff0000', color: '#fff' } });
|
|
144
|
+
expect(css['background-color']).toBe('#ff0000');
|
|
145
|
+
expect(css.color).toBe('#fff');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('already-wrapped var() passes through unchanged', () => {
|
|
149
|
+
const css = baseOf({ base: { backgroundColor: 'var(--primary)' } });
|
|
150
|
+
expect(css['background-color']).toBe('var(--primary)');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('CSS-named colors and keywords are not wrapped', () => {
|
|
154
|
+
const css = baseOf({ base: { color: 'red', backgroundColor: 'transparent', borderColor: 'currentColor' } });
|
|
155
|
+
expect(css.color).toBe('red');
|
|
156
|
+
expect(css['background-color']).toBe('transparent');
|
|
157
|
+
expect(css['border-color']).toBe('currentColor');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('functional notations (rgb / rgba / hsl / oklch) pass through unchanged', () => {
|
|
161
|
+
const css = baseOf({ base: { backgroundColor: 'rgba(0,0,0,0.5)', color: 'oklch(70% 0.2 30)' } });
|
|
162
|
+
expect(css['background-color']).toBe('rgba(0,0,0,0.5)');
|
|
163
|
+
expect(css.color).toBe('oklch(70% 0.2 30)');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('non-color properties with string values are untouched', () => {
|
|
167
|
+
const css = baseOf({ base: { display: 'flex', fontSize: '16px' } });
|
|
168
|
+
expect(css.display).toBe('flex');
|
|
169
|
+
expect(css['font-size']).toBe('16px');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('buildInstanceStyleCombo', () => {
|
|
174
|
+
test('flat style folds every authored property into one combo', () => {
|
|
175
|
+
const combo = buildInstanceStyleCombo(
|
|
176
|
+
'is-instance',
|
|
177
|
+
'c-card',
|
|
178
|
+
{ fontSize: '40px', color: 'red' },
|
|
179
|
+
undefined,
|
|
180
|
+
DEFAULT_BREAKPOINTS,
|
|
181
|
+
);
|
|
182
|
+
expect(combo).not.toBeNull();
|
|
183
|
+
expect(combo!.name).toBe('is-instance');
|
|
184
|
+
expect(combo!.comboParent).toBe('c-card');
|
|
185
|
+
expect(combo!.base['font-size']).toBe('40px');
|
|
186
|
+
expect(combo!.base.color).toBe('red');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('responsive style places breakpoint values under the right tier', () => {
|
|
190
|
+
const style: ResponsiveStyleObject = {
|
|
191
|
+
base: { color: 'red' },
|
|
192
|
+
mobile: { color: 'blue' },
|
|
193
|
+
};
|
|
194
|
+
const combo = buildInstanceStyleCombo(
|
|
195
|
+
'is-instance',
|
|
196
|
+
'c-card',
|
|
197
|
+
style,
|
|
198
|
+
undefined,
|
|
199
|
+
DEFAULT_BREAKPOINTS,
|
|
200
|
+
);
|
|
201
|
+
expect(combo).not.toBeNull();
|
|
202
|
+
expect(combo!.base.color).toBe('red');
|
|
203
|
+
expect(combo!.breakpoints?.small?.color).toBe('blue');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('Webflow-handled interactive rules fold into pseudoStates', () => {
|
|
207
|
+
const interactive: InteractiveStyles = [
|
|
208
|
+
{ postfix: ':hover', style: { base: { color: 'green' } } },
|
|
209
|
+
];
|
|
210
|
+
const combo = buildInstanceStyleCombo(
|
|
211
|
+
'is-instance',
|
|
212
|
+
'c-card',
|
|
213
|
+
{ color: 'red' },
|
|
214
|
+
interactive,
|
|
215
|
+
DEFAULT_BREAKPOINTS,
|
|
216
|
+
);
|
|
217
|
+
expect(combo).not.toBeNull();
|
|
218
|
+
expect(combo!.base.color).toBe('red');
|
|
219
|
+
expect(combo!.pseudoStates?.hover?.color).toBe('green');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('interactive-only override produces a combo with no base', () => {
|
|
223
|
+
const interactive: InteractiveStyles = [
|
|
224
|
+
{ postfix: ':hover', style: { base: { color: 'green' } } },
|
|
225
|
+
];
|
|
226
|
+
const combo = buildInstanceStyleCombo(
|
|
227
|
+
'is-instance',
|
|
228
|
+
'c-card',
|
|
229
|
+
undefined,
|
|
230
|
+
interactive,
|
|
231
|
+
DEFAULT_BREAKPOINTS,
|
|
232
|
+
);
|
|
233
|
+
expect(combo).not.toBeNull();
|
|
234
|
+
expect(combo!.pseudoStates?.hover?.color).toBe('green');
|
|
235
|
+
expect(Object.keys(combo!.base)).toHaveLength(0);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('empty inputs return null', () => {
|
|
239
|
+
expect(buildInstanceStyleCombo('is-instance', 'c-card', undefined, undefined, DEFAULT_BREAKPOINTS)).toBeNull();
|
|
240
|
+
expect(buildInstanceStyleCombo('is-instance', 'c-card', {}, [], DEFAULT_BREAKPOINTS)).toBeNull();
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('mapStylesToWebflow — StyleMappings', () => {
|
|
245
|
+
// Mirrors `Button.json` from real projects: two `_mapping` properties bound
|
|
246
|
+
// to the same component prop. Regression: the per-mapping combo fan-out
|
|
247
|
+
// collided in `styleClasses` (Map keyed by combo name) and silently dropped
|
|
248
|
+
// every property but the last-iterated one — so a button-secondary instance
|
|
249
|
+
// got the bg-color delta but lost the font-color delta (or vice versa,
|
|
250
|
+
// depending on JSON declaration order).
|
|
251
|
+
const buttonStyle: ResponsiveStyleObject = {
|
|
252
|
+
base: {
|
|
253
|
+
backgroundColor: {
|
|
254
|
+
_mapping: true,
|
|
255
|
+
prop: 'version',
|
|
256
|
+
values: { default: 'var(--accent)', secondary: 'var(--gray-200)' },
|
|
257
|
+
},
|
|
258
|
+
color: {
|
|
259
|
+
_mapping: true,
|
|
260
|
+
prop: 'version',
|
|
261
|
+
values: { default: 'var(--white)', secondary: 'var(--text)' },
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
test('two mappings on same prop merge into a single combo carrying both deltas', () => {
|
|
267
|
+
const { primaryClass, comboClasses } = mapStylesToWebflow(
|
|
268
|
+
'c-button',
|
|
269
|
+
buttonStyle,
|
|
270
|
+
undefined,
|
|
271
|
+
DEFAULT_BREAKPOINTS,
|
|
272
|
+
undefined,
|
|
273
|
+
{ instanceProps: { version: 'secondary' }, componentDefaults: { version: 'default' } },
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
expect(comboClasses).toHaveLength(1);
|
|
277
|
+
const combo = comboClasses[0]!;
|
|
278
|
+
expect(combo.name).toBe('is-version-secondary');
|
|
279
|
+
expect(combo.comboParent).toBe('c-button');
|
|
280
|
+
expect(combo.base['background-color']).toBe('var(--gray-200)');
|
|
281
|
+
expect(combo.base.color).toBe('var(--text)');
|
|
282
|
+
|
|
283
|
+
expect(primaryClass.base['background-color']).toBe('var(--accent)');
|
|
284
|
+
expect(primaryClass.base.color).toBe('var(--white)');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('default-prop instance bakes defaults into primary and emits no combo', () => {
|
|
288
|
+
const { primaryClass, comboClasses } = mapStylesToWebflow(
|
|
289
|
+
'c-button',
|
|
290
|
+
buttonStyle,
|
|
291
|
+
undefined,
|
|
292
|
+
DEFAULT_BREAKPOINTS,
|
|
293
|
+
undefined,
|
|
294
|
+
{ instanceProps: { version: 'default' }, componentDefaults: { version: 'default' } },
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
expect(comboClasses).toHaveLength(0);
|
|
298
|
+
expect(primaryClass.base['background-color']).toBe('var(--accent)');
|
|
299
|
+
expect(primaryClass.base.color).toBe('var(--white)');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('mappings on different props produce a single combo with a deterministic merged name', () => {
|
|
303
|
+
const style: ResponsiveStyleObject = {
|
|
304
|
+
base: {
|
|
305
|
+
backgroundColor: {
|
|
306
|
+
_mapping: true,
|
|
307
|
+
prop: 'version',
|
|
308
|
+
values: { default: '#000', secondary: '#eee' },
|
|
309
|
+
},
|
|
310
|
+
color: {
|
|
311
|
+
_mapping: true,
|
|
312
|
+
prop: 'tone',
|
|
313
|
+
values: { quiet: '#777', loud: '#f00' },
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
const { comboClasses } = mapStylesToWebflow(
|
|
318
|
+
'c-card',
|
|
319
|
+
style,
|
|
320
|
+
undefined,
|
|
321
|
+
DEFAULT_BREAKPOINTS,
|
|
322
|
+
undefined,
|
|
323
|
+
{
|
|
324
|
+
instanceProps: { version: 'secondary', tone: 'loud' },
|
|
325
|
+
componentDefaults: { version: 'default', tone: 'quiet' },
|
|
326
|
+
},
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
expect(comboClasses).toHaveLength(1);
|
|
330
|
+
const combo = comboClasses[0]!;
|
|
331
|
+
// Sorted segments → stable name across declaration-order changes.
|
|
332
|
+
expect(combo.name).toBe('is-tone-loud-version-secondary');
|
|
333
|
+
expect(combo.base['background-color']).toBe('#eee');
|
|
334
|
+
expect(combo.base.color).toBe('#f00');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test('without instanceProps, defaults still bake into primary and no combos emit', () => {
|
|
338
|
+
const { primaryClass, comboClasses } = mapStylesToWebflow(
|
|
339
|
+
'c-button',
|
|
340
|
+
buttonStyle,
|
|
341
|
+
undefined,
|
|
342
|
+
DEFAULT_BREAKPOINTS,
|
|
343
|
+
undefined,
|
|
344
|
+
{ componentDefaults: { version: 'default' } },
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
expect(comboClasses).toHaveLength(0);
|
|
348
|
+
expect(primaryClass.base['background-color']).toBe('var(--accent)');
|
|
349
|
+
expect(primaryClass.base.color).toBe('var(--white)');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// Regression: two placements of the same component under different ancestor
|
|
353
|
+
// `theme="…"` values used to collapse onto one combo class slot — the last
|
|
354
|
+
// write won, silently mis-rendering the earlier theme's instance. The
|
|
355
|
+
// `themeSuffix` option keeps each theme's combo on a distinct slot.
|
|
356
|
+
test('themeSuffix appended to combo name keeps per-theme combos distinct', () => {
|
|
357
|
+
const { comboClasses } = mapStylesToWebflow(
|
|
358
|
+
'c-button-theme-dark',
|
|
359
|
+
buttonStyle,
|
|
360
|
+
undefined,
|
|
361
|
+
DEFAULT_BREAKPOINTS,
|
|
362
|
+
undefined,
|
|
363
|
+
{
|
|
364
|
+
instanceProps: { version: 'secondary' },
|
|
365
|
+
componentDefaults: { version: 'default' },
|
|
366
|
+
themeSuffix: '-theme-dark',
|
|
367
|
+
},
|
|
368
|
+
);
|
|
369
|
+
expect(comboClasses).toHaveLength(1);
|
|
370
|
+
expect(comboClasses[0]!.name).toBe('is-version-secondary-theme-dark');
|
|
371
|
+
expect(comboClasses[0]!.comboParent).toBe('c-button-theme-dark');
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test('empty themeSuffix is identical to omitting it (default-theme path stays byte-stable)', () => {
|
|
375
|
+
const { comboClasses } = mapStylesToWebflow(
|
|
376
|
+
'c-button',
|
|
377
|
+
buttonStyle,
|
|
378
|
+
undefined,
|
|
379
|
+
DEFAULT_BREAKPOINTS,
|
|
380
|
+
undefined,
|
|
381
|
+
{
|
|
382
|
+
instanceProps: { version: 'secondary' },
|
|
383
|
+
componentDefaults: { version: 'default' },
|
|
384
|
+
themeSuffix: '',
|
|
385
|
+
},
|
|
386
|
+
);
|
|
387
|
+
expect(comboClasses[0]!.name).toBe('is-version-secondary');
|
|
388
|
+
});
|
|
389
|
+
});
|