chaincss 2.1.39 → 2.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chaincss",
3
- "version": "2.1.39",
3
+ "version": "2.2.0",
4
4
  "description": "ChainCSS - The first CSS-in-JS library with true auto-detection mixed mode. Zero runtime by default, dynamic when you need it.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,117 @@
1
+ // src/compiler/css-if-transpiler.ts
2
+ /**
3
+ * CSS if() Transpiler
4
+ * Detects conditional style patterns and emits:
5
+ * 1. Native CSS if() — Chrome 137+
6
+ * 2. @supports fallback — Firefox, Safari
7
+ */
8
+
9
+ export interface IfCondition {
10
+ property: string;
11
+ variable: string;
12
+ conditions: Record<string, string | number>;
13
+ defaultValue: string | number;
14
+ }
15
+
16
+ export interface DetectedCondition {
17
+ property: string;
18
+ variable: string;
19
+ conditions: Record<string, string | number>;
20
+ defaultValue: string | number;
21
+ }
22
+
23
+ /**
24
+ * Detect conditional patterns from _conditions metadata.
25
+ * When chain.when() branches set the same property to different values,
26
+ * those can be compiled to CSS if().
27
+ */
28
+ export function detectIfPatterns(
29
+ styles: Record<string, any>
30
+ ): DetectedCondition[] {
31
+ const conditions: DetectedCondition[] = [];
32
+ if (!styles._conditions) return conditions;
33
+
34
+ const condEntries = Object.entries(styles._conditions || {});
35
+ for (const [variable, branches] of condEntries) {
36
+ const branch = branches as { true: Record<string, any>; false: Record<string, any> };
37
+ const trueStyles = branch.true || {};
38
+ const falseStyles = branch.false || {};
39
+
40
+ const allProps = new Set([...Object.keys(trueStyles), ...Object.keys(falseStyles)]);
41
+ for (const prop of allProps) {
42
+ if (prop.startsWith('_') || prop === 'selectors') continue;
43
+ const trueVal = trueStyles[prop];
44
+ const falseVal = falseStyles[prop];
45
+ if (trueVal !== undefined && falseVal !== undefined && trueVal !== falseVal) {
46
+ conditions.push({
47
+ property: prop,
48
+ variable: variable.startsWith('--') ? variable : '--' + variable,
49
+ conditions: { true: trueVal },
50
+ defaultValue: falseVal,
51
+ });
52
+ }
53
+ }
54
+ }
55
+ return conditions;
56
+ }
57
+
58
+ /**
59
+ * Generate CSS if() output for detected conditions.
60
+ */
61
+ export function emitCSSIf(
62
+ selector: string,
63
+ detectedConditions: DetectedCondition[],
64
+ baseProperties: Record<string, string | number> = {}
65
+ ): string {
66
+ if (detectedConditions.length === 0) return '';
67
+
68
+ let css = '';
69
+
70
+ // Native CSS if() block
71
+ css += '/* Native CSS if() — Chrome 137+ */\n';
72
+ css += selector + ' {\n';
73
+ for (const [prop, value] of Object.entries(baseProperties)) {
74
+ css += ' ' + prop + ': ' + value + ';\n';
75
+ }
76
+ for (const cond of detectedConditions) {
77
+ const entries = Object.entries(cond.conditions);
78
+ if (entries.length === 1) {
79
+ const [condition, val] = entries[0];
80
+ css += ' ' + cond.property + ': if(style(' + cond.variable + ': ' + condition + '): ' + val + ' else ' + cond.defaultValue + ');\n';
81
+ } else {
82
+ let chain = '';
83
+ for (let i = 0; i < entries.length; i++) {
84
+ const [condition, val] = entries[i];
85
+ chain += i === 0
86
+ ? 'if(style(' + cond.variable + ': ' + condition + '): ' + val
87
+ : ' else if(style(' + cond.variable + ': ' + condition + '): ' + val;
88
+ }
89
+ chain += ' else ' + cond.defaultValue + ')'.repeat(entries.length);
90
+ css += ' ' + cond.property + ': ' + chain + ';\n';
91
+ }
92
+ }
93
+ css += '}\n\n';
94
+
95
+ // @supports fallback
96
+ css += '/* Fallback for browsers without CSS if() */\n';
97
+ css += '@supports not (property: if()) {\n';
98
+ css += ' ' + selector + ' {\n';
99
+ for (const [prop, value] of Object.entries(baseProperties)) {
100
+ css += ' ' + prop + ': ' + value + ';\n';
101
+ }
102
+ for (const cond of detectedConditions) {
103
+ css += ' ' + cond.property + ': ' + cond.defaultValue + ';\n';
104
+ }
105
+ css += ' }\n';
106
+ for (const cond of detectedConditions) {
107
+ for (const [condition, val] of Object.entries(cond.conditions)) {
108
+ const modClass = selector + '--' + cond.variable.replace('--', '') + '-' + condition;
109
+ css += ' ' + modClass + ' { ' + cond.property + ': ' + val + '; }\n';
110
+ }
111
+ }
112
+ css += '}\n';
113
+
114
+ return css;
115
+ }
116
+
117
+ export default { detectIfPatterns, emitCSSIf };
@@ -0,0 +1,322 @@
1
+ // src/compiler/design-orchestrator.ts
2
+ /**
3
+ * Design System Orchestrator
4
+ *
5
+ * 1. WCAG Contrast Ratio Checker — validates text/background combos at build time
6
+ * 2. Contextual Tokens — tokens that auto-flip based on container context
7
+ * 3. Token Relationship Validator — ensures design tokens are consistent
8
+ */
9
+
10
+ // ============================================================================
11
+ // Types
12
+ // ============================================================================
13
+
14
+ export interface ContrastResult {
15
+ foreground: string;
16
+ background: string;
17
+ ratio: number;
18
+ passes: { AA: boolean; AALarge: boolean; AAA: boolean; AAALarge: boolean };
19
+ suggestion?: string;
20
+ }
21
+
22
+ export interface ContrastReport {
23
+ checks: ContrastResult[];
24
+ failures: ContrastResult[];
25
+ warnings: ContrastResult[];
26
+ passCount: number;
27
+ failCount: number;
28
+ summary: string;
29
+ }
30
+
31
+ export interface ContextualToken {
32
+ name: string;
33
+ default: string;
34
+ contexts: Record<string, string>; // e.g., { 'dark-section': 'white', 'light-section': 'black' }
35
+ }
36
+
37
+ export interface TokenContext {
38
+ name: string;
39
+ parentSelector?: string;
40
+ tokens: Record<string, any>;
41
+ }
42
+
43
+ // ============================================================================
44
+ // Color Utilities
45
+ // ============================================================================
46
+
47
+ /**
48
+ * Parse CSS color to RGBA components.
49
+ * Supports: hex, rgb(), rgba(), named colors
50
+ */
51
+ function parseColor(color: string): { r: number; g: number; b: number; a: number } | null {
52
+ const trimmed = color.trim().toLowerCase();
53
+
54
+ // hex
55
+ const hexMatch = trimmed.match(/^#([a-f0-9]{3}|[a-f0-9]{6}|[a-f0-9]{8})$/);
56
+ if (hexMatch) {
57
+ let hex = hexMatch[1];
58
+ if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
59
+ if (hex.length === 8) {
60
+ return {
61
+ r: parseInt(hex.slice(0, 2), 16),
62
+ g: parseInt(hex.slice(2, 4), 16),
63
+ b: parseInt(hex.slice(4, 6), 16),
64
+ a: parseInt(hex.slice(6, 8), 16) / 255,
65
+ };
66
+ }
67
+ return {
68
+ r: parseInt(hex.slice(0, 2), 16),
69
+ g: parseInt(hex.slice(2, 4), 16),
70
+ b: parseInt(hex.slice(4, 6), 16),
71
+ a: 1,
72
+ };
73
+ }
74
+
75
+ // rgb/rgba
76
+ const rgbMatch = trimmed.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)$/);
77
+ if (rgbMatch) {
78
+ return {
79
+ r: parseInt(rgbMatch[1]),
80
+ g: parseInt(rgbMatch[2]),
81
+ b: parseInt(rgbMatch[3]),
82
+ a: rgbMatch[4] ? parseFloat(rgbMatch[4]) : 1,
83
+ };
84
+ }
85
+
86
+ // Named colors (common subset)
87
+ const named: Record<string, [number, number, number]> = {
88
+ white: [255, 255, 255], black: [0, 0, 0],
89
+ red: [255, 0, 0], green: [0, 128, 0], blue: [0, 0, 255],
90
+ gray: [128, 128, 128], grey: [128, 128, 128],
91
+ transparent: [0, 0, 0],
92
+ };
93
+ if (named[trimmed]) {
94
+ const [r, g, b] = named[trimmed];
95
+ return { r, g, b, a: trimmed === 'transparent' ? 0 : 1 };
96
+ }
97
+
98
+ return null;
99
+ }
100
+
101
+ /**
102
+ * Calculate relative luminance per WCAG 2.1.
103
+ */
104
+ function relativeLuminance(r: number, g: number, b: number): number {
105
+ const rsrgb = r / 255;
106
+ const gsrgb = g / 255;
107
+ const bsrgb = b / 255;
108
+
109
+ const rLin = rsrgb <= 0.04045 ? rsrgb / 12.92 : Math.pow((rsrgb + 0.055) / 1.055, 2.4);
110
+ const gLin = gsrgb <= 0.04045 ? gsrgb / 12.92 : Math.pow((gsrgb + 0.055) / 1.055, 2.4);
111
+ const bLin = bsrgb <= 0.04045 ? bsrgb / 12.92 : Math.pow((bsrgb + 0.055) / 1.055, 2.4);
112
+
113
+ return 0.2126 * rLin + 0.7152 * gLin + 0.0722 * bLin;
114
+ }
115
+
116
+ /**
117
+ * Calculate WCAG contrast ratio between two colors.
118
+ * Returns value between 1 (no contrast) and 21 (max contrast).
119
+ */
120
+ export function contrastRatio(foreground: string, background: string): number {
121
+ const fg = parseColor(foreground);
122
+ const bg = parseColor(background);
123
+ if (!fg || !bg) return -1;
124
+
125
+ const lumFg = relativeLuminance(fg.r, fg.g, fg.b) + 0.05;
126
+ const lumBg = relativeLuminance(bg.r, bg.g, bg.b) + 0.05;
127
+
128
+ const lighter = Math.max(lumFg, lumBg);
129
+ const darker = Math.min(lumFg, lumBg);
130
+
131
+ return lighter / darker;
132
+ }
133
+
134
+ /**
135
+ * Check WCAG compliance levels.
136
+ * AA: 4.5:1 normal, 3:1 large text
137
+ * AAA: 7:1 normal, 4.5:1 large text
138
+ */
139
+ export function checkContrast(foreground: string, background: string): ContrastResult {
140
+ const ratio = contrastRatio(foreground, background);
141
+
142
+ return {
143
+ foreground,
144
+ background,
145
+ ratio: Math.round(ratio * 100) / 100,
146
+ passes: {
147
+ AA: ratio >= 4.5,
148
+ AALarge: ratio >= 3,
149
+ AAA: ratio >= 7,
150
+ AAALarge: ratio >= 4.5,
151
+ },
152
+ suggestion: ratio < 4.5
153
+ ? `Contrast ratio ${Math.round(ratio * 100) / 100} fails AA. Need ${Math.round((4.5 - ratio) * 100) / 100} more. Consider darkening/lightening.`
154
+ : undefined,
155
+ };
156
+ }
157
+
158
+ // ============================================================================
159
+ // Contrast Report Generator
160
+ // ============================================================================
161
+
162
+ /**
163
+ * Run contrast checks across a set of style definitions.
164
+ */
165
+ export function auditContrast(
166
+ styles: Array<{ selector: string; color: string; backgroundColor: string }>
167
+ ): ContrastReport {
168
+ const checks: ContrastResult[] = [];
169
+
170
+ for (const style of styles) {
171
+ if (style.color && style.backgroundColor) {
172
+ checks.push(checkContrast(style.color, style.backgroundColor));
173
+ }
174
+ }
175
+
176
+ const failures = checks.filter(c => !c.passes.AA);
177
+ const warnings = checks.filter(c => c.passes.AA && !c.passes.AAA);
178
+
179
+ return {
180
+ checks,
181
+ failures,
182
+ warnings,
183
+ passCount: checks.length - failures.length,
184
+ failCount: failures.length,
185
+ summary: failures.length === 0
186
+ ? 'All ' + checks.length + ' contrast checks pass AA.'
187
+ : failures.length + ' of ' + checks.length + ' contrast checks FAIL AA.',
188
+ };
189
+ }
190
+
191
+ // ============================================================================
192
+ // Contextual Token Engine
193
+ // ============================================================================
194
+
195
+ /**
196
+ * Contextual tokens that auto-resolve based on parent container.
197
+ *
198
+ * @example
199
+ * const buttonText = contextualToken({
200
+ * default: '#1a1a1a',
201
+ * contexts: {
202
+ * '.dark-section': '#ffffff',
203
+ * '.hero': '#ffffff',
204
+ * },
205
+ * });
206
+ *
207
+ * resolveContextual(buttonText, '.dark-section .my-button')
208
+ * // => '#ffffff'
209
+ */
210
+ export function createContextualToken(
211
+ defaultValue: string,
212
+ contexts: Record<string, string> = {}
213
+ ): ContextualToken {
214
+ const name = 'ctx-' + Math.random().toString(36).slice(2, 8);
215
+ return { name, default: defaultValue, contexts };
216
+ }
217
+
218
+ /**
219
+ * Resolve a contextual token based on the current selector path.
220
+ * Matches the most specific context that applies.
221
+ */
222
+ export function resolveContextual(
223
+ token: ContextualToken,
224
+ selectorPath: string
225
+ ): string {
226
+ // Find matching contexts — longest match wins (most specific)
227
+ let bestMatch = token.default;
228
+ let bestLength = 0;
229
+
230
+ for (const [context, value] of Object.entries(token.contexts)) {
231
+ if (selectorPath.includes(context) && context.length > bestLength) {
232
+ bestMatch = value;
233
+ bestLength = context.length;
234
+ }
235
+ }
236
+
237
+ return bestMatch;
238
+ }
239
+
240
+ /**
241
+ * Generate CSS custom property fallback for contextual tokens.
242
+ *
243
+ * @example
244
+ * generateContextualCSS('button-text', contextualToken)
245
+ * // => "
246
+ * // .my-button { --button-text: #1a1a1a; }
247
+ * // .dark-section .my-button { --button-text: #ffffff; }
248
+ * // "
249
+ */
250
+ export function generateContextualCSS(
251
+ propertyName: string,
252
+ token: ContextualToken,
253
+ baseSelector: string
254
+ ): string {
255
+ let css = '';
256
+
257
+ // Default
258
+ css += baseSelector + ' { ' + propertyName + ': ' + token.default + '; }\n';
259
+
260
+ // Context overrides
261
+ for (const [context, value] of Object.entries(token.contexts)) {
262
+ css += context + ' ' + baseSelector + ' { ' + propertyName + ': ' + value + '; }\n';
263
+ }
264
+
265
+ return css;
266
+ }
267
+
268
+ // ============================================================================
269
+ // Token Relationship Validator
270
+ // ============================================================================
271
+
272
+ /**
273
+ * Validate that token references are consistent.
274
+ * E.g., "primary" should have both foreground and background variants
275
+ * that contrast well with each other.
276
+ */
277
+ export function validateTokenRelationships(
278
+ tokens: Record<string, any>,
279
+ pairs: Array<{ foreground: string; background: string; label: string }>
280
+ ): ContrastReport {
281
+ const styles: Array<{ selector: string; color: string; backgroundColor: string }> = [];
282
+
283
+ for (const pair of pairs) {
284
+ const fg = resolveTokenPath(tokens, pair.foreground);
285
+ const bg = resolveTokenPath(tokens, pair.background);
286
+ if (fg && bg) {
287
+ styles.push({ selector: pair.label, color: fg, backgroundColor: bg });
288
+ }
289
+ }
290
+
291
+ return auditContrast(styles);
292
+ }
293
+
294
+ /**
295
+ * Resolve a dot-path token reference like "colors.primary.500".
296
+ */
297
+ function resolveTokenPath(tokens: Record<string, any>, path: string): string | null {
298
+ const parts = path.split('.');
299
+ let current: any = tokens;
300
+ for (const part of parts) {
301
+ if (current === undefined || current === null) return null;
302
+ current = current[part];
303
+ }
304
+ return typeof current === 'string' ? current : null;
305
+ }
306
+
307
+ // ============================================================================
308
+ // Quick API
309
+ // ============================================================================
310
+
311
+ export const orchestrator = {
312
+ contrastRatio,
313
+ checkContrast,
314
+ auditContrast,
315
+ createContextualToken,
316
+ resolveContextual,
317
+ generateContextualCSS,
318
+ validateTokenRelationships,
319
+ parseColor,
320
+ };
321
+
322
+ export default orchestrator;