meno-core 1.0.48 → 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/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-B2RTLDXY.js → chunk-AZQYF6KE.js} +132 -1
- package/dist/chunks/chunk-AZQYF6KE.js.map +7 -0
- package/dist/chunks/{chunk-NKUV77SR.js → chunk-CHD5UCFF.js} +21 -9
- package/dist/chunks/{chunk-NKUV77SR.js.map → chunk-CHD5UCFF.js.map} +2 -2
- package/dist/chunks/{chunk-TPQ7APVQ.js → chunk-EQYDSPBB.js} +418 -62
- package/dist/chunks/chunk-EQYDSPBB.js.map +7 -0
- 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-D5E3OKSL.js → chunk-JER5NQVM.js} +5 -5
- package/dist/chunks/{chunk-BJRKEPMP.js → chunk-KPU2XHOS.js} +5 -2
- package/dist/chunks/{chunk-BJRKEPMP.js.map → chunk-KPU2XHOS.js.map} +2 -2
- package/dist/chunks/{chunk-NP76N4HQ.js → chunk-LKAGAQ3M.js} +2 -2
- package/dist/chunks/{chunk-3FHJUHAS.js → chunk-S2CX6HFM.js} +260 -25
- package/dist/chunks/chunk-S2CX6HFM.js.map +7 -0
- package/dist/chunks/{configService-IGJEC3MC.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 +54 -20
- package/dist/lib/client/index.js.map +3 -3
- package/dist/lib/server/index.js +9 -9
- 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.ts +8 -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/providers/fileSystemCMSProvider.test.ts +163 -0
- package/lib/server/providers/fileSystemCMSProvider.ts +200 -11
- 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 +113 -0
- package/lib/server/ssr/htmlGenerator.ts +51 -4
- package/lib/server/ssr/liveReloadIntegration.test.ts +209 -0
- package/lib/server/ssr/ssrRenderer.test.ts +306 -0
- package/lib/server/ssr/ssrRenderer.ts +182 -44
- 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/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/cms.ts +28 -3
- 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.ts +15 -1
- 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-TPQ7APVQ.js.map +0 -7
- package/dist/chunks/chunk-UUA5LEWF.js.map +0 -7
- /package/dist/chunks/{chunk-RQSTH2BS.js.map → chunk-H4JSCDNW.js.map} +0 -0
- /package/dist/chunks/{chunk-D5E3OKSL.js.map → chunk-JER5NQVM.js.map} +0 -0
- /package/dist/chunks/{chunk-NP76N4HQ.js.map → chunk-LKAGAQ3M.js.map} +0 -0
- /package/dist/chunks/{configService-IGJEC3MC.js.map → configService-CCA6AIDI.js.map} +0 -0
|
@@ -7,6 +7,13 @@ import {
|
|
|
7
7
|
scalePropertyValue,
|
|
8
8
|
getResponsiveValues,
|
|
9
9
|
DEFAULT_RESPONSIVE_SCALES,
|
|
10
|
+
DEFAULT_FLUID_RANGE,
|
|
11
|
+
DEFAULT_SITE_MARGIN,
|
|
12
|
+
buildFluidClamp,
|
|
13
|
+
buildFluidClampWithExplicitMin,
|
|
14
|
+
buildFluidPropertyValue,
|
|
15
|
+
buildSiteMarginClamp,
|
|
16
|
+
getSmallestBreakpointName,
|
|
10
17
|
type ResponsiveScales,
|
|
11
18
|
type CSSPropertyType,
|
|
12
19
|
type BreakpointScales,
|
|
@@ -381,5 +388,141 @@ describe('responsiveScaling', () => {
|
|
|
381
388
|
mobile: 0.4,
|
|
382
389
|
});
|
|
383
390
|
});
|
|
391
|
+
|
|
392
|
+
test('should default mode to "breakpoints" and provide fluidRange', () => {
|
|
393
|
+
expect(DEFAULT_RESPONSIVE_SCALES.mode).toBe('breakpoints');
|
|
394
|
+
expect(DEFAULT_RESPONSIVE_SCALES.fluidRange).toEqual({ min: 320, max: 1440 });
|
|
395
|
+
expect(DEFAULT_FLUID_RANGE).toEqual({ min: 320, max: 1440 });
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
describe('buildFluidClamp', () => {
|
|
400
|
+
test('px input — produces clamp(MIN, intercept + slope*100vw, MAX)', () => {
|
|
401
|
+
// base 32, scale 0.75 → MIN = 32 + (32-16)*(0.75-1) = 32 - 4 = 28
|
|
402
|
+
// slope_px = (32-28)/(1440-320) = 4/1120 ≈ 0.003571
|
|
403
|
+
// intercept_px = 28 - 0.003571*320 ≈ 26.857
|
|
404
|
+
// slope_vw = slope_px*100 ≈ 0.357
|
|
405
|
+
const result = buildFluidClamp(32, 'px', 0.75, 320, 1440);
|
|
406
|
+
expect(result.startsWith('clamp(28px,')).toBe(true);
|
|
407
|
+
expect(result.endsWith(', 32px)')).toBe(true);
|
|
408
|
+
expect(result).toContain('vw');
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test('returns plain value (no clamp) when MIN === MAX', () => {
|
|
412
|
+
// 16 with scale 0.5 stays 16 because <= baseReference.
|
|
413
|
+
expect(buildFluidClamp(16, 'px', 0.5, 320, 1440)).toBe('16px');
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test('returns plain value when scale === 1', () => {
|
|
417
|
+
expect(buildFluidClamp(32, 'px', 1, 320, 1440)).toBe('32px');
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test('rem input — emits a clamp() with rem unit on both bounds', () => {
|
|
421
|
+
// calculateResponsiveValue rounds, so we use a large enough rem value that
|
|
422
|
+
// MIN and MAX stay distinct after rounding. base=8rem, scale=0.5, baseRef=1
|
|
423
|
+
// → MIN = round(8 + (8-1)*(-0.5)) = round(4.5) = 5
|
|
424
|
+
const result = buildFluidClamp(8, 'rem', 0.5, 20, 90, 1.0);
|
|
425
|
+
expect(result.startsWith('clamp(5rem,')).toBe(true);
|
|
426
|
+
expect(result.endsWith(', 8rem)')).toBe(true);
|
|
427
|
+
expect(result).toContain('vw');
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
describe('buildFluidClampWithExplicitMin', () => {
|
|
432
|
+
test('px input — MIN=94, MAX=150, fluidRange 320–1700', () => {
|
|
433
|
+
// slope_px = (150-94)/(1700-320) = 56/1380 ≈ 0.04058 → slope_vw ≈ 4.058
|
|
434
|
+
// intercept = 94 - 0.04058*320 ≈ 81.0145
|
|
435
|
+
const result = buildFluidClampWithExplicitMin(94, 150, 'px', 320, 1700);
|
|
436
|
+
expect(result).toMatch(/^clamp\(94px,\s*81\.0145px\s*\+\s*4\.058vw,\s*150px\)$/);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test('returns plain MAX when MIN === MAX', () => {
|
|
440
|
+
expect(buildFluidClampWithExplicitMin(50, 50, 'px', 320, 1700)).toBe('50px');
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test('returns plain MAX when vpMin === vpMax (degenerate range)', () => {
|
|
444
|
+
expect(buildFluidClampWithExplicitMin(40, 80, 'px', 1000, 1000)).toBe('80px');
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test('rem unit — bounds emitted in rem', () => {
|
|
448
|
+
const result = buildFluidClampWithExplicitMin(2, 4, 'rem', 320, 1440);
|
|
449
|
+
expect(result.startsWith('clamp(2rem,')).toBe(true);
|
|
450
|
+
expect(result.endsWith(', 4rem)')).toBe(true);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test('mobile larger than desktop (negative slope) still produces a valid clamp', () => {
|
|
454
|
+
// The function doesn't enforce min<max; it just interpolates. Useful when
|
|
455
|
+
// an author wants the value to shrink as viewport grows.
|
|
456
|
+
const result = buildFluidClampWithExplicitMin(200, 100, 'px', 320, 1700);
|
|
457
|
+
expect(result.startsWith('clamp(200px,')).toBe(true);
|
|
458
|
+
expect(result.endsWith(', 100px)')).toBe(true);
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
describe('buildFluidPropertyValue', () => {
|
|
463
|
+
test('single px value → single clamp()', () => {
|
|
464
|
+
const out = buildFluidPropertyValue('32px', 0.75, 320, 1440);
|
|
465
|
+
expect(out).not.toBeNull();
|
|
466
|
+
expect(out!.startsWith('clamp(')).toBe(true);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test('multi-value padding "20px 40px" → both tokens scaled', () => {
|
|
470
|
+
const out = buildFluidPropertyValue('20px 40px', 0.5, 320, 1440);
|
|
471
|
+
expect(out).not.toBeNull();
|
|
472
|
+
const parts = out!.split(' ');
|
|
473
|
+
// The two tokens get joined by space — but each clamp() also has spaces inside.
|
|
474
|
+
// Check both clamp(...) substrings present.
|
|
475
|
+
expect((out!.match(/clamp\(/g) ?? []).length).toBe(2);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
test('"%" and "em" pass through verbatim', () => {
|
|
479
|
+
expect(buildFluidPropertyValue('50%', 0.5, 320, 1440)).toBeNull();
|
|
480
|
+
expect(buildFluidPropertyValue('1.5em', 0.5, 320, 1440)).toBeNull();
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test('"auto" pass through (no scalable token) → null', () => {
|
|
484
|
+
expect(buildFluidPropertyValue('auto', 0.5, 320, 1440)).toBeNull();
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test('mixed "20px auto" → first token clamped, second kept', () => {
|
|
488
|
+
const out = buildFluidPropertyValue('20px auto', 0.5, 320, 1440);
|
|
489
|
+
expect(out).not.toBeNull();
|
|
490
|
+
expect(out).toContain('clamp(');
|
|
491
|
+
expect(out!.endsWith(' auto')).toBe(true);
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
describe('buildSiteMarginClamp', () => {
|
|
496
|
+
test('emits clamp with px on both bounds', () => {
|
|
497
|
+
const out = buildSiteMarginClamp({ min: 16, max: 32 }, { min: 320, max: 1440 });
|
|
498
|
+
expect(out.startsWith('clamp(16px,')).toBe(true);
|
|
499
|
+
expect(out.endsWith(', 32px)')).toBe(true);
|
|
500
|
+
expect(out).toContain('vw');
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test('returns plain px when min === max', () => {
|
|
504
|
+
expect(buildSiteMarginClamp({ min: 16, max: 16 }, { min: 320, max: 1440 }))
|
|
505
|
+
.toBe('16px');
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test('default DEFAULT_SITE_MARGIN is 16/32', () => {
|
|
509
|
+
expect(DEFAULT_SITE_MARGIN).toEqual({ min: 16, max: 32 });
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
describe('getSmallestBreakpointName', () => {
|
|
514
|
+
test('picks the entry with smallest breakpoint value', () => {
|
|
515
|
+
const bp = {
|
|
516
|
+
tablet: { breakpoint: 1024 },
|
|
517
|
+
mobile: { breakpoint: 540 },
|
|
518
|
+
small: { breakpoint: 360 },
|
|
519
|
+
};
|
|
520
|
+
expect(getSmallestBreakpointName(bp)).toBe('small');
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
test('returns null for empty / undefined', () => {
|
|
524
|
+
expect(getSmallestBreakpointName(undefined)).toBeNull();
|
|
525
|
+
expect(getSmallestBreakpointName({})).toBeNull();
|
|
526
|
+
});
|
|
384
527
|
});
|
|
385
528
|
});
|
|
@@ -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;
|