meno-core 1.0.45 → 1.0.47
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 +211 -124
- package/dist/bin/cli.js +2 -2
- package/dist/build-static.js +7 -7
- package/dist/chunks/{chunk-NZTSJS5C.js → chunk-2QK6U5UK.js} +3 -2
- package/dist/chunks/{chunk-NZTSJS5C.js.map → chunk-2QK6U5UK.js.map} +2 -2
- package/dist/chunks/{chunk-TVH3TC2T.js → chunk-47UNLQUU.js} +6 -6
- package/dist/chunks/{chunk-F7MA62WG.js → chunk-BCLGRZ3U.js} +5 -5
- package/dist/chunks/{chunk-F7MA62WG.js.map → chunk-BCLGRZ3U.js.map} +2 -2
- package/dist/chunks/{chunk-5ZASE4IG.js → chunk-FED5MME6.js} +234 -11
- package/dist/chunks/{chunk-5ZASE4IG.js.map → chunk-FED5MME6.js.map} +3 -3
- package/dist/chunks/{chunk-BZQKEJQY.js → chunk-FGUZOYJX.js} +49 -30
- package/dist/chunks/chunk-FGUZOYJX.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-OUNJ76QM.js → chunk-LJFB5EBT.js} +5 -5
- package/dist/chunks/{chunk-GYF3ABI3.js → chunk-UUA5LEWF.js} +3 -3
- package/dist/chunks/{chunk-GYF3ABI3.js.map → chunk-UUA5LEWF.js.map} +2 -2
- package/dist/chunks/{chunk-WQSG5WHC.js → chunk-ZTKHJQ2Z.js} +2 -2
- package/dist/chunks/{configService-6KTT6GRT.js → configService-DYCUEURL.js} +3 -3
- package/dist/chunks/{constants-L5IKLB6U.js → constants-GWBAD66U.js} +2 -2
- package/dist/entries/server-router.js +7 -7
- package/dist/lib/client/index.js +7 -5
- package/dist/lib/client/index.js.map +2 -2
- package/dist/lib/server/index.js +631 -208
- package/dist/lib/server/index.js.map +3 -3
- package/dist/lib/shared/index.js +7 -3
- package/dist/lib/shared/index.js.map +2 -2
- package/dist/lib/test-utils/index.js +1 -1
- package/lib/client/core/ComponentBuilder.test.ts +21 -0
- package/lib/client/core/ComponentBuilder.ts +8 -1
- package/lib/client/templateEngine.test.ts +64 -0
- package/lib/server/astro/astroEmitHelpers.ts +23 -0
- package/lib/server/astro/cmsPageEmitter.ts +46 -3
- package/lib/server/astro/componentEmitter.test.ts +59 -0
- package/lib/server/astro/componentEmitter.ts +53 -12
- package/lib/server/astro/cssCollector.ts +58 -11
- package/lib/server/astro/nodeToAstro.test.ts +397 -5
- package/lib/server/astro/nodeToAstro.ts +494 -65
- package/lib/server/astro/pageEmitter.ts +46 -3
- 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/ssr/htmlGenerator.test.ts +3 -2
- package/lib/server/ssr/htmlGenerator.ts +6 -1
- package/lib/server/ssr/imageMetadata.ts +15 -9
- package/lib/server/ssr/jsCollector.ts +2 -2
- package/lib/server/ssr/ssrRenderer.test.ts +79 -0
- package/lib/server/ssr/ssrRenderer.ts +35 -20
- package/lib/shared/constants.ts +1 -0
- package/lib/shared/cssGeneration.test.ts +109 -3
- package/lib/shared/cssGeneration.ts +98 -13
- package/lib/shared/cssNamedColors.ts +47 -0
- package/lib/shared/cssProperties.ts +2 -2
- package/lib/shared/index.ts +1 -0
- package/lib/shared/styleNodeUtils.test.ts +47 -1
- package/lib/shared/styleNodeUtils.ts +7 -7
- package/package.json +1 -1
- package/dist/chunks/chunk-BZQKEJQY.js.map +0 -7
- /package/dist/chunks/{chunk-TVH3TC2T.js.map → chunk-47UNLQUU.js.map} +0 -0
- /package/dist/chunks/{chunk-OUNJ76QM.js.map → chunk-LJFB5EBT.js.map} +0 -0
- /package/dist/chunks/{chunk-WQSG5WHC.js.map → chunk-ZTKHJQ2Z.js.map} +0 -0
- /package/dist/chunks/{configService-6KTT6GRT.js.map → configService-DYCUEURL.js.map} +0 -0
- /package/dist/chunks/{constants-L5IKLB6U.js.map → constants-GWBAD66U.js.map} +0 -0
|
@@ -119,6 +119,12 @@ interface SSRContext {
|
|
|
119
119
|
isProductionBuild?: boolean;
|
|
120
120
|
/** Collector for SSR fallback HTML of complex nodes (list, locale-list) keyed by element path */
|
|
121
121
|
ssrFallbackCollector?: Map<string, string>;
|
|
122
|
+
/**
|
|
123
|
+
* Collector mapping the raw slice of a RAW_HTML_PREFIX string to its fully processed HTML
|
|
124
|
+
* (after image rewriting, component expansion, and link localization). Consumed by the
|
|
125
|
+
* Astro exporter so its `<Fragment set:html>` output matches SSR exactly.
|
|
126
|
+
*/
|
|
127
|
+
processedRawHtmlCollector?: Map<string, string>;
|
|
122
128
|
/** Image format: 'webp' uses plain <img>, 'avif' uses <picture> with AVIF+WebP sources */
|
|
123
129
|
imageFormat?: 'webp' | 'avif';
|
|
124
130
|
}
|
|
@@ -369,7 +375,7 @@ export async function buildComponentHTML(
|
|
|
369
375
|
cmsContext?: CMSContext,
|
|
370
376
|
cmsService?: CMSService,
|
|
371
377
|
isProductionBuild?: boolean
|
|
372
|
-
): Promise<{ html: string; interactiveStylesMap: Map<string, InteractiveStyles>; preloadImages: PreloadImage[]; neededCollections: Set<string>; ssrFallbackCollector: Map<string, string> }> {
|
|
378
|
+
): Promise<{ html: string; interactiveStylesMap: Map<string, InteractiveStyles>; preloadImages: PreloadImage[]; neededCollections: Set<string>; ssrFallbackCollector: Map<string, string>; processedRawHtmlCollector: Map<string, string> }> {
|
|
373
379
|
// Create map to collect interactive styles during render
|
|
374
380
|
const interactiveStylesMap = new Map<string, InteractiveStyles>();
|
|
375
381
|
// Create array to collect high-priority images for preloading
|
|
@@ -378,8 +384,10 @@ export async function buildComponentHTML(
|
|
|
378
384
|
const neededCollections = new Set<string>();
|
|
379
385
|
// Create map to collect SSR fallback HTML for complex nodes (list, locale-list)
|
|
380
386
|
const ssrFallbackCollector = new Map<string, string>();
|
|
387
|
+
// Create map to collect raw-HTML → processed-HTML for Astro exporter parity
|
|
388
|
+
const processedRawHtmlCollector = new Map<string, string>();
|
|
381
389
|
|
|
382
|
-
if (!node) return { html: '', interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector };
|
|
390
|
+
if (!node) return { html: '', interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector, processedRawHtmlCollector };
|
|
383
391
|
|
|
384
392
|
// Register components for this render
|
|
385
393
|
ssrComponentRegistry.merge(globalComponents);
|
|
@@ -411,12 +419,13 @@ export async function buildComponentHTML(
|
|
|
411
419
|
neededCollections, // Track collections that need client-side data
|
|
412
420
|
isProductionBuild,
|
|
413
421
|
ssrFallbackCollector, // Collect SSR fallback HTML for complex nodes
|
|
422
|
+
processedRawHtmlCollector, // Collect raw→processed HTML for Astro exporter
|
|
414
423
|
imageFormat: configService.getImageFormat(),
|
|
415
424
|
};
|
|
416
425
|
|
|
417
426
|
const html = await renderNode(node, ctx);
|
|
418
427
|
|
|
419
|
-
return { html, interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector };
|
|
428
|
+
return { html, interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector, processedRawHtmlCollector };
|
|
420
429
|
}
|
|
421
430
|
|
|
422
431
|
/**
|
|
@@ -803,10 +812,12 @@ async function renderNode(
|
|
|
803
812
|
}
|
|
804
813
|
// Check for raw HTML marker (from rich-text fields) - don't escape
|
|
805
814
|
if (text.startsWith(RAW_HTML_PREFIX)) {
|
|
806
|
-
|
|
815
|
+
const rawSlice = text.slice(RAW_HTML_PREFIX.length);
|
|
816
|
+
let rawHtml = rawSlice;
|
|
807
817
|
if (ctx.imageMetadataMap) rawHtml = rewriteRichTextImages(rawHtml, ctx.imageMetadataMap, ctx.imageFormat);
|
|
808
818
|
rawHtml = await expandRichTextComponents(rawHtml, ctx);
|
|
809
819
|
rawHtml = localizeRichTextLinks(rawHtml, ctx);
|
|
820
|
+
ctx.processedRawHtmlCollector?.set(rawSlice, rawHtml);
|
|
810
821
|
return rawHtml;
|
|
811
822
|
}
|
|
812
823
|
return escapeHtml(text);
|
|
@@ -823,10 +834,12 @@ async function renderNode(
|
|
|
823
834
|
}
|
|
824
835
|
// Check for raw HTML marker (from rich-text fields) - don't escape
|
|
825
836
|
if (text.startsWith(RAW_HTML_PREFIX)) {
|
|
826
|
-
|
|
837
|
+
const rawSlice = text.slice(RAW_HTML_PREFIX.length);
|
|
838
|
+
let rawHtml = rawSlice;
|
|
827
839
|
if (ctx.imageMetadataMap) rawHtml = rewriteRichTextImages(rawHtml, ctx.imageMetadataMap, ctx.imageFormat);
|
|
828
840
|
rawHtml = await expandRichTextComponents(rawHtml, ctx);
|
|
829
841
|
rawHtml = localizeRichTextLinks(rawHtml, ctx);
|
|
842
|
+
ctx.processedRawHtmlCollector?.set(rawSlice, rawHtml);
|
|
830
843
|
return rawHtml;
|
|
831
844
|
}
|
|
832
845
|
return escapeHtml(text);
|
|
@@ -1230,15 +1243,15 @@ async function renderComponent(
|
|
|
1230
1243
|
|
|
1231
1244
|
// Merge instance style overrides, className, and attributes
|
|
1232
1245
|
// processedStructure is typically an HTML node (the component's root element)
|
|
1233
|
-
// Handle
|
|
1246
|
+
// Handle component nodes, HTML nodes, and Link nodes (Link and HTML share top-level style/attributes)
|
|
1234
1247
|
const rootNode = processedStructure as ComponentNode & { props?: Record<string, unknown>; style?: any; attributes?: Record<string, string | number | boolean> };
|
|
1235
|
-
if (isComponentNode(rootNode) || isHtmlNode(rootNode)) {
|
|
1248
|
+
if (isComponentNode(rootNode) || isHtmlNode(rootNode) || isLinkNode(rootNode)) {
|
|
1236
1249
|
if (!rootNode.props) {
|
|
1237
1250
|
rootNode.props = {};
|
|
1238
1251
|
}
|
|
1239
1252
|
|
|
1240
|
-
// For HTML root nodes, merge styles into node.style (where the renderer reads them)
|
|
1241
|
-
if (isHtmlNode(rootNode)) {
|
|
1253
|
+
// For HTML and Link root nodes, merge styles into node.style (where the renderer reads them)
|
|
1254
|
+
if (isHtmlNode(rootNode) || isLinkNode(rootNode)) {
|
|
1242
1255
|
if (propsWithStyleAndAttrs.style) {
|
|
1243
1256
|
const existingStyle = rootNode.style;
|
|
1244
1257
|
if (existingStyle && typeof existingStyle === 'object') {
|
|
@@ -1264,8 +1277,8 @@ async function renderComponent(
|
|
|
1264
1277
|
|
|
1265
1278
|
// Merge className from component instance (includes responsive utility classes)
|
|
1266
1279
|
if (propsWithStyleAndAttrs.className) {
|
|
1267
|
-
if (isHtmlNode(rootNode)) {
|
|
1268
|
-
// For HTML root nodes, merge into attributes where the renderer reads them
|
|
1280
|
+
if (isHtmlNode(rootNode) || isLinkNode(rootNode)) {
|
|
1281
|
+
// For HTML and Link root nodes, merge into attributes where the renderer reads them
|
|
1269
1282
|
if (!rootNode.attributes) rootNode.attributes = {};
|
|
1270
1283
|
const existingClass = (rootNode.attributes.class || '') as string;
|
|
1271
1284
|
rootNode.attributes.class = existingClass
|
|
@@ -1280,11 +1293,11 @@ async function renderComponent(
|
|
|
1280
1293
|
}
|
|
1281
1294
|
|
|
1282
1295
|
// Merge attributes into props
|
|
1283
|
-
Object.assign(rootNode.props, nodeAttributes);
|
|
1296
|
+
Object.assign(rootNode.props as Record<string, unknown>, nodeAttributes);
|
|
1284
1297
|
|
|
1285
|
-
// Forward nodeAttributes to rootNode.attributes for HTML root nodes
|
|
1298
|
+
// Forward nodeAttributes to rootNode.attributes for HTML/Link root nodes
|
|
1286
1299
|
// so attributes from outer components (e.g. data-component) reach the final HTML element
|
|
1287
|
-
if (isHtmlNode(rootNode) && Object.keys(nodeAttributes).length > 0) {
|
|
1300
|
+
if ((isHtmlNode(rootNode) || isLinkNode(rootNode)) && Object.keys(nodeAttributes).length > 0) {
|
|
1288
1301
|
if (!rootNode.attributes) {
|
|
1289
1302
|
rootNode.attributes = {};
|
|
1290
1303
|
}
|
|
@@ -1428,7 +1441,7 @@ async function renderHtmlElement(
|
|
|
1428
1441
|
if (voidElements.includes(tag.toLowerCase())) {
|
|
1429
1442
|
// Special handling for img tags - inject srcset and render as <picture> when AVIF available
|
|
1430
1443
|
if (tag.toLowerCase() === 'img') {
|
|
1431
|
-
return renderImageElement(propsWithStyleAndAttrs, classAttr, attrs, ctx);
|
|
1444
|
+
return renderImageElement(propsWithStyleAndAttrs, classAttr, styleAttr, attrs, ctx);
|
|
1432
1445
|
}
|
|
1433
1446
|
|
|
1434
1447
|
return `<${tag}${classAttr}${styleAttr}${attrs} />`;
|
|
@@ -1443,6 +1456,7 @@ async function renderHtmlElement(
|
|
|
1443
1456
|
function renderImageElement(
|
|
1444
1457
|
propsWithStyleAndAttrs: Record<string, unknown>,
|
|
1445
1458
|
classAttr: string,
|
|
1459
|
+
styleAttr: string,
|
|
1446
1460
|
attrs: string,
|
|
1447
1461
|
ctx: SSRContext
|
|
1448
1462
|
): string {
|
|
@@ -1525,7 +1539,7 @@ function renderImageElement(
|
|
|
1525
1539
|
? ` class="${escapeHtml(imgClasses.join(' '))}"`
|
|
1526
1540
|
: '';
|
|
1527
1541
|
|
|
1528
|
-
return `<picture${pictureClassAttr}>` +
|
|
1542
|
+
return `<picture${pictureClassAttr}${styleAttr}>` +
|
|
1529
1543
|
`<source type="image/avif" srcset="${escapeHtml(metadata.avifSrcset)}" sizes="${escapeHtml(sizesAttr)}" />` +
|
|
1530
1544
|
`<source type="image/webp" srcset="${escapeHtml(metadata.srcset)}" sizes="${escapeHtml(sizesAttr)}" />` +
|
|
1531
1545
|
`<img${imgClassAttr}${imgAttrs}${attrs} />` +
|
|
@@ -1538,7 +1552,7 @@ function renderImageElement(
|
|
|
1538
1552
|
imgAttrs += ` sizes="${escapeHtml(sizesAttr)}"`;
|
|
1539
1553
|
}
|
|
1540
1554
|
|
|
1541
|
-
return `<img${classAttr}${imgAttrs}${attrs} />`;
|
|
1555
|
+
return `<img${classAttr}${styleAttr}${imgAttrs}${attrs} />`;
|
|
1542
1556
|
}
|
|
1543
1557
|
|
|
1544
1558
|
/**
|
|
@@ -1699,7 +1713,7 @@ export async function renderPageSSR(
|
|
|
1699
1713
|
cmsContext?: CMSContext,
|
|
1700
1714
|
cmsService?: CMSService,
|
|
1701
1715
|
isProductionBuild?: boolean
|
|
1702
|
-
): Promise<{ html: string; meta: string; title: string; javascript: string; componentCSS?: string; locale: string; interactiveStylesMap: Map<string, InteractiveStyles>; preloadImages: PreloadImage[]; neededCollections: Set<string>; ssrFallbackCollector: Map<string, string> }> {
|
|
1716
|
+
): Promise<{ html: string; meta: string; title: string; javascript: string; componentCSS?: string; locale: string; interactiveStylesMap: Map<string, InteractiveStyles>; preloadImages: PreloadImage[]; neededCollections: Set<string>; ssrFallbackCollector: Map<string, string>; processedRawHtmlCollector: Map<string, string> }> {
|
|
1703
1717
|
// Extract page content
|
|
1704
1718
|
const rootNode = pageData?.root || undefined;
|
|
1705
1719
|
if (!rootNode) {
|
|
@@ -1735,9 +1749,9 @@ export async function renderPageSSR(
|
|
|
1735
1749
|
|
|
1736
1750
|
// Render the component tree to HTML with i18n and CMS support
|
|
1737
1751
|
// Also collect interactive styles, preload images, and needed collections during render
|
|
1738
|
-
const { html: contentHTML, interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector } = rootNode
|
|
1752
|
+
const { html: contentHTML, interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector, processedRawHtmlCollector } = rootNode
|
|
1739
1753
|
? await buildComponentHTML(rootNode, globalComponents, pageComponents, effectiveLocale, config, slugMappings, pagePath, cmsContext, cmsService, isProductionBuild)
|
|
1740
|
-
: { html: '', interactiveStylesMap: new Map<string, InteractiveStyles>(), preloadImages: [], neededCollections: new Set<string>(), ssrFallbackCollector: new Map<string, string>() };
|
|
1754
|
+
: { html: '', interactiveStylesMap: new Map<string, InteractiveStyles>(), preloadImages: [], neededCollections: new Set<string>(), ssrFallbackCollector: new Map<string, string>(), processedRawHtmlCollector: new Map<string, string>() };
|
|
1741
1755
|
|
|
1742
1756
|
// Collect JavaScript and CSS from all components
|
|
1743
1757
|
const javascript = await collectComponentJavaScript(globalComponents, pageComponents);
|
|
@@ -1769,5 +1783,6 @@ export async function renderPageSSR(
|
|
|
1769
1783
|
preloadImages,
|
|
1770
1784
|
neededCollections,
|
|
1771
1785
|
ssrFallbackCollector,
|
|
1786
|
+
processedRawHtmlCollector,
|
|
1772
1787
|
};
|
|
1773
1788
|
}
|
package/lib/shared/constants.ts
CHANGED
|
@@ -139,6 +139,7 @@ export const IFRAME_MESSAGE_TYPES = {
|
|
|
139
139
|
DUPLICATE_ELEMENT: 'DUPLICATE_ELEMENT',
|
|
140
140
|
MAKE_COMPONENT: 'MAKE_COMPONENT',
|
|
141
141
|
SWAP_TO_COMPONENT: 'SWAP_TO_COMPONENT',
|
|
142
|
+
SMART_CONNECT: 'SMART_CONNECT',
|
|
142
143
|
} as const;
|
|
143
144
|
|
|
144
145
|
// Component node type constants
|
|
@@ -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';
|
|
@@ -395,7 +396,11 @@ export function generateRuleForClass(className: string): string | null {
|
|
|
395
396
|
// unless it's a hex color (#...) or RGB value
|
|
396
397
|
if ((prefix === 'bgc' || prefix === 'bg' || prefix === 'c' || prefix === 'bc')) {
|
|
397
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
|
|
398
400
|
if (!value.startsWith('#') && !value.includes('rgb') && !value.includes('px')) {
|
|
401
|
+
if (isCssNamedColor(value)) {
|
|
402
|
+
return `${cssProp}: ${value};`;
|
|
403
|
+
}
|
|
399
404
|
return `${cssProp}: var(--${value});`;
|
|
400
405
|
}
|
|
401
406
|
}
|
|
@@ -849,12 +854,14 @@ export function generateInteractiveCSS(
|
|
|
849
854
|
elementClass: string,
|
|
850
855
|
interactiveStyles: InteractiveStyles,
|
|
851
856
|
breakpoints: BreakpointConfig = DEFAULT_BREAKPOINTS,
|
|
852
|
-
remConfig?: RemConversionConfig
|
|
857
|
+
remConfig?: RemConversionConfig,
|
|
858
|
+
responsiveScales?: ResponsiveScales
|
|
853
859
|
): string {
|
|
854
860
|
const css: string[] = [];
|
|
855
861
|
|
|
856
862
|
// Extract breakpoint values for CSS media queries
|
|
857
863
|
const breakpointValues = getBreakpointValues(breakpoints);
|
|
864
|
+
const scalingEnabled = responsiveScales?.enabled === true;
|
|
858
865
|
|
|
859
866
|
for (const rule of interactiveStyles) {
|
|
860
867
|
const { prefix, postfix, style } = rule;
|
|
@@ -862,6 +869,11 @@ export function generateInteractiveCSS(
|
|
|
862
869
|
// Build full selector: {prefix}.element{postfix}
|
|
863
870
|
const fullSelector = `${prefix || ''}.${elementClass}${postfix || ''}`;
|
|
864
871
|
|
|
872
|
+
// Breakpoint styles (sorted by value descending)
|
|
873
|
+
const sortedBreakpoints = Object.entries(breakpointValues).sort(
|
|
874
|
+
([, a], [, b]) => b - a
|
|
875
|
+
);
|
|
876
|
+
|
|
865
877
|
if (isResponsiveStyle(style)) {
|
|
866
878
|
// Generate responsive rules
|
|
867
879
|
const responsive = style as ResponsiveStyleObject;
|
|
@@ -874,15 +886,30 @@ export function generateInteractiveCSS(
|
|
|
874
886
|
}
|
|
875
887
|
}
|
|
876
888
|
|
|
877
|
-
// Breakpoint styles (sorted by value descending)
|
|
878
|
-
const sortedBreakpoints = Object.entries(breakpointValues).sort(
|
|
879
|
-
([, a], [, b]) => b - a
|
|
880
|
-
);
|
|
881
|
-
|
|
882
889
|
for (const [breakpointName, breakpointValue] of sortedBreakpoints) {
|
|
883
|
-
const
|
|
884
|
-
|
|
885
|
-
|
|
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);
|
|
886
913
|
if (properties) {
|
|
887
914
|
css.push(
|
|
888
915
|
`@media (max-width: ${breakpointValue}px) { ${fullSelector} { ${properties}; } }`
|
|
@@ -898,6 +925,24 @@ export function generateInteractiveCSS(
|
|
|
898
925
|
if (properties) {
|
|
899
926
|
css.push(`${fullSelector} { ${properties}; }`);
|
|
900
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
|
+
}
|
|
901
946
|
}
|
|
902
947
|
}
|
|
903
948
|
}
|
|
@@ -905,6 +950,45 @@ export function generateInteractiveCSS(
|
|
|
905
950
|
return css.join('\n');
|
|
906
951
|
}
|
|
907
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
|
+
|
|
908
992
|
/**
|
|
909
993
|
* Collect and generate CSS for all interactive styles in a page/component
|
|
910
994
|
*
|
|
@@ -915,12 +999,13 @@ export function generateInteractiveCSS(
|
|
|
915
999
|
export function generateAllInteractiveCSS(
|
|
916
1000
|
interactiveStylesMap: Map<string, InteractiveStyles>,
|
|
917
1001
|
breakpoints: BreakpointConfig = DEFAULT_BREAKPOINTS,
|
|
918
|
-
remConfig?: RemConversionConfig
|
|
1002
|
+
remConfig?: RemConversionConfig,
|
|
1003
|
+
responsiveScales?: ResponsiveScales
|
|
919
1004
|
): string {
|
|
920
1005
|
const cssBlocks: string[] = [];
|
|
921
1006
|
|
|
922
1007
|
for (const [elementClass, styles] of interactiveStylesMap) {
|
|
923
|
-
const css = generateInteractiveCSS(elementClass, styles, breakpoints, remConfig);
|
|
1008
|
+
const css = generateInteractiveCSS(elementClass, styles, breakpoints, remConfig, responsiveScales);
|
|
924
1009
|
if (css) {
|
|
925
1010
|
cssBlocks.push(css);
|
|
926
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
|
+
}
|
|
@@ -495,7 +495,7 @@ function matchesAbbreviation(property: string, input: string): boolean {
|
|
|
495
495
|
* Supports both startsWith matching and camelCase abbreviation matching
|
|
496
496
|
*/
|
|
497
497
|
export function filterCSSProperties(input: string): string[] {
|
|
498
|
-
const normalizedInput = input.trim();
|
|
498
|
+
const normalizedInput = (input ?? '').trim();
|
|
499
499
|
if (!normalizedInput) {
|
|
500
500
|
return CSS_PROPERTIES.slice(0, 15); // Show first 15 by default
|
|
501
501
|
}
|
|
@@ -544,7 +544,7 @@ export function filterPropertyValues(propertyName: string, input: string): strin
|
|
|
544
544
|
return []; // No predefined values for this property
|
|
545
545
|
}
|
|
546
546
|
|
|
547
|
-
const normalizedInput = input.trim().toLowerCase();
|
|
547
|
+
const normalizedInput = (input ?? '').trim().toLowerCase();
|
|
548
548
|
if (!normalizedInput) {
|
|
549
549
|
return values; // Return all values if no input
|
|
550
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';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, test, expect } from 'bun:test';
|
|
2
2
|
import { applyStylesToNode, mergeNodeStyles, extractStylesFromNode } from './styleNodeUtils';
|
|
3
|
-
import type { HtmlNode, ComponentInstanceNode } from './types';
|
|
3
|
+
import type { HtmlNode, ComponentInstanceNode, LinkNode } from './types';
|
|
4
4
|
import { NODE_TYPE } from './constants';
|
|
5
5
|
|
|
6
6
|
describe('styleNodeUtils', () => {
|
|
@@ -37,6 +37,13 @@ describe('styleNodeUtils', () => {
|
|
|
37
37
|
const result = applyStylesToNode(node as any, styles);
|
|
38
38
|
expect(result.style).toEqual(styles);
|
|
39
39
|
});
|
|
40
|
+
|
|
41
|
+
test('should apply styles to link node at top level', () => {
|
|
42
|
+
const node: LinkNode = { type: NODE_TYPE.LINK, href: '/foo', children: [] };
|
|
43
|
+
const styles = { backgroundColor: '#252525', color: '#ffffff' };
|
|
44
|
+
const result = applyStylesToNode(node, styles);
|
|
45
|
+
expect(result.style).toEqual(styles);
|
|
46
|
+
});
|
|
40
47
|
});
|
|
41
48
|
|
|
42
49
|
describe('mergeNodeStyles', () => {
|
|
@@ -87,6 +94,34 @@ describe('styleNodeUtils', () => {
|
|
|
87
94
|
const result = mergeNodeStyles(node, instanceStyles);
|
|
88
95
|
expect((result as ComponentInstanceNode).props?.style).toEqual(instanceStyles);
|
|
89
96
|
});
|
|
97
|
+
|
|
98
|
+
test('should merge styles for link node at top level', () => {
|
|
99
|
+
const node: LinkNode = {
|
|
100
|
+
type: NODE_TYPE.LINK,
|
|
101
|
+
href: '/foo',
|
|
102
|
+
children: [],
|
|
103
|
+
style: { color: '#000000', backgroundColor: '#ffffff' }
|
|
104
|
+
};
|
|
105
|
+
const instanceStyles = { backgroundColor: '#252525', color: '#ffffff' };
|
|
106
|
+
const result = mergeNodeStyles(node, instanceStyles);
|
|
107
|
+
expect(result.style).toEqual({ backgroundColor: '#252525', color: '#ffffff' });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('should deep-merge responsive styles for link node', () => {
|
|
111
|
+
const node: LinkNode = {
|
|
112
|
+
type: NODE_TYPE.LINK,
|
|
113
|
+
href: '/foo',
|
|
114
|
+
children: [],
|
|
115
|
+
style: { base: { color: '#000', padding: '8px' }, tablet: { fontSize: '14px' }, mobile: {} }
|
|
116
|
+
};
|
|
117
|
+
const instanceStyles = { base: { backgroundColor: '#252525', color: '#fff' }, tablet: {}, mobile: {} };
|
|
118
|
+
const result = mergeNodeStyles(node, instanceStyles);
|
|
119
|
+
expect(result.style).toEqual({
|
|
120
|
+
base: { color: '#fff', padding: '8px', backgroundColor: '#252525' },
|
|
121
|
+
tablet: { fontSize: '14px' },
|
|
122
|
+
mobile: {},
|
|
123
|
+
});
|
|
124
|
+
});
|
|
90
125
|
});
|
|
91
126
|
|
|
92
127
|
describe('extractStylesFromNode', () => {
|
|
@@ -128,5 +163,16 @@ describe('styleNodeUtils', () => {
|
|
|
128
163
|
const styles = extractStylesFromNode(node as any);
|
|
129
164
|
expect(styles).toEqual({ width: '100%' });
|
|
130
165
|
});
|
|
166
|
+
|
|
167
|
+
test('should extract styles from link node', () => {
|
|
168
|
+
const node: LinkNode = {
|
|
169
|
+
type: NODE_TYPE.LINK,
|
|
170
|
+
href: '/foo',
|
|
171
|
+
children: [],
|
|
172
|
+
style: { backgroundColor: '#252525' }
|
|
173
|
+
};
|
|
174
|
+
const styles = extractStylesFromNode(node);
|
|
175
|
+
expect(styles).toEqual({ backgroundColor: '#252525' });
|
|
176
|
+
});
|
|
131
177
|
});
|
|
132
178
|
});
|