@mgcrea/react-native-tailwind 0.9.0 → 0.10.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/README.md +356 -30
- package/dist/babel/config-loader.test.ts +152 -0
- package/dist/babel/index.cjs +575 -60
- package/dist/babel/plugin.d.ts +23 -1
- package/dist/babel/plugin.test.ts +417 -0
- package/dist/babel/plugin.ts +265 -32
- package/dist/babel/utils/colorSchemeModifierProcessing.d.ts +34 -0
- package/dist/babel/utils/colorSchemeModifierProcessing.ts +89 -0
- package/dist/babel/utils/dynamicProcessing.d.ts +33 -2
- package/dist/babel/utils/dynamicProcessing.ts +352 -33
- package/dist/babel/utils/styleInjection.d.ts +14 -1
- package/dist/babel/utils/styleInjection.ts +125 -7
- package/dist/babel/utils/styleTransforms.test.ts +56 -0
- package/dist/babel/utils/twProcessing.d.ts +2 -0
- package/dist/babel/utils/twProcessing.ts +22 -1
- package/dist/parser/aspectRatio.js +1 -1
- package/dist/parser/aspectRatio.test.js +1 -1
- package/dist/parser/index.d.ts +2 -2
- package/dist/parser/index.js +1 -1
- package/dist/parser/modifiers.d.ts +48 -2
- package/dist/parser/modifiers.js +1 -1
- package/dist/parser/modifiers.test.js +1 -1
- package/dist/parser/spacing.d.ts +1 -1
- package/dist/parser/spacing.js +1 -1
- package/dist/parser/spacing.test.js +1 -1
- package/dist/runtime.cjs +1 -1
- package/dist/runtime.cjs.map +3 -3
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +3 -3
- package/dist/runtime.test.js +1 -1
- package/dist/types/config.d.ts +7 -0
- package/dist/types/config.js +0 -0
- package/package.json +4 -4
- package/src/babel/config-loader.test.ts +152 -0
- package/src/babel/plugin.test.ts +417 -0
- package/src/babel/plugin.ts +265 -32
- package/src/babel/utils/colorSchemeModifierProcessing.ts +89 -0
- package/src/babel/utils/dynamicProcessing.ts +352 -33
- package/src/babel/utils/styleInjection.ts +125 -7
- package/src/babel/utils/styleTransforms.test.ts +56 -0
- package/src/babel/utils/twProcessing.ts +22 -1
- package/src/parser/aspectRatio.test.ts +25 -2
- package/src/parser/aspectRatio.ts +3 -3
- package/src/parser/index.ts +12 -1
- package/src/parser/modifiers.test.ts +151 -1
- package/src/parser/modifiers.ts +139 -4
- package/src/parser/spacing.test.ts +63 -0
- package/src/parser/spacing.ts +10 -6
- package/src/runtime.test.ts +27 -0
- package/src/runtime.ts +2 -1
- package/src/types/config.ts +7 -0
- package/dist/babel/index.test.ts +0 -481
- package/dist/config/palettes.d.ts +0 -302
- package/dist/config/palettes.js +0 -1
- package/dist/parser/__snapshots__/aspectRatio.test.js.snap +0 -9
- package/dist/parser/__snapshots__/borders.test.js.snap +0 -23
- package/dist/parser/__snapshots__/colors.test.js.snap +0 -251
- package/dist/parser/__snapshots__/shadows.test.js.snap +0 -76
- package/dist/parser/__snapshots__/sizing.test.js.snap +0 -61
- package/dist/parser/__snapshots__/spacing.test.js.snap +0 -40
- package/dist/parser/__snapshots__/transforms.test.js.snap +0 -58
- package/dist/parser/__snapshots__/typography.test.js.snap +0 -30
- package/dist/parser/aspectRatio.test.d.ts +0 -1
- package/dist/parser/borders.test.d.ts +0 -1
- package/dist/parser/colors.test.d.ts +0 -1
- package/dist/parser/layout.test.d.ts +0 -1
- package/dist/parser/modifiers.test.d.ts +0 -1
- package/dist/parser/shadows.test.d.ts +0 -1
- package/dist/parser/sizing.test.d.ts +0 -1
- package/dist/parser/spacing.test.d.ts +0 -1
- package/dist/parser/typography.test.d.ts +0 -1
- package/dist/types.d.ts +0 -42
- package/dist/types.js +0 -1
|
@@ -347,3 +347,59 @@ describe("Style merging - edge cases", () => {
|
|
|
347
347
|
expect(output).not.toContain("className");
|
|
348
348
|
});
|
|
349
349
|
});
|
|
350
|
+
|
|
351
|
+
describe("Style function merging - mergeStyleFunctionAttribute", () => {
|
|
352
|
+
it("should merge modifier className with existing function style prop", () => {
|
|
353
|
+
const input = `
|
|
354
|
+
import { Pressable } from 'react-native';
|
|
355
|
+
export function Component() {
|
|
356
|
+
return (
|
|
357
|
+
<Pressable
|
|
358
|
+
className="bg-blue-500 active:bg-blue-700"
|
|
359
|
+
style={({ pressed }) => pressed && { opacity: 0.8 }}
|
|
360
|
+
/>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
`;
|
|
364
|
+
|
|
365
|
+
const output = transform(input);
|
|
366
|
+
|
|
367
|
+
// Should create wrapper function that merges both style functions
|
|
368
|
+
expect(output).toContain("_state");
|
|
369
|
+
expect(output).toMatch(/_state\s*=>/);
|
|
370
|
+
|
|
371
|
+
// Should call both the new style function and existing style function
|
|
372
|
+
expect(output).toContain("_bg_blue_500");
|
|
373
|
+
expect(output).toContain("_active_bg_blue_700");
|
|
374
|
+
|
|
375
|
+
// Should not have className in output
|
|
376
|
+
expect(output).not.toContain("className");
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("should merge modifier className with static existing style when using Pressable", () => {
|
|
380
|
+
const input = `
|
|
381
|
+
import { Pressable } from 'react-native';
|
|
382
|
+
export function Component() {
|
|
383
|
+
return (
|
|
384
|
+
<Pressable
|
|
385
|
+
className="p-4 active:bg-gray-100"
|
|
386
|
+
style={{ borderRadius: 8 }}
|
|
387
|
+
/>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
`;
|
|
391
|
+
|
|
392
|
+
const output = transform(input);
|
|
393
|
+
|
|
394
|
+
// Should create function that wraps className styles
|
|
395
|
+
expect(output).toContain("_state");
|
|
396
|
+
|
|
397
|
+
// Should include both the className styles and the static style
|
|
398
|
+
expect(output).toContain("_p_4");
|
|
399
|
+
expect(output).toContain("_active_bg_gray_100");
|
|
400
|
+
expect(output).toContain("borderRadius");
|
|
401
|
+
|
|
402
|
+
// Should not have className in output
|
|
403
|
+
expect(output).not.toContain("className");
|
|
404
|
+
});
|
|
405
|
+
});
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import type { NodePath } from "@babel/core";
|
|
6
6
|
import type * as BabelTypes from "@babel/types";
|
|
7
7
|
import type { ModifierType, ParsedModifier } from "../../parser/index.js";
|
|
8
|
+
import { expandSchemeModifier, isSchemeModifier } from "../../parser/index.js";
|
|
9
|
+
import type { SchemeModifierConfig } from "../../types/config.js";
|
|
8
10
|
import type { StyleObject } from "../../types/core.js";
|
|
9
11
|
|
|
10
12
|
/**
|
|
@@ -14,6 +16,7 @@ import type { StyleObject } from "../../types/core.js";
|
|
|
14
16
|
export interface TwProcessingState {
|
|
15
17
|
styleRegistry: Map<string, StyleObject>;
|
|
16
18
|
customColors: Record<string, string>;
|
|
19
|
+
schemeModifierConfig: SchemeModifierConfig;
|
|
17
20
|
stylesIdentifier: string;
|
|
18
21
|
}
|
|
19
22
|
|
|
@@ -30,7 +33,25 @@ export function processTwCall(
|
|
|
30
33
|
splitModifierClasses: (className: string) => { baseClasses: string[]; modifierClasses: ParsedModifier[] },
|
|
31
34
|
t: typeof BabelTypes,
|
|
32
35
|
): void {
|
|
33
|
-
const { baseClasses, modifierClasses } = splitModifierClasses(className);
|
|
36
|
+
const { baseClasses, modifierClasses: rawModifierClasses } = splitModifierClasses(className);
|
|
37
|
+
|
|
38
|
+
// Expand scheme: modifiers into dark: and light: modifiers
|
|
39
|
+
const modifierClasses: ParsedModifier[] = [];
|
|
40
|
+
for (const modifier of rawModifierClasses) {
|
|
41
|
+
if (isSchemeModifier(modifier.modifier)) {
|
|
42
|
+
// Expand scheme: into dark: and light:
|
|
43
|
+
const expanded = expandSchemeModifier(
|
|
44
|
+
modifier,
|
|
45
|
+
state.customColors,
|
|
46
|
+
state.schemeModifierConfig.darkSuffix ?? "-dark",
|
|
47
|
+
state.schemeModifierConfig.lightSuffix ?? "-light",
|
|
48
|
+
);
|
|
49
|
+
modifierClasses.push(...expanded);
|
|
50
|
+
} else {
|
|
51
|
+
// Keep other modifiers as-is
|
|
52
|
+
modifierClasses.push(modifier);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
34
55
|
|
|
35
56
|
// Build TwStyle object properties
|
|
36
57
|
const objectProperties: BabelTypes.ObjectProperty[] = [];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { ASPECT_RATIO_PRESETS, parseAspectRatio } from "./aspectRatio";
|
|
3
|
+
import { parseClassName } from "./index";
|
|
3
4
|
|
|
4
5
|
describe("ASPECT_RATIO_PRESETS", () => {
|
|
5
6
|
it("should export aspect ratio presets", () => {
|
|
@@ -29,8 +30,8 @@ describe("parseAspectRatio - preset values", () => {
|
|
|
29
30
|
});
|
|
30
31
|
|
|
31
32
|
it("should parse aspect-auto", () => {
|
|
32
|
-
// aspect-auto removes the aspect ratio constraint
|
|
33
|
-
expect(parseAspectRatio("aspect-auto")).toEqual({});
|
|
33
|
+
// aspect-auto removes the aspect ratio constraint by explicitly setting to undefined
|
|
34
|
+
expect(parseAspectRatio("aspect-auto")).toEqual({ aspectRatio: undefined });
|
|
34
35
|
});
|
|
35
36
|
});
|
|
36
37
|
|
|
@@ -131,6 +132,28 @@ describe("parseAspectRatio - type validation", () => {
|
|
|
131
132
|
});
|
|
132
133
|
});
|
|
133
134
|
|
|
135
|
+
describe("parseAspectRatio - override behavior", () => {
|
|
136
|
+
it("should allow aspect-auto to override previously set aspect ratios", () => {
|
|
137
|
+
// This tests the fix for issue #3 - aspect-auto must explicitly set aspectRatio: undefined
|
|
138
|
+
// to override previous aspect ratio values when using Object.assign
|
|
139
|
+
const result = parseClassName("aspect-square aspect-auto");
|
|
140
|
+
expect(result).toEqual({ aspectRatio: undefined });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should allow aspect ratios to override aspect-auto", () => {
|
|
144
|
+
const result = parseClassName("aspect-auto aspect-square");
|
|
145
|
+
expect(result).toEqual({ aspectRatio: 1 });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("should apply last aspect ratio in sequence", () => {
|
|
149
|
+
const result = parseClassName("aspect-square aspect-video aspect-auto");
|
|
150
|
+
expect(result).toEqual({ aspectRatio: undefined });
|
|
151
|
+
|
|
152
|
+
const result2 = parseClassName("aspect-auto aspect-square aspect-video");
|
|
153
|
+
expect(result2).toEqual({ aspectRatio: 16 / 9 });
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
134
157
|
describe("parseAspectRatio - comprehensive coverage", () => {
|
|
135
158
|
it("should parse all preset variants without errors", () => {
|
|
136
159
|
const presets = ["aspect-auto", "aspect-square", "aspect-video"];
|
|
@@ -52,10 +52,10 @@ export function parseAspectRatio(cls: string): StyleObject | null {
|
|
|
52
52
|
// Check for preset values
|
|
53
53
|
if (cls in ASPECT_RATIO_PRESETS) {
|
|
54
54
|
const aspectRatio = ASPECT_RATIO_PRESETS[cls];
|
|
55
|
-
// aspect-auto removes the aspect ratio constraint by
|
|
56
|
-
//
|
|
55
|
+
// aspect-auto removes the aspect ratio constraint by explicitly setting to undefined
|
|
56
|
+
// This ensures it overrides any previously set aspectRatio in Object.assign
|
|
57
57
|
if (aspectRatio === undefined) {
|
|
58
|
-
return {};
|
|
58
|
+
return { aspectRatio: undefined };
|
|
59
59
|
}
|
|
60
60
|
return { aspectRatio };
|
|
61
61
|
}
|
package/src/parser/index.ts
CHANGED
|
@@ -84,10 +84,21 @@ export { parseTypography } from "./typography";
|
|
|
84
84
|
|
|
85
85
|
// Re-export modifier utilities
|
|
86
86
|
export {
|
|
87
|
+
expandSchemeModifier,
|
|
87
88
|
hasModifier,
|
|
89
|
+
isColorClass,
|
|
90
|
+
isColorSchemeModifier,
|
|
88
91
|
isPlatformModifier,
|
|
92
|
+
isSchemeModifier,
|
|
89
93
|
isStateModifier,
|
|
90
94
|
parseModifier,
|
|
91
95
|
splitModifierClasses,
|
|
92
96
|
} from "./modifiers";
|
|
93
|
-
export type {
|
|
97
|
+
export type {
|
|
98
|
+
ColorSchemeModifierType,
|
|
99
|
+
ModifierType,
|
|
100
|
+
ParsedModifier,
|
|
101
|
+
PlatformModifierType,
|
|
102
|
+
SchemeModifierType,
|
|
103
|
+
StateModifierType,
|
|
104
|
+
} from "./modifiers";
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import type { ModifierType, ParsedModifier } from "./modifiers";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
expandSchemeModifier,
|
|
5
|
+
hasModifier,
|
|
6
|
+
isColorClass,
|
|
7
|
+
isSchemeModifier,
|
|
8
|
+
parseModifier,
|
|
9
|
+
splitModifierClasses,
|
|
10
|
+
} from "./modifiers";
|
|
4
11
|
|
|
5
12
|
describe("parseModifier - basic functionality", () => {
|
|
6
13
|
it("should parse active modifier", () => {
|
|
@@ -373,3 +380,146 @@ describe("type safety", () => {
|
|
|
373
380
|
}
|
|
374
381
|
});
|
|
375
382
|
});
|
|
383
|
+
|
|
384
|
+
describe("isSchemeModifier", () => {
|
|
385
|
+
it("should return true for scheme modifier", () => {
|
|
386
|
+
expect(isSchemeModifier("scheme")).toBe(true);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("should return false for non-scheme modifiers", () => {
|
|
390
|
+
expect(isSchemeModifier("dark")).toBe(false);
|
|
391
|
+
expect(isSchemeModifier("light")).toBe(false);
|
|
392
|
+
expect(isSchemeModifier("active")).toBe(false);
|
|
393
|
+
expect(isSchemeModifier("ios")).toBe(false);
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
describe("isColorClass", () => {
|
|
398
|
+
it("should return true for text color classes", () => {
|
|
399
|
+
expect(isColorClass("text-red-500")).toBe(true);
|
|
400
|
+
expect(isColorClass("text-systemGray")).toBe(true);
|
|
401
|
+
expect(isColorClass("text-blue-50")).toBe(true);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("should return true for background color classes", () => {
|
|
405
|
+
expect(isColorClass("bg-red-500")).toBe(true);
|
|
406
|
+
expect(isColorClass("bg-systemGray")).toBe(true);
|
|
407
|
+
expect(isColorClass("bg-transparent")).toBe(true);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("should return true for border color classes", () => {
|
|
411
|
+
expect(isColorClass("border-red-500")).toBe(true);
|
|
412
|
+
expect(isColorClass("border-systemGray")).toBe(true);
|
|
413
|
+
expect(isColorClass("border-black")).toBe(true);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("should return false for non-color classes", () => {
|
|
417
|
+
expect(isColorClass("m-4")).toBe(false);
|
|
418
|
+
expect(isColorClass("p-2")).toBe(false);
|
|
419
|
+
expect(isColorClass("flex")).toBe(false);
|
|
420
|
+
expect(isColorClass("rounded-lg")).toBe(false);
|
|
421
|
+
expect(isColorClass("font-bold")).toBe(false);
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
describe("expandSchemeModifier", () => {
|
|
426
|
+
const customColors = {
|
|
427
|
+
"systemGray-dark": "#333333",
|
|
428
|
+
"systemGray-light": "#CCCCCC",
|
|
429
|
+
"primary-dark": "#1E40AF",
|
|
430
|
+
"primary-light": "#BFDBFE",
|
|
431
|
+
"accent-dark": "#DC2626",
|
|
432
|
+
"accent-light": "#FECACA",
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
it("should expand text color scheme modifier with default suffixes", () => {
|
|
436
|
+
const modifier = { modifier: "scheme" as const, baseClass: "text-systemGray" };
|
|
437
|
+
const result = expandSchemeModifier(modifier, customColors);
|
|
438
|
+
|
|
439
|
+
expect(result).toHaveLength(2);
|
|
440
|
+
expect((result as [ParsedModifier, ParsedModifier])[0]).toEqual({
|
|
441
|
+
modifier: "dark",
|
|
442
|
+
baseClass: "text-systemGray-dark",
|
|
443
|
+
});
|
|
444
|
+
expect((result as [ParsedModifier, ParsedModifier])[1]).toEqual({
|
|
445
|
+
modifier: "light",
|
|
446
|
+
baseClass: "text-systemGray-light",
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it("should expand background color scheme modifier", () => {
|
|
451
|
+
const modifier = { modifier: "scheme" as const, baseClass: "bg-primary" };
|
|
452
|
+
const result = expandSchemeModifier(modifier, customColors);
|
|
453
|
+
|
|
454
|
+
expect(result).toHaveLength(2);
|
|
455
|
+
expect((result as [ParsedModifier, ParsedModifier])[0]).toEqual({
|
|
456
|
+
modifier: "dark",
|
|
457
|
+
baseClass: "bg-primary-dark",
|
|
458
|
+
});
|
|
459
|
+
expect((result as [ParsedModifier, ParsedModifier])[1]).toEqual({
|
|
460
|
+
modifier: "light",
|
|
461
|
+
baseClass: "bg-primary-light",
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("should expand border color scheme modifier", () => {
|
|
466
|
+
const modifier = { modifier: "scheme" as const, baseClass: "border-accent" };
|
|
467
|
+
const result = expandSchemeModifier(modifier, customColors);
|
|
468
|
+
|
|
469
|
+
expect(result).toHaveLength(2);
|
|
470
|
+
expect((result as [ParsedModifier, ParsedModifier])[0]).toEqual({
|
|
471
|
+
modifier: "dark",
|
|
472
|
+
baseClass: "border-accent-dark",
|
|
473
|
+
});
|
|
474
|
+
expect((result as [ParsedModifier, ParsedModifier])[1]).toEqual({
|
|
475
|
+
modifier: "light",
|
|
476
|
+
baseClass: "border-accent-light",
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("should use custom suffixes when provided", () => {
|
|
481
|
+
const modifier = { modifier: "scheme" as const, baseClass: "text-systemGray" };
|
|
482
|
+
const _result = expandSchemeModifier(modifier, customColors, "-darkMode", "-lightMode");
|
|
483
|
+
|
|
484
|
+
const expectedColors = {
|
|
485
|
+
"systemGray-darkMode": "#333333",
|
|
486
|
+
"systemGray-lightMode": "#CCCCCC",
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
expect(expandSchemeModifier(modifier, expectedColors, "-darkMode", "-lightMode")).toHaveLength(2);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it("should return empty array for non-color classes", () => {
|
|
493
|
+
const modifier = { modifier: "scheme" as const, baseClass: "m-4" };
|
|
494
|
+
const result = expandSchemeModifier(modifier, customColors);
|
|
495
|
+
|
|
496
|
+
expect(result).toEqual([]);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("should return empty array when dark color variant is missing", () => {
|
|
500
|
+
const modifier = { modifier: "scheme" as const, baseClass: "text-missing" };
|
|
501
|
+
const incompleteColors = {
|
|
502
|
+
"missing-light": "#FFFFFF",
|
|
503
|
+
};
|
|
504
|
+
const result = expandSchemeModifier(modifier, incompleteColors);
|
|
505
|
+
|
|
506
|
+
expect(result).toEqual([]);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("should return empty array when light color variant is missing", () => {
|
|
510
|
+
const modifier = { modifier: "scheme" as const, baseClass: "text-missing" };
|
|
511
|
+
const incompleteColors = {
|
|
512
|
+
"missing-dark": "#000000",
|
|
513
|
+
};
|
|
514
|
+
const result = expandSchemeModifier(modifier, incompleteColors);
|
|
515
|
+
|
|
516
|
+
expect(result).toEqual([]);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it("should return empty array when both color variants are missing", () => {
|
|
520
|
+
const modifier = { modifier: "scheme" as const, baseClass: "text-missing" };
|
|
521
|
+
const result = expandSchemeModifier(modifier, customColors);
|
|
522
|
+
|
|
523
|
+
expect(result).toEqual([]);
|
|
524
|
+
});
|
|
525
|
+
});
|
package/src/parser/modifiers.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Modifier parsing utilities for state-based
|
|
2
|
+
* Modifier parsing utilities for state-based, platform-specific, and color scheme class names
|
|
3
3
|
* - State modifiers: active:, hover:, focus:, disabled:, placeholder:
|
|
4
4
|
* - Platform modifiers: ios:, android:, web:
|
|
5
|
+
* - Color scheme modifiers: dark:, light:
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
export type StateModifierType = "active" | "hover" | "focus" | "disabled" | "placeholder";
|
|
8
9
|
export type PlatformModifierType = "ios" | "android" | "web";
|
|
9
|
-
export type
|
|
10
|
+
export type ColorSchemeModifierType = "dark" | "light";
|
|
11
|
+
export type SchemeModifierType = "scheme";
|
|
12
|
+
export type ModifierType =
|
|
13
|
+
| StateModifierType
|
|
14
|
+
| PlatformModifierType
|
|
15
|
+
| ColorSchemeModifierType
|
|
16
|
+
| SchemeModifierType;
|
|
10
17
|
|
|
11
18
|
export type ParsedModifier = {
|
|
12
19
|
modifier: ModifierType;
|
|
@@ -30,9 +37,24 @@ const STATE_MODIFIERS: readonly StateModifierType[] = [
|
|
|
30
37
|
const PLATFORM_MODIFIERS: readonly PlatformModifierType[] = ["ios", "android", "web"] as const;
|
|
31
38
|
|
|
32
39
|
/**
|
|
33
|
-
*
|
|
40
|
+
* Supported color scheme modifiers that map to Appearance.colorScheme values
|
|
34
41
|
*/
|
|
35
|
-
const
|
|
42
|
+
const COLOR_SCHEME_MODIFIERS: readonly ColorSchemeModifierType[] = ["dark", "light"] as const;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Scheme modifier that expands to both dark: and light: modifiers
|
|
46
|
+
*/
|
|
47
|
+
const SCHEME_MODIFIERS: readonly SchemeModifierType[] = ["scheme"] as const;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* All supported modifiers (state + platform + color scheme + scheme)
|
|
51
|
+
*/
|
|
52
|
+
const SUPPORTED_MODIFIERS: readonly ModifierType[] = [
|
|
53
|
+
...STATE_MODIFIERS,
|
|
54
|
+
...PLATFORM_MODIFIERS,
|
|
55
|
+
...COLOR_SCHEME_MODIFIERS,
|
|
56
|
+
...SCHEME_MODIFIERS,
|
|
57
|
+
] as const;
|
|
36
58
|
|
|
37
59
|
/**
|
|
38
60
|
* Parse a class name to detect and extract modifiers
|
|
@@ -107,6 +129,119 @@ export function isPlatformModifier(modifier: ModifierType): modifier is Platform
|
|
|
107
129
|
return PLATFORM_MODIFIERS.includes(modifier as PlatformModifierType);
|
|
108
130
|
}
|
|
109
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Check if a modifier is a color scheme modifier (dark, light)
|
|
134
|
+
*
|
|
135
|
+
* @param modifier - Modifier type to check
|
|
136
|
+
* @returns true if modifier is a color scheme modifier
|
|
137
|
+
*/
|
|
138
|
+
export function isColorSchemeModifier(modifier: ModifierType): modifier is ColorSchemeModifierType {
|
|
139
|
+
return COLOR_SCHEME_MODIFIERS.includes(modifier as ColorSchemeModifierType);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if a modifier is a scheme modifier (scheme)
|
|
144
|
+
*
|
|
145
|
+
* @param modifier - Modifier type to check
|
|
146
|
+
* @returns true if modifier is a scheme modifier
|
|
147
|
+
*/
|
|
148
|
+
export function isSchemeModifier(modifier: ModifierType): modifier is SchemeModifierType {
|
|
149
|
+
return SCHEME_MODIFIERS.includes(modifier as SchemeModifierType);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check if a class name is a color-based utility class
|
|
154
|
+
*
|
|
155
|
+
* @param className - Class name to check
|
|
156
|
+
* @returns true if class is color-based (text-*, bg-*, border-*)
|
|
157
|
+
*/
|
|
158
|
+
export function isColorClass(className: string): boolean {
|
|
159
|
+
return className.startsWith("text-") || className.startsWith("bg-") || className.startsWith("border-");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Expand scheme modifier into dark and light modifiers
|
|
164
|
+
*
|
|
165
|
+
* @param schemeModifier - Parsed scheme modifier
|
|
166
|
+
* @param customColors - Custom colors from config
|
|
167
|
+
* @param darkSuffix - Suffix for dark variant (default: "-dark")
|
|
168
|
+
* @param lightSuffix - Suffix for light variant (default: "-light")
|
|
169
|
+
* @returns Array of expanded modifiers (dark: and light:), or empty array if validation fails
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* expandSchemeModifier(
|
|
173
|
+
* { modifier: "scheme", baseClass: "text-systemGray" },
|
|
174
|
+
* { "systemGray-dark": "#333", "systemGray-light": "#ccc" },
|
|
175
|
+
* "-dark",
|
|
176
|
+
* "-light"
|
|
177
|
+
* )
|
|
178
|
+
* // Returns: [
|
|
179
|
+
* // { modifier: "dark", baseClass: "text-systemGray-dark" },
|
|
180
|
+
* // { modifier: "light", baseClass: "text-systemGray-light" }
|
|
181
|
+
* // ]
|
|
182
|
+
*/
|
|
183
|
+
export function expandSchemeModifier(
|
|
184
|
+
schemeModifier: ParsedModifier,
|
|
185
|
+
customColors: Record<string, string>,
|
|
186
|
+
darkSuffix = "-dark",
|
|
187
|
+
lightSuffix = "-light",
|
|
188
|
+
): ParsedModifier[] {
|
|
189
|
+
const { baseClass } = schemeModifier;
|
|
190
|
+
|
|
191
|
+
// Only process color-based classes
|
|
192
|
+
if (!isColorClass(baseClass)) {
|
|
193
|
+
if (process.env.NODE_ENV !== "production") {
|
|
194
|
+
console.warn(
|
|
195
|
+
`[react-native-tailwind] scheme: modifier only supports color classes (text-*, bg-*, border-*). ` +
|
|
196
|
+
`Found: "${baseClass}". This modifier will be ignored.`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Extract the color name from the class
|
|
203
|
+
// e.g., "text-systemGray" -> "systemGray"
|
|
204
|
+
const match = baseClass.match(/^(text|bg|border)-(.+)$/);
|
|
205
|
+
if (!match) {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const [, prefix, colorName] = match;
|
|
210
|
+
|
|
211
|
+
// Build variant class names
|
|
212
|
+
const darkColorName = `${colorName}${darkSuffix}`;
|
|
213
|
+
const lightColorName = `${colorName}${lightSuffix}`;
|
|
214
|
+
|
|
215
|
+
// Validate that both color variants exist
|
|
216
|
+
const darkColorExists = customColors[darkColorName] !== undefined;
|
|
217
|
+
const lightColorExists = customColors[lightColorName] !== undefined;
|
|
218
|
+
|
|
219
|
+
if (!darkColorExists || !lightColorExists) {
|
|
220
|
+
if (process.env.NODE_ENV !== "production") {
|
|
221
|
+
const missing = [];
|
|
222
|
+
if (!darkColorExists) missing.push(`${colorName}${darkSuffix}`);
|
|
223
|
+
if (!lightColorExists) missing.push(`${colorName}${lightSuffix}`);
|
|
224
|
+
console.warn(
|
|
225
|
+
`[react-native-tailwind] scheme:${baseClass} requires both color variants to exist. ` +
|
|
226
|
+
`Missing: ${missing.join(", ")}. This modifier will be ignored.`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Expand to dark: and light: modifiers
|
|
233
|
+
return [
|
|
234
|
+
{
|
|
235
|
+
modifier: "dark" as ColorSchemeModifierType,
|
|
236
|
+
baseClass: `${prefix}-${darkColorName}`,
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
modifier: "light" as ColorSchemeModifierType,
|
|
240
|
+
baseClass: `${prefix}-${lightColorName}`,
|
|
241
|
+
},
|
|
242
|
+
];
|
|
243
|
+
}
|
|
244
|
+
|
|
110
245
|
/**
|
|
111
246
|
* Split a space-separated className string into base and modifier classes
|
|
112
247
|
*
|
|
@@ -60,6 +60,69 @@ describe("parseSpacing - margin", () => {
|
|
|
60
60
|
});
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
describe("parseSpacing - negative margin", () => {
|
|
64
|
+
it("should parse negative margin all sides", () => {
|
|
65
|
+
expect(parseSpacing("-m-0")).toEqual({ margin: -0 }); // JavaScript -0 is distinct from +0
|
|
66
|
+
expect(parseSpacing("-m-4")).toEqual({ margin: -16 });
|
|
67
|
+
expect(parseSpacing("-m-8")).toEqual({ margin: -32 });
|
|
68
|
+
expect(parseSpacing("-m-96")).toEqual({ margin: -384 });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should parse negative margin with fractional values", () => {
|
|
72
|
+
expect(parseSpacing("-m-0.5")).toEqual({ margin: -2 });
|
|
73
|
+
expect(parseSpacing("-m-1.5")).toEqual({ margin: -6 });
|
|
74
|
+
expect(parseSpacing("-m-2.5")).toEqual({ margin: -10 });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should parse negative margin horizontal", () => {
|
|
78
|
+
expect(parseSpacing("-mx-4")).toEqual({ marginHorizontal: -16 });
|
|
79
|
+
expect(parseSpacing("-mx-8")).toEqual({ marginHorizontal: -32 });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should parse negative margin vertical", () => {
|
|
83
|
+
expect(parseSpacing("-my-4")).toEqual({ marginVertical: -16 });
|
|
84
|
+
expect(parseSpacing("-my-8")).toEqual({ marginVertical: -32 });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should parse negative margin directional", () => {
|
|
88
|
+
expect(parseSpacing("-mt-4")).toEqual({ marginTop: -16 });
|
|
89
|
+
expect(parseSpacing("-mr-4")).toEqual({ marginRight: -16 });
|
|
90
|
+
expect(parseSpacing("-mb-4")).toEqual({ marginBottom: -16 });
|
|
91
|
+
expect(parseSpacing("-ml-4")).toEqual({ marginLeft: -16 });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should parse negative margin with arbitrary values", () => {
|
|
95
|
+
expect(parseSpacing("-m-[16px]")).toEqual({ margin: -16 });
|
|
96
|
+
expect(parseSpacing("-m-[16]")).toEqual({ margin: -16 });
|
|
97
|
+
expect(parseSpacing("-m-[100px]")).toEqual({ margin: -100 });
|
|
98
|
+
expect(parseSpacing("-m-[100]")).toEqual({ margin: -100 });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should parse negative margin directional with arbitrary values", () => {
|
|
102
|
+
expect(parseSpacing("-mt-[24px]")).toEqual({ marginTop: -24 });
|
|
103
|
+
expect(parseSpacing("-mr-[32]")).toEqual({ marginRight: -32 });
|
|
104
|
+
expect(parseSpacing("-mb-[16px]")).toEqual({ marginBottom: -16 });
|
|
105
|
+
expect(parseSpacing("-ml-[48]")).toEqual({ marginLeft: -48 });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should parse negative margin horizontal/vertical with arbitrary values", () => {
|
|
109
|
+
expect(parseSpacing("-mx-[20px]")).toEqual({ marginHorizontal: -20 });
|
|
110
|
+
expect(parseSpacing("-my-[30]")).toEqual({ marginVertical: -30 });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should not parse negative padding (invalid)", () => {
|
|
114
|
+
expect(parseSpacing("-p-4")).toBeNull();
|
|
115
|
+
expect(parseSpacing("-px-4")).toBeNull();
|
|
116
|
+
expect(parseSpacing("-pt-4")).toBeNull();
|
|
117
|
+
expect(parseSpacing("-p-[16px]")).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should not parse negative gap (invalid)", () => {
|
|
121
|
+
expect(parseSpacing("-gap-4")).toBeNull();
|
|
122
|
+
expect(parseSpacing("-gap-[16px]")).toBeNull();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
63
126
|
describe("parseSpacing - padding", () => {
|
|
64
127
|
it("should parse padding all sides", () => {
|
|
65
128
|
expect(parseSpacing("p-0")).toEqual({ padding: 0 });
|
package/src/parser/spacing.ts
CHANGED
|
@@ -69,24 +69,28 @@ function parseArbitrarySpacing(value: string): number | null {
|
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
71
|
* Parse spacing classes (margin, padding, gap)
|
|
72
|
-
* Examples: m-4, mx-2, mt-8, p-4, px-2, pt-8, gap-4, m-[16px]
|
|
72
|
+
* Examples: m-4, mx-2, mt-8, p-4, px-2, pt-8, gap-4, m-[16px], -m-4, -mt-[10px]
|
|
73
73
|
*/
|
|
74
74
|
export function parseSpacing(cls: string): StyleObject | null {
|
|
75
|
-
// Margin: m-4, mx-2, mt-8, m-[16px], etc.
|
|
76
|
-
|
|
75
|
+
// Margin: m-4, mx-2, mt-8, m-[16px], -m-4, -mt-2, etc.
|
|
76
|
+
// Supports negative values for margins (but not padding or gap)
|
|
77
|
+
const marginMatch = cls.match(/^(-?)m([xytrbls]?)-(.+)$/);
|
|
77
78
|
if (marginMatch) {
|
|
78
|
-
const [, dir, valueStr] = marginMatch;
|
|
79
|
+
const [, negativePrefix, dir, valueStr] = marginMatch;
|
|
80
|
+
const isNegative = negativePrefix === "-";
|
|
79
81
|
|
|
80
82
|
// Try arbitrary value first
|
|
81
83
|
const arbitraryValue = parseArbitrarySpacing(valueStr);
|
|
82
84
|
if (arbitraryValue !== null) {
|
|
83
|
-
|
|
85
|
+
const finalValue = isNegative ? -arbitraryValue : arbitraryValue;
|
|
86
|
+
return getMarginStyle(dir, finalValue);
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
// Try preset scale
|
|
87
90
|
const scaleValue = SPACING_SCALE[valueStr];
|
|
88
91
|
if (scaleValue !== undefined) {
|
|
89
|
-
|
|
92
|
+
const finalValue = isNegative ? -scaleValue : scaleValue;
|
|
93
|
+
return getMarginStyle(dir, finalValue);
|
|
90
94
|
}
|
|
91
95
|
}
|
|
92
96
|
|
package/src/runtime.test.ts
CHANGED
|
@@ -45,6 +45,33 @@ describe("runtime", () => {
|
|
|
45
45
|
});
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
it("should preserve zero values in template literals", () => {
|
|
49
|
+
// Issue #1 fix: 0 should be treated as valid value, not filtered out
|
|
50
|
+
const result = tw`opacity-${0} m-4`;
|
|
51
|
+
expect(result?.style).toEqual({
|
|
52
|
+
opacity: 0,
|
|
53
|
+
margin: 16,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should preserve empty string values in template literals", () => {
|
|
58
|
+
// Empty strings should be preserved (even though they don't contribute to className)
|
|
59
|
+
const result = tw`m-4 ${""}p-2`;
|
|
60
|
+
expect(result?.style).toEqual({
|
|
61
|
+
margin: 16,
|
|
62
|
+
padding: 8,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should handle mixed falsy and truthy numeric values", () => {
|
|
67
|
+
const spacing = 0;
|
|
68
|
+
const result = tw`m-${spacing} p-4`;
|
|
69
|
+
expect(result?.style).toEqual({
|
|
70
|
+
margin: 0,
|
|
71
|
+
padding: 16,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
48
75
|
it("should return empty style object for empty className", () => {
|
|
49
76
|
const result = tw``;
|
|
50
77
|
expect(result).toEqual({ style: {} });
|
package/src/runtime.ts
CHANGED
|
@@ -211,8 +211,9 @@ export function tw<T extends NativeStyle = NativeStyle>(
|
|
|
211
211
|
const className = strings.reduce((acc, str, i) => {
|
|
212
212
|
const value = values[i];
|
|
213
213
|
// Handle falsy values (false, null, undefined) - don't add them
|
|
214
|
+
// Note: 0 and empty string are preserved as they may be valid values
|
|
214
215
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
215
|
-
const valueStr = value ? String(value) : "";
|
|
216
|
+
const valueStr = value != null && value !== false ? String(value) : "";
|
|
216
217
|
return acc + str + valueStr;
|
|
217
218
|
}, "");
|
|
218
219
|
|