chaincss 2.1.38 → 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/ROADMAP.md +31 -0
- package/dist/cli/index.js +458 -3
- package/dist/compiler/analyzer.d.ts +12 -0
- 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 +49 -0
- package/dist/compiler/math-engine.d.ts +89 -0
- package/dist/compiler/scroll-timeline.d.ts +91 -0
- package/dist/compiler/style-graph.d.ts +30 -0
- package/dist/core/compiler.d.ts +12 -0
- package/dist/core/types.d.ts +145 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +1765 -9
- package/dist/plugins/vite.js +451 -3
- package/package.json +1 -1
- package/src/compiler/analyzer.ts +62 -0
- package/src/compiler/css-if-transpiler.ts +117 -0
- package/src/compiler/design-orchestrator.ts +322 -0
- package/src/compiler/intent-engine.ts +402 -0
- package/src/compiler/math-engine.ts +511 -0
- package/src/compiler/scroll-timeline.ts +284 -0
- package/src/compiler/style-graph.ts +660 -0
- package/src/core/compiler.ts +40 -0
- package/src/core/types.ts +206 -0
- package/src/index.ts +103 -1
- package/demo/demo/node_modules/caniuse-db/fulldata-json/data-2.0.json +0 -1
- package/demo/index.html +0 -16
- package/demo/package.json +0 -20
- package/demo/src/App.tsx +0 -117
- package/demo/src/chaincss-barrel.ts +0 -9
- package/demo/src/main.tsx +0 -8
- package/demo/src/styles.chain.ts +0 -300
- package/demo/vite.config.ts +0 -46
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// FILE: src/compiler/math-engine.ts
|
|
3
|
+
// Zero-runtime CSS Math Engine with Unit Resolution
|
|
4
|
+
// ============================================================================
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
CSSUnit,
|
|
8
|
+
CSSMathValue,
|
|
9
|
+
MathContext,
|
|
10
|
+
MathResult,
|
|
11
|
+
FluidTypeConfig
|
|
12
|
+
} from '../core/types.js';
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Types
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
export type { CSSUnit, CSSMathValue, MathContext, MathResult, FluidTypeConfig };
|
|
19
|
+
|
|
20
|
+
export type MathOp = 'add' | 'subtract' | 'multiply' | 'divide';
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Constants
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
const DEFAULT_CONTEXT: Required<MathContext> = {
|
|
27
|
+
rootFontSize: 16,
|
|
28
|
+
viewportWidth: 1920,
|
|
29
|
+
viewportHeight: 1080,
|
|
30
|
+
parentFontSize: 16,
|
|
31
|
+
dpi: 96,
|
|
32
|
+
elementWidth: 1920,
|
|
33
|
+
elementHeight: 1080,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const UNIT_CATEGORIES: Record<string, CSSUnit[]> = {
|
|
37
|
+
absolute: ['px', 'cm', 'mm', 'in', 'pt', 'pc'],
|
|
38
|
+
relative: ['rem', 'em', '%', 'ch', 'ex'],
|
|
39
|
+
viewport: ['vw', 'vh', 'vmin', 'vmax'],
|
|
40
|
+
angle: ['deg', 'rad', 'turn', 'grad'],
|
|
41
|
+
time: ['s', 'ms'],
|
|
42
|
+
resolution: ['dpi', 'dpcm', 'dppx'],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const PX_CONVERSIONS: Record<string, number> = {
|
|
46
|
+
'cm': 37.795,
|
|
47
|
+
'mm': 3.7795,
|
|
48
|
+
'in': 96,
|
|
49
|
+
'pt': 1.333,
|
|
50
|
+
'pc': 16,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// Parsing Utilities
|
|
55
|
+
// ============================================================================
|
|
56
|
+
|
|
57
|
+
function parseCSSValue(input: string | number): CSSMathValue {
|
|
58
|
+
if (typeof input === 'number') {
|
|
59
|
+
return { value: input, unit: 'px' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const trimmed = input.trim();
|
|
63
|
+
const match = trimmed.match(/^(-?\d+(?:\.\d+)?)\s*(px|rem|em|%|vw|vh|vmin|vmax|ch|ex|cm|mm|in|pt|pc|deg|rad|turn|grad|s|ms|dpi|dpcm|dppx)?$/i);
|
|
64
|
+
|
|
65
|
+
if (match) {
|
|
66
|
+
return {
|
|
67
|
+
value: parseFloat(match[1]),
|
|
68
|
+
unit: (match[2]?.toLowerCase() as CSSUnit) || 'px',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Fallback: try to extract numeric value
|
|
73
|
+
const numMatch = trimmed.match(/(-?\d+(?:\.\d+)?)/);
|
|
74
|
+
return {
|
|
75
|
+
value: numMatch ? parseFloat(numMatch[1]) : 0,
|
|
76
|
+
unit: 'px',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getUnitCategory(unit: CSSUnit): string {
|
|
81
|
+
for (const [category, units] of Object.entries(UNIT_CATEGORIES)) {
|
|
82
|
+
if (units.includes(unit)) return category;
|
|
83
|
+
}
|
|
84
|
+
return 'unknown';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// Unit Resolution Engine
|
|
89
|
+
// ============================================================================
|
|
90
|
+
|
|
91
|
+
function resolveToPx(value: CSSMathValue, context: Required<MathContext>): number {
|
|
92
|
+
const { value: v, unit } = value;
|
|
93
|
+
|
|
94
|
+
switch (unit) {
|
|
95
|
+
case 'px': return v;
|
|
96
|
+
case 'rem': return v * context.rootFontSize;
|
|
97
|
+
case 'em': return v * context.parentFontSize;
|
|
98
|
+
case '%': return (v / 100) * context.parentFontSize; // Default to font-relative
|
|
99
|
+
case 'vw': return (v / 100) * context.viewportWidth;
|
|
100
|
+
case 'vh': return (v / 100) * context.viewportHeight;
|
|
101
|
+
case 'vmin': return (v / 100) * Math.min(context.viewportWidth, context.viewportHeight);
|
|
102
|
+
case 'vmax': return (v / 100) * Math.max(context.viewportWidth, context.viewportHeight);
|
|
103
|
+
case 'cm': return v * (PX_CONVERSIONS['cm'] || 37.795);
|
|
104
|
+
case 'mm': return v * (PX_CONVERSIONS['mm'] || 3.7795);
|
|
105
|
+
case 'in': return v * (PX_CONVERSIONS['in'] || 96);
|
|
106
|
+
case 'pt': return v * (PX_CONVERSIONS['pt'] || 1.333);
|
|
107
|
+
case 'pc': return v * (PX_CONVERSIONS['pc'] || 16);
|
|
108
|
+
// ch/ex are approximate
|
|
109
|
+
case 'ch': return v * (context.parentFontSize * 0.5);
|
|
110
|
+
case 'ex': return v * (context.parentFontSize * 0.45);
|
|
111
|
+
default: return v;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function resolveFromPx(px: number, targetUnit: CSSUnit, context: Required<MathContext>): number {
|
|
116
|
+
switch (targetUnit) {
|
|
117
|
+
case 'px': return px;
|
|
118
|
+
case 'rem': return px / context.rootFontSize;
|
|
119
|
+
case 'em': return px / context.parentFontSize;
|
|
120
|
+
case '%': return (px / context.parentFontSize) * 100;
|
|
121
|
+
case 'vw': return (px / context.viewportWidth) * 100;
|
|
122
|
+
case 'vh': return (px / context.viewportHeight) * 100;
|
|
123
|
+
case 'vmin': return (px / Math.min(context.viewportWidth, context.viewportHeight)) * 100;
|
|
124
|
+
case 'vmax': return (px / Math.max(context.viewportWidth, context.viewportHeight)) * 100;
|
|
125
|
+
case 'cm': return px / (PX_CONVERSIONS['cm'] || 37.795);
|
|
126
|
+
case 'mm': return px / (PX_CONVERSIONS['mm'] || 3.7795);
|
|
127
|
+
case 'in': return px / (PX_CONVERSIONS['in'] || 96);
|
|
128
|
+
case 'pt': return px / (PX_CONVERSIONS['pt'] || 1.333);
|
|
129
|
+
case 'pc': return px / (PX_CONVERSIONS['pc'] || 16);
|
|
130
|
+
case 'ch': return px / (context.parentFontSize * 0.5);
|
|
131
|
+
case 'ex': return px / (context.parentFontSize * 0.45);
|
|
132
|
+
default: return px;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function canResolve(a: CSSMathValue, b: CSSMathValue, context: Required<MathContext>): boolean {
|
|
137
|
+
const catA = getUnitCategory(a.unit);
|
|
138
|
+
const catB = getUnitCategory(b.unit);
|
|
139
|
+
|
|
140
|
+
// Same category can resolve
|
|
141
|
+
if (catA === catB) return true;
|
|
142
|
+
|
|
143
|
+
// Mixed absolute/relative can resolve with context
|
|
144
|
+
if ((catA === 'absolute' || catA === 'relative') &&
|
|
145
|
+
(catB === 'absolute' || catB === 'relative')) return true;
|
|
146
|
+
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ============================================================================
|
|
151
|
+
// Result Factory
|
|
152
|
+
// ============================================================================
|
|
153
|
+
|
|
154
|
+
function createResult(
|
|
155
|
+
value: number,
|
|
156
|
+
unit: CSSUnit | 'calc' | 'mixed',
|
|
157
|
+
expression: string,
|
|
158
|
+
resolved: CSSMathValue | null,
|
|
159
|
+
explanations: string[] = []
|
|
160
|
+
): MathResult {
|
|
161
|
+
return {
|
|
162
|
+
value,
|
|
163
|
+
unit,
|
|
164
|
+
expression,
|
|
165
|
+
resolved,
|
|
166
|
+
explanations,
|
|
167
|
+
toString(): string {
|
|
168
|
+
return this.expression;
|
|
169
|
+
},
|
|
170
|
+
toCalc(): string {
|
|
171
|
+
return unit === 'calc' ? expression : `calc(${expression})`;
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ============================================================================
|
|
177
|
+
// Core Math Operations
|
|
178
|
+
// ============================================================================
|
|
179
|
+
|
|
180
|
+
function operate(
|
|
181
|
+
a: string | number,
|
|
182
|
+
op: MathOp,
|
|
183
|
+
b: string | number,
|
|
184
|
+
context?: MathContext
|
|
185
|
+
): MathResult {
|
|
186
|
+
const ctx: Required<MathContext> = { ...DEFAULT_CONTEXT, ...context };
|
|
187
|
+
const valA = parseCSSValue(a);
|
|
188
|
+
const valB = parseCSSValue(b);
|
|
189
|
+
const explanations: string[] = [];
|
|
190
|
+
|
|
191
|
+
// Same unit — direct operation
|
|
192
|
+
if (valA.unit === valB.unit) {
|
|
193
|
+
let result: number;
|
|
194
|
+
switch (op) {
|
|
195
|
+
case 'add': result = valA.value + valB.value; break;
|
|
196
|
+
case 'subtract': result = valA.value - valB.value; break;
|
|
197
|
+
case 'multiply': result = valA.value * valB.value; break;
|
|
198
|
+
case 'divide': result = valA.value / valB.value; break;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const rounded = Math.round(result * 100) / 100;
|
|
202
|
+
explanations.push(`Same unit (${valA.unit}) — direct ${op}`);
|
|
203
|
+
|
|
204
|
+
return createResult(
|
|
205
|
+
rounded,
|
|
206
|
+
valA.unit,
|
|
207
|
+
`${rounded}${valA.unit}`,
|
|
208
|
+
{ value: rounded, unit: valA.unit },
|
|
209
|
+
explanations
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Can resolve to common unit
|
|
214
|
+
if (canResolve(valA, valB, ctx)) {
|
|
215
|
+
const pxA = resolveToPx(valA, ctx);
|
|
216
|
+
const pxB = resolveToPx(valB, ctx);
|
|
217
|
+
|
|
218
|
+
let pxResult: number;
|
|
219
|
+
switch (op) {
|
|
220
|
+
case 'add': pxResult = pxA + pxB; break;
|
|
221
|
+
case 'subtract': pxResult = pxA - pxB; break;
|
|
222
|
+
case 'multiply': pxResult = pxA * pxB; break;
|
|
223
|
+
case 'divide': pxResult = pxA / pxB; break;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const rounded = Math.round(pxResult * 100) / 100;
|
|
227
|
+
explanations.push(`Resolved ${valA.value}${valA.unit} → ${Math.round(pxA * 100) / 100}px`);
|
|
228
|
+
explanations.push(`Resolved ${valB.value}${valB.unit} → ${Math.round(pxB * 100) / 100}px`);
|
|
229
|
+
explanations.push(`${op} → ${rounded}px`);
|
|
230
|
+
|
|
231
|
+
return createResult(
|
|
232
|
+
rounded,
|
|
233
|
+
'px',
|
|
234
|
+
`${rounded}px`,
|
|
235
|
+
{ value: rounded, unit: 'px' },
|
|
236
|
+
explanations
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Cannot resolve — emit calc()
|
|
241
|
+
const expr = `${valA.value}${valA.unit} ${getOpSymbol(op)} ${valB.value}${valB.unit}`;
|
|
242
|
+
explanations.push(`Cannot resolve ${valA.unit} ↔ ${valB.unit} — using calc()`);
|
|
243
|
+
|
|
244
|
+
return createResult(
|
|
245
|
+
0,
|
|
246
|
+
'calc',
|
|
247
|
+
`calc(${expr})`,
|
|
248
|
+
null,
|
|
249
|
+
explanations
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function getOpSymbol(op: MathOp): string {
|
|
254
|
+
switch (op) {
|
|
255
|
+
case 'add': return '+';
|
|
256
|
+
case 'subtract': return '-';
|
|
257
|
+
case 'multiply': return '*';
|
|
258
|
+
case 'divide': return '/';
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ============================================================================
|
|
263
|
+
// Public API
|
|
264
|
+
// ============================================================================
|
|
265
|
+
|
|
266
|
+
export const math = {
|
|
267
|
+
/**
|
|
268
|
+
* Add two CSS values with unit resolution.
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* math.add('10px', '2rem') // → '42px' (with default context)
|
|
272
|
+
* math.add('10px', '2rem', { rootFontSize: 16 }) // → '42px'
|
|
273
|
+
* math.add('10px', '2vw') // → 'calc(10px + 2vw)'
|
|
274
|
+
*/
|
|
275
|
+
add(a: string | number, b: string | number, context?: MathContext): MathResult {
|
|
276
|
+
return operate(a, 'add', b, context);
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Subtract two CSS values with unit resolution.
|
|
281
|
+
*/
|
|
282
|
+
subtract(a: string | number, b: string | number, context?: MathContext): MathResult {
|
|
283
|
+
return operate(a, 'subtract', b, context);
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Multiply two CSS values with unit resolution.
|
|
288
|
+
*/
|
|
289
|
+
multiply(a: string | number, b: string | number, context?: MathContext): MathResult {
|
|
290
|
+
return operate(a, 'multiply', b, context);
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Divide two CSS values with unit resolution.
|
|
295
|
+
*/
|
|
296
|
+
divide(a: string | number, b: string | number, context?: MathContext): MathResult {
|
|
297
|
+
return operate(a, 'divide', b, context);
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Sum multiple CSS values.
|
|
302
|
+
*/
|
|
303
|
+
sum(...values: (string | number)[]): MathResult {
|
|
304
|
+
if (values.length === 0) {
|
|
305
|
+
return createResult(0, 'px', '0px', { value: 0, unit: 'px' });
|
|
306
|
+
}
|
|
307
|
+
if (values.length === 1) {
|
|
308
|
+
const parsed = parseCSSValue(values[0]);
|
|
309
|
+
return createResult(parsed.value, parsed.unit, `${parsed.value}${parsed.unit}`, parsed);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
let result = this.add(values[0], values[1]);
|
|
313
|
+
for (let i = 2; i < values.length; i++) {
|
|
314
|
+
result = this.add(result.expression, values[i]);
|
|
315
|
+
}
|
|
316
|
+
return result;
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Resolve a CSS value to pixels.
|
|
321
|
+
*/
|
|
322
|
+
toPx(value: string | number, context?: MathContext): number {
|
|
323
|
+
const ctx: Required<MathContext> = { ...DEFAULT_CONTEXT, ...context };
|
|
324
|
+
const parsed = parseCSSValue(value);
|
|
325
|
+
return resolveToPx(parsed, ctx);
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Convert between CSS units.
|
|
330
|
+
*/
|
|
331
|
+
convert(
|
|
332
|
+
value: string | number,
|
|
333
|
+
toUnit: CSSUnit,
|
|
334
|
+
context?: MathContext
|
|
335
|
+
): MathResult {
|
|
336
|
+
const ctx: Required<MathContext> = { ...DEFAULT_CONTEXT, ...context };
|
|
337
|
+
const parsed = parseCSSValue(value);
|
|
338
|
+
const px = resolveToPx(parsed, ctx);
|
|
339
|
+
const converted = resolveFromPx(px, toUnit, ctx);
|
|
340
|
+
const rounded = Math.round(converted * 1000) / 1000;
|
|
341
|
+
|
|
342
|
+
return createResult(
|
|
343
|
+
rounded,
|
|
344
|
+
toUnit,
|
|
345
|
+
`${rounded}${toUnit}`,
|
|
346
|
+
{ value: rounded, unit: toUnit },
|
|
347
|
+
[`${parsed.value}${parsed.unit} → ${rounded}${toUnit}`]
|
|
348
|
+
);
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Create a fluid typography clamp() expression.
|
|
353
|
+
*
|
|
354
|
+
* @example
|
|
355
|
+
* math.fluidType({ minSize: 14, maxSize: 20 })
|
|
356
|
+
* // → 'clamp(14px, 0.625vw + 12px, 20px)'
|
|
357
|
+
* math.fluidType({ minSize: 14, maxSize: 20, unit: 'rem', rootFontSize: 16 })
|
|
358
|
+
* // → 'clamp(0.875rem, 0.625vw + 0.75rem, 1.25rem)'
|
|
359
|
+
*/
|
|
360
|
+
fluidType(config: FluidTypeConfig): MathResult {
|
|
361
|
+
const {
|
|
362
|
+
minSize,
|
|
363
|
+
maxSize,
|
|
364
|
+
minWidth = 320,
|
|
365
|
+
maxWidth = 1280,
|
|
366
|
+
unit = 'px',
|
|
367
|
+
rootFontSize = 16,
|
|
368
|
+
} = config;
|
|
369
|
+
|
|
370
|
+
const slope = (maxSize - minSize) / (maxWidth - minWidth);
|
|
371
|
+
const intercept = minSize - slope * minWidth;
|
|
372
|
+
const slopeVw = Math.round(slope * 100 * 10000) / 10000;
|
|
373
|
+
const interceptRounded = Math.round(intercept * 100) / 100;
|
|
374
|
+
|
|
375
|
+
const minStr = unit === 'rem' ? `${minSize / rootFontSize}rem` : `${minSize}${unit}`;
|
|
376
|
+
const maxStr = unit === 'rem' ? `${maxSize / rootFontSize}rem` : `${maxSize}${unit}`;
|
|
377
|
+
const prefStr = `${slopeVw}vw + ${unit === 'rem' ? interceptRounded / rootFontSize + 'rem' : interceptRounded + unit}`;
|
|
378
|
+
|
|
379
|
+
const expression = `clamp(${minStr}, ${prefStr}, ${maxStr})`;
|
|
380
|
+
|
|
381
|
+
return createResult(
|
|
382
|
+
0,
|
|
383
|
+
'calc',
|
|
384
|
+
expression,
|
|
385
|
+
null,
|
|
386
|
+
[`Fluid type: ${minSize}${unit} → ${maxSize}${unit} between ${minWidth}px and ${maxWidth}px`]
|
|
387
|
+
);
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Scale a value by a factor with unit preservation.
|
|
392
|
+
*/
|
|
393
|
+
scale(value: string | number, factor: number): MathResult {
|
|
394
|
+
const parsed = parseCSSValue(value);
|
|
395
|
+
const scaled = Math.round(parsed.value * factor * 100) / 100;
|
|
396
|
+
|
|
397
|
+
return createResult(
|
|
398
|
+
scaled,
|
|
399
|
+
parsed.unit,
|
|
400
|
+
`${scaled}${parsed.unit}`,
|
|
401
|
+
{ value: scaled, unit: parsed.unit },
|
|
402
|
+
[`Scaled ${parsed.value}${parsed.unit} × ${factor} = ${scaled}${parsed.unit}`]
|
|
403
|
+
);
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Clamp a CSS value between min and max.
|
|
408
|
+
*/
|
|
409
|
+
clampValue(
|
|
410
|
+
value: string | number,
|
|
411
|
+
min: string | number,
|
|
412
|
+
max: string | number,
|
|
413
|
+
context?: MathContext
|
|
414
|
+
): MathResult {
|
|
415
|
+
const parsed = parseCSSValue(value);
|
|
416
|
+
const parsedMin = parseCSSValue(min);
|
|
417
|
+
const parsedMax = parseCSSValue(max);
|
|
418
|
+
|
|
419
|
+
// If all same unit, resolve directly
|
|
420
|
+
if (parsed.unit === parsedMin.unit && parsed.unit === parsedMax.unit) {
|
|
421
|
+
const clamped = Math.max(parsedMin.value, Math.min(parsedMax.value, parsed.value));
|
|
422
|
+
return createResult(
|
|
423
|
+
clamped,
|
|
424
|
+
parsed.unit,
|
|
425
|
+
`${clamped}${parsed.unit}`,
|
|
426
|
+
{ value: clamped, unit: parsed.unit },
|
|
427
|
+
[`Clamped ${parsed.value} between ${parsedMin.value} and ${parsedMax.value}`]
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Otherwise emit clamp()
|
|
432
|
+
const valStr = `${parsed.value}${parsed.unit}`;
|
|
433
|
+
const minStr = `${parsedMin.value}${parsedMin.unit}`;
|
|
434
|
+
const maxStr = `${parsedMax.value}${parsedMax.unit}`;
|
|
435
|
+
|
|
436
|
+
return createResult(
|
|
437
|
+
0,
|
|
438
|
+
'calc',
|
|
439
|
+
`clamp(${minStr}, ${valStr}, ${maxStr})`,
|
|
440
|
+
null,
|
|
441
|
+
['Mixed units — using clamp()']
|
|
442
|
+
);
|
|
443
|
+
},
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Parse a CSS value into its numeric and unit parts.
|
|
447
|
+
*/
|
|
448
|
+
parse(value: string | number): CSSMathValue {
|
|
449
|
+
return parseCSSValue(value);
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Check if two values have compatible units for direct operations.
|
|
454
|
+
*/
|
|
455
|
+
compatible(a: string | number, b: string | number): boolean {
|
|
456
|
+
const valA = parseCSSValue(a);
|
|
457
|
+
const valB = parseCSSValue(b);
|
|
458
|
+
return valA.unit === valB.unit || getUnitCategory(valA.unit) === getUnitCategory(valB.unit);
|
|
459
|
+
},
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Get the category of a CSS unit.
|
|
463
|
+
*/
|
|
464
|
+
unitCategory(unit: CSSUnit): string {
|
|
465
|
+
return getUnitCategory(unit);
|
|
466
|
+
},
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Create a CSS min() expression.
|
|
470
|
+
*/
|
|
471
|
+
cssMin(...values: (string | number)[]): string {
|
|
472
|
+
const formatted = values.map(v => {
|
|
473
|
+
const parsed = parseCSSValue(v);
|
|
474
|
+
return `${parsed.value}${parsed.unit}`;
|
|
475
|
+
});
|
|
476
|
+
return `min(${formatted.join(', ')})`;
|
|
477
|
+
},
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Create a CSS max() expression.
|
|
481
|
+
*/
|
|
482
|
+
cssMax(...values: (string | number)[]): string {
|
|
483
|
+
const formatted = values.map(v => {
|
|
484
|
+
const parsed = parseCSSValue(v);
|
|
485
|
+
return `${parsed.value}${parsed.unit}`;
|
|
486
|
+
});
|
|
487
|
+
return `max(${formatted.join(', ')})`;
|
|
488
|
+
},
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Format a number with specified precision.
|
|
492
|
+
*/
|
|
493
|
+
precision(value: number, decimals: number = 2): string {
|
|
494
|
+
return value.toFixed(decimals);
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
// ============================================================================
|
|
499
|
+
// Convenience exports (match helpers.ts pattern)
|
|
500
|
+
// ============================================================================
|
|
501
|
+
|
|
502
|
+
export const add = math.add.bind(math);
|
|
503
|
+
export const subtract = math.subtract.bind(math);
|
|
504
|
+
export const multiply = math.multiply.bind(math);
|
|
505
|
+
export const divide = math.divide.bind(math);
|
|
506
|
+
export const fluidType = math.fluidType.bind(math);
|
|
507
|
+
export const convert = math.convert.bind(math);
|
|
508
|
+
export const toPx = math.toPx.bind(math);
|
|
509
|
+
export const scale = math.scale.bind(math);
|
|
510
|
+
|
|
511
|
+
export default math;
|