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/dist/compiler/css-if-transpiler.d.ts +33 -0
- package/dist/compiler/design-orchestrator.d.ts +119 -0
- package/dist/compiler/intent-engine.d.ts +19 -1
- package/dist/compiler/scroll-timeline.d.ts +91 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +651 -2
- package/package.json +1 -1
- package/src/compiler/css-if-transpiler.ts +117 -0
- package/src/compiler/design-orchestrator.ts +322 -0
- package/src/compiler/intent-engine.ts +291 -1
- package/src/compiler/scroll-timeline.ts +284 -0
- package/src/index.ts +34 -0
package/package.json
CHANGED
|
@@ -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;
|