chaincss 1.12.14 → 1.13.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.
package/README.md CHANGED
@@ -144,4 +144,4 @@ just go to the docs section of [https://chaincss.dev](https://www.chaincss.dev)
144
144
 
145
145
  ## License
146
146
 
147
- MIT © [Rommel Caneos](https://github.com/melcanz08)
147
+ MIT © [Rommel Caneos](https://github.com/melcanz08)
@@ -1,4 +1,5 @@
1
- import React, { useMemo, useEffect, useRef, useState } from 'react';
1
+ import React from 'react';
2
+ const { useMemo, useEffect, useRef, useState } = React;
2
3
  import { $, compile, chain } from './rtt';
3
4
 
4
5
  const styleCache = new Map();
@@ -135,4 +136,27 @@ export function withChainStyles(styles, options = {}) {
135
136
 
136
137
  export function cx(...classes) {
137
138
  return classes.filter(Boolean).join(' ');
139
+ }
140
+
141
+ let debugEnabled = false;
142
+
143
+ export function enableChainCSSDebug() {
144
+ if (typeof window !== 'undefined') {
145
+ debugEnabled = true;
146
+ window.__CHAINCSS_DEBUG__ = true;
147
+ console.log('🔍 ChainCSS Debug Mode Enabled');
148
+ console.log('💡 Tip: Hover over elements to see their atomic classes');
149
+ }
150
+ }
151
+
152
+ export function disableChainCSSDebug() {
153
+ if (typeof window !== 'undefined') {
154
+ debugEnabled = false;
155
+ window.__CHAINCSS_DEBUG__ = false;
156
+ console.log('🔍 ChainCSS Debug Mode Disabled');
157
+ }
158
+ }
159
+
160
+ export function isDebugEnabled() {
161
+ return debugEnabled || (typeof window !== 'undefined' && window.__CHAINCSS_DEBUG__);
138
162
  }
package/node/btt.js CHANGED
@@ -353,6 +353,82 @@ function $(useTokens = true) {
353
353
  };
354
354
  }
355
355
 
356
+ // theme method
357
+ if (prop === 'theme') {
358
+ return function(themeTokens, callback) {
359
+ // Store original tokens to restore later
360
+ const originalTokens = tokens;
361
+
362
+ // Create a temporary token store for this theme
363
+ const themeTokenStore = {
364
+ get: (path) => {
365
+ // Try to get from theme tokens first, fallback to original
366
+ const themeValue = themeTokens.get ? themeTokens.get(path) : null;
367
+ if (themeValue !== null && themeValue !== undefined) {
368
+ return themeValue;
369
+ }
370
+ return originalTokens.get(path);
371
+ },
372
+ // Make it look like the original tokens object
373
+ ...themeTokens
374
+ };
375
+
376
+ // Create a proxy to intercept token resolution
377
+ const tokenProxy = new Proxy(themeTokenStore, {
378
+ get: (target, prop) => {
379
+ if (prop === 'get') {
380
+ return target.get.bind(target);
381
+ }
382
+ return target[prop];
383
+ }
384
+ });
385
+
386
+ // Temporarily override the global tokens
387
+ const originalTokensRef = globalThis.__CHAINCSS_TOKENS__ || tokens;
388
+ const tempTokens = themeTokenStore;
389
+
390
+ // Create a new $ function that uses the theme tokens
391
+ const themed$ = (useTokens = true) => {
392
+ // Store original resolver
393
+ const originalResolver = resolveToken;
394
+
395
+ // Create temporary token resolver
396
+ const themeResolver = (value, useTokensFlag) => {
397
+ if (!useTokensFlag || typeof value !== 'string' || !value.startsWith('$')) {
398
+ return value;
399
+ }
400
+ const tokenPath = value.slice(1);
401
+ const tokenValue = tempTokens.get(tokenPath);
402
+ return tokenValue || value;
403
+ };
404
+
405
+ // Temporarily replace resolveToken
406
+ globalThis.__CHAINCSS_TEMP_RESOLVER__ = themeResolver;
407
+
408
+ const result = $(useTokens);
409
+
410
+ // Restore original resolver
411
+ delete globalThis.__CHAINCSS_TEMP_RESOLVER__;
412
+
413
+ return result;
414
+ };
415
+
416
+ // Execute callback with themed chain
417
+ const result = callback(themed$);
418
+
419
+ // Store theme data for CSS generation
420
+ if (!catcher.themes) catcher.themes = [];
421
+ catcher.themes.push({
422
+ name: `theme-${Date.now()}`,
423
+ styles: result,
424
+ tokens: themeTokens,
425
+ fallback: originalTokens
426
+ });
427
+
428
+ return proxy;
429
+ };
430
+ }
431
+
356
432
  // Regular CSS properties
357
433
  const cssProperty = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
358
434
  if (validProperties && validProperties.length > 0 && !validProperties.includes(cssProperty)) {
@@ -657,6 +733,29 @@ const compile = (obj) => {
657
733
  if (!obj.hasOwnProperty(key)) continue;
658
734
  const element = obj[key];
659
735
 
736
+ // Handle themes
737
+ if (element.themes && Array.isArray(element.themes)) {
738
+ element.themes.forEach(theme => {
739
+ // Generate CSS for each theme variant
740
+ if (theme.styles && theme.styles.selectors) {
741
+ let themeCSS = '';
742
+ let themeSelectors = theme.styles.selectors || [];
743
+
744
+ for (let prop in theme.styles) {
745
+ if (prop !== 'selectors' && theme.styles.hasOwnProperty(prop)) {
746
+ const kebabKey = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
747
+ themeCSS += ` ${kebabKey}: ${theme.styles[prop]};\n`;
748
+ }
749
+ }
750
+
751
+ if (themeCSS) {
752
+ cssString += `${themeSelectors.join(', ')} {\n${themeCSS}}\n`;
753
+ }
754
+ }
755
+ });
756
+ continue;
757
+ }
758
+
660
759
  if (element.atRules && Array.isArray(element.atRules)) {
661
760
  element.atRules.forEach(rule => {
662
761
  cssString += processAtRule(rule, null);
@@ -676,17 +775,9 @@ const compile = (obj) => {
676
775
  element[prop].forEach(rule => {
677
776
  atRulesCSS += processAtRule(rule, element.selectors);
678
777
  });
679
- } else if (prop === 'nestedRules' && Array.isArray(element[prop])) {
680
- element[prop].forEach(rule => {
681
- let nestedBody = '';
682
- for (let nestedProp in rule.styles) {
683
- const kebabKey = nestedProp.replace(/([A-Z])/g, '-$1').toLowerCase();
684
- nestedBody += ` ${kebabKey}: ${rule.styles[nestedProp]};\n`;
685
- }
686
- if (nestedBody) {
687
- atRulesCSS += `${element.selectors.join(', ')} ${rule.selector} {\n${nestedBody} }\n`;
688
- }
689
- });
778
+ } else if (prop === 'themes' && Array.isArray(element[prop])) {
779
+ // Process themes (already handled above)
780
+ continue;
690
781
  } else if (prop === 'hover' && typeof element[prop] === 'object') {
691
782
  let hoverBody = '';
692
783
  for (let hoverKey in element[prop]) {
package/node/chaincss.js CHANGED
@@ -124,6 +124,8 @@ function parseArgs(args) {
124
124
  result.inputFile = arg;
125
125
  } else if (!result.outputFile) {
126
126
  result.outputFile = arg;
127
+ }else if (arg === '--validate-themes') {
128
+ result.validateThemes = true;
127
129
  }
128
130
  }
129
131
  return result;
@@ -148,6 +150,14 @@ const applyCliOptions = (cliOptions) => {
148
150
  if (cliOptions.atomic) {
149
151
  config.atomic.enabled = true;
150
152
  }
153
+ /*if (cliOptions.validateThemes) {
154
+ const configPath = path.join(process.cwd(), 'chaincss.config.cjs');
155
+ if (fs.existsSync(configPath)) {
156
+ await import('./theme-validator.js').then(module => {
157
+ module.validateThemeFiles(configPath);
158
+ });
159
+ }
160
+ }*/
151
161
  };
152
162
 
153
163
  function watch(inputFile, outputFile) {
@@ -1,10 +1,10 @@
1
- // node/plugins/next-plugin.js
2
1
  const path = require('path');
3
2
 
4
3
  module.exports = function withChainCSS(nextConfig = {}) {
5
4
  return {
6
5
  ...nextConfig,
7
6
  webpack(config, options) {
7
+ // Add loader for .jcss files (works in RSC)
8
8
  config.module.rules.push({
9
9
  test: /\.jcss$/,
10
10
  use: [
@@ -12,18 +12,109 @@ module.exports = function withChainCSS(nextConfig = {}) {
12
12
  {
13
13
  loader: path.resolve(__dirname, '../loaders/chaincss-loader.js'),
14
14
  options: {
15
- mode: process.env.NODE_ENV === 'production' ? 'build' : 'runtime',
16
- atomic: process.env.NODE_ENV === 'production'
15
+ mode: 'build', // Build mode for RSC - extracts CSS
16
+ atomic: true
17
17
  }
18
18
  }
19
19
  ]
20
20
  });
21
21
 
22
+ // Enable CSS extraction for RSC
23
+ if (options.isServer) {
24
+ config.optimization = {
25
+ ...config.optimization,
26
+ splitChunks: {
27
+ cacheGroups: {
28
+ styles: {
29
+ name: 'styles',
30
+ type: 'css/mini-extract',
31
+ chunks: 'all',
32
+ enforce: true
33
+ }
34
+ }
35
+ }
36
+ };
37
+ }
38
+
39
+ if (typeof nextConfig.webpack === 'function') {
40
+ return nextConfig.webpack(config, options);
41
+ }
42
+ return config;
43
+ },
44
+
45
+ // Enable CSS support in App Router
46
+ experimental: {
47
+ ...nextConfig.experimental,
48
+ turbo: {
49
+ ...nextConfig.experimental?.turbo,
50
+ rules: {
51
+ ...nextConfig.experimental?.turbo?.rules,
52
+ '*.jcss': {
53
+ loaders: ['chaincss/loader'],
54
+ as: '*.css'
55
+ }
56
+ }
57
+ }
58
+ }
59
+ };
60
+ };
61
+ const path = require('path');
62
+
63
+ module.exports = function withChainCSS(nextConfig = {}) {
64
+ return {
65
+ ...nextConfig,
66
+ webpack(config, options) {
67
+ // Add loader for .jcss files (works in RSC)
68
+ config.module.rules.push({
69
+ test: /\.jcss$/,
70
+ use: [
71
+ options.defaultLoaders.babel,
72
+ {
73
+ loader: path.resolve(__dirname, '../loaders/chaincss-loader.js'),
74
+ options: {
75
+ mode: 'build', // Build mode for RSC - extracts CSS
76
+ atomic: true
77
+ }
78
+ }
79
+ ]
80
+ });
81
+
82
+ // Enable CSS extraction for RSC
83
+ if (options.isServer) {
84
+ config.optimization = {
85
+ ...config.optimization,
86
+ splitChunks: {
87
+ cacheGroups: {
88
+ styles: {
89
+ name: 'styles',
90
+ type: 'css/mini-extract',
91
+ chunks: 'all',
92
+ enforce: true
93
+ }
94
+ }
95
+ }
96
+ };
97
+ }
98
+
22
99
  if (typeof nextConfig.webpack === 'function') {
23
100
  return nextConfig.webpack(config, options);
24
101
  }
25
102
  return config;
26
103
  },
27
- pageExtensions: [...(nextConfig.pageExtensions || []), 'jcss']
104
+
105
+ // Enable CSS support in App Router
106
+ experimental: {
107
+ ...nextConfig.experimental,
108
+ turbo: {
109
+ ...nextConfig.experimental?.turbo,
110
+ rules: {
111
+ ...nextConfig.experimental?.turbo?.rules,
112
+ '*.jcss': {
113
+ loaders: ['chaincss/loader'],
114
+ as: '*.css'
115
+ }
116
+ }
117
+ }
118
+ }
28
119
  };
29
120
  };
@@ -1,4 +1,3 @@
1
- // node/plugins/vite-plugin.js
2
1
  import path from 'node:path';
3
2
  import fs from 'node:fs';
4
3
  import { createRequire } from 'node:module';
@@ -25,10 +24,8 @@ const compiledCache = new Map();
25
24
  const compileScript = (scriptBlock, filename, get) => {
26
25
  const dirname = path.dirname(filename);
27
26
 
28
- // Reset CSS output
29
27
  chain.cssOutput = '';
30
28
 
31
- // Create a function from the script - no temp files!
32
29
  const fn = new Function(
33
30
  '$',
34
31
  'run',
@@ -40,7 +37,6 @@ const compileScript = (scriptBlock, filename, get) => {
40
37
  scriptBlock
41
38
  );
42
39
 
43
- // Execute with helpers
44
40
  fn($, run, originalCompile, chain, get, filename, dirname);
45
41
 
46
42
  return chain.cssOutput || '';
@@ -52,10 +48,8 @@ const processJavascriptBlocks = (content, filename, get) => {
52
48
 
53
49
  for (let i = 0; i < blocks.length; i++) {
54
50
  if (i % 2 === 0) {
55
- // Static content
56
51
  output += blocks[i];
57
52
  } else {
58
- // JavaScript block
59
53
  const css = compileScript(blocks[i], filename, get);
60
54
  if (css && typeof css === 'string') {
61
55
  output += css;
@@ -69,10 +63,8 @@ const processJavascriptBlocks = (content, filename, get) => {
69
63
  const processJCSSFile = (filePath) => {
70
64
  const abs = path.resolve(filePath);
71
65
 
72
- // Return cached result if available
73
66
  if (fileCache.has(abs)) return fileCache.get(abs);
74
67
 
75
- // Check if file exists
76
68
  if (!fs.existsSync(abs)) {
77
69
  throw new Error(`ChainCSS: File not found: ${abs}`);
78
70
  }
@@ -80,26 +72,21 @@ const processJCSSFile = (filePath) => {
80
72
  const content = fs.readFileSync(abs, 'utf8');
81
73
  const dirname = path.dirname(abs);
82
74
 
83
- // Create get function for this file
84
75
  const get = (relativePath) => {
85
76
  const targetPath = path.resolve(dirname, relativePath);
86
77
  return processJCSSFile(targetPath);
87
78
  };
88
79
 
89
- // Process the file
90
80
  const result = processJavascriptBlocks(content, abs, get);
91
81
 
92
- // Cache the result
93
82
  fileCache.set(abs, result);
94
83
  return result;
95
84
  };
96
85
 
97
- // Minify and prefix CSS
98
86
  const processCSS = async (css, filepath, options = {}) => {
99
87
  const { minify = true, prefix = true } = options;
100
88
  let processed = css;
101
89
 
102
- // Add prefixing
103
90
  if (prefix && prefixer) {
104
91
  try {
105
92
  const result = await prefixer.process(css, { from: filepath });
@@ -109,7 +96,6 @@ const processCSS = async (css, filepath, options = {}) => {
109
96
  }
110
97
  }
111
98
 
112
- // Minify
113
99
  if (minify) {
114
100
  const minified = new CleanCSS({ level: 2 }).minify(processed);
115
101
  if (minified.errors.length) {
@@ -121,45 +107,152 @@ const processCSS = async (css, filepath, options = {}) => {
121
107
  return processed;
122
108
  };
123
109
 
110
+ // Helper to track used selectors for tree shaking
111
+ const trackUsedSelectors = (bundle) => {
112
+ const usedSelectors = new Set();
113
+ if (!bundle) return usedSelectors;
114
+
115
+ const classRegex = /class(?:Name)?=["']([^"']+)["']/g;
116
+ let match;
117
+ while ((match = classRegex.exec(bundle)) !== null) {
118
+ match[1].split(' ').forEach(cls => {
119
+ if (cls && cls !== '') {
120
+ usedSelectors.add(`.${cls}`);
121
+ }
122
+ });
123
+ }
124
+ return usedSelectors;
125
+ };
126
+
127
+ // Helper to filter unused CSS
128
+ function filterUsedCSS(css, usedSelectors) {
129
+ const lines = css.split('\n');
130
+ const filteredLines = [];
131
+ let inRule = false;
132
+
133
+ for (const line of lines) {
134
+ const selectorMatch = line.match(/^([^{]+){/);
135
+ if (selectorMatch) {
136
+ const selectors = selectorMatch[1].split(',').map(s => s.trim());
137
+ const isUsed = selectors.some(selector => {
138
+ const baseSelector = selector.split(':')[0];
139
+ return usedSelectors.has(baseSelector);
140
+ });
141
+
142
+ if (isUsed) {
143
+ filteredLines.push(line);
144
+ inRule = true;
145
+ } else {
146
+ inRule = false;
147
+ }
148
+ } else if (inRule) {
149
+ filteredLines.push(line);
150
+ if (line.includes('}')) {
151
+ inRule = false;
152
+ }
153
+ } else if (!line.includes('}')) {
154
+ filteredLines.push(line);
155
+ }
156
+ }
157
+
158
+ return filteredLines.join('\n');
159
+ }
160
+
124
161
  export default function chaincssVite(opts = {}) {
125
162
  const {
126
163
  extension = '.jcss',
127
164
  minify = process.env.NODE_ENV === 'production',
128
165
  prefix = true,
129
- hmr = true
166
+ hmr = true,
167
+ debug = process.env.NODE_ENV === 'development',
168
+ treeShake = process.env.NODE_ENV === 'production'
130
169
  } = opts;
131
170
 
171
+ let generatedCSS = '';
172
+ let generatedClassMap = {};
173
+
132
174
  return {
133
175
  name: 'vite-plugin-chaincss',
134
176
  enforce: 'pre',
135
177
 
136
- // Transform .jcss files
137
178
  async transform(code, id) {
138
179
  if (!id.endsWith(extension)) return null;
139
180
 
140
181
  try {
141
- // Create get function for root file
142
182
  const dirname = path.dirname(id);
143
183
  const get = (relativePath) => {
144
184
  const targetPath = path.resolve(dirname, relativePath);
145
185
  return processJCSSFile(targetPath);
146
186
  };
147
187
 
148
- // Process the file
149
188
  let css = processJavascriptBlocks(code, id, get);
150
189
 
151
- // Process CSS (prefix + minify)
190
+ generatedCSS = css;
191
+ if (chain.classMap) {
192
+ generatedClassMap = chain.classMap;
193
+ }
194
+
152
195
  css = await processCSS(css, id, { minify, prefix });
153
196
 
154
- // In development, inject CSS for HMR
197
+ // Development with Debug Mode
198
+ if (process.env.NODE_ENV !== 'production' && debug) {
199
+ const classMapStr = JSON.stringify(generatedClassMap);
200
+ return {
201
+ code: `
202
+ // ChainCSS with Debug Mode
203
+ const id = ${JSON.stringify(id)};
204
+ const css = ${JSON.stringify(css)};
205
+ const classMap = ${classMapStr};
206
+
207
+ let style = document.querySelector(\`style[data-chaincss="\${id}"]\`);
208
+ if (!style) {
209
+ style = document.createElement('style');
210
+ style.setAttribute('data-chaincss', id);
211
+ document.head.appendChild(style);
212
+ }
213
+ style.textContent = css;
214
+
215
+ // Debug Mode: Add inspector attributes
216
+ if (typeof window !== 'undefined' && window.__CHAINCSS_DEBUG__ !== false) {
217
+ // Mark elements with their chaincss classes
218
+ const observer = new MutationObserver(() => {
219
+ document.querySelectorAll('[class*="chain-"]').forEach(el => {
220
+ const classes = Array.from(el.classList).filter(c => c.includes('chain-')).join(' ');
221
+ if (classes && !el.hasAttribute('data-chaincss-class')) {
222
+ el.setAttribute('data-chaincss-class', classes);
223
+ }
224
+ });
225
+ });
226
+ observer.observe(document.body, { childList: true, subtree: true });
227
+
228
+ console.log('🔍 ChainCSS Debug Mode Active');
229
+ console.log('📊 Class Map:', classMap);
230
+ }
231
+
232
+ if (import.meta.hot) {
233
+ import.meta.hot.accept((newModule) => {
234
+ if (newModule?.default) {
235
+ style.textContent = newModule.default;
236
+ }
237
+ });
238
+ import.meta.hot.dispose(() => {
239
+ style.remove();
240
+ });
241
+ }
242
+
243
+ export default css;
244
+ `,
245
+ map: null
246
+ };
247
+ }
248
+
249
+ // Development without Debug
155
250
  if (process.env.NODE_ENV !== 'production') {
156
251
  return {
157
252
  code: `
158
- // ChainCSS HMR
159
253
  const id = ${JSON.stringify(id)};
160
254
  const css = ${JSON.stringify(css)};
161
255
 
162
- // Add style to head
163
256
  let style = document.querySelector(\`style[data-chaincss="\${id}"]\`);
164
257
  if (!style) {
165
258
  style = document.createElement('style');
@@ -168,14 +261,12 @@ export default function chaincssVite(opts = {}) {
168
261
  }
169
262
  style.textContent = css;
170
263
 
171
- // HMR handling
172
264
  if (import.meta.hot) {
173
265
  import.meta.hot.accept((newModule) => {
174
266
  if (newModule?.default) {
175
267
  style.textContent = newModule.default;
176
268
  }
177
269
  });
178
-
179
270
  import.meta.hot.dispose(() => {
180
271
  style.remove();
181
272
  });
@@ -187,7 +278,7 @@ export default function chaincssVite(opts = {}) {
187
278
  };
188
279
  }
189
280
 
190
- // Production: just export CSS
281
+ // Production with Tree Shaking tracking
191
282
  return {
192
283
  code: `export default ${JSON.stringify(css)};`,
193
284
  map: null
@@ -199,12 +290,89 @@ export default function chaincssVite(opts = {}) {
199
290
  }
200
291
  },
201
292
 
202
- // Handle HMR updates
293
+ // Add debug styles to HTML
294
+ transformIndexHtml(html) {
295
+ if (debug && process.env.NODE_ENV !== 'production') {
296
+ return {
297
+ html,
298
+ tags: [
299
+ {
300
+ tag: 'script',
301
+ injectTo: 'head',
302
+ children: `
303
+ window.__CHAINCSS_DEBUG__ = true;
304
+ console.log('🔍 ChainCSS Debug Mode: Hover over elements to see their atomic classes');
305
+ `
306
+ },
307
+ {
308
+ tag: 'style',
309
+ injectTo: 'head',
310
+ children: `
311
+ [data-chaincss-class]:hover::after {
312
+ content: attr(data-chaincss-class);
313
+ position: absolute;
314
+ background: #667eea;
315
+ color: white;
316
+ padding: 2px 8px;
317
+ font-size: 11px;
318
+ border-radius: 4px;
319
+ font-family: monospace;
320
+ z-index: 9999;
321
+ pointer-events: none;
322
+ white-space: nowrap;
323
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
324
+ }
325
+ `
326
+ }
327
+ ]
328
+ };
329
+ }
330
+ return html;
331
+ },
332
+
333
+ // Tree Shaking: Remove unused CSS
334
+ generateBundle(options, bundle) {
335
+ if (!treeShake) return;
336
+
337
+ const jsBundle = Object.values(bundle).find(
338
+ file => file.type === 'chunk' && file.isEntry
339
+ );
340
+
341
+ if (!jsBundle) return;
342
+
343
+ const usedSelectors = trackUsedSelectors(jsBundle.code);
344
+
345
+ const totalSelectors = Object.keys(generatedClassMap).length;
346
+ const usedCount = usedSelectors.size;
347
+ const deadCount = totalSelectors - usedCount;
348
+ const savings = totalSelectors > 0 ? (deadCount / totalSelectors * 100).toFixed(1) : 0;
349
+
350
+ if (deadCount > 0) {
351
+ console.log(`\n ChainCSS Tree Shaking Results:`);
352
+ console.log(`Total styles: ${totalSelectors}`);
353
+ console.log(`Used styles: ${usedCount}`);
354
+ console.log(`Dead code eliminated: ${deadCount} (${savings}% savings)`);
355
+ }
356
+
357
+ const cssFile = Object.values(bundle).find(
358
+ file => file.type === 'asset' && file.fileName.endsWith('.css')
359
+ );
360
+
361
+ if (cssFile && typeof cssFile.source === 'string') {
362
+ const originalCSS = cssFile.source;
363
+ const filteredCSS = filterUsedCSS(originalCSS, usedSelectors);
364
+
365
+ if (filteredCSS.length < originalCSS.length) {
366
+ cssFile.source = filteredCSS;
367
+ const cssSavings = ((originalCSS.length - filteredCSS.length) / originalCSS.length * 100).toFixed(1);
368
+ console.log(` 🎨 CSS size reduced by ${cssSavings}%`);
369
+ }
370
+ }
371
+ },
372
+
203
373
  handleHotUpdate({ file, server }) {
204
374
  if (file.endsWith(extension)) {
205
- // Invalidate cache for changed file
206
375
  fileCache.delete(file);
207
- // Trigger reload
208
376
  server.ws.send({
209
377
  type: 'full-reload',
210
378
  path: '*'
@@ -0,0 +1,32 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ export function validateThemeFiles(configPath) {
5
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
6
+
7
+ if (!config.themes) return;
8
+
9
+ const { contract, themes } = config;
10
+
11
+ console.log('\n🎨 Validating Theme Contract...\n');
12
+
13
+ const errors = [];
14
+
15
+ themes.forEach((theme, index) => {
16
+ const themeName = theme.name || `theme-${index}`;
17
+ try {
18
+ validateTheme(contract, theme.values);
19
+ console.log(`✅ ${themeName}: Valid`);
20
+ } catch (err) {
21
+ errors.push(`❌ ${themeName}: ${err.message}`);
22
+ }
23
+ });
24
+
25
+ if (errors.length > 0) {
26
+ console.error('\nTheme Contract Validation Failed:\n');
27
+ errors.forEach(err => console.error(err));
28
+ process.exit(1);
29
+ }
30
+
31
+ console.log('\n✅ All themes valid!\n');
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chaincss",
3
- "version": "1.12.14",
3
+ "version": "1.13.0",
4
4
  "description": "Chainable CSS-in-JS with build-time compilation, atomic CSS, and zero-runtime options",
5
5
  "keywords": [
6
6
  "css",
@@ -0,0 +1,98 @@
1
+ export function createThemeContract(contractShape) {
2
+ // Store the contract for validation
3
+ const contract = contractShape;
4
+
5
+ // Create a proxy that validates token access
6
+ const contractProxy = new Proxy(contract, {
7
+ get(target, prop) {
8
+ if (prop === '__isContract') return true;
9
+ if (prop === '__validate') return (theme) => validateTheme(contract, theme);
10
+ return target[prop];
11
+ }
12
+ });
13
+
14
+ return contractProxy;
15
+ }
16
+
17
+ export function validateTheme(contract, theme, path = '') {
18
+ const errors = [];
19
+
20
+ function validate(contractPart, themePart, currentPath) {
21
+ if (typeof contractPart === 'object' && contractPart !== null) {
22
+ // Check if theme has all required keys
23
+ const requiredKeys = Object.keys(contractPart);
24
+ const themeKeys = Object.keys(themePart || {});
25
+
26
+ requiredKeys.forEach(key => {
27
+ const newPath = currentPath ? `${currentPath}.${key}` : key;
28
+
29
+ if (!themePart || !themePart.hasOwnProperty(key)) {
30
+ errors.push(` Missing required token: "${newPath}"`);
31
+ } else {
32
+ validate(contractPart[key], themePart[key], newPath);
33
+ }
34
+ });
35
+
36
+ // Warn about extra keys (optional, could be allowed)
37
+ themeKeys.forEach(key => {
38
+ if (!contractPart.hasOwnProperty(key)) {
39
+ const newPath = currentPath ? `${currentPath}.${key}` : key;
40
+ console.warn(` Extra token not in contract: "${newPath}"`);
41
+ }
42
+ });
43
+ } else {
44
+ // Leaf node - just check type (optional)
45
+ if (typeof themePart !== 'string') {
46
+ errors.push(` Token "${currentPath}" must be a string, got ${typeof themePart}`);
47
+ }
48
+ }
49
+ }
50
+
51
+ validate(contract, theme, path);
52
+
53
+ if (errors.length > 0) {
54
+ throw new Error(`Theme Contract Validation Failed:\n${errors.join('\n')}`);
55
+ }
56
+
57
+ return true;
58
+ }
59
+
60
+ export function createTheme(contract, themeValues) {
61
+ // Validate at creation time
62
+ if (contract.__isContract) {
63
+ contract.__validate(themeValues);
64
+ } else {
65
+ validateTheme(contract, themeValues);
66
+ }
67
+
68
+ // Create the actual theme tokens
69
+ const tokens = {};
70
+
71
+ function buildTokens(contractPart, themePart, target, path = '') {
72
+ Object.keys(contractPart).forEach(key => {
73
+ const newPath = path ? `${path}.${key}` : key;
74
+
75
+ if (typeof contractPart[key] === 'object' && contractPart[key] !== null) {
76
+ target[key] = {};
77
+ buildTokens(contractPart[key], themePart[key] || {}, target[key], newPath);
78
+ } else {
79
+ target[key] = themePart[key];
80
+ }
81
+ });
82
+ }
83
+
84
+ buildTokens(contract, themeValues, tokens);
85
+
86
+ // Add getter method
87
+ tokens.get = (path) => {
88
+ const parts = path.split('.');
89
+ let current = tokens;
90
+ for (const part of parts) {
91
+ if (current === undefined) return undefined;
92
+ current = current[part];
93
+ }
94
+ return current;
95
+ };
96
+
97
+ return tokens;
98
+ }
package/shared/tokens.mjs CHANGED
@@ -1,5 +1,15 @@
1
+ // shared/tokens.mjs
2
+
1
3
  class DesignTokens {
2
- constructor(tokens = {}) {
4
+ constructor(tokens = {}, contract = null) {
5
+ // Store contract for validation
6
+ this.contract = contract;
7
+
8
+ // Validate against contract if provided
9
+ if (contract) {
10
+ this.validateContract(tokens, contract);
11
+ }
12
+
3
13
  this.tokens = this.deepFreeze({
4
14
  colors: {},
5
15
  spacing: {},
@@ -14,6 +24,48 @@ class DesignTokens {
14
24
  this.flattened = this.flattenTokens(this.tokens);
15
25
  }
16
26
 
27
+ // Validate token structure against contract
28
+ validateContract(tokens, contract, path = '') {
29
+ const errors = [];
30
+
31
+ const validate = (contractPart, tokenPart, currentPath) => {
32
+ if (typeof contractPart === 'object' && contractPart !== null) {
33
+ const requiredKeys = Object.keys(contractPart);
34
+ const tokenKeys = Object.keys(tokenPart || {});
35
+
36
+ requiredKeys.forEach(key => {
37
+ const newPath = currentPath ? `${currentPath}.${key}` : key;
38
+
39
+ if (!tokenPart || !tokenPart.hasOwnProperty(key)) {
40
+ errors.push(`❌ Missing required token: "${newPath}"`);
41
+ } else {
42
+ validate(contractPart[key], tokenPart[key], newPath);
43
+ }
44
+ });
45
+
46
+ // Warn about extra keys
47
+ tokenKeys.forEach(key => {
48
+ if (!contractPart.hasOwnProperty(key)) {
49
+ const newPath = currentPath ? `${currentPath}.${key}` : key;
50
+ console.warn(`⚠️ Extra token not in contract: "${newPath}"`);
51
+ }
52
+ });
53
+ } else {
54
+ if (typeof tokenPart !== 'string') {
55
+ errors.push(`❌ Token "${currentPath}" must be a string, got ${typeof tokenPart}`);
56
+ }
57
+ }
58
+ };
59
+
60
+ validate(contract, tokens, path);
61
+
62
+ if (errors.length > 0) {
63
+ throw new Error(`Theme Contract Validation Failed:\n${errors.join('\n')}`);
64
+ }
65
+
66
+ return true;
67
+ }
68
+
17
69
  // Deep freeze to prevent accidental modifications
18
70
  deepFreeze(obj) {
19
71
  Object.keys(obj).forEach(key => {
@@ -57,7 +109,7 @@ class DesignTokens {
57
109
  return css;
58
110
  }
59
111
 
60
- // Create a theme variant
112
+ // Create a theme variant with contract validation
61
113
  createTheme(name, overrides) {
62
114
  const themeTokens = { ...this.flattened };
63
115
 
@@ -67,7 +119,13 @@ class DesignTokens {
67
119
  }
68
120
  });
69
121
 
70
- return new DesignTokens(this.expandTokens(themeTokens));
122
+ // Validate theme against original contract if exists
123
+ if (this.contract) {
124
+ const expandedTokens = this.expandTokens(themeTokens);
125
+ this.validateContract(expandedTokens, this.contract);
126
+ }
127
+
128
+ return new DesignTokens(this.expandTokens(themeTokens), this.contract);
71
129
  }
72
130
 
73
131
  // Expand flattened tokens back to nested structure
@@ -234,8 +292,13 @@ const defaultTokens = {
234
292
  const tokens = new DesignTokens(defaultTokens);
235
293
 
236
294
  // Token utility functions
237
- const createTokens = (customTokens) => {
238
- return new DesignTokens(customTokens);
295
+ const createTokens = (customTokens, contract = null) => {
296
+ return new DesignTokens(customTokens, contract);
297
+ };
298
+
299
+ // Define a theme contract
300
+ const defineThemeContract = (contract) => {
301
+ return contract;
239
302
  };
240
303
 
241
304
  // Generate responsive values
@@ -251,6 +314,7 @@ const responsive = (values) => {
251
314
  export {
252
315
  tokens,
253
316
  createTokens,
317
+ defineThemeContract,
254
318
  responsive,
255
319
  DesignTokens
256
320
  };
package/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /// <reference types="react" />
2
2
 
3
- declare module '@melcanz85/chaincss' {
3
+ declare module 'chaincss' {
4
4
  // ============================================================================
5
5
  // Core Types
6
6
  // ============================================================================
@@ -33,6 +33,12 @@ declare module '@melcanz85/chaincss' {
33
33
 
34
34
  // Selector shortcut
35
35
  $(selector: string): ChainBuilder;
36
+
37
+ // Theme method
38
+ theme<T extends Record<string, any>>(
39
+ tokens: T,
40
+ callback: (chain: ChainBuilder) => void
41
+ ): ChainBuilder;
36
42
  }
37
43
 
38
44
  export interface KeyframeBuilder {
@@ -123,6 +129,10 @@ declare module '@melcanz85/chaincss' {
123
129
  ): (Component: React.ComponentType<P>) => React.FC<P & { chainStyles?: Record<string, string> }>;
124
130
 
125
131
  export function cx(...classes: (string | undefined | null | false)[]): string;
132
+
133
+ export function enableChainCSSDebug(): void;
134
+ export function disableChainCSSDebug(): void;
135
+ export function isDebugEnabled(): boolean;
126
136
 
127
137
  // ============================================================================
128
138
  // Configuration
@@ -179,6 +189,8 @@ declare module '@melcanz85/chaincss' {
179
189
  minify?: boolean;
180
190
  prefix?: boolean;
181
191
  hmr?: boolean;
192
+ debug?: boolean;
193
+ treeShake?: boolean;
182
194
  }
183
195
 
184
196
  export function vitePlugin(options?: VitePluginOptions): any;
@@ -188,9 +200,9 @@ declare module '@melcanz85/chaincss' {
188
200
  // Vite Plugin Subpath Export
189
201
  // ============================================================================
190
202
 
191
- declare module '@melcanz85/chaincss/vite-plugin' {
203
+ declare module 'chaincss/vite-plugin' {
192
204
  import { Plugin } from 'vite';
193
- import { VitePluginOptions } from '@melcanz85/chaincss';
205
+ import { VitePluginOptions } from 'chaincss';
194
206
 
195
207
  export default function chaincssVite(options?: VitePluginOptions): Plugin;
196
208
  }
@@ -199,16 +211,16 @@ declare module '@melcanz85/chaincss/vite-plugin' {
199
211
  // React Subpath Export
200
212
  // ============================================================================
201
213
 
202
- declare module '@melcanz85/chaincss/react' {
203
- export * from '@melcanz85/chaincss';
214
+ declare module 'chaincss/react' {
215
+ export * from 'chaincss';
204
216
 
205
217
  // Re-export React-specific hooks
206
- export const useChainStyles: typeof import('@melcanz85/chaincss').useChainStyles;
207
- export const useDynamicChainStyles: typeof import('@melcanz85/chaincss').useDynamicChainStyles;
208
- export const useThemeChainStyles: typeof import('@melcanz85/chaincss').useThemeChainStyles;
209
- export const ChainCSSGlobal: typeof import('@melcanz85/chaincss').ChainCSSGlobal;
210
- export const withChainStyles: typeof import('@melcanz85/chaincss').withChainStyles;
211
- export const cx: typeof import('@melcanz85/chaincss').cx;
218
+ export const useChainStyles: typeof import('chaincss').useChainStyles;
219
+ export const useDynamicChainStyles: typeof import('chaincss').useDynamicChainStyles;
220
+ export const useThemeChainStyles: typeof import('chaincss').useThemeChainStyles;
221
+ export const ChainCSSGlobal: typeof import('chaincss').ChainCSSGlobal;
222
+ export const withChainStyles: typeof import('chaincss').withChainStyles;
223
+ export const cx: typeof import('chaincss').cx;
212
224
  }
213
225
 
214
226
  // ============================================================================
@@ -236,4 +248,30 @@ export type Recipe<TVariants extends Record<string, Record<string, any>>> = {
236
248
 
237
249
  export function recipe<TVariants extends Record<string, Record<string, any>>>(
238
250
  options: RecipeOptions<TVariants>
239
- ): Recipe<TVariants>;
251
+ ): Recipe<TVariants>;
252
+
253
+ //================
254
+ // THEME CONTRACT
255
+ //================
256
+ export interface ThemeContract {
257
+ [key: string]: string | Record<string, any>;
258
+ }
259
+
260
+ export function defineThemeContract<T extends ThemeContract>(
261
+ contract: T
262
+ ): T & { __isContract: true; __validate: (theme: any) => void };
263
+
264
+ export function createTheme<T extends Record<string, any>>(
265
+ contract: T,
266
+ values: T
267
+ ): DesignTokens;
268
+
269
+ export function createTokens(
270
+ customTokens: Partial<Tokens>,
271
+ contract?: ThemeContract
272
+ ): DesignTokens;
273
+
274
+ export function validateTheme(
275
+ contract: ThemeContract,
276
+ theme: any
277
+ ): boolean;