meno-core 1.0.47 → 1.0.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build-astro.ts +2 -2
- package/dist/build-static.js +7 -7
- package/dist/chunks/{chunk-UUA5LEWF.js → chunk-6IVUG7FY.js} +138 -7
- package/dist/chunks/chunk-6IVUG7FY.js.map +7 -0
- package/dist/chunks/{chunk-XSWR3QLI.js → chunk-AZQYF6KE.js} +261 -130
- package/dist/chunks/chunk-AZQYF6KE.js.map +7 -0
- package/dist/chunks/{chunk-47UNLQUU.js → chunk-CHD5UCFF.js} +57 -12
- package/dist/chunks/chunk-CHD5UCFF.js.map +7 -0
- package/dist/chunks/{chunk-FGUZOYJX.js → chunk-EQYDSPBB.js} +435 -131
- package/dist/chunks/chunk-EQYDSPBB.js.map +7 -0
- package/dist/chunks/{chunk-IF3RATBY.js → chunk-H4JSCDNW.js} +2 -2
- package/dist/chunks/{chunk-KITQJYZV.js → chunk-J23ZX5AP.js} +40 -4
- package/dist/chunks/chunk-J23ZX5AP.js.map +7 -0
- package/dist/chunks/{chunk-LJFB5EBT.js → chunk-JER5NQVM.js} +5 -5
- package/dist/chunks/{chunk-ZTKHJQ2Z.js → chunk-KPU2XHOS.js} +5 -2
- package/dist/chunks/{chunk-ZTKHJQ2Z.js.map → chunk-KPU2XHOS.js.map} +2 -2
- package/dist/chunks/{chunk-BCLGRZ3U.js → chunk-LKAGAQ3M.js} +2 -2
- package/dist/chunks/{chunk-FED5MME6.js → chunk-S2CX6HFM.js} +262 -26
- package/dist/chunks/chunk-S2CX6HFM.js.map +7 -0
- package/dist/chunks/{configService-DYCUEURL.js → configService-CCA6AIDI.js} +3 -3
- package/dist/entries/server-router.js +9 -9
- package/dist/entries/server-router.js.map +2 -2
- package/dist/lib/client/index.js +64 -20
- package/dist/lib/client/index.js.map +3 -3
- package/dist/lib/server/index.js +1737 -296
- package/dist/lib/server/index.js.map +4 -4
- package/dist/lib/shared/index.js +50 -10
- package/dist/lib/shared/index.js.map +3 -3
- package/entries/server-router.tsx +6 -2
- package/lib/client/core/ComponentBuilder.test.ts +17 -0
- package/lib/client/core/ComponentBuilder.ts +25 -1
- package/lib/client/core/builders/embedBuilder.ts +15 -2
- package/lib/client/core/builders/linkNodeBuilder.ts +15 -2
- package/lib/client/core/builders/localeListBuilder.ts +17 -6
- package/lib/client/styles/StyleInjector.ts +3 -2
- package/lib/client/theme.ts +4 -4
- package/lib/server/cssGenerator.test.ts +64 -1
- package/lib/server/cssGenerator.ts +48 -9
- package/lib/server/index.ts +1 -1
- package/lib/server/jsonLoader.test.ts +0 -17
- package/lib/server/jsonLoader.ts +0 -81
- package/lib/server/providers/fileSystemCMSProvider.test.ts +163 -0
- package/lib/server/providers/fileSystemCMSProvider.ts +200 -11
- package/lib/server/routes/api/variables.ts +4 -2
- package/lib/server/routes/index.ts +1 -1
- package/lib/server/routes/pages.ts +23 -1
- package/lib/server/services/cmsService.test.ts +246 -0
- package/lib/server/services/cmsService.ts +122 -5
- package/lib/server/services/configService.ts +5 -0
- package/lib/server/ssr/attributeBuilder.ts +41 -0
- package/lib/server/ssr/htmlGenerator.test.ts +114 -2
- package/lib/server/ssr/htmlGenerator.ts +53 -6
- package/lib/server/ssr/liveReloadIntegration.test.ts +209 -0
- package/lib/server/ssr/ssrRenderer.test.ts +362 -1
- package/lib/server/ssr/ssrRenderer.ts +216 -72
- package/lib/server/utils/jsonLineMapper.test.ts +53 -1
- package/lib/server/utils/jsonLineMapper.ts +43 -3
- package/lib/server/webflow/buildWebflow.ts +343 -123
- package/lib/server/webflow/index.ts +1 -0
- package/lib/server/webflow/nodeToWebflow.test.ts +3170 -0
- package/lib/server/webflow/nodeToWebflow.ts +2141 -129
- package/lib/server/webflow/styleMapper.test.ts +389 -0
- package/lib/server/webflow/styleMapper.ts +517 -63
- package/lib/server/webflow/templateWrapper.ts +49 -0
- package/lib/server/webflow/types.ts +218 -18
- package/lib/shared/cssGeneration.test.ts +267 -1
- package/lib/shared/cssGeneration.ts +240 -18
- package/lib/shared/cssProperties.test.ts +247 -1
- package/lib/shared/cssProperties.ts +196 -6
- package/lib/shared/elementClassName.test.ts +15 -0
- package/lib/shared/elementClassName.ts +7 -3
- package/lib/shared/interfaces/contentProvider.ts +39 -6
- package/lib/shared/pathSecurity.ts +16 -0
- package/lib/shared/registry/nodeTypes/ListNodeType.ts +1 -1
- package/lib/shared/responsiveScaling.test.ts +143 -0
- package/lib/shared/responsiveScaling.ts +253 -2
- package/lib/shared/themeDefaults.test.ts +3 -3
- package/lib/shared/themeDefaults.ts +3 -3
- package/lib/shared/types/cms.ts +28 -3
- package/lib/shared/types/index.ts +2 -0
- package/lib/shared/types/variables.ts +37 -0
- package/lib/shared/utilityClassConfig.ts +3 -0
- package/lib/shared/utilityClassMapper.test.ts +123 -0
- package/lib/shared/utilityClassMapper.ts +179 -8
- package/lib/shared/validation/schemas.ts +15 -1
- package/lib/shared/validation/validators.ts +26 -1
- package/package.json +1 -1
- package/dist/chunks/chunk-47UNLQUU.js.map +0 -7
- package/dist/chunks/chunk-FED5MME6.js.map +0 -7
- package/dist/chunks/chunk-FGUZOYJX.js.map +0 -7
- package/dist/chunks/chunk-KITQJYZV.js.map +0 -7
- package/dist/chunks/chunk-UUA5LEWF.js.map +0 -7
- package/dist/chunks/chunk-XSWR3QLI.js.map +0 -7
- /package/dist/chunks/{chunk-IF3RATBY.js.map → chunk-H4JSCDNW.js.map} +0 -0
- /package/dist/chunks/{chunk-LJFB5EBT.js.map → chunk-JER5NQVM.js.map} +0 -0
- /package/dist/chunks/{chunk-BCLGRZ3U.js.map → chunk-LKAGAQ3M.js.map} +0 -0
- /package/dist/chunks/{configService-DYCUEURL.js.map → configService-CCA6AIDI.js.map} +0 -0
|
@@ -11,18 +11,47 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export type BreakpointScales = Record<string, number>;
|
|
13
13
|
|
|
14
|
+
export type ResponsiveMode = 'breakpoints' | 'fluid';
|
|
15
|
+
|
|
16
|
+
export interface FluidRange {
|
|
17
|
+
/** Lower viewport bound in pixels (e.g. 320) */
|
|
18
|
+
min: number;
|
|
19
|
+
/** Upper viewport bound in pixels (e.g. 1440) */
|
|
20
|
+
max: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SiteMarginConfig {
|
|
24
|
+
/** Lower bound of the side margin at smallest viewport (px). */
|
|
25
|
+
min: number;
|
|
26
|
+
/** Upper bound of the side margin at largest viewport (px). */
|
|
27
|
+
max: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
14
30
|
export interface ResponsiveScales {
|
|
15
31
|
enabled: boolean;
|
|
32
|
+
/** Scaling strategy. Missing → 'breakpoints' (back-compat with old configs). */
|
|
33
|
+
mode?: ResponsiveMode;
|
|
16
34
|
baseReference: number;
|
|
35
|
+
/** Viewport bounds used when `mode === 'fluid'`. Defaults to 320 / 1440 px. */
|
|
36
|
+
fluidRange?: FluidRange;
|
|
37
|
+
/**
|
|
38
|
+
* Container side margin range, in fluid mode emitted as a CSS variable
|
|
39
|
+
* `--site-margin: clamp(min, …, max)` on `:root`. Lets users write
|
|
40
|
+
* `width: calc(100% - var(--site-margin) * 2)` for the container pattern.
|
|
41
|
+
*/
|
|
42
|
+
siteMargin?: SiteMarginConfig;
|
|
17
43
|
fontSize?: BreakpointScales;
|
|
18
44
|
padding?: BreakpointScales;
|
|
19
45
|
margin?: BreakpointScales;
|
|
20
46
|
gap?: BreakpointScales;
|
|
21
47
|
borderRadius?: BreakpointScales;
|
|
22
48
|
size?: BreakpointScales;
|
|
23
|
-
[key: string]: boolean | number | BreakpointScales | undefined;
|
|
49
|
+
[key: string]: boolean | number | string | BreakpointScales | FluidRange | SiteMarginConfig | undefined;
|
|
24
50
|
}
|
|
25
51
|
|
|
52
|
+
export const DEFAULT_FLUID_RANGE: FluidRange = { min: 320, max: 1440 };
|
|
53
|
+
export const DEFAULT_SITE_MARGIN: SiteMarginConfig = { min: 16, max: 32 };
|
|
54
|
+
|
|
26
55
|
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';
|
|
27
56
|
|
|
28
57
|
/**
|
|
@@ -44,7 +73,7 @@ function getScaleCategory(property: CSSPropertyType): keyof ResponsiveScales | n
|
|
|
44
73
|
* Extract numeric value and unit from a CSS value string
|
|
45
74
|
* e.g., "67px" -> { value: 67, unit: "px" }
|
|
46
75
|
*/
|
|
47
|
-
function parseValue(valueStr: string): { value: number; unit: string } | null {
|
|
76
|
+
export function parseValue(valueStr: string): { value: number; unit: string } | null {
|
|
48
77
|
const match = valueStr.trim().match(/^(-?[\d.]+)(px|rem|em|%|pt)$/);
|
|
49
78
|
if (!match) return null;
|
|
50
79
|
return {
|
|
@@ -53,6 +82,59 @@ function parseValue(valueStr: string): { value: number; unit: string } | null {
|
|
|
53
82
|
};
|
|
54
83
|
}
|
|
55
84
|
|
|
85
|
+
/**
|
|
86
|
+
* CSS properties that get fluid `clamp()` / breakpoint scaling. Mirrors the
|
|
87
|
+
* `AUTO_RESPONSIVE_TYPE_MAP` keys in `cssGeneration.ts`; centralized here so
|
|
88
|
+
* `utilityClassMapper.ts` can consult it without a cross-module import cycle.
|
|
89
|
+
*/
|
|
90
|
+
export const SCALABLE_CSS_PROPERTIES: ReadonlySet<string> = new Set([
|
|
91
|
+
'padding',
|
|
92
|
+
'padding-left',
|
|
93
|
+
'padding-right',
|
|
94
|
+
'padding-top',
|
|
95
|
+
'padding-bottom',
|
|
96
|
+
'paddingLeft',
|
|
97
|
+
'paddingRight',
|
|
98
|
+
'paddingTop',
|
|
99
|
+
'paddingBottom',
|
|
100
|
+
'margin',
|
|
101
|
+
'margin-left',
|
|
102
|
+
'margin-right',
|
|
103
|
+
'margin-top',
|
|
104
|
+
'margin-bottom',
|
|
105
|
+
'marginLeft',
|
|
106
|
+
'marginRight',
|
|
107
|
+
'marginTop',
|
|
108
|
+
'marginBottom',
|
|
109
|
+
'font-size',
|
|
110
|
+
'fontSize',
|
|
111
|
+
'gap',
|
|
112
|
+
'row-gap',
|
|
113
|
+
'column-gap',
|
|
114
|
+
'rowGap',
|
|
115
|
+
'columnGap',
|
|
116
|
+
'border-radius',
|
|
117
|
+
'borderRadius',
|
|
118
|
+
'border-top-left-radius',
|
|
119
|
+
'border-top-right-radius',
|
|
120
|
+
'border-bottom-left-radius',
|
|
121
|
+
'border-bottom-right-radius',
|
|
122
|
+
'borderTopLeftRadius',
|
|
123
|
+
'borderTopRightRadius',
|
|
124
|
+
'borderBottomLeftRadius',
|
|
125
|
+
'borderBottomRightRadius',
|
|
126
|
+
'width',
|
|
127
|
+
'height',
|
|
128
|
+
'max-width',
|
|
129
|
+
'max-height',
|
|
130
|
+
'min-width',
|
|
131
|
+
'min-height',
|
|
132
|
+
'maxWidth',
|
|
133
|
+
'maxHeight',
|
|
134
|
+
'minWidth',
|
|
135
|
+
'minHeight',
|
|
136
|
+
]);
|
|
137
|
+
|
|
56
138
|
/**
|
|
57
139
|
* Calculate responsive value using the formula:
|
|
58
140
|
* responsive_value = base_value + (base_value - base_reference) * (scale - 1)
|
|
@@ -224,12 +306,181 @@ export function resolveVariableValueAtBreakpoint(
|
|
|
224
306
|
return variable.value;
|
|
225
307
|
}
|
|
226
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Pick the smallest (most restrictive) breakpoint name from a config object,
|
|
311
|
+
* by `breakpoint` field. Used in fluid mode to choose which scale multiplier
|
|
312
|
+
* defines the MIN of the clamp.
|
|
313
|
+
*
|
|
314
|
+
* Returns null if there are no entries.
|
|
315
|
+
*/
|
|
316
|
+
export function getSmallestBreakpointName(
|
|
317
|
+
breakpoints: Record<string, { breakpoint: number }> | undefined | null
|
|
318
|
+
): string | null {
|
|
319
|
+
if (!breakpoints) return null;
|
|
320
|
+
const entries = Object.entries(breakpoints);
|
|
321
|
+
if (entries.length === 0) return null;
|
|
322
|
+
let smallestName = entries[0][0];
|
|
323
|
+
let smallestWidth = entries[0][1].breakpoint;
|
|
324
|
+
for (const [name, cfg] of entries) {
|
|
325
|
+
if (cfg.breakpoint < smallestWidth) {
|
|
326
|
+
smallestWidth = cfg.breakpoint;
|
|
327
|
+
smallestName = name;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return smallestName;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Build a `clamp(MIN, intercept + slope*100vw, MAX)` expression that interpolates
|
|
335
|
+
* linearly between (vpMin, MIN) and (vpMax, MAX).
|
|
336
|
+
*
|
|
337
|
+
* - `baseValue` is the desktop/MAX value in CSS units.
|
|
338
|
+
* - `unit` is the CSS unit ("px", "rem", etc.) used for both bounds.
|
|
339
|
+
* - `scale` is the multiplier applied at the small end (mobile). Reuses
|
|
340
|
+
* `calculateResponsiveValue` so the same multipliers used in 'breakpoints'
|
|
341
|
+
* mode produce the same MIN endpoint in 'fluid' mode.
|
|
342
|
+
* - `vpMin` / `vpMax` are viewport widths in pixels.
|
|
343
|
+
*
|
|
344
|
+
* If MIN === MAX (e.g. value at or below baseReference, or scale === 1),
|
|
345
|
+
* returns a plain `${baseValue}${unit}` — no clamp emitted.
|
|
346
|
+
*/
|
|
347
|
+
export function buildFluidClamp(
|
|
348
|
+
baseValue: number,
|
|
349
|
+
unit: string,
|
|
350
|
+
scale: number,
|
|
351
|
+
vpMin: number,
|
|
352
|
+
vpMax: number,
|
|
353
|
+
baseReference = 16
|
|
354
|
+
): string {
|
|
355
|
+
const max = baseValue;
|
|
356
|
+
const min = calculateResponsiveValue(baseValue, baseReference, scale);
|
|
357
|
+
|
|
358
|
+
if (min === max || vpMax === vpMin) {
|
|
359
|
+
return `${formatNumber(max)}${unit}`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Convert viewport bounds into the same unit as the value, so the
|
|
363
|
+
// intercept term stays in `unit`. For px / rem we keep px throughout vw maths
|
|
364
|
+
// but emit the intercept in the value's unit by converting (1rem = 16px).
|
|
365
|
+
const unitToPx = unit === 'rem' ? 16 : 1;
|
|
366
|
+
const minPx = min * unitToPx;
|
|
367
|
+
const maxPx = max * unitToPx;
|
|
368
|
+
const slopePx = (maxPx - minPx) / (vpMax - vpMin); // px per px-of-viewport
|
|
369
|
+
const interceptPx = minPx - slopePx * vpMin; // px
|
|
370
|
+
const interceptInUnit = interceptPx / unitToPx;
|
|
371
|
+
const slopeVw = slopePx * 100; // because 1vw = viewport_width / 100 px
|
|
372
|
+
|
|
373
|
+
return `clamp(${formatNumber(min)}${unit}, ${formatNumber(interceptInUnit)}${unit} + ${formatNumber(slopeVw)}vw, ${formatNumber(max)}${unit})`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Build a `clamp(MIN, intercept + slope*100vw, MAX)` expression where MIN is
|
|
378
|
+
* supplied directly (e.g. from an authored mobile override) rather than
|
|
379
|
+
* derived from `responsiveScales` × `baseReference`.
|
|
380
|
+
*
|
|
381
|
+
* Used when a node has both `style.base.<prop>` and `style.mobile.<prop>` in
|
|
382
|
+
* fluid mode — the mobile value is consumed as the clamp's small end so the
|
|
383
|
+
* smooth interpolation lands exactly on the authored mobile value at the
|
|
384
|
+
* narrow viewport, avoiding the jump that a separate @media override would
|
|
385
|
+
* create.
|
|
386
|
+
*
|
|
387
|
+
* Same units / vw math as `buildFluidClamp`. Returns plain `${max}${unit}`
|
|
388
|
+
* when MIN === MAX or the viewport range degenerates.
|
|
389
|
+
*/
|
|
390
|
+
export function buildFluidClampWithExplicitMin(
|
|
391
|
+
minValue: number,
|
|
392
|
+
maxValue: number,
|
|
393
|
+
unit: string,
|
|
394
|
+
vpMin: number,
|
|
395
|
+
vpMax: number
|
|
396
|
+
): string {
|
|
397
|
+
if (minValue === maxValue || vpMax === vpMin) {
|
|
398
|
+
return `${formatNumber(maxValue)}${unit}`;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const unitToPx = unit === 'rem' ? 16 : 1;
|
|
402
|
+
const minPx = minValue * unitToPx;
|
|
403
|
+
const maxPx = maxValue * unitToPx;
|
|
404
|
+
const slopePx = (maxPx - minPx) / (vpMax - vpMin);
|
|
405
|
+
const interceptPx = minPx - slopePx * vpMin;
|
|
406
|
+
const interceptInUnit = interceptPx / unitToPx;
|
|
407
|
+
const slopeVw = slopePx * 100;
|
|
408
|
+
|
|
409
|
+
return `clamp(${formatNumber(minValue)}${unit}, ${formatNumber(interceptInUnit)}${unit} + ${formatNumber(slopeVw)}vw, ${formatNumber(maxValue)}${unit})`;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Trim noisy floating-point output (e.g. 21.81818181818182 → 21.8182).
|
|
414
|
+
*/
|
|
415
|
+
function formatNumber(n: number): string {
|
|
416
|
+
if (Number.isInteger(n)) return String(n);
|
|
417
|
+
return Number(n.toFixed(4)).toString();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Build the CSS value for the `--site-margin` variable in fluid mode.
|
|
422
|
+
* Always emits a clamp() going from `siteMargin.min` to `siteMargin.max` (px),
|
|
423
|
+
* interpolated linearly across `fluidRange.min`..`fluidRange.max`.
|
|
424
|
+
*
|
|
425
|
+
* If `min === max` the function returns the plain value (no clamp).
|
|
426
|
+
*/
|
|
427
|
+
export function buildSiteMarginClamp(
|
|
428
|
+
siteMargin: SiteMarginConfig,
|
|
429
|
+
fluidRange: FluidRange
|
|
430
|
+
): string {
|
|
431
|
+
const { min, max } = siteMargin;
|
|
432
|
+
const { min: vpMin, max: vpMax } = fluidRange;
|
|
433
|
+
|
|
434
|
+
if (min === max || vpMax === vpMin) {
|
|
435
|
+
return `${formatNumber(max)}px`;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const slopePx = (max - min) / (vpMax - vpMin);
|
|
439
|
+
const interceptPx = min - slopePx * vpMin;
|
|
440
|
+
const slopeVw = slopePx * 100;
|
|
441
|
+
|
|
442
|
+
return `clamp(${formatNumber(min)}px, ${formatNumber(interceptPx)}px + ${formatNumber(slopeVw)}vw, ${formatNumber(max)}px)`;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Build a fluid clamp() value for a CSS string value (e.g. "32px", "2rem").
|
|
447
|
+
* Multi-value strings ("20px 40px") are scaled per-token, like
|
|
448
|
+
* `scalePropertyValue` does for breakpoint mode. Tokens without a unit
|
|
449
|
+
* (`0`, `auto`, `inherit`) are kept verbatim.
|
|
450
|
+
*
|
|
451
|
+
* Returns null if no token is scalable.
|
|
452
|
+
*/
|
|
453
|
+
export function buildFluidPropertyValue(
|
|
454
|
+
valueStr: string,
|
|
455
|
+
scale: number,
|
|
456
|
+
vpMin: number,
|
|
457
|
+
vpMax: number,
|
|
458
|
+
baseReference = 16
|
|
459
|
+
): string | null {
|
|
460
|
+
const parts = parseMultiValue(valueStr);
|
|
461
|
+
if (parts.length === 0) return null;
|
|
462
|
+
|
|
463
|
+
let anyScaled = false;
|
|
464
|
+
const out = parts.map(part => {
|
|
465
|
+
const parsed = parseValue(part);
|
|
466
|
+
if (!parsed) return part;
|
|
467
|
+
if (parsed.unit === '%' || parsed.unit === 'em') return part;
|
|
468
|
+
anyScaled = true;
|
|
469
|
+
return buildFluidClamp(parsed.value, parsed.unit, scale, vpMin, vpMax, baseReference);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
return anyScaled ? out.join(' ') : null;
|
|
473
|
+
}
|
|
474
|
+
|
|
227
475
|
/**
|
|
228
476
|
* Default responsive scales configuration
|
|
229
477
|
*/
|
|
230
478
|
export const DEFAULT_RESPONSIVE_SCALES: ResponsiveScales = {
|
|
231
479
|
enabled: false,
|
|
480
|
+
mode: 'breakpoints',
|
|
232
481
|
baseReference: 16,
|
|
482
|
+
fluidRange: { ...DEFAULT_FLUID_RANGE },
|
|
483
|
+
siteMargin: { ...DEFAULT_SITE_MARGIN },
|
|
233
484
|
fontSize: {
|
|
234
485
|
tablet: 0.88,
|
|
235
486
|
mobile: 0.75,
|
|
@@ -82,9 +82,9 @@ describe('themeDefaults', () => {
|
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
test('should have text colors', () => {
|
|
85
|
-
expect(DEFAULT_PROPS_PANEL_COLORS.text).toBe('#
|
|
85
|
+
expect(DEFAULT_PROPS_PANEL_COLORS.text).toBe('#ebebeb');
|
|
86
86
|
expect(DEFAULT_PROPS_PANEL_COLORS.textSecondary).toBe('#cccccc');
|
|
87
|
-
expect(DEFAULT_PROPS_PANEL_COLORS.textMuted).toBe('#
|
|
87
|
+
expect(DEFAULT_PROPS_PANEL_COLORS.textMuted).toBe('#b0b0b0');
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
test('should have code syntax colors', () => {
|
|
@@ -127,7 +127,7 @@ describe('themeDefaults', () => {
|
|
|
127
127
|
test('should have text colors', () => {
|
|
128
128
|
expect(LIGHT_PROPS_PANEL_COLORS.text).toBe('#1a1a1a');
|
|
129
129
|
expect(LIGHT_PROPS_PANEL_COLORS.textSecondary).toBe('#333333');
|
|
130
|
-
expect(LIGHT_PROPS_PANEL_COLORS.textMuted).toBe('#
|
|
130
|
+
expect(LIGHT_PROPS_PANEL_COLORS.textMuted).toBe('#525a63');
|
|
131
131
|
});
|
|
132
132
|
|
|
133
133
|
test('should have code syntax colors', () => {
|
|
@@ -85,9 +85,9 @@ export const DEFAULT_PROPS_PANEL_COLORS: PropsPanelColors = {
|
|
|
85
85
|
backgroundTertiary: '#252525',
|
|
86
86
|
border: '#333333',
|
|
87
87
|
borderSecondary: '#444444',
|
|
88
|
-
text: '#
|
|
88
|
+
text: '#ebebeb',
|
|
89
89
|
textSecondary: '#cccccc',
|
|
90
|
-
textMuted: '#
|
|
90
|
+
textMuted: '#b0b0b0',
|
|
91
91
|
codeString: '#ffffff',
|
|
92
92
|
codeNumber: '#b5cea8',
|
|
93
93
|
codeKey: '#9cdcfe',
|
|
@@ -114,7 +114,7 @@ export const LIGHT_PROPS_PANEL_COLORS: PropsPanelColors = {
|
|
|
114
114
|
borderSecondary: '#d0d0d0',
|
|
115
115
|
text: '#1a1a1a',
|
|
116
116
|
textSecondary: '#333333',
|
|
117
|
-
textMuted: '#
|
|
117
|
+
textMuted: '#525a63',
|
|
118
118
|
codeString: '#a31515',
|
|
119
119
|
codeNumber: '#098658',
|
|
120
120
|
codeKey: '#0451a5',
|
package/lib/shared/types/cms.ts
CHANGED
|
@@ -96,9 +96,14 @@ export interface CMSItem {
|
|
|
96
96
|
/** ISO timestamp when item was created */
|
|
97
97
|
_createdAt?: string;
|
|
98
98
|
/**
|
|
99
|
-
* Locale codes where this item is
|
|
100
|
-
* undefined or [] =
|
|
101
|
-
* ['fr', 'de'] = French and German are
|
|
99
|
+
* Locale codes where this item is hidden from the live site.
|
|
100
|
+
* undefined or [] = visible for all locales (backward compatible).
|
|
101
|
+
* ['fr', 'de'] = French and German are hidden, other locales visible.
|
|
102
|
+
*
|
|
103
|
+
* NOTE: this is locale-level visibility ("Hidden" in the UI), NOT the WIP
|
|
104
|
+
* draft-version concept. The draft version of an item is stored in a
|
|
105
|
+
* sibling `{filename}.draft.json` file. The legacy `_draftLocales` name
|
|
106
|
+
* is preserved for backward compatibility.
|
|
102
107
|
*/
|
|
103
108
|
_draftLocales?: string[];
|
|
104
109
|
/**
|
|
@@ -107,10 +112,30 @@ export interface CMSItem {
|
|
|
107
112
|
* Example: "/blog/my-post" for urlPattern "/blog/{{slug}}"
|
|
108
113
|
*/
|
|
109
114
|
_url?: string;
|
|
115
|
+
/**
|
|
116
|
+
* Transient flag set when an item was loaded from a `{filename}.draft.json`
|
|
117
|
+
* file (i.e. it is the draft version, not the published one). Never persisted.
|
|
118
|
+
*/
|
|
119
|
+
_isDraft?: boolean;
|
|
120
|
+
/**
|
|
121
|
+
* Transient flag set on a published item when a draft sibling exists.
|
|
122
|
+
* Used by the Studio item list to render a "Has draft" badge. Never persisted.
|
|
123
|
+
*/
|
|
124
|
+
_hasDraft?: boolean;
|
|
110
125
|
/** User-defined fields based on schema */
|
|
111
126
|
[field: string]: unknown;
|
|
112
127
|
}
|
|
113
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Bundle of both versions of a CMS item, returned by editor APIs.
|
|
131
|
+
* - `published` is omitted for draft-only items (new items not yet published).
|
|
132
|
+
* - `draft` is omitted when there are no pending changes.
|
|
133
|
+
*/
|
|
134
|
+
export interface CMSItemVersions {
|
|
135
|
+
published?: CMSItem;
|
|
136
|
+
draft?: CMSItem;
|
|
137
|
+
}
|
|
138
|
+
|
|
114
139
|
/** CMS Collection - schema + items */
|
|
115
140
|
export interface CMSCollection {
|
|
116
141
|
schema: CMSSchema;
|
|
@@ -82,6 +82,7 @@ export type {
|
|
|
82
82
|
CMSClientDataConfig,
|
|
83
83
|
CMSSchema,
|
|
84
84
|
CMSItem,
|
|
85
|
+
CMSItemVersions,
|
|
85
86
|
CMSCollection,
|
|
86
87
|
CMSRouteMatch,
|
|
87
88
|
CMSFilterOperator,
|
|
@@ -153,6 +154,7 @@ export {
|
|
|
153
154
|
VARIABLE_GROUPS,
|
|
154
155
|
VARIABLE_GROUP_VALUES,
|
|
155
156
|
VARIABLE_TYPES,
|
|
157
|
+
VARIABLE_TYPE_VALUES,
|
|
156
158
|
getGroupForProperty,
|
|
157
159
|
getGroupAbbreviation,
|
|
158
160
|
getDefaultScalingType,
|
|
@@ -19,7 +19,13 @@ export type VariableGroup =
|
|
|
19
19
|
| 'line-height' | 'letter-spacing'
|
|
20
20
|
| 'margin' | 'padding' | 'gap'
|
|
21
21
|
| 'size'
|
|
22
|
+
| 'position'
|
|
22
23
|
| 'border-radius' | 'border-width'
|
|
24
|
+
| 'outline'
|
|
25
|
+
| 'shadow'
|
|
26
|
+
| 'filter'
|
|
27
|
+
| 'duration'
|
|
28
|
+
| 'aspect-ratio'
|
|
23
29
|
| 'opacity' | 'z-index'
|
|
24
30
|
| 'text-align'
|
|
25
31
|
| 'other';
|
|
@@ -35,8 +41,14 @@ export const VARIABLE_GROUPS: { value: VariableGroup; label: string }[] = [
|
|
|
35
41
|
{ value: 'padding', label: 'Padding' },
|
|
36
42
|
{ value: 'gap', label: 'Gap' },
|
|
37
43
|
{ value: 'size', label: 'Size' },
|
|
44
|
+
{ value: 'position', label: 'Position' },
|
|
38
45
|
{ value: 'border-radius', label: 'Border Radius' },
|
|
39
46
|
{ value: 'border-width', label: 'Border Width' },
|
|
47
|
+
{ value: 'outline', label: 'Outline' },
|
|
48
|
+
{ value: 'shadow', label: 'Shadow' },
|
|
49
|
+
{ value: 'filter', label: 'Filter' },
|
|
50
|
+
{ value: 'duration', label: 'Duration' },
|
|
51
|
+
{ value: 'aspect-ratio', label: 'Aspect Ratio' },
|
|
40
52
|
{ value: 'opacity', label: 'Opacity' },
|
|
41
53
|
{ value: 'z-index', label: 'Z-Index' },
|
|
42
54
|
{ value: 'text-align', label: 'Text Align' },
|
|
@@ -56,6 +68,26 @@ const CSS_PROPERTY_TO_GROUP: Record<string, VariableGroup> = {
|
|
|
56
68
|
'text-align': 'text-align',
|
|
57
69
|
'opacity': 'opacity',
|
|
58
70
|
'z-index': 'z-index',
|
|
71
|
+
'top': 'position',
|
|
72
|
+
'right': 'position',
|
|
73
|
+
'bottom': 'position',
|
|
74
|
+
'left': 'position',
|
|
75
|
+
'inset': 'position',
|
|
76
|
+
'inset-block': 'position',
|
|
77
|
+
'inset-inline': 'position',
|
|
78
|
+
'inset-block-start': 'position',
|
|
79
|
+
'inset-block-end': 'position',
|
|
80
|
+
'inset-inline-start': 'position',
|
|
81
|
+
'inset-inline-end': 'position',
|
|
82
|
+
'box-shadow': 'shadow',
|
|
83
|
+
'text-shadow': 'shadow',
|
|
84
|
+
'filter': 'filter',
|
|
85
|
+
'backdrop-filter': 'filter',
|
|
86
|
+
'aspect-ratio': 'aspect-ratio',
|
|
87
|
+
'transition-duration': 'duration',
|
|
88
|
+
'animation-duration': 'duration',
|
|
89
|
+
'transition-delay': 'duration',
|
|
90
|
+
'animation-delay': 'duration',
|
|
59
91
|
};
|
|
60
92
|
|
|
61
93
|
/** Prefix-based mappings for CSS properties */
|
|
@@ -81,6 +113,8 @@ const CSS_PROPERTY_PREFIX_GROUPS: { prefix: string; group: VariableGroup }[] = [
|
|
|
81
113
|
{ prefix: 'border-right-width', group: 'border-width' },
|
|
82
114
|
{ prefix: 'border-bottom-width', group: 'border-width' },
|
|
83
115
|
{ prefix: 'border-left-width', group: 'border-width' },
|
|
116
|
+
{ prefix: 'outline-width', group: 'outline' },
|
|
117
|
+
{ prefix: 'outline-offset', group: 'outline' },
|
|
84
118
|
];
|
|
85
119
|
|
|
86
120
|
/**
|
|
@@ -142,6 +176,9 @@ export const VARIABLE_TYPES: { value: VariableType; label: string }[] = [
|
|
|
142
176
|
{ value: 'none', label: 'None' },
|
|
143
177
|
];
|
|
144
178
|
|
|
179
|
+
/** All valid variable scaling type values */
|
|
180
|
+
export const VARIABLE_TYPE_VALUES: VariableType[] = VARIABLE_TYPES.map(t => t.value);
|
|
181
|
+
|
|
145
182
|
/**
|
|
146
183
|
* Returns an abbreviation for a variable group by taking the first letter of each word.
|
|
147
184
|
* e.g. 'letter-spacing' -> 'ls', 'font-size' -> 'fs', 'spacing' -> 's'
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { describe, test, expect } from 'bun:test';
|
|
2
2
|
import { stylesToClasses, responsiveStylesToClasses, classToStyle, classesToStyles } from './utilityClassMapper';
|
|
3
|
+
import { getStyleValue, clearRegistry } from './styleValueRegistry';
|
|
4
|
+
import type { ResponsiveScales } from './responsiveScaling';
|
|
3
5
|
|
|
4
6
|
describe('utilityClassMapper', () => {
|
|
5
7
|
describe('stylesToClasses', () => {
|
|
@@ -93,6 +95,127 @@ describe('utilityClassMapper', () => {
|
|
|
93
95
|
expect(responsiveStylesToClasses(null)).toEqual([]);
|
|
94
96
|
expect(responsiveStylesToClasses(undefined)).toEqual([]);
|
|
95
97
|
});
|
|
98
|
+
|
|
99
|
+
describe('fluid mode — mobile-as-MIN consumption', () => {
|
|
100
|
+
const fluidScales: ResponsiveScales = {
|
|
101
|
+
enabled: true,
|
|
102
|
+
mode: 'fluid',
|
|
103
|
+
baseReference: 16,
|
|
104
|
+
fluidRange: { min: 320, max: 1700 },
|
|
105
|
+
fontSize: { tablet: 0.8, mobile: 0.5 },
|
|
106
|
+
padding: { tablet: 0.75, mobile: 0.5 },
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
test('consumes scalable mobile override into base clamp; no mob- class emitted', () => {
|
|
110
|
+
clearRegistry();
|
|
111
|
+
const classes = responsiveStylesToClasses(
|
|
112
|
+
{ base: { fontSize: '150px' }, mobile: { fontSize: '94px' } },
|
|
113
|
+
{ fluidActive: true, responsiveScales: fluidScales }
|
|
114
|
+
);
|
|
115
|
+
// base became a hashed class wrapping clamp(94px, ..., 150px)
|
|
116
|
+
const hashed = classes.find((c) => /^fs-h[0-9a-z]+$/.test(c));
|
|
117
|
+
expect(hashed).toBeDefined();
|
|
118
|
+
expect(getStyleValue(hashed!)).toMatch(/^clamp\(94px,/);
|
|
119
|
+
expect(getStyleValue(hashed!)).toContain(', 150px)');
|
|
120
|
+
// no .mob-fs-* emitted
|
|
121
|
+
expect(classes.some((c) => c.startsWith('mob-fs-'))).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('mobile-only non-scalable property (display) stays as mob- class', () => {
|
|
125
|
+
clearRegistry();
|
|
126
|
+
const classes = responsiveStylesToClasses(
|
|
127
|
+
{
|
|
128
|
+
base: { fontSize: '150px' },
|
|
129
|
+
mobile: { fontSize: '94px', display: 'none' },
|
|
130
|
+
},
|
|
131
|
+
{ fluidActive: true, responsiveScales: fluidScales }
|
|
132
|
+
);
|
|
133
|
+
// fontSize consumed into base clamp
|
|
134
|
+
const hashed = classes.find((c) => /^fs-h[0-9a-z]+$/.test(c));
|
|
135
|
+
expect(hashed).toBeDefined();
|
|
136
|
+
expect(getStyleValue(hashed!)).toMatch(/^clamp\(94px,/);
|
|
137
|
+
// display:none survives on mobile
|
|
138
|
+
expect(classes).toContain('mob-h');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('tablet override stays as .t-* class (unchanged behavior)', () => {
|
|
142
|
+
clearRegistry();
|
|
143
|
+
const classes = responsiveStylesToClasses(
|
|
144
|
+
{
|
|
145
|
+
base: { fontSize: '150px' },
|
|
146
|
+
tablet: { fontSize: '120px' },
|
|
147
|
+
mobile: { fontSize: '94px' },
|
|
148
|
+
},
|
|
149
|
+
{ fluidActive: true, responsiveScales: fluidScales }
|
|
150
|
+
);
|
|
151
|
+
expect(classes).toContain('t-fs-120px');
|
|
152
|
+
// mobile consumed
|
|
153
|
+
expect(classes.some((c) => c.startsWith('mob-fs-'))).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('no mobile override → base passes through unchanged (no consumption)', () => {
|
|
157
|
+
clearRegistry();
|
|
158
|
+
const classes = responsiveStylesToClasses(
|
|
159
|
+
{ base: { fontSize: '150px' } },
|
|
160
|
+
{ fluidActive: true, responsiveScales: fluidScales }
|
|
161
|
+
);
|
|
162
|
+
expect(classes).toContain('fs-150px');
|
|
163
|
+
expect(classes.some((c) => /^fs-h/.test(c))).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('mismatched units (rem vs px) → falls back, mob- class still emitted', () => {
|
|
167
|
+
clearRegistry();
|
|
168
|
+
const classes = responsiveStylesToClasses(
|
|
169
|
+
{ base: { fontSize: '10rem' }, mobile: { fontSize: '94px' } },
|
|
170
|
+
{ fluidActive: true, responsiveScales: fluidScales }
|
|
171
|
+
);
|
|
172
|
+
expect(classes).toContain('fs-10rem');
|
|
173
|
+
expect(classes).toContain('mob-fs-94px');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('non-scalable mobile override (display) is not consumed', () => {
|
|
177
|
+
clearRegistry();
|
|
178
|
+
const classes = responsiveStylesToClasses(
|
|
179
|
+
{ base: { display: 'flex' }, mobile: { display: 'block' } },
|
|
180
|
+
{ fluidActive: true, responsiveScales: fluidScales }
|
|
181
|
+
);
|
|
182
|
+
expect(classes).toContain('f');
|
|
183
|
+
expect(classes).toContain('mob-b');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('fluidActive off → no consumption, mob- class emitted', () => {
|
|
187
|
+
clearRegistry();
|
|
188
|
+
const classes = responsiveStylesToClasses(
|
|
189
|
+
{ base: { fontSize: '150px' }, mobile: { fontSize: '94px' } },
|
|
190
|
+
{ fluidActive: false, responsiveScales: fluidScales }
|
|
191
|
+
);
|
|
192
|
+
expect(classes).toContain('fs-150px');
|
|
193
|
+
expect(classes).toContain('mob-fs-94px');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('fluidActive on but responsiveScales disabled → no consumption', () => {
|
|
197
|
+
clearRegistry();
|
|
198
|
+
const classes = responsiveStylesToClasses(
|
|
199
|
+
{ base: { fontSize: '150px' }, mobile: { fontSize: '94px' } },
|
|
200
|
+
{ fluidActive: true, responsiveScales: { ...fluidScales, enabled: false } }
|
|
201
|
+
);
|
|
202
|
+
expect(classes).toContain('fs-150px');
|
|
203
|
+
expect(classes).toContain('mob-fs-94px');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('clamp math: MIN=94px, MAX=150px, fluidRange 320–1700', () => {
|
|
207
|
+
clearRegistry();
|
|
208
|
+
const classes = responsiveStylesToClasses(
|
|
209
|
+
{ base: { fontSize: '150px' }, mobile: { fontSize: '94px' } },
|
|
210
|
+
{ fluidActive: true, responsiveScales: fluidScales }
|
|
211
|
+
);
|
|
212
|
+
const hashed = classes.find((c) => /^fs-h[0-9a-z]+$/.test(c))!;
|
|
213
|
+
const value = String(getStyleValue(hashed));
|
|
214
|
+
// slope = (150-94)/(1700-320) = 56/1380 ≈ 0.04058 px/px → slopeVw ≈ 4.058vw
|
|
215
|
+
// intercept = 94 - 0.04058*320 = 94 - 12.9855 ≈ 81.0145px
|
|
216
|
+
expect(value).toMatch(/clamp\(94px,\s*81\.0145px\s*\+\s*4\.058vw,\s*150px\)/);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
96
219
|
});
|
|
97
220
|
|
|
98
221
|
describe('classToStyle', () => {
|