meno-core 1.1.4 → 1.1.6
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/chunks/{chunk-ZEDLSHYQ.js → chunk-GHNLTFCN.js} +44 -12
- package/dist/chunks/chunk-GHNLTFCN.js.map +7 -0
- package/dist/chunks/{chunk-UOF4MCAD.js → chunk-KX2LPIZZ.js} +607 -16
- package/dist/chunks/{chunk-UOF4MCAD.js.map → chunk-KX2LPIZZ.js.map} +3 -3
- package/dist/chunks/{chunk-FQBIC2OB.js → chunk-YMBUSHII.js} +1 -1
- package/dist/chunks/{chunk-FQBIC2OB.js.map → chunk-YMBUSHII.js.map} +1 -1
- package/dist/lib/client/index.js +56 -12
- package/dist/lib/client/index.js.map +2 -2
- package/dist/lib/server/index.js +121 -26
- package/dist/lib/server/index.js.map +3 -3
- package/dist/lib/shared/index.js +8 -2
- package/dist/lib/shared/index.js.map +1 -1
- package/lib/client/core/ComponentBuilder.test.ts +137 -0
- package/lib/client/core/ComponentBuilder.ts +52 -13
- package/lib/client/core/builders/embedBuilder.ts +15 -1
- package/lib/client/core/builders/linkNodeBuilder.ts +15 -1
- package/lib/client/core/builders/localeListBuilder.ts +15 -1
- package/lib/client/templateEngine.test.ts +106 -3
- package/lib/client/templateEngine.ts +50 -10
- package/lib/server/fileWatcher.test.ts +90 -0
- package/lib/server/fileWatcher.ts +36 -0
- package/lib/server/services/configService.ts +0 -5
- package/lib/server/ssr/htmlGenerator.test.ts +105 -3
- package/lib/server/ssr/htmlGenerator.ts +94 -42
- package/lib/server/ssr/ssrRenderer.test.ts +100 -0
- package/lib/server/ssr/ssrRenderer.ts +59 -14
- package/lib/shared/cssGeneration.test.ts +36 -0
- package/lib/shared/cssGeneration.ts +6 -1
- package/lib/shared/tailwindThemeScale.ts +1 -0
- package/lib/shared/templateStyleVars.ts +49 -0
- package/lib/shared/types/comment.ts +9 -3
- package/lib/shared/utilityClassConfig.ts +469 -10
- package/lib/shared/utilityClassMapper.test.ts +200 -2
- package/lib/shared/utilityClassMapper.ts +153 -0
- package/lib/shared/utilityClassNames.ts +90 -0
- package/package.json +1 -1
- package/dist/chunks/chunk-ZEDLSHYQ.js.map +0 -7
|
@@ -7,6 +7,9 @@ import {
|
|
|
7
7
|
splitStyleByClassability,
|
|
8
8
|
isClassableStyleValue,
|
|
9
9
|
styleHasMapping,
|
|
10
|
+
classMergeKey,
|
|
11
|
+
mergeInstanceClasses,
|
|
12
|
+
stripClassOverriddenStyleProps,
|
|
10
13
|
} from './utilityClassMapper';
|
|
11
14
|
import { getStyleValue, clearRegistry } from './styleValueRegistry';
|
|
12
15
|
import { defaultTailwindThemeVariables } from './tailwindThemeScale';
|
|
@@ -178,8 +181,10 @@ describe('utilityClassMapper', () => {
|
|
|
178
181
|
});
|
|
179
182
|
|
|
180
183
|
test('handles properties without a Tailwind root via arbitrary properties', () => {
|
|
181
|
-
|
|
182
|
-
|
|
184
|
+
// textOverflow graduated to the `text-ellipsis` static; clip-path still has no root.
|
|
185
|
+
const classes = stylesToClasses({ clipPath: 'circle(50%)' });
|
|
186
|
+
expect(classes).toContain('[clip-path:circle(50%)]');
|
|
187
|
+
expect(stylesToClasses({ textOverflow: 'ellipsis' })).toContain('text-ellipsis');
|
|
183
188
|
});
|
|
184
189
|
|
|
185
190
|
test('align-items stretch maps to a distinct class from flex-start', () => {
|
|
@@ -283,6 +288,122 @@ describe('utilityClassMapper', () => {
|
|
|
283
288
|
expect(classToStyle('translate-y-4')).toEqual({ prop: 'translate', value: '0 16px' });
|
|
284
289
|
});
|
|
285
290
|
|
|
291
|
+
test('filter functions wrap the value in their function', () => {
|
|
292
|
+
expect(classToStyle('blur-[70px]')).toEqual({ prop: 'filter', value: 'blur(70px)' });
|
|
293
|
+
expect(classToStyle('backdrop-blur-[8px]')).toEqual({ prop: 'backdropFilter', value: 'blur(8px)' });
|
|
294
|
+
expect(classToStyle('blur-(--glow)')).toEqual({ prop: 'filter', value: 'blur(var(--glow))' });
|
|
295
|
+
expect(classToStyle('drop-shadow-[0_4px_6px_#0003]')).toEqual({
|
|
296
|
+
prop: 'filter',
|
|
297
|
+
value: 'drop-shadow(0 4px 6px #0003)',
|
|
298
|
+
});
|
|
299
|
+
// Length/shadow named scales are deliberately not absorbed (no baked theme values).
|
|
300
|
+
expect(classToStyle('blur-sm')).toBeNull();
|
|
301
|
+
expect(classToStyle('drop-shadow-md')).toBeNull();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('filter numeric steps follow Tailwind per-function scales', () => {
|
|
305
|
+
expect(classToStyle('brightness-50')).toEqual({ prop: 'filter', value: 'brightness(0.5)' });
|
|
306
|
+
expect(classToStyle('contrast-125')).toEqual({ prop: 'filter', value: 'contrast(1.25)' });
|
|
307
|
+
expect(classToStyle('saturate-150')).toEqual({ prop: 'filter', value: 'saturate(1.5)' });
|
|
308
|
+
expect(classToStyle('grayscale-50')).toEqual({ prop: 'filter', value: 'grayscale(50%)' });
|
|
309
|
+
expect(classToStyle('invert-25')).toEqual({ prop: 'filter', value: 'invert(25%)' });
|
|
310
|
+
expect(classToStyle('hue-rotate-90')).toEqual({ prop: 'filter', value: 'hue-rotate(90deg)' });
|
|
311
|
+
expect(classToStyle('backdrop-opacity-50')).toEqual({ prop: 'backdropFilter', value: 'opacity(0.5)' });
|
|
312
|
+
// Bare 100% switches are statics.
|
|
313
|
+
expect(classToStyle('grayscale')).toEqual({ prop: 'filter', value: 'grayscale(100%)' });
|
|
314
|
+
expect(classToStyle('invert')).toEqual({ prop: 'filter', value: 'invert(100%)' });
|
|
315
|
+
expect(classToStyle('backdrop-sepia')).toEqual({ prop: 'backdropFilter', value: 'sepia(100%)' });
|
|
316
|
+
expect(classToStyle('filter-none')).toEqual({ prop: 'filter', value: 'none' });
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('single filter-function values emit the compact class and round-trip', () => {
|
|
320
|
+
expect(stylesToClasses({ filter: 'blur(70px)' })).toEqual(['blur-[70px]']);
|
|
321
|
+
expect(stylesToClasses({ filter: 'brightness(0.5)' })).toEqual(['brightness-50']);
|
|
322
|
+
expect(stylesToClasses({ filter: 'grayscale(100%)' })).toEqual(['grayscale']);
|
|
323
|
+
expect(stylesToClasses({ filter: 'grayscale(50%)' })).toEqual(['grayscale-50']);
|
|
324
|
+
expect(stylesToClasses({ filter: 'hue-rotate(90deg)' })).toEqual(['hue-rotate-90']);
|
|
325
|
+
expect(stylesToClasses({ filter: 'drop-shadow(0 4px 6px #0003)' })).toEqual(['drop-shadow-[0_4px_6px_#0003]']);
|
|
326
|
+
expect(stylesToClasses({ filter: 'saturate(var(--sat))' })).toEqual(['saturate-(--sat)']);
|
|
327
|
+
expect(stylesToClasses({ backdropFilter: 'blur(8px)' })).toEqual(['backdrop-blur-[8px]']);
|
|
328
|
+
expect(stylesToClasses({ backdropFilter: 'opacity(0.5)' })).toEqual(['backdrop-opacity-50']);
|
|
329
|
+
// Multi-function values have no compact spelling → arbitrary form on the filter root.
|
|
330
|
+
expect(stylesToClasses({ filter: 'blur(4px) brightness(0.5)' })).toEqual(['filter-[blur(4px)_brightness(0.5)]']);
|
|
331
|
+
expect(classesToStyles(['blur-[70px]'])).toEqual({ base: { filter: 'blur(70px)' } });
|
|
332
|
+
expect(classesToStyles(['brightness-50'])).toEqual({ base: { filter: 'brightness(0.5)' } });
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test('per-axis skew and scale compose into transform/scale', () => {
|
|
336
|
+
expect(classToStyle('skew-x-[10deg]')).toEqual({ prop: 'transform', value: 'skewX(10deg)' });
|
|
337
|
+
expect(classToStyle('skew-y-3')).toEqual({ prop: 'transform', value: 'skewY(3deg)' });
|
|
338
|
+
expect(classToStyle('scale-x-[1.2]')).toEqual({ prop: 'scale', value: '1.2 1' });
|
|
339
|
+
expect(classToStyle('scale-y-95')).toEqual({ prop: 'scale', value: '1 0.95' });
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test('side border-radius restores both corners on round-trip', () => {
|
|
343
|
+
expect(classesToStyles(['rounded-t-lg'])).toEqual({
|
|
344
|
+
base: { borderTopLeftRadius: 'var(--radius-lg)', borderTopRightRadius: 'var(--radius-lg)' },
|
|
345
|
+
});
|
|
346
|
+
expect(classesToStyles(['rounded-l-[8px]'])).toEqual({
|
|
347
|
+
base: { borderTopLeftRadius: '8px', borderBottomLeftRadius: '8px' },
|
|
348
|
+
});
|
|
349
|
+
expect(classesToStyles(['rounded-b-full'])).toEqual({
|
|
350
|
+
base: { borderBottomLeftRadius: '9999px', borderBottomRightRadius: '9999px' },
|
|
351
|
+
});
|
|
352
|
+
// Corner forms are unaffected (single corner, no mirror).
|
|
353
|
+
expect(classesToStyles(['rounded-tl-[8px]'])).toEqual({ base: { borderTopLeftRadius: '8px' } });
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test('long-tail Tailwind vocabulary (backgrounds, decoration, SVG, logical inset, scroll)', () => {
|
|
357
|
+
expect(classToStyle('bg-cover')).toEqual({ prop: 'backgroundSize', value: 'cover' });
|
|
358
|
+
expect(classToStyle('bg-clip-text')).toEqual({ prop: 'backgroundClip', value: 'text' });
|
|
359
|
+
expect(classToStyle('float-left')).toEqual({ prop: 'float', value: 'left' });
|
|
360
|
+
expect(classToStyle('text-ellipsis')).toEqual({ prop: 'textOverflow', value: 'ellipsis' });
|
|
361
|
+
expect(classToStyle('text-balance')).toEqual({ prop: 'textWrap', value: 'balance' });
|
|
362
|
+
// Shared `decoration` root disambiguates by value type; `stroke` likewise.
|
|
363
|
+
expect(classToStyle('decoration-2')).toEqual({ prop: 'textDecorationThickness', value: '2px' });
|
|
364
|
+
expect(classToStyle('decoration-[#f00]')).toEqual({ prop: 'textDecorationColor', value: '#f00' });
|
|
365
|
+
expect(classToStyle('decoration-wavy')).toEqual({ prop: 'textDecorationStyle', value: 'wavy' });
|
|
366
|
+
expect(classToStyle('underline-offset-2')).toEqual({ prop: 'textUnderlineOffset', value: '2px' });
|
|
367
|
+
expect(classToStyle('outline-1')).toEqual({ prop: 'outlineWidth', value: '1px' });
|
|
368
|
+
expect(classToStyle('outline-dashed')).toEqual({ prop: 'outlineStyle', value: 'dashed' });
|
|
369
|
+
expect(classToStyle('stroke-2')).toEqual({ prop: 'strokeWidth', value: '2' });
|
|
370
|
+
expect(classToStyle('stroke-[#f00]')).toEqual({ prop: 'stroke', value: '#f00' });
|
|
371
|
+
expect(classToStyle('fill-current')).toEqual({ prop: 'fill', value: 'currentColor' });
|
|
372
|
+
expect(classToStyle('caret-[#f00]')).toEqual({ prop: 'caretColor', value: '#f00' });
|
|
373
|
+
expect(classToStyle('start-0')).toEqual({ prop: 'insetInlineStart', value: '0px' });
|
|
374
|
+
expect(classToStyle('end-4')).toEqual({ prop: 'insetInlineEnd', value: '16px' });
|
|
375
|
+
expect(classToStyle('indent-4')).toEqual({ prop: 'textIndent', value: '16px' });
|
|
376
|
+
expect(classToStyle('border-spacing-2')).toEqual({ prop: 'borderSpacing', value: '8px' });
|
|
377
|
+
expect(classToStyle('scroll-mt-2')).toEqual({ prop: 'scrollMarginTop', value: '8px' });
|
|
378
|
+
expect(classToStyle('touch-pan-x')).toEqual({ prop: 'touchAction', value: 'pan-x' });
|
|
379
|
+
expect(classToStyle('overscroll-contain')).toEqual({ prop: 'overscrollBehavior', value: 'contain' });
|
|
380
|
+
expect(classToStyle('break-inside-avoid')).toEqual({ prop: 'breakInside', value: 'avoid' });
|
|
381
|
+
expect(classToStyle('auto-cols-fr')).toEqual({ prop: 'gridAutoColumns', value: 'minmax(0, 1fr)' });
|
|
382
|
+
expect(classToStyle('rounded-tr-xl')).toEqual({ prop: 'borderTopRightRadius', value: 'var(--radius-xl)' });
|
|
383
|
+
expect(classToStyle('caption-top')).toEqual({ prop: 'captionSide', value: 'top' });
|
|
384
|
+
expect(classToStyle('transform-gpu')).toEqual({ prop: 'transform', value: 'translateZ(0)' });
|
|
385
|
+
expect(classToStyle('animate-none')).toEqual({ prop: 'animation', value: 'none' });
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test('static/keyword additions parse and forward-map', () => {
|
|
389
|
+
expect(classToStyle('italic')).toEqual({ prop: 'fontStyle', value: 'italic' });
|
|
390
|
+
expect(classToStyle('isolate')).toEqual({ prop: 'isolation', value: 'isolate' });
|
|
391
|
+
expect(classToStyle('border-collapse')).toEqual({ prop: 'borderCollapse', value: 'collapse' });
|
|
392
|
+
expect(classToStyle('tabular-nums')).toEqual({ prop: 'fontVariantNumeric', value: 'tabular-nums' });
|
|
393
|
+
expect(classToStyle('snap-x')).toEqual({ prop: 'scrollSnapType', value: 'x mandatory' });
|
|
394
|
+
expect(classToStyle('mix-blend-multiply')).toEqual({ prop: 'mixBlendMode', value: 'multiply' });
|
|
395
|
+
expect(classToStyle('will-change-scroll')).toEqual({ prop: 'willChange', value: 'scroll-position' });
|
|
396
|
+
expect(classToStyle('columns-3')).toEqual({ prop: 'columns', value: '3' });
|
|
397
|
+
expect(classToStyle('h-dvh')).toEqual({ prop: 'height', value: '100dvh' });
|
|
398
|
+
// Read-side aliases resolve to the canonical value; forward keeps the canonical class.
|
|
399
|
+
expect(classToStyle('text-nowrap')).toEqual({ prop: 'whiteSpace', value: 'nowrap' });
|
|
400
|
+
expect(classToStyle('break-words')).toEqual({ prop: 'overflowWrap', value: 'break-word' });
|
|
401
|
+
expect(stylesToClasses({ whiteSpace: 'nowrap' })).toEqual(['whitespace-nowrap']);
|
|
402
|
+
expect(stylesToClasses({ mixBlendMode: 'multiply' })).toEqual(['mix-blend-multiply']);
|
|
403
|
+
expect(stylesToClasses({ columns: '3' })).toEqual(['columns-3']);
|
|
404
|
+
expect(stylesToClasses({ height: '100dvh' })).toEqual(['h-dvh']);
|
|
405
|
+
});
|
|
406
|
+
|
|
286
407
|
test('transition longhands compose; duration/ease override the shorthand', () => {
|
|
287
408
|
expect(classToStyle('duration-300')).toEqual({ prop: 'transitionDuration', value: '300ms' });
|
|
288
409
|
expect(classToStyle('delay-150')).toEqual({ prop: 'transitionDelay', value: '150ms' });
|
|
@@ -868,4 +989,81 @@ describe('utilityClassMapper', () => {
|
|
|
868
989
|
expect(styleHasMapping(undefined)).toBe(false);
|
|
869
990
|
});
|
|
870
991
|
});
|
|
992
|
+
|
|
993
|
+
describe('classMergeKey', () => {
|
|
994
|
+
test('same (breakpoint, property) → same key across value forms', () => {
|
|
995
|
+
expect(classMergeKey('max-w-full')).toBe(classMergeKey('max-w-[283px]'));
|
|
996
|
+
expect(classMergeKey('p-4')).toBe(classMergeKey('p-[13px]'));
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
test('breakpoint forms normalize to one key (max-lg / legacy tablet / bare lg)', () => {
|
|
1000
|
+
const key = classMergeKey('max-lg:p-4');
|
|
1001
|
+
expect(classMergeKey('tablet:p-[10px]')).toBe(key);
|
|
1002
|
+
expect(classMergeKey('lg:p-2.5')).toBe(key);
|
|
1003
|
+
expect(classMergeKey('p-4')).not.toBe(key); // base ≠ tablet
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
test('bare color-token class recovers the property from its root', () => {
|
|
1007
|
+
expect(classMergeKey('bg-accent')).toBe(classMergeKey('bg-[var(--gray-200)]'));
|
|
1008
|
+
expect(classMergeKey('text-muted')).toBe(classMergeKey('text-[#ff0000]'));
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
test('unrecognized classes (element hashes, foreign) yield null', () => {
|
|
1012
|
+
expect(classMergeKey('m_hero_ab12cd')).toBeNull();
|
|
1013
|
+
expect(classMergeKey('js-toggle')).toBeNull();
|
|
1014
|
+
});
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
describe('mergeInstanceClasses', () => {
|
|
1018
|
+
test('instance class drops the root class for the same property', () => {
|
|
1019
|
+
expect(mergeInstanceClasses(['max-w-full', 'flex'], 'max-w-[283px]')).toEqual(['flex', 'max-w-[283px]']);
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
test('conflicts are per-breakpoint — root tablet override survives a base instance class', () => {
|
|
1023
|
+
expect(mergeInstanceClasses(['max-w-full', 'max-lg:max-w-[90%]'], 'max-w-[283px]')).toEqual([
|
|
1024
|
+
'max-lg:max-w-[90%]',
|
|
1025
|
+
'max-w-[283px]',
|
|
1026
|
+
]);
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
test('null-key classes on either side are always kept', () => {
|
|
1030
|
+
expect(mergeInstanceClasses(['m_hero_ab12cd', 'p-4'], 'p-[24px] js-x')).toEqual([
|
|
1031
|
+
'm_hero_ab12cd',
|
|
1032
|
+
'p-[24px]',
|
|
1033
|
+
'js-x',
|
|
1034
|
+
]);
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
test('empty instance string is a no-op', () => {
|
|
1038
|
+
expect(mergeInstanceClasses(['p-4'], '')).toEqual(['p-4']);
|
|
1039
|
+
});
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
describe('stripClassOverriddenStyleProps', () => {
|
|
1043
|
+
test('drops the flat style property an instance class overrides', () => {
|
|
1044
|
+
expect(stripClassOverriddenStyleProps({ maxWidth: '100%', display: 'flex' }, 'max-w-[283px]')).toEqual({
|
|
1045
|
+
display: 'flex',
|
|
1046
|
+
});
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
test('drops per-breakpoint: base class strips base only, tablet override survives', () => {
|
|
1050
|
+
expect(
|
|
1051
|
+
stripClassOverriddenStyleProps({ base: { maxWidth: '100%' }, tablet: { maxWidth: '90%' } }, 'max-w-[283px]'),
|
|
1052
|
+
).toEqual({ tablet: { maxWidth: '90%' } });
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
test('unresolved bindings are stripped by property key too', () => {
|
|
1056
|
+
const mapping = { _mapping: true as const, prop: 'maxWidth', values: { narrow: '283px' } };
|
|
1057
|
+
expect(stripClassOverriddenStyleProps({ maxWidth: mapping, color: 'red' }, 'max-w-[283px]')).toEqual({
|
|
1058
|
+
color: 'red',
|
|
1059
|
+
});
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
test('returns the same object when nothing conflicts, null when nothing remains', () => {
|
|
1063
|
+
const style = { maxWidth: '100%' };
|
|
1064
|
+
expect(stripClassOverriddenStyleProps(style, 'p-[24px]')).toBe(style);
|
|
1065
|
+
expect(stripClassOverriddenStyleProps(style, 'max-w-[283px]')).toBeNull();
|
|
1066
|
+
expect(stripClassOverriddenStyleProps(null, 'max-w-[283px]')).toBeNull();
|
|
1067
|
+
});
|
|
1068
|
+
});
|
|
871
1069
|
});
|
|
@@ -21,6 +21,10 @@ import {
|
|
|
21
21
|
borderPresets,
|
|
22
22
|
SPACING_SCALE_ROOTS,
|
|
23
23
|
SPACING_SCALE_REVERSE,
|
|
24
|
+
FILTER_FN_ROOTS,
|
|
25
|
+
FILTER_ROOT_BY_PROP_FN,
|
|
26
|
+
filterArgToStep,
|
|
27
|
+
ROUNDED_SIDE_CORNERS,
|
|
24
28
|
} from './utilityClassConfig';
|
|
25
29
|
import {
|
|
26
30
|
camelToKebab,
|
|
@@ -168,6 +172,32 @@ function propertyValueToClass(prop: string, value: string | number): string | nu
|
|
|
168
172
|
|
|
169
173
|
let className: string | null = null;
|
|
170
174
|
|
|
175
|
+
// Tailwind filter functions: a filter/backdrop-filter value that is a SINGLE function call
|
|
176
|
+
// emits the compact functional class — `blur(70px)` → `blur-[70px]`, `brightness(0.5)` →
|
|
177
|
+
// `brightness-50`, `saturate(var(--x))` → `saturate-(--x)` — mirroring the parser's filter
|
|
178
|
+
// branch so hand-authored filter classes survive a style round-trip. Multi-function values
|
|
179
|
+
// (`blur(4px) brightness(.5)`) and nested-paren args have no compact spelling → arbitrary form.
|
|
180
|
+
// (The exact 100% switches — `grayscale(100%)` → bare `grayscale` — hit the static table above.)
|
|
181
|
+
if (prop === 'filter' || prop === 'backdropFilter') {
|
|
182
|
+
const property = camelToKebab(prop);
|
|
183
|
+
const varForm = stringValue.match(/^([a-z-]+)\(\s*var\((--[\w-]+)\)\s*\)$/);
|
|
184
|
+
const plainForm = varForm ? null : stringValue.match(/^([a-z-]+)\(([^()]+)\)$/);
|
|
185
|
+
const fnRoot = FILTER_ROOT_BY_PROP_FN.get(`${property}|${(varForm ?? plainForm)?.[1]}`);
|
|
186
|
+
if (fnRoot && varForm) {
|
|
187
|
+
className = `${fnRoot}-(${varForm[2]})`;
|
|
188
|
+
} else if (fnRoot && plainForm) {
|
|
189
|
+
const arg = (plainForm[2] ?? '').trim();
|
|
190
|
+
const scale = FILTER_FN_ROOTS[fnRoot]?.scale;
|
|
191
|
+
const step = scale ? filterArgToStep(scale, arg) : null;
|
|
192
|
+
if (step != null) {
|
|
193
|
+
className = `${fnRoot}-${step}`;
|
|
194
|
+
} else {
|
|
195
|
+
const encoded = encodeArbitraryValue(arg);
|
|
196
|
+
if (encoded) className = `${fnRoot}-[${encoded}]`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
171
201
|
// Named keyword values: width: 100% → w-full, margin: auto → m-auto
|
|
172
202
|
const kwTable = keywordValues[root];
|
|
173
203
|
if (kwTable) {
|
|
@@ -554,6 +584,13 @@ export function classesToStyles(classes: string[], knownTokens?: ReadonlySet<str
|
|
|
554
584
|
if (cleanClass.startsWith('size-') && styleEntry.prop === 'width') {
|
|
555
585
|
styles[breakpoint]!.height = styleEntry.value;
|
|
556
586
|
}
|
|
587
|
+
// Side border-radius (`rounded-t-*`) covers TWO corners with no CSS shorthand — classToStyle
|
|
588
|
+
// returns the first corner; mirror the value onto the second (same pattern as `size-*`).
|
|
589
|
+
const sideRoot = cleanClass.match(/^(rounded-[trbl])-/)?.[1];
|
|
590
|
+
const corners = sideRoot ? ROUNDED_SIDE_CORNERS[sideRoot] : undefined;
|
|
591
|
+
if (corners) {
|
|
592
|
+
styles[breakpoint]![cssPropertyToCamel(corners[1] ?? '')] = styleEntry.value;
|
|
593
|
+
}
|
|
557
594
|
}
|
|
558
595
|
}
|
|
559
596
|
|
|
@@ -571,6 +608,122 @@ export function classesToStyles(classes: string[], knownTokens?: ReadonlySet<str
|
|
|
571
608
|
return hasAny ? styles : null;
|
|
572
609
|
}
|
|
573
610
|
|
|
611
|
+
/**
|
|
612
|
+
* Color class root → its camelCase CSS property (the color half of `COLOR_TOKEN_ROOTS`).
|
|
613
|
+
* A bare color-token class (`bg-accent`, `text-muted`) can't be resolved by `classToStyle`
|
|
614
|
+
* without the project token set, but its PROPERTY is unambiguous from the root — enough to
|
|
615
|
+
* detect a conflict with another class targeting the same property.
|
|
616
|
+
*/
|
|
617
|
+
const COLOR_ROOT_TO_PROP: Record<string, string> = {
|
|
618
|
+
text: 'color',
|
|
619
|
+
bg: 'backgroundColor',
|
|
620
|
+
border: 'borderColor',
|
|
621
|
+
accent: 'accentColor',
|
|
622
|
+
outline: 'outlineColor',
|
|
623
|
+
decoration: 'textDecorationColor',
|
|
624
|
+
caret: 'caretColor',
|
|
625
|
+
fill: 'fill',
|
|
626
|
+
stroke: 'stroke',
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Merge key for one utility class: normalized breakpoint + the camelCase CSS property the
|
|
631
|
+
* class decodes to. Two classes with the same key style the same thing — only one should
|
|
632
|
+
* survive a merge. `null` when the class isn't a recognized utility (element `m_…` hash
|
|
633
|
+
* classes, foreign classes) — those never participate in conflict detection and are kept.
|
|
634
|
+
* Mirror of the meno-astro runtime's `classMergeKey` (runtime/style.ts), which is inlined
|
|
635
|
+
* there to keep the published runtime off new meno-core exports.
|
|
636
|
+
*/
|
|
637
|
+
export function classMergeKey(className: string): string | null {
|
|
638
|
+
const { breakpoint, base } = splitVariantPrefix(className);
|
|
639
|
+
// Every breakpoint form shares a key — an instance's `max-lg:p-4` must override a root's
|
|
640
|
+
// `max-tablet:p-4` or legacy `tablet:p-4`. Normalize strips `max-` and folds the Tailwind
|
|
641
|
+
// alias under it; fold the bare alias too so `lg:`/`sm:` (unprefixed) also land on identity.
|
|
642
|
+
let bp = normalizeBreakpointVariant(breakpoint) ?? '';
|
|
643
|
+
if (bp === 'lg') bp = 'tablet';
|
|
644
|
+
else if (bp === 'sm') bp = 'mobile';
|
|
645
|
+
const entry = classToStyle(className);
|
|
646
|
+
if (entry) return `${bp}|${entry.prop}`;
|
|
647
|
+
// Bare color-token class — recover the property from the root so an instance's
|
|
648
|
+
// `bg-[var(--gray-200)]` still conflicts with a root's `bg-accent`.
|
|
649
|
+
const dash = base.indexOf('-');
|
|
650
|
+
if (dash > 0) {
|
|
651
|
+
const prop = COLOR_ROOT_TO_PROP[base.slice(0, dash)];
|
|
652
|
+
if (prop && /^[a-zA-Z][\w-]*$/.test(base.slice(dash + 1))) return `${bp}|${prop}`;
|
|
653
|
+
}
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Merge a component instance's class string over the root element's own classes.
|
|
659
|
+
* Instance wins per (breakpoint, CSS property): a root class whose merge key collides with
|
|
660
|
+
* an instance class is dropped, everything else (element classes, foreign classes) is kept.
|
|
661
|
+
* Both classes have equal specificity in the generated stylesheet, so without this the
|
|
662
|
+
* WINNER would be stylesheet order — i.e. class collection order, not the instance.
|
|
663
|
+
* Mirror of the meno-astro runtime's `mergeInstanceClasses`, so Meno Select agrees with a
|
|
664
|
+
* real Astro build on which value applies.
|
|
665
|
+
*/
|
|
666
|
+
export function mergeInstanceClasses(own: string[], instanceClass: string): string[] {
|
|
667
|
+
const instance = instanceClass.split(/\s+/).filter(Boolean);
|
|
668
|
+
if (instance.length === 0) return own;
|
|
669
|
+
const overridden = new Set(instance.map(classMergeKey).filter((k): k is string => k !== null));
|
|
670
|
+
const kept = own.filter((cls) => {
|
|
671
|
+
const key = classMergeKey(cls);
|
|
672
|
+
return key === null || !overridden.has(key);
|
|
673
|
+
});
|
|
674
|
+
return [...kept, ...instance];
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Drop from a root's style object every (breakpoint, property) an instance class string
|
|
679
|
+
* overrides. A prop-bound root style (`maxWidth: {_mapping}` → resolved `100%`) becomes a
|
|
680
|
+
* utility class (`max-w-full`) at render time — the class channel can't see it, so an
|
|
681
|
+
* instance's `max-w-[283px]` would fight it on stylesheet order. Stripping the property
|
|
682
|
+
* before class generation is the style-object half of `mergeInstanceClasses` (the astro
|
|
683
|
+
* runtime's `inlineStyle()` does the same drop for its prop-bound inline declarations).
|
|
684
|
+
* Pure: returns the input object untouched when nothing conflicts, a new object otherwise,
|
|
685
|
+
* `null` when nothing remains.
|
|
686
|
+
*/
|
|
687
|
+
export function stripClassOverriddenStyleProps(
|
|
688
|
+
style: StyleValue | null | undefined,
|
|
689
|
+
instanceClass: string,
|
|
690
|
+
): StyleValue | null {
|
|
691
|
+
if (!style || typeof style !== 'object') return null;
|
|
692
|
+
const overridden = new Set(
|
|
693
|
+
instanceClass
|
|
694
|
+
.split(/\s+/)
|
|
695
|
+
.filter(Boolean)
|
|
696
|
+
.map(classMergeKey)
|
|
697
|
+
.filter((k): k is string => k !== null),
|
|
698
|
+
);
|
|
699
|
+
if (overridden.size === 0) return style;
|
|
700
|
+
|
|
701
|
+
const stripBucket = (bucket: StyleObject, bp: string): StyleObject => {
|
|
702
|
+
const out: StyleObject = {};
|
|
703
|
+
for (const [prop, value] of Object.entries(bucket)) {
|
|
704
|
+
if (!overridden.has(`${bp}|${prop}`)) out[prop] = value as StyleObject[string];
|
|
705
|
+
}
|
|
706
|
+
return out;
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
if (isResponsiveStyle(style)) {
|
|
710
|
+
const out: ResponsiveStyleObject = {};
|
|
711
|
+
let changed = false;
|
|
712
|
+
for (const [bpName, bucket] of Object.entries(style)) {
|
|
713
|
+
if (!bucket || typeof bucket !== 'object') continue;
|
|
714
|
+
const stripped = stripBucket(bucket as StyleObject, bpName === 'base' ? '' : bpName);
|
|
715
|
+
if (Object.keys(stripped).length !== Object.keys(bucket).length) changed = true;
|
|
716
|
+
if (Object.keys(stripped).length > 0) out[bpName] = stripped;
|
|
717
|
+
}
|
|
718
|
+
if (!changed) return style;
|
|
719
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const flat = stripBucket(style as StyleObject, '');
|
|
723
|
+
if (Object.keys(flat).length === Object.keys(style as StyleObject).length) return style;
|
|
724
|
+
return Object.keys(flat).length > 0 ? flat : null;
|
|
725
|
+
}
|
|
726
|
+
|
|
574
727
|
/**
|
|
575
728
|
* Whether a single style value can be stored as a STATIC utility class. Static strings (no
|
|
576
729
|
* `{{template}}`) and numbers can; template-bearing strings and `_mapping` objects cannot — a
|
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
EASE_TIMING,
|
|
32
32
|
TRANSITION_PROPERTY_VALUES,
|
|
33
33
|
TRANSITION_DEFAULT_TIMING,
|
|
34
|
+
FILTER_FN_ROOTS,
|
|
35
|
+
filterStepToArg,
|
|
34
36
|
} from './utilityClassConfig';
|
|
35
37
|
import { isCssNamedColor } from './cssNamedColors';
|
|
36
38
|
import { THEME_SCALE_BY_CLASS, themeScaleValue } from './tailwindThemeScale';
|
|
@@ -252,6 +254,10 @@ export function resolveRootProperty(root: string, value: string): string {
|
|
|
252
254
|
if (isLengthValue(value)) return 'outline-width';
|
|
253
255
|
return 'outline';
|
|
254
256
|
}
|
|
257
|
+
case 'decoration':
|
|
258
|
+
return isLengthValue(value) || value === 'from-font' ? 'text-decoration-thickness' : 'text-decoration-color';
|
|
259
|
+
case 'stroke':
|
|
260
|
+
return isColorValue(value) ? 'stroke' : 'stroke-width';
|
|
255
261
|
default:
|
|
256
262
|
return prefixToCSSProperty[root] ?? root;
|
|
257
263
|
}
|
|
@@ -521,6 +527,62 @@ export function parseUtilityClass(className: string, knownTokens?: ReadonlySet<s
|
|
|
521
527
|
}
|
|
522
528
|
|
|
523
529
|
function parseRootValue(root: string, rest: string, knownTokens?: ReadonlySet<string>): ParsedUtilityClass | null {
|
|
530
|
+
// Tailwind filter functions: the class value is an ARGUMENT to the function, not the property
|
|
531
|
+
// value itself — `blur-[70px]` → filter: blur(70px), `brightness-50` → filter: brightness(0.5),
|
|
532
|
+
// `backdrop-saturate-(--x)` → backdrop-filter: saturate(var(--x)). Must run before the generic
|
|
533
|
+
// arbitrary/var branches, which would assign the bare argument to filter (invalid CSS). Numeric
|
|
534
|
+
// steps follow Tailwind's per-function scale (see FILTER_FN_ROOTS); the length/shadow named
|
|
535
|
+
// scales (`blur-sm`, `drop-shadow-md`) are deliberately not absorbed — use the arbitrary form.
|
|
536
|
+
const filterFn = FILTER_FN_ROOTS[root];
|
|
537
|
+
if (filterFn) {
|
|
538
|
+
const { property, fn, scale } = filterFn;
|
|
539
|
+
if (rest.startsWith('[') && rest.endsWith(']') && rest.length > 2) {
|
|
540
|
+
const arg = decodeArbitraryValue(rest.slice(1, -1));
|
|
541
|
+
return { property, value: `${fn}(${arg})`, root, kind: 'arbitrary' };
|
|
542
|
+
}
|
|
543
|
+
const varArg = rest.match(/^\((--[\w-]+)\)$/);
|
|
544
|
+
if (varArg) {
|
|
545
|
+
return { property, value: `${fn}(var(${varArg[1]}))`, root, kind: 'var' };
|
|
546
|
+
}
|
|
547
|
+
if (scale && /^\d+(\.\d+)?$/.test(rest)) {
|
|
548
|
+
return { property, value: `${fn}(${filterStepToArg(scale, rest)})`, root, kind: 'numeric' };
|
|
549
|
+
}
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Per-axis skew (`skew-x-[10deg]`, `skew-y-3`): CSS has no individual skew property, so the
|
|
554
|
+
// value is wrapped into transform's skewX()/skewY(). Like Tailwind's numeric scale, a bare
|
|
555
|
+
// number is degrees. NB two skew classes on one element clobber each other (last `transform`
|
|
556
|
+
// wins) — same single-property limitation as translate-x/translate-y above.
|
|
557
|
+
if (root === 'skew-x' || root === 'skew-y') {
|
|
558
|
+
const fn = root === 'skew-x' ? 'skewX' : 'skewY';
|
|
559
|
+
if (rest.startsWith('[') && rest.endsWith(']') && rest.length > 2) {
|
|
560
|
+
return {
|
|
561
|
+
property: 'transform',
|
|
562
|
+
value: `${fn}(${decodeArbitraryValue(rest.slice(1, -1))})`,
|
|
563
|
+
root,
|
|
564
|
+
kind: 'arbitrary',
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
if (/^\d+(\.\d+)?$/.test(rest)) {
|
|
568
|
+
return { property: 'transform', value: `${fn}(${rest}deg)`, root, kind: 'numeric' };
|
|
569
|
+
}
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Per-axis scale (`scale-x-[1.2]`, `scale-y-95`): composed into the two-value `scale`
|
|
574
|
+
// property with the other axis at 1. Numeric steps are Tailwind's percentage-of-100.
|
|
575
|
+
if (root === 'scale-x' || root === 'scale-y') {
|
|
576
|
+
const pair = (v: string) => (root === 'scale-x' ? `${v} 1` : `1 ${v}`);
|
|
577
|
+
if (rest.startsWith('[') && rest.endsWith(']') && rest.length > 2) {
|
|
578
|
+
return { property: 'scale', value: pair(decodeArbitraryValue(rest.slice(1, -1))), root, kind: 'arbitrary' };
|
|
579
|
+
}
|
|
580
|
+
if (/^\d+$/.test(rest)) {
|
|
581
|
+
return { property: 'scale', value: pair(String(Number(rest) / 100)), root, kind: 'numeric' };
|
|
582
|
+
}
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
|
|
524
586
|
// Arbitrary value: root-[value]
|
|
525
587
|
if (rest.startsWith('[') && rest.endsWith(']') && rest.length > 2) {
|
|
526
588
|
const value = decodeArbitraryValue(rest.slice(1, -1));
|
|
@@ -546,6 +608,22 @@ function parseRootValue(root: string, rest: string, knownTokens?: ReadonlySet<st
|
|
|
546
608
|
return { property, value, root, kind: 'var' };
|
|
547
609
|
}
|
|
548
610
|
|
|
611
|
+
// Side/corner border-radius NAMED forms (`rounded-t-lg`, `rounded-tr-xl`, `rounded-l-full`):
|
|
612
|
+
// resolve the value exactly like the plain `rounded` root — theme scale → var(--radius-lg),
|
|
613
|
+
// keywords → px. Bracket/var forms are already handled by the generic branches above (the
|
|
614
|
+
// prefix map carries the property); the CSS generator expands side roots to both corners.
|
|
615
|
+
if (root.startsWith('rounded-') && prefixToCSSProperty[root]) {
|
|
616
|
+
const themed = THEME_SCALE_BY_CLASS.get(`rounded-${rest}`);
|
|
617
|
+
if (themed) {
|
|
618
|
+
return { property: prefixToCSSProperty[root] ?? root, value: themeScaleValue(themed), root, kind: 'var' };
|
|
619
|
+
}
|
|
620
|
+
const kw = keywordValues[root]?.[rest] ?? keywordValues.rounded?.[rest];
|
|
621
|
+
if (kw !== undefined) {
|
|
622
|
+
return { property: prefixToCSSProperty[root] ?? root, value: kw, root, kind: 'keyword' };
|
|
623
|
+
}
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
|
|
549
627
|
// Border width/style family (`border-2`, `border-b`, `border-t-4`, `border-solid`, …).
|
|
550
628
|
// Placed after the arbitrary `[..]` and var `(--x)` escapes so those win; colors
|
|
551
629
|
// (`border-primary`) return null here and fall through to the color-token branch below.
|
|
@@ -614,6 +692,18 @@ function parseRootValue(root: string, rest: string, knownTokens?: ReadonlySet<st
|
|
|
614
692
|
if ((root === 'duration' || root === 'delay') && /^\d+$/.test(rest)) {
|
|
615
693
|
return { property: prefixToCSSProperty[root] ?? root, value: `${rest}ms`, root, kind: 'numeric' };
|
|
616
694
|
}
|
|
695
|
+
// Numeric px widths/offsets: `outline-1` → outline-width 1px, `underline-offset-2` → 2px,
|
|
696
|
+
// `decoration-2` → text-decoration-thickness 2px. (`stroke-2` stays unitless — SVG stroke-width.)
|
|
697
|
+
if ((root === 'outline' || root === 'outline-offset' || root === 'underline-offset') && /^\d+$/.test(rest)) {
|
|
698
|
+
const property = root === 'outline' ? 'outline-width' : (prefixToCSSProperty[root] ?? root);
|
|
699
|
+
return { property, value: `${rest}px`, root, kind: 'numeric' };
|
|
700
|
+
}
|
|
701
|
+
if (root === 'decoration' && /^\d+$/.test(rest)) {
|
|
702
|
+
return { property: 'text-decoration-thickness', value: `${rest}px`, root, kind: 'numeric' };
|
|
703
|
+
}
|
|
704
|
+
if (root === 'stroke' && /^\d+$/.test(rest)) {
|
|
705
|
+
return { property: 'stroke-width', value: rest, root, kind: 'numeric' };
|
|
706
|
+
}
|
|
617
707
|
// `ease-in-out` → transition-timing-function: cubic-bezier(…).
|
|
618
708
|
if (root === 'ease') {
|
|
619
709
|
const timing = EASE_TIMING[rest];
|