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
|
@@ -9,9 +9,17 @@ import type {
|
|
|
9
9
|
ResponsiveStyleObject,
|
|
10
10
|
StyleMapping,
|
|
11
11
|
InteractiveStyles,
|
|
12
|
+
InteractiveStyleRule,
|
|
12
13
|
} from '../../shared/types/styles';
|
|
13
14
|
import type { BreakpointConfig } from '../../shared/breakpoints';
|
|
14
15
|
import type { WebflowStyleClass, WebflowBreakpoint, WebflowPseudoState, CSSProperties } from './types';
|
|
16
|
+
import {
|
|
17
|
+
type ResponsiveScales,
|
|
18
|
+
type CSSPropertyType,
|
|
19
|
+
getScaleMultiplier,
|
|
20
|
+
scalePropertyValue,
|
|
21
|
+
} from '../../shared/responsiveScaling';
|
|
22
|
+
import { isCssNamedColor } from '../../shared/cssNamedColors';
|
|
15
23
|
|
|
16
24
|
// ---------------------------------------------------------------------------
|
|
17
25
|
// Helpers
|
|
@@ -24,6 +32,57 @@ const UNITLESS_PROPERTIES = new Set([
|
|
|
24
32
|
'tab-size',
|
|
25
33
|
]);
|
|
26
34
|
|
|
35
|
+
/**
|
|
36
|
+
* CSS properties that accept time values (`0`, `0s`, `0ms`). Excluded from
|
|
37
|
+
* the bare-zero → `0px` normalization below — `transition-duration: 0px`
|
|
38
|
+
* would be invalid and silently dropped.
|
|
39
|
+
*/
|
|
40
|
+
const TIME_PROPERTIES = new Set([
|
|
41
|
+
'transition-duration', 'transition-delay',
|
|
42
|
+
'animation-duration', 'animation-delay',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Webflow's class system rejects bare `0` for length properties — it expects
|
|
47
|
+
* a unit. Numeric `0` already flows through as `0px` (see `styleObjectToCSS`),
|
|
48
|
+
* but string `'0'` (from authored `marginTop: '0'`, shorthand expansion of
|
|
49
|
+
* `margin: 0 auto`, etc.) needs the same treatment. Unitless props (opacity,
|
|
50
|
+
* z-index) and time props (transition-duration) keep the bare `0`.
|
|
51
|
+
*/
|
|
52
|
+
function normalizeZero(cssProp: string, cssValue: string): string {
|
|
53
|
+
if (cssValue !== '0') return cssValue;
|
|
54
|
+
if (UNITLESS_PROPERTIES.has(cssProp)) return cssValue;
|
|
55
|
+
if (TIME_PROPERTIES.has(cssProp)) return cssValue;
|
|
56
|
+
return '0px';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Color properties whose authored value can be a bare token (e.g. `"primary"`)
|
|
61
|
+
* that Meno's runtime auto-wraps into `var(--primary)` (see
|
|
62
|
+
* `cssGeneration.ts` `styleObjectToCSS`). Mirrored here so the Webflow exporter
|
|
63
|
+
* doesn't ship literal `"background-color: primary"` to the Designer — that
|
|
64
|
+
* value is invalid CSS and Webflow renders nothing. After wrapping, the
|
|
65
|
+
* second-pass `substituteVarsInStyleClass` in `nodeToWebflow` resolves the
|
|
66
|
+
* `var(--…)` against the project's theme/variable maps.
|
|
67
|
+
*/
|
|
68
|
+
const COLOR_PROPS_CAMEL = new Set(['color', 'backgroundColor', 'borderColor']);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* If `value` is a bare Meno color token on a color-accepting property, wrap it
|
|
72
|
+
* in `var(--…)`. Hex / rgb / hsl / any functional notation / already-wrapped
|
|
73
|
+
* `var(...)` / CSS-named keywords (red, transparent, currentColor, inherit, …)
|
|
74
|
+
* pass through unchanged.
|
|
75
|
+
*/
|
|
76
|
+
function maybeWrapColorVar(camelProp: string, value: string): string {
|
|
77
|
+
if (!COLOR_PROPS_CAMEL.has(camelProp)) return value;
|
|
78
|
+
if (!value) return value;
|
|
79
|
+
if (value.startsWith('#')) return value;
|
|
80
|
+
if (value.startsWith('var(')) return value;
|
|
81
|
+
if (value.includes('(')) return value;
|
|
82
|
+
if (isCssNamedColor(value)) return value;
|
|
83
|
+
return `var(--${value})`;
|
|
84
|
+
}
|
|
85
|
+
|
|
27
86
|
function isStyleMapping(value: unknown): value is StyleMapping {
|
|
28
87
|
return (
|
|
29
88
|
typeof value === 'object' &&
|
|
@@ -47,7 +106,87 @@ function toKebabCase(prop: string): string {
|
|
|
47
106
|
}
|
|
48
107
|
|
|
49
108
|
/**
|
|
50
|
-
*
|
|
109
|
+
* Split a CSS value at top-level whitespace, leaving parenthesised groups
|
|
110
|
+
* (`var(--x, 16px)`, `calc(1rem + 2px)`) intact as a single token.
|
|
111
|
+
*/
|
|
112
|
+
function splitTopLevel(value: string): string[] {
|
|
113
|
+
const out: string[] = [];
|
|
114
|
+
let depth = 0;
|
|
115
|
+
let buf = '';
|
|
116
|
+
for (const ch of value.trim()) {
|
|
117
|
+
if (ch === '(') depth++;
|
|
118
|
+
else if (ch === ')') depth--;
|
|
119
|
+
if (depth === 0 && /\s/.test(ch)) {
|
|
120
|
+
if (buf) { out.push(buf); buf = ''; }
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
buf += ch;
|
|
124
|
+
}
|
|
125
|
+
if (buf) out.push(buf);
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Expand `margin` / `padding` / `gap` shorthand values into the longhand
|
|
131
|
+
* properties Webflow's class system models directly (per-side margin/padding,
|
|
132
|
+
* per-axis row-gap/column-gap). Webflow drops or mis-renders these shorthands
|
|
133
|
+
* when written via `Style.setProperties`, so every Webflow-bound style flows
|
|
134
|
+
* through this expansion at the CSS-conversion boundary.
|
|
135
|
+
*
|
|
136
|
+
* Returns `null` when the property isn't a handled shorthand or the value
|
|
137
|
+
* shape doesn't match (1–4 tokens for margin/padding, 1–2 for gap). Callers
|
|
138
|
+
* fall through to passing the value as-is in those cases.
|
|
139
|
+
*/
|
|
140
|
+
function expandShorthand(cssProp: string, cssValue: string): CSSProperties | null {
|
|
141
|
+
if (cssProp !== 'margin' && cssProp !== 'padding' && cssProp !== 'gap') {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
const parts = splitTopLevel(cssValue);
|
|
145
|
+
|
|
146
|
+
if (cssProp === 'gap') {
|
|
147
|
+
if (parts.length === 1) {
|
|
148
|
+
const v = normalizeZero('row-gap', parts[0]!);
|
|
149
|
+
return { 'row-gap': v, 'column-gap': v };
|
|
150
|
+
}
|
|
151
|
+
if (parts.length === 2) {
|
|
152
|
+
return {
|
|
153
|
+
'row-gap': normalizeZero('row-gap', parts[0]!),
|
|
154
|
+
'column-gap': normalizeZero('column-gap', parts[1]!),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// margin / padding
|
|
161
|
+
let top: string, right: string, bottom: string, left: string;
|
|
162
|
+
if (parts.length === 1) {
|
|
163
|
+
top = right = bottom = left = parts[0]!;
|
|
164
|
+
} else if (parts.length === 2) {
|
|
165
|
+
top = bottom = parts[0]!;
|
|
166
|
+
right = left = parts[1]!;
|
|
167
|
+
} else if (parts.length === 3) {
|
|
168
|
+
top = parts[0]!;
|
|
169
|
+
right = left = parts[1]!;
|
|
170
|
+
bottom = parts[2]!;
|
|
171
|
+
} else if (parts.length === 4) {
|
|
172
|
+
[top, right, bottom, left] = parts as [string, string, string, string];
|
|
173
|
+
} else {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
[`${cssProp}-top`]: normalizeZero(`${cssProp}-top`, top),
|
|
178
|
+
[`${cssProp}-right`]: normalizeZero(`${cssProp}-right`, right),
|
|
179
|
+
[`${cssProp}-bottom`]: normalizeZero(`${cssProp}-bottom`, bottom),
|
|
180
|
+
[`${cssProp}-left`]: normalizeZero(`${cssProp}-left`, left),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Convert a flat StyleObject to CSS properties, skipping StyleMappings.
|
|
186
|
+
* `margin`, `padding`, and `gap` shorthands are expanded in place (see
|
|
187
|
+
* `expandShorthand`) so Webflow's class system receives only longhands.
|
|
188
|
+
* Iteration order follows CSS cascade — a longhand declared after a
|
|
189
|
+
* shorthand wins; declared before, the shorthand's expansion clobbers it.
|
|
51
190
|
*/
|
|
52
191
|
function styleObjectToCSS(style: StyleObject): CSSProperties {
|
|
53
192
|
const css: CSSProperties = {};
|
|
@@ -56,11 +195,18 @@ function styleObjectToCSS(style: StyleObject): CSSProperties {
|
|
|
56
195
|
if (value === '' || value === undefined || value === null) continue;
|
|
57
196
|
if (typeof value === 'boolean' || typeof value === 'object') continue;
|
|
58
197
|
const cssProp = toKebabCase(prop);
|
|
198
|
+
let cssValue: string;
|
|
59
199
|
if (typeof value === 'number') {
|
|
60
200
|
if (isNaN(value)) continue;
|
|
61
|
-
|
|
201
|
+
cssValue = UNITLESS_PROPERTIES.has(cssProp) ? String(value) : `${value}px`;
|
|
202
|
+
} else {
|
|
203
|
+
cssValue = maybeWrapColorVar(prop, String(value));
|
|
204
|
+
}
|
|
205
|
+
const expanded = expandShorthand(cssProp, cssValue);
|
|
206
|
+
if (expanded) {
|
|
207
|
+
Object.assign(css, expanded);
|
|
62
208
|
} else {
|
|
63
|
-
css[cssProp] =
|
|
209
|
+
css[cssProp] = normalizeZero(cssProp, cssValue);
|
|
64
210
|
}
|
|
65
211
|
}
|
|
66
212
|
return css;
|
|
@@ -96,17 +242,154 @@ function collectStyleMappings(
|
|
|
96
242
|
}
|
|
97
243
|
|
|
98
244
|
/**
|
|
99
|
-
* Map interactive style postfix to Webflow pseudo-state
|
|
245
|
+
* Map interactive style postfix to a Webflow pseudo-state.
|
|
246
|
+
* Order matters — longer suffixes (`:focus-visible`, `:focus-within`,
|
|
247
|
+
* `:nth-child(odd)`) must be tested before their substring matches.
|
|
100
248
|
*/
|
|
101
|
-
function postfixToPseudoState(postfix: string): WebflowPseudoState | null {
|
|
102
|
-
|
|
249
|
+
export function postfixToPseudoState(postfix: string): WebflowPseudoState | null {
|
|
250
|
+
// Pseudo-class style: ':hover', ':focus-visible', etc.
|
|
103
251
|
if (postfix.includes(':focus-visible')) return 'focus-visible';
|
|
252
|
+
if (postfix.includes(':focus-within')) return 'focus-within';
|
|
253
|
+
if (postfix.includes(':nth-child(odd)')) return 'nth-child(odd)';
|
|
254
|
+
if (postfix.includes(':nth-child(even)')) return 'nth-child(even)';
|
|
255
|
+
if (postfix.includes(':first-child')) return 'first-child';
|
|
256
|
+
if (postfix.includes(':last-child')) return 'last-child';
|
|
257
|
+
if (postfix.includes(':placeholder')) return 'placeholder';
|
|
258
|
+
if (postfix.includes(':empty')) return 'empty';
|
|
259
|
+
if (postfix.includes(':before')) return 'before';
|
|
260
|
+
if (postfix.includes(':after')) return 'after';
|
|
261
|
+
if (postfix.includes(':hover')) return 'hover';
|
|
104
262
|
if (postfix.includes(':focus')) return 'focus';
|
|
105
263
|
if (postfix.includes(':active')) return 'active';
|
|
106
264
|
if (postfix.includes(':visited')) return 'visited';
|
|
265
|
+
if (postfix.includes(':pressed')) return 'pressed';
|
|
266
|
+
// Pseudo-element style: '::before', '::after', '::placeholder'
|
|
267
|
+
if (postfix.includes('::before')) return 'before';
|
|
268
|
+
if (postfix.includes('::after')) return 'after';
|
|
269
|
+
if (postfix.includes('::placeholder')) return 'placeholder';
|
|
107
270
|
return null;
|
|
108
271
|
}
|
|
109
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Whether an interactive-styles rule fits Webflow's class-system surface.
|
|
275
|
+
* "Yes" means: empty `prefix`, a known pseudo-state `postfix`, and a
|
|
276
|
+
* non-responsive `style` (no breakpoint subdivision). Webflow's
|
|
277
|
+
* `Style.setProperties({ pseudo })` covers exactly this case.
|
|
278
|
+
*
|
|
279
|
+
* Anything else (descendant selectors via `prefix`, class-style postfixes
|
|
280
|
+
* like `.is-open`, responsive pseudos that need media queries) gets routed
|
|
281
|
+
* to the manual-paste `interactiveCss` bundle via `generateInteractiveCSS`.
|
|
282
|
+
*/
|
|
283
|
+
export function isWebflowHandledRule(rule: InteractiveStyleRule): boolean {
|
|
284
|
+
if (rule.prefix && rule.prefix.trim().length > 0) return false;
|
|
285
|
+
if (!rule.postfix) return false;
|
|
286
|
+
if (postfixToPseudoState(rule.postfix) === null) return false;
|
|
287
|
+
const s = rule.style as ResponsiveStyleObject;
|
|
288
|
+
if (!s || typeof s !== 'object') return false;
|
|
289
|
+
const responsiveKeys = Object.keys(s).filter((k) => k !== 'base');
|
|
290
|
+
return responsiveKeys.length === 0;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Pick the Webflow breakpoint tier closest to a Meno breakpoint by its
|
|
295
|
+
* max-width threshold. Meno's responsive cascade is max-width based, and
|
|
296
|
+
* Webflow's tiers below `main` are also max-width — so we map the threshold
|
|
297
|
+
* into Webflow's bucket.
|
|
298
|
+
*
|
|
299
|
+
* Webflow tiers (max-width values per Webflow's defaults):
|
|
300
|
+
* tiny < 480, small 480-767, medium 768-991, main 992-1279,
|
|
301
|
+
* large 1280-1439, xl 1440-1919, xxl ≥ 1920.
|
|
302
|
+
*/
|
|
303
|
+
function widthToWebflowBreakpoint(maxWidthPx: number): WebflowBreakpoint {
|
|
304
|
+
if (maxWidthPx < 480) return 'tiny';
|
|
305
|
+
if (maxWidthPx < 768) return 'small';
|
|
306
|
+
if (maxWidthPx < 992) return 'medium';
|
|
307
|
+
if (maxWidthPx < 1280) return 'main';
|
|
308
|
+
if (maxWidthPx < 1440) return 'large';
|
|
309
|
+
if (maxWidthPx < 1920) return 'xl';
|
|
310
|
+
return 'xxl';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Resolve a Meno breakpoint name to its Webflow tier. The two well-known
|
|
315
|
+
* names (`tablet`, `mobile`) map to fixed tiers; custom names route by their
|
|
316
|
+
* configured numeric width via `widthToWebflowBreakpoint`. Defaults if a
|
|
317
|
+
* project's breakpoints config omits the entry.
|
|
318
|
+
*/
|
|
319
|
+
function menoBreakpointToWebflow(
|
|
320
|
+
bpName: string,
|
|
321
|
+
breakpoints: BreakpointConfig
|
|
322
|
+
): WebflowBreakpoint {
|
|
323
|
+
if (bpName === 'tablet') return 'medium';
|
|
324
|
+
if (bpName === 'mobile') return 'small';
|
|
325
|
+
const entry = breakpoints[bpName];
|
|
326
|
+
if (entry && typeof entry.breakpoint === 'number') {
|
|
327
|
+
return widthToWebflowBreakpoint(entry.breakpoint);
|
|
328
|
+
}
|
|
329
|
+
// Unknown name with no width info — default to `main` (no-op).
|
|
330
|
+
return 'main';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Merge a CSS map into the breakpoints record on a WebflowStyleClass under
|
|
335
|
+
* the given Webflow tier, layering on top of anything already there.
|
|
336
|
+
*/
|
|
337
|
+
function mergeIntoBreakpoint(
|
|
338
|
+
cls: WebflowStyleClass,
|
|
339
|
+
tier: WebflowBreakpoint,
|
|
340
|
+
css: CSSProperties
|
|
341
|
+
): void {
|
|
342
|
+
if (!cls.breakpoints) cls.breakpoints = {};
|
|
343
|
+
cls.breakpoints[tier] = { ...cls.breakpoints[tier], ...css };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Convert a kebab-case CSS property to camelCase for scale-category lookup. */
|
|
347
|
+
function kebabToCamel(s: string): string {
|
|
348
|
+
return s.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* For each scalable property in the class's base map, fill in auto-scaled
|
|
353
|
+
* values at every Meno breakpoint that doesn't already carry an explicit
|
|
354
|
+
* value for that property. Mirrors what Meno's runtime CSS generator does
|
|
355
|
+
* for utility classes, so the Webflow site renders the same numbers per
|
|
356
|
+
* breakpoint as the Meno preview.
|
|
357
|
+
*
|
|
358
|
+
* Skips values that still contain `var(--…)` — the variable-aware pass in
|
|
359
|
+
* `nodeToWebflow` handles those before this runs (it expands per-breakpoint
|
|
360
|
+
* variable values into authored breakpoint entries, which take precedence
|
|
361
|
+
* over global category scaling here).
|
|
362
|
+
*/
|
|
363
|
+
export function applyAutoScaling(
|
|
364
|
+
cls: WebflowStyleClass,
|
|
365
|
+
breakpoints: BreakpointConfig,
|
|
366
|
+
responsiveScales: ResponsiveScales | undefined
|
|
367
|
+
): void {
|
|
368
|
+
if (!responsiveScales?.enabled) return;
|
|
369
|
+
const baseRef = responsiveScales.baseReference || 16;
|
|
370
|
+
|
|
371
|
+
for (const [prop, baseValue] of Object.entries(cls.base)) {
|
|
372
|
+
if (!baseValue || baseValue.includes('var(--')) continue;
|
|
373
|
+
const camelProp = kebabToCamel(prop) as CSSPropertyType;
|
|
374
|
+
|
|
375
|
+
for (const [bpName, bpEntry] of Object.entries(breakpoints)) {
|
|
376
|
+
if (!bpEntry) continue;
|
|
377
|
+
const scale = getScaleMultiplier(responsiveScales, camelProp, bpName);
|
|
378
|
+
if (scale === null) continue;
|
|
379
|
+
const scaled = scalePropertyValue(baseValue, baseRef, scale);
|
|
380
|
+
if (scaled === null || scaled === baseValue) continue;
|
|
381
|
+
|
|
382
|
+
const tier = menoBreakpointToWebflow(bpName, breakpoints);
|
|
383
|
+
if (!cls.breakpoints) cls.breakpoints = {};
|
|
384
|
+
const bucket = cls.breakpoints[tier] || {};
|
|
385
|
+
// Author override wins — only fill missing entries.
|
|
386
|
+
if (bucket[prop] !== undefined) continue;
|
|
387
|
+
bucket[prop] = scaled;
|
|
388
|
+
cls.breakpoints[tier] = bucket;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
110
393
|
// ---------------------------------------------------------------------------
|
|
111
394
|
// Main Mapper
|
|
112
395
|
// ---------------------------------------------------------------------------
|
|
@@ -114,10 +397,41 @@ function postfixToPseudoState(postfix: string): WebflowPseudoState | null {
|
|
|
114
397
|
export interface StyleMapperResult {
|
|
115
398
|
/** The primary style class for this element */
|
|
116
399
|
primaryClass: WebflowStyleClass;
|
|
117
|
-
/**
|
|
400
|
+
/**
|
|
401
|
+
* Combo classes carrying *only* the deltas for the current instance's
|
|
402
|
+
* non-default StyleMapping prop values. Already filtered by the mapper —
|
|
403
|
+
* the caller attaches every entry to the element verbatim.
|
|
404
|
+
*/
|
|
118
405
|
comboClasses: WebflowStyleClass[];
|
|
119
406
|
}
|
|
120
407
|
|
|
408
|
+
export interface MapStylesOptions {
|
|
409
|
+
/**
|
|
410
|
+
* Resolved props for the current component instance. Only the value the
|
|
411
|
+
* mapping actually resolves to (`instanceProps[mapping.prop]`) becomes a
|
|
412
|
+
* combo on this element; other values are ignored. When omitted, no
|
|
413
|
+
* combos are emitted (page elements without a prop context).
|
|
414
|
+
*/
|
|
415
|
+
instanceProps?: Record<string, unknown>;
|
|
416
|
+
/**
|
|
417
|
+
* Default values from the enclosing component's interface. When provided,
|
|
418
|
+
* each StyleMapping's default-value entry is baked into `primaryClass.base`
|
|
419
|
+
* so the primary represents the default-prop visual; combos cover only the
|
|
420
|
+
* non-default deltas. Omit for page-level elements (defaults unavailable).
|
|
421
|
+
*/
|
|
422
|
+
componentDefaults?: Record<string, unknown>;
|
|
423
|
+
/**
|
|
424
|
+
* Suffix appended to combo class names so two placements that resolve their
|
|
425
|
+
* `var(--…)` refs against different ancestor themes occupy distinct map
|
|
426
|
+
* slots in `ctx.styleClasses`. Without this, the default theme's combo and a
|
|
427
|
+
* non-default theme's combo collapse onto the same name and the last-written
|
|
428
|
+
* one wins — silently corrupting whichever placement was processed earlier.
|
|
429
|
+
* Caller passes e.g. `'-theme-dark'`; empty/undefined means default theme,
|
|
430
|
+
* no suffix added. Caller is responsible for sanitizing the theme name.
|
|
431
|
+
*/
|
|
432
|
+
themeSuffix?: string;
|
|
433
|
+
}
|
|
434
|
+
|
|
121
435
|
/**
|
|
122
436
|
* Convert Meno element styles to Webflow style classes.
|
|
123
437
|
*
|
|
@@ -125,13 +439,22 @@ export interface StyleMapperResult {
|
|
|
125
439
|
* @param style - Element's responsive style object
|
|
126
440
|
* @param interactiveStyles - Element's interactive (hover/focus/etc.) styles
|
|
127
441
|
* @param breakpoints - Project breakpoint configuration
|
|
442
|
+
* @param responsiveScales - When `enabled`, auto-fill scaled per-breakpoint
|
|
443
|
+
* values for scalable properties not explicitly authored at that
|
|
444
|
+
* breakpoint (mirrors Meno's runtime CSS generator).
|
|
445
|
+
* @param options - Instance-aware combo emission. See `MapStylesOptions`.
|
|
128
446
|
*/
|
|
129
447
|
export function mapStylesToWebflow(
|
|
130
448
|
className: string,
|
|
131
449
|
style: StyleObject | ResponsiveStyleObject | undefined,
|
|
132
450
|
interactiveStyles: InteractiveStyles | undefined,
|
|
133
|
-
breakpoints: BreakpointConfig
|
|
451
|
+
breakpoints: BreakpointConfig,
|
|
452
|
+
responsiveScales?: ResponsiveScales,
|
|
453
|
+
options?: MapStylesOptions
|
|
134
454
|
): StyleMapperResult {
|
|
455
|
+
const instanceProps = options?.instanceProps;
|
|
456
|
+
const componentDefaults = options?.componentDefaults;
|
|
457
|
+
const themeSuffix = options?.themeSuffix ?? '';
|
|
135
458
|
// Convert underscores to dashes for Webflow class naming convention
|
|
136
459
|
const webflowClassName = className.replace(/_/g, '-');
|
|
137
460
|
|
|
@@ -141,6 +464,10 @@ export function mapStylesToWebflow(
|
|
|
141
464
|
};
|
|
142
465
|
|
|
143
466
|
// --- Base + breakpoint styles ---
|
|
467
|
+
// Meno's `base` is the desktop default → Webflow's `main` tier (set as
|
|
468
|
+
// `primaryClass.base` so the consumer writes it without a breakpoint
|
|
469
|
+
// option). Named tiers (`tablet`, `mobile`) and custom breakpoints route
|
|
470
|
+
// to Webflow tiers via `menoBreakpointToWebflow`.
|
|
144
471
|
if (style) {
|
|
145
472
|
if (isResponsiveStyle(style)) {
|
|
146
473
|
const responsive = style as ResponsiveStyleObject;
|
|
@@ -149,93 +476,220 @@ export function mapStylesToWebflow(
|
|
|
149
476
|
primaryClass.base = styleObjectToCSS(responsive.base);
|
|
150
477
|
}
|
|
151
478
|
|
|
152
|
-
if (responsive.tablet) {
|
|
153
|
-
if (!primaryClass.breakpoints) primaryClass.breakpoints = {};
|
|
154
|
-
primaryClass.breakpoints.Tablet = styleObjectToCSS(responsive.tablet);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (responsive.mobile) {
|
|
158
|
-
if (!primaryClass.breakpoints) primaryClass.breakpoints = {};
|
|
159
|
-
primaryClass.breakpoints.MobilePortrait = styleObjectToCSS(responsive.mobile);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Handle additional custom breakpoints (map to closest Webflow breakpoint)
|
|
163
479
|
for (const [bpName, bpStyle] of Object.entries(responsive)) {
|
|
164
|
-
if (!bpStyle || bpName === 'base'
|
|
165
|
-
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
...styleObjectToCSS(bpStyle),
|
|
170
|
-
};
|
|
480
|
+
if (!bpStyle || bpName === 'base') continue;
|
|
481
|
+
const css = styleObjectToCSS(bpStyle);
|
|
482
|
+
if (Object.keys(css).length === 0) continue;
|
|
483
|
+
const tier = menoBreakpointToWebflow(bpName, breakpoints);
|
|
484
|
+
mergeIntoBreakpoint(primaryClass, tier, css);
|
|
171
485
|
}
|
|
172
486
|
} else {
|
|
173
|
-
// Flat style object — treat as base/
|
|
487
|
+
// Flat style object — treat as base/main.
|
|
174
488
|
primaryClass.base = styleObjectToCSS(style as StyleObject);
|
|
175
489
|
}
|
|
176
490
|
}
|
|
177
491
|
|
|
178
|
-
// --- Interactive styles (hover, focus,
|
|
492
|
+
// --- Interactive styles (hover, focus, …) ---
|
|
493
|
+
// Pseudo-state postfixes with empty prefix and non-responsive ruleStyle go
|
|
494
|
+
// to `primaryClass.pseudoStates` so Webflow's class system applies them via
|
|
495
|
+
// `Style.setProperties({ pseudo })`. Anything else (prefix-built selectors,
|
|
496
|
+
// class-style postfixes, breakpoint-divided pseudos) is collected into
|
|
497
|
+
// `interactiveCss` server-side via `generateInteractiveCSS` — see
|
|
498
|
+
// `buildWebflow.ts` and `isWebflowHandledRule` below.
|
|
179
499
|
if (interactiveStyles && interactiveStyles.length > 0) {
|
|
180
500
|
for (const rule of interactiveStyles) {
|
|
181
|
-
if (!rule
|
|
501
|
+
if (!isWebflowHandledRule(rule)) continue;
|
|
502
|
+
|
|
503
|
+
const baseProps: CSSProperties = isResponsiveStyle(rule.style as StyleObject | ResponsiveStyleObject)
|
|
504
|
+
? styleObjectToCSS(((rule.style as ResponsiveStyleObject).base) || {})
|
|
505
|
+
: styleObjectToCSS(rule.style as StyleObject);
|
|
182
506
|
|
|
183
|
-
|
|
507
|
+
// Skip empty rules — writing an empty pseudoStates entry triggers a
|
|
508
|
+
// destructive wipe in `applyStyleScope` on re-import (it diffs
|
|
509
|
+
// `existing` against `next={}` and removes every property).
|
|
510
|
+
if (Object.keys(baseProps).length === 0) continue;
|
|
511
|
+
|
|
512
|
+
const pseudoState = postfixToPseudoState(rule.postfix!);
|
|
184
513
|
if (!pseudoState) continue;
|
|
185
514
|
|
|
186
|
-
const ruleStyle = rule.style;
|
|
187
515
|
if (!primaryClass.pseudoStates) primaryClass.pseudoStates = {};
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (responsive.base) {
|
|
193
|
-
primaryClass.pseudoStates[pseudoState] = {
|
|
194
|
-
...primaryClass.pseudoStates[pseudoState],
|
|
195
|
-
...styleObjectToCSS(responsive.base),
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
} else {
|
|
199
|
-
primaryClass.pseudoStates[pseudoState] = {
|
|
200
|
-
...primaryClass.pseudoStates[pseudoState],
|
|
201
|
-
...styleObjectToCSS(ruleStyle as StyleObject),
|
|
202
|
-
};
|
|
203
|
-
}
|
|
516
|
+
primaryClass.pseudoStates[pseudoState] = {
|
|
517
|
+
...primaryClass.pseudoStates[pseudoState],
|
|
518
|
+
...baseProps,
|
|
519
|
+
};
|
|
204
520
|
}
|
|
205
521
|
}
|
|
206
522
|
|
|
207
|
-
// ---
|
|
523
|
+
// --- Default-prop bake + single consolidated combo for the instance ---
|
|
524
|
+
// Webflow combo classes only make sense as deltas: the primary carries the
|
|
525
|
+
// default-prop visual; one combo per element holds the merged deltas for
|
|
526
|
+
// every non-default StyleMapping prop value the instance authored. Folding
|
|
527
|
+
// every mapped delta into one combo (rather than one combo per prop) means
|
|
528
|
+
// each element wears exactly one extra class regardless of how many props
|
|
529
|
+
// its component declares — simpler in the Webflow Designer and avoids
|
|
530
|
+
// fan-out where two unrelated instances accidentally share a delta combo.
|
|
208
531
|
const comboClasses: WebflowStyleClass[] = [];
|
|
209
532
|
const mappings = collectStyleMappings(style);
|
|
210
533
|
|
|
534
|
+
const comboCss: CSSProperties = {};
|
|
535
|
+
const comboNameParts: string[] = [];
|
|
536
|
+
|
|
211
537
|
for (const { property, mapping } of mappings) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
538
|
+
const defaultValue = componentDefaults?.[mapping.prop];
|
|
539
|
+
const defaultKey = defaultValue != null ? String(defaultValue) : undefined;
|
|
540
|
+
|
|
541
|
+
// Bake the default-value resolution into the primary class's base so the
|
|
542
|
+
// primary stands on its own for default-prop instances. Values authored
|
|
543
|
+
// outside the mapping (declared on `style.fontSize` directly) already
|
|
544
|
+
// landed in `primaryClass.base` via `styleObjectToCSS`.
|
|
545
|
+
if (defaultKey !== undefined && defaultKey in mapping.values) {
|
|
546
|
+
const defaultCss = mappingValueToCSS(property, mapping.values[defaultKey]);
|
|
547
|
+
if (defaultCss) {
|
|
548
|
+
// StyleMapping bakes feed into the primary's `main` tier (its `base`),
|
|
549
|
+
// matching how `styleObjectToCSS` handles non-mapped properties.
|
|
550
|
+
primaryClass.base = { ...primaryClass.base, ...defaultCss };
|
|
551
|
+
}
|
|
226
552
|
}
|
|
553
|
+
|
|
554
|
+
if (!instanceProps) continue;
|
|
555
|
+
const instanceValue = instanceProps[mapping.prop];
|
|
556
|
+
if (instanceValue == null) continue;
|
|
557
|
+
const instanceKey = String(instanceValue);
|
|
558
|
+
if (instanceKey === defaultKey) continue; // covered by primary
|
|
559
|
+
if (!(instanceKey in mapping.values)) continue; // unknown value
|
|
560
|
+
const css = mappingValueToCSS(property, mapping.values[instanceKey]);
|
|
561
|
+
if (!css) continue;
|
|
562
|
+
|
|
563
|
+
Object.assign(comboCss, css);
|
|
564
|
+
const part = `${sanitizeClassName(mapping.prop)}-${sanitizeClassName(instanceKey)}`;
|
|
565
|
+
// Same prop appears once per mapping (e.g. `version` on both
|
|
566
|
+
// `backgroundColor` and `color` Button mappings) — dedupe so the combo
|
|
567
|
+
// name stays `is-version-secondary` rather than repeating the segment.
|
|
568
|
+
if (!comboNameParts.includes(part)) comboNameParts.push(part);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (comboNameParts.length > 0 && Object.keys(comboCss).length > 0) {
|
|
572
|
+
// Sort so different declaration orders of the same prop set produce the
|
|
573
|
+
// same combo name — instances with identical variants share one class.
|
|
574
|
+
comboNameParts.sort();
|
|
575
|
+
comboClasses.push({
|
|
576
|
+
name: `is-${comboNameParts.join('-')}${themeSuffix}`,
|
|
577
|
+
base: comboCss,
|
|
578
|
+
comboParent: webflowClassName,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// --- Auto-scaling: fill in scaled values at each Meno breakpoint for any
|
|
583
|
+
// scalable base property the author hasn't already overridden. Variable
|
|
584
|
+
// refs (`var(--…)`) are skipped here — `nodeToWebflow` expands them with
|
|
585
|
+
// breakpoint awareness before this runs, so authored breakpoint values
|
|
586
|
+
// already capture the variable's per-breakpoint scaling.
|
|
587
|
+
applyAutoScaling(primaryClass, breakpoints, responsiveScales);
|
|
588
|
+
for (const combo of comboClasses) {
|
|
589
|
+
applyAutoScaling(combo, breakpoints, responsiveScales);
|
|
227
590
|
}
|
|
228
591
|
|
|
229
592
|
return { primaryClass, comboClasses };
|
|
230
593
|
}
|
|
231
594
|
|
|
595
|
+
/**
|
|
596
|
+
* Convert a single StyleMapping value (already looked up from `mapping.values`)
|
|
597
|
+
* into a CSSProperties entry, mirroring `styleObjectToCSS`'s unit handling
|
|
598
|
+
* and shorthand expansion. Returns `null` when the value is empty/missing.
|
|
599
|
+
*/
|
|
600
|
+
function mappingValueToCSS(
|
|
601
|
+
property: string,
|
|
602
|
+
rawValue: string | number | undefined | null
|
|
603
|
+
): CSSProperties | null {
|
|
604
|
+
if (rawValue === '' || rawValue === undefined || rawValue === null) return null;
|
|
605
|
+
const cssProp = toKebabCase(property);
|
|
606
|
+
const cssValueStr = typeof rawValue === 'number'
|
|
607
|
+
? (UNITLESS_PROPERTIES.has(cssProp) ? String(rawValue) : `${rawValue}px`)
|
|
608
|
+
: maybeWrapColorVar(property, String(rawValue));
|
|
609
|
+
return expandShorthand(cssProp, cssValueStr) ?? { [cssProp]: normalizeZero(cssProp, cssValueStr) };
|
|
610
|
+
}
|
|
611
|
+
|
|
232
612
|
/**
|
|
233
613
|
* Sanitize a string for use as a CSS class name segment
|
|
234
614
|
*/
|
|
235
|
-
function sanitizeClassName(name: string): string {
|
|
615
|
+
export function sanitizeClassName(name: string): string {
|
|
236
616
|
return name
|
|
237
617
|
.toLowerCase()
|
|
238
618
|
.replace(/[^a-z0-9-]/g, '-')
|
|
239
619
|
.replace(/-+/g, '-')
|
|
240
620
|
.replace(/^-|-$/g, '');
|
|
241
621
|
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Build a single combo class from a component instance's style /
|
|
625
|
+
* interactiveStyles overrides — the `acceptsStyles` analogue of the
|
|
626
|
+
* prop-mapped combo in `mapStylesToWebflow`.
|
|
627
|
+
*
|
|
628
|
+
* Every override the instance authored — flat or responsive base values,
|
|
629
|
+
* per-breakpoint values, Webflow-handled pseudo-state values — is folded
|
|
630
|
+
* into one combo's `.base` / `.breakpoints` / `.pseudoStates`. The caller
|
|
631
|
+
* supplies a stable name (typically derived from the instance's outer
|
|
632
|
+
* location so two distinct instances don't collide) and the body root's
|
|
633
|
+
* primary class for `comboParent`. Returns `null` if no overrides survive
|
|
634
|
+
* the filters.
|
|
635
|
+
*/
|
|
636
|
+
export function buildInstanceStyleCombo(
|
|
637
|
+
comboName: string,
|
|
638
|
+
rootClassName: string,
|
|
639
|
+
style: StyleObject | ResponsiveStyleObject | undefined,
|
|
640
|
+
interactiveStyles: InteractiveStyles | undefined,
|
|
641
|
+
breakpoints: BreakpointConfig,
|
|
642
|
+
responsiveScales?: ResponsiveScales,
|
|
643
|
+
): WebflowStyleClass | null {
|
|
644
|
+
const base: CSSProperties = {};
|
|
645
|
+
const bps: Partial<Record<WebflowBreakpoint, CSSProperties>> = {};
|
|
646
|
+
const pseudos: Partial<Record<WebflowPseudoState, CSSProperties>> = {};
|
|
647
|
+
|
|
648
|
+
if (style) {
|
|
649
|
+
if (isResponsiveStyle(style)) {
|
|
650
|
+
const responsive = style as ResponsiveStyleObject;
|
|
651
|
+
if (responsive.base) Object.assign(base, styleObjectToCSS(responsive.base));
|
|
652
|
+
for (const [bpName, bpStyle] of Object.entries(responsive)) {
|
|
653
|
+
if (!bpStyle || bpName === 'base') continue;
|
|
654
|
+
const css = styleObjectToCSS(bpStyle);
|
|
655
|
+
if (Object.keys(css).length === 0) continue;
|
|
656
|
+
const tier = menoBreakpointToWebflow(bpName, breakpoints);
|
|
657
|
+
bps[tier] = { ...bps[tier], ...css };
|
|
658
|
+
}
|
|
659
|
+
} else {
|
|
660
|
+
Object.assign(base, styleObjectToCSS(style as StyleObject));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (interactiveStyles && interactiveStyles.length > 0) {
|
|
665
|
+
for (const rule of interactiveStyles) {
|
|
666
|
+
if (!isWebflowHandledRule(rule)) continue;
|
|
667
|
+
const ruleStyle = rule.style as StyleObject | ResponsiveStyleObject;
|
|
668
|
+
const flat = isResponsiveStyle(ruleStyle)
|
|
669
|
+
? ((ruleStyle as ResponsiveStyleObject).base as StyleObject | undefined)
|
|
670
|
+
: (ruleStyle as StyleObject);
|
|
671
|
+
if (!flat) continue;
|
|
672
|
+
const css = styleObjectToCSS(flat);
|
|
673
|
+
if (Object.keys(css).length === 0) continue;
|
|
674
|
+
const pseudo = postfixToPseudoState(rule.postfix!);
|
|
675
|
+
if (!pseudo) continue;
|
|
676
|
+
pseudos[pseudo] = { ...pseudos[pseudo], ...css };
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (
|
|
681
|
+
Object.keys(base).length === 0
|
|
682
|
+
&& Object.keys(bps).length === 0
|
|
683
|
+
&& Object.keys(pseudos).length === 0
|
|
684
|
+
) return null;
|
|
685
|
+
|
|
686
|
+
const cls: WebflowStyleClass = {
|
|
687
|
+
name: comboName,
|
|
688
|
+
base,
|
|
689
|
+
comboParent: rootClassName,
|
|
690
|
+
};
|
|
691
|
+
if (Object.keys(bps).length > 0) cls.breakpoints = bps;
|
|
692
|
+
if (Object.keys(pseudos).length > 0) cls.pseudoStates = pseudos;
|
|
693
|
+
applyAutoScaling(cls, breakpoints, responsiveScales);
|
|
694
|
+
return cls;
|
|
695
|
+
}
|