@melcanz85/chaincss 1.8.0 → 1.9.2

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/react-hooks.js ADDED
@@ -0,0 +1,175 @@
1
+ import { useMemo, useEffect, useRef, useState } from 'react';
2
+ import { $, compile } from './transpiler';
3
+
4
+ // Cache for generated styles to avoid duplication
5
+ const styleCache = new Map();
6
+ let styleSheet = null;
7
+
8
+ // Initialize style sheet (add to document head)
9
+ const initStyleSheet = () => {
10
+ if (typeof document === 'undefined') return null; // SSR safety
11
+
12
+ if (!styleSheet) {
13
+ // Check if already exists
14
+ const existing = document.getElementById('chaincss-styles');
15
+ if (existing) {
16
+ styleSheet = existing;
17
+ return styleSheet;
18
+ }
19
+
20
+ // Create new style element
21
+ const style = document.createElement('style');
22
+ style.id = 'chaincss-styles';
23
+ style.setAttribute('data-chaincss', 'true');
24
+ document.head.appendChild(style);
25
+ styleSheet = style;
26
+ }
27
+
28
+ return styleSheet;
29
+ };
30
+
31
+ // Update styles in the style sheet
32
+ const updateStyles = (css) => {
33
+ const sheet = initStyleSheet();
34
+ if (sheet) {
35
+ sheet.textContent = css;
36
+ }
37
+ };
38
+
39
+ // Main hook for using ChainCSS styles in React
40
+ export function useChainStyles(styles, options = {}) {
41
+ const {
42
+ cache = true,
43
+ namespace = 'chain',
44
+ watch = false
45
+ } = options;
46
+
47
+ // Generate a unique ID for this component instance
48
+ const id = useRef(`chain-${Math.random().toString(36).substr(2, 9)}`);
49
+
50
+ // Store the generated class names
51
+ const [classNames, setClassNames] = useState({});
52
+
53
+ // Process styles and generate CSS
54
+ const processed = useMemo(() => {
55
+ if (!styles || Object.keys(styles).length === 0) return { classNames: {}, css: '' };
56
+
57
+ // Check cache first
58
+ const cacheKey = JSON.stringify(styles);
59
+ if (cache && styleCache.has(cacheKey)) {
60
+ return styleCache.get(cacheKey);
61
+ }
62
+
63
+ // Generate unique class names for each style
64
+ const newClassNames = {};
65
+ const compiledStyles = {};
66
+
67
+ Object.entries(styles).forEach(([key, styleDef]) => {
68
+ // Generate a unique class name
69
+ const className = `${namespace}-${key}-${id.current}`;
70
+
71
+ // Create a style definition with the unique class
72
+ const styleObj = typeof styleDef === 'function'
73
+ ? styleDef()
74
+ : styleDef;
75
+
76
+ // Store the class name mapping
77
+ newClassNames[key] = className;
78
+
79
+ // Create the style rule
80
+ compiledStyles[`${key}_${id.current}`] = {
81
+ selectors: [`.${className}`],
82
+ ...styleObj
83
+ };
84
+ });
85
+
86
+ // Compile to CSS
87
+ compile(compiledStyles);
88
+ const css = chain.cssOutput;
89
+
90
+ const result = { classNames: newClassNames, css };
91
+
92
+ if (cache) {
93
+ styleCache.set(cacheKey, result);
94
+ }
95
+
96
+ return result;
97
+ }, [styles, namespace]);
98
+
99
+ // Update the style sheet when styles change
100
+ useEffect(() => {
101
+ if (processed.css) {
102
+ // For simple apps, just append
103
+ if (!watch) {
104
+ const sheet = initStyleSheet();
105
+ if (sheet) {
106
+ // Remove old styles for this component
107
+ const existingStyles = sheet.textContent || '';
108
+ const styleRegex = new RegExp(`\\.[\\w-]*${id.current}[\\s\\S]*?}`, 'g');
109
+ const cleanedStyles = existingStyles.replace(styleRegex, '');
110
+
111
+ // Add new styles
112
+ sheet.textContent = cleanedStyles + processed.css;
113
+ }
114
+ } else {
115
+ // For watch mode, update everything
116
+ updateStyles(processed.css);
117
+ }
118
+ }
119
+
120
+ // Cleanup on unmount
121
+ return () => {
122
+ if (!watch && styleSheet) {
123
+ const existingStyles = styleSheet.textContent || '';
124
+ const styleRegex = new RegExp(`\\.[\\w-]*${id.current}[\\s\\S]*?}`, 'g');
125
+ styleSheet.textContent = existingStyles.replace(styleRegex, '');
126
+ }
127
+ };
128
+ }, [processed.css, watch]);
129
+
130
+ return processed.classNames;
131
+ }
132
+
133
+ // Hook for dynamic styles that depend on props/state
134
+ export function useDynamicChainStyles(styleFactory, deps = [], options = {}) {
135
+ const styles = useMemo(() => {
136
+ return styleFactory();
137
+ }, deps);
138
+
139
+ return useChainStyles(styles, options);
140
+ }
141
+
142
+ // Hook for theme-aware styles
143
+ export function useThemeChainStyles(styles, theme, options = {}) {
144
+ const themedStyles = useMemo(() => {
145
+ if (typeof styles === 'function') {
146
+ return styles(theme);
147
+ }
148
+ return styles;
149
+ }, [styles, theme]);
150
+
151
+ return useChainStyles(themedStyles, options);
152
+ }
153
+
154
+ // Component for injecting global ChainCSS styles
155
+ export function ChainCSSGlobal({ styles }) {
156
+ useChainStyles(styles, { watch: true });
157
+ return null;
158
+ }
159
+
160
+ // HOC for adding ChainCSS styles to components
161
+ export function withChainStyles(styles, options = {}) {
162
+ return function WrappedComponent(props) {
163
+ const classNames = useChainStyles(
164
+ typeof styles === 'function' ? styles(props) : styles,
165
+ options
166
+ );
167
+
168
+ return <WrappedComponent {...props} chainStyles={classNames} />;
169
+ };
170
+ }
171
+
172
+ // Utility to combine multiple class names
173
+ export function cx(...classes) {
174
+ return classes.filter(Boolean).join(' ');
175
+ }
package/tokens.js ADDED
@@ -0,0 +1,256 @@
1
+ class DesignTokens {
2
+ constructor(tokens = {}) {
3
+ this.tokens = this.deepFreeze({
4
+ colors: {},
5
+ spacing: {},
6
+ typography: {},
7
+ breakpoints: {},
8
+ zIndex: {},
9
+ shadows: {},
10
+ borderRadius: {},
11
+ ...tokens
12
+ });
13
+
14
+ this.flattened = this.flattenTokens(this.tokens);
15
+ }
16
+
17
+ // Deep freeze to prevent accidental modifications
18
+ deepFreeze(obj) {
19
+ Object.keys(obj).forEach(key => {
20
+ if (typeof obj[key] === 'object' && obj[key] !== null) {
21
+ this.deepFreeze(obj[key]);
22
+ }
23
+ });
24
+ return Object.freeze(obj);
25
+ }
26
+
27
+ // Flatten nested tokens for easy access
28
+ flattenTokens(obj, prefix = '') {
29
+ return Object.keys(obj).reduce((acc, key) => {
30
+ const prefixed = prefix ? `${prefix}.${key}` : key;
31
+
32
+ if (typeof obj[key] === 'object' && obj[key] !== null) {
33
+ Object.assign(acc, this.flattenTokens(obj[key], prefixed));
34
+ } else {
35
+ acc[prefixed] = obj[key];
36
+ }
37
+
38
+ return acc;
39
+ }, {});
40
+ }
41
+
42
+ // Get token value by path (e.g., 'colors.primary')
43
+ get(path, defaultValue = '') {
44
+ return this.flattened[path] || defaultValue;
45
+ }
46
+
47
+ // Generate CSS variables from tokens
48
+ toCSSVariables(prefix = 'chain') {
49
+ let css = ':root {\n';
50
+
51
+ Object.entries(this.flattened).forEach(([key, value]) => {
52
+ const varName = `--${prefix}-${key.replace(/\./g, '-')}`;
53
+ css += ` ${varName}: ${value};\n`;
54
+ });
55
+
56
+ css += '}\n';
57
+ return css;
58
+ }
59
+
60
+ // Create a theme variant
61
+ createTheme(name, overrides) {
62
+ const themeTokens = { ...this.flattened };
63
+
64
+ Object.entries(overrides).forEach(([key, value]) => {
65
+ if (themeTokens[key]) {
66
+ themeTokens[key] = value;
67
+ }
68
+ });
69
+
70
+ return new DesignTokens(this.expandTokens(themeTokens));
71
+ }
72
+
73
+ // Expand flattened tokens back to nested structure
74
+ expandTokens(flattened) {
75
+ const result = {};
76
+
77
+ Object.entries(flattened).forEach(([key, value]) => {
78
+ const parts = key.split('.');
79
+ let current = result;
80
+
81
+ for (let i = 0; i < parts.length - 1; i++) {
82
+ current[parts[i]] = current[parts[i]] || {};
83
+ current = current[parts[i]];
84
+ }
85
+
86
+ current[parts[parts.length - 1]] = value;
87
+ });
88
+
89
+ return result;
90
+ }
91
+ }
92
+
93
+ // Default tokens
94
+ const defaultTokens = {
95
+ colors: {
96
+ primary: '#667eea',
97
+ secondary: '#764ba2',
98
+ success: '#48bb78',
99
+ danger: '#f56565',
100
+ warning: '#ed8936',
101
+ info: '#4299e1',
102
+ light: '#f7fafc',
103
+ dark: '#1a202c',
104
+ white: '#ffffff',
105
+ black: '#000000',
106
+ gray: {
107
+ 100: '#f7fafc',
108
+ 200: '#edf2f7',
109
+ 300: '#e2e8f0',
110
+ 400: '#cbd5e0',
111
+ 500: '#a0aec0',
112
+ 600: '#718096',
113
+ 700: '#4a5568',
114
+ 800: '#2d3748',
115
+ 900: '#1a202c'
116
+ }
117
+ },
118
+
119
+ spacing: {
120
+ 0: '0',
121
+ 1: '0.25rem',
122
+ 2: '0.5rem',
123
+ 3: '0.75rem',
124
+ 4: '1rem',
125
+ 5: '1.25rem',
126
+ 6: '1.5rem',
127
+ 8: '2rem',
128
+ 10: '2.5rem',
129
+ 12: '3rem',
130
+ 16: '4rem',
131
+ 20: '5rem',
132
+ 24: '6rem',
133
+ 32: '8rem',
134
+ 40: '10rem',
135
+ 48: '12rem',
136
+ 56: '14rem',
137
+ 64: '16rem',
138
+ xs: '0.5rem',
139
+ sm: '1rem',
140
+ md: '1.5rem',
141
+ lg: '2rem',
142
+ xl: '3rem',
143
+ '2xl': '4rem',
144
+ '3xl': '6rem'
145
+ },
146
+
147
+ typography: {
148
+ fontFamily: {
149
+ sans: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
150
+ serif: 'Georgia, Cambria, "Times New Roman", Times, serif',
151
+ mono: 'SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
152
+ },
153
+ fontSize: {
154
+ xs: '0.75rem',
155
+ sm: '0.875rem',
156
+ base: '1rem',
157
+ lg: '1.125rem',
158
+ xl: '1.25rem',
159
+ '2xl': '1.5rem',
160
+ '3xl': '1.875rem',
161
+ '4xl': '2.25rem',
162
+ '5xl': '3rem'
163
+ },
164
+ fontWeight: {
165
+ hairline: '100',
166
+ thin: '200',
167
+ light: '300',
168
+ normal: '400',
169
+ medium: '500',
170
+ semibold: '600',
171
+ bold: '700',
172
+ extrabold: '800',
173
+ black: '900'
174
+ },
175
+ lineHeight: {
176
+ none: '1',
177
+ tight: '1.25',
178
+ snug: '1.375',
179
+ normal: '1.5',
180
+ relaxed: '1.625',
181
+ loose: '2'
182
+ }
183
+ },
184
+
185
+ breakpoints: {
186
+ sm: '640px',
187
+ md: '768px',
188
+ lg: '1024px',
189
+ xl: '1280px',
190
+ '2xl': '1536px'
191
+ },
192
+
193
+ zIndex: {
194
+ 0: '0',
195
+ 10: '10',
196
+ 20: '20',
197
+ 30: '30',
198
+ 40: '40',
199
+ 50: '50',
200
+ auto: 'auto',
201
+ dropdown: '1000',
202
+ sticky: '1020',
203
+ fixed: '1030',
204
+ modal: '1040',
205
+ popover: '1050',
206
+ tooltip: '1060'
207
+ },
208
+
209
+ shadows: {
210
+ sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
211
+ base: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
212
+ md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
213
+ lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
214
+ xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
215
+ '2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
216
+ inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)',
217
+ none: 'none'
218
+ },
219
+
220
+ borderRadius: {
221
+ none: '0',
222
+ sm: '0.125rem',
223
+ base: '0.25rem',
224
+ md: '0.375rem',
225
+ lg: '0.5rem',
226
+ xl: '0.75rem',
227
+ '2xl': '1rem',
228
+ '3xl': '1.5rem',
229
+ full: '9999px'
230
+ }
231
+ };
232
+
233
+ // Create and export tokens instance
234
+ const tokens = new DesignTokens(defaultTokens);
235
+
236
+ // Token utility functions
237
+ const createTokens = (customTokens) => {
238
+ return new DesignTokens(customTokens);
239
+ };
240
+
241
+ // Generate responsive values
242
+ const responsive = (values) => {
243
+ if (typeof values === 'string') return values;
244
+
245
+ return Object.entries(values).map(([breakpoint, value]) => {
246
+ if (breakpoint === 'base') return value;
247
+ return `@media (min-width: ${tokens.get(`breakpoints.${breakpoint}`)}) { ${value} }`;
248
+ }).join(' ');
249
+ };
250
+
251
+ module.exports = {
252
+ tokens,
253
+ createTokens,
254
+ responsive,
255
+ DesignTokens
256
+ };
package/transpiler.js CHANGED
@@ -1,5 +1,7 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs');
3
+ const tokenModule = require('./tokens');
4
+ const tokens = tokenModule.tokens;
3
5
 
