meno-core 1.1.0 → 1.1.1
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-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 +14 -5
- package/dist/lib/server/index.js.map +2 -2
- package/dist/lib/shared/index.js +5 -1
- package/dist/lib/shared/index.js.map +2 -2
- 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/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/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-NUP7H7D3.js.map → chunk-7ZLF4NE5.js.map} +0 -0
|
@@ -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];
|
package/package.json
CHANGED
|
File without changes
|