meno-core 1.1.1 → 1.1.3
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-7ZLF4NE5.js → chunk-3JXK2QFU.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-J4IPTP5X.js → chunk-LOZL5HOF.js} +29 -3
- package/dist/chunks/chunk-LOZL5HOF.js.map +7 -0
- package/dist/lib/client/index.js +2 -2
- package/dist/lib/server/index.js +812 -218
- package/dist/lib/server/index.js.map +4 -4
- package/dist/lib/shared/index.js +4 -2
- package/dist/lib/shared/index.js.map +1 -1
- package/lib/client/scripts/formHandler.ts +17 -0
- package/lib/server/index.ts +19 -0
- package/lib/server/middleware/cors.test.ts +1 -1
- package/lib/server/middleware/cors.ts +1 -1
- package/lib/server/routes/api/functions.ts +2 -2
- package/lib/server/services/ColorService.ts +38 -7
- package/lib/server/services/VariableService.ts +34 -5
- 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/nodeUtils.ts +18 -0
- package/lib/shared/types/colors.ts +6 -2
- package/lib/shared/types/variables.ts +14 -7
- package/lib/shared/utilityClassMapper.test.ts +10 -0
- package/lib/shared/utilityClassMapper.ts +9 -1
- package/lib/shared/validation/schemas.test.ts +78 -0
- package/lib/shared/validation/schemas.ts +53 -5
- package/package.json +1 -1
- package/dist/chunks/chunk-J4IPTP5X.js.map +0 -7
- package/dist/chunks/chunk-QWTQZHG3.js.map +0 -7
- /package/dist/chunks/{chunk-7ZLF4NE5.js.map → chunk-3JXK2QFU.js.map} +0 -0
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
* Manages loading and caching of CSS variables from variables.json
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
6
7
|
import { loadJSONFile, parseJSON } from '../jsonLoader';
|
|
7
8
|
import { projectPaths } from '../projectContext';
|
|
8
9
|
import type { VariablesConfig } from '../../shared/types';
|
|
9
10
|
import { CachedConfigLoader } from './CachedConfigLoader';
|
|
10
11
|
import { writeFile } from '../runtime';
|
|
11
12
|
import { createLogger } from '../../shared/logger';
|
|
13
|
+
import { isThemeCssProject, loadThemeModel, saveVariables, themeCssPath } from '../themeCssStore';
|
|
12
14
|
|
|
13
15
|
const log = createLogger('VariableService');
|
|
14
16
|
|
|
@@ -21,6 +23,14 @@ export class VariableService extends CachedConfigLoader<VariablesConfig> {
|
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
protected async performLoad(): Promise<VariablesConfig> {
|
|
26
|
+
if (isThemeCssProject()) {
|
|
27
|
+
try {
|
|
28
|
+
return { variables: (await loadThemeModel()).variables };
|
|
29
|
+
} catch (error) {
|
|
30
|
+
log.warn('Failed to load variables from theme.css:', error);
|
|
31
|
+
return this.getDefaultConfig();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
24
34
|
try {
|
|
25
35
|
const content = await loadJSONFile(projectPaths.variables());
|
|
26
36
|
if (content) {
|
|
@@ -46,7 +56,7 @@ export class VariableService extends CachedConfigLoader<VariablesConfig> {
|
|
|
46
56
|
private migrateConfig(config: VariablesConfig): VariablesConfig {
|
|
47
57
|
for (const variable of config.variables) {
|
|
48
58
|
if ((variable.group as string) === 'spacing') {
|
|
49
|
-
const lower = `${variable.cssVar} ${variable.name}`.toLowerCase();
|
|
59
|
+
const lower = `${variable.cssVar} ${variable.name ?? ''}`.toLowerCase();
|
|
50
60
|
if (lower.includes('margin')) {
|
|
51
61
|
variable.group = 'margin';
|
|
52
62
|
} else if (lower.includes('gap')) {
|
|
@@ -68,6 +78,22 @@ export class VariableService extends CachedConfigLoader<VariablesConfig> {
|
|
|
68
78
|
error?: string;
|
|
69
79
|
filePath: string;
|
|
70
80
|
}> {
|
|
81
|
+
if (isThemeCssProject()) {
|
|
82
|
+
const filePath = themeCssPath();
|
|
83
|
+
if (!existsSync(filePath)) {
|
|
84
|
+
return { status: 'missing', config: this.getDefaultConfig(), filePath };
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
return { status: 'valid', config: { variables: (await loadThemeModel()).variables }, filePath };
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return {
|
|
90
|
+
status: 'invalid',
|
|
91
|
+
config: this.getDefaultConfig(),
|
|
92
|
+
error: error instanceof Error ? error.message : 'Parse error',
|
|
93
|
+
filePath,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
71
97
|
const filePath = projectPaths.variables();
|
|
72
98
|
try {
|
|
73
99
|
const content = await loadJSONFile(filePath);
|
|
@@ -95,12 +121,15 @@ export class VariableService extends CachedConfigLoader<VariablesConfig> {
|
|
|
95
121
|
}
|
|
96
122
|
|
|
97
123
|
/**
|
|
98
|
-
* Save variables configuration
|
|
124
|
+
* Save variables configuration. Astro projects write the variables half of
|
|
125
|
+
* `src/styles/theme.css` (preserving colors); legacy JSON projects write `variables.json`.
|
|
99
126
|
*/
|
|
100
127
|
async saveConfig(config: VariablesConfig): Promise<void> {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
128
|
+
if (isThemeCssProject()) {
|
|
129
|
+
await saveVariables(config.variables);
|
|
130
|
+
} else {
|
|
131
|
+
await writeFile(projectPaths.variables(), JSON.stringify(config, null, 2));
|
|
132
|
+
}
|
|
104
133
|
this.setCache(config);
|
|
105
134
|
}
|
|
106
135
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { parseThemeCss, serializeThemeCss, type ScaleConfig, type ThemeModel } from './themeCssCodec';
|
|
3
|
+
import type { BreakpointConfig } from '../shared/breakpoints';
|
|
4
|
+
import { DEFAULT_RESPONSIVE_SCALES } from '../shared/responsiveScaling';
|
|
5
|
+
|
|
6
|
+
const BREAKPOINTS: BreakpointConfig = {
|
|
7
|
+
tablet: { breakpoint: 1024, previewPoint: 768 },
|
|
8
|
+
mobile: { breakpoint: 540, previewPoint: 375 },
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const NO_SCALE: ScaleConfig = {
|
|
12
|
+
breakpoints: BREAKPOINTS,
|
|
13
|
+
responsiveScales: { ...DEFAULT_RESPONSIVE_SCALES, enabled: false },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const SCALE_ON: ScaleConfig = {
|
|
17
|
+
breakpoints: BREAKPOINTS,
|
|
18
|
+
responsiveScales: { ...DEFAULT_RESPONSIVE_SCALES, enabled: true, baseReference: 16 },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe('parseThemeCss', () => {
|
|
22
|
+
test('reads colors (default on :root, others on [theme]) and variables by section', () => {
|
|
23
|
+
const css = `:root {
|
|
24
|
+
/* Colors: light */
|
|
25
|
+
--text: #050505;
|
|
26
|
+
--background: #ffffff;
|
|
27
|
+
|
|
28
|
+
/* Font Size */
|
|
29
|
+
--h1-size-1: 48px;
|
|
30
|
+
|
|
31
|
+
/* Padding */
|
|
32
|
+
--section-pad: 64px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
[theme="dark"] {
|
|
36
|
+
--text: #ffffff;
|
|
37
|
+
--background: #111111;
|
|
38
|
+
}`;
|
|
39
|
+
const model = parseThemeCss(css, BREAKPOINTS);
|
|
40
|
+
expect(model.themes.default).toBe('light');
|
|
41
|
+
expect(model.themes.themes.light?.colors).toEqual({ text: '#050505', background: '#ffffff' });
|
|
42
|
+
expect(model.themes.themes.dark?.colors).toEqual({ text: '#ffffff', background: '#111111' });
|
|
43
|
+
|
|
44
|
+
const fs = model.variables.find((v) => v.cssVar === '--h1-size-1');
|
|
45
|
+
expect(fs).toMatchObject({ value: '48px', group: 'font-size', type: 'fontSize' });
|
|
46
|
+
const pad = model.variables.find((v) => v.cssVar === '--section-pad');
|
|
47
|
+
expect(pad).toMatchObject({ value: '64px', group: 'padding', type: 'padding' });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('override @media maps to per-variable scales; auto region is discarded', () => {
|
|
51
|
+
const css = `:root {
|
|
52
|
+
/* Font Size */
|
|
53
|
+
--h1: 48px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* === Responsive scaling — auto-generated from project.config.json; regenerated on save === */
|
|
57
|
+
@media (max-width: 1024px) {
|
|
58
|
+
:root {
|
|
59
|
+
--h1: 40px;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* === Responsive overrides — per-variable; preserved across regen === */
|
|
64
|
+
@media (max-width: 540px) {
|
|
65
|
+
:root {
|
|
66
|
+
--h1: 28px;
|
|
67
|
+
}
|
|
68
|
+
}`;
|
|
69
|
+
const model = parseThemeCss(css, BREAKPOINTS);
|
|
70
|
+
const h1 = model.variables.find((v) => v.cssVar === '--h1');
|
|
71
|
+
// auto (tablet 40px) discarded; override (mobile 28px) preserved
|
|
72
|
+
expect(h1?.scales).toEqual({ mobile: '28px' });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('preserves unmodeled CSS verbatim in raw', () => {
|
|
76
|
+
const css = `:root {
|
|
77
|
+
/* Colors: light */
|
|
78
|
+
--text: #000;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@font-face {
|
|
82
|
+
font-family: "Inter";
|
|
83
|
+
src: url("/fonts/inter.woff2") format("woff2");
|
|
84
|
+
}`;
|
|
85
|
+
const model = parseThemeCss(css, BREAKPOINTS);
|
|
86
|
+
expect(model.raw.join('\n')).toContain('@font-face');
|
|
87
|
+
expect(model.raw.join('\n')).toContain('inter.woff2');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('serializeThemeCss', () => {
|
|
92
|
+
test('emits grouped :root with colors + variable sections', () => {
|
|
93
|
+
const model: ThemeModel = {
|
|
94
|
+
themes: { default: 'light', themes: { light: { label: 'Light', colors: { text: '#050505' } } } },
|
|
95
|
+
variables: [{ cssVar: '--h1', value: '48px', type: 'fontSize', group: 'font-size' }],
|
|
96
|
+
raw: [],
|
|
97
|
+
};
|
|
98
|
+
const css = serializeThemeCss(model, NO_SCALE);
|
|
99
|
+
expect(css).toContain('/* Colors: light */');
|
|
100
|
+
expect(css).toContain('--text: #050505;');
|
|
101
|
+
expect(css).toContain('/* Font Size */');
|
|
102
|
+
expect(css).toContain('--h1: 48px;');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('regenerates auto @media from scale config when enabled', () => {
|
|
106
|
+
const model: ThemeModel = {
|
|
107
|
+
themes: { default: 'light', themes: { light: { colors: {} } } },
|
|
108
|
+
variables: [{ cssVar: '--h1', value: '48px', type: 'fontSize', group: 'font-size' }],
|
|
109
|
+
raw: [],
|
|
110
|
+
};
|
|
111
|
+
const css = serializeThemeCss(model, SCALE_ON);
|
|
112
|
+
expect(css).toContain('Responsive scaling');
|
|
113
|
+
expect(css).toMatch(/@media \(max-width: 1024px\)/);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('round-trip', () => {
|
|
118
|
+
const model: ThemeModel = {
|
|
119
|
+
themes: {
|
|
120
|
+
default: 'light',
|
|
121
|
+
themes: {
|
|
122
|
+
light: { label: 'Light', colors: { text: '#050505', background: '#ffffff' } },
|
|
123
|
+
dark: { label: 'Dark', colors: { text: '#ffffff', background: '#111111' } },
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
variables: [
|
|
127
|
+
{ cssVar: '--h1-size-1', value: '48px', type: 'fontSize', group: 'font-size', scales: { mobile: '28px' } },
|
|
128
|
+
{ cssVar: '--section-pad', value: '64px', type: 'padding', group: 'padding' },
|
|
129
|
+
{ cssVar: '--brand-font', value: 'Inter, sans-serif', type: 'none', group: 'font-family' },
|
|
130
|
+
],
|
|
131
|
+
raw: ['@font-face {\n font-family: "Inter";\n src: url("/x.woff2");\n}'],
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
test('css → model → css is stable', () => {
|
|
135
|
+
const css1 = serializeThemeCss(model, SCALE_ON);
|
|
136
|
+
const model2 = parseThemeCss(css1, BREAKPOINTS);
|
|
137
|
+
const css2 = serializeThemeCss(model2, SCALE_ON);
|
|
138
|
+
expect(css2).toBe(css1);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('persisted model fields survive a round-trip', () => {
|
|
142
|
+
const css = serializeThemeCss(model, SCALE_ON);
|
|
143
|
+
const back = parseThemeCss(css, BREAKPOINTS);
|
|
144
|
+
expect(back.themes.default).toBe('light');
|
|
145
|
+
expect(Object.keys(back.themes.themes).sort()).toEqual(['dark', 'light']);
|
|
146
|
+
expect(back.themes.themes.dark?.colors.text).toBe('#ffffff');
|
|
147
|
+
|
|
148
|
+
const h1 = back.variables.find((v) => v.cssVar === '--h1-size-1');
|
|
149
|
+
expect(h1).toMatchObject({ value: '48px', group: 'font-size', scales: { mobile: '28px' } });
|
|
150
|
+
const font = back.variables.find((v) => v.cssVar === '--brand-font');
|
|
151
|
+
expect(font).toMatchObject({ value: 'Inter, sans-serif', group: 'font-family', type: 'none' });
|
|
152
|
+
expect(back.raw.join('\n')).toContain('@font-face');
|
|
153
|
+
});
|
|
154
|
+
});
|