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,8 +9,14 @@ import { getStyleValue, getDynamicStyle, isDynamicClass } from './styleValueRegi
|
|
|
9
9
|
import { isCssNamedColor } from './cssNamedColors';
|
|
10
10
|
import type { BreakpointConfig, LegacyBreakpointConfig } from './breakpoints';
|
|
11
11
|
import { DEFAULT_BREAKPOINTS, getBreakpointValues } from './breakpoints';
|
|
12
|
-
import type { ResponsiveScales, CSSPropertyType } from './responsiveScaling';
|
|
13
|
-
import {
|
|
12
|
+
import type { ResponsiveScales, CSSPropertyType, ResponsiveMode } from './responsiveScaling';
|
|
13
|
+
import {
|
|
14
|
+
scalePropertyValue,
|
|
15
|
+
getScaleMultiplier,
|
|
16
|
+
buildFluidPropertyValue,
|
|
17
|
+
getSmallestBreakpointName,
|
|
18
|
+
DEFAULT_FLUID_RANGE,
|
|
19
|
+
} from './responsiveScaling';
|
|
14
20
|
import type { InteractiveStyles, StyleObject, ResponsiveStyleObject, StyleValue } from './types/styles';
|
|
15
21
|
import type { RemConversionConfig } from './pxToRem';
|
|
16
22
|
import { applyRemConversion, convertPxToRem, shouldConvertProperty } from './pxToRem';
|
|
@@ -69,6 +75,14 @@ export function sortClassesByPropertyOrder(classes: Iterable<string>): string[]
|
|
|
69
75
|
return Array.from(classes).sort((a, b) => getClassPropertyOrder(a) - getClassPropertyOrder(b));
|
|
70
76
|
}
|
|
71
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Pick the active responsive mode. Defaults to 'breakpoints' when missing,
|
|
80
|
+
* preserving behavior for projects saved before the 'fluid' option existed.
|
|
81
|
+
*/
|
|
82
|
+
function getResponsiveMode(scales: ResponsiveScales | undefined | null): ResponsiveMode {
|
|
83
|
+
return (scales?.mode as ResponsiveMode | undefined) ?? 'breakpoints';
|
|
84
|
+
}
|
|
85
|
+
|
|
72
86
|
/** CSS property → responsive scale category mapping (used by generateUtilityCSS and generateSingleClassCSS) */
|
|
73
87
|
const AUTO_RESPONSIVE_TYPE_MAP: Record<string, string> = {
|
|
74
88
|
'padding': 'padding',
|
|
@@ -192,6 +206,15 @@ function extractPropertyAndValue(className: string): { property: string; value:
|
|
|
192
206
|
const classValue = className.substring(knownPrefix.length + 1);
|
|
193
207
|
const cssProp = prefixToCSSProperty[knownPrefix];
|
|
194
208
|
if (cssProp) {
|
|
209
|
+
// Hash-fallback class (e.g. `pt-h1y9pr6i`): the part after the
|
|
210
|
+
// prefix is a hash, not a real CSS value. Returning `value: 'h1y9pr6i'`
|
|
211
|
+
// would make auto-responsive scaling and other downstream consumers
|
|
212
|
+
// emit broken rules like `padding-top: h1y9pr6i;`. The class IS
|
|
213
|
+
// resolvable via the style-value registry — `generateRuleForClass`
|
|
214
|
+
// handles it directly. Skip the dynamic-extraction path here.
|
|
215
|
+
if (/^h[0-9a-z]+$/.test(classValue)) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
195
218
|
return { property: cssProp, value: classValue };
|
|
196
219
|
}
|
|
197
220
|
}
|
|
@@ -200,6 +223,39 @@ function extractPropertyAndValue(className: string): { property: string; value:
|
|
|
200
223
|
return null;
|
|
201
224
|
}
|
|
202
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Resolve `{ property, value }` for a utility class, consulting the style-value
|
|
228
|
+
* registry for hash-fallback classes (e.g. `fs-h1glej9a` for `222.3px`).
|
|
229
|
+
*
|
|
230
|
+
* Used by the auto-responsive scaling path so hashed classes don't silently
|
|
231
|
+
* bypass `clamp()` / `@media` rewriting. `extractPropertyAndValue` itself
|
|
232
|
+
* intentionally returns null for hashed classes because its other consumers
|
|
233
|
+
* would emit broken `${prop}: ${hash};` rules.
|
|
234
|
+
*
|
|
235
|
+
* Returns the registered value as-is (no parsing). Downstream helpers
|
|
236
|
+
* (`buildFluidPropertyValue`, `scalePropertyValue`) already null out
|
|
237
|
+
* unparseable / `%` / `em` / `var()` / clamp-blob inputs, so a Webflow-style
|
|
238
|
+
* already-clamped value will pass through untouched via the existing
|
|
239
|
+
* `if (!fluidValue) return rule;` guards.
|
|
240
|
+
*/
|
|
241
|
+
function resolveScalablePropertyValue(className: string): { property: string; value: string } | null {
|
|
242
|
+
const direct = extractPropertyAndValue(className);
|
|
243
|
+
if (direct) return direct;
|
|
244
|
+
|
|
245
|
+
for (const knownPrefix of SORTED_PREFIXES) {
|
|
246
|
+
if (!className.startsWith(knownPrefix + '-')) continue;
|
|
247
|
+
const classValue = className.substring(knownPrefix.length + 1);
|
|
248
|
+
if (!/^h[0-9a-z]+$/.test(classValue)) continue;
|
|
249
|
+
const cssProp = prefixToCSSProperty[knownPrefix];
|
|
250
|
+
if (!cssProp) continue;
|
|
251
|
+
const registered = getStyleValue(className);
|
|
252
|
+
if (registered == null || registered === '') continue;
|
|
253
|
+
return { property: cssProp, value: String(registered) };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
203
259
|
/**
|
|
204
260
|
* Generate CSS rule for a utility class
|
|
205
261
|
* Handles dynamic classes like p-10px, m-20px, fs-48px, etc.
|
|
@@ -409,6 +465,139 @@ export function generateRuleForClass(className: string): string | null {
|
|
|
409
465
|
return `${cssProp}: ${value};`;
|
|
410
466
|
}
|
|
411
467
|
|
|
468
|
+
/**
|
|
469
|
+
* In fluid mode, return a CSS declaration string with the value replaced by
|
|
470
|
+
* `clamp(...)` when the property is auto-responsive AND has a small-end scale.
|
|
471
|
+
* Otherwise returns the original `rule` unchanged.
|
|
472
|
+
*
|
|
473
|
+
* The MIN of the clamp comes from the smallest configured breakpoint's
|
|
474
|
+
* multiplier, matching the user-confirmed mapping (mobile multiplier ⇒ MIN,
|
|
475
|
+
* base value ⇒ MAX, tablet ignored in fluid mode).
|
|
476
|
+
*/
|
|
477
|
+
function applyFluidToUtilityRule(
|
|
478
|
+
rule: string,
|
|
479
|
+
className: string,
|
|
480
|
+
responsiveScales: ResponsiveScales,
|
|
481
|
+
breakpoints: BreakpointConfig
|
|
482
|
+
): string {
|
|
483
|
+
const propValue = resolveScalablePropertyValue(className);
|
|
484
|
+
if (!propValue) return rule;
|
|
485
|
+
|
|
486
|
+
const category = AUTO_RESPONSIVE_TYPE_MAP[propValue.property];
|
|
487
|
+
if (!category) return rule;
|
|
488
|
+
|
|
489
|
+
const scaleConfig = responsiveScales[category as keyof ResponsiveScales] as
|
|
490
|
+
| Record<string, number>
|
|
491
|
+
| undefined;
|
|
492
|
+
if (!scaleConfig) return rule;
|
|
493
|
+
|
|
494
|
+
const smallest = getSmallestBreakpointName(breakpoints);
|
|
495
|
+
if (!smallest) return rule;
|
|
496
|
+
|
|
497
|
+
const scale = scaleConfig[smallest];
|
|
498
|
+
if (scale == null || scale === 1) return rule;
|
|
499
|
+
|
|
500
|
+
const range = responsiveScales.fluidRange ?? DEFAULT_FLUID_RANGE;
|
|
501
|
+
const baseRef = responsiveScales.baseReference ?? 16;
|
|
502
|
+
const fluidValue = buildFluidPropertyValue(
|
|
503
|
+
propValue.value,
|
|
504
|
+
scale,
|
|
505
|
+
range.min,
|
|
506
|
+
range.max,
|
|
507
|
+
baseRef
|
|
508
|
+
);
|
|
509
|
+
if (!fluidValue) return rule;
|
|
510
|
+
|
|
511
|
+
return `${propValue.property}: ${fluidValue};`;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* In fluid mode, transform a flat StyleObject by replacing each scalable
|
|
516
|
+
* property's value with a `clamp(...)` expression. Properties without a
|
|
517
|
+
* matching scale category pass through unchanged.
|
|
518
|
+
*/
|
|
519
|
+
function applyFluidToStyle(
|
|
520
|
+
style: StyleObject,
|
|
521
|
+
responsiveScales: ResponsiveScales,
|
|
522
|
+
breakpoints: BreakpointConfig
|
|
523
|
+
): StyleObject {
|
|
524
|
+
const range = responsiveScales.fluidRange ?? DEFAULT_FLUID_RANGE;
|
|
525
|
+
const baseRef = responsiveScales.baseReference ?? 16;
|
|
526
|
+
const smallest = getSmallestBreakpointName(breakpoints);
|
|
527
|
+
if (!smallest) return style;
|
|
528
|
+
|
|
529
|
+
const out: StyleObject = {};
|
|
530
|
+
for (const [prop, value] of Object.entries(style)) {
|
|
531
|
+
if (typeof value === 'object' && value !== null && '_mapping' in value) {
|
|
532
|
+
out[prop] = value;
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (value == null || value === '') {
|
|
536
|
+
out[prop] = value;
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const scale = getScaleMultiplier(
|
|
541
|
+
responsiveScales,
|
|
542
|
+
prop as CSSPropertyType,
|
|
543
|
+
smallest
|
|
544
|
+
);
|
|
545
|
+
if (scale == null || scale === 1) {
|
|
546
|
+
out[prop] = value;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const fluidValue = buildFluidPropertyValue(
|
|
551
|
+
String(value),
|
|
552
|
+
scale,
|
|
553
|
+
range.min,
|
|
554
|
+
range.max,
|
|
555
|
+
baseRef
|
|
556
|
+
);
|
|
557
|
+
out[prop] = fluidValue ?? value;
|
|
558
|
+
}
|
|
559
|
+
return out;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** Values that don't make sense in a fluid container pattern. */
|
|
563
|
+
const CONTAINER_RESERVED_VALUES = new Set(['auto', 'inherit', 'initial', 'unset', '']);
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Detect "container intent" on a flat StyleObject and rewrite it into the
|
|
567
|
+
* fluid container pattern. Trigger: `width === maxWidth` (both set, equal).
|
|
568
|
+
*
|
|
569
|
+
* Transformation:
|
|
570
|
+
* { width: '1200px', maxWidth: '1200px' }
|
|
571
|
+
* → { width: 'calc(100% - var(--site-margin) * 2)', maxWidth: '1200px',
|
|
572
|
+
* marginLeft: 'auto', marginRight: 'auto' }
|
|
573
|
+
*
|
|
574
|
+
* No-op when fluid mode is inactive (`fluidActive === false`) — the
|
|
575
|
+
* `--site-margin` CSS variable is only emitted in fluid mode, so emitting the
|
|
576
|
+
* calc() in breakpoints mode would reference an undefined variable.
|
|
577
|
+
*
|
|
578
|
+
* Margins are ALWAYS overwritten with `auto` (per user spec), even if the
|
|
579
|
+
* caller had set explicit values.
|
|
580
|
+
*/
|
|
581
|
+
export function applyContainerPattern(
|
|
582
|
+
style: StyleObject,
|
|
583
|
+
fluidActive: boolean
|
|
584
|
+
): StyleObject {
|
|
585
|
+
if (!fluidActive) return style;
|
|
586
|
+
const w = style.width;
|
|
587
|
+
const mw = style.maxWidth;
|
|
588
|
+
if (w == null || mw == null) return style;
|
|
589
|
+
if (typeof w !== 'string' || typeof mw !== 'string') return style;
|
|
590
|
+
if (w !== mw) return style;
|
|
591
|
+
if (CONTAINER_RESERVED_VALUES.has(w.trim())) return style;
|
|
592
|
+
|
|
593
|
+
return {
|
|
594
|
+
...style,
|
|
595
|
+
width: 'calc(100% - var(--site-margin) * 2)',
|
|
596
|
+
marginLeft: 'auto',
|
|
597
|
+
marginRight: 'auto',
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
412
601
|
/**
|
|
413
602
|
* Generate CSS for all utility classes used in the application
|
|
414
603
|
* Scans through all classes and generates the necessary CSS rules
|
|
@@ -473,7 +662,7 @@ export function generateUtilityCSS(
|
|
|
473
662
|
|
|
474
663
|
// Check if this class should get auto-responsive scaling
|
|
475
664
|
if (responsiveScales?.enabled) {
|
|
476
|
-
const propValue =
|
|
665
|
+
const propValue = resolveScalablePropertyValue(className);
|
|
477
666
|
if (propValue) {
|
|
478
667
|
const category = AUTO_RESPONSIVE_TYPE_MAP[propValue.property];
|
|
479
668
|
if (category && responsiveScales[category as keyof ResponsiveScales]) {
|
|
@@ -484,10 +673,17 @@ export function generateUtilityCSS(
|
|
|
484
673
|
}
|
|
485
674
|
}
|
|
486
675
|
|
|
676
|
+
const mode = getResponsiveMode(responsiveScales);
|
|
677
|
+
const fluidActive = responsiveScales?.enabled === true && mode === 'fluid';
|
|
678
|
+
|
|
487
679
|
// Generate base rules — sorted so shorthands (border) appear before longhands (border-color)
|
|
488
680
|
for (const className of sortClassesByPropertyOrder(baseClasses)) {
|
|
489
|
-
|
|
681
|
+
let rule = generateRuleForClass(className);
|
|
490
682
|
if (rule) {
|
|
683
|
+
// In fluid mode, replace auto-responsive raw values with clamp() expressions.
|
|
684
|
+
if (fluidActive && autoResponsiveClasses.has(className)) {
|
|
685
|
+
rule = applyFluidToUtilityRule(rule, className, responsiveScales!, breakpoints);
|
|
686
|
+
}
|
|
491
687
|
// Escape special characters in class name for CSS selector
|
|
492
688
|
const escapedClassName = escapeCSSClassName(className);
|
|
493
689
|
const finalRule = applyRemConversion(rule, remConfig);
|
|
@@ -499,10 +695,11 @@ export function generateUtilityCSS(
|
|
|
499
695
|
type MediaQueryMap = Record<string, { classes: Array<{ className: string; rule: string }>; value: number }>;
|
|
500
696
|
const autoResponsiveMediaQueries: MediaQueryMap = {};
|
|
501
697
|
|
|
502
|
-
// Generate auto-responsive rules for classes with enabled scaling
|
|
503
|
-
|
|
698
|
+
// Generate auto-responsive rules for classes with enabled scaling.
|
|
699
|
+
// In fluid mode the base rule already encodes scaling via clamp(), so skip @media.
|
|
700
|
+
if (responsiveScales?.enabled && !fluidActive) {
|
|
504
701
|
for (const className of autoResponsiveClasses) {
|
|
505
|
-
const propValue =
|
|
702
|
+
const propValue = resolveScalablePropertyValue(className);
|
|
506
703
|
if (!propValue) continue;
|
|
507
704
|
|
|
508
705
|
const category = AUTO_RESPONSIVE_TYPE_MAP[propValue.property];
|
|
@@ -640,16 +837,27 @@ export function generateSingleClassCSS(
|
|
|
640
837
|
|
|
641
838
|
if (!matched) {
|
|
642
839
|
// Base (non-responsive) class
|
|
643
|
-
|
|
840
|
+
let rule = generateRuleForClass(className);
|
|
644
841
|
if (!rule) return '';
|
|
645
842
|
|
|
843
|
+
const mode = getResponsiveMode(responsiveScales);
|
|
844
|
+
const fluidActive = responsiveScales?.enabled === true && mode === 'fluid';
|
|
845
|
+
|
|
846
|
+
// In fluid mode, replace auto-responsive raw values with clamp().
|
|
847
|
+
if (fluidActive) {
|
|
848
|
+
const propValue = resolveScalablePropertyValue(className);
|
|
849
|
+
if (propValue && AUTO_RESPONSIVE_TYPE_MAP[propValue.property]) {
|
|
850
|
+
rule = applyFluidToUtilityRule(rule, className, responsiveScales!, breakpoints);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
646
854
|
const escapedClassName = escapeCSSClassName(className);
|
|
647
855
|
const finalRule = applyRemConversion(rule, remConfig);
|
|
648
856
|
css.push(`.${escapedClassName} { ${finalRule} }`);
|
|
649
857
|
|
|
650
|
-
// Auto-responsive scaling
|
|
651
|
-
if (responsiveScales?.enabled) {
|
|
652
|
-
const propValue =
|
|
858
|
+
// Auto-responsive scaling via @media — only in 'breakpoints' mode.
|
|
859
|
+
if (responsiveScales?.enabled && !fluidActive) {
|
|
860
|
+
const propValue = resolveScalablePropertyValue(className);
|
|
653
861
|
if (propValue) {
|
|
654
862
|
const category = AUTO_RESPONSIVE_TYPE_MAP[propValue.property];
|
|
655
863
|
if (category) {
|
|
@@ -862,6 +1070,8 @@ export function generateInteractiveCSS(
|
|
|
862
1070
|
// Extract breakpoint values for CSS media queries
|
|
863
1071
|
const breakpointValues = getBreakpointValues(breakpoints);
|
|
864
1072
|
const scalingEnabled = responsiveScales?.enabled === true;
|
|
1073
|
+
const mode = getResponsiveMode(responsiveScales);
|
|
1074
|
+
const fluidActive = scalingEnabled && mode === 'fluid';
|
|
865
1075
|
|
|
866
1076
|
for (const rule of interactiveStyles) {
|
|
867
1077
|
const { prefix, postfix, style } = rule;
|
|
@@ -878,9 +1088,15 @@ export function generateInteractiveCSS(
|
|
|
878
1088
|
// Generate responsive rules
|
|
879
1089
|
const responsive = style as ResponsiveStyleObject;
|
|
880
1090
|
|
|
881
|
-
// Base styles
|
|
1091
|
+
// Base styles — in fluid mode rewrite scalable values to clamp()
|
|
1092
|
+
// and rewrite width===maxWidth into the container pattern.
|
|
882
1093
|
if (responsive.base && Object.keys(responsive.base).length > 0) {
|
|
883
|
-
|
|
1094
|
+
let baseStyle = responsive.base;
|
|
1095
|
+
if (fluidActive) {
|
|
1096
|
+
baseStyle = applyContainerPattern(baseStyle, true);
|
|
1097
|
+
baseStyle = applyFluidToStyle(baseStyle, responsiveScales!, breakpoints);
|
|
1098
|
+
}
|
|
1099
|
+
const properties = applyRemConversion(styleObjectToCSS(baseStyle), remConfig);
|
|
884
1100
|
if (properties) {
|
|
885
1101
|
css.push(`${fullSelector} { ${properties}; }`);
|
|
886
1102
|
}
|
|
@@ -891,12 +1107,13 @@ export function generateInteractiveCSS(
|
|
|
891
1107
|
|
|
892
1108
|
// Merge explicit breakpoint styles with auto-scaled values derived
|
|
893
1109
|
// from the base, skipping any property the author explicitly set.
|
|
1110
|
+
// In fluid mode skip the auto-scaled portion — base already has clamp().
|
|
894
1111
|
let merged: StyleObject | null = null;
|
|
895
1112
|
if (explicit && Object.keys(explicit).length > 0) {
|
|
896
|
-
merged = { ...explicit };
|
|
1113
|
+
merged = fluidActive ? applyContainerPattern({ ...explicit }, true) : { ...explicit };
|
|
897
1114
|
}
|
|
898
1115
|
|
|
899
|
-
if (scalingEnabled && responsive.base) {
|
|
1116
|
+
if (scalingEnabled && !fluidActive && responsive.base) {
|
|
900
1117
|
const scaled = scaleStyleForBreakpoint(
|
|
901
1118
|
responsive.base,
|
|
902
1119
|
responsiveScales!,
|
|
@@ -921,13 +1138,18 @@ export function generateInteractiveCSS(
|
|
|
921
1138
|
// Flat style object
|
|
922
1139
|
const flatStyle = style as StyleObject;
|
|
923
1140
|
if (Object.keys(flatStyle).length > 0) {
|
|
924
|
-
|
|
1141
|
+
let baseFlat = flatStyle;
|
|
1142
|
+
if (fluidActive) {
|
|
1143
|
+
baseFlat = applyContainerPattern(baseFlat, true);
|
|
1144
|
+
baseFlat = applyFluidToStyle(baseFlat, responsiveScales!, breakpoints);
|
|
1145
|
+
}
|
|
1146
|
+
const properties = applyRemConversion(styleObjectToCSS(baseFlat), remConfig);
|
|
925
1147
|
if (properties) {
|
|
926
1148
|
css.push(`${fullSelector} { ${properties}; }`);
|
|
927
1149
|
}
|
|
928
1150
|
|
|
929
|
-
// Auto-scale the flat style into each enabled breakpoint.
|
|
930
|
-
if (scalingEnabled) {
|
|
1151
|
+
// Auto-scale the flat style into each enabled breakpoint — breakpoints mode only.
|
|
1152
|
+
if (scalingEnabled && !fluidActive) {
|
|
931
1153
|
for (const [breakpointName, breakpointValue] of sortedBreakpoints) {
|
|
932
1154
|
const scaled = scaleStyleForBreakpoint(
|
|
933
1155
|
flatStyle,
|
|
@@ -2,12 +2,32 @@ import { describe, test, expect } from 'bun:test';
|
|
|
2
2
|
import {
|
|
3
3
|
CSS_PROPERTIES,
|
|
4
4
|
CSS_PROPERTIES_DEFINITION,
|
|
5
|
+
CSS_PROPERTY_GROUPS,
|
|
5
6
|
filterCSSProperties,
|
|
6
7
|
getPropertyValues,
|
|
7
8
|
filterPropertyValues,
|
|
8
|
-
getPropertyType
|
|
9
|
+
getPropertyType,
|
|
10
|
+
VISUAL_MODE_PROPERTIES,
|
|
11
|
+
isVisualModeProperty,
|
|
12
|
+
VISUAL_MODE_RULES,
|
|
13
|
+
isVisualModeRowVisible,
|
|
14
|
+
type VisualModeRuleContext,
|
|
15
|
+
UNITLESS_PROPERTIES,
|
|
16
|
+
appendsPxByDefault,
|
|
9
17
|
} from './cssProperties';
|
|
10
18
|
|
|
19
|
+
const baseCtx: VisualModeRuleContext = {
|
|
20
|
+
display: 'block',
|
|
21
|
+
parentDisplay: 'block',
|
|
22
|
+
position: 'static',
|
|
23
|
+
tagName: 'div',
|
|
24
|
+
hasPaddingLonghand: false,
|
|
25
|
+
hasMarginLonghand: false,
|
|
26
|
+
hasBackgroundImage: false,
|
|
27
|
+
};
|
|
28
|
+
const ctx = (overrides: Partial<VisualModeRuleContext> = {}): VisualModeRuleContext =>
|
|
29
|
+
({ ...baseCtx, ...overrides });
|
|
30
|
+
|
|
11
31
|
describe('cssProperties', () => {
|
|
12
32
|
describe('CSS_PROPERTIES', () => {
|
|
13
33
|
test('is an array of property names', () => {
|
|
@@ -249,4 +269,230 @@ describe('cssProperties', () => {
|
|
|
249
269
|
expect(getPropertyType('padding')).toBe('string');
|
|
250
270
|
});
|
|
251
271
|
});
|
|
272
|
+
|
|
273
|
+
describe('VISUAL_MODE_PROPERTIES', () => {
|
|
274
|
+
test('contains common Webflow-style groups', () => {
|
|
275
|
+
expect(VISUAL_MODE_PROPERTIES['Layout']).toContain('display');
|
|
276
|
+
expect(VISUAL_MODE_PROPERTIES['Spacing']).toContain('padding');
|
|
277
|
+
expect(VISUAL_MODE_PROPERTIES['Typography']).toContain('fontSize');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('every group name maps to a known CSS_PROPERTY_GROUPS group', () => {
|
|
281
|
+
for (const groupName of Object.keys(VISUAL_MODE_PROPERTIES)) {
|
|
282
|
+
expect(CSS_PROPERTY_GROUPS[groupName]).toBeDefined();
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('every property in VISUAL_MODE_PROPERTIES belongs to its declared group in CSS_PROPERTY_GROUPS', () => {
|
|
287
|
+
for (const [groupName, props] of Object.entries(VISUAL_MODE_PROPERTIES)) {
|
|
288
|
+
for (const prop of props) {
|
|
289
|
+
expect(CSS_PROPERTY_GROUPS[groupName]).toContain(prop);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe('isVisualModeProperty', () => {
|
|
296
|
+
test('returns true for properties listed in VISUAL_MODE_PROPERTIES', () => {
|
|
297
|
+
expect(isVisualModeProperty('display')).toBe(true);
|
|
298
|
+
expect(isVisualModeProperty('padding')).toBe(true);
|
|
299
|
+
expect(isVisualModeProperty('color')).toBe(true);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('returns false for properties not in VISUAL_MODE_PROPERTIES', () => {
|
|
303
|
+
expect(isVisualModeProperty('gridTemplateRows')).toBe(false);
|
|
304
|
+
expect(isVisualModeProperty('clipPath')).toBe(false);
|
|
305
|
+
expect(isVisualModeProperty('nonExistent')).toBe(false);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe('VISUAL_MODE_RULES — flex/grid container props', () => {
|
|
310
|
+
test('flexDirection visible only when display is flex/inline-flex', () => {
|
|
311
|
+
expect(VISUAL_MODE_RULES.flexDirection(ctx({ display: 'flex' }))).toBe(true);
|
|
312
|
+
expect(VISUAL_MODE_RULES.flexDirection(ctx({ display: 'inline-flex' }))).toBe(true);
|
|
313
|
+
expect(VISUAL_MODE_RULES.flexDirection(ctx({ display: 'block' }))).toBe(false);
|
|
314
|
+
expect(VISUAL_MODE_RULES.flexDirection(ctx({ display: 'grid' }))).toBe(false);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test('justifyContent visible for flex AND grid', () => {
|
|
318
|
+
expect(VISUAL_MODE_RULES.justifyContent(ctx({ display: 'flex' }))).toBe(true);
|
|
319
|
+
expect(VISUAL_MODE_RULES.justifyContent(ctx({ display: 'grid' }))).toBe(true);
|
|
320
|
+
expect(VISUAL_MODE_RULES.justifyContent(ctx({ display: 'block' }))).toBe(false);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test('gridTemplateColumns visible only for grid/inline-grid', () => {
|
|
324
|
+
expect(VISUAL_MODE_RULES.gridTemplateColumns(ctx({ display: 'grid' }))).toBe(true);
|
|
325
|
+
expect(VISUAL_MODE_RULES.gridTemplateColumns(ctx({ display: 'inline-grid' }))).toBe(true);
|
|
326
|
+
expect(VISUAL_MODE_RULES.gridTemplateColumns(ctx({ display: 'flex' }))).toBe(false);
|
|
327
|
+
expect(VISUAL_MODE_RULES.gridTemplateColumns(ctx({ display: 'block' }))).toBe(false);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test('gap visible for both flex and grid containers', () => {
|
|
331
|
+
expect(VISUAL_MODE_RULES.gap(ctx({ display: 'flex' }))).toBe(true);
|
|
332
|
+
expect(VISUAL_MODE_RULES.gap(ctx({ display: 'grid' }))).toBe(true);
|
|
333
|
+
expect(VISUAL_MODE_RULES.gap(ctx({ display: 'block' }))).toBe(false);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe('VISUAL_MODE_RULES — flex/grid item props (parent display)', () => {
|
|
338
|
+
test('flexGrow visible only when parent is flex', () => {
|
|
339
|
+
expect(VISUAL_MODE_RULES.flexGrow(ctx({ parentDisplay: 'flex' }))).toBe(true);
|
|
340
|
+
expect(VISUAL_MODE_RULES.flexGrow(ctx({ parentDisplay: 'inline-flex' }))).toBe(true);
|
|
341
|
+
expect(VISUAL_MODE_RULES.flexGrow(ctx({ parentDisplay: 'block' }))).toBe(false);
|
|
342
|
+
expect(VISUAL_MODE_RULES.flexGrow(ctx({ parentDisplay: 'grid' }))).toBe(false);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test('gridArea visible only when parent is grid', () => {
|
|
346
|
+
expect(VISUAL_MODE_RULES.gridArea(ctx({ parentDisplay: 'grid' }))).toBe(true);
|
|
347
|
+
expect(VISUAL_MODE_RULES.gridArea(ctx({ parentDisplay: 'inline-grid' }))).toBe(true);
|
|
348
|
+
expect(VISUAL_MODE_RULES.gridArea(ctx({ parentDisplay: 'flex' }))).toBe(false);
|
|
349
|
+
expect(VISUAL_MODE_RULES.gridArea(ctx({ parentDisplay: 'block' }))).toBe(false);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test('alignSelf visible when parent is flex OR grid', () => {
|
|
353
|
+
expect(VISUAL_MODE_RULES.alignSelf(ctx({ parentDisplay: 'flex' }))).toBe(true);
|
|
354
|
+
expect(VISUAL_MODE_RULES.alignSelf(ctx({ parentDisplay: 'grid' }))).toBe(true);
|
|
355
|
+
expect(VISUAL_MODE_RULES.alignSelf(ctx({ parentDisplay: 'block' }))).toBe(false);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test('item rules ignore own display — only parent display matters', () => {
|
|
359
|
+
// Element has display:block but parent is flex → flexGrow still visible.
|
|
360
|
+
expect(VISUAL_MODE_RULES.flexGrow(ctx({ display: 'block', parentDisplay: 'flex' }))).toBe(true);
|
|
361
|
+
// Element has display:flex but parent is block → flexGrow hidden.
|
|
362
|
+
expect(VISUAL_MODE_RULES.flexGrow(ctx({ display: 'flex', parentDisplay: 'block' }))).toBe(false);
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe('VISUAL_MODE_RULES — position-dependent', () => {
|
|
367
|
+
test('top/right/bottom/left/zIndex hidden when position is static', () => {
|
|
368
|
+
const c = ctx({ position: 'static' });
|
|
369
|
+
expect(VISUAL_MODE_RULES.top(c)).toBe(false);
|
|
370
|
+
expect(VISUAL_MODE_RULES.right(c)).toBe(false);
|
|
371
|
+
expect(VISUAL_MODE_RULES.bottom(c)).toBe(false);
|
|
372
|
+
expect(VISUAL_MODE_RULES.left(c)).toBe(false);
|
|
373
|
+
expect(VISUAL_MODE_RULES.zIndex(c)).toBe(false);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test('top/right/bottom/left/zIndex visible when position is non-static', () => {
|
|
377
|
+
for (const position of ['relative', 'absolute', 'fixed', 'sticky']) {
|
|
378
|
+
const c = ctx({ position });
|
|
379
|
+
expect(VISUAL_MODE_RULES.top(c)).toBe(true);
|
|
380
|
+
expect(VISUAL_MODE_RULES.zIndex(c)).toBe(true);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
describe('VISUAL_MODE_RULES — tag-based', () => {
|
|
386
|
+
test('objectFit visible only for img/video', () => {
|
|
387
|
+
expect(VISUAL_MODE_RULES.objectFit(ctx({ tagName: 'img' }))).toBe(true);
|
|
388
|
+
expect(VISUAL_MODE_RULES.objectFit(ctx({ tagName: 'video' }))).toBe(true);
|
|
389
|
+
expect(VISUAL_MODE_RULES.objectFit(ctx({ tagName: 'div' }))).toBe(false);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test('listStyle visible only for ul/ol/li', () => {
|
|
393
|
+
expect(VISUAL_MODE_RULES.listStyle(ctx({ tagName: 'ul' }))).toBe(true);
|
|
394
|
+
expect(VISUAL_MODE_RULES.listStyle(ctx({ tagName: 'ol' }))).toBe(true);
|
|
395
|
+
expect(VISUAL_MODE_RULES.listStyle(ctx({ tagName: 'li' }))).toBe(true);
|
|
396
|
+
expect(VISUAL_MODE_RULES.listStyle(ctx({ tagName: 'div' }))).toBe(false);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
describe('VISUAL_MODE_RULES — padding/margin shorthand-vs-longhand toggle', () => {
|
|
401
|
+
test('padding shown when no longhand set; longhands hidden', () => {
|
|
402
|
+
const c = ctx({ hasPaddingLonghand: false });
|
|
403
|
+
expect(VISUAL_MODE_RULES.padding(c)).toBe(true);
|
|
404
|
+
expect(VISUAL_MODE_RULES.paddingTop(c)).toBe(false);
|
|
405
|
+
expect(VISUAL_MODE_RULES.paddingRight(c)).toBe(false);
|
|
406
|
+
expect(VISUAL_MODE_RULES.paddingBottom(c)).toBe(false);
|
|
407
|
+
expect(VISUAL_MODE_RULES.paddingLeft(c)).toBe(false);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test('padding hidden when any longhand set; longhands shown', () => {
|
|
411
|
+
const c = ctx({ hasPaddingLonghand: true });
|
|
412
|
+
expect(VISUAL_MODE_RULES.padding(c)).toBe(false);
|
|
413
|
+
expect(VISUAL_MODE_RULES.paddingTop(c)).toBe(true);
|
|
414
|
+
expect(VISUAL_MODE_RULES.paddingRight(c)).toBe(true);
|
|
415
|
+
expect(VISUAL_MODE_RULES.paddingBottom(c)).toBe(true);
|
|
416
|
+
expect(VISUAL_MODE_RULES.paddingLeft(c)).toBe(true);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test('margin mirrors padding behavior', () => {
|
|
420
|
+
const noLong = ctx({ hasMarginLonghand: false });
|
|
421
|
+
expect(VISUAL_MODE_RULES.margin(noLong)).toBe(true);
|
|
422
|
+
expect(VISUAL_MODE_RULES.marginTop(noLong)).toBe(false);
|
|
423
|
+
const withLong = ctx({ hasMarginLonghand: true });
|
|
424
|
+
expect(VISUAL_MODE_RULES.margin(withLong)).toBe(false);
|
|
425
|
+
expect(VISUAL_MODE_RULES.marginTop(withLong)).toBe(true);
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
describe('isVisualModeRowVisible', () => {
|
|
430
|
+
test('returns true for properties without a rule', () => {
|
|
431
|
+
// width has no rule entry → always visible regardless of context.
|
|
432
|
+
expect(isVisualModeRowVisible('width', ctx())).toBe(true);
|
|
433
|
+
expect(isVisualModeRowVisible('color', ctx())).toBe(true);
|
|
434
|
+
expect(isVisualModeRowVisible('nonExistentProp', ctx())).toBe(true);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test('delegates to the rule for known properties', () => {
|
|
438
|
+
expect(isVisualModeRowVisible('flexDirection', ctx({ display: 'flex' }))).toBe(true);
|
|
439
|
+
expect(isVisualModeRowVisible('flexDirection', ctx({ display: 'block' }))).toBe(false);
|
|
440
|
+
expect(isVisualModeRowVisible('top', ctx({ position: 'absolute' }))).toBe(true);
|
|
441
|
+
expect(isVisualModeRowVisible('top', ctx({ position: 'static' }))).toBe(false);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
describe('UNITLESS_PROPERTIES', () => {
|
|
446
|
+
test('contains the canonical unitless props', () => {
|
|
447
|
+
expect(UNITLESS_PROPERTIES.has('fontWeight')).toBe(true);
|
|
448
|
+
expect(UNITLESS_PROPERTIES.has('lineHeight')).toBe(true);
|
|
449
|
+
expect(UNITLESS_PROPERTIES.has('opacity')).toBe(true);
|
|
450
|
+
expect(UNITLESS_PROPERTIES.has('zIndex')).toBe(true);
|
|
451
|
+
expect(UNITLESS_PROPERTIES.has('flexGrow')).toBe(true);
|
|
452
|
+
expect(UNITLESS_PROPERTIES.has('order')).toBe(true);
|
|
453
|
+
expect(UNITLESS_PROPERTIES.has('aspectRatio')).toBe(true);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test('does not contain length-accepting props', () => {
|
|
457
|
+
expect(UNITLESS_PROPERTIES.has('width')).toBe(false);
|
|
458
|
+
expect(UNITLESS_PROPERTIES.has('padding')).toBe(false);
|
|
459
|
+
expect(UNITLESS_PROPERTIES.has('fontSize')).toBe(false);
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
describe('appendsPxByDefault', () => {
|
|
464
|
+
test('returns true for length-accepting props', () => {
|
|
465
|
+
expect(appendsPxByDefault('width')).toBe(true);
|
|
466
|
+
expect(appendsPxByDefault('height')).toBe(true);
|
|
467
|
+
expect(appendsPxByDefault('padding')).toBe(true);
|
|
468
|
+
expect(appendsPxByDefault('paddingTop')).toBe(true);
|
|
469
|
+
expect(appendsPxByDefault('margin')).toBe(true);
|
|
470
|
+
expect(appendsPxByDefault('gap')).toBe(true);
|
|
471
|
+
expect(appendsPxByDefault('fontSize')).toBe(true);
|
|
472
|
+
expect(appendsPxByDefault('borderRadius')).toBe(true);
|
|
473
|
+
expect(appendsPxByDefault('top')).toBe(true);
|
|
474
|
+
expect(appendsPxByDefault('left')).toBe(true);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test('returns false for unitless props', () => {
|
|
478
|
+
expect(appendsPxByDefault('lineHeight')).toBe(false);
|
|
479
|
+
expect(appendsPxByDefault('fontWeight')).toBe(false);
|
|
480
|
+
expect(appendsPxByDefault('opacity')).toBe(false);
|
|
481
|
+
expect(appendsPxByDefault('zIndex')).toBe(false);
|
|
482
|
+
expect(appendsPxByDefault('flexGrow')).toBe(false);
|
|
483
|
+
expect(appendsPxByDefault('order')).toBe(false);
|
|
484
|
+
expect(appendsPxByDefault('aspectRatio')).toBe(false);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test('returns false for keyword-only (select / boolean) props', () => {
|
|
488
|
+
expect(appendsPxByDefault('display')).toBe(false);
|
|
489
|
+
expect(appendsPxByDefault('position')).toBe(false);
|
|
490
|
+
expect(appendsPxByDefault('borderStyle')).toBe(false);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test('defaults to true for properties without a recognized unitless / keyword type', () => {
|
|
494
|
+
expect(appendsPxByDefault('inset')).toBe(true);
|
|
495
|
+
expect(appendsPxByDefault('someUnknownNewCSSProperty')).toBe(true);
|
|
496
|
+
});
|
|
497
|
+
});
|
|
252
498
|
});
|