meno-core 1.0.48 → 1.0.50
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 +6 -2
- package/dist/build-static.js +7 -7
- package/dist/chunks/{chunk-D5E3OKSL.js → chunk-56EUSC6D.js} +5 -5
- package/dist/chunks/{chunk-3FHJUHAS.js → chunk-7NIC4I3V.js} +300 -43
- package/dist/chunks/chunk-7NIC4I3V.js.map +7 -0
- package/dist/chunks/{chunk-B2RTLDXY.js → chunk-AZQYF6KE.js} +132 -1
- package/dist/chunks/chunk-AZQYF6KE.js.map +7 -0
- package/dist/chunks/{chunk-TPQ7APVQ.js → chunk-CVLFID6V.js} +473 -73
- package/dist/chunks/chunk-CVLFID6V.js.map +7 -0
- package/dist/chunks/{chunk-NP76N4HQ.js → chunk-EDQSMAMP.js} +13 -2
- package/dist/chunks/{chunk-NP76N4HQ.js.map → chunk-EDQSMAMP.js.map} +2 -2
- package/dist/chunks/{chunk-RQSTH2BS.js → chunk-H4JSCDNW.js} +2 -2
- package/dist/chunks/{chunk-EK4KESLU.js → chunk-J23ZX5AP.js} +8 -2
- package/dist/chunks/{chunk-EK4KESLU.js.map → chunk-J23ZX5AP.js.map} +2 -2
- package/dist/chunks/{chunk-UUA5LEWF.js → chunk-LPVETICS.js} +156 -8
- package/dist/chunks/chunk-LPVETICS.js.map +7 -0
- package/dist/chunks/{chunk-BJRKEPMP.js → chunk-PQ2HRXDR.js} +5 -2
- package/dist/chunks/chunk-PQ2HRXDR.js.map +7 -0
- package/dist/chunks/{chunk-NKUV77SR.js → chunk-YWJJD5D6.js} +133 -37
- package/dist/chunks/chunk-YWJJD5D6.js.map +7 -0
- package/dist/chunks/{configService-IGJEC3MC.js → configService-VOY2MY2K.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 +92 -32
- package/dist/lib/client/index.js.map +3 -3
- package/dist/lib/server/index.js +14 -12
- package/dist/lib/server/index.js.map +2 -2
- package/dist/lib/shared/index.js +46 -10
- package/dist/lib/shared/index.js.map +3 -3
- package/entries/server-router.tsx +6 -2
- package/lib/client/core/ComponentBuilder.test.ts +34 -0
- package/lib/client/core/ComponentBuilder.ts +33 -4
- package/lib/client/core/builders/embedBuilder.ts +28 -7
- package/lib/client/core/builders/linkNodeBuilder.ts +28 -7
- package/lib/client/core/builders/localeListBuilder.ts +30 -11
- package/lib/client/styles/StyleInjector.ts +3 -2
- package/lib/client/templateEngine.ts +24 -0
- 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/fileWatcher.test.ts +134 -0
- package/lib/server/fileWatcher.ts +100 -32
- package/lib/server/jsonLoader.ts +1 -0
- package/lib/server/providers/fileSystemCMSProvider.test.ts +163 -0
- package/lib/server/providers/fileSystemCMSProvider.ts +240 -19
- 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 +6 -0
- package/lib/server/services/fileWatcherService.ts +17 -0
- package/lib/server/ssr/attributeBuilder.ts +41 -0
- package/lib/server/ssr/htmlGenerator.test.ts +113 -0
- package/lib/server/ssr/htmlGenerator.ts +62 -7
- package/lib/server/ssr/liveReloadIntegration.test.ts +209 -0
- package/lib/server/ssr/ssrRenderer.test.ts +564 -0
- package/lib/server/ssr/ssrRenderer.ts +228 -49
- package/lib/server/webflow/buildWebflow.ts +1 -1
- package/lib/server/websocketManager.test.ts +61 -6
- package/lib/server/websocketManager.ts +25 -1
- package/lib/shared/cssGeneration.test.ts +267 -1
- package/lib/shared/cssGeneration.ts +240 -18
- package/lib/shared/cssProperties.test.ts +275 -1
- package/lib/shared/cssProperties.ts +223 -7
- package/lib/shared/interfaces/contentProvider.ts +39 -6
- package/lib/shared/pathSecurity.ts +16 -0
- 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/api.ts +10 -1
- package/lib/shared/types/cms.ts +46 -12
- package/lib/shared/types/index.ts +1 -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.test.ts +93 -0
- package/lib/shared/validation/schemas.ts +71 -16
- package/lib/shared/validation/validators.ts +26 -1
- package/package.json +1 -1
- package/dist/chunks/chunk-3FHJUHAS.js.map +0 -7
- package/dist/chunks/chunk-B2RTLDXY.js.map +0 -7
- package/dist/chunks/chunk-BJRKEPMP.js.map +0 -7
- package/dist/chunks/chunk-NKUV77SR.js.map +0 -7
- package/dist/chunks/chunk-TPQ7APVQ.js.map +0 -7
- package/dist/chunks/chunk-UUA5LEWF.js.map +0 -7
- /package/dist/chunks/{chunk-D5E3OKSL.js.map → chunk-56EUSC6D.js.map} +0 -0
- /package/dist/chunks/{chunk-RQSTH2BS.js.map → chunk-H4JSCDNW.js.map} +0 -0
- /package/dist/chunks/{configService-IGJEC3MC.js.map → configService-VOY2MY2K.js.map} +0 -0
|
@@ -8,6 +8,7 @@ import type { ReactElement } from "react";
|
|
|
8
8
|
import type { EmbedNode, StyleObject, ResponsiveStyleObject, InteractiveStyles } from "../../../shared/types";
|
|
9
9
|
import { extractAttributesFromNode } from "../../../shared/attributeNodeUtils";
|
|
10
10
|
import { responsiveStylesToClasses } from "../../../shared/utilityClassMapper";
|
|
11
|
+
import { getCachedResponsiveScalesConfig } from "../../responsiveStyleResolver";
|
|
11
12
|
import { pathToString } from "../../../shared/pathArrayUtils";
|
|
12
13
|
import { generateElementClassName, type ElementClassContext } from "../../../shared/elementClassName";
|
|
13
14
|
import { InteractiveStylesRegistry } from "../../InteractiveStylesRegistry";
|
|
@@ -126,7 +127,13 @@ export function buildEmbed(
|
|
|
126
127
|
i18nResolver
|
|
127
128
|
) as StyleObject | ResponsiveStyleObject;
|
|
128
129
|
}
|
|
129
|
-
const
|
|
130
|
+
const scales = getCachedResponsiveScalesConfig();
|
|
131
|
+
const fluidActive =
|
|
132
|
+
scales?.enabled === true && scales?.mode === 'fluid';
|
|
133
|
+
const utilityClasses = responsiveStylesToClasses(
|
|
134
|
+
processedStyle as StyleObject | ResponsiveStyleObject,
|
|
135
|
+
{ fluidActive, responsiveScales: scales ?? undefined }
|
|
136
|
+
);
|
|
130
137
|
UtilityClassCollector.collect(utilityClasses);
|
|
131
138
|
classNames.push(...utilityClasses);
|
|
132
139
|
}
|
|
@@ -174,7 +181,13 @@ export function buildEmbed(
|
|
|
174
181
|
const previewClasses: string[] = [];
|
|
175
182
|
for (const rule of nodeInteractiveStyles) {
|
|
176
183
|
if (rule.previewProp && ctx.componentResolvedProps[rule.previewProp] === true) {
|
|
177
|
-
const
|
|
184
|
+
const previewScales = getCachedResponsiveScalesConfig();
|
|
185
|
+
const previewFluidActive =
|
|
186
|
+
previewScales?.enabled === true && previewScales?.mode === 'fluid';
|
|
187
|
+
const styleClasses = responsiveStylesToClasses(
|
|
188
|
+
rule.style as StyleObject | ResponsiveStyleObject,
|
|
189
|
+
{ fluidActive: previewFluidActive, responsiveScales: previewScales ?? undefined }
|
|
190
|
+
);
|
|
178
191
|
UtilityClassCollector.collect(styleClasses);
|
|
179
192
|
previewClasses.push(...styleClasses);
|
|
180
193
|
}
|
|
@@ -186,11 +199,19 @@ export function buildEmbed(
|
|
|
186
199
|
}
|
|
187
200
|
}
|
|
188
201
|
|
|
189
|
-
// Add extracted attributes className if present
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
202
|
+
// Add extracted attributes class/className if present
|
|
203
|
+
// JSON uses "class"; React uses "className". Without merging, the raw "class"
|
|
204
|
+
// would be spread onto the React element below and override the className that
|
|
205
|
+
// holds the utility classes derived from node.style.
|
|
206
|
+
if ('class' in extractedAttributes || 'className' in extractedAttributes) {
|
|
207
|
+
const attrClass = ((extractedAttributes as Record<string, unknown>).class
|
|
208
|
+
?? (extractedAttributes as Record<string, unknown>).className
|
|
209
|
+
?? '') as string;
|
|
210
|
+
if (attrClass) {
|
|
211
|
+
classNames.push(...attrClass.split(/\s+/));
|
|
212
|
+
}
|
|
213
|
+
delete (extractedAttributes as Record<string, unknown>).class;
|
|
214
|
+
delete (extractedAttributes as Record<string, unknown>).className;
|
|
194
215
|
}
|
|
195
216
|
|
|
196
217
|
// Set final className
|
|
@@ -8,6 +8,7 @@ import type { ReactElement } from "react";
|
|
|
8
8
|
import type { LinkNode, StyleObject, ResponsiveStyleObject, InteractiveStyles } from "../../../shared/types";
|
|
9
9
|
import { extractAttributesFromNode } from "../../../shared/attributeNodeUtils";
|
|
10
10
|
import { responsiveStylesToClasses } from "../../../shared/utilityClassMapper";
|
|
11
|
+
import { getCachedResponsiveScalesConfig } from "../../responsiveStyleResolver";
|
|
11
12
|
import { pathToString } from "../../../shared/pathArrayUtils";
|
|
12
13
|
import { generateElementClassName, type ElementClassContext } from "../../../shared/elementClassName";
|
|
13
14
|
import { InteractiveStylesRegistry } from "../../InteractiveStylesRegistry";
|
|
@@ -101,7 +102,13 @@ export function buildLinkNode(
|
|
|
101
102
|
i18nResolver
|
|
102
103
|
) as StyleObject | ResponsiveStyleObject;
|
|
103
104
|
}
|
|
104
|
-
const
|
|
105
|
+
const scales = getCachedResponsiveScalesConfig();
|
|
106
|
+
const fluidActive =
|
|
107
|
+
scales?.enabled === true && scales?.mode === 'fluid';
|
|
108
|
+
const utilityClasses = responsiveStylesToClasses(
|
|
109
|
+
processedStyle as StyleObject | ResponsiveStyleObject,
|
|
110
|
+
{ fluidActive, responsiveScales: scales ?? undefined }
|
|
111
|
+
);
|
|
105
112
|
UtilityClassCollector.collect(utilityClasses);
|
|
106
113
|
classNames.push(...utilityClasses);
|
|
107
114
|
}
|
|
@@ -149,7 +156,13 @@ export function buildLinkNode(
|
|
|
149
156
|
const previewClasses: string[] = [];
|
|
150
157
|
for (const rule of nodeInteractiveStyles) {
|
|
151
158
|
if (rule.previewProp && ctx.componentResolvedProps[rule.previewProp] === true) {
|
|
152
|
-
const
|
|
159
|
+
const previewScales = getCachedResponsiveScalesConfig();
|
|
160
|
+
const previewFluidActive =
|
|
161
|
+
previewScales?.enabled === true && previewScales?.mode === 'fluid';
|
|
162
|
+
const styleClasses = responsiveStylesToClasses(
|
|
163
|
+
rule.style as StyleObject | ResponsiveStyleObject,
|
|
164
|
+
{ fluidActive: previewFluidActive, responsiveScales: previewScales ?? undefined }
|
|
165
|
+
);
|
|
153
166
|
UtilityClassCollector.collect(styleClasses);
|
|
154
167
|
previewClasses.push(...styleClasses);
|
|
155
168
|
}
|
|
@@ -161,11 +174,19 @@ export function buildLinkNode(
|
|
|
161
174
|
}
|
|
162
175
|
}
|
|
163
176
|
|
|
164
|
-
// Add extracted attributes className if present
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
177
|
+
// Add extracted attributes class/className if present
|
|
178
|
+
// JSON uses "class"; React uses "className". Without merging, the raw "class"
|
|
179
|
+
// would be spread onto the React element below and override the className that
|
|
180
|
+
// holds the utility classes derived from node.style.
|
|
181
|
+
if ('class' in extractedAttributes || 'className' in extractedAttributes) {
|
|
182
|
+
const attrClass = ((extractedAttributes as Record<string, unknown>).class
|
|
183
|
+
?? (extractedAttributes as Record<string, unknown>).className
|
|
184
|
+
?? '') as string;
|
|
185
|
+
if (attrClass) {
|
|
186
|
+
classNames.push(...attrClass.split(/\s+/));
|
|
187
|
+
}
|
|
188
|
+
delete (extractedAttributes as Record<string, unknown>).class;
|
|
189
|
+
delete (extractedAttributes as Record<string, unknown>).className;
|
|
169
190
|
}
|
|
170
191
|
|
|
171
192
|
// Add is-current class when link href matches current page path
|
|
@@ -8,6 +8,7 @@ import type { ReactElement } from "react";
|
|
|
8
8
|
import type { LocaleListNode, StyleObject, ResponsiveStyleObject, InteractiveStyles } from "../../../shared/types";
|
|
9
9
|
import { extractAttributesFromNode } from "../../../shared/attributeNodeUtils";
|
|
10
10
|
import { responsiveStylesToClasses } from "../../../shared/utilityClassMapper";
|
|
11
|
+
import { getCachedResponsiveScalesConfig } from "../../responsiveStyleResolver";
|
|
11
12
|
import { pathToString } from "../../../shared/pathArrayUtils";
|
|
12
13
|
import { generateElementClassName, type ElementClassContext } from "../../../shared/elementClassName";
|
|
13
14
|
import { InteractiveStylesRegistry } from "../../InteractiveStylesRegistry";
|
|
@@ -73,9 +74,16 @@ export function buildLocaleList(
|
|
|
73
74
|
// Start building className
|
|
74
75
|
let classNames: string[] = [];
|
|
75
76
|
|
|
77
|
+
// Resolve fluid mode once for all conversions in this builder
|
|
78
|
+
const scales = getCachedResponsiveScalesConfig();
|
|
79
|
+
const fluidActive = scales?.enabled === true && scales?.mode === 'fluid';
|
|
80
|
+
|
|
76
81
|
// Convert container styles to utility classes
|
|
77
82
|
if (node.style) {
|
|
78
|
-
const utilityClasses = responsiveStylesToClasses(
|
|
83
|
+
const utilityClasses = responsiveStylesToClasses(
|
|
84
|
+
node.style as StyleObject | ResponsiveStyleObject,
|
|
85
|
+
{ fluidActive, responsiveScales: scales ?? undefined }
|
|
86
|
+
);
|
|
79
87
|
UtilityClassCollector.collect(utilityClasses);
|
|
80
88
|
classNames.push(...utilityClasses);
|
|
81
89
|
}
|
|
@@ -123,7 +131,10 @@ export function buildLocaleList(
|
|
|
123
131
|
const previewClasses: string[] = [];
|
|
124
132
|
for (const rule of nodeInteractiveStyles) {
|
|
125
133
|
if (rule.previewProp && ctx.componentResolvedProps[rule.previewProp] === true) {
|
|
126
|
-
const styleClasses = responsiveStylesToClasses(
|
|
134
|
+
const styleClasses = responsiveStylesToClasses(
|
|
135
|
+
rule.style as StyleObject | ResponsiveStyleObject,
|
|
136
|
+
{ fluidActive, responsiveScales: scales ?? undefined }
|
|
137
|
+
);
|
|
127
138
|
UtilityClassCollector.collect(styleClasses);
|
|
128
139
|
previewClasses.push(...styleClasses);
|
|
129
140
|
}
|
|
@@ -135,11 +146,19 @@ export function buildLocaleList(
|
|
|
135
146
|
}
|
|
136
147
|
}
|
|
137
148
|
|
|
138
|
-
// Add extracted attributes className if present
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
149
|
+
// Add extracted attributes class/className if present
|
|
150
|
+
// JSON uses "class"; React uses "className". Without merging, the raw "class"
|
|
151
|
+
// would be spread onto the React element below and override the className that
|
|
152
|
+
// holds the utility classes derived from node.style.
|
|
153
|
+
if ('class' in extractedAttributes || 'className' in extractedAttributes) {
|
|
154
|
+
const attrClass = ((extractedAttributes as Record<string, unknown>).class
|
|
155
|
+
?? (extractedAttributes as Record<string, unknown>).className
|
|
156
|
+
?? '') as string;
|
|
157
|
+
if (attrClass) {
|
|
158
|
+
classNames.push(...attrClass.split(/\s+/));
|
|
159
|
+
}
|
|
160
|
+
delete (extractedAttributes as Record<string, unknown>).class;
|
|
161
|
+
delete (extractedAttributes as Record<string, unknown>).className;
|
|
143
162
|
}
|
|
144
163
|
|
|
145
164
|
// Set final className
|
|
@@ -169,10 +188,10 @@ export function buildLocaleList(
|
|
|
169
188
|
const currentLocaleCode = locale || i18nConfig?.defaultLocale || 'en';
|
|
170
189
|
|
|
171
190
|
// Convert item styles, active item styles, separator styles, and flag styles to utility classes
|
|
172
|
-
const itemClasses = node.itemStyle ? responsiveStylesToClasses(node.itemStyle as StyleObject | ResponsiveStyleObject) : [];
|
|
173
|
-
const activeItemClasses = node.activeItemStyle ? responsiveStylesToClasses(node.activeItemStyle as StyleObject | ResponsiveStyleObject) : [];
|
|
174
|
-
const separatorClasses = node.separatorStyle ? responsiveStylesToClasses(node.separatorStyle as StyleObject | ResponsiveStyleObject) : [];
|
|
175
|
-
const flagClasses = node.flagStyle ? responsiveStylesToClasses(node.flagStyle as StyleObject | ResponsiveStyleObject) : [];
|
|
191
|
+
const itemClasses = node.itemStyle ? responsiveStylesToClasses(node.itemStyle as StyleObject | ResponsiveStyleObject, { fluidActive, responsiveScales: scales ?? undefined }) : [];
|
|
192
|
+
const activeItemClasses = node.activeItemStyle ? responsiveStylesToClasses(node.activeItemStyle as StyleObject | ResponsiveStyleObject, { fluidActive, responsiveScales: scales ?? undefined }) : [];
|
|
193
|
+
const separatorClasses = node.separatorStyle ? responsiveStylesToClasses(node.separatorStyle as StyleObject | ResponsiveStyleObject, { fluidActive, responsiveScales: scales ?? undefined }) : [];
|
|
194
|
+
const flagClasses = node.flagStyle ? responsiveStylesToClasses(node.flagStyle as StyleObject | ResponsiveStyleObject, { fluidActive, responsiveScales: scales ?? undefined }) : [];
|
|
176
195
|
UtilityClassCollector.collect(itemClasses);
|
|
177
196
|
UtilityClassCollector.collect(activeItemClasses);
|
|
178
197
|
UtilityClassCollector.collect(separatorClasses);
|
|
@@ -12,7 +12,7 @@ import type { ComponentRegistry } from '../componentRegistry';
|
|
|
12
12
|
import type { ElementRegistry } from '../elementRegistry';
|
|
13
13
|
import { hasTemplates, processCodeTemplates } from '../templateEngine';
|
|
14
14
|
import { generateAllInteractiveCSS } from '../../shared/cssGeneration';
|
|
15
|
-
import { getCachedBreakpointConfig, getCachedRemConversionConfig } from '../responsiveStyleResolver';
|
|
15
|
+
import { getCachedBreakpointConfig, getCachedRemConversionConfig, getCachedResponsiveScalesConfig } from '../responsiveStyleResolver';
|
|
16
16
|
import { DEFAULT_BREAKPOINTS } from '../../shared/breakpoints';
|
|
17
17
|
import { InteractiveStylesRegistry } from '../InteractiveStylesRegistry';
|
|
18
18
|
import { UtilityClassCollector } from './UtilityClassCollector';
|
|
@@ -153,7 +153,8 @@ export class StyleInjector {
|
|
|
153
153
|
|
|
154
154
|
const breakpointConfig = getCachedBreakpointConfig() || DEFAULT_BREAKPOINTS;
|
|
155
155
|
const remConversionConfig = getCachedRemConversionConfig() || undefined;
|
|
156
|
-
const
|
|
156
|
+
const responsiveScalesConfig = getCachedResponsiveScalesConfig() || undefined;
|
|
157
|
+
const interactiveCSS = generateAllInteractiveCSS(interactiveStylesMap, breakpointConfig, remConversionConfig, responsiveScalesConfig);
|
|
157
158
|
if (!interactiveCSS) return;
|
|
158
159
|
|
|
159
160
|
if (document.head) {
|
|
@@ -797,6 +797,30 @@ export function processStructure(
|
|
|
797
797
|
// Special handling for interactiveStyles - preserve as-is without mangling
|
|
798
798
|
// Interactive styles contain StyleMapping objects that shouldn't be converted to nodes
|
|
799
799
|
(processed as unknown as Record<string, unknown>).interactiveStyles = value;
|
|
800
|
+
} else if (key === 'source' && preservedType === NODE_TYPE.LIST) {
|
|
801
|
+
// Pre-resolve a bare prop-name source against the current component's
|
|
802
|
+
// props. Without this, a list with `sourceType: "prop"` placed inside
|
|
803
|
+
// another component's slot would fail to resolve at SSR time, because
|
|
804
|
+
// by then `ctx.componentResolvedProps` belongs to the slot host (not
|
|
805
|
+
// the component whose prop this references). The {{template}} form is
|
|
806
|
+
// already handled by the default-recursion branch below.
|
|
807
|
+
const structureRec = structure as Record<string, unknown>;
|
|
808
|
+
const sourceType = (structureRec.sourceType as string | undefined) ?? 'prop';
|
|
809
|
+
if (
|
|
810
|
+
sourceType === 'prop' &&
|
|
811
|
+
typeof value === 'string' &&
|
|
812
|
+
value !== '' &&
|
|
813
|
+
!hasTemplates(value) &&
|
|
814
|
+
context.props &&
|
|
815
|
+
Array.isArray((context.props as Record<string, unknown>)[value])
|
|
816
|
+
) {
|
|
817
|
+
(processed as unknown as Record<string, unknown>).source = (context.props as Record<string, unknown>)[value];
|
|
818
|
+
} else {
|
|
819
|
+
const processedValue = processStructure(value as ComponentNode | ComponentNode[] | string | number | null | undefined, context, viewportWidth, instanceChildren, preserveResponsiveStyles, depth + 1);
|
|
820
|
+
if (processedValue !== null && processedValue !== undefined) {
|
|
821
|
+
(processed as unknown as Record<string, unknown>).source = processedValue;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
800
824
|
} else if (key !== 'type' && key !== 'children' && key !== 'style' && key !== 'props') {
|
|
801
825
|
const processedValue = processStructure(value as ComponentNode | ComponentNode[] | string | number | null | undefined, context, viewportWidth, instanceChildren, preserveResponsiveStyles, depth + 1);
|
|
802
826
|
// Only assign if it's a valid value
|
package/lib/client/theme.ts
CHANGED
|
@@ -77,9 +77,9 @@ const lightThemeColors: ThemeColors = {
|
|
|
77
77
|
backgroundTertiary: '#eaeef2',
|
|
78
78
|
border: '#d0d7de',
|
|
79
79
|
borderSecondary: '#d1d5da',
|
|
80
|
-
text: '#
|
|
80
|
+
text: '#1a1a1a',
|
|
81
81
|
textSecondary: '#586069',
|
|
82
|
-
textMuted: '#
|
|
82
|
+
textMuted: '#525a63',
|
|
83
83
|
codeString: '#032f62',
|
|
84
84
|
codeNumber: '#005cc5',
|
|
85
85
|
codeKey: '#005cc5',
|
|
@@ -125,9 +125,9 @@ const darkThemeColors: ThemeColors = {
|
|
|
125
125
|
backgroundTertiary: '#252525',
|
|
126
126
|
border: '#333333',
|
|
127
127
|
borderSecondary: '#444444',
|
|
128
|
-
text: '#
|
|
128
|
+
text: '#ebebeb',
|
|
129
129
|
textSecondary: '#cccccc',
|
|
130
|
-
textMuted: '#
|
|
130
|
+
textMuted: '#b0b0b0',
|
|
131
131
|
codeString: '#ffffff',
|
|
132
132
|
codeNumber: '#b5cea8',
|
|
133
133
|
codeKey: '#9cdcfe',
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, test, expect } from 'bun:test';
|
|
2
|
-
import { generateColorVariablesCSS, generateThemeColorVariablesCSS } from './cssGenerator';
|
|
2
|
+
import { generateColorVariablesCSS, generateThemeColorVariablesCSS, generateVariablesCSS } from './cssGenerator';
|
|
3
3
|
import type { ColorVariables, ThemeConfig } from '../shared/types/colors';
|
|
4
|
+
import type { ResponsiveScales } from '../shared/responsiveScaling';
|
|
5
|
+
import { DEFAULT_BREAKPOINTS } from '../shared/breakpoints';
|
|
4
6
|
|
|
5
7
|
describe('cssGenerator', () => {
|
|
6
8
|
describe('generateColorVariablesCSS', () => {
|
|
@@ -169,4 +171,65 @@ describe('cssGenerator', () => {
|
|
|
169
171
|
expect(css).toContain('\n\n');
|
|
170
172
|
});
|
|
171
173
|
});
|
|
174
|
+
|
|
175
|
+
describe('generateVariablesCSS — fluid mode', () => {
|
|
176
|
+
const fluidScales: ResponsiveScales = {
|
|
177
|
+
enabled: true,
|
|
178
|
+
mode: 'fluid',
|
|
179
|
+
baseReference: 16,
|
|
180
|
+
fluidRange: { min: 320, max: 1440 },
|
|
181
|
+
siteMargin: { min: 16, max: 32 },
|
|
182
|
+
fontSize: { tablet: 0.88, mobile: 0.75 },
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
test('emits --site-margin clamp on :root when fluid mode is enabled (no user vars)', () => {
|
|
186
|
+
const css = generateVariablesCSS(
|
|
187
|
+
{ variables: [] },
|
|
188
|
+
DEFAULT_BREAKPOINTS,
|
|
189
|
+
fluidScales
|
|
190
|
+
);
|
|
191
|
+
expect(css).toContain('--site-margin: clamp(16px,');
|
|
192
|
+
expect(css).toContain(', 32px)');
|
|
193
|
+
expect(css).not.toContain('@media');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
test('emits user variables with clamp() in fluid mode (no @media)', () => {
|
|
198
|
+
const css = generateVariablesCSS(
|
|
199
|
+
{
|
|
200
|
+
variables: [
|
|
201
|
+
{ name: 'h1-fs', cssVar: '--h1-fs', value: '32px', type: 'fontSize' },
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
DEFAULT_BREAKPOINTS,
|
|
205
|
+
fluidScales
|
|
206
|
+
);
|
|
207
|
+
// mobile fontSize scale=0.75 → MIN=28
|
|
208
|
+
expect(css).toContain('--h1-fs: clamp(28px,');
|
|
209
|
+
expect(css).toContain(', 32px)');
|
|
210
|
+
expect(css).not.toContain('@media');
|
|
211
|
+
// and the site-margin var still emitted
|
|
212
|
+
expect(css).toContain('--site-margin: clamp(16px,');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('breakpoints mode (default) keeps @media blocks for vars and DOES NOT emit --site-margin', () => {
|
|
216
|
+
const css = generateVariablesCSS(
|
|
217
|
+
{
|
|
218
|
+
variables: [
|
|
219
|
+
{ name: 'h1-fs', cssVar: '--h1-fs', value: '32px', type: 'fontSize' },
|
|
220
|
+
],
|
|
221
|
+
},
|
|
222
|
+
DEFAULT_BREAKPOINTS,
|
|
223
|
+
{ ...fluidScales, mode: 'breakpoints' }
|
|
224
|
+
);
|
|
225
|
+
expect(css).not.toContain('--site-margin');
|
|
226
|
+
expect(css).toContain('@media (max-width: 1024px)');
|
|
227
|
+
expect(css).not.toContain('clamp(');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('returns empty string when no user vars AND fluid disabled', () => {
|
|
231
|
+
const css = generateVariablesCSS({ variables: [] }, DEFAULT_BREAKPOINTS);
|
|
232
|
+
expect(css).toBe('');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
172
235
|
});
|
|
@@ -9,7 +9,14 @@ import { resolvePaletteColor } from '../shared/types/colors';
|
|
|
9
9
|
import type { VariablesConfig, CSSVariable } from '../shared/types/variables';
|
|
10
10
|
import type { BreakpointConfig } from '../shared/breakpoints';
|
|
11
11
|
import type { ResponsiveScales, BreakpointScales } from '../shared/responsiveScaling';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
scalePropertyValue,
|
|
14
|
+
buildSiteMarginClamp,
|
|
15
|
+
buildFluidPropertyValue,
|
|
16
|
+
getSmallestBreakpointName,
|
|
17
|
+
DEFAULT_FLUID_RANGE,
|
|
18
|
+
DEFAULT_SITE_MARGIN,
|
|
19
|
+
} from '../shared/responsiveScaling';
|
|
13
20
|
|
|
14
21
|
/**
|
|
15
22
|
* Generate CSS color variable declarations from color variables
|
|
@@ -75,18 +82,50 @@ export function generateVariablesCSS(
|
|
|
75
82
|
breakpoints?: BreakpointConfig,
|
|
76
83
|
responsiveScales?: ResponsiveScales
|
|
77
84
|
): string {
|
|
78
|
-
|
|
79
|
-
|
|
85
|
+
const cssBlocks: string[] = [];
|
|
86
|
+
const fluidActive =
|
|
87
|
+
responsiveScales?.enabled === true && responsiveScales?.mode === 'fluid';
|
|
88
|
+
|
|
89
|
+
// Build base :root block: user-defined variables + (in fluid mode) --site-margin.
|
|
90
|
+
// In fluid mode user variables get clamp() values directly on :root rather
|
|
91
|
+
// than @media overrides — so we run them through buildFluidPropertyValue here.
|
|
92
|
+
const fluidRange = responsiveScales?.fluidRange ?? DEFAULT_FLUID_RANGE;
|
|
93
|
+
const fluidBaseRef = responsiveScales?.baseReference || 16;
|
|
94
|
+
const smallestBp = fluidActive ? getSmallestBreakpointName(breakpoints) : null;
|
|
95
|
+
|
|
96
|
+
const baseVars: string[] = (config.variables ?? []).map(v => {
|
|
97
|
+
if (fluidActive && smallestBp && v.type !== 'none') {
|
|
98
|
+
const categoryScales = responsiveScales?.[v.type] as BreakpointScales | undefined;
|
|
99
|
+
const scale = categoryScales?.[smallestBp];
|
|
100
|
+
if (scale != null && scale !== 1) {
|
|
101
|
+
const fluid = buildFluidPropertyValue(
|
|
102
|
+
v.value,
|
|
103
|
+
scale,
|
|
104
|
+
fluidRange.min,
|
|
105
|
+
fluidRange.max,
|
|
106
|
+
fluidBaseRef
|
|
107
|
+
);
|
|
108
|
+
if (fluid) return ` ${v.cssVar}: ${fluid};`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return ` ${v.cssVar}: ${v.value};`;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (fluidActive) {
|
|
115
|
+
const siteMargin = responsiveScales?.siteMargin ?? DEFAULT_SITE_MARGIN;
|
|
116
|
+
baseVars.push(` --site-margin: ${buildSiteMarginClamp(siteMargin, fluidRange)};`);
|
|
80
117
|
}
|
|
81
118
|
|
|
82
|
-
|
|
119
|
+
if (baseVars.length === 0) {
|
|
120
|
+
return '';
|
|
121
|
+
}
|
|
83
122
|
|
|
84
|
-
// Base :root block with all variables
|
|
85
|
-
const baseVars = config.variables.map(v => ` ${v.cssVar}: ${v.value};`);
|
|
86
123
|
cssBlocks.push(`:root {\n${baseVars.join('\n')}\n}`);
|
|
87
124
|
|
|
88
|
-
// Generate @media blocks for responsive scaling
|
|
89
|
-
|
|
125
|
+
// Generate @media blocks for responsive scaling — only in breakpoints mode.
|
|
126
|
+
// In fluid mode all scaling is encoded in clamp() on the base :root block.
|
|
127
|
+
const userVariables = config.variables ?? [];
|
|
128
|
+
if (breakpoints && responsiveScales?.enabled && !fluidActive && userVariables.length > 0) {
|
|
90
129
|
const baseRef = responsiveScales.baseReference || 16;
|
|
91
130
|
|
|
92
131
|
// Sort breakpoints by value descending (largest first)
|
|
@@ -96,7 +135,7 @@ export function generateVariablesCSS(
|
|
|
96
135
|
for (const [bpName, bpEntry] of sortedBreakpoints) {
|
|
97
136
|
const scaledVars: string[] = [];
|
|
98
137
|
|
|
99
|
-
for (const variable of
|
|
138
|
+
for (const variable of userVariables) {
|
|
100
139
|
// Per-variable override takes priority — stored string is the CSS value
|
|
101
140
|
if (variable.scales && variable.scales[bpName]) {
|
|
102
141
|
const overrideValue = variable.scales[bpName];
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, test, expect, afterEach, beforeEach } from 'bun:test';
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { FileWatcher } from './fileWatcher';
|
|
6
|
+
import { setProjectRoot, getProjectRoot } from './projectContext';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Regression test for the "blank project" case: when `templates/` (or `cms/`)
|
|
10
|
+
* doesn't exist when the dev server starts, the watcher used to silently
|
|
11
|
+
* early-out and never fire onPageChange / onCMSChange even after the
|
|
12
|
+
* directory was created by an external tool. The deferred-attach path in
|
|
13
|
+
* fileWatcher.ts fixes this — the test asserts that callbacks now fire after
|
|
14
|
+
* a target directory is created post-startup.
|
|
15
|
+
*
|
|
16
|
+
* fs.watch on different OSes coalesces events differently, so the test waits
|
|
17
|
+
* generously and only asserts that the callback fires at least once.
|
|
18
|
+
*/
|
|
19
|
+
async function settle(ms = 300): Promise<void> {
|
|
20
|
+
await new Promise(r => setTimeout(r, ms));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('FileWatcher deferred attach', () => {
|
|
24
|
+
let projectRoot: string;
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
projectRoot = join(tmpdir(), `meno-fw-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
28
|
+
mkdirSync(projectRoot, { recursive: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
try { rmSync(projectRoot, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('watchTemplates fires onPageChange after templates/ is created post-startup', async () => {
|
|
36
|
+
const seen: string[] = [];
|
|
37
|
+
const watcher = new FileWatcher({
|
|
38
|
+
onPageChange: async (p) => { seen.push(p); },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// templates/ does NOT exist yet — pre-fix this returned silently.
|
|
42
|
+
const templatesDir = join(projectRoot, 'templates');
|
|
43
|
+
watcher.watchTemplates(templatesDir);
|
|
44
|
+
|
|
45
|
+
// Simulate the external write that triggers the bug: create templates/
|
|
46
|
+
// and drop a JSON file into it.
|
|
47
|
+
await settle(50);
|
|
48
|
+
mkdirSync(templatesDir);
|
|
49
|
+
await settle(100);
|
|
50
|
+
writeFileSync(join(templatesDir, 'blog.json'), '{}');
|
|
51
|
+
await settle(300);
|
|
52
|
+
|
|
53
|
+
watcher.stopAll();
|
|
54
|
+
expect(seen.length).toBeGreaterThanOrEqual(1);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('watchCMS fires onCMSChange after cms/ is created post-startup', async () => {
|
|
58
|
+
const seen: string[] = [];
|
|
59
|
+
const watcher = new FileWatcher({
|
|
60
|
+
onCMSChange: async (collection) => { seen.push(collection); },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const cmsDir = join(projectRoot, 'cms');
|
|
64
|
+
watcher.watchCMS(cmsDir);
|
|
65
|
+
|
|
66
|
+
await settle(50);
|
|
67
|
+
mkdirSync(join(cmsDir, 'blog'), { recursive: true });
|
|
68
|
+
await settle(100);
|
|
69
|
+
writeFileSync(join(cmsDir, 'blog', 'post.json'), '{}');
|
|
70
|
+
await settle(300);
|
|
71
|
+
|
|
72
|
+
watcher.stopAll();
|
|
73
|
+
expect(seen.length).toBeGreaterThanOrEqual(1);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('returns silently when parent dir also does not exist', () => {
|
|
77
|
+
const watcher = new FileWatcher({});
|
|
78
|
+
expect(() => watcher.watchTemplates(join(projectRoot, 'nonexistent-parent', 'templates'))).not.toThrow();
|
|
79
|
+
watcher.stopAll();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('FileWatcher project.config.json', () => {
|
|
84
|
+
let projectRoot: string;
|
|
85
|
+
let prevRoot: string;
|
|
86
|
+
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
prevRoot = getProjectRoot();
|
|
89
|
+
projectRoot = join(tmpdir(), `meno-fw-cfg-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
90
|
+
mkdirSync(projectRoot, { recursive: true });
|
|
91
|
+
// watchProjectConfig reads getProjectRoot() — point it at the temp dir.
|
|
92
|
+
setProjectRoot(projectRoot);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
afterEach(() => {
|
|
96
|
+
setProjectRoot(prevRoot);
|
|
97
|
+
try { rmSync(projectRoot, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('fires onProjectConfigChange when project.config.json is written', async () => {
|
|
101
|
+
let fired = 0;
|
|
102
|
+
const watcher = new FileWatcher({
|
|
103
|
+
onProjectConfigChange: async () => { fired++; },
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
watcher.watchProjectConfig();
|
|
107
|
+
await settle(50);
|
|
108
|
+
|
|
109
|
+
// Simulate AI / external tool editing project.config.json to add a locale.
|
|
110
|
+
writeFileSync(join(projectRoot, 'project.config.json'), JSON.stringify({
|
|
111
|
+
i18n: { defaultLocale: 'en', locales: [{ code: 'en' }, { code: 'fr' }] }
|
|
112
|
+
}));
|
|
113
|
+
await settle(300);
|
|
114
|
+
|
|
115
|
+
watcher.stopAll();
|
|
116
|
+
expect(fired).toBeGreaterThanOrEqual(1);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('ignores writes to unrelated files at the project root', async () => {
|
|
120
|
+
let fired = 0;
|
|
121
|
+
const watcher = new FileWatcher({
|
|
122
|
+
onProjectConfigChange: async () => { fired++; },
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
watcher.watchProjectConfig();
|
|
126
|
+
await settle(50);
|
|
127
|
+
|
|
128
|
+
writeFileSync(join(projectRoot, 'README.md'), 'hello');
|
|
129
|
+
await settle(200);
|
|
130
|
+
|
|
131
|
+
watcher.stopAll();
|
|
132
|
+
expect(fired).toBe(0);
|
|
133
|
+
});
|
|
134
|
+
});
|