chaincss 2.1.39 → 2.3.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/accessibility-engine.d.ts +57 -0
- package/dist/compiler/constraint-solver.d.ts +85 -0
- package/dist/compiler/css-if-transpiler.d.ts +33 -0
- package/dist/compiler/design-orchestrator.d.ts +119 -0
- package/dist/compiler/intent-api.d.ts +73 -0
- package/dist/compiler/intent-engine.d.ts +19 -1
- package/dist/compiler/layout-intelligence.d.ts +71 -0
- package/dist/compiler/pass-manager.d.ts +157 -0
- package/dist/compiler/pattern-learner.d.ts +112 -0
- package/dist/compiler/responsive-inference.d.ts +63 -0
- package/dist/compiler/scroll-timeline.d.ts +91 -0
- package/dist/compiler/semantic-tokens.d.ts +57 -0
- package/dist/compiler/source-optimizer.d.ts +109 -0
- package/dist/compiler/style-ir.d.ts +183 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +4126 -2
- package/package.json +1 -1
- package/src/compiler/accessibility-engine.ts +502 -0
- package/src/compiler/constraint-solver.ts +407 -0
- package/src/compiler/css-if-transpiler.ts +117 -0
- package/src/compiler/design-orchestrator.ts +322 -0
- package/src/compiler/intent-api.ts +505 -0
- package/src/compiler/intent-engine.ts +291 -1
- package/src/compiler/layout-intelligence.ts +697 -0
- package/src/compiler/pass-manager.ts +657 -0
- package/src/compiler/pattern-learner.ts +398 -0
- package/src/compiler/responsive-inference.ts +415 -0
- package/src/compiler/scroll-timeline.ts +284 -0
- package/src/compiler/semantic-tokens.ts +468 -0
- package/src/compiler/source-optimizer.ts +541 -0
- package/src/compiler/style-ir.ts +495 -0
- package/src/index.ts +209 -0
- package/ROADMAP.md +0 -31
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
// src/compiler/constraint-solver.ts
|
|
2
|
+
/**
|
|
3
|
+
* Constraint-Based Styling Engine
|
|
4
|
+
*
|
|
5
|
+
* Declare relationships, not values. The solver resolves them to CSS.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* .constrain('width', '< parent')
|
|
9
|
+
* .constrain('height', '= width * 0.5')
|
|
10
|
+
* .constrain('sidebar', 'sticky until footer')
|
|
11
|
+
* .constrain('columns', '>= 3 when > 768px')
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { StyleIR, IRRule, IRDeclaration, IRPass } from './style-ir.js';
|
|
15
|
+
import { createDeclaration } from './style-ir.js';
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Types
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
export type ConstraintOperator = '<' | '>' | '<=' | '>=' | '=' | '!=' | '≈';
|
|
22
|
+
|
|
23
|
+
export type ConstraintTarget =
|
|
24
|
+
| 'parent'
|
|
25
|
+
| 'viewport'
|
|
26
|
+
| 'sibling'
|
|
27
|
+
| 'self'
|
|
28
|
+
| string; // CSS value or selector
|
|
29
|
+
|
|
30
|
+
export interface Constraint {
|
|
31
|
+
property: string;
|
|
32
|
+
operator: ConstraintOperator;
|
|
33
|
+
expression: string;
|
|
34
|
+
target?: ConstraintTarget;
|
|
35
|
+
condition?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ResolvedConstraint {
|
|
39
|
+
constraint: Constraint;
|
|
40
|
+
cssProperty: string;
|
|
41
|
+
cssValue: string;
|
|
42
|
+
method: 'direct' | 'calc' | 'aspect-ratio' | 'container-query' | 'sticky' | 'clamp' | 'custom-property' | 'color-mix';
|
|
43
|
+
explanation: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Expression Parser
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Tokenize a constraint expression like "width * 0.5" or "clamp(14, parent.width / 20, 24)"
|
|
52
|
+
*/
|
|
53
|
+
function tokenize(expr: string): string[] {
|
|
54
|
+
const tokens: string[] = [];
|
|
55
|
+
let current = '';
|
|
56
|
+
let inParens = 0;
|
|
57
|
+
|
|
58
|
+
for (const char of expr) {
|
|
59
|
+
if (char === '(') { inParens++; current += char; }
|
|
60
|
+
else if (char === ')') { inParens--; current += char; }
|
|
61
|
+
else if (char === ' ' && inParens === 0) {
|
|
62
|
+
if (current) tokens.push(current);
|
|
63
|
+
current = '';
|
|
64
|
+
} else {
|
|
65
|
+
current += char;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (current) tokens.push(current);
|
|
69
|
+
return tokens;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parse a constraint expression into structured form.
|
|
74
|
+
*/
|
|
75
|
+
function parseExpression(expr: string): {
|
|
76
|
+
left: string;
|
|
77
|
+
operator: string;
|
|
78
|
+
right: string;
|
|
79
|
+
isFunction: boolean;
|
|
80
|
+
functionName?: string;
|
|
81
|
+
functionArgs?: string[];
|
|
82
|
+
} {
|
|
83
|
+
const trimmed = expr.trim();
|
|
84
|
+
|
|
85
|
+
// Check for function calls: clamp(14, parent.width / 20, 24)
|
|
86
|
+
const funcMatch = trimmed.match(/^([a-zA-Z]+)\((.+)\)$/);
|
|
87
|
+
if (funcMatch) {
|
|
88
|
+
const args = funcMatch[2].split(',').map(a => a.trim());
|
|
89
|
+
return {
|
|
90
|
+
left: '',
|
|
91
|
+
operator: 'function',
|
|
92
|
+
right: '',
|
|
93
|
+
isFunction: true,
|
|
94
|
+
functionName: funcMatch[1],
|
|
95
|
+
functionArgs: args,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check for operators: *, /, +, -
|
|
100
|
+
const tokens = tokenize(trimmed);
|
|
101
|
+
if (tokens.length === 3) {
|
|
102
|
+
return {
|
|
103
|
+
left: tokens[0],
|
|
104
|
+
operator: tokens[1],
|
|
105
|
+
right: tokens[2],
|
|
106
|
+
isFunction: false,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Single value
|
|
111
|
+
return {
|
|
112
|
+
left: trimmed,
|
|
113
|
+
operator: '',
|
|
114
|
+
right: '',
|
|
115
|
+
isFunction: false,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ============================================================================
|
|
120
|
+
// Reference Resolver
|
|
121
|
+
// ============================================================================
|
|
122
|
+
|
|
123
|
+
const KNOWN_REFERENCES: Record<string, string> = {
|
|
124
|
+
'parent': '100%',
|
|
125
|
+
'parent.width': '100%',
|
|
126
|
+
'parent.height': '100%',
|
|
127
|
+
'viewport': '100vw',
|
|
128
|
+
'viewport.width': '100vw',
|
|
129
|
+
'viewport.height': '100vh',
|
|
130
|
+
'self': '100%',
|
|
131
|
+
'self.width': '100%',
|
|
132
|
+
'self.height': '100%',
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
function resolveReference(ref: string): string {
|
|
136
|
+
return KNOWN_REFERENCES[ref] || ref;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// Solver
|
|
141
|
+
// ============================================================================
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Resolve a single constraint into a CSS property+value.
|
|
145
|
+
*/
|
|
146
|
+
export function resolveConstraint(constraint: Constraint, context?: Record<string, string>): ResolvedConstraint {
|
|
147
|
+
const { property, operator, expression } = constraint;
|
|
148
|
+
const parsed = parseExpression(expression);
|
|
149
|
+
|
|
150
|
+
// --- 1. Size constraints (width < parent) ---
|
|
151
|
+
if (operator === '<' && expression === 'parent') {
|
|
152
|
+
return {
|
|
153
|
+
constraint,
|
|
154
|
+
cssProperty: 'max-' + property,
|
|
155
|
+
cssValue: '100%',
|
|
156
|
+
method: 'direct',
|
|
157
|
+
explanation: property + ' < parent → max-' + property + ': 100%',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (operator === '>' && expression === 'parent') {
|
|
162
|
+
return {
|
|
163
|
+
constraint,
|
|
164
|
+
cssProperty: 'min-' + property,
|
|
165
|
+
cssValue: '100%',
|
|
166
|
+
method: 'direct',
|
|
167
|
+
explanation: property + ' > parent → min-' + property + ': 100%',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// --- 2. Math expression (height = width * 0.5) ---
|
|
172
|
+
if (operator === '=' && parsed.operator === '*') {
|
|
173
|
+
const leftRef = resolveReference(parsed.left);
|
|
174
|
+
const rightNum = parseFloat(parsed.right);
|
|
175
|
+
|
|
176
|
+
if (!isNaN(rightNum)) {
|
|
177
|
+
// For width/height relations, use aspect-ratio
|
|
178
|
+
if ((property === 'height' && parsed.left === 'width') ||
|
|
179
|
+
(property === 'width' && parsed.left === 'height')) {
|
|
180
|
+
const ratio = rightNum;
|
|
181
|
+
const gcd = findGCD(Math.round(ratio * 100), 100);
|
|
182
|
+
const num = Math.round(ratio * 100) / gcd;
|
|
183
|
+
const den = 100 / gcd;
|
|
184
|
+
return {
|
|
185
|
+
constraint,
|
|
186
|
+
cssProperty: 'aspect-ratio',
|
|
187
|
+
cssValue: num + ' / ' + den,
|
|
188
|
+
method: 'aspect-ratio',
|
|
189
|
+
explanation: property + ' = ' + expression + ' → aspect-ratio: ' + num + '/' + den,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// General math: use calc()
|
|
194
|
+
return {
|
|
195
|
+
constraint,
|
|
196
|
+
cssProperty: property,
|
|
197
|
+
cssValue: 'calc(' + leftRef + ' * ' + rightNum + ')',
|
|
198
|
+
method: 'calc',
|
|
199
|
+
explanation: property + ' = ' + expression + ' → calc(' + leftRef + ' * ' + rightNum + ')',
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Expression with division: width / 20
|
|
205
|
+
if (operator === '=' && parsed.operator === '/') {
|
|
206
|
+
const leftRef = resolveReference(parsed.left);
|
|
207
|
+
const rightNum = parseFloat(parsed.right);
|
|
208
|
+
if (!isNaN(rightNum)) {
|
|
209
|
+
return {
|
|
210
|
+
constraint,
|
|
211
|
+
cssProperty: property,
|
|
212
|
+
cssValue: 'calc(' + leftRef + ' / ' + rightNum + ')',
|
|
213
|
+
method: 'calc',
|
|
214
|
+
explanation: property + ' = ' + expression + ' → calc(' + leftRef + ' / ' + rightNum + ')',
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// --- 3. Function expressions (clamp, min, max) ---
|
|
220
|
+
if (parsed.isFunction && parsed.functionName) {
|
|
221
|
+
const resolvedArgs = (parsed.functionArgs || []).map(resolveReference);
|
|
222
|
+
return {
|
|
223
|
+
constraint,
|
|
224
|
+
cssProperty: property,
|
|
225
|
+
cssValue: parsed.functionName + '(' + resolvedArgs.join(', ') + ')',
|
|
226
|
+
method: parsed.functionName as any,
|
|
227
|
+
explanation: property + ' = ' + expression + ' → ' + parsed.functionName + '()',
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// --- 4. Simple value assignment ---
|
|
232
|
+
if (operator === '=' && !parsed.operator) {
|
|
233
|
+
const resolved = resolveReference(expression);
|
|
234
|
+
return {
|
|
235
|
+
constraint,
|
|
236
|
+
cssProperty: property,
|
|
237
|
+
cssValue: resolved,
|
|
238
|
+
method: 'direct',
|
|
239
|
+
explanation: property + ' = ' + expression + ' → ' + resolved,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// --- 5. Viewport-relative ---
|
|
244
|
+
if (operator === '=' && expression.includes('vw') || expression.includes('vh')) {
|
|
245
|
+
return {
|
|
246
|
+
constraint,
|
|
247
|
+
cssProperty: property,
|
|
248
|
+
cssValue: expression,
|
|
249
|
+
method: 'direct',
|
|
250
|
+
explanation: property + ' = ' + expression,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Fallback: pass through as calc()
|
|
255
|
+
return {
|
|
256
|
+
constraint,
|
|
257
|
+
cssProperty: property,
|
|
258
|
+
cssValue: expression,
|
|
259
|
+
method: 'direct',
|
|
260
|
+
explanation: property + ' = ' + expression + ' (passthrough)',
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Resolve sticky-until constraint.
|
|
266
|
+
* "sticky until footer" → position: sticky + scroll-driven animation
|
|
267
|
+
*/
|
|
268
|
+
export function resolveStickyUntil(selector: string, untilSelector: string): ResolvedConstraint {
|
|
269
|
+
return {
|
|
270
|
+
constraint: {
|
|
271
|
+
property: 'position',
|
|
272
|
+
operator: '=',
|
|
273
|
+
expression: 'sticky until ' + untilSelector,
|
|
274
|
+
},
|
|
275
|
+
cssProperty: 'position',
|
|
276
|
+
cssValue: 'sticky; top: 0; animation: sticky-' + selector.replace('.', '') + ' 1s linear both; animation-timeline: scroll(); animation-range: contain 0% contain 100%',
|
|
277
|
+
method: 'sticky',
|
|
278
|
+
explanation: 'sticky until ' + untilSelector + ' → position: sticky + scroll-timeline',
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ============================================================================
|
|
283
|
+
// Container Query Constraint
|
|
284
|
+
// ============================================================================
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Resolve ">= N when > Xpx" constraints to container queries.
|
|
288
|
+
*/
|
|
289
|
+
export function resolveContainerQuery(
|
|
290
|
+
property: string,
|
|
291
|
+
operator: string,
|
|
292
|
+
value: string,
|
|
293
|
+
condition: string
|
|
294
|
+
): { atRule: { type: string; query: string; declarations: Array<{ property: string; value: string }> }; explanation: string } {
|
|
295
|
+
const widthMatch = condition.match(/>\s*(\d+)(px|rem|em)?/);
|
|
296
|
+
if (widthMatch) {
|
|
297
|
+
const width = widthMatch[1] + (widthMatch[2] || 'px');
|
|
298
|
+
return {
|
|
299
|
+
atRule: {
|
|
300
|
+
type: 'container',
|
|
301
|
+
query: '(min-width: ' + width + ')',
|
|
302
|
+
declarations: [{ property, value }],
|
|
303
|
+
},
|
|
304
|
+
explanation: property + ' ' + operator + ' ' + value + ' when > ' + width + ' → @container (min-width: ' + width + ')',
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
atRule: { type: 'container', query: condition, declarations: [{ property, value }] },
|
|
309
|
+
explanation: property + ' when ' + condition + ' → @container ' + condition,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ============================================================================
|
|
314
|
+
// Constraint Solver Pass (for IR Pipeline)
|
|
315
|
+
// ============================================================================
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* IR Pass: resolve all constraints in the IR to concrete CSS.
|
|
319
|
+
*/
|
|
320
|
+
export const constraintSolverPass: IRPass = (ir: StyleIR): StyleIR => {
|
|
321
|
+
for (const rule of ir.rules) {
|
|
322
|
+
// Check for constraint metadata
|
|
323
|
+
const constraints: Constraint[] = rule.meta._constraints || [];
|
|
324
|
+
if (constraints.length === 0) continue;
|
|
325
|
+
|
|
326
|
+
for (const constraint of constraints) {
|
|
327
|
+
const resolved = resolveConstraint(constraint);
|
|
328
|
+
|
|
329
|
+
// Add the resolved declaration
|
|
330
|
+
rule.declarations.push({
|
|
331
|
+
id: 'constraint-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6),
|
|
332
|
+
property: resolved.cssProperty,
|
|
333
|
+
value: resolved.cssValue,
|
|
334
|
+
history: [{
|
|
335
|
+
pass: 'constraint-solver',
|
|
336
|
+
action: 'resolved-constraint',
|
|
337
|
+
timestamp: Date.now(),
|
|
338
|
+
reason: resolved.explanation,
|
|
339
|
+
}],
|
|
340
|
+
meta: { constraint },
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return ir;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// ============================================================================
|
|
348
|
+
// Chain API Extension
|
|
349
|
+
// ============================================================================
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Parse a chain-style constraint call into the Constraint format.
|
|
353
|
+
*
|
|
354
|
+
* @example
|
|
355
|
+
* parseConstraint('width', '< parent')
|
|
356
|
+
* // => { property: 'width', operator: '<', expression: 'parent' }
|
|
357
|
+
*/
|
|
358
|
+
export function parseConstraint(property: string, expression: string): Constraint {
|
|
359
|
+
// Detect operator from expression
|
|
360
|
+
let operator: ConstraintOperator = '=';
|
|
361
|
+
let cleanExpr = expression;
|
|
362
|
+
|
|
363
|
+
const opMatch = expression.match(/^([<>=!≈]+)\s*(.*)/);
|
|
364
|
+
if (opMatch) {
|
|
365
|
+
operator = opMatch[1] as ConstraintOperator;
|
|
366
|
+
cleanExpr = opMatch[2];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Detect condition: ">= 3 when > 768px"
|
|
370
|
+
let condition: string | undefined;
|
|
371
|
+
const condMatch = cleanExpr.match(/^(.+)\s+when\s+(.+)$/);
|
|
372
|
+
if (condMatch) {
|
|
373
|
+
cleanExpr = condMatch[1];
|
|
374
|
+
condition = condMatch[2];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
property,
|
|
379
|
+
operator,
|
|
380
|
+
expression: cleanExpr,
|
|
381
|
+
condition,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ============================================================================
|
|
386
|
+
// Utilities
|
|
387
|
+
// ============================================================================
|
|
388
|
+
|
|
389
|
+
function findGCD(a: number, b: number): number {
|
|
390
|
+
return b === 0 ? a : findGCD(b, a % b);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ============================================================================
|
|
394
|
+
// Quick API
|
|
395
|
+
// ============================================================================
|
|
396
|
+
|
|
397
|
+
export const constraintSolver = {
|
|
398
|
+
resolve: resolveConstraint,
|
|
399
|
+
resolveStickyUntil,
|
|
400
|
+
resolveContainerQuery,
|
|
401
|
+
parseConstraint,
|
|
402
|
+
parseExpression,
|
|
403
|
+
resolveReference,
|
|
404
|
+
pass: constraintSolverPass,
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
export default constraintSolver;
|
|
@@ -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 };
|