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
|
@@ -2,12 +2,32 @@ import { describe, test, expect } from 'bun:test';
|
|
|
2
2
|
import {
|
|
3
3
|
CSS_PROPERTIES,
|
|
4
4
|
CSS_PROPERTIES_DEFINITION,
|
|
5
|
+
CSS_PROPERTY_GROUPS,
|
|
5
6
|
filterCSSProperties,
|
|
6
7
|
getPropertyValues,
|
|
7
8
|
filterPropertyValues,
|
|
8
|
-
getPropertyType
|
|
9
|
+
getPropertyType,
|
|
10
|
+
VISUAL_MODE_PROPERTIES,
|
|
11
|
+
isVisualModeProperty,
|
|
12
|
+
VISUAL_MODE_RULES,
|
|
13
|
+
isVisualModeRowVisible,
|
|
14
|
+
type VisualModeRuleContext,
|
|
15
|
+
UNITLESS_PROPERTIES,
|
|
16
|
+
appendsPxByDefault,
|
|
9
17
|
} from './cssProperties';
|
|
10
18
|
|
|
19
|
+
const baseCtx: VisualModeRuleContext = {
|
|
20
|
+
display: 'block',
|
|
21
|
+
parentDisplay: 'block',
|
|
22
|
+
position: 'static',
|
|
23
|
+
tagName: 'div',
|
|
24
|
+
hasPaddingLonghand: false,
|
|
25
|
+
hasMarginLonghand: false,
|
|
26
|
+
hasBackgroundImage: false,
|
|
27
|
+
};
|
|
28
|
+
const ctx = (overrides: Partial<VisualModeRuleContext> = {}): VisualModeRuleContext =>
|
|
29
|
+
({ ...baseCtx, ...overrides });
|
|
30
|
+
|
|
11
31
|
describe('cssProperties', () => {
|
|
12
32
|
describe('CSS_PROPERTIES', () => {
|
|
13
33
|
test('is an array of property names', () => {
|
|
@@ -249,4 +269,230 @@ describe('cssProperties', () => {
|
|
|
249
269
|
expect(getPropertyType('padding')).toBe('string');
|
|
250
270
|
});
|
|
251
271
|
});
|
|
272
|
+
|
|
273
|
+
describe('VISUAL_MODE_PROPERTIES', () => {
|
|
274
|
+
test('contains common Webflow-style groups', () => {
|
|
275
|
+
expect(VISUAL_MODE_PROPERTIES['Layout']).toContain('display');
|
|
276
|
+
expect(VISUAL_MODE_PROPERTIES['Spacing']).toContain('padding');
|
|
277
|
+
expect(VISUAL_MODE_PROPERTIES['Typography']).toContain('fontSize');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('every group name maps to a known CSS_PROPERTY_GROUPS group', () => {
|
|
281
|
+
for (const groupName of Object.keys(VISUAL_MODE_PROPERTIES)) {
|
|
282
|
+
expect(CSS_PROPERTY_GROUPS[groupName]).toBeDefined();
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('every property in VISUAL_MODE_PROPERTIES belongs to its declared group in CSS_PROPERTY_GROUPS', () => {
|
|
287
|
+
for (const [groupName, props] of Object.entries(VISUAL_MODE_PROPERTIES)) {
|
|
288
|
+
for (const prop of props) {
|
|
289
|
+
expect(CSS_PROPERTY_GROUPS[groupName]).toContain(prop);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe('isVisualModeProperty', () => {
|
|
296
|
+
test('returns true for properties listed in VISUAL_MODE_PROPERTIES', () => {
|
|
297
|
+
expect(isVisualModeProperty('display')).toBe(true);
|
|
298
|
+
expect(isVisualModeProperty('padding')).toBe(true);
|
|
299
|
+
expect(isVisualModeProperty('color')).toBe(true);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('returns false for properties not in VISUAL_MODE_PROPERTIES', () => {
|
|
303
|
+
expect(isVisualModeProperty('gridTemplateRows')).toBe(false);
|
|
304
|
+
expect(isVisualModeProperty('clipPath')).toBe(false);
|
|
305
|
+
expect(isVisualModeProperty('nonExistent')).toBe(false);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe('VISUAL_MODE_RULES — flex/grid container props', () => {
|
|
310
|
+
test('flexDirection visible only when display is flex/inline-flex', () => {
|
|
311
|
+
expect(VISUAL_MODE_RULES.flexDirection(ctx({ display: 'flex' }))).toBe(true);
|
|
312
|
+
expect(VISUAL_MODE_RULES.flexDirection(ctx({ display: 'inline-flex' }))).toBe(true);
|
|
313
|
+
expect(VISUAL_MODE_RULES.flexDirection(ctx({ display: 'block' }))).toBe(false);
|
|
314
|
+
expect(VISUAL_MODE_RULES.flexDirection(ctx({ display: 'grid' }))).toBe(false);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test('justifyContent visible for flex AND grid', () => {
|
|
318
|
+
expect(VISUAL_MODE_RULES.justifyContent(ctx({ display: 'flex' }))).toBe(true);
|
|
319
|
+
expect(VISUAL_MODE_RULES.justifyContent(ctx({ display: 'grid' }))).toBe(true);
|
|
320
|
+
expect(VISUAL_MODE_RULES.justifyContent(ctx({ display: 'block' }))).toBe(false);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test('gridTemplateColumns visible only for grid/inline-grid', () => {
|
|
324
|
+
expect(VISUAL_MODE_RULES.gridTemplateColumns(ctx({ display: 'grid' }))).toBe(true);
|
|
325
|
+
expect(VISUAL_MODE_RULES.gridTemplateColumns(ctx({ display: 'inline-grid' }))).toBe(true);
|
|
326
|
+
expect(VISUAL_MODE_RULES.gridTemplateColumns(ctx({ display: 'flex' }))).toBe(false);
|
|
327
|
+
expect(VISUAL_MODE_RULES.gridTemplateColumns(ctx({ display: 'block' }))).toBe(false);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test('gap visible for both flex and grid containers', () => {
|
|
331
|
+
expect(VISUAL_MODE_RULES.gap(ctx({ display: 'flex' }))).toBe(true);
|
|
332
|
+
expect(VISUAL_MODE_RULES.gap(ctx({ display: 'grid' }))).toBe(true);
|
|
333
|
+
expect(VISUAL_MODE_RULES.gap(ctx({ display: 'block' }))).toBe(false);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe('VISUAL_MODE_RULES — flex/grid item props (parent display)', () => {
|
|
338
|
+
test('flexGrow visible only when parent is flex', () => {
|
|
339
|
+
expect(VISUAL_MODE_RULES.flexGrow(ctx({ parentDisplay: 'flex' }))).toBe(true);
|
|
340
|
+
expect(VISUAL_MODE_RULES.flexGrow(ctx({ parentDisplay: 'inline-flex' }))).toBe(true);
|
|
341
|
+
expect(VISUAL_MODE_RULES.flexGrow(ctx({ parentDisplay: 'block' }))).toBe(false);
|
|
342
|
+
expect(VISUAL_MODE_RULES.flexGrow(ctx({ parentDisplay: 'grid' }))).toBe(false);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test('gridArea visible only when parent is grid', () => {
|
|
346
|
+
expect(VISUAL_MODE_RULES.gridArea(ctx({ parentDisplay: 'grid' }))).toBe(true);
|
|
347
|
+
expect(VISUAL_MODE_RULES.gridArea(ctx({ parentDisplay: 'inline-grid' }))).toBe(true);
|
|
348
|
+
expect(VISUAL_MODE_RULES.gridArea(ctx({ parentDisplay: 'flex' }))).toBe(false);
|
|
349
|
+
expect(VISUAL_MODE_RULES.gridArea(ctx({ parentDisplay: 'block' }))).toBe(false);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test('alignSelf visible when parent is flex OR grid', () => {
|
|
353
|
+
expect(VISUAL_MODE_RULES.alignSelf(ctx({ parentDisplay: 'flex' }))).toBe(true);
|
|
354
|
+
expect(VISUAL_MODE_RULES.alignSelf(ctx({ parentDisplay: 'grid' }))).toBe(true);
|
|
355
|
+
expect(VISUAL_MODE_RULES.alignSelf(ctx({ parentDisplay: 'block' }))).toBe(false);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test('item rules ignore own display — only parent display matters', () => {
|
|
359
|
+
// Element has display:block but parent is flex → flexGrow still visible.
|
|
360
|
+
expect(VISUAL_MODE_RULES.flexGrow(ctx({ display: 'block', parentDisplay: 'flex' }))).toBe(true);
|
|
361
|
+
// Element has display:flex but parent is block → flexGrow hidden.
|
|
362
|
+
expect(VISUAL_MODE_RULES.flexGrow(ctx({ display: 'flex', parentDisplay: 'block' }))).toBe(false);
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe('VISUAL_MODE_RULES — position-dependent', () => {
|
|
367
|
+
test('top/right/bottom/left/zIndex hidden when position is static', () => {
|
|
368
|
+
const c = ctx({ position: 'static' });
|
|
369
|
+
expect(VISUAL_MODE_RULES.top(c)).toBe(false);
|
|
370
|
+
expect(VISUAL_MODE_RULES.right(c)).toBe(false);
|
|
371
|
+
expect(VISUAL_MODE_RULES.bottom(c)).toBe(false);
|
|
372
|
+
expect(VISUAL_MODE_RULES.left(c)).toBe(false);
|
|
373
|
+
expect(VISUAL_MODE_RULES.zIndex(c)).toBe(false);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test('top/right/bottom/left/zIndex visible when position is non-static', () => {
|
|
377
|
+
for (const position of ['relative', 'absolute', 'fixed', 'sticky']) {
|
|
378
|
+
const c = ctx({ position });
|
|
379
|
+
expect(VISUAL_MODE_RULES.top(c)).toBe(true);
|
|
380
|
+
expect(VISUAL_MODE_RULES.zIndex(c)).toBe(true);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
describe('VISUAL_MODE_RULES — tag-based', () => {
|
|
386
|
+
test('objectFit visible only for img/video', () => {
|
|
387
|
+
expect(VISUAL_MODE_RULES.objectFit(ctx({ tagName: 'img' }))).toBe(true);
|
|
388
|
+
expect(VISUAL_MODE_RULES.objectFit(ctx({ tagName: 'video' }))).toBe(true);
|
|
389
|
+
expect(VISUAL_MODE_RULES.objectFit(ctx({ tagName: 'div' }))).toBe(false);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test('listStyle visible only for ul/ol/li', () => {
|
|
393
|
+
expect(VISUAL_MODE_RULES.listStyle(ctx({ tagName: 'ul' }))).toBe(true);
|
|
394
|
+
expect(VISUAL_MODE_RULES.listStyle(ctx({ tagName: 'ol' }))).toBe(true);
|
|
395
|
+
expect(VISUAL_MODE_RULES.listStyle(ctx({ tagName: 'li' }))).toBe(true);
|
|
396
|
+
expect(VISUAL_MODE_RULES.listStyle(ctx({ tagName: 'div' }))).toBe(false);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
describe('VISUAL_MODE_RULES — padding/margin shorthand-vs-longhand toggle', () => {
|
|
401
|
+
test('padding shown when no longhand set; longhands hidden', () => {
|
|
402
|
+
const c = ctx({ hasPaddingLonghand: false });
|
|
403
|
+
expect(VISUAL_MODE_RULES.padding(c)).toBe(true);
|
|
404
|
+
expect(VISUAL_MODE_RULES.paddingTop(c)).toBe(false);
|
|
405
|
+
expect(VISUAL_MODE_RULES.paddingRight(c)).toBe(false);
|
|
406
|
+
expect(VISUAL_MODE_RULES.paddingBottom(c)).toBe(false);
|
|
407
|
+
expect(VISUAL_MODE_RULES.paddingLeft(c)).toBe(false);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test('padding hidden when any longhand set; longhands shown', () => {
|
|
411
|
+
const c = ctx({ hasPaddingLonghand: true });
|
|
412
|
+
expect(VISUAL_MODE_RULES.padding(c)).toBe(false);
|
|
413
|
+
expect(VISUAL_MODE_RULES.paddingTop(c)).toBe(true);
|
|
414
|
+
expect(VISUAL_MODE_RULES.paddingRight(c)).toBe(true);
|
|
415
|
+
expect(VISUAL_MODE_RULES.paddingBottom(c)).toBe(true);
|
|
416
|
+
expect(VISUAL_MODE_RULES.paddingLeft(c)).toBe(true);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test('margin mirrors padding behavior', () => {
|
|
420
|
+
const noLong = ctx({ hasMarginLonghand: false });
|
|
421
|
+
expect(VISUAL_MODE_RULES.margin(noLong)).toBe(true);
|
|
422
|
+
expect(VISUAL_MODE_RULES.marginTop(noLong)).toBe(false);
|
|
423
|
+
const withLong = ctx({ hasMarginLonghand: true });
|
|
424
|
+
expect(VISUAL_MODE_RULES.margin(withLong)).toBe(false);
|
|
425
|
+
expect(VISUAL_MODE_RULES.marginTop(withLong)).toBe(true);
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
describe('isVisualModeRowVisible', () => {
|
|
430
|
+
test('returns true for properties without a rule', () => {
|
|
431
|
+
// width has no rule entry → always visible regardless of context.
|
|
432
|
+
expect(isVisualModeRowVisible('width', ctx())).toBe(true);
|
|
433
|
+
expect(isVisualModeRowVisible('color', ctx())).toBe(true);
|
|
434
|
+
expect(isVisualModeRowVisible('nonExistentProp', ctx())).toBe(true);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test('delegates to the rule for known properties', () => {
|
|
438
|
+
expect(isVisualModeRowVisible('flexDirection', ctx({ display: 'flex' }))).toBe(true);
|
|
439
|
+
expect(isVisualModeRowVisible('flexDirection', ctx({ display: 'block' }))).toBe(false);
|
|
440
|
+
expect(isVisualModeRowVisible('top', ctx({ position: 'absolute' }))).toBe(true);
|
|
441
|
+
expect(isVisualModeRowVisible('top', ctx({ position: 'static' }))).toBe(false);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
describe('UNITLESS_PROPERTIES', () => {
|
|
446
|
+
test('contains the canonical unitless props', () => {
|
|
447
|
+
expect(UNITLESS_PROPERTIES.has('fontWeight')).toBe(true);
|
|
448
|
+
expect(UNITLESS_PROPERTIES.has('lineHeight')).toBe(true);
|
|
449
|
+
expect(UNITLESS_PROPERTIES.has('opacity')).toBe(true);
|
|
450
|
+
expect(UNITLESS_PROPERTIES.has('zIndex')).toBe(true);
|
|
451
|
+
expect(UNITLESS_PROPERTIES.has('flexGrow')).toBe(true);
|
|
452
|
+
expect(UNITLESS_PROPERTIES.has('order')).toBe(true);
|
|
453
|
+
expect(UNITLESS_PROPERTIES.has('aspectRatio')).toBe(true);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test('does not contain length-accepting props', () => {
|
|
457
|
+
expect(UNITLESS_PROPERTIES.has('width')).toBe(false);
|
|
458
|
+
expect(UNITLESS_PROPERTIES.has('padding')).toBe(false);
|
|
459
|
+
expect(UNITLESS_PROPERTIES.has('fontSize')).toBe(false);
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
describe('appendsPxByDefault', () => {
|
|
464
|
+
test('returns true for length-accepting props', () => {
|
|
465
|
+
expect(appendsPxByDefault('width')).toBe(true);
|
|
466
|
+
expect(appendsPxByDefault('height')).toBe(true);
|
|
467
|
+
expect(appendsPxByDefault('padding')).toBe(true);
|
|
468
|
+
expect(appendsPxByDefault('paddingTop')).toBe(true);
|
|
469
|
+
expect(appendsPxByDefault('margin')).toBe(true);
|
|
470
|
+
expect(appendsPxByDefault('gap')).toBe(true);
|
|
471
|
+
expect(appendsPxByDefault('fontSize')).toBe(true);
|
|
472
|
+
expect(appendsPxByDefault('borderRadius')).toBe(true);
|
|
473
|
+
expect(appendsPxByDefault('top')).toBe(true);
|
|
474
|
+
expect(appendsPxByDefault('left')).toBe(true);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test('returns false for unitless props', () => {
|
|
478
|
+
expect(appendsPxByDefault('lineHeight')).toBe(false);
|
|
479
|
+
expect(appendsPxByDefault('fontWeight')).toBe(false);
|
|
480
|
+
expect(appendsPxByDefault('opacity')).toBe(false);
|
|
481
|
+
expect(appendsPxByDefault('zIndex')).toBe(false);
|
|
482
|
+
expect(appendsPxByDefault('flexGrow')).toBe(false);
|
|
483
|
+
expect(appendsPxByDefault('order')).toBe(false);
|
|
484
|
+
expect(appendsPxByDefault('aspectRatio')).toBe(false);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test('returns false for keyword-only (select / boolean) props', () => {
|
|
488
|
+
expect(appendsPxByDefault('display')).toBe(false);
|
|
489
|
+
expect(appendsPxByDefault('position')).toBe(false);
|
|
490
|
+
expect(appendsPxByDefault('borderStyle')).toBe(false);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test('defaults to true for properties without a recognized unitless / keyword type', () => {
|
|
494
|
+
expect(appendsPxByDefault('inset')).toBe(true);
|
|
495
|
+
expect(appendsPxByDefault('someUnknownNewCSSProperty')).toBe(true);
|
|
496
|
+
});
|
|
497
|
+
});
|
|
252
498
|
});
|
|
@@ -421,14 +421,15 @@ function getPropertyPriority(property: string): number {
|
|
|
421
421
|
* Order determines display order in the UI
|
|
422
422
|
*/
|
|
423
423
|
export const CSS_PROPERTY_GROUPS: Record<string, string[]> = {
|
|
424
|
-
'Layout': ['display'
|
|
425
|
-
'
|
|
426
|
-
'
|
|
427
|
-
'
|
|
424
|
+
'Layout': ['display'],
|
|
425
|
+
'Spacing': ['margin', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft', 'padding', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'],
|
|
426
|
+
'Size': ['width', 'height', 'minWidth', 'maxWidth', 'minHeight', 'maxHeight', 'aspectRatio'],
|
|
427
|
+
'Position': ['position', 'top', 'right', 'bottom', 'left', 'inset', 'zIndex'],
|
|
428
|
+
'Flexbox': ['flex', 'flexDirection', 'flexWrap', 'flexFlow', 'justifyContent', 'alignItems', 'alignContent', 'gap', 'rowGap', 'columnGap', 'alignSelf', 'flexGrow', 'flexShrink', 'flexBasis', 'order'],
|
|
428
429
|
'Grid': ['grid', 'gridTemplateColumns', 'gridTemplateRows', 'gridTemplateAreas', 'gridGap', 'gridColumn', 'gridRow', 'gridArea', 'gridAutoFlow', 'gridAutoColumns', 'gridAutoRows', 'justifyItems', 'justifySelf', 'placeContent', 'placeItems', 'placeSelf'],
|
|
429
|
-
'Typography': ['
|
|
430
|
+
'Typography': ['fontWeight', 'fontSize', 'fontFamily', 'fontStyle', 'lineHeight', 'color', 'textAlign', 'textDecoration', 'textTransform', 'letterSpacing', 'wordSpacing', 'wordBreak', 'overflowWrap', 'textIndent', 'verticalAlign'],
|
|
430
431
|
'Background': ['background', 'backgroundColor', 'backgroundImage', 'backgroundSize', 'backgroundPosition', 'backgroundRepeat', 'opacity'],
|
|
431
|
-
'Borders': ['
|
|
432
|
+
'Borders': ['borderRadius', 'borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomLeftRadius', 'borderBottomRightRadius', 'border', 'borderWidth', 'borderStyle', 'borderColor', 'borderTop', 'borderRight', 'borderBottom', 'borderLeft'],
|
|
432
433
|
'Outline': ['outline', 'outlineWidth', 'outlineStyle', 'outlineColor', 'outlineOffset'],
|
|
433
434
|
'Effects': ['boxShadow', 'textShadow', 'filter', 'backdropFilter', 'transform', 'transformOrigin', 'transition', 'animation', 'backfaceVisibility', 'mixBlendMode', 'clipPath'],
|
|
434
435
|
'Overflow': ['overflow', 'overflowX', 'overflowY', 'whiteSpace', 'textOverflow', 'visibility', 'content'],
|
|
@@ -452,6 +453,165 @@ export function getPropertyGroup(propertyName: string): string {
|
|
|
452
453
|
return 'Other';
|
|
453
454
|
}
|
|
454
455
|
|
|
456
|
+
/**
|
|
457
|
+
* Subset of CSS properties shown as "always-visible" rows in the visual style
|
|
458
|
+
* editor mode (Webflow-like). Each property here renders even when unset, so
|
|
459
|
+
* the user can see at a glance which properties are available and click an
|
|
460
|
+
* empty row to start typing a value.
|
|
461
|
+
*
|
|
462
|
+
* Keys must be group names from CSS_PROPERTY_GROUPS so the visual editor can
|
|
463
|
+
* reuse the same group headers as list mode.
|
|
464
|
+
*/
|
|
465
|
+
export const VISUAL_MODE_PROPERTIES: Record<string, string[]> = {
|
|
466
|
+
'Layout': ['display'],
|
|
467
|
+
'Grid': ['gridTemplateColumns'],
|
|
468
|
+
'Flexbox': ['flexDirection', 'flexWrap', 'justifyContent', 'alignItems', 'gap', 'flexGrow', 'flexShrink', 'flexBasis'],
|
|
469
|
+
'Spacing': ['margin', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft', 'padding', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'],
|
|
470
|
+
'Size': ['width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight'],
|
|
471
|
+
'Position': ['position', 'top', 'right', 'bottom', 'left', 'zIndex'],
|
|
472
|
+
'Typography': ['fontFamily', 'fontWeight', 'fontSize', 'lineHeight', 'color', 'letterSpacing', 'textAlign', 'textTransform', 'textDecoration'],
|
|
473
|
+
'Background': ['backgroundColor', 'backgroundImage', 'backgroundSize', 'backgroundPosition', 'backgroundRepeat', 'opacity'],
|
|
474
|
+
'Borders': ['borderRadius', 'borderWidth', 'borderStyle', 'borderColor'],
|
|
475
|
+
'Effects': ['boxShadow', 'transform', 'transition', 'filter'],
|
|
476
|
+
'Overflow': ['overflow', 'whiteSpace'],
|
|
477
|
+
'Interaction': ['cursor', 'pointerEvents'],
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const VISUAL_MODE_PROPERTIES_SET = new Set(
|
|
481
|
+
Object.values(VISUAL_MODE_PROPERTIES).flat()
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* True if `prop` belongs to the always-visible visual-mode list.
|
|
486
|
+
*/
|
|
487
|
+
export function isVisualModeProperty(prop: string): boolean {
|
|
488
|
+
return VISUAL_MODE_PROPERTIES_SET.has(prop);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Context used by visual-mode visibility rules. Captures everything a rule
|
|
493
|
+
* needs to decide whether a property row is meaningful for the currently
|
|
494
|
+
* selected element. Built once per render in `StyleEditor`.
|
|
495
|
+
*/
|
|
496
|
+
export interface VisualModeRuleContext {
|
|
497
|
+
/** Effective `display` of the selected element — getComputedStyle().display
|
|
498
|
+
* with declared (instance/inherited/effective) merge as fallback when the
|
|
499
|
+
* iframe hasn't loaded yet. Defaults to 'block'. */
|
|
500
|
+
display: string;
|
|
501
|
+
/** Effective `display` of the DOM parent — used for flex/grid item props
|
|
502
|
+
* whose applicability depends on the parent's layout mode. '' when no
|
|
503
|
+
* parent is reachable (root element, iframe missing). */
|
|
504
|
+
parentDisplay: string;
|
|
505
|
+
/** Effective `position` of the selected element. Defaults to 'static'. */
|
|
506
|
+
position: string;
|
|
507
|
+
/** Lowercase HTML tag name of the selected element ('div', 'img', 'ul'…). */
|
|
508
|
+
tagName: string;
|
|
509
|
+
/** True if any of paddingTop / paddingRight / paddingBottom / paddingLeft
|
|
510
|
+
* is set anywhere in the cascade (instance + inherited + effective).
|
|
511
|
+
* When true, the panel shows the four longhand rows and hides `padding`. */
|
|
512
|
+
hasPaddingLonghand: boolean;
|
|
513
|
+
/** Same as `hasPaddingLonghand` for the four `margin*` longhands. */
|
|
514
|
+
hasMarginLonghand: boolean;
|
|
515
|
+
/** True if `backgroundImage` (or the `background` shorthand) is set anywhere
|
|
516
|
+
* in the cascade. Drives visibility of background-image-only properties
|
|
517
|
+
* like `backgroundPosition` / `backgroundSize` / `backgroundRepeat`. */
|
|
518
|
+
hasBackgroundImage: boolean;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export type VisualModeRule = (ctx: VisualModeRuleContext) => boolean;
|
|
522
|
+
|
|
523
|
+
const isFlexDisplay = (d: string) => d === 'flex' || d === 'inline-flex';
|
|
524
|
+
const isGridDisplay = (d: string) => d === 'grid' || d === 'inline-grid';
|
|
525
|
+
const isListTag = (t: string) => t === 'ul' || t === 'ol' || t === 'li';
|
|
526
|
+
const isMediaTag = (t: string) => t === 'img' || t === 'video';
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Visibility rules for the visual-mode style panel. Each entry is a predicate
|
|
530
|
+
* that returns true when the row should be rendered for the current element
|
|
531
|
+
* context. Properties without an entry are always visible. Properties that
|
|
532
|
+
* are *explicitly set* on the element bypass these rules (the explicit-value
|
|
533
|
+
* override is enforced by the caller, not here).
|
|
534
|
+
*/
|
|
535
|
+
export const VISUAL_MODE_RULES: Record<string, VisualModeRule> = {
|
|
536
|
+
// Flex/Grid CONTAINER props — own display must be flex/grid.
|
|
537
|
+
flexDirection: ctx => isFlexDisplay(ctx.display),
|
|
538
|
+
flexWrap: ctx => isFlexDisplay(ctx.display),
|
|
539
|
+
justifyContent: ctx => isFlexDisplay(ctx.display) || isGridDisplay(ctx.display),
|
|
540
|
+
alignItems: ctx => isFlexDisplay(ctx.display) || isGridDisplay(ctx.display),
|
|
541
|
+
alignContent: ctx => isFlexDisplay(ctx.display) || isGridDisplay(ctx.display),
|
|
542
|
+
gap: ctx => isFlexDisplay(ctx.display) || isGridDisplay(ctx.display),
|
|
543
|
+
rowGap: ctx => isFlexDisplay(ctx.display) || isGridDisplay(ctx.display),
|
|
544
|
+
columnGap: ctx => isFlexDisplay(ctx.display) || isGridDisplay(ctx.display),
|
|
545
|
+
gridTemplateColumns: ctx => isGridDisplay(ctx.display),
|
|
546
|
+
gridTemplateRows: ctx => isGridDisplay(ctx.display),
|
|
547
|
+
gridTemplateAreas: ctx => isGridDisplay(ctx.display),
|
|
548
|
+
gridGap: ctx => isGridDisplay(ctx.display),
|
|
549
|
+
gridAutoFlow: ctx => isGridDisplay(ctx.display),
|
|
550
|
+
gridAutoColumns: ctx => isGridDisplay(ctx.display),
|
|
551
|
+
gridAutoRows: ctx => isGridDisplay(ctx.display),
|
|
552
|
+
justifyItems: ctx => isGridDisplay(ctx.display),
|
|
553
|
+
placeItems: ctx => isGridDisplay(ctx.display),
|
|
554
|
+
placeContent: ctx => isGridDisplay(ctx.display),
|
|
555
|
+
|
|
556
|
+
// Flex/Grid ITEM props — PARENT display must be flex/grid.
|
|
557
|
+
flex: ctx => isFlexDisplay(ctx.parentDisplay),
|
|
558
|
+
flexFlow: ctx => isFlexDisplay(ctx.parentDisplay),
|
|
559
|
+
flexGrow: ctx => isFlexDisplay(ctx.parentDisplay),
|
|
560
|
+
flexShrink: ctx => isFlexDisplay(ctx.parentDisplay),
|
|
561
|
+
flexBasis: ctx => isFlexDisplay(ctx.parentDisplay),
|
|
562
|
+
alignSelf: ctx => isFlexDisplay(ctx.parentDisplay) || isGridDisplay(ctx.parentDisplay),
|
|
563
|
+
justifySelf: ctx => isGridDisplay(ctx.parentDisplay),
|
|
564
|
+
placeSelf: ctx => isGridDisplay(ctx.parentDisplay),
|
|
565
|
+
order: ctx => isFlexDisplay(ctx.parentDisplay) || isGridDisplay(ctx.parentDisplay),
|
|
566
|
+
gridArea: ctx => isGridDisplay(ctx.parentDisplay),
|
|
567
|
+
gridColumn: ctx => isGridDisplay(ctx.parentDisplay),
|
|
568
|
+
gridRow: ctx => isGridDisplay(ctx.parentDisplay),
|
|
569
|
+
|
|
570
|
+
// Position-dependent inset properties.
|
|
571
|
+
top: ctx => ctx.position !== 'static',
|
|
572
|
+
right: ctx => ctx.position !== 'static',
|
|
573
|
+
bottom: ctx => ctx.position !== 'static',
|
|
574
|
+
left: ctx => ctx.position !== 'static',
|
|
575
|
+
inset: ctx => ctx.position !== 'static',
|
|
576
|
+
zIndex: ctx => ctx.position !== 'static',
|
|
577
|
+
|
|
578
|
+
// Tag-based.
|
|
579
|
+
objectFit: ctx => isMediaTag(ctx.tagName),
|
|
580
|
+
objectPosition: ctx => isMediaTag(ctx.tagName),
|
|
581
|
+
listStyle: ctx => isListTag(ctx.tagName),
|
|
582
|
+
listStyleType: ctx => isListTag(ctx.tagName),
|
|
583
|
+
listStylePosition: ctx => isListTag(ctx.tagName),
|
|
584
|
+
|
|
585
|
+
// Padding/margin shorthand-vs-longhand auto-toggle.
|
|
586
|
+
// When ANY longhand is set, hide shorthand; otherwise hide longhands.
|
|
587
|
+
padding: ctx => !ctx.hasPaddingLonghand,
|
|
588
|
+
paddingTop: ctx => ctx.hasPaddingLonghand,
|
|
589
|
+
paddingRight: ctx => ctx.hasPaddingLonghand,
|
|
590
|
+
paddingBottom: ctx => ctx.hasPaddingLonghand,
|
|
591
|
+
paddingLeft: ctx => ctx.hasPaddingLonghand,
|
|
592
|
+
margin: ctx => !ctx.hasMarginLonghand,
|
|
593
|
+
marginTop: ctx => ctx.hasMarginLonghand,
|
|
594
|
+
marginRight: ctx => ctx.hasMarginLonghand,
|
|
595
|
+
marginBottom: ctx => ctx.hasMarginLonghand,
|
|
596
|
+
marginLeft: ctx => ctx.hasMarginLonghand,
|
|
597
|
+
|
|
598
|
+
// Background-image-only props — meaningless without a background image.
|
|
599
|
+
backgroundPosition: ctx => ctx.hasBackgroundImage,
|
|
600
|
+
backgroundSize: ctx => ctx.hasBackgroundImage,
|
|
601
|
+
backgroundRepeat: ctx => ctx.hasBackgroundImage,
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* True if `prop` should be visible in the visual-mode panel given `ctx`.
|
|
606
|
+
* Properties without an entry in `VISUAL_MODE_RULES` default to visible.
|
|
607
|
+
* Callers are responsible for the explicit-value override (a property that
|
|
608
|
+
* the user has set must remain visible regardless of the rule outcome).
|
|
609
|
+
*/
|
|
610
|
+
export function isVisualModeRowVisible(prop: string, ctx: VisualModeRuleContext): boolean {
|
|
611
|
+
const rule = VISUAL_MODE_RULES[prop];
|
|
612
|
+
return rule ? rule(ctx) : true;
|
|
613
|
+
}
|
|
614
|
+
|
|
455
615
|
/**
|
|
456
616
|
* Check if property matches the abbreviation pattern
|
|
457
617
|
* For example, "bC" matches "backgroundColor" (b→b, C→C capital letter)
|
|
@@ -563,3 +723,33 @@ export function filterPropertyValues(propertyName: string, input: string): strin
|
|
|
563
723
|
export function getPropertyType(propertyName: string): 'string' | 'select' | 'boolean' | 'number' | undefined {
|
|
564
724
|
return CSS_PROPERTIES_DEFINITION[propertyName]?.type;
|
|
565
725
|
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* CSS properties whose numeric values are unitless by spec — `font-weight: 400`,
|
|
729
|
+
* `line-height: 1.5`, `opacity: 0.8`, `z-index: 10`, etc. The auto-px commit
|
|
730
|
+
* normalizer must NOT append `px` to bare numbers for these.
|
|
731
|
+
*/
|
|
732
|
+
export const UNITLESS_PROPERTIES: ReadonlySet<string> = new Set([
|
|
733
|
+
'fontWeight', 'lineHeight',
|
|
734
|
+
'opacity', 'fillOpacity', 'strokeOpacity', 'stopOpacity',
|
|
735
|
+
'zIndex', 'order',
|
|
736
|
+
'flexGrow', 'flexShrink', 'flex',
|
|
737
|
+
'columnCount', 'columns', 'tabSize', 'orphans', 'widows',
|
|
738
|
+
'gridRow', 'gridColumn', 'gridRowStart', 'gridRowEnd',
|
|
739
|
+
'gridColumnStart', 'gridColumnEnd',
|
|
740
|
+
'animationIterationCount',
|
|
741
|
+
'aspectRatio',
|
|
742
|
+
'scale',
|
|
743
|
+
]);
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Whether bare numeric values for `propertyName` should be treated as pixels at
|
|
747
|
+
* commit time (e.g. typing `52` for `width` becomes `52px`). Unitless props,
|
|
748
|
+
* `select`/`boolean` keyword props, and existing `number`-typed entries return false.
|
|
749
|
+
*/
|
|
750
|
+
export function appendsPxByDefault(propertyName: string): boolean {
|
|
751
|
+
if (UNITLESS_PROPERTIES.has(propertyName)) return false;
|
|
752
|
+
const def = CSS_PROPERTIES_DEFINITION[propertyName];
|
|
753
|
+
if (def?.type === 'select' || def?.type === 'boolean' || def?.type === 'number') return false;
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
@@ -99,26 +99,59 @@ export interface CMSSchemaInfo {
|
|
|
99
99
|
* CMS Provider Interface
|
|
100
100
|
* Abstracts CMS data loading from the underlying storage mechanism
|
|
101
101
|
* (follows same pattern as PageProvider)
|
|
102
|
+
*
|
|
103
|
+
* Draft model: each item may have a published version (`{filename}.json`)
|
|
104
|
+
* and/or a draft version (`{filename}.draft.json`). The published-facing
|
|
105
|
+
* methods (`getItems`, `getItemByFilename`, `getItemBySlug`, `getItemById`)
|
|
106
|
+
* return ONLY published items. Draft access goes through the `*Draft`
|
|
107
|
+
* methods. SSR and the static export must never read drafts.
|
|
102
108
|
*/
|
|
103
109
|
export interface CMSProvider {
|
|
104
110
|
/** Load all CMS schemas (extracted from page files with source: 'cms') */
|
|
105
111
|
getAllSchemas(): Promise<Map<string, CMSSchemaInfo>>;
|
|
106
112
|
|
|
107
|
-
/** Load all items for a collection */
|
|
113
|
+
/** Load all PUBLISHED items for a collection */
|
|
108
114
|
getItems(collection: string): Promise<CMSItem[]>;
|
|
109
115
|
|
|
110
|
-
/** Get single item by filename (stable identifier) */
|
|
116
|
+
/** Get single PUBLISHED item by filename (stable identifier) */
|
|
111
117
|
getItemByFilename(collection: string, filename: string): Promise<CMSItem | null>;
|
|
112
118
|
|
|
113
|
-
/** Get single item by slug (backward compat - maps to filename lookup) */
|
|
119
|
+
/** Get single PUBLISHED item by slug (backward compat - maps to filename lookup) */
|
|
114
120
|
getItemBySlug(collection: string, slug: string): Promise<CMSItem | null>;
|
|
115
121
|
|
|
116
|
-
/** Get single item by ID */
|
|
122
|
+
/** Get single PUBLISHED item by ID */
|
|
117
123
|
getItemById(collection: string, id: string): Promise<CMSItem | null>;
|
|
118
124
|
|
|
119
|
-
/** Save item (
|
|
125
|
+
/** Save item (published) - writes to cms/<collection>/<_filename>.json */
|
|
120
126
|
saveItem(collection: string, item: CMSItem): Promise<void>;
|
|
121
127
|
|
|
122
|
-
/**
|
|
128
|
+
/**
|
|
129
|
+
* Delete item by filename. Removes both the published file and any
|
|
130
|
+
* accompanying draft file.
|
|
131
|
+
*/
|
|
123
132
|
deleteItem(collection: string, filename: string): Promise<void>;
|
|
133
|
+
|
|
134
|
+
// ---- Draft-version methods --------------------------------------------
|
|
135
|
+
|
|
136
|
+
/** Get the draft version of an item, or null if no draft exists */
|
|
137
|
+
getDraft(collection: string, filename: string): Promise<CMSItem | null>;
|
|
138
|
+
|
|
139
|
+
/** List all drafts in a collection (used by Studio item list for badges) */
|
|
140
|
+
getAllDrafts(collection: string): Promise<CMSItem[]>;
|
|
141
|
+
|
|
142
|
+
/** Whether a draft file exists for the given filename */
|
|
143
|
+
hasDraft(collection: string, filename: string): Promise<boolean>;
|
|
144
|
+
|
|
145
|
+
/** Save the draft version of an item to cms/<collection>/<_filename>.draft.json */
|
|
146
|
+
saveDraft(collection: string, item: CMSItem): Promise<void>;
|
|
147
|
+
|
|
148
|
+
/** Discard the draft version of an item (no-op if no draft exists) */
|
|
149
|
+
discardDraft(collection: string, filename: string): Promise<void>;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Promote a draft to published. Reads `{filename}.draft.json`, writes its
|
|
153
|
+
* content to `{filename}.json`, then removes the draft file.
|
|
154
|
+
* Returns the newly published item. Throws if no draft exists.
|
|
155
|
+
*/
|
|
156
|
+
publishDraft(collection: string, filename: string): Promise<CMSItem>;
|
|
124
157
|
}
|
|
@@ -93,3 +93,19 @@ export const SAFE_IDENTIFIER_REGEX = /^[a-zA-Z0-9_-]+$/;
|
|
|
93
93
|
export function isValidIdentifier(name: string): boolean {
|
|
94
94
|
return SAFE_IDENTIFIER_REGEX.test(name);
|
|
95
95
|
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Suffix used for CMS draft files: `{filename}.draft.json`. The provider must
|
|
99
|
+
* never accept user-supplied filenames ending in `.draft`, because that would
|
|
100
|
+
* clash with the draft-suffix convention and let a user shadow another item's
|
|
101
|
+
* draft file.
|
|
102
|
+
*/
|
|
103
|
+
export const CMS_DRAFT_SUFFIX = '.draft';
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* True when a user-supplied CMS filename collides with the reserved `.draft`
|
|
107
|
+
* suffix. Use as a hard reject in CMS provider write paths.
|
|
108
|
+
*/
|
|
109
|
+
export function isReservedDraftFilename(filename: string): boolean {
|
|
110
|
+
return filename.endsWith(CMS_DRAFT_SUFFIX);
|
|
111
|
+
}
|