4
6
  const chain = {
5
7
  cssOutput: undefined,
@@ -72,7 +74,22 @@ const chain = {
72
74
  // Initialize properties synchronously when module loads
73
75
  chain.initializeProperties();
74
76
 
75
- function $(){
77
+ const resolveToken = (value, useTokens) => {
78
+ if (!useTokens || typeof value !== 'string' || !value.startsWith('$')) {
79
+ return value;
80
+ }
81
+
82
+ const tokenPath = value.slice(1);
83
+ const tokenValue = tokens.get(tokenPath);
84
+
85
+ if (!tokenValue) {
86
+ return value;
87
+ }
88
+
89
+ return tokenValue;
90
+ };
91
+
92
+ function $(useTokens = true){
76
93
  const catcher = {};
77
94
 
78
95
  // Use cached properties if available
@@ -102,19 +119,16 @@ function $(){
102
119
  return result;
103
120
  };
104
121
  }
105
-
106
122
  // Convert camelCase to kebab-case for CSS property
107
- const cssProperty = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
108
-
123
+ const cssProperty = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
109
124
  // Validate property exists (optional) - use cached properties
110
125
  if (validProperties && validProperties.length > 0 && !validProperties.includes(cssProperty)) {
111
- console.warn(`⚠️ Warning: '${cssProperty}' may not be a valid CSS property`);
126
+ console.warn(`Warning: '${cssProperty}' may not be a valid CSS property`);
112
127
  }
113
-
114
128
  // Return a function that sets the value
115
129
  return function(value) {
116
- catcher[prop] = value;
117
- return proxy; // Return proxy for chaining
130
+ catcher[prop] = resolveToken(value, useTokens); // ← USE IT HERE
131
+ return proxy;
118
132
  };
119
133
  }
120
134
  };
@@ -178,38 +192,13 @@ const compile = (obj) => {
178
192
  }
179
193
 
180
194
  chain.cssOutput = cssString.trim();
181
- return cssString.trim();
182
195
  };
183
196
 
184
- const get = (filename) => {
185
- const fileExt = path.extname(filename).toLowerCase();
186
- if (fileExt !== '.jcss') {
187
- throw new Error(`Import error: ${filename} must have .jcss extension`);
188
- }
189
-
190
- // Try to resolve the path
191
- const resolvedPath = path.resolve(process.cwd(), filename);
192
-
193
- // Check if file exists
194
- if (!fs.existsSync(resolvedPath)) {
195
- throw new Error(`File not found: ${filename} (resolved to: ${resolvedPath})`);
196
- }
197
-
198
- return require(resolvedPath);
199
- };
200
-
201
- // Make chaincss available globally
202
- if (typeof global !== 'undefined') {
203
- global.chain = chain;
204
- global.run = run;
205
- global.compile = compile;
206
- global.$ = $;
207
- }
208
-
209
197
  module.exports = {
210
198
  chain,
211
199
  $,
212
200
  run,
213
201
  compile,
214
- get
202
+ createTokens: tokenModule.createTokens,
203
+ responsive: tokenModule.responsive
215
204
  };
package/types.d.ts ADDED
@@ -0,0 +1,148 @@
1
+ /// <reference types="react" />
2
+
3
+ declare module '@melcanz85/chaincss' {
4
+ // Style definition returned by .block()
5
+ export interface StyleDefinition {
6
+ selectors: string[];
7
+ [cssProperty: string]: any;
8
+ }
9
+
10
+ // Base interface for CSS properties (dynamic)
11
+ export interface CSSPropertyBuilder {
12
+ [key: string]: (value: string | number) => ChainBuilder;
13
+ }
14
+
15
+ // Special methods interface
16
+ export interface SpecialMethods {
17
+ block(...selectors: string[]): StyleDefinition;
18
+ token?(path: string): ChainBuilder;
19
+ }
20
+
21
+ // ChainBuilder is the intersection of both
22
+ export type ChainBuilder = CSSPropertyBuilder & SpecialMethods;
23
+
24
+ // The main $ function
25
+ export function $(): ChainBuilder;
26
+
27
+ // Run function for inline styles
28
+ export function run(...styles: StyleDefinition[]): string;
29
+
30
+ // Compile function for objects
31
+ export function compile(styles: Record<string, StyleDefinition>): void;
32
+
33
+ // Get function for importing (VM-safe version)
34
+ export function get(filename: string): any;
35
+
36
+ // Chain object (internal state)
37
+ export const chain: {
38
+ cssOutput: string;
39
+ catcher: any;
40
+ cachedValidProperties: string[];
41
+ };
42
+
43
+ // Processor function
44
+ export function processor(inputFile: string, outputFile: string): Promise<void>;
45
+
46
+ // Watch function
47
+ export function watch(inputFile: string, outputFile: string): void;
48
+
49
+ // Atomic optimizer configuration
50
+ export interface AtomicConfig {
51
+ enabled?: boolean;
52
+ threshold?: number;
53
+ naming?: 'hash' | 'readable' | 'short';
54
+ cache?: boolean;
55
+ cachePath?: string;
56
+ minify?: boolean;
57
+ }
58
+
59
+ // Prefixer configuration
60
+ export interface PrefixerConfig {
61
+ mode?: 'auto' | 'full';
62
+ browsers?: string[];
63
+ enabled?: boolean;
64
+ sourceMap?: boolean;
65
+ sourceMapInline?: boolean;
66
+ }
67
+
68
+ // ChainCSS configuration
69
+ export interface ChainCSSConfig {
70
+ atomic?: AtomicConfig;
71
+ prefixer?: PrefixerConfig;
72
+ sourceMaps?: boolean;
73
+ }
74
+
75
+ // Function to configure ChainCSS
76
+ export function configure(config: ChainCSSConfig): void;
77
+
78
+ // Atomic optimizer instance
79
+ export const atomicOptimizer: {
80
+ optimize(styles: Record<string, StyleDefinition>): string;
81
+ getStats(): {
82
+ totalStyles: number;
83
+ atomicStyles: number;
84
+ uniqueProperties: number;
85
+ savings?: string;
86
+ };
87
+ };
88
+
89
+ // Token system types
90
+ export interface Tokens {
91
+ colors: Record<string, string | Record<string, string>>;
92
+ spacing: Record<string, string>;
93
+ typography: {
94
+ fontFamily: Record<string, string>;
95
+ fontSize: Record<string, string>;
96
+ fontWeight: Record<string, string>;
97
+ lineHeight: Record<string, string>;
98
+ };
99
+ breakpoints: Record<string, string>;
100
+ zIndex: Record<string, string>;
101
+ shadows: Record<string, string>;
102
+ borderRadius: Record<string, string>;
103
+ }
104
+
105
+ export class DesignTokens {
106
+ constructor(tokens: Partial<Tokens>);
107
+ get(path: string, defaultValue?: string): string;
108
+ toCSSVariables(prefix?: string): string;
109
+ createTheme(name: string, overrides: Record<string, any>): DesignTokens;
110
+ }
111
+
112
+ export const tokens: DesignTokens;
113
+ export function createTokens(customTokens: Partial<Tokens>): DesignTokens;
114
+ export function responsive(values: Record<string, string> | string): string;
115
+
116
+ // React hooks types (add to your existing declare module)
117
+ export interface UseChainStylesOptions {
118
+ cache?: boolean;
119
+ namespace?: string;
120
+ watch?: boolean;
121
+ }
122
+
123
+ export function useChainStyles(
124
+ styles: Record<string, any> | (() => Record<string, any>),
125
+ options?: UseChainStylesOptions
126
+ ): Record<string, string>;
127
+
128
+ export function useDynamicChainStyles(
129
+ styleFactory: () => Record<string, any>,
130
+ deps?: any[],
131
+ options?: UseChainStylesOptions
132
+ ): Record<string, string>;
133
+
134
+ export function useThemeChainStyles(
135
+ styles: Record<string, any> | ((theme: any) => Record<string, any>),
136
+ theme: any,
137
+ options?: UseChainStylesOptions
138
+ ): Record<string, string>;
139
+
140
+ export const ChainCSSGlobal: React.FC<{ styles: Record<string, any> }>;
141
+
142
+ export function withChainStyles(
143
+ styles: Record<string, any> | ((props: any) => Record<string, any>),
144
+ options?: UseChainStylesOptions
145
+ ): <P extends object>(Component: React.ComponentType<P>) => React.FC<P & { chainStyles?: Record<string, string> }>;
146
+
147
+ export function cx(...classes: (string | undefined | null | false)[]): string;
148
+ }
@@ -1,22 +0,0 @@
1
- name: Publish Package
2
-
3
- on:
4
- push:
5
- tags:
6
- - 'v*'
7
-
8
- permissions:
9
- id-token: write
10
- contents: read
11
-
12
- jobs:
13
- publish:
14
- runs-on: ubuntu-latest
15
- steps:
16
- - uses: actions/checkout@v4
17
- - uses: actions/setup-node@v4
18
- with:
19
- node-version: '24'
20
- registry-url: 'https://registry.npmjs.org'
21
- - run: npm ci
22
- - run: npm publish
package/publish.sh DELETED
@@ -1,7 +0,0 @@
1
- #!/bin/bash
2
- npm version patch
3
- git push --tags
4
- git checkout main
5
- git merge master
6
- git push origin main
7
- git checkout master