meno-core 1.0.44 → 1.0.46
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 +214 -63
- package/dist/bin/cli.js +2 -2
- package/dist/build-static.js +9 -9
- package/dist/chunks/{chunk-BXB6AX76.js → chunk-2QK6U5UK.js} +7 -3
- package/dist/chunks/chunk-2QK6U5UK.js.map +7 -0
- package/dist/chunks/{chunk-3XXLBUYA.js → chunk-77ZB6353.js} +33 -21
- package/dist/chunks/chunk-77ZB6353.js.map +7 -0
- package/dist/chunks/{chunk-5TJA272J.js → chunk-C6U5T5S5.js} +9 -9
- package/dist/chunks/{chunk-KPNSCQNA.js → chunk-FED5MME6.js} +285 -19
- package/dist/chunks/chunk-FED5MME6.js.map +7 -0
- package/dist/chunks/{chunk-5Z5VQRTJ.js → chunk-I7YIGZXT.js} +4 -4
- package/dist/chunks/{chunk-5Z5VQRTJ.js.map → chunk-I7YIGZXT.js.map} +2 -2
- package/dist/chunks/{chunk-46AVQA4V.js → chunk-IF3RATBY.js} +2 -2
- package/dist/chunks/{chunk-S2HXJTAF.js → chunk-KITQJYZV.js} +5 -1
- package/dist/chunks/chunk-KITQJYZV.js.map +7 -0
- package/dist/chunks/{chunk-G3WIPJN3.js → chunk-ORN7S4AP.js} +6 -6
- package/dist/chunks/{chunk-2NBGG2LJ.js → chunk-UUA5LEWF.js} +89 -4
- package/dist/chunks/{chunk-2NBGG2LJ.js.map → chunk-UUA5LEWF.js.map} +2 -2
- package/dist/chunks/{chunk-X5SH4HXS.js → chunk-XSWR3QLI.js} +33 -1
- package/dist/chunks/chunk-XSWR3QLI.js.map +7 -0
- package/dist/chunks/{chunk-YEJVSNVF.js → chunk-ZTKHJQ2Z.js} +14 -3
- package/dist/chunks/chunk-ZTKHJQ2Z.js.map +7 -0
- package/dist/chunks/{chunk-OLNQMCSR.js → chunk-ZWYDT3QJ.js} +3 -3
- package/dist/chunks/{configService-KYO3TXDS.js → configService-DYCUEURL.js} +4 -4
- package/dist/chunks/{constants-34NUGHQR.js → constants-GWBAD66U.js} +2 -2
- package/dist/entries/server-router.js +10 -10
- package/dist/lib/client/index.js +5 -5
- package/dist/lib/server/index.js +589 -145
- package/dist/lib/server/index.js.map +3 -3
- package/dist/lib/shared/index.js +14 -6
- package/dist/lib/shared/index.js.map +2 -2
- package/dist/lib/test-utils/index.js +1 -1
- package/lib/client/templateEngine.test.ts +64 -0
- package/lib/server/astro/astroEmitHelpers.ts +18 -0
- package/lib/server/astro/cmsPageEmitter.ts +31 -1
- package/lib/server/astro/componentEmitter.test.ts +59 -0
- package/lib/server/astro/componentEmitter.ts +43 -10
- package/lib/server/astro/cssCollector.ts +58 -11
- package/lib/server/astro/nodeToAstro.test.ts +397 -5
- package/lib/server/astro/nodeToAstro.ts +478 -63
- package/lib/server/astro/pageEmitter.ts +31 -1
- package/lib/server/astro/tailwindMapper.test.ts +119 -0
- package/lib/server/astro/tailwindMapper.ts +67 -1
- package/lib/server/runtime/httpServer.ts +12 -4
- package/lib/server/services/configService.ts +13 -0
- package/lib/server/ssr/htmlGenerator.ts +5 -2
- package/lib/server/ssr/jsCollector.ts +2 -2
- package/lib/server/ssr/ssrRenderer.test.ts +32 -0
- package/lib/server/ssr/ssrRenderer.ts +26 -11
- package/lib/shared/constants.ts +9 -2
- package/lib/shared/cssGeneration.test.ts +109 -3
- package/lib/shared/cssGeneration.ts +109 -13
- package/lib/shared/cssNamedColors.ts +47 -0
- package/lib/shared/cssProperties.ts +99 -4
- package/lib/shared/index.ts +1 -0
- package/lib/shared/nodeUtils.ts +28 -0
- package/lib/shared/responsiveScaling.test.ts +20 -3
- package/lib/shared/responsiveScaling.ts +55 -1
- package/lib/shared/responsiveStyleUtils.test.ts +7 -8
- package/lib/shared/responsiveStyleUtils.ts +6 -7
- package/lib/shared/types/components.ts +1 -1
- package/lib/shared/types/variables.test.ts +9 -2
- package/lib/shared/types/variables.ts +5 -1
- package/lib/shared/utilityClassMapper.ts +14 -1
- package/lib/shared/validation/propValidator.ts +6 -0
- package/lib/shared/validation/schemas.ts +2 -2
- package/package.json +1 -1
- package/dist/chunks/chunk-3XXLBUYA.js.map +0 -7
- package/dist/chunks/chunk-BXB6AX76.js.map +0 -7
- package/dist/chunks/chunk-KPNSCQNA.js.map +0 -7
- package/dist/chunks/chunk-S2HXJTAF.js.map +0 -7
- package/dist/chunks/chunk-X5SH4HXS.js.map +0 -7
- package/dist/chunks/chunk-YEJVSNVF.js.map +0 -7
- /package/dist/chunks/{chunk-5TJA272J.js.map → chunk-C6U5T5S5.js.map} +0 -0
- /package/dist/chunks/{chunk-46AVQA4V.js.map → chunk-IF3RATBY.js.map} +0 -0
- /package/dist/chunks/{chunk-G3WIPJN3.js.map → chunk-ORN7S4AP.js.map} +0 -0
- /package/dist/chunks/{chunk-OLNQMCSR.js.map → chunk-ZWYDT3QJ.js.map} +0 -0
- /package/dist/chunks/{configService-KYO3TXDS.js.map → configService-DYCUEURL.js.map} +0 -0
- /package/dist/chunks/{constants-34NUGHQR.js.map → constants-GWBAD66U.js.map} +0 -0
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { generateRuleForClass, generateUtilityCSS, extractUtilityClassesFromHTML } from './cssGeneration';
|
|
2
|
+
import { generateRuleForClass, generateUtilityCSS, generateSingleClassCSS, extractUtilityClassesFromHTML, generateInteractiveCSS } from './cssGeneration';
|
|
3
|
+
import { DEFAULT_BREAKPOINTS } from './breakpoints';
|
|
4
|
+
import type { ResponsiveScales } from './responsiveScaling';
|
|
5
|
+
import type { InteractiveStyles } from './types/styles';
|
|
3
6
|
|
|
4
7
|
describe('extractUtilityClassesFromHTML', () => {
|
|
5
8
|
test('extracts ins- classes', () => {
|
|
@@ -102,9 +105,33 @@ describe('cssGeneration', () => {
|
|
|
102
105
|
expect(rule).toBe('border: 1px solid var(--primary);');
|
|
103
106
|
});
|
|
104
107
|
|
|
105
|
-
test('border-color
|
|
108
|
+
test('border-color with named CSS color outputs directly', () => {
|
|
106
109
|
const rule = generateRuleForClass('bc-red');
|
|
107
|
-
expect(rule).toBe('border-color:
|
|
110
|
+
expect(rule).toBe('border-color: red;');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('border-color with variable uses var()', () => {
|
|
114
|
+
const rule = generateRuleForClass('bc-primary');
|
|
115
|
+
expect(rule).toBe('border-color: var(--primary);');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('CSS named colors vs variables', () => {
|
|
120
|
+
test('color with named color outputs directly', () => {
|
|
121
|
+
expect(generateRuleForClass('c-red')).toBe('color: red;');
|
|
122
|
+
expect(generateRuleForClass('c-blue')).toBe('color: blue;');
|
|
123
|
+
expect(generateRuleForClass('c-transparent')).toBe('color: transparent;');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('background-color with named color outputs directly', () => {
|
|
127
|
+
expect(generateRuleForClass('bgc-green')).toBe('background-color: green;');
|
|
128
|
+
expect(generateRuleForClass('bgc-white')).toBe('background-color: white;');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('color with variable name still uses var()', () => {
|
|
132
|
+
expect(generateRuleForClass('c-primary')).toBe('color: var(--primary);');
|
|
133
|
+
expect(generateRuleForClass('c-text')).toBe('color: var(--text);');
|
|
134
|
+
expect(generateRuleForClass('bgc-background')).toBe('background-color: var(--background);');
|
|
108
135
|
});
|
|
109
136
|
});
|
|
110
137
|
|
|
@@ -190,3 +217,82 @@ describe('cssGeneration', () => {
|
|
|
190
217
|
});
|
|
191
218
|
});
|
|
192
219
|
});
|
|
220
|
+
|
|
221
|
+
describe('generateSingleClassCSS - responsive classes', () => {
|
|
222
|
+
test('generates correct media query for tablet-prefixed class', () => {
|
|
223
|
+
const css = generateSingleClassCSS('t-fs-132px', DEFAULT_BREAKPOINTS);
|
|
224
|
+
expect(css).toContain('@media (max-width: 1024px)');
|
|
225
|
+
expect(css).toContain('font-size: 132px');
|
|
226
|
+
expect(css).toContain('.t-fs-132px');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('generates correct media query for mobile-prefixed class', () => {
|
|
230
|
+
const css = generateSingleClassCSS('mob-fs-24px', DEFAULT_BREAKPOINTS);
|
|
231
|
+
expect(css).toContain('@media (max-width: 540px)');
|
|
232
|
+
expect(css).toContain('font-size: 24px');
|
|
233
|
+
expect(css).toContain('.mob-fs-24px');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('generates base rule for non-prefixed class', () => {
|
|
237
|
+
const css = generateSingleClassCSS('fs-48px', DEFAULT_BREAKPOINTS);
|
|
238
|
+
expect(css).toContain('.fs-48px');
|
|
239
|
+
expect(css).toContain('font-size: 48px');
|
|
240
|
+
expect(css).not.toContain('@media');
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('generateInteractiveCSS — auto-responsive scaling', () => {
|
|
245
|
+
const scalesEnabled: ResponsiveScales = {
|
|
246
|
+
enabled: true,
|
|
247
|
+
baseReference: 16,
|
|
248
|
+
padding: { tablet: 0.75, mobile: 0.5 },
|
|
249
|
+
fontSize: { tablet: 0.88, mobile: 0.75 },
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
test('flat interactive style gets scaled @media overrides', () => {
|
|
253
|
+
const styles: InteractiveStyles = [
|
|
254
|
+
{ prefix: '', postfix: ':hover', style: { fontSize: '100px' } },
|
|
255
|
+
];
|
|
256
|
+
const css = generateInteractiveCSS('c_heading_text', styles, DEFAULT_BREAKPOINTS, undefined, scalesEnabled);
|
|
257
|
+
expect(css).toContain('.c_heading_text:hover { font-size: 100px; }');
|
|
258
|
+
// tablet: 100 + (100-16)*(0.88-1) = 89.92 → 90
|
|
259
|
+
expect(css).toContain('@media (max-width: 1024px) { .c_heading_text:hover { font-size: 90px; } }');
|
|
260
|
+
// mobile: 100 + (100-16)*(0.75-1) = 79
|
|
261
|
+
expect(css).toContain('@media (max-width: 540px) { .c_heading_text:hover { font-size: 79px; } }');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('responsive style auto-scales properties not explicitly overridden', () => {
|
|
265
|
+
const styles: InteractiveStyles = [
|
|
266
|
+
{
|
|
267
|
+
prefix: '',
|
|
268
|
+
postfix: ':hover',
|
|
269
|
+
style: {
|
|
270
|
+
base: { padding: '40px', fontSize: '100px' },
|
|
271
|
+
tablet: { fontSize: '50px' }, // explicit tablet fontSize wins
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
];
|
|
275
|
+
const css = generateInteractiveCSS('c_btn', styles, DEFAULT_BREAKPOINTS, undefined, scalesEnabled);
|
|
276
|
+
// Tablet @media must contain the explicit fontSize AND the auto-scaled padding
|
|
277
|
+
// padding tablet: 40 + (40-16)*(0.75-1) = 34
|
|
278
|
+
expect(css).toMatch(/@media \(max-width: 1024px\) \{[^}]*font-size:\s*50px[^}]*\}/);
|
|
279
|
+
expect(css).toMatch(/@media \(max-width: 1024px\) \{[^}]*padding:\s*34px[^}]*\}/);
|
|
280
|
+
// Mobile: no explicit branch — both padding and fontSize auto-scale from base
|
|
281
|
+
// padding mobile: 40 + (40-16)*(-0.5) = 28; fontSize mobile: 79
|
|
282
|
+
expect(css).toMatch(/@media \(max-width: 540px\) \{[^}]*padding:\s*28px[^}]*\}/);
|
|
283
|
+
expect(css).toMatch(/@media \(max-width: 540px\) \{[^}]*font-size:\s*79px[^}]*\}/);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('scaling disabled is a no-op', () => {
|
|
287
|
+
const styles: InteractiveStyles = [
|
|
288
|
+
{ prefix: '', postfix: ':hover', style: { fontSize: '100px' } },
|
|
289
|
+
];
|
|
290
|
+
const cssNoScales = generateInteractiveCSS('c_heading', styles, DEFAULT_BREAKPOINTS);
|
|
291
|
+
const cssDisabled = generateInteractiveCSS('c_heading', styles, DEFAULT_BREAKPOINTS, undefined, {
|
|
292
|
+
...scalesEnabled,
|
|
293
|
+
enabled: false,
|
|
294
|
+
});
|
|
295
|
+
expect(cssNoScales).toBe(cssDisabled);
|
|
296
|
+
expect(cssNoScales).not.toContain('@media');
|
|
297
|
+
});
|
|
298
|
+
});
|
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
import { prefixToCSSProperty, propertyMap } from './utilityClassConfig';
|
|
8
8
|
import { getStyleValue, getDynamicStyle, isDynamicClass } from './styleValueRegistry';
|
|
9
|
+
import { isCssNamedColor } from './cssNamedColors';
|
|
9
10
|
import type { BreakpointConfig, LegacyBreakpointConfig } from './breakpoints';
|
|
10
11
|
import { DEFAULT_BREAKPOINTS, getBreakpointValues } from './breakpoints';
|
|
11
|
-
import type { ResponsiveScales } from './responsiveScaling';
|
|
12
|
-
import { scalePropertyValue } from './responsiveScaling';
|
|
12
|
+
import type { ResponsiveScales, CSSPropertyType } from './responsiveScaling';
|
|
13
|
+
import { scalePropertyValue, getScaleMultiplier } from './responsiveScaling';
|
|
13
14
|
import type { InteractiveStyles, StyleObject, ResponsiveStyleObject, StyleValue } from './types/styles';
|
|
14
15
|
import type { RemConversionConfig } from './pxToRem';
|
|
15
16
|
import { applyRemConversion, convertPxToRem, shouldConvertProperty } from './pxToRem';
|
|
@@ -84,6 +85,17 @@ const AUTO_RESPONSIVE_TYPE_MAP: Record<string, string> = {
|
|
|
84
85
|
'gap': 'gap',
|
|
85
86
|
'row-gap': 'gap',
|
|
86
87
|
'column-gap': 'gap',
|
|
88
|
+
'border-radius': 'borderRadius',
|
|
89
|
+
'border-top-left-radius': 'borderRadius',
|
|
90
|
+
'border-top-right-radius': 'borderRadius',
|
|
91
|
+
'border-bottom-left-radius': 'borderRadius',
|
|
92
|
+
'border-bottom-right-radius': 'borderRadius',
|
|
93
|
+
'width': 'size',
|
|
94
|
+
'height': 'size',
|
|
95
|
+
'max-width': 'size',
|
|
96
|
+
'max-height': 'size',
|
|
97
|
+
'min-width': 'size',
|
|
98
|
+
'min-height': 'size',
|
|
87
99
|
};
|
|
88
100
|
|
|
89
101
|
/**
|
|
@@ -384,7 +396,11 @@ export function generateRuleForClass(className: string): string | null {
|
|
|
384
396
|
// unless it's a hex color (#...) or RGB value
|
|
385
397
|
if ((prefix === 'bgc' || prefix === 'bg' || prefix === 'c' || prefix === 'bc')) {
|
|
386
398
|
// If it's not a hex color, RGB value, or pixel value, treat it as a CSS variable name
|
|
399
|
+
// Unless it's a CSS named color (red, blue, transparent, etc.) — use directly
|
|
387
400
|
if (!value.startsWith('#') && !value.includes('rgb') && !value.includes('px')) {
|
|
401
|
+
if (isCssNamedColor(value)) {
|
|
402
|
+
return `${cssProp}: ${value};`;
|
|
403
|
+
}
|
|
388
404
|
return `${cssProp}: var(--${value});`;
|
|
389
405
|
}
|
|
390
406
|
}
|
|
@@ -838,12 +854,14 @@ export function generateInteractiveCSS(
|
|
|
838
854
|
elementClass: string,
|
|
839
855
|
interactiveStyles: InteractiveStyles,
|
|
840
856
|
breakpoints: BreakpointConfig = DEFAULT_BREAKPOINTS,
|
|
841
|
-
remConfig?: RemConversionConfig
|
|
857
|
+
remConfig?: RemConversionConfig,
|
|
858
|
+
responsiveScales?: ResponsiveScales
|
|
842
859
|
): string {
|
|
843
860
|
const css: string[] = [];
|
|
844
861
|
|
|
845
862
|
// Extract breakpoint values for CSS media queries
|
|
846
863
|
const breakpointValues = getBreakpointValues(breakpoints);
|
|
864
|
+
const scalingEnabled = responsiveScales?.enabled === true;
|
|
847
865
|
|
|
848
866
|
for (const rule of interactiveStyles) {
|
|
849
867
|
const { prefix, postfix, style } = rule;
|
|
@@ -851,6 +869,11 @@ export function generateInteractiveCSS(
|
|
|
851
869
|
// Build full selector: {prefix}.element{postfix}
|
|
852
870
|
const fullSelector = `${prefix || ''}.${elementClass}${postfix || ''}`;
|
|
853
871
|
|
|
872
|
+
// Breakpoint styles (sorted by value descending)
|
|
873
|
+
const sortedBreakpoints = Object.entries(breakpointValues).sort(
|
|
874
|
+
([, a], [, b]) => b - a
|
|
875
|
+
);
|
|
876
|
+
|
|
854
877
|
if (isResponsiveStyle(style)) {
|
|
855
878
|
// Generate responsive rules
|
|
856
879
|
const responsive = style as ResponsiveStyleObject;
|
|
@@ -863,15 +886,30 @@ export function generateInteractiveCSS(
|
|
|
863
886
|
}
|
|
864
887
|
}
|
|
865
888
|
|
|
866
|
-
// Breakpoint styles (sorted by value descending)
|
|
867
|
-
const sortedBreakpoints = Object.entries(breakpointValues).sort(
|
|
868
|
-
([, a], [, b]) => b - a
|
|
869
|
-
);
|
|
870
|
-
|
|
871
889
|
for (const [breakpointName, breakpointValue] of sortedBreakpoints) {
|
|
872
|
-
const
|
|
873
|
-
|
|
874
|
-
|
|
890
|
+
const explicit = responsive[breakpointName];
|
|
891
|
+
|
|
892
|
+
// Merge explicit breakpoint styles with auto-scaled values derived
|
|
893
|
+
// from the base, skipping any property the author explicitly set.
|
|
894
|
+
let merged: StyleObject | null = null;
|
|
895
|
+
if (explicit && Object.keys(explicit).length > 0) {
|
|
896
|
+
merged = { ...explicit };
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
if (scalingEnabled && responsive.base) {
|
|
900
|
+
const scaled = scaleStyleForBreakpoint(
|
|
901
|
+
responsive.base,
|
|
902
|
+
responsiveScales!,
|
|
903
|
+
breakpointName,
|
|
904
|
+
new Set(Object.keys(explicit ?? {}))
|
|
905
|
+
);
|
|
906
|
+
if (Object.keys(scaled).length > 0) {
|
|
907
|
+
merged = { ...(merged ?? {}), ...scaled };
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (merged && Object.keys(merged).length > 0) {
|
|
912
|
+
const properties = applyRemConversion(styleObjectToCSS(merged), remConfig);
|
|
875
913
|
if (properties) {
|
|
876
914
|
css.push(
|
|
877
915
|
`@media (max-width: ${breakpointValue}px) { ${fullSelector} { ${properties}; } }`
|
|
@@ -887,6 +925,24 @@ export function generateInteractiveCSS(
|
|
|
887
925
|
if (properties) {
|
|
888
926
|
css.push(`${fullSelector} { ${properties}; }`);
|
|
889
927
|
}
|
|
928
|
+
|
|
929
|
+
// Auto-scale the flat style into each enabled breakpoint.
|
|
930
|
+
if (scalingEnabled) {
|
|
931
|
+
for (const [breakpointName, breakpointValue] of sortedBreakpoints) {
|
|
932
|
+
const scaled = scaleStyleForBreakpoint(
|
|
933
|
+
flatStyle,
|
|
934
|
+
responsiveScales!,
|
|
935
|
+
breakpointName,
|
|
936
|
+
new Set()
|
|
937
|
+
);
|
|
938
|
+
if (Object.keys(scaled).length === 0) continue;
|
|
939
|
+
const scaledProps = applyRemConversion(styleObjectToCSS(scaled), remConfig);
|
|
940
|
+
if (!scaledProps) continue;
|
|
941
|
+
css.push(
|
|
942
|
+
`@media (max-width: ${breakpointValue}px) { ${fullSelector} { ${scaledProps}; } }`
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
890
946
|
}
|
|
891
947
|
}
|
|
892
948
|
}
|
|
@@ -894,6 +950,45 @@ export function generateInteractiveCSS(
|
|
|
894
950
|
return css.join('\n');
|
|
895
951
|
}
|
|
896
952
|
|
|
953
|
+
/**
|
|
954
|
+
* Build a new style object containing auto-scaled values for every property
|
|
955
|
+
* in a given base style that falls into a scale category at `breakpointName`.
|
|
956
|
+
* Properties listed in `skipProperties` are left out (the author has set an
|
|
957
|
+
* explicit override at that breakpoint).
|
|
958
|
+
*/
|
|
959
|
+
function scaleStyleForBreakpoint(
|
|
960
|
+
baseStyle: StyleObject,
|
|
961
|
+
responsiveScales: ResponsiveScales,
|
|
962
|
+
breakpointName: string,
|
|
963
|
+
skipProperties: Set<string>
|
|
964
|
+
): StyleObject {
|
|
965
|
+
const result: StyleObject = {};
|
|
966
|
+
const baseRef = responsiveScales.baseReference ?? 16;
|
|
967
|
+
|
|
968
|
+
for (const [property, value] of Object.entries(baseStyle)) {
|
|
969
|
+
if (skipProperties.has(property)) continue;
|
|
970
|
+
if (typeof value === 'object' && value !== null && '_mapping' in value) continue;
|
|
971
|
+
if (value == null) continue;
|
|
972
|
+
|
|
973
|
+
const strValue = String(value);
|
|
974
|
+
if (strValue === '') continue;
|
|
975
|
+
|
|
976
|
+
const scale = getScaleMultiplier(
|
|
977
|
+
responsiveScales,
|
|
978
|
+
property as CSSPropertyType,
|
|
979
|
+
breakpointName
|
|
980
|
+
);
|
|
981
|
+
if (scale == null) continue;
|
|
982
|
+
|
|
983
|
+
const scaledValue = scalePropertyValue(strValue, baseRef, scale);
|
|
984
|
+
if (scaledValue == null || scaledValue === strValue) continue;
|
|
985
|
+
|
|
986
|
+
result[property] = scaledValue;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
return result;
|
|
990
|
+
}
|
|
991
|
+
|
|
897
992
|
/**
|
|
898
993
|
* Collect and generate CSS for all interactive styles in a page/component
|
|
899
994
|
*
|
|
@@ -904,12 +999,13 @@ export function generateInteractiveCSS(
|
|
|
904
999
|
export function generateAllInteractiveCSS(
|
|
905
1000
|
interactiveStylesMap: Map<string, InteractiveStyles>,
|
|
906
1001
|
breakpoints: BreakpointConfig = DEFAULT_BREAKPOINTS,
|
|
907
|
-
remConfig?: RemConversionConfig
|
|
1002
|
+
remConfig?: RemConversionConfig,
|
|
1003
|
+
responsiveScales?: ResponsiveScales
|
|
908
1004
|
): string {
|
|
909
1005
|
const cssBlocks: string[] = [];
|
|
910
1006
|
|
|
911
1007
|
for (const [elementClass, styles] of interactiveStylesMap) {
|
|
912
|
-
const css = generateInteractiveCSS(elementClass, styles, breakpoints, remConfig);
|
|
1008
|
+
const css = generateInteractiveCSS(elementClass, styles, breakpoints, remConfig, responsiveScales);
|
|
913
1009
|
if (css) {
|
|
914
1010
|
cssBlocks.push(css);
|
|
915
1011
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS Named Colors
|
|
3
|
+
* Complete set of CSS Level 4 named colors + keywords
|
|
4
|
+
* Used to distinguish named colors from CSS variable references
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const CSS_NAMED_COLORS: ReadonlySet<string> = new Set([
|
|
8
|
+
// CSS Level 4 named colors
|
|
9
|
+
'aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure',
|
|
10
|
+
'beige', 'bisque', 'black', 'blanchedalmond', 'blue', 'blueviolet', 'brown', 'burlywood',
|
|
11
|
+
'cadetblue', 'chartreuse', 'chocolate', 'coral', 'cornflowerblue', 'cornsilk', 'crimson', 'cyan',
|
|
12
|
+
'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray', 'darkgreen', 'darkgrey', 'darkkhaki',
|
|
13
|
+
'darkmagenta', 'darkolivegreen', 'darkorange', 'darkorchid', 'darkred', 'darksalmon',
|
|
14
|
+
'darkseagreen', 'darkslateblue', 'darkslategray', 'darkslategrey', 'darkturquoise', 'darkviolet',
|
|
15
|
+
'deeppink', 'deepskyblue', 'dimgray', 'dimgrey', 'dodgerblue',
|
|
16
|
+
'firebrick', 'floralwhite', 'forestgreen', 'fuchsia',
|
|
17
|
+
'gainsboro', 'ghostwhite', 'gold', 'goldenrod', 'gray', 'green', 'greenyellow', 'grey',
|
|
18
|
+
'honeydew', 'hotpink',
|
|
19
|
+
'indianred', 'indigo', 'ivory',
|
|
20
|
+
'khaki',
|
|
21
|
+
'lavender', 'lavenderblush', 'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan',
|
|
22
|
+
'lightgoldenrodyellow', 'lightgray', 'lightgreen', 'lightgrey', 'lightpink', 'lightsalmon',
|
|
23
|
+
'lightseagreen', 'lightskyblue', 'lightslategray', 'lightslategrey', 'lightsteelblue', 'lightyellow',
|
|
24
|
+
'lime', 'limegreen', 'linen',
|
|
25
|
+
'magenta', 'maroon', 'mediumaquamarine', 'mediumblue', 'mediumorchid', 'mediumpurple',
|
|
26
|
+
'mediumseagreen', 'mediumslateblue', 'mediumspringgreen', 'mediumturquoise', 'mediumvioletred',
|
|
27
|
+
'midnightblue', 'mintcream', 'mistyrose', 'moccasin',
|
|
28
|
+
'navajowhite', 'navy',
|
|
29
|
+
'oldlace', 'olive', 'olivedrab', 'orange', 'orangered', 'orchid',
|
|
30
|
+
'palegoldenrod', 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip', 'peachpuff',
|
|
31
|
+
'peru', 'pink', 'plum', 'powderblue', 'purple',
|
|
32
|
+
'rebeccapurple', 'red', 'rosybrown', 'royalblue',
|
|
33
|
+
'saddlebrown', 'salmon', 'sandybrown', 'seagreen', 'seashell', 'sienna', 'silver', 'skyblue',
|
|
34
|
+
'slateblue', 'slategray', 'slategrey', 'snow', 'springgreen', 'steelblue',
|
|
35
|
+
'tan', 'teal', 'thistle', 'tomato', 'turquoise',
|
|
36
|
+
'violet',
|
|
37
|
+
'wheat', 'white', 'whitesmoke',
|
|
38
|
+
'yellow', 'yellowgreen',
|
|
39
|
+
// CSS color keywords
|
|
40
|
+
'transparent', 'currentcolor',
|
|
41
|
+
// CSS-wide keywords (should never be wrapped in var())
|
|
42
|
+
'inherit', 'initial', 'unset', 'revert',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
export function isCssNamedColor(value: string): boolean {
|
|
46
|
+
return CSS_NAMED_COLORS.has(value.toLowerCase());
|
|
47
|
+
}
|
|
@@ -327,6 +327,95 @@ export const CSS_PROPERTIES_DEFINITION: Record<string, CSSPropertyDefinition> =
|
|
|
327
327
|
*/
|
|
328
328
|
export const CSS_PROPERTIES = Object.keys(CSS_PROPERTIES_DEFINITION);
|
|
329
329
|
|
|
330
|
+
/**
|
|
331
|
+
* Popularity tiers for CSS properties (lower = more common).
|
|
332
|
+
* Properties not listed default to tier 3.
|
|
333
|
+
* Used to sort autocomplete suggestions within each match group.
|
|
334
|
+
*/
|
|
335
|
+
const CSS_PROPERTY_PRIORITY: Record<string, number> = {
|
|
336
|
+
// Tier 1: Most commonly used
|
|
337
|
+
display: 1,
|
|
338
|
+
position: 1,
|
|
339
|
+
width: 1,
|
|
340
|
+
height: 1,
|
|
341
|
+
margin: 1,
|
|
342
|
+
padding: 1,
|
|
343
|
+
color: 1,
|
|
344
|
+
backgroundColor: 1,
|
|
345
|
+
fontSize: 1,
|
|
346
|
+
fontWeight: 1,
|
|
347
|
+
border: 1,
|
|
348
|
+
borderRadius: 1,
|
|
349
|
+
flex: 1,
|
|
350
|
+
flexDirection: 1,
|
|
351
|
+
justifyContent: 1,
|
|
352
|
+
alignItems: 1,
|
|
353
|
+
gap: 1,
|
|
354
|
+
opacity: 1,
|
|
355
|
+
overflow: 1,
|
|
356
|
+
zIndex: 1,
|
|
357
|
+
top: 1,
|
|
358
|
+
right: 1,
|
|
359
|
+
bottom: 1,
|
|
360
|
+
left: 1,
|
|
361
|
+
cursor: 1,
|
|
362
|
+
background: 1,
|
|
363
|
+
boxShadow: 1,
|
|
364
|
+
transition: 1,
|
|
365
|
+
transform: 1,
|
|
366
|
+
lineHeight: 1,
|
|
367
|
+
textAlign: 1,
|
|
368
|
+
|
|
369
|
+
// Tier 2: Common but less frequent
|
|
370
|
+
maxWidth: 2,
|
|
371
|
+
maxHeight: 2,
|
|
372
|
+
minWidth: 3,
|
|
373
|
+
minHeight: 3,
|
|
374
|
+
marginTop: 2,
|
|
375
|
+
marginBottom: 2,
|
|
376
|
+
marginLeft: 2,
|
|
377
|
+
marginRight: 2,
|
|
378
|
+
paddingTop: 2,
|
|
379
|
+
paddingBottom: 2,
|
|
380
|
+
paddingLeft: 2,
|
|
381
|
+
paddingRight: 2,
|
|
382
|
+
fontFamily: 2,
|
|
383
|
+
fontStyle: 2,
|
|
384
|
+
textDecoration: 2,
|
|
385
|
+
textTransform: 2,
|
|
386
|
+
letterSpacing: 2,
|
|
387
|
+
borderWidth: 2,
|
|
388
|
+
borderStyle: 2,
|
|
389
|
+
borderColor: 2,
|
|
390
|
+
borderBottom: 2,
|
|
391
|
+
borderTop: 2,
|
|
392
|
+
flexWrap: 2,
|
|
393
|
+
flexGrow: 2,
|
|
394
|
+
flexShrink: 2,
|
|
395
|
+
flexBasis: 2,
|
|
396
|
+
alignSelf: 2,
|
|
397
|
+
gridTemplateColumns: 2,
|
|
398
|
+
gridTemplateRows: 2,
|
|
399
|
+
backgroundImage: 2,
|
|
400
|
+
backgroundSize: 2,
|
|
401
|
+
backgroundPosition: 2,
|
|
402
|
+
whiteSpace: 2,
|
|
403
|
+
textOverflow: 2,
|
|
404
|
+
visibility: 2,
|
|
405
|
+
pointerEvents: 2,
|
|
406
|
+
userSelect: 2,
|
|
407
|
+
boxSizing: 2,
|
|
408
|
+
objectFit: 2,
|
|
409
|
+
filter: 2,
|
|
410
|
+
animation: 2,
|
|
411
|
+
outline: 2,
|
|
412
|
+
inset: 2,
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
function getPropertyPriority(property: string): number {
|
|
416
|
+
return CSS_PROPERTY_PRIORITY[property] ?? 3;
|
|
417
|
+
}
|
|
418
|
+
|
|
330
419
|
/**
|
|
331
420
|
* CSS Property Groups for organizing styles in the editor
|
|
332
421
|
* Order determines display order in the UI
|
|
@@ -406,7 +495,7 @@ function matchesAbbreviation(property: string, input: string): boolean {
|
|
|
406
495
|
* Supports both startsWith matching and camelCase abbreviation matching
|
|
407
496
|
*/
|
|
408
497
|
export function filterCSSProperties(input: string): string[] {
|
|
409
|
-
const normalizedInput = input.trim();
|
|
498
|
+
const normalizedInput = (input ?? '').trim();
|
|
410
499
|
if (!normalizedInput) {
|
|
411
500
|
return CSS_PROPERTIES.slice(0, 15); // Show first 15 by default
|
|
412
501
|
}
|
|
@@ -422,8 +511,14 @@ export function filterCSSProperties(input: string): string[] {
|
|
|
422
511
|
!startsWithMatches.includes(prop) // Don't duplicate startsWith matches
|
|
423
512
|
);
|
|
424
513
|
|
|
425
|
-
//
|
|
426
|
-
|
|
514
|
+
// Sort each group by popularity, then combine: exact matches first, then fuzzy matches
|
|
515
|
+
const byPriority = (a: string, b: string) =>
|
|
516
|
+
getPropertyPriority(a) - getPropertyPriority(b);
|
|
517
|
+
|
|
518
|
+
return [
|
|
519
|
+
...startsWithMatches.sort(byPriority),
|
|
520
|
+
...abbreviationMatches.sort(byPriority),
|
|
521
|
+
];
|
|
427
522
|
}
|
|
428
523
|
|
|
429
524
|
/**
|
|
@@ -449,7 +544,7 @@ export function filterPropertyValues(propertyName: string, input: string): strin
|
|
|
449
544
|
return []; // No predefined values for this property
|
|
450
545
|
}
|
|
451
546
|
|
|
452
|
-
const normalizedInput = input.trim().toLowerCase();
|
|
547
|
+
const normalizedInput = (input ?? '').trim().toLowerCase();
|
|
453
548
|
if (!normalizedInput) {
|
|
454
549
|
return values; // Return all values if no input
|
|
455
550
|
}
|
package/lib/shared/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ export * from './styleUtils';
|
|
|
23
23
|
export * from './cssProperties';
|
|
24
24
|
export * from './cssGeneration';
|
|
25
25
|
export * from './colorProperties';
|
|
26
|
+
export * from './cssNamedColors';
|
|
26
27
|
export * from './utilityClassMapper';
|
|
27
28
|
export * from './elementClassName';
|
|
28
29
|
export * from './styleValueRegistry';
|
package/lib/shared/nodeUtils.ts
CHANGED
|
@@ -146,6 +146,34 @@ export function isSlotMarker(node: ComponentNode | null | undefined): node is Sl
|
|
|
146
146
|
return node?.type === NODE_TYPE.SLOT;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Check whether a component definition's structure contains any slot marker.
|
|
151
|
+
*
|
|
152
|
+
* Used by callers that decide whether to preserve a component instance's
|
|
153
|
+
* `children` (e.g. the Figma paste swap) — there is no point passing children
|
|
154
|
+
* to a component whose template will never render them.
|
|
155
|
+
*/
|
|
156
|
+
export function componentHasSlot(def: ComponentDefinition | undefined | null): boolean {
|
|
157
|
+
const structure = def?.component?.structure;
|
|
158
|
+
if (!structure) return false;
|
|
159
|
+
return structureContainsSlot(structure);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function structureContainsSlot(node: unknown): boolean {
|
|
163
|
+
if (node == null) return false;
|
|
164
|
+
if (Array.isArray(node)) {
|
|
165
|
+
for (const item of node) {
|
|
166
|
+
if (structureContainsSlot(item)) return true;
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
if (typeof node !== 'object') return false;
|
|
171
|
+
const obj = node as Record<string, unknown>;
|
|
172
|
+
if (obj.type === NODE_TYPE.SLOT) return true;
|
|
173
|
+
if (obj.children !== undefined) return structureContainsSlot(obj.children);
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
149
177
|
/**
|
|
150
178
|
* Type guard to check if a node is an embed node
|
|
151
179
|
*/
|
|
@@ -175,10 +175,21 @@ describe('responsiveScaling', () => {
|
|
|
175
175
|
expect(result).toBeNull();
|
|
176
176
|
});
|
|
177
177
|
|
|
178
|
-
test('should scale percentage value', () => {
|
|
178
|
+
test('should NOT scale percentage value', () => {
|
|
179
|
+
// % is relative to the container and should pass through unchanged
|
|
179
180
|
const result = scaleValue('100%', 16, 0.5);
|
|
180
|
-
|
|
181
|
-
|
|
181
|
+
expect(result).toBe('100%');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('should NOT scale em value (small)', () => {
|
|
185
|
+
// em is relative to local font-size and should pass through unchanged
|
|
186
|
+
const result = scaleValue('2em', 16, 0.75);
|
|
187
|
+
expect(result).toBe('2em');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('should NOT scale em value (large)', () => {
|
|
191
|
+
const result = scaleValue('20em', 16, 0.75);
|
|
192
|
+
expect(result).toBe('20em');
|
|
182
193
|
});
|
|
183
194
|
});
|
|
184
195
|
|
|
@@ -216,6 +227,12 @@ describe('responsiveScaling', () => {
|
|
|
216
227
|
const result = scalePropertyValue('inherit', 16, 0.88);
|
|
217
228
|
expect(result).toBe('inherit');
|
|
218
229
|
});
|
|
230
|
+
|
|
231
|
+
test('should pass % and em through while scaling px in mixed values', () => {
|
|
232
|
+
// 20px → 19px; 50% stays; 2em stays
|
|
233
|
+
const result = scalePropertyValue('20px 50% 2em', 16, 0.75);
|
|
234
|
+
expect(result).toBe('19px 50% 2em');
|
|
235
|
+
});
|
|
219
236
|
});
|
|
220
237
|
|
|
221
238
|
describe('getResponsiveValues', () => {
|
|
@@ -18,10 +18,12 @@ export interface ResponsiveScales {
|
|
|
18
18
|
padding?: BreakpointScales;
|
|
19
19
|
margin?: BreakpointScales;
|
|
20
20
|
gap?: BreakpointScales;
|
|
21
|
+
borderRadius?: BreakpointScales;
|
|
22
|
+
size?: BreakpointScales;
|
|
21
23
|
[key: string]: boolean | number | BreakpointScales | undefined;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
export type CSSPropertyType = 'fontSize' | 'padding' | 'margin' | 'gap' | 'paddingTop' | 'paddingRight' | 'paddingBottom' | 'paddingLeft' | 'marginTop' | 'marginRight' | 'marginBottom' | 'marginLeft' | 'rowGap' | 'columnGap';
|
|
26
|
+
export type CSSPropertyType = 'fontSize' | 'padding' | 'margin' | 'gap' | 'paddingTop' | 'paddingRight' | 'paddingBottom' | 'paddingLeft' | 'marginTop' | 'marginRight' | 'marginBottom' | 'marginLeft' | 'rowGap' | 'columnGap' | 'borderRadius' | 'borderTopLeftRadius' | 'borderTopRightRadius' | 'borderBottomLeftRadius' | 'borderBottomRightRadius' | 'width' | 'height' | 'maxWidth' | 'maxHeight' | 'minWidth' | 'minHeight';
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* Map CSS property to its scale category
|
|
@@ -33,6 +35,8 @@ function getScaleCategory(property: CSSPropertyType): keyof ResponsiveScales | n
|
|
|
33
35
|
if (propertyStr.startsWith('padding')) return 'padding';
|
|
34
36
|
if (propertyStr.startsWith('margin')) return 'margin';
|
|
35
37
|
if (property === 'gap' || property === 'rowGap' || property === 'columnGap') return 'gap';
|
|
38
|
+
if (propertyStr === 'borderRadius' || (propertyStr.startsWith('border') && propertyStr.includes('Radius'))) return 'borderRadius';
|
|
39
|
+
if (property === 'width' || property === 'height' || property === 'maxWidth' || property === 'maxHeight' || property === 'minWidth' || property === 'minHeight') return 'size';
|
|
36
40
|
return null;
|
|
37
41
|
}
|
|
38
42
|
|
|
@@ -108,6 +112,12 @@ export function scaleValue(
|
|
|
108
112
|
const parsed = parseValue(valueStr);
|
|
109
113
|
if (!parsed) return null;
|
|
110
114
|
|
|
115
|
+
// `%` and `em` are already relative to their rendering context
|
|
116
|
+
// (container / local font-size), so they should not be rescaled.
|
|
117
|
+
if (parsed.unit === '%' || parsed.unit === 'em') {
|
|
118
|
+
return valueStr.trim();
|
|
119
|
+
}
|
|
120
|
+
|
|
111
121
|
const scaledValue = calculateResponsiveValue(parsed.value, baseReference, scale);
|
|
112
122
|
return `${scaledValue}${parsed.unit}`;
|
|
113
123
|
}
|
|
@@ -178,6 +188,42 @@ export function getResponsiveValues(
|
|
|
178
188
|
return result;
|
|
179
189
|
}
|
|
180
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Resolve the effective value of a CSS variable at a given breakpoint.
|
|
193
|
+
*
|
|
194
|
+
* Precedence (mirrors generateVariablesCSS in cssGenerator.ts so the editor
|
|
195
|
+
* widget stays in sync with what the browser will render):
|
|
196
|
+
* 1. `breakpoint === 'base'` → variable.value
|
|
197
|
+
* 2. explicit per-variable override → variable.scales[breakpoint]
|
|
198
|
+
* 3. globally-scaled value (ResponsiveScales multiplier for variable.type)
|
|
199
|
+
* 4. fallback → variable.value
|
|
200
|
+
*/
|
|
201
|
+
export function resolveVariableValueAtBreakpoint(
|
|
202
|
+
variable: { value: string; type: string; scales?: Record<string, string> },
|
|
203
|
+
breakpoint: string,
|
|
204
|
+
responsiveScales?: ResponsiveScales | null
|
|
205
|
+
): string {
|
|
206
|
+
if (breakpoint === 'base') return variable.value;
|
|
207
|
+
|
|
208
|
+
const override = variable.scales?.[breakpoint];
|
|
209
|
+
if (override !== undefined) return override;
|
|
210
|
+
|
|
211
|
+
if (responsiveScales?.enabled && variable.type !== 'none') {
|
|
212
|
+
const scale = getScaleMultiplier(
|
|
213
|
+
responsiveScales,
|
|
214
|
+
variable.type as CSSPropertyType,
|
|
215
|
+
breakpoint
|
|
216
|
+
);
|
|
217
|
+
if (scale !== null) {
|
|
218
|
+
const baseRef = responsiveScales.baseReference || 16;
|
|
219
|
+
const scaled = scalePropertyValue(variable.value, baseRef, scale);
|
|
220
|
+
if (scaled !== null) return scaled;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return variable.value;
|
|
225
|
+
}
|
|
226
|
+
|
|
181
227
|
/**
|
|
182
228
|
* Default responsive scales configuration
|
|
183
229
|
*/
|
|
@@ -200,4 +246,12 @@ export const DEFAULT_RESPONSIVE_SCALES: ResponsiveScales = {
|
|
|
200
246
|
tablet: 0.65,
|
|
201
247
|
mobile: 0.4,
|
|
202
248
|
},
|
|
249
|
+
borderRadius: {
|
|
250
|
+
tablet: 0.85,
|
|
251
|
+
mobile: 0.7,
|
|
252
|
+
},
|
|
253
|
+
size: {
|
|
254
|
+
tablet: 0.9,
|
|
255
|
+
mobile: 0.75,
|
|
256
|
+
},
|
|
203
257
|
};
|