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
|
@@ -442,8 +442,6 @@ export async function generateSSRHTML(
|
|
|
442
442
|
// Include component CSS (no longer generating inline style classes)
|
|
443
443
|
const componentCSS = rendered.componentCSS || '';
|
|
444
444
|
|
|
445
|
-
// Extract and generate utility CSS from rendered HTML
|
|
446
|
-
const usedUtilityClasses = extractUtilityClassesFromHTML(rendered.html);
|
|
447
445
|
const breakpointConfig = await loadBreakpointConfig();
|
|
448
446
|
const responsiveScalesConfig = configService.getResponsiveScales();
|
|
449
447
|
|
|
@@ -451,11 +449,29 @@ export async function generateSSRHTML(
|
|
|
451
449
|
const variablesConfig = await variableService.loadConfig();
|
|
452
450
|
const variablesCSS = generateVariablesCSS(variablesConfig, breakpointConfig, responsiveScalesConfig);
|
|
453
451
|
const remConversionConfig = configService.getRemConversion();
|
|
452
|
+
|
|
453
|
+
// Named design-token classes (`text-muted` → color: var(--muted), `bg-<token>`, …) only resolve to a
|
|
454
|
+
// CSS rule when the generator knows which token names are DEFINED — without that set, `text-muted` is
|
|
455
|
+
// indistinguishable from a typo and is dropped, both at extraction (filtered by parseUtilityClass) and
|
|
456
|
+
// at generation. Build it from the project's theme colors + variables — mirroring the Astro build's
|
|
457
|
+
// readKnownTokensSync and the canvas client's loadKnownTokens — so an instance-level color class that
|
|
458
|
+
// renders in a real Astro build also renders in the SSR design canvas.
|
|
459
|
+
const knownTokens = new Set<string>();
|
|
460
|
+
for (const theme of Object.values(themeConfig.themes)) {
|
|
461
|
+
for (const name of Object.keys(theme.colors)) knownTokens.add(name);
|
|
462
|
+
}
|
|
463
|
+
for (const variable of variablesConfig.variables) {
|
|
464
|
+
if (variable.cssVar) knownTokens.add(variable.cssVar.replace(/^--/, ''));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Extract and generate utility CSS from rendered HTML
|
|
468
|
+
const usedUtilityClasses = extractUtilityClassesFromHTML(rendered.html, knownTokens);
|
|
454
469
|
const utilityCSS = generateUtilityCSS(
|
|
455
470
|
usedUtilityClasses,
|
|
456
471
|
breakpointConfig,
|
|
457
472
|
responsiveScalesConfig,
|
|
458
473
|
remConversionConfig,
|
|
474
|
+
knownTokens,
|
|
459
475
|
);
|
|
460
476
|
|
|
461
477
|
// Generate interactive styles CSS from map collected during render
|
|
@@ -1115,6 +1115,31 @@ describe('ssrRenderer', () => {
|
|
|
1115
1115
|
expect(html).toContain('<div');
|
|
1116
1116
|
});
|
|
1117
1117
|
|
|
1118
|
+
test('component instance class-STRING override lands on the root element', async () => {
|
|
1119
|
+
// The class-based instance seam (`<Heading class="text-muted text-[#ff0000]" />`): the instance's
|
|
1120
|
+
// `class` prop must be applied onto the component root, mirroring real Astro's
|
|
1121
|
+
// cx(style(...), variants(...), className). Without it, instance-level colors render in a real
|
|
1122
|
+
// build but vanish in the SSR design canvas.
|
|
1123
|
+
const componentDef: ComponentDefinition = {
|
|
1124
|
+
component: {
|
|
1125
|
+
structure: { type: 'node', tag: 'h1', children: '{{text}}' },
|
|
1126
|
+
interface: { text: { type: 'string', default: 'Hi' } },
|
|
1127
|
+
},
|
|
1128
|
+
};
|
|
1129
|
+
|
|
1130
|
+
const node = {
|
|
1131
|
+
type: 'component',
|
|
1132
|
+
component: 'Heading',
|
|
1133
|
+
props: { text: 'Hi', class: 'text-muted text-[#ff0000]' },
|
|
1134
|
+
};
|
|
1135
|
+
|
|
1136
|
+
const html = await render(node, { globalComponents: { Heading: componentDef } });
|
|
1137
|
+
const classMatch = html.match(/<h1[^>]*\sclass="([^"]*)"/);
|
|
1138
|
+
expect(classMatch).not.toBeNull();
|
|
1139
|
+
expect(classMatch?.[1]).toContain('text-muted');
|
|
1140
|
+
expect(classMatch?.[1]).toContain('text-[#ff0000]');
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1118
1143
|
test('component with defineVars: true adds data attributes', async () => {
|
|
1119
1144
|
const componentDef: ComponentDefinition = {
|
|
1120
1145
|
component: {
|
|
@@ -1850,8 +1850,18 @@ async function renderNode(
|
|
|
1850
1850
|
delete nodeAttributesWithoutClass.className;
|
|
1851
1851
|
delete nodeAttributesWithoutClass.class;
|
|
1852
1852
|
|
|
1853
|
-
//
|
|
1854
|
-
|
|
1853
|
+
// A component INSTANCE carries its class-string override as a prop (`<Heading class="text-muted" />`
|
|
1854
|
+
// → nodeProps.class) — the class-based equivalent of the legacy instance style object. Real Astro
|
|
1855
|
+
// applies it onto the component root via cx(style(...), variants(...), className); the SSR canvas
|
|
1856
|
+
// must too, or instance-level classes (named colors like `text-muted`/`text-text`, arbitrary
|
|
1857
|
+
// `text-[#ff0000]`) silently vanish in design mode while rendering correctly in a real build. Last in
|
|
1858
|
+
// the list so it wins source order, matching cx's instance-last precedence. Empty for HTML nodes
|
|
1859
|
+
// (nodeProps is {} there), so this only affects component instances.
|
|
1860
|
+
const instanceClassName = typeof nodeProps.class === 'string' ? nodeProps.class : '';
|
|
1861
|
+
|
|
1862
|
+
// Merge classNames: element class (first for specificity) + attribute classes + utility classes +
|
|
1863
|
+
// the component instance's class-string override (last → wins)
|
|
1864
|
+
const allClassNames = [elementClass, attrClassName, ...utilityClasses, instanceClassName].filter(Boolean);
|
|
1855
1865
|
const mergedClassName = allClassNames.length > 0 ? allClassNames.join(' ') : '';
|
|
1856
1866
|
|
|
1857
1867
|
const propsWithStyleAndAttrs = {
|
|
@@ -694,6 +694,16 @@ describe('styleObjectToCSS', () => {
|
|
|
694
694
|
expect(styleObjectToCSS({ color: 'rgb(0,0,0)' })).toBe('color: rgb(0,0,0)');
|
|
695
695
|
});
|
|
696
696
|
|
|
697
|
+
test('named CSS colors and keywords pass through — never wrapped in var()', () => {
|
|
698
|
+
// Regression: a literal `red` was being emitted as `var(--red)` (a token
|
|
699
|
+
// that does not exist) instead of the real CSS color.
|
|
700
|
+
expect(styleObjectToCSS({ color: 'red' })).toBe('color: red');
|
|
701
|
+
expect(styleObjectToCSS({ backgroundColor: 'transparent' })).toBe('background-color: transparent');
|
|
702
|
+
expect(styleObjectToCSS({ color: 'currentColor' })).toBe('color: currentColor');
|
|
703
|
+
expect(styleObjectToCSS({ borderColor: 'rebeccapurple' })).toBe('border-color: rebeccapurple');
|
|
704
|
+
expect(styleObjectToCSS({ color: 'inherit' })).toBe('color: inherit');
|
|
705
|
+
});
|
|
706
|
+
|
|
697
707
|
test('skips _mapping (binding) values — bound props yield empty/partial output', () => {
|
|
698
708
|
expect(styleObjectToCSS({ color: { _mapping: { prop: 'x' } } as never })).toBe('');
|
|
699
709
|
expect(styleObjectToCSS({ color: { _mapping: { prop: 'x' } } as never, padding: '4px' })).toBe('padding: 4px');
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
breakpointClassPrefixes,
|
|
12
12
|
resolveRootProperty,
|
|
13
13
|
camelToKebab,
|
|
14
|
+
isBareColorTokenName,
|
|
14
15
|
} from './utilityClassNames';
|
|
15
16
|
import { getStyleValue, getDynamicStyle, isDynamicClass } from './styleValueRegistry';
|
|
16
17
|
import type { BreakpointConfig } from './breakpoints';
|
|
@@ -213,6 +214,9 @@ export function generateRuleForClass(className: string, knownTokens?: ReadonlySe
|
|
|
213
214
|
}
|
|
214
215
|
|
|
215
216
|
// Multi-property roots (Tailwind axis shorthands)
|
|
217
|
+
if (root === 'size') {
|
|
218
|
+
return `width: ${value}; height: ${value};`;
|
|
219
|
+
}
|
|
216
220
|
if (root === 'px') {
|
|
217
221
|
return `padding-left: ${value}; padding-right: ${value};`;
|
|
218
222
|
}
|
|
@@ -726,15 +730,13 @@ export function styleObjectToCSS(style: StyleObject): string {
|
|
|
726
730
|
// Convert camelCase to kebab-case
|
|
727
731
|
const cssProperty = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
728
732
|
|
|
729
|
-
// Handle CSS variable references for color properties
|
|
733
|
+
// Handle CSS variable references for color properties. Only a bare Meno
|
|
734
|
+
// color *token* (e.g. `primary`) is wrapped in `var(--token)`. Named CSS
|
|
735
|
+
// colors (`red`), hex, color functions and Tailwind palette names are real
|
|
736
|
+
// values and pass through untouched — mirrors the utility-class engine so
|
|
737
|
+
// the interactive path and the static path resolve colors identically.
|
|
730
738
|
let cssValue = String(value);
|
|
731
|
-
if (
|
|
732
|
-
(prop === 'backgroundColor' || prop === 'color' || prop === 'borderColor') &&
|
|
733
|
-
!cssValue.startsWith('#') &&
|
|
734
|
-
!cssValue.startsWith('rgb') &&
|
|
735
|
-
!cssValue.startsWith('var(') &&
|
|
736
|
-
!cssValue.includes('(')
|
|
737
|
-
) {
|
|
739
|
+
if ((prop === 'backgroundColor' || prop === 'color' || prop === 'borderColor') && isBareColorTokenName(cssValue)) {
|
|
738
740
|
cssValue = `var(--${cssValue})`;
|
|
739
741
|
}
|
|
740
742
|
|
package/lib/shared/index.ts
CHANGED
|
@@ -30,6 +30,12 @@ export { splitVariantPrefix, normalizeBreakpointVariant } from './utilityClassNa
|
|
|
30
30
|
// Utility-class catalog pieces — the seed for the Styles panel's class-bar autocomplete (§13 Phase 5).
|
|
31
31
|
// Selective (not `export *`) to keep the barrel collision-free.
|
|
32
32
|
export { propertyMap, staticUtilityReverse, keywordValues, presetClassReverse } from './utilityClassConfig';
|
|
33
|
+
// Tailwind theme-scale tokens + the seed-variable generator (`text-lg` → `var(--text-lg, …)`).
|
|
34
|
+
export {
|
|
35
|
+
THEME_SCALE_ENTRIES,
|
|
36
|
+
defaultTailwindThemeVariables,
|
|
37
|
+
type ThemeScaleEntry,
|
|
38
|
+
} from './tailwindThemeScale';
|
|
33
39
|
export * from './elementClassName';
|
|
34
40
|
export * from './styleValueRegistry';
|
|
35
41
|
|
|
@@ -33,8 +33,10 @@ describe('generateInteractiveCSS', () => {
|
|
|
33
33
|
const css = generateInteractiveCSS(elementClass, interactiveStyles);
|
|
34
34
|
|
|
35
35
|
expect(css).toContain(`.${elementClass}:hover`);
|
|
36
|
+
// `primary` is a bare Meno token → wrapped in var(); `white` is a real
|
|
37
|
+
// named CSS color → passes through untouched (not var(--white)).
|
|
36
38
|
expect(css).toContain('background-color: var(--primary)');
|
|
37
|
-
expect(css).toContain('color:
|
|
39
|
+
expect(css).toContain('color: white');
|
|
38
40
|
});
|
|
39
41
|
|
|
40
42
|
test('should generate active styles for page element', () => {
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailwind named-scale → CSS-variable theme table.
|
|
3
|
+
*
|
|
4
|
+
* Tailwind's *named* value scales (`text-lg`, `font-semibold`, `rounded-md`, `shadow-lg`,
|
|
5
|
+
* `max-w-md`, `leading-tight`, `tracking-wide`, `font-sans`) have no single numeric mapping — each
|
|
6
|
+
* name is a theme token. Meno is token-based, so the class resolves to a plain variable reference
|
|
7
|
+
* with **no baked default** — it renders the project's variable when defined, and is inert when not
|
|
8
|
+
* (Meno deliberately does NOT smuggle Tailwind's opinionated defaults into the render):
|
|
9
|
+
*
|
|
10
|
+
* `text-lg` → `font-size: var(--text-lg)`
|
|
11
|
+
*
|
|
12
|
+
* Define `--text-lg` (in `variables.json`) → `text-lg` uses it everywhere; leave it undefined →
|
|
13
|
+
* the declaration has no effect (use an arbitrary value like `text-[18px]` for a one-off). See the
|
|
14
|
+
* styling docs for the authoring guidance.
|
|
15
|
+
*
|
|
16
|
+
* This table is the single source of truth for:
|
|
17
|
+
* - the parser (`parseUtilityClass`): class → `var(--token)`
|
|
18
|
+
* - the inverse mapper (`propertyValueToClass`): `var(--token)` → compact class
|
|
19
|
+
* - `defaultTailwindThemeVariables()`: an OPT-IN starter set (Tailwind values) a user can choose
|
|
20
|
+
* to create — it is never auto-applied and never a render fallback.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { CSSVariable, VariableGroup, VariableType } from './types/variables';
|
|
24
|
+
|
|
25
|
+
/** One Tailwind theme token: the class it backs, its CSS var, default value and UI grouping. */
|
|
26
|
+
export interface ThemeScaleEntry {
|
|
27
|
+
/** Full utility class, e.g. `text-lg`, or the bare default (`rounded`, `shadow`). */
|
|
28
|
+
className: string;
|
|
29
|
+
/** The class root (`text`, `font`, `rounded`, `shadow`, `max-w`, `leading`, `tracking`). */
|
|
30
|
+
root: string;
|
|
31
|
+
/** CSS custom property, e.g. `--text-lg`. */
|
|
32
|
+
cssVar: string;
|
|
33
|
+
/** kebab-case CSS property the class targets. */
|
|
34
|
+
property: string;
|
|
35
|
+
/** Tailwind's default value — the starter value for the OPT-IN seed set, NOT a render fallback. */
|
|
36
|
+
value: string;
|
|
37
|
+
/** Variable picker group + scaling type for the seeded variable. */
|
|
38
|
+
group: VariableGroup;
|
|
39
|
+
/** Human label for the seeded variable, e.g. `Text LG`. */
|
|
40
|
+
label: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type ScaleSpec = {
|
|
44
|
+
root: string;
|
|
45
|
+
property: string;
|
|
46
|
+
group: VariableGroup;
|
|
47
|
+
/** Prefix for the css var; class is `${root}-${name}` (or bare `${root}` when name is ''). */
|
|
48
|
+
varPrefix: string;
|
|
49
|
+
/** Human label prefix for the seeded variable. */
|
|
50
|
+
labelPrefix: string;
|
|
51
|
+
/** [classSuffix, cssValue] pairs. '' suffix = the bare class (`rounded`, `shadow`). */
|
|
52
|
+
values: [string, string][];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const SANS = 'ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif';
|
|
56
|
+
const SERIF = 'ui-serif, Georgia, Cambria, serif';
|
|
57
|
+
const MONO = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace';
|
|
58
|
+
|
|
59
|
+
const SPECS: ScaleSpec[] = [
|
|
60
|
+
{
|
|
61
|
+
root: 'text',
|
|
62
|
+
property: 'font-size',
|
|
63
|
+
group: 'font-size',
|
|
64
|
+
varPrefix: 'text',
|
|
65
|
+
labelPrefix: 'Text',
|
|
66
|
+
values: [
|
|
67
|
+
['xs', '0.75rem'],
|
|
68
|
+
['sm', '0.875rem'],
|
|
69
|
+
['base', '1rem'],
|
|
70
|
+
['lg', '1.125rem'],
|
|
71
|
+
['xl', '1.25rem'],
|
|
72
|
+
['2xl', '1.5rem'],
|
|
73
|
+
['3xl', '1.875rem'],
|
|
74
|
+
['4xl', '2.25rem'],
|
|
75
|
+
['5xl', '3rem'],
|
|
76
|
+
['6xl', '3.75rem'],
|
|
77
|
+
['7xl', '4.5rem'],
|
|
78
|
+
['8xl', '6rem'],
|
|
79
|
+
['9xl', '8rem'],
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
// Only the weights Meno's static utilities don't already cover (normal/bold stay static).
|
|
84
|
+
root: 'font',
|
|
85
|
+
property: 'font-weight',
|
|
86
|
+
group: 'font-weight',
|
|
87
|
+
varPrefix: 'font-weight',
|
|
88
|
+
labelPrefix: 'Font Weight',
|
|
89
|
+
values: [
|
|
90
|
+
['thin', '100'],
|
|
91
|
+
['extralight', '200'],
|
|
92
|
+
['light', '300'],
|
|
93
|
+
['medium', '500'],
|
|
94
|
+
['semibold', '600'],
|
|
95
|
+
['extrabold', '800'],
|
|
96
|
+
['black', '900'],
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
root: 'font',
|
|
101
|
+
property: 'font-family',
|
|
102
|
+
group: 'font-family',
|
|
103
|
+
varPrefix: 'font',
|
|
104
|
+
labelPrefix: 'Font',
|
|
105
|
+
values: [
|
|
106
|
+
['sans', SANS],
|
|
107
|
+
['serif', SERIF],
|
|
108
|
+
['mono', MONO],
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
root: 'rounded',
|
|
113
|
+
property: 'border-radius',
|
|
114
|
+
group: 'border-radius',
|
|
115
|
+
varPrefix: 'radius',
|
|
116
|
+
labelPrefix: 'Radius',
|
|
117
|
+
values: [
|
|
118
|
+
['', '0.25rem'],
|
|
119
|
+
['sm', '0.125rem'],
|
|
120
|
+
['md', '0.375rem'],
|
|
121
|
+
['lg', '0.5rem'],
|
|
122
|
+
['xl', '0.75rem'],
|
|
123
|
+
['2xl', '1rem'],
|
|
124
|
+
['3xl', '1.5rem'],
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
root: 'shadow',
|
|
129
|
+
property: 'box-shadow',
|
|
130
|
+
group: 'shadow',
|
|
131
|
+
varPrefix: 'shadow',
|
|
132
|
+
labelPrefix: 'Shadow',
|
|
133
|
+
values: [
|
|
134
|
+
['', '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)'],
|
|
135
|
+
['sm', '0 1px 2px 0 rgb(0 0 0 / 0.05)'],
|
|
136
|
+
['md', '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)'],
|
|
137
|
+
['lg', '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)'],
|
|
138
|
+
['xl', '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)'],
|
|
139
|
+
['2xl', '0 25px 50px -12px rgb(0 0 0 / 0.25)'],
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
root: 'max-w',
|
|
144
|
+
property: 'max-width',
|
|
145
|
+
group: 'size',
|
|
146
|
+
varPrefix: 'container',
|
|
147
|
+
labelPrefix: 'Container',
|
|
148
|
+
values: [
|
|
149
|
+
['xs', '20rem'],
|
|
150
|
+
['sm', '24rem'],
|
|
151
|
+
['md', '28rem'],
|
|
152
|
+
['lg', '32rem'],
|
|
153
|
+
['xl', '36rem'],
|
|
154
|
+
['2xl', '42rem'],
|
|
155
|
+
['3xl', '48rem'],
|
|
156
|
+
['4xl', '56rem'],
|
|
157
|
+
['5xl', '64rem'],
|
|
158
|
+
['6xl', '72rem'],
|
|
159
|
+
['7xl', '80rem'],
|
|
160
|
+
['prose', '65ch'],
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
root: 'leading',
|
|
165
|
+
property: 'line-height',
|
|
166
|
+
group: 'line-height',
|
|
167
|
+
varPrefix: 'leading',
|
|
168
|
+
labelPrefix: 'Leading',
|
|
169
|
+
values: [
|
|
170
|
+
['none', '1'],
|
|
171
|
+
['tight', '1.25'],
|
|
172
|
+
['snug', '1.375'],
|
|
173
|
+
['relaxed', '1.625'],
|
|
174
|
+
['loose', '2'],
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
root: 'tracking',
|
|
179
|
+
property: 'letter-spacing',
|
|
180
|
+
group: 'letter-spacing',
|
|
181
|
+
varPrefix: 'tracking',
|
|
182
|
+
labelPrefix: 'Tracking',
|
|
183
|
+
values: [
|
|
184
|
+
['tighter', '-0.05em'],
|
|
185
|
+
['tight', '-0.025em'],
|
|
186
|
+
['wide', '0.025em'],
|
|
187
|
+
['wider', '0.05em'],
|
|
188
|
+
['widest', '0.1em'],
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
/** Size codes read better uppercased (`LG`, `2XL`); word names get title case (`Semibold`). */
|
|
194
|
+
function labelSuffix(suffix: string): string {
|
|
195
|
+
if (/^(xs|sm|md|lg|xl|\dxl)$/.test(suffix)) return suffix.toUpperCase();
|
|
196
|
+
return suffix.charAt(0).toUpperCase() + suffix.slice(1);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** All theme entries, flattened. */
|
|
200
|
+
export const THEME_SCALE_ENTRIES: ThemeScaleEntry[] = SPECS.flatMap((spec) =>
|
|
201
|
+
spec.values.map(([suffix, value]) => {
|
|
202
|
+
const className = suffix ? `${spec.root}-${suffix}` : spec.root;
|
|
203
|
+
const cssVar = suffix ? `--${spec.varPrefix}-${suffix}` : `--${spec.varPrefix}`;
|
|
204
|
+
const label = suffix ? `${spec.labelPrefix} ${labelSuffix(suffix)}` : spec.labelPrefix;
|
|
205
|
+
return { className, root: spec.root, cssVar, property: spec.property, value, group: spec.group, label };
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
/** className → entry (parser lookup). e.g. `text-lg`, `rounded`, `font-sans`. */
|
|
210
|
+
export const THEME_SCALE_BY_CLASS: ReadonlyMap<string, ThemeScaleEntry> = new Map(
|
|
211
|
+
THEME_SCALE_ENTRIES.map((e) => [e.className, e]),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
/** cssVar → entry (inverse-mapper lookup). e.g. `--text-lg` → `text-lg`. */
|
|
215
|
+
export const THEME_SCALE_BY_CSSVAR: ReadonlyMap<string, ThemeScaleEntry> = new Map(
|
|
216
|
+
THEME_SCALE_ENTRIES.map((e) => [e.cssVar, e]),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The CSS value a themed class resolves to: a plain `var(--token)` reference with no fallback.
|
|
221
|
+
* Renders the project variable when defined, inert otherwise (token-based by design — no baked
|
|
222
|
+
* Tailwind default in the render path).
|
|
223
|
+
*/
|
|
224
|
+
export function themeScaleValue(entry: ThemeScaleEntry): string {
|
|
225
|
+
return `var(${entry.cssVar})`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Map a variable group to its responsive-scaling type (mirrors getDefaultScalingType). */
|
|
229
|
+
function scalingTypeFor(group: VariableGroup): VariableType {
|
|
230
|
+
switch (group) {
|
|
231
|
+
case 'font-size':
|
|
232
|
+
return 'fontSize';
|
|
233
|
+
case 'border-radius':
|
|
234
|
+
return 'borderRadius';
|
|
235
|
+
case 'size':
|
|
236
|
+
return 'size';
|
|
237
|
+
default:
|
|
238
|
+
return 'none';
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The full set of Tailwind theme tokens as Meno `CSSVariable`s, ready to merge into a project's
|
|
244
|
+
* `variables.json`. Seeding makes the tokens visible/editable in the variable picker (filed under
|
|
245
|
+
* their group); rendering does NOT require seeding — the class carries the Tailwind default as the
|
|
246
|
+
* `var()` fallback. Idempotent merge is the caller's job (skip a `cssVar` that already exists).
|
|
247
|
+
*/
|
|
248
|
+
export function defaultTailwindThemeVariables(): CSSVariable[] {
|
|
249
|
+
return THEME_SCALE_ENTRIES.map((e) => ({
|
|
250
|
+
name: e.label,
|
|
251
|
+
cssVar: e.cssVar,
|
|
252
|
+
value: e.value,
|
|
253
|
+
type: scalingTypeFor(e.group),
|
|
254
|
+
group: e.group,
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
@@ -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)
|