@colorye/react-native-css 0.2.1

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.
@@ -0,0 +1,524 @@
1
+ /**
2
+ * Build-time CSS transformations
3
+ * Pre-computes static values during Metro transform to reduce runtime work.
4
+ */
5
+
6
+ // ============================================================================
7
+ // Regex patterns (compiled once)
8
+ // ============================================================================
9
+ const remOrEmUnitRe = /([\d.]+)(rem|em)/g;
10
+ const pxUnitRe = /^([\d.]+)px$/;
11
+ const viewportUnitRe = /([\d.]+)(vh|vw|vmin|vmax)/;
12
+ const cssVarRe = /var\(/;
13
+ const calcRe = /calc\(/;
14
+ const colorRe1 = /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)/g;
15
+ const colorRe2 = /rgba?\(\s*(\d+)\s+(\d+)\s+(\d+)\s*\/\s*([\d.]+)\s*\)/g;
16
+ const borderRe = /^(\S+)\s+(solid|dashed|dotted)\s+(\S+)$/;
17
+ const borderSimpleRe = /^(\S+)\s+(solid|dashed|dotted)$/;
18
+ const spacingRe = /^\s*(\S+)(?:\s+(\S+)(?:\s+(\S+)(?:\s+(\S+))?)?)?\s*$/;
19
+ const fontWeightRe = /^(normal|bold|[1-9]00)$/;
20
+ const transformFnRe =
21
+ /(perspective|rotate[XYZ]?|scale[XY]?|translate[XY]?|skew[XY]?)\s*\(\s*([^,)]+)(?:,\s*([^)]+))?\)/g;
22
+
23
+ const UNSUPPORTED_PROPERTIES = ["outline"];
24
+
25
+ // ============================================================================
26
+ // Helpers
27
+ // ============================================================================
28
+ const itohex = (component) => {
29
+ const hex = Number(component).toString(16);
30
+ return hex.length === 1 ? `0${hex}` : hex;
31
+ };
32
+
33
+ const toNumber = (value) => {
34
+ if (typeof value === "number") return value;
35
+ if (typeof value !== "string") return value;
36
+ const trimmed = value.trim();
37
+ if (trimmed === "") return value;
38
+ const num = Number(trimmed);
39
+ return !isNaN(num) ? num : value;
40
+ };
41
+
42
+ // ============================================================================
43
+ // Check if value needs runtime processing
44
+ // ============================================================================
45
+ export function isDynamicValue(value) {
46
+ if (typeof value !== "string") return false;
47
+ return viewportUnitRe.test(value) || cssVarRe.test(value);
48
+ }
49
+
50
+ export function hasMediaQuery(declaration) {
51
+ if (!declaration || typeof declaration !== "object") return false;
52
+ return Object.keys(declaration).some((key) => key.startsWith("@media"));
53
+ }
54
+
55
+ // ============================================================================
56
+ // Static transformations (can be done at build time)
57
+ // ============================================================================
58
+
59
+ /** Remove !important */
60
+ export function removeImportant(value) {
61
+ if (typeof value !== "string") return value;
62
+ return value.replace(/\s*!important\s*/g, "").trim();
63
+ }
64
+
65
+ /** Convert rem/em to px (1rem = 16px) */
66
+ export function transformRemEm(value) {
67
+ if (typeof value !== "string") return value;
68
+ return value.replace(remOrEmUnitRe, (_, num) => `${parseFloat(num) * 16}px`);
69
+ }
70
+
71
+ /** Remove px unit and convert to number */
72
+ export function removePxUnit(value) {
73
+ if (typeof value !== "string") return value;
74
+ const match = pxUnitRe.exec(value);
75
+ if (match) return parseFloat(match[1]);
76
+ return value;
77
+ }
78
+
79
+ /** Transform rgba/rgb to hex */
80
+ export function transformColor(value) {
81
+ if (typeof value !== "string") return value;
82
+
83
+ // Reset lastIndex for global regexes
84
+ colorRe1.lastIndex = 0;
85
+ colorRe2.lastIndex = 0;
86
+
87
+ value = value.replace(colorRe1, (_, r, g, b, a) => {
88
+ const alpha = parseFloat(a);
89
+ if (isNaN(alpha) || alpha >= 1) return `#${itohex(r)}${itohex(g)}${itohex(b)}`;
90
+ return `#${itohex(r)}${itohex(g)}${itohex(b)}${itohex(Math.round(alpha * 255))}`;
91
+ });
92
+
93
+ value = value.replace(colorRe2, (_, r, g, b, a) => {
94
+ const alpha = parseFloat(a);
95
+ if (isNaN(alpha) || alpha >= 1) return `#${itohex(r)}${itohex(g)}${itohex(b)}`;
96
+ return `#${itohex(r)}${itohex(g)}${itohex(b)}${itohex(Math.round(alpha * 255))}`;
97
+ });
98
+
99
+ return value;
100
+ }
101
+
102
+ /** Transform position: fixed → absolute */
103
+ export function transformPosition(property, value) {
104
+ if (property === "position" && value === "fixed") return "absolute";
105
+ return value;
106
+ }
107
+
108
+ /** Transform border-radius with % → 9999 */
109
+ export function transformBorderRadius(property, value) {
110
+ if (property.toLowerCase().endsWith("radius") && typeof value === "string" && value.includes("%")) {
111
+ return 9999;
112
+ }
113
+ return value;
114
+ }
115
+
116
+ /** Transform opacity with % → decimal */
117
+ export function transformOpacity(property, value) {
118
+ if (property === "opacity" && typeof value === "string" && value.endsWith("%")) {
119
+ const num = parseFloat(value);
120
+ if (!isNaN(num)) {
121
+ return num / 100;
122
+ }
123
+ }
124
+ return value;
125
+ }
126
+
127
+ /** Get aliased property name */
128
+ export function getAliasedProperty(property) {
129
+ if (property === "background") return "backgroundColor";
130
+ return property;
131
+ }
132
+
133
+ /** Check if property is supported */
134
+ export function isPropertySupported(property, value) {
135
+ if (UNSUPPORTED_PROPERTIES.includes(property)) return false;
136
+ if (value === undefined || value === null) return false;
137
+ if (typeof value === "object") return false; // Skip nested objects (media queries)
138
+ return true;
139
+ }
140
+
141
+ // ============================================================================
142
+ // Shorthand expansion
143
+ // ============================================================================
144
+
145
+ /** Expand border shorthand */
146
+ export function expandBorder(property, value) {
147
+ if (!["border", "borderTop", "borderBottom", "borderLeft", "borderRight"].includes(property)) {
148
+ return null;
149
+ }
150
+
151
+ if (value === "none" || value === "0") {
152
+ return { [`${property}Width`]: 0 };
153
+ }
154
+
155
+ // Try "width style color" format
156
+ let match = borderRe.exec(String(value));
157
+ if (match) {
158
+ const [, width, style, color] = match;
159
+ return {
160
+ [`${property}Width`]: toNumber(width),
161
+ [`${property}Style`]: style,
162
+ [`${property}Color`]: color,
163
+ };
164
+ }
165
+
166
+ // Try "width style" format
167
+ match = borderSimpleRe.exec(String(value));
168
+ if (match) {
169
+ const [, width, style] = match;
170
+ return {
171
+ [`${property}Width`]: toNumber(width),
172
+ [`${property}Style`]: style,
173
+ };
174
+ }
175
+
176
+ // Single value (width only)
177
+ return { [`${property}Width`]: toNumber(value) };
178
+ }
179
+
180
+ function splitSpacingValues(str) {
181
+ const parts = [];
182
+ let current = "";
183
+ let depth = 0;
184
+ for (let i = 0; i < str.length; i++) {
185
+ const char = str[i];
186
+ if (char === "(") {
187
+ depth++;
188
+ current += char;
189
+ } else if (char === ")") {
190
+ depth--;
191
+ current += char;
192
+ } else if (char === " " && depth === 0) {
193
+ if (current) {
194
+ parts.push(current);
195
+ current = "";
196
+ }
197
+ } else {
198
+ current += char;
199
+ }
200
+ }
201
+ if (current) {
202
+ parts.push(current);
203
+ }
204
+ return parts.filter(Boolean);
205
+ }
206
+
207
+ /** Expand margin/padding shorthand */
208
+ export function expandSpacing(property, value) {
209
+ if (!["padding", "margin"].includes(property)) return null;
210
+
211
+ const parts = splitSpacingValues(String(value));
212
+ if (parts.length === 0) return null;
213
+
214
+ const top = parts[0];
215
+ const right = parts[1] !== undefined ? parts[1] : top;
216
+ const bottom = parts[2] !== undefined ? parts[2] : top;
217
+ const left = parts[3] !== undefined ? parts[3] : right;
218
+
219
+ return {
220
+ [`${property}Top`]: toNumber(top),
221
+ [`${property}Right`]: toNumber(right),
222
+ [`${property}Bottom`]: toNumber(bottom),
223
+ [`${property}Left`]: toNumber(left),
224
+ };
225
+ }
226
+
227
+ /** Expand fontWeight */
228
+ export function expandFontWeight(property, value) {
229
+ if (property !== "fontWeight") return null;
230
+ if (!fontWeightRe.test(String(value))) return null;
231
+ return { fontWeight: String(value) };
232
+ }
233
+
234
+ /** Expand flex */
235
+ export function expandFlex(property, value) {
236
+ if (property !== "flex") return null;
237
+ return { flex: parseInt(value, 10) };
238
+ }
239
+
240
+ /** Expand transform property */
241
+ export function expandTransform(property, value) {
242
+ if (property !== "transform") return null;
243
+ if (typeof value !== "string") return null;
244
+
245
+ // If contains dynamic values, skip
246
+ if (isDynamicValue(value)) return null;
247
+
248
+ const transforms = [];
249
+ let match;
250
+ const re = new RegExp(transformFnRe.source, "g");
251
+
252
+ while ((match = re.exec(value)) !== null) {
253
+ const [, fn, val1, val2] = match;
254
+
255
+ if (fn === "translate" || fn === "skew") {
256
+ transforms.push({ [`${fn}X`]: toNumber(val1) });
257
+ if (val2 !== undefined) {
258
+ transforms.push({ [`${fn}Y`]: toNumber(val2) });
259
+ }
260
+ } else {
261
+ transforms.push({ [fn]: toNumber(val1) });
262
+ }
263
+ }
264
+
265
+ return transforms.length > 0 ? { transform: transforms } : null;
266
+ }
267
+
268
+ /** Expand logical properties like paddingInline, marginInline, paddingBlock, marginBlock */
269
+ export function expandLogicalProperty(property, value) {
270
+ if (
271
+ ![
272
+ "paddingInline",
273
+ "marginInline",
274
+ "paddingBlock",
275
+ "marginBlock",
276
+ "paddingInlineStart",
277
+ "paddingInlineEnd",
278
+ "marginInlineStart",
279
+ "marginInlineEnd",
280
+ "insetInline",
281
+ "insetInlineStart",
282
+ "insetInlineEnd",
283
+ "insetBlock",
284
+ "insetBlockStart",
285
+ "insetBlockEnd",
286
+ ].includes(property)
287
+ ) {
288
+ return null;
289
+ }
290
+
291
+ const strValue = String(value).trim();
292
+ const parts = [];
293
+ let current = "";
294
+ let depth = 0;
295
+ for (let i = 0; i < strValue.length; i++) {
296
+ const char = strValue[i];
297
+ if (char === "(") {
298
+ depth++;
299
+ current += char;
300
+ } else if (char === ")") {
301
+ depth--;
302
+ current += char;
303
+ } else if (char === " " && depth === 0) {
304
+ if (current) {
305
+ parts.push(current);
306
+ current = "";
307
+ }
308
+ } else {
309
+ current += char;
310
+ }
311
+ }
312
+ if (current) {
313
+ parts.push(current);
314
+ }
315
+
316
+ const cleanedParts = parts.filter(Boolean);
317
+
318
+ if (property === "paddingInlineStart") return { paddingStart: toNumber(value) };
319
+ if (property === "paddingInlineEnd") return { paddingEnd: toNumber(value) };
320
+ if (property === "marginInlineStart") return { marginStart: toNumber(value) };
321
+ if (property === "marginInlineEnd") return { marginEnd: toNumber(value) };
322
+ if (property === "insetInlineStart") return { start: toNumber(value) };
323
+ if (property === "insetInlineEnd") return { end: toNumber(value) };
324
+ if (property === "insetBlockStart") return { top: toNumber(value) };
325
+ if (property === "insetBlockEnd") return { bottom: toNumber(value) };
326
+
327
+ if (property === "paddingInline") {
328
+ if (cleanedParts.length === 1) {
329
+ return { paddingHorizontal: toNumber(cleanedParts[0]) };
330
+ }
331
+ if (cleanedParts.length >= 2) {
332
+ return {
333
+ paddingStart: toNumber(cleanedParts[0]),
334
+ paddingEnd: toNumber(cleanedParts[1]),
335
+ };
336
+ }
337
+ }
338
+
339
+ if (property === "marginInline") {
340
+ if (cleanedParts.length === 1) {
341
+ return { marginHorizontal: toNumber(cleanedParts[0]) };
342
+ }
343
+ if (cleanedParts.length >= 2) {
344
+ return {
345
+ marginStart: toNumber(cleanedParts[0]),
346
+ marginEnd: toNumber(cleanedParts[1]),
347
+ };
348
+ }
349
+ }
350
+
351
+ if (property === "paddingBlock") {
352
+ if (cleanedParts.length === 1) {
353
+ return { paddingVertical: toNumber(cleanedParts[0]) };
354
+ }
355
+ if (cleanedParts.length >= 2) {
356
+ return {
357
+ paddingTop: toNumber(cleanedParts[0]),
358
+ paddingBottom: toNumber(cleanedParts[1]),
359
+ };
360
+ }
361
+ }
362
+
363
+ if (property === "marginBlock") {
364
+ if (cleanedParts.length === 1) {
365
+ return { marginVertical: toNumber(cleanedParts[0]) };
366
+ }
367
+ if (cleanedParts.length >= 2) {
368
+ return {
369
+ marginTop: toNumber(cleanedParts[0]),
370
+ marginBottom: toNumber(cleanedParts[1]),
371
+ };
372
+ }
373
+ }
374
+
375
+ if (property === "insetInline") {
376
+ if (cleanedParts.length === 1) {
377
+ return {
378
+ left: toNumber(cleanedParts[0]),
379
+ right: toNumber(cleanedParts[0]),
380
+ };
381
+ }
382
+ if (cleanedParts.length >= 2) {
383
+ return {
384
+ start: toNumber(cleanedParts[0]),
385
+ end: toNumber(cleanedParts[1]),
386
+ };
387
+ }
388
+ }
389
+
390
+ if (property === "insetBlock") {
391
+ if (cleanedParts.length === 1) {
392
+ return {
393
+ top: toNumber(cleanedParts[0]),
394
+ bottom: toNumber(cleanedParts[0]),
395
+ };
396
+ }
397
+ if (cleanedParts.length >= 2) {
398
+ return {
399
+ top: toNumber(cleanedParts[0]),
400
+ bottom: toNumber(cleanedParts[1]),
401
+ };
402
+ }
403
+ }
404
+
405
+ return null;
406
+ }
407
+
408
+ // ============================================================================
409
+ // Main pre-computation function
410
+ // ============================================================================
411
+
412
+ /**
413
+ * Pre-compute a single property value
414
+ * Returns { property, value, isDynamic }
415
+ */
416
+ export function precomputeValue(property, value) {
417
+ // Handle non-string values
418
+ if (typeof value !== "string") {
419
+ return { property, value, isDynamic: false };
420
+ }
421
+
422
+ // Check if dynamic (needs runtime)
423
+ const isDynamic = isDynamicValue(value) || calcRe.test(value);
424
+
425
+ // Apply static transformations (even for dynamic values, some parts can be pre-processed)
426
+ let processed = value;
427
+ processed = removeImportant(processed);
428
+ processed = transformRemEm(processed);
429
+ processed = transformColor(processed);
430
+ processed = transformPosition(property, processed);
431
+ processed = transformBorderRadius(property, processed);
432
+ processed = transformOpacity(property, processed);
433
+
434
+ // Get aliased property
435
+ const aliasedProp = getAliasedProperty(property);
436
+
437
+ // For static values, remove px and convert to number
438
+ if (!isDynamic) {
439
+ processed = removePxUnit(processed);
440
+ if (typeof processed === "string") {
441
+ processed = toNumber(processed);
442
+ }
443
+ }
444
+
445
+ return { property: aliasedProp, value: processed, isDynamic };
446
+ }
447
+
448
+ /**
449
+ * Pre-compute an entire declaration object
450
+ * Returns { _static: {...}, _dynamic: {...}, _hasDynamic: boolean }
451
+ */
452
+ export function precomputeDeclaration(declaration) {
453
+ if (!declaration || typeof declaration !== "object") {
454
+ return { _static: {}, _dynamic: {}, _hasDynamic: false };
455
+ }
456
+
457
+ const staticProps = {};
458
+ const dynamicProps = {};
459
+ let hasDynamic = false;
460
+
461
+ for (const [property, value] of Object.entries(declaration)) {
462
+ // Handle media queries - always dynamic (needs runtime evaluation)
463
+ if (property.startsWith("@media")) {
464
+ // Recursively pre-compute media query content
465
+ const mediaResult = precomputeDeclaration(value);
466
+ dynamicProps[property] = {
467
+ _static: mediaResult._static,
468
+ _dynamic: mediaResult._dynamic,
469
+ };
470
+ hasDynamic = true;
471
+ continue;
472
+ }
473
+
474
+ // Keep CSS variables as-is
475
+ if (property.startsWith("--")) {
476
+ staticProps[property] = value;
477
+ continue;
478
+ }
479
+
480
+ // Skip unsupported
481
+ if (!isPropertySupported(property, value)) continue;
482
+
483
+ // Pre-compute the value
484
+ const { property: prop, value: val, isDynamic } = precomputeValue(property, value);
485
+
486
+ // Try to expand shorthand
487
+ const expanded =
488
+ expandBorder(prop, val) ||
489
+ expandSpacing(prop, val) ||
490
+ expandFontWeight(prop, val) ||
491
+ expandFlex(prop, val) ||
492
+ expandTransform(prop, val) ||
493
+ expandLogicalProperty(prop, val);
494
+
495
+ if (expanded) {
496
+ // Add expanded properties
497
+ for (const [expProp, expVal] of Object.entries(expanded)) {
498
+ if (isDynamic) {
499
+ dynamicProps[expProp] = expVal;
500
+ hasDynamic = true;
501
+ } else {
502
+ staticProps[expProp] = expVal;
503
+ }
504
+ }
505
+ } else {
506
+ // Single property
507
+ if (isDynamic) {
508
+ dynamicProps[prop] = val;
509
+ hasDynamic = true;
510
+ } else {
511
+ staticProps[prop] = val;
512
+ }
513
+ }
514
+ }
515
+
516
+ return { _static: staticProps, _dynamic: dynamicProps, _hasDynamic: hasDynamic };
517
+ }
518
+
519
+ export default {
520
+ isDynamicValue,
521
+ hasMediaQuery,
522
+ precomputeValue,
523
+ precomputeDeclaration,
524
+ };