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 +1 -1
- package/browser/react-hooks.js +25 -1
- package/node/btt.js +102 -11
- package/node/chaincss.js +10 -0
- package/node/plugins/next-plugin.js +95 -4
- package/node/plugins/vite-plugin.js +196 -28
- package/node/theme-validator.js +32 -0
- package/package.json +1 -1
- package/shared/theme-contract.js +98 -0
- package/shared/tokens.mjs +69 -5
- package/types.d.ts +50 -12
package/README.md
CHANGED
package/browser/react-hooks.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import 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 === '
|
|
680
|
-
|
|
681
|
-
|
|
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:
|
|
16
|
-
atomic:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
@@ -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
|
-
|
|
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 '
|
|
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 '
|
|
203
|
+
declare module 'chaincss/vite-plugin' {
|
|
192
204
|
import { Plugin } from 'vite';
|
|
193
|
-
import { VitePluginOptions } from '
|
|
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 '
|
|
203
|
-
export * from '
|
|
214
|
+
declare module 'chaincss/react' {
|
|
215
|
+
export * from 'chaincss';
|
|
204
216
|
|
|
205
217
|
// Re-export React-specific hooks
|
|
206
|
-
export const useChainStyles: typeof import('
|
|
207
|
-
export const useDynamicChainStyles: typeof import('
|
|
208
|
-
export const useThemeChainStyles: typeof import('
|
|
209
|
-
export const ChainCSSGlobal: typeof import('
|
|
210
|
-
export const withChainStyles: typeof import('
|
|
211
|
-
export const cx: typeof import('
|
|
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;
|