@melcanz85/chaincss 1.7.3 → 1.9.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/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
+ };