meno-core 1.1.0 → 1.1.2
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-NUP7H7D3.js → chunk-7ZLF4NE5.js} +2 -2
- package/dist/chunks/{chunk-QWTQZHG3.js → chunk-FQBIC2OB.js} +1 -1
- package/dist/chunks/chunk-FQBIC2OB.js.map +7 -0
- package/dist/chunks/{chunk-2IIQK7T3.js → chunk-J4IPTP5X.js} +393 -3
- package/dist/chunks/{chunk-2IIQK7T3.js.map → chunk-J4IPTP5X.js.map} +3 -3
- package/dist/lib/client/index.js +2 -2
- package/dist/lib/server/index.js +804 -218
- package/dist/lib/server/index.js.map +4 -4
- package/dist/lib/shared/index.js +6 -2
- package/dist/lib/shared/index.js.map +2 -2
- package/lib/server/index.ts +19 -0
- package/lib/server/services/ColorService.ts +38 -7
- package/lib/server/services/VariableService.ts +34 -5
- package/lib/server/ssr/htmlGenerator.ts +18 -2
- package/lib/server/ssr/ssrRenderer.test.ts +25 -0
- package/lib/server/ssr/ssrRenderer.ts +12 -2
- package/lib/server/themeCssCodec.test.ts +154 -0
- package/lib/server/themeCssCodec.ts +488 -0
- package/lib/server/themeCssStore.test.ts +139 -0
- package/lib/server/themeCssStore.ts +210 -0
- package/lib/shared/cssGeneration.test.ts +10 -0
- package/lib/shared/cssGeneration.ts +10 -8
- package/lib/shared/index.ts +6 -0
- package/lib/shared/interactiveStyles.test.ts +3 -1
- package/lib/shared/tailwindThemeScale.ts +256 -0
- package/lib/shared/types/colors.ts +6 -2
- package/lib/shared/types/variables.ts +14 -7
- package/lib/shared/utilityClassConfig.ts +116 -0
- package/lib/shared/utilityClassMapper.test.ts +148 -0
- package/lib/shared/utilityClassMapper.ts +33 -0
- package/lib/shared/utilityClassNames.ts +146 -0
- package/package.json +1 -1
- package/dist/chunks/chunk-QWTQZHG3.js.map +0 -7
- /package/dist/chunks/{chunk-NUP7H7D3.js.map → chunk-7ZLF4NE5.js.map} +0 -0
|
@@ -94,6 +94,9 @@ export const propertyOrder: string[] = [
|
|
|
94
94
|
// Transform & Effects
|
|
95
95
|
'opacity',
|
|
96
96
|
'transform',
|
|
97
|
+
'translate',
|
|
98
|
+
'rotate',
|
|
99
|
+
'scale',
|
|
97
100
|
'transformOrigin',
|
|
98
101
|
'boxShadow',
|
|
99
102
|
'textShadow',
|
|
@@ -109,6 +112,8 @@ export const propertyOrder: string[] = [
|
|
|
109
112
|
'bottom',
|
|
110
113
|
'left',
|
|
111
114
|
'inset',
|
|
115
|
+
'insetInline',
|
|
116
|
+
'insetBlock',
|
|
112
117
|
'zIndex',
|
|
113
118
|
// Grid Layout
|
|
114
119
|
'gridTemplateColumns',
|
|
@@ -117,7 +122,11 @@ export const propertyOrder: string[] = [
|
|
|
117
122
|
'gridGap',
|
|
118
123
|
'gridAutoFlow',
|
|
119
124
|
'gridColumn',
|
|
125
|
+
'gridColumnStart',
|
|
126
|
+
'gridColumnEnd',
|
|
120
127
|
'gridRow',
|
|
128
|
+
'gridRowStart',
|
|
129
|
+
'gridRowEnd',
|
|
121
130
|
'gridArea',
|
|
122
131
|
'gridAutoRows',
|
|
123
132
|
'gridAutoColumns',
|
|
@@ -146,6 +155,10 @@ export const propertyOrder: string[] = [
|
|
|
146
155
|
'pointerEvents',
|
|
147
156
|
'userSelect',
|
|
148
157
|
'transition',
|
|
158
|
+
'transitionProperty',
|
|
159
|
+
'transitionDuration',
|
|
160
|
+
'transitionTimingFunction',
|
|
161
|
+
'transitionDelay',
|
|
149
162
|
'objectFit',
|
|
150
163
|
'objectPosition',
|
|
151
164
|
'boxSizing',
|
|
@@ -308,6 +321,7 @@ export const prefixToCSSProperty: Record<string, string> = {
|
|
|
308
321
|
// Sizing
|
|
309
322
|
w: 'width',
|
|
310
323
|
h: 'height',
|
|
324
|
+
size: 'width', // multi-prop (width + height) — expanded in generateRuleForClass / classesToStyles
|
|
311
325
|
'max-w': 'max-width',
|
|
312
326
|
'max-h': 'max-height',
|
|
313
327
|
'min-w': 'min-width',
|
|
@@ -352,14 +366,28 @@ export const prefixToCSSProperty: Record<string, string> = {
|
|
|
352
366
|
'grid-cols': 'grid-template-columns',
|
|
353
367
|
'grid-rows': 'grid-template-rows',
|
|
354
368
|
col: 'grid-column',
|
|
369
|
+
'col-span': 'grid-column',
|
|
370
|
+
'col-start': 'grid-column-start',
|
|
371
|
+
'col-end': 'grid-column-end',
|
|
355
372
|
row: 'grid-row',
|
|
373
|
+
'row-span': 'grid-row',
|
|
374
|
+
'row-start': 'grid-row-start',
|
|
375
|
+
'row-end': 'grid-row-end',
|
|
356
376
|
'auto-rows': 'grid-auto-rows',
|
|
357
377
|
'auto-cols': 'grid-auto-columns',
|
|
358
378
|
|
|
359
379
|
// Effects
|
|
360
380
|
opacity: 'opacity',
|
|
361
381
|
transform: 'transform',
|
|
382
|
+
scale: 'scale',
|
|
383
|
+
rotate: 'rotate',
|
|
384
|
+
'translate-x': 'translate',
|
|
385
|
+
'translate-y': 'translate',
|
|
362
386
|
origin: 'transform-origin',
|
|
387
|
+
// Transitions (each maps to a distinct longhand → compose across classes)
|
|
388
|
+
duration: 'transition-duration',
|
|
389
|
+
delay: 'transition-delay',
|
|
390
|
+
ease: 'transition-timing-function',
|
|
363
391
|
shadow: 'box-shadow',
|
|
364
392
|
'text-shadow': 'text-shadow',
|
|
365
393
|
filter: 'filter',
|
|
@@ -371,6 +399,8 @@ export const prefixToCSSProperty: Record<string, string> = {
|
|
|
371
399
|
bottom: 'bottom',
|
|
372
400
|
left: 'left',
|
|
373
401
|
inset: 'inset',
|
|
402
|
+
'inset-x': 'inset-inline',
|
|
403
|
+
'inset-y': 'inset-block',
|
|
374
404
|
z: 'z-index',
|
|
375
405
|
|
|
376
406
|
// Outline
|
|
@@ -757,8 +787,11 @@ export const SPACING_SCALE_ROOTS: Set<string> = new Set([
|
|
|
757
787
|
'bottom',
|
|
758
788
|
'left',
|
|
759
789
|
'inset',
|
|
790
|
+
'inset-x',
|
|
791
|
+
'inset-y',
|
|
760
792
|
'w',
|
|
761
793
|
'h',
|
|
794
|
+
'size',
|
|
762
795
|
'min-w',
|
|
763
796
|
'min-h',
|
|
764
797
|
'max-w',
|
|
@@ -796,6 +829,7 @@ export const keywordValues: Record<string, Record<string, string>> = {
|
|
|
796
829
|
h: { auto: 'auto', full: '100%', screen: '100vh', fit: 'fit-content', min: 'min-content', max: 'max-content' },
|
|
797
830
|
'max-w': { full: '100%', none: 'none', screen: '100vw', fit: 'fit-content', min: 'min-content', max: 'max-content' },
|
|
798
831
|
'max-h': { full: '100%', none: 'none', screen: '100vh', fit: 'fit-content', min: 'min-content', max: 'max-content' },
|
|
832
|
+
size: { auto: 'auto', full: '100%', fit: 'fit-content', min: 'min-content', max: 'max-content' },
|
|
799
833
|
'min-w': { auto: 'auto', full: '100%', fit: 'fit-content', min: 'min-content', max: 'max-content' },
|
|
800
834
|
'min-h': { auto: 'auto', full: '100%', screen: '100vh', fit: 'fit-content', min: 'min-content', max: 'max-content' },
|
|
801
835
|
m: { auto: 'auto' },
|
|
@@ -819,6 +853,10 @@ export const keywordValues: Record<string, Record<string, string>> = {
|
|
|
819
853
|
z: { auto: 'auto' },
|
|
820
854
|
leading: { normal: 'normal' },
|
|
821
855
|
tracking: { normal: 'normal' },
|
|
856
|
+
'inset-x': { auto: 'auto', full: '100%' },
|
|
857
|
+
'inset-y': { auto: 'auto', full: '100%' },
|
|
858
|
+
aspect: { auto: 'auto', square: '1 / 1', video: '16 / 9' },
|
|
859
|
+
order: { first: '-9999', last: '9999', none: '0' },
|
|
822
860
|
};
|
|
823
861
|
|
|
824
862
|
/**
|
|
@@ -846,6 +884,84 @@ export const colorTokenProps: Set<string> = new Set([
|
|
|
846
884
|
'outlineColor',
|
|
847
885
|
]);
|
|
848
886
|
|
|
887
|
+
/**
|
|
888
|
+
* Roots that accept Tailwind fractional values (`w-1/2` → `50%`, `top-1/3` → `33.333333%`).
|
|
889
|
+
* Tailwind resolves `n/d` to `calc(n / d * 100%)` (pre-computed here to a plain percentage so
|
|
890
|
+
* the value round-trips and renders without a calc). Sizing, position/inset, flex-basis and the
|
|
891
|
+
* individual translate axes take fractions.
|
|
892
|
+
*/
|
|
893
|
+
export const FRACTION_ROOTS: Set<string> = new Set([
|
|
894
|
+
'w',
|
|
895
|
+
'h',
|
|
896
|
+
'size',
|
|
897
|
+
'min-w',
|
|
898
|
+
'min-h',
|
|
899
|
+
'max-w',
|
|
900
|
+
'max-h',
|
|
901
|
+
'basis',
|
|
902
|
+
'top',
|
|
903
|
+
'right',
|
|
904
|
+
'bottom',
|
|
905
|
+
'left',
|
|
906
|
+
'inset',
|
|
907
|
+
'inset-x',
|
|
908
|
+
'inset-y',
|
|
909
|
+
'translate-x',
|
|
910
|
+
'translate-y',
|
|
911
|
+
]);
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Class roots whose value is negatable with a leading `-` (`-mt-4`, `-top-2`, `-translate-x-1/2`,
|
|
915
|
+
* `-rotate-3`). Mirrors Tailwind: margins, position/inset, translate and rotate. The inverse parser
|
|
916
|
+
* strips the `-`, resolves the positive class, then negates the numeric/length/percentage value.
|
|
917
|
+
*/
|
|
918
|
+
export const NEGATABLE_ROOTS: Set<string> = new Set([
|
|
919
|
+
'm',
|
|
920
|
+
'mt',
|
|
921
|
+
'mr',
|
|
922
|
+
'mb',
|
|
923
|
+
'ml',
|
|
924
|
+
'mx',
|
|
925
|
+
'my',
|
|
926
|
+
'top',
|
|
927
|
+
'right',
|
|
928
|
+
'bottom',
|
|
929
|
+
'left',
|
|
930
|
+
'inset',
|
|
931
|
+
'inset-x',
|
|
932
|
+
'inset-y',
|
|
933
|
+
'translate-x',
|
|
934
|
+
'translate-y',
|
|
935
|
+
'rotate',
|
|
936
|
+
]);
|
|
937
|
+
|
|
938
|
+
/** Tailwind `ease-*` → `transition-timing-function` value. */
|
|
939
|
+
export const EASE_TIMING: Record<string, string> = {
|
|
940
|
+
linear: 'linear',
|
|
941
|
+
in: 'cubic-bezier(0.4, 0, 1, 1)',
|
|
942
|
+
out: 'cubic-bezier(0, 0, 0.2, 1)',
|
|
943
|
+
'in-out': 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
944
|
+
initial: 'initial',
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Tailwind transition-enable classes → the `transition-property` list they animate. Emitted as the
|
|
949
|
+
* `transition` shorthand with Tailwind's default 150ms / ease curve baked in, so a bare `transition`
|
|
950
|
+
* animates standalone; a following `duration-*` / `ease-*` longhand still overrides duration/timing
|
|
951
|
+
* (shorthand sorts before longhand). The empty key is the bare `transition` class.
|
|
952
|
+
*/
|
|
953
|
+
export const TRANSITION_PROPERTY_VALUES: Record<string, string> = {
|
|
954
|
+
'': 'color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter',
|
|
955
|
+
all: 'all',
|
|
956
|
+
colors: 'color, background-color, border-color, text-decoration-color, fill, stroke',
|
|
957
|
+
opacity: 'opacity',
|
|
958
|
+
shadow: 'box-shadow',
|
|
959
|
+
transform: 'transform, translate, scale, rotate',
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
/** Tailwind's default transition timing baked into the `transition` shorthand. */
|
|
963
|
+
export const TRANSITION_DEFAULT_TIMING = '150ms cubic-bezier(0.4, 0, 0.2, 1)';
|
|
964
|
+
|
|
849
965
|
// ============================================================================
|
|
850
966
|
// STYLE PRESETS
|
|
851
967
|
// Predefined values for complex CSS properties (shadows, gradients, borders)
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
styleHasMapping,
|
|
10
10
|
} from './utilityClassMapper';
|
|
11
11
|
import { getStyleValue, clearRegistry } from './styleValueRegistry';
|
|
12
|
+
import { defaultTailwindThemeVariables } from './tailwindThemeScale';
|
|
12
13
|
import type { ResponsiveScales } from './responsiveScaling';
|
|
13
14
|
|
|
14
15
|
describe('utilityClassMapper', () => {
|
|
@@ -143,6 +144,19 @@ describe('utilityClassMapper', () => {
|
|
|
143
144
|
expect(classes).toContain('grid-cols-[repeat(3,_1fr)]');
|
|
144
145
|
});
|
|
145
146
|
|
|
147
|
+
test('emits compact grid-cols-N for the canonical Tailwind track scale', () => {
|
|
148
|
+
expect(stylesToClasses({ gridTemplateColumns: 'repeat(3, minmax(0, 1fr))' })).toContain('grid-cols-3');
|
|
149
|
+
expect(stylesToClasses({ gridTemplateRows: 'repeat(2, minmax(0, 1fr))' })).toContain('grid-rows-2');
|
|
150
|
+
expect(stylesToClasses({ gridTemplateColumns: 'none' })).toContain('grid-cols-none');
|
|
151
|
+
expect(stylesToClasses({ gridTemplateColumns: 'subgrid' })).toContain('grid-cols-subgrid');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('keeps non-canonical grid track values as arbitrary classes', () => {
|
|
155
|
+
// No compact Tailwind spelling → stays arbitrary (preserves prior behavior).
|
|
156
|
+
expect(stylesToClasses({ gridTemplateColumns: '1fr 1fr 1fr' })).toContain('grid-cols-[1fr_1fr_1fr]');
|
|
157
|
+
expect(stylesToClasses({ gridTemplateColumns: 'repeat(3, 1fr)' })).toContain('grid-cols-[repeat(3,_1fr)]');
|
|
158
|
+
});
|
|
159
|
+
|
|
146
160
|
test('handles z-index Tailwind-style bare number', () => {
|
|
147
161
|
const classes = stylesToClasses({ zIndex: 10 });
|
|
148
162
|
expect(classes).toContain('z-10');
|
|
@@ -181,6 +195,140 @@ describe('utilityClassMapper', () => {
|
|
|
181
195
|
expect(evenly).not.toContain('justify-end');
|
|
182
196
|
expect(classToStyle('justify-evenly')).toEqual({ prop: 'justifyContent', value: 'space-evenly' });
|
|
183
197
|
});
|
|
198
|
+
|
|
199
|
+
test('parses the Tailwind numeric grid scale (read side)', () => {
|
|
200
|
+
expect(classToStyle('grid-cols-3')).toEqual({
|
|
201
|
+
prop: 'gridTemplateColumns',
|
|
202
|
+
value: 'repeat(3, minmax(0, 1fr))',
|
|
203
|
+
});
|
|
204
|
+
expect(classToStyle('grid-rows-2')).toEqual({ prop: 'gridTemplateRows', value: 'repeat(2, minmax(0, 1fr))' });
|
|
205
|
+
expect(classToStyle('grid-cols-none')).toEqual({ prop: 'gridTemplateColumns', value: 'none' });
|
|
206
|
+
// Tailwind defines no `grid-cols-0` → left unrecognized.
|
|
207
|
+
expect(classToStyle('grid-cols-0')).toBeNull();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('a responsive AI-authored grid round-trips through the breakpoint variants', () => {
|
|
211
|
+
const styles = classesToStyles(['grid-cols-3', 'tablet:grid-cols-2', 'mobile:grid-cols-1']);
|
|
212
|
+
expect(styles).toEqual({
|
|
213
|
+
base: { gridTemplateColumns: 'repeat(3, minmax(0, 1fr))' },
|
|
214
|
+
tablet: { gridTemplateColumns: 'repeat(2, minmax(0, 1fr))' },
|
|
215
|
+
mobile: { gridTemplateColumns: 'repeat(1, minmax(0, 1fr))' },
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('Tailwind layout/transform extensions', () => {
|
|
221
|
+
test('fractional sizing → percentage', () => {
|
|
222
|
+
expect(classToStyle('w-1/2')).toEqual({ prop: 'width', value: '50%' });
|
|
223
|
+
expect(classToStyle('w-1/3')).toEqual({ prop: 'width', value: '33.333333%' });
|
|
224
|
+
expect(classToStyle('basis-1/2')).toEqual({ prop: 'flexBasis', value: '50%' });
|
|
225
|
+
expect(classToStyle('left-1/2')).toEqual({ prop: 'left', value: '50%' });
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('negative spacing/position via the leading-dash form', () => {
|
|
229
|
+
expect(classToStyle('-mt-4')).toEqual({ prop: 'marginTop', value: '-16px' });
|
|
230
|
+
expect(classToStyle('-top-4')).toEqual({ prop: 'top', value: '-16px' });
|
|
231
|
+
expect(classToStyle('-translate-y-1/2')).toEqual({ prop: 'translate', value: '0 -50%' });
|
|
232
|
+
// Non-negatable root → unrecognized rather than mis-parsed.
|
|
233
|
+
expect(classToStyle('-flex')).toBeNull();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('grid placement (span / start / end)', () => {
|
|
237
|
+
expect(classToStyle('col-span-2')).toEqual({ prop: 'gridColumn', value: 'span 2 / span 2' });
|
|
238
|
+
expect(classToStyle('col-span-full')).toEqual({ prop: 'gridColumn', value: '1 / -1' });
|
|
239
|
+
expect(classToStyle('row-span-3')).toEqual({ prop: 'gridRow', value: 'span 3 / span 3' });
|
|
240
|
+
expect(classToStyle('col-start-2')).toEqual({ prop: 'gridColumnStart', value: '2' });
|
|
241
|
+
expect(classToStyle('row-end-4')).toEqual({ prop: 'gridRowEnd', value: '4' });
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('inset axes use logical properties', () => {
|
|
245
|
+
expect(classToStyle('inset-x-0')).toEqual({ prop: 'insetInline', value: '0px' });
|
|
246
|
+
expect(classToStyle('inset-y-4')).toEqual({ prop: 'insetBlock', value: '16px' });
|
|
247
|
+
expect(classToStyle('inset-x-auto')).toEqual({ prop: 'insetInline', value: 'auto' });
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('order keywords, aspect keywords', () => {
|
|
251
|
+
expect(classToStyle('order-first')).toEqual({ prop: 'order', value: '-9999' });
|
|
252
|
+
expect(classToStyle('order-last')).toEqual({ prop: 'order', value: '9999' });
|
|
253
|
+
expect(classToStyle('aspect-video')).toEqual({ prop: 'aspectRatio', value: '16 / 9' });
|
|
254
|
+
expect(classToStyle('aspect-square')).toEqual({ prop: 'aspectRatio', value: '1 / 1' });
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('size-* sets width and height and restores both on round-trip', () => {
|
|
258
|
+
expect(classesToStyles(['size-8'])).toEqual({ base: { width: '32px', height: '32px' } });
|
|
259
|
+
expect(classesToStyles(['size-full'])).toEqual({ base: { width: '100%', height: '100%' } });
|
|
260
|
+
// a plain w-* must NOT pull in height
|
|
261
|
+
expect(classesToStyles(['w-8'])).toEqual({ base: { width: '32px' } });
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('numeric line-height on the spacing scale', () => {
|
|
265
|
+
expect(classToStyle('leading-6')).toEqual({ prop: 'lineHeight', value: '24px' });
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('individual transforms', () => {
|
|
269
|
+
expect(classToStyle('scale-105')).toEqual({ prop: 'scale', value: '1.05' });
|
|
270
|
+
expect(classToStyle('rotate-45')).toEqual({ prop: 'rotate', value: '45deg' });
|
|
271
|
+
expect(classToStyle('-rotate-3')).toEqual({ prop: 'rotate', value: '-3deg' });
|
|
272
|
+
expect(classToStyle('translate-x-2')).toEqual({ prop: 'translate', value: '8px' });
|
|
273
|
+
expect(classToStyle('translate-y-4')).toEqual({ prop: 'translate', value: '0 16px' });
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('transition longhands compose; duration/ease override the shorthand', () => {
|
|
277
|
+
expect(classToStyle('duration-300')).toEqual({ prop: 'transitionDuration', value: '300ms' });
|
|
278
|
+
expect(classToStyle('delay-150')).toEqual({ prop: 'transitionDelay', value: '150ms' });
|
|
279
|
+
expect(classToStyle('ease-in-out')).toEqual({
|
|
280
|
+
prop: 'transitionTimingFunction',
|
|
281
|
+
value: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
282
|
+
});
|
|
283
|
+
// `transition-all` carries the default timing so it animates standalone.
|
|
284
|
+
expect(classToStyle('transition-all')).toEqual({
|
|
285
|
+
prop: 'transition',
|
|
286
|
+
value: 'all 150ms cubic-bezier(0.4, 0, 0.2, 1)',
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe('Tailwind named scales → var(--token)', () => {
|
|
292
|
+
test('named scales resolve to a plain variable reference (no baked default)', () => {
|
|
293
|
+
expect(classToStyle('text-lg')).toEqual({ prop: 'fontSize', value: 'var(--text-lg)' });
|
|
294
|
+
expect(classToStyle('font-semibold')).toEqual({ prop: 'fontWeight', value: 'var(--font-weight-semibold)' });
|
|
295
|
+
expect(classToStyle('rounded-md')).toEqual({ prop: 'borderRadius', value: 'var(--radius-md)' });
|
|
296
|
+
expect(classToStyle('max-w-md')).toEqual({ prop: 'maxWidth', value: 'var(--container-md)' });
|
|
297
|
+
expect(classToStyle('leading-tight')).toEqual({ prop: 'lineHeight', value: 'var(--leading-tight)' });
|
|
298
|
+
// bare `rounded` / `shadow` (no suffix) map to the DEFAULT token.
|
|
299
|
+
expect(classToStyle('rounded')).toEqual({ prop: 'borderRadius', value: 'var(--radius)' });
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('existing static/keyword/preset meanings are preserved (not turned into vars)', () => {
|
|
303
|
+
expect(classToStyle('text-center')).toEqual({ prop: 'textAlign', value: 'center' });
|
|
304
|
+
expect(classToStyle('font-bold')).toEqual({ prop: 'fontWeight', value: 'bold' });
|
|
305
|
+
expect(classToStyle('rounded-full')).toEqual({ prop: 'borderRadius', value: '9999px' });
|
|
306
|
+
expect(classToStyle('shadow-none')).toEqual({ prop: 'boxShadow', value: 'none' });
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('round-trips to the compact class; legacy fallback form still recognized', () => {
|
|
310
|
+
expect(stylesToClasses({ fontSize: 'var(--text-lg)' })).toEqual(['text-lg']);
|
|
311
|
+
expect(stylesToClasses({ boxShadow: 'var(--shadow-lg)' })).toEqual(['shadow-lg']);
|
|
312
|
+
// A legacy `var(--token, fallback)` value (e.g. authored by hand) still collapses to the class.
|
|
313
|
+
expect(stylesToClasses({ fontSize: 'var(--text-lg, 1.125rem)' })).toEqual(['text-lg']);
|
|
314
|
+
// A var() the table doesn't know stays a normal var-shorthand class, not a theme token.
|
|
315
|
+
expect(stylesToClasses({ fontSize: 'var(--my-custom)' })).toEqual(['text-(length:--my-custom)']);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test('seed-variable generator yields unique, grouped CSSVariables', () => {
|
|
319
|
+
const vars = defaultTailwindThemeVariables();
|
|
320
|
+
expect(vars.length).toBeGreaterThan(40);
|
|
321
|
+
const cssVars = vars.map((v) => v.cssVar);
|
|
322
|
+
expect(new Set(cssVars).size).toBe(cssVars.length); // no duplicate cssVars
|
|
323
|
+
expect(vars.find((v) => v.cssVar === '--text-lg')).toEqual({
|
|
324
|
+
name: 'Text LG',
|
|
325
|
+
cssVar: '--text-lg',
|
|
326
|
+
value: '1.125rem',
|
|
327
|
+
type: 'fontSize',
|
|
328
|
+
group: 'font-size',
|
|
329
|
+
});
|
|
330
|
+
expect(vars.find((v) => v.cssVar === '--font-weight-semibold')?.name).toBe('Font Weight Semibold');
|
|
331
|
+
});
|
|
184
332
|
});
|
|
185
333
|
|
|
186
334
|
describe('responsiveStylesToClasses', () => {
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
isBareColorTokenName,
|
|
36
36
|
} from './utilityClassNames';
|
|
37
37
|
import { registerStyleValue, registerDynamicStyle, getStyleValue, getDynamicStyle } from './styleValueRegistry';
|
|
38
|
+
import { THEME_SCALE_BY_CSSVAR } from './tailwindThemeScale';
|
|
38
39
|
import {
|
|
39
40
|
SCALABLE_CSS_PROPERTIES,
|
|
40
41
|
buildFluidClampWithExplicitMin,
|
|
@@ -99,6 +100,19 @@ function rootFormMisresolves(root: string, prop: string, value: string): boolean
|
|
|
99
100
|
function propertyValueToClass(prop: string, value: string | number): string | null {
|
|
100
101
|
let stringValue = String(value);
|
|
101
102
|
|
|
103
|
+
// Tailwind theme token: a `var(--token)` value (or a legacy `var(--token, …)` with fallback)
|
|
104
|
+
// whose token is a known Tailwind scale var (and whose property matches) emits the compact class —
|
|
105
|
+
// `var(--text-lg)` → `text-lg`. Runs before the fontFamily comma-split below (which would shred a
|
|
106
|
+
// multi-family `var()` fallback). The parser reconstructs the value from the class, so no registry
|
|
107
|
+
// entry is needed.
|
|
108
|
+
const varRef = stringValue.match(/^var\((--[\w-]+)(?:\s*,[\s\S]*)?\)$/);
|
|
109
|
+
if (varRef) {
|
|
110
|
+
const entry = THEME_SCALE_BY_CSSVAR.get(varRef[1] ?? '');
|
|
111
|
+
if (entry && entry.property === camelToKebab(prop)) {
|
|
112
|
+
return entry.className;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
102
116
|
// Strip outer quotes from fontFamily values (e.g. "Space Grotesk" → Space Grotesk)
|
|
103
117
|
if (prop === 'fontFamily') {
|
|
104
118
|
stringValue = stringValue
|
|
@@ -194,6 +208,20 @@ function propertyValueToClass(prop: string, value: string | number): string | nu
|
|
|
194
208
|
}
|
|
195
209
|
}
|
|
196
210
|
|
|
211
|
+
// Tailwind grid track scale: gridTemplateColumns "repeat(3, minmax(0, 1fr))" → grid-cols-3
|
|
212
|
+
// (and gridTemplateRows → grid-rows-N, plus the none/subgrid keywords). Keeps grid-cols-N that
|
|
213
|
+
// came in from hand/AI-authored class strings round-tripping to the compact form rather than
|
|
214
|
+
// expanding to grid-cols-[repeat(3,_minmax(0,_1fr))]. Other track values ("1fr 1fr 1fr",
|
|
215
|
+
// "repeat(3, 1fr)") have no compact Tailwind spelling and stay arbitrary.
|
|
216
|
+
if (!className && (root === 'grid-cols' || root === 'grid-rows')) {
|
|
217
|
+
const repeatMatch = stringValue.match(/^repeat\(\s*([1-9]\d*)\s*,\s*minmax\(\s*0\s*,\s*1fr\s*\)\s*\)$/);
|
|
218
|
+
if (repeatMatch) {
|
|
219
|
+
className = `${root}-${repeatMatch[1]}`;
|
|
220
|
+
} else if (stringValue === 'none' || stringValue === 'subgrid') {
|
|
221
|
+
className = `${root}-${stringValue}`;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
197
225
|
// CSS variables: var(--x) → root-(--x). The `font-*` root is shared by font-weight and
|
|
198
226
|
// font-family and is disambiguated by value type — but a `var(--x)` value is ambiguous, so
|
|
199
227
|
// weight/size vars carry a Tailwind data-type hint (`number:` / `length:`) the parser reads
|
|
@@ -513,6 +541,11 @@ export function classesToStyles(classes: string[], knownTokens?: ReadonlySet<str
|
|
|
513
541
|
styles[breakpoint] = {};
|
|
514
542
|
}
|
|
515
543
|
styles[breakpoint]![styleEntry.prop] = styleEntry.value;
|
|
544
|
+
// `size-*` is width AND height (no CSS shorthand) — restore both so the editor doesn't
|
|
545
|
+
// drop height on the next save. classToStyle returns the width half; mirror it to height.
|
|
546
|
+
if (cleanClass.startsWith('size-') && styleEntry.prop === 'width') {
|
|
547
|
+
styles[breakpoint]!.height = styleEntry.value;
|
|
548
|
+
}
|
|
516
549
|
}
|
|
517
550
|
}
|
|
518
551
|
|
|
@@ -26,8 +26,14 @@ import {
|
|
|
26
26
|
SPACING_SCALE,
|
|
27
27
|
SPACING_SCALE_ROOTS,
|
|
28
28
|
COLOR_TOKEN_ROOTS,
|
|
29
|
+
FRACTION_ROOTS,
|
|
30
|
+
NEGATABLE_ROOTS,
|
|
31
|
+
EASE_TIMING,
|
|
32
|
+
TRANSITION_PROPERTY_VALUES,
|
|
33
|
+
TRANSITION_DEFAULT_TIMING,
|
|
29
34
|
} from './utilityClassConfig';
|
|
30
35
|
import { isCssNamedColor } from './cssNamedColors';
|
|
36
|
+
import { THEME_SCALE_BY_CLASS, themeScaleValue } from './tailwindThemeScale';
|
|
31
37
|
|
|
32
38
|
/** Class roots sorted longest-first so `border-t-…` wins over `border-…`. */
|
|
33
39
|
export const SORTED_ROOTS: readonly string[] = Object.keys(prefixToCSSProperty).sort((a, b) => b.length - a.length);
|
|
@@ -341,6 +347,37 @@ export interface ParsedUtilityClass {
|
|
|
341
347
|
|
|
342
348
|
const HASH_VALUE_RE = /^h[0-9a-z]+$/;
|
|
343
349
|
|
|
350
|
+
/** Tailwind fraction → percentage (`1/2` → `50%`, `1/3` → `33.333333%`). Null when not `n/d`. */
|
|
351
|
+
function resolveFraction(rest: string): string | null {
|
|
352
|
+
const m = rest.match(/^(\d+)\/(\d+)$/);
|
|
353
|
+
if (!m) return null;
|
|
354
|
+
const d = Number(m[2]);
|
|
355
|
+
if (d === 0) return null;
|
|
356
|
+
const pct = (Number(m[1]) / d) * 100;
|
|
357
|
+
return `${pct.toFixed(6).replace(/\.?0+$/, '')}%`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Resolve a translate axis value: fraction, `full`, or a spacing-scale step. Null otherwise. */
|
|
361
|
+
function resolveSpacingOrFraction(rest: string): string | null {
|
|
362
|
+
const frac = resolveFraction(rest);
|
|
363
|
+
if (frac != null) return frac;
|
|
364
|
+
if (rest === 'full') return '100%';
|
|
365
|
+
return SPACING_SCALE[rest] ?? null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Negate a resolved value for the leading-`-` Tailwind form. Plain numeric/length/percentage
|
|
370
|
+
* tokens flip sign; `translate-y`'s `0 <y>` composite negates only the y token. Non-numeric values
|
|
371
|
+
* (keywords, var(), calc) aren't negatable → null (the class stays unrecognized).
|
|
372
|
+
*/
|
|
373
|
+
function negateCssValue(root: string, value: string): string | null {
|
|
374
|
+
if (root === 'translate-y') {
|
|
375
|
+
const m = value.match(/^0 (.+)$/);
|
|
376
|
+
return m ? `0 -${m[1]}` : null;
|
|
377
|
+
}
|
|
378
|
+
return /^\d/.test(value) ? `-${value}` : null;
|
|
379
|
+
}
|
|
380
|
+
|
|
344
381
|
/**
|
|
345
382
|
* Parse a single (unprefixed) utility class name into its CSS property and
|
|
346
383
|
* decoded value. Returns null for class names that aren't Meno utilities.
|
|
@@ -358,11 +395,43 @@ export function parseUtilityClass(className: string, knownTokens?: ReadonlySet<s
|
|
|
358
395
|
return { property: camelToKebab(stat.prop), value: stat.value, root: null, kind: 'static' };
|
|
359
396
|
}
|
|
360
397
|
|
|
398
|
+
// Bare `transition` enables Tailwind's default property set + 150ms ease curve.
|
|
399
|
+
if (className === 'transition') {
|
|
400
|
+
return {
|
|
401
|
+
property: 'transition',
|
|
402
|
+
value: `${TRANSITION_PROPERTY_VALUES['']} ${TRANSITION_DEFAULT_TIMING}`,
|
|
403
|
+
root: 'transition',
|
|
404
|
+
kind: 'keyword',
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Leading-`-` negative form (`-mt-4`, `-top-2`, `-translate-y-1/2`, `-rotate-3`): parse the
|
|
409
|
+
// positive class, then negate its value when the root is negatable. Not a negatable root or a
|
|
410
|
+
// non-numeric value → unrecognized (don't misread foreign `-foo` classes as utilities).
|
|
411
|
+
if (className.startsWith('-') && className.length > 1) {
|
|
412
|
+
const positive = parseUtilityClass(className.slice(1), knownTokens);
|
|
413
|
+
if (positive?.root && positive.value != null && NEGATABLE_ROOTS.has(positive.root)) {
|
|
414
|
+
const negated = negateCssValue(positive.root, positive.value);
|
|
415
|
+
if (negated != null) return { ...positive, value: negated };
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
|
|
361
420
|
const preset = presetClassReverse[className];
|
|
362
421
|
if (preset) {
|
|
363
422
|
return { property: camelToKebab(preset.prop), value: preset.value, root: null, kind: 'preset' };
|
|
364
423
|
}
|
|
365
424
|
|
|
425
|
+
// Tailwind named value scales (`text-lg`, `font-semibold`, `rounded-md`, `shadow-lg`, `max-w-md`,
|
|
426
|
+
// `leading-tight`, `tracking-wide`, `font-sans`, + bare `rounded`/`shadow`). These resolve to a
|
|
427
|
+
// plain `var(--token)` reference (no baked default — Meno is token-based): renders the project
|
|
428
|
+
// variable when defined, inert when not. Checked after static/preset so `text-center`,
|
|
429
|
+
// `font-bold`, `rounded-full`, `shadow-none`, `shadow-2` keep their existing meaning.
|
|
430
|
+
const themed = THEME_SCALE_BY_CLASS.get(className);
|
|
431
|
+
if (themed) {
|
|
432
|
+
return { property: themed.property, value: themeScaleValue(themed), root: themed.root, kind: 'var' };
|
|
433
|
+
}
|
|
434
|
+
|
|
366
435
|
// Arbitrary property: [clip-path:circle(50%)], [background:linear-gradient(…)]
|
|
367
436
|
const arbProp = className.match(/^\[([a-z-]+):(.+)\]$/);
|
|
368
437
|
if (arbProp) {
|
|
@@ -422,6 +491,83 @@ function parseRootValue(root: string, rest: string, knownTokens?: ReadonlySet<st
|
|
|
422
491
|
return { property: resolveRootProperty(root, rest), value: rest, root, kind: 'keyword' };
|
|
423
492
|
}
|
|
424
493
|
|
|
494
|
+
// Tailwind grid track scale: `grid-cols-3` → `repeat(3, minmax(0, 1fr))`,
|
|
495
|
+
// `grid-rows-2` likewise, plus the `none` / `subgrid` keywords. Meno's own
|
|
496
|
+
// form is the arbitrary `grid-cols-[1fr_1fr_1fr]` (handled above); this
|
|
497
|
+
// additionally absorbs the bare Tailwind numeric scale that hand- and
|
|
498
|
+
// AI-authored class strings reach for. Tailwind only defines the positive
|
|
499
|
+
// integer scale, so `grid-cols-0` stays unrecognized.
|
|
500
|
+
if (root === 'grid-cols' || root === 'grid-rows') {
|
|
501
|
+
if (/^[1-9]\d*$/.test(rest)) {
|
|
502
|
+
const value = `repeat(${rest}, minmax(0, 1fr))`;
|
|
503
|
+
return { property: prefixToCSSProperty[root] ?? root, value, root, kind: 'numeric' };
|
|
504
|
+
}
|
|
505
|
+
if (rest === 'none' || rest === 'subgrid') {
|
|
506
|
+
return { property: prefixToCSSProperty[root] ?? root, value: rest, root, kind: 'keyword' };
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Grid placement: `col-span-2` → grid-column: span 2 / span 2; `col-span-full` → 1 / -1.
|
|
511
|
+
if (root === 'col-span' || root === 'row-span') {
|
|
512
|
+
const property = root === 'col-span' ? 'grid-column' : 'grid-row';
|
|
513
|
+
if (rest === 'full') return { property, value: '1 / -1', root, kind: 'keyword' };
|
|
514
|
+
if (/^[1-9]\d*$/.test(rest)) return { property, value: `span ${rest} / span ${rest}`, root, kind: 'numeric' };
|
|
515
|
+
}
|
|
516
|
+
// Grid line start/end: `col-start-2`, `col-end-3`, `row-start-1`, `row-end-4` (+ `auto`).
|
|
517
|
+
if (root === 'col-start' || root === 'col-end' || root === 'row-start' || root === 'row-end') {
|
|
518
|
+
const property = prefixToCSSProperty[root] ?? root;
|
|
519
|
+
if (rest === 'auto') return { property, value: 'auto', root, kind: 'keyword' };
|
|
520
|
+
if (/^-?[1-9]\d*$/.test(rest)) return { property, value: rest, root, kind: 'numeric' };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Individual translate axis: `translate-x-4` → translate: 16px; `translate-y-1/2` → translate: 0 50%.
|
|
524
|
+
if (root === 'translate-x' || root === 'translate-y') {
|
|
525
|
+
const len = resolveSpacingOrFraction(rest);
|
|
526
|
+
if (len != null) {
|
|
527
|
+
const value = root === 'translate-x' ? len : `0 ${len}`;
|
|
528
|
+
return { property: 'translate', value, root, kind: 'numeric' };
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
// `scale-105` → scale: 1.05 (Tailwind's percentage-of-100 scale).
|
|
532
|
+
if (root === 'scale' && /^\d+$/.test(rest)) {
|
|
533
|
+
return { property: 'scale', value: String(Number(rest) / 100), root, kind: 'numeric' };
|
|
534
|
+
}
|
|
535
|
+
// `rotate-45` → rotate: 45deg (negatives via the leading-`-` form).
|
|
536
|
+
if (root === 'rotate' && /^\d+(\.\d+)?$/.test(rest)) {
|
|
537
|
+
return { property: 'rotate', value: `${rest}deg`, root, kind: 'numeric' };
|
|
538
|
+
}
|
|
539
|
+
// `duration-300` → transition-duration: 300ms; `delay-150` → transition-delay: 150ms.
|
|
540
|
+
if ((root === 'duration' || root === 'delay') && /^\d+$/.test(rest)) {
|
|
541
|
+
return { property: prefixToCSSProperty[root] ?? root, value: `${rest}ms`, root, kind: 'numeric' };
|
|
542
|
+
}
|
|
543
|
+
// `ease-in-out` → transition-timing-function: cubic-bezier(…).
|
|
544
|
+
if (root === 'ease') {
|
|
545
|
+
const timing = EASE_TIMING[rest];
|
|
546
|
+
if (timing) return { property: 'transition-timing-function', value: timing, root, kind: 'keyword' };
|
|
547
|
+
}
|
|
548
|
+
// `transition-all` / `transition-colors` / … → the `transition` shorthand (bare `transition`
|
|
549
|
+
// is handled in parseUtilityClass). A following `duration-*` / `ease-*` longhand still overrides.
|
|
550
|
+
if (root === 'transition') {
|
|
551
|
+
const props = TRANSITION_PROPERTY_VALUES[rest];
|
|
552
|
+
if (props !== undefined) {
|
|
553
|
+
return { property: 'transition', value: `${props} ${TRANSITION_DEFAULT_TIMING}`, root, kind: 'keyword' };
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Numeric line-height on the spacing scale: `leading-6` → 1.5rem (24px). Named leadings
|
|
558
|
+
// (`leading-tight`) resolve to a var() earlier; `leading-normal` stays the `normal` keyword.
|
|
559
|
+
if (root === 'leading') {
|
|
560
|
+
const scale = SPACING_SCALE[rest];
|
|
561
|
+
if (scale !== undefined) return { property: 'line-height', value: scale, root, kind: 'keyword' };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Tailwind fractional values: `w-1/2` → 50%, `top-1/3` → 33.333333%, `basis-1/2` → 50%.
|
|
565
|
+
// (translate fractions are handled by the axis branch above.)
|
|
566
|
+
if (FRACTION_ROOTS.has(root)) {
|
|
567
|
+
const value = resolveFraction(rest);
|
|
568
|
+
if (value != null) return { property: resolveRootProperty(root, value), value, root, kind: 'numeric' };
|
|
569
|
+
}
|
|
570
|
+
|
|
425
571
|
// Named spacing scale: p-4 → 16px, gap-6 → 24px (Tailwind's step table).
|
|
426
572
|
if (SPACING_SCALE_ROOTS.has(root)) {
|
|
427
573
|
const scaleVal = SPACING_SCALE[rest];
|