eva-css-fluid 1.0.3 → 2.0.0

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.
@@ -0,0 +1,455 @@
1
+ /**
2
+ * EVA CSS Configuration Schema
3
+ *
4
+ * Defines and validates the structure of eva.config.js and package.json "eva" configuration
5
+ */
6
+
7
+ /**
8
+ * Default configuration values
9
+ */
10
+ const defaults = {
11
+ sizes: [4, 8, 12, 16, 24, 32, 48, 64, 96, 128],
12
+ fontSizes: [12, 14, 16, 18, 20, 24, 32, 48],
13
+ buildClass: true,
14
+ pxRemSuffix: false,
15
+ nameBySize: true,
16
+ customClass: false,
17
+ classConfig: {},
18
+ debug: false,
19
+ theme: {
20
+ name: 'eva',
21
+ colors: {
22
+ brand: { lightness: 80, chroma: 0.1924, hue: 169 },
23
+ accent: { lightness: 80, chroma: 0.1924, hue: 10 },
24
+ extra: { lightness: 80, chroma: 0.1924, hue: 200 },
25
+ dark: { lightness: 20, chroma: 0.05, hue: 200 },
26
+ light: { lightness: 95, chroma: 0.01, hue: 200 }
27
+ },
28
+ lightMode: {
29
+ lightness: 96.4,
30
+ darkness: 6.4
31
+ },
32
+ darkMode: {
33
+ lightness: 5,
34
+ darkness: 95
35
+ },
36
+ autoSwitch: false
37
+ },
38
+ purge: {
39
+ enabled: false,
40
+ content: ['**/*.{html,js,jsx,tsx,vue,svelte}'],
41
+ css: 'dist/eva.css',
42
+ output: 'dist/eva-purged.css',
43
+ safelist: ['theme-', 'current-', 'toggle-theme', 'all-grads']
44
+ }
45
+ };
46
+
47
+ /**
48
+ * Available property keys for classConfig
49
+ */
50
+ const availableProperties = [
51
+ 'w', 'mw', 'h', 'mh',
52
+ 'p', 'px', 'py', 'pt', 'pb', 'pr', 'pl',
53
+ 'm', 'mx', 'my', 'mt', 'mb', 'mr', 'ml',
54
+ 'g', 'gap', 'br',
55
+ 'fs', 'lh'
56
+ ];
57
+
58
+ /**
59
+ * Validation errors
60
+ */
61
+ class ValidationError extends Error {
62
+ constructor(message, field) {
63
+ super(message);
64
+ this.name = 'ValidationError';
65
+ this.field = field;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Validate configuration object
71
+ *
72
+ * @param {Object} config - Configuration object to validate
73
+ * @param {string} source - Source of config ('eva.config.js' or 'package.json')
74
+ * @returns {Object} Validated configuration
75
+ * @throws {ValidationError} If configuration is invalid
76
+ */
77
+ function validateConfig(config, source = 'config file') {
78
+ if (!config || typeof config !== 'object') {
79
+ throw new ValidationError(
80
+ `Configuration must be an object, received ${typeof config}`,
81
+ 'root'
82
+ );
83
+ }
84
+
85
+ const errors = [];
86
+ const warnings = [];
87
+
88
+ // Validate sizes
89
+ if (config.sizes !== undefined) {
90
+ if (!Array.isArray(config.sizes)) {
91
+ errors.push('sizes: Must be an array of numbers');
92
+ } else if (config.sizes.length === 0) {
93
+ errors.push('sizes: Array cannot be empty');
94
+ } else if (!config.sizes.every(s => typeof s === 'number' && s > 0)) {
95
+ errors.push('sizes: All values must be positive numbers');
96
+ } else if (!config.sizes.includes(16)) {
97
+ errors.push('sizes: Must include 16 as a base size (required by EVA CSS)');
98
+ }
99
+ }
100
+
101
+ // Validate fontSizes
102
+ if (config.fontSizes !== undefined) {
103
+ if (!Array.isArray(config.fontSizes)) {
104
+ errors.push('fontSizes: Must be an array of numbers');
105
+ } else if (config.fontSizes.length === 0) {
106
+ errors.push('fontSizes: Array cannot be empty');
107
+ } else if (!config.fontSizes.every(s => typeof s === 'number' && s > 0)) {
108
+ errors.push('fontSizes: All values must be positive numbers');
109
+ }
110
+ }
111
+
112
+ // Validate boolean flags
113
+ const booleanFlags = ['buildClass', 'pxRemSuffix', 'nameBySize', 'customClass', 'debug'];
114
+ booleanFlags.forEach(flag => {
115
+ if (config[flag] !== undefined && typeof config[flag] !== 'boolean') {
116
+ errors.push(`${flag}: Must be a boolean (true or false)`);
117
+ }
118
+ });
119
+
120
+ // Validate classConfig
121
+ if (config.classConfig !== undefined) {
122
+ if (typeof config.classConfig !== 'object' || Array.isArray(config.classConfig)) {
123
+ errors.push('classConfig: Must be an object mapping properties to size arrays');
124
+ } else {
125
+ // Check if customClass is enabled when classConfig is provided
126
+ if (config.customClass === false && Object.keys(config.classConfig).length > 0) {
127
+ warnings.push('classConfig: Setting classConfig has no effect when customClass is false');
128
+ }
129
+
130
+ // Validate each property in classConfig
131
+ Object.entries(config.classConfig).forEach(([prop, sizes]) => {
132
+ if (!availableProperties.includes(prop)) {
133
+ errors.push(
134
+ `classConfig.${prop}: Unknown property. Available properties: ${availableProperties.join(', ')}`
135
+ );
136
+ }
137
+
138
+ if (!Array.isArray(sizes)) {
139
+ errors.push(`classConfig.${prop}: Must be an array of size values`);
140
+ } else if (sizes.length === 0) {
141
+ errors.push(`classConfig.${prop}: Array cannot be empty`);
142
+ } else {
143
+ // Validate that all sizes in classConfig exist in the main sizes array
144
+ const mainSizes = config.sizes || defaults.sizes;
145
+ sizes.forEach(size => {
146
+ if (!mainSizes.includes(size)) {
147
+ errors.push(
148
+ `classConfig.${prop}: Size ${size} is not in the main sizes array [${mainSizes.join(', ')}]`
149
+ );
150
+ }
151
+ });
152
+ }
153
+ });
154
+ }
155
+ }
156
+
157
+ // Validate theme configuration
158
+ if (config.theme !== undefined) {
159
+ if (typeof config.theme !== 'object' || Array.isArray(config.theme)) {
160
+ errors.push('theme: Must be an object');
161
+ } else {
162
+ // Validate theme name
163
+ if (config.theme.name !== undefined) {
164
+ if (typeof config.theme.name !== 'string' || config.theme.name.trim() === '') {
165
+ errors.push('theme.name: Must be a non-empty string');
166
+ }
167
+ }
168
+
169
+ // Validate colors
170
+ if (config.theme.colors !== undefined) {
171
+ if (typeof config.theme.colors !== 'object' || Array.isArray(config.theme.colors)) {
172
+ errors.push('theme.colors: Must be an object');
173
+ } else {
174
+ const validColorNames = ['brand', 'accent', 'extra', 'dark', 'light'];
175
+
176
+ Object.entries(config.theme.colors).forEach(([colorName, colorValue]) => {
177
+ if (!validColorNames.includes(colorName)) {
178
+ errors.push(
179
+ `theme.colors.${colorName}: Unknown color. Valid colors: ${validColorNames.join(', ')}`
180
+ );
181
+ }
182
+
183
+ // Validate color value (HEX string or OKLCH object)
184
+ if (typeof colorValue === 'string') {
185
+ // HEX format
186
+ if (!/^#[0-9A-Fa-f]{6}$/.test(colorValue)) {
187
+ errors.push(
188
+ `theme.colors.${colorName}: Invalid HEX color "${colorValue}". Must be format #RRGGBB`
189
+ );
190
+ }
191
+ } else if (typeof colorValue === 'object' && !Array.isArray(colorValue)) {
192
+ // OKLCH format
193
+ if (typeof colorValue.lightness !== 'number' || colorValue.lightness < 0 || colorValue.lightness > 100) {
194
+ errors.push(
195
+ `theme.colors.${colorName}.lightness: Must be a number between 0 and 100`
196
+ );
197
+ }
198
+ if (typeof colorValue.chroma !== 'number' || colorValue.chroma < 0 || colorValue.chroma > 0.4) {
199
+ errors.push(
200
+ `theme.colors.${colorName}.chroma: Must be a number between 0 and 0.4`
201
+ );
202
+ }
203
+ if (typeof colorValue.hue !== 'number' || colorValue.hue < 0 || colorValue.hue > 360) {
204
+ errors.push(
205
+ `theme.colors.${colorName}.hue: Must be a number between 0 and 360`
206
+ );
207
+ }
208
+ } else {
209
+ errors.push(
210
+ `theme.colors.${colorName}: Must be a HEX string (#RRGGBB) or OKLCH object {lightness, chroma, hue}`
211
+ );
212
+ }
213
+ });
214
+ }
215
+ }
216
+
217
+ // Validate lightMode
218
+ if (config.theme.lightMode !== undefined) {
219
+ if (typeof config.theme.lightMode !== 'object' || Array.isArray(config.theme.lightMode)) {
220
+ errors.push('theme.lightMode: Must be an object with lightness and darkness properties');
221
+ } else {
222
+ if (typeof config.theme.lightMode.lightness !== 'number' || config.theme.lightMode.lightness < 0 || config.theme.lightMode.lightness > 100) {
223
+ errors.push('theme.lightMode.lightness: Must be a number between 0 and 100');
224
+ }
225
+ if (typeof config.theme.lightMode.darkness !== 'number' || config.theme.lightMode.darkness < 0 || config.theme.lightMode.darkness > 100) {
226
+ errors.push('theme.lightMode.darkness: Must be a number between 0 and 100');
227
+ }
228
+ }
229
+ }
230
+
231
+ // Validate darkMode
232
+ if (config.theme.darkMode !== undefined) {
233
+ if (typeof config.theme.darkMode !== 'object' || Array.isArray(config.theme.darkMode)) {
234
+ errors.push('theme.darkMode: Must be an object with lightness and darkness properties');
235
+ } else {
236
+ if (typeof config.theme.darkMode.lightness !== 'number' || config.theme.darkMode.lightness < 0 || config.theme.darkMode.lightness > 100) {
237
+ errors.push('theme.darkMode.lightness: Must be a number between 0 and 100');
238
+ }
239
+ if (typeof config.theme.darkMode.darkness !== 'number' || config.theme.darkMode.darkness < 0 || config.theme.darkMode.darkness > 100) {
240
+ errors.push('theme.darkMode.darkness: Must be a number between 0 and 100');
241
+ }
242
+ }
243
+ }
244
+
245
+ // Validate autoSwitch
246
+ if (config.theme.autoSwitch !== undefined && typeof config.theme.autoSwitch !== 'boolean') {
247
+ errors.push('theme.autoSwitch: Must be a boolean');
248
+ }
249
+ }
250
+ }
251
+
252
+ // Validate purge configuration
253
+ if (config.purge !== undefined) {
254
+ if (typeof config.purge !== 'object' || Array.isArray(config.purge)) {
255
+ errors.push('purge: Must be an object');
256
+ } else {
257
+ if (config.purge.enabled !== undefined && typeof config.purge.enabled !== 'boolean') {
258
+ errors.push('purge.enabled: Must be a boolean');
259
+ }
260
+
261
+ if (config.purge.content !== undefined) {
262
+ if (!Array.isArray(config.purge.content)) {
263
+ errors.push('purge.content: Must be an array of glob patterns');
264
+ } else if (config.purge.content.length === 0) {
265
+ errors.push('purge.content: Array cannot be empty');
266
+ }
267
+ }
268
+
269
+ if (config.purge.css !== undefined && typeof config.purge.css !== 'string') {
270
+ errors.push('purge.css: Must be a string (path to CSS file)');
271
+ }
272
+
273
+ if (config.purge.output !== undefined && typeof config.purge.output !== 'string') {
274
+ errors.push('purge.output: Must be a string (output path)');
275
+ }
276
+
277
+ if (config.purge.safelist !== undefined && !Array.isArray(config.purge.safelist)) {
278
+ errors.push('purge.safelist: Must be an array of strings or patterns');
279
+ }
280
+ }
281
+ }
282
+
283
+ // Throw if there are errors
284
+ if (errors.length > 0) {
285
+ const errorMessage = [
286
+ `Invalid EVA CSS configuration in ${source}:`,
287
+ '',
288
+ ...errors.map(e => ` ❌ ${e}`),
289
+ '',
290
+ 'See documentation: https://eva-css.xyz/configuration'
291
+ ].join('\n');
292
+
293
+ throw new ValidationError(errorMessage, 'validation');
294
+ }
295
+
296
+ // Display warnings
297
+ if (warnings.length > 0) {
298
+ console.warn(`\n⚠️ EVA CSS configuration warnings in ${source}:`);
299
+ warnings.forEach(w => console.warn(` ${w}`));
300
+ console.warn('');
301
+ }
302
+
303
+ return config;
304
+ }
305
+
306
+ /**
307
+ * Merge user configuration with defaults
308
+ *
309
+ * @param {Object} userConfig - User provided configuration
310
+ * @returns {Object} Merged configuration
311
+ */
312
+ function mergeWithDefaults(userConfig) {
313
+ const config = { ...defaults };
314
+
315
+ // Merge top-level properties
316
+ Object.keys(userConfig).forEach(key => {
317
+ if (key === 'purge' && typeof userConfig.purge === 'object') {
318
+ // Deep merge purge configuration
319
+ config.purge = {
320
+ ...defaults.purge,
321
+ ...userConfig.purge
322
+ };
323
+ } else if (key === 'theme' && typeof userConfig.theme === 'object') {
324
+ // Deep merge theme configuration
325
+ config.theme = {
326
+ ...defaults.theme,
327
+ ...userConfig.theme,
328
+ colors: {
329
+ ...defaults.theme.colors,
330
+ ...(userConfig.theme.colors || {})
331
+ },
332
+ lightMode: {
333
+ ...defaults.theme.lightMode,
334
+ ...(userConfig.theme.lightMode || {})
335
+ },
336
+ darkMode: {
337
+ ...defaults.theme.darkMode,
338
+ ...(userConfig.theme.darkMode || {})
339
+ }
340
+ };
341
+ } else if (key === 'classConfig' && typeof userConfig.classConfig === 'object') {
342
+ // Replace classConfig entirely (no merge)
343
+ config.classConfig = userConfig.classConfig;
344
+ } else {
345
+ config[key] = userConfig[key];
346
+ }
347
+ });
348
+
349
+ return config;
350
+ }
351
+
352
+ /**
353
+ * Convert JavaScript config to SCSS variable format
354
+ *
355
+ * @param {Object} config - Configuration object
356
+ * @returns {string} SCSS variable declarations
357
+ */
358
+ function toScssVariables(config) {
359
+ const lines = [];
360
+
361
+ // Sizes
362
+ if (config.sizes) {
363
+ lines.push(`$sizes: ${config.sizes.join(', ')};`);
364
+ }
365
+
366
+ // Font sizes
367
+ if (config.fontSizes) {
368
+ lines.push(`$font-sizes: ${config.fontSizes.join(', ')};`);
369
+ }
370
+
371
+ // Boolean flags
372
+ lines.push(`$build-class: ${config.buildClass};`);
373
+ lines.push(`$px-rem-suffix: ${config.pxRemSuffix};`);
374
+ lines.push(`$name-by-size: ${config.nameBySize};`);
375
+ lines.push(`$custom-class: ${config.customClass};`);
376
+ lines.push(`$debug: ${config.debug};`);
377
+
378
+ // Class config (convert to SCSS map)
379
+ if (config.customClass && Object.keys(config.classConfig).length > 0) {
380
+ lines.push('$class-config: (');
381
+ Object.entries(config.classConfig).forEach(([prop, sizes], index, arr) => {
382
+ const isLast = index === arr.length - 1;
383
+ // Handle single-item arrays with trailing comma for SCSS
384
+ const sizesList = sizes.length === 1 ? `${sizes[0]},` : sizes.join(', ');
385
+ lines.push(` ${prop}: (${sizesList})${isLast ? '' : ','}`);
386
+ });
387
+ lines.push(');');
388
+ } else {
389
+ lines.push('$class-config: ();');
390
+ }
391
+
392
+ // Theme configuration
393
+ if (config.theme) {
394
+ lines.push('');
395
+ lines.push('// Theme configuration');
396
+ lines.push(`$theme-name: '${config.theme.name}';`);
397
+ lines.push(`$auto-theme-switch: ${config.theme.autoSwitch};`);
398
+
399
+ // Light and dark mode settings
400
+ lines.push(`$light-mode-lightness: ${config.theme.lightMode.lightness}%;`);
401
+ lines.push(`$light-mode-darkness: ${config.theme.lightMode.darkness}%;`);
402
+ lines.push(`$dark-mode-lightness: ${config.theme.darkMode.lightness}%;`);
403
+ lines.push(`$dark-mode-darkness: ${config.theme.darkMode.darkness}%;`);
404
+
405
+ // Colors map
406
+ lines.push('$theme-colors: (');
407
+ const colorEntries = Object.entries(config.theme.colors);
408
+ colorEntries.forEach(([colorName, colorValue], index) => {
409
+ const isLast = index === colorEntries.length - 1;
410
+ lines.push(` '${colorName}': (${colorValue.lightness}% ${colorValue.chroma} ${colorValue.hue})${isLast ? '' : ','}`);
411
+ });
412
+ lines.push(');');
413
+ }
414
+
415
+ return lines.join('\n');
416
+ }
417
+
418
+ /**
419
+ * Normalize config from package.json format to standard format
420
+ * Handles camelCase vs kebab-case conversions
421
+ *
422
+ * @param {Object} config - Configuration from package.json or eva.config.js
423
+ * @returns {Object} Normalized configuration
424
+ */
425
+ function normalizeConfig(config) {
426
+ const normalized = {};
427
+
428
+ // Map camelCase to internal format
429
+ const keyMap = {
430
+ fontSizes: 'fontSizes',
431
+ buildClass: 'buildClass',
432
+ pxRemSuffix: 'pxRemSuffix',
433
+ nameBySize: 'nameBySize',
434
+ customClass: 'customClass',
435
+ classConfig: 'classConfig'
436
+ };
437
+
438
+ Object.entries(config).forEach(([key, value]) => {
439
+ // Use mapped key if it exists, otherwise use original key
440
+ const normalizedKey = keyMap[key] || key;
441
+ normalized[normalizedKey] = value;
442
+ });
443
+
444
+ return normalized;
445
+ }
446
+
447
+ module.exports = {
448
+ defaults,
449
+ availableProperties,
450
+ ValidationError,
451
+ validateConfig,
452
+ mergeWithDefaults,
453
+ toScssVariables,
454
+ normalizeConfig
455
+ };
package/src/core.scss ADDED
@@ -0,0 +1,43 @@
1
+ // ===========================================
2
+ // EVA CSS - Core Framework Entry Point
3
+ // ===========================================
4
+ // Use this when you want the framework
5
+ // without pre-built utility classes
6
+ // Perfect for building your own components
7
+ // ===========================================
8
+
9
+ // Configuration variables with defaults
10
+ $sizes: 4, 8, 12, 16, 24, 32, 48, 64, 96, 128 !default;
11
+ $font-sizes: 12, 14, 16, 18, 20, 24, 32, 48 !default;
12
+ $px-rem-suffix: false !default;
13
+ $name-by-size: true !default;
14
+ $class-config: () !default;
15
+
16
+ // Core variable generation system
17
+ @use '_eva' with (
18
+ $sizes: $sizes,
19
+ $font-sizes: $font-sizes,
20
+ $build-class: false, // ← No utility classes
21
+ $px-rem-suffix: $px-rem-suffix,
22
+ $name-by-size: $name-by-size,
23
+ $custom-class: false,
24
+ $class-config: $class-config
25
+ );
26
+
27
+ // Color system
28
+ @use '_colors';
29
+
30
+ // Gradients
31
+ @use '_gradients';
32
+
33
+ // Theme system
34
+ @use '_theme';
35
+
36
+ // CSS Reset
37
+ @use '_reset';
38
+
39
+ // Typography
40
+ @use '_font';
41
+
42
+ // Result: Variables + theme + reset + typography
43
+ // No utility classes - build your own!
package/src/index.scss CHANGED
@@ -13,6 +13,8 @@ $build-class: true !default;
13
13
  $px-rem-suffix: false !default;
14
14
  $name-by-size: true !default;
15
15
  $custom-class: false !default;
16
+ $class-config: () !default;
17
+ $debug: false !default;
16
18
 
17
19
  // ===========================================
18
20
  // CORE FRAMEWORK MODULES
@@ -27,7 +29,9 @@ $custom-class: false !default;
27
29
  $build-class: $build-class,
28
30
  $px-rem-suffix: $px-rem-suffix,
29
31
  $name-by-size: $name-by-size,
30
- $custom-class: $custom-class
32
+ $custom-class: $custom-class,
33
+ $class-config: $class-config,
34
+ $debug: $debug
31
35
  );
32
36
 
33
37
  // OKLCH color system with opacity/brightness modifiers
@@ -0,0 +1,33 @@
1
+ // ===========================================
2
+ // EVA CSS - Variables Only Entry Point
3
+ // ===========================================
4
+ // Use this when you only want CSS variables
5
+ // and no utility classes
6
+ // ===========================================
7
+
8
+ // Configuration variables with defaults
9
+ $sizes: 4, 8, 12, 16, 24, 32, 48, 64, 96, 128 !default;
10
+ $font-sizes: 12, 14, 16, 18, 20, 24, 32, 48 !default;
11
+ $px-rem-suffix: false !default;
12
+ $name-by-size: true !default;
13
+ $class-config: () !default;
14
+
15
+ // Import only the core variable generation system
16
+ @use '_eva' with (
17
+ $sizes: $sizes,
18
+ $font-sizes: $font-sizes,
19
+ $build-class: false, // ← No utility classes
20
+ $px-rem-suffix: $px-rem-suffix,
21
+ $name-by-size: $name-by-size,
22
+ $custom-class: false,
23
+ $class-config: $class-config
24
+ );
25
+
26
+ // Import colors system
27
+ @use '_colors';
28
+
29
+ // Import theme system
30
+ @use '_theme';
31
+
32
+ // Result: Only CSS variables like var(--16), var(--brand), etc.
33
+ // Perfect for custom component development!