Skip to content

Commit 432a23c

Browse files
authored
Merge pull request #6 from e-simpson/outline-pr
feat: Add outline support for new arch
2 parents 362b0fd + 8433ac6 commit 432a23c

8 files changed

Lines changed: 252 additions & 3 deletions

File tree

docs/astro.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export default defineConfig({
5555
{ label: "Colors", slug: "reference/colors" },
5656
{ label: "Typography", slug: "reference/typography" },
5757
{ label: "Borders", slug: "reference/borders" },
58+
{ label: "Outlines", slug: "reference/outlines" },
5859
{ label: "Shadows & Elevation", slug: "reference/shadows" },
5960
{ label: "Aspect Ratio", slug: "reference/aspect-ratio" },
6061
{ label: "Transforms", slug: "reference/transforms" },

docs/src/content/docs/reference/borders.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,4 +167,5 @@ Apply colors to individual border sides. See the [Colors reference](/react-nativ
167167
## Related
168168

169169
- [Colors](/react-native-tailwind/reference/colors/) - Border color utilities
170+
- [Outlines](/react-native-tailwind/reference/outlines/) - Outline width, style, and offset utilities
170171
- [Shadows](/react-native-tailwind/reference/shadows/) - Shadow and elevation
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
title: Outlines
3+
description: Outline width, style, and offset utilities
4+
---
5+
6+
Utilities for controlling the outline style of an element.
7+
8+
> **Note**: Outline support requires React Native 0.73+ (New Architecture) and setting the `outline` style property.
9+
10+
## Outline Width
11+
12+
```tsx
13+
<View className="outline" /> // outlineWidth: 1, outlineStyle: 'solid'
14+
<View className="outline-0" /> // outlineWidth: 0
15+
<View className="outline-2" /> // outlineWidth: 2
16+
<View className="outline-4" /> // outlineWidth: 4
17+
<View className="outline-[2px]" /> // outlineWidth: 2
18+
<View className="outline-none" /> // outlineWidth: 0
19+
```
20+
21+
## Outline Color
22+
23+
```tsx
24+
<View className="outline-blue-500" /> // outlineColor: '#3B82F6'
25+
<View className="outline-[#ff0000]" /> // outlineColor: '#ff0000'
26+
<View className="outline-red-500/50" /> // outlineColor: '#EF4444' (50% opacity)
27+
```
28+
29+
## Outline Style
30+
31+
```tsx
32+
<View className="outline-solid" /> // outlineStyle: 'solid'
33+
<View className="outline-dashed" /> // outlineStyle: 'dashed'
34+
<View className="outline-dotted" /> // outlineStyle: 'dotted'
35+
```
36+
37+
## Outline Offset
38+
39+
Utilities for controlling the offset of an element's outline.
40+
41+
```tsx
42+
<View className="outline-offset-0" /> // outlineOffset: 0
43+
<View className="outline-offset-1" /> // outlineOffset: 1
44+
<View className="outline-offset-2" /> // outlineOffset: 2
45+
<View className="outline-offset-4" /> // outlineOffset: 4
46+
<View className="outline-offset-[3px]" /> // outlineOffset: 3
47+
```
48+
49+
## Example
50+
51+
```tsx
52+
<View className="w-32 h-32 bg-white outline outline-blue-500 outline-offset-2 rounded-lg" />
53+
```
54+
55+
## Related
56+
57+
- [Borders](/react-native-tailwind/reference/borders/) - Border width, radius, and style utilities
58+
- [Colors](/react-native-tailwind/reference/colors/) - Color utilities
59+
- [Shadows](/react-native-tailwind/reference/shadows/) - Shadow and elevation

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export {
2525
parseBorder,
2626
parseColor,
2727
parseLayout,
28+
parseOutline,
2829
parsePlaceholderClass,
2930
parsePlaceholderClasses,
3031
parseShadow,

src/parser/colors.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ export { COLORS };
1212
* Parse color classes (background, text, border)
1313
* Supports opacity modifier: bg-blue-500/50, text-black/80, border-red-500/30
1414
*/
15-
export function parseColor(cls: string, customColors?: Record<string, string>): StyleObject | null {
15+
export function parseColor(
16+
cls: string,
17+
customColors?: Record<string, string>
18+
): StyleObject | null {
1619
// Helper to get color with custom override (custom colors take precedence)
1720
const getColor = (key: string): string | undefined => {
1821
return customColors?.[key] ?? COLORS[key];
@@ -32,7 +35,7 @@ export function parseColor(cls: string, customColors?: Record<string, string>):
3235
/* v8 ignore next 5 */
3336
if (process.env.NODE_ENV !== "production") {
3437
console.warn(
35-
`[react-native-tailwind] Invalid opacity value: ${opacity}. Opacity must be between 0 and 100.`,
38+
`[react-native-tailwind] Invalid opacity value: ${opacity}. Opacity must be between 0 and 100.`
3639
);
3740
}
3841
return null;
@@ -65,7 +68,7 @@ export function parseColor(cls: string, customColors?: Record<string, string>):
6568
/* v8 ignore next 5 */
6669
if (process.env.NODE_ENV !== "production") {
6770
console.warn(
68-
`[react-native-tailwind] Unsupported arbitrary color value: ${colorKey}. Only hex colors are supported (e.g., [#ff0000], [#f00], or [#ff0000aa]).`,
71+
`[react-native-tailwind] Unsupported arbitrary color value: ${colorKey}. Only hex colors are supported (e.g., [#ff0000], [#f00], or [#ff0000aa]).`
6972
);
7073
}
7174
return null;
@@ -116,6 +119,29 @@ export function parseColor(cls: string, customColors?: Record<string, string>):
116119
}
117120
}
118121

122+
// Outline color: outline-blue-500, outline-blue-500/50, outline-[#ff0000]/80
123+
if (
124+
cls.startsWith("outline-") &&
125+
!cls.match(/^outline-[0-9]/) &&
126+
!cls.startsWith("outline-offset-")
127+
) {
128+
const colorKey = cls.substring(8); // "outline-".length = 8
129+
130+
// Skip outline-style values
131+
if (["solid", "dashed", "dotted", "none"].includes(colorKey)) {
132+
return null;
133+
}
134+
135+
// Skip arbitrary values that don't look like colors (e.g., outline-[3px] is width)
136+
if (colorKey.startsWith("[") && !colorKey.startsWith("[#")) {
137+
return null;
138+
}
139+
const color = parseColorWithOpacity(colorKey);
140+
if (color) {
141+
return { outlineColor: color };
142+
}
143+
}
144+
119145
// Directional border colors: border-t-red-500, border-l-blue-500/50, border-r-[#ff0000]
120146
const dirBorderMatch = cls.match(/^border-([trblxy])-(.+)$/);
121147
if (dirBorderMatch) {

src/parser/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { parseAspectRatio } from "./aspectRatio";
99
import { parseBorder } from "./borders";
1010
import { parseColor } from "./colors";
1111
import { parseLayout } from "./layout";
12+
import { parseOutline } from "./outline";
1213
import { parseShadow } from "./shadows";
1314
import { parseSizing } from "./sizing";
1415
import { parseSpacing } from "./spacing";
@@ -56,6 +57,7 @@ export function parseClass(cls: string, customTheme?: CustomTheme): StyleObject
5657
const parsers: Array<(cls: string) => StyleObject | null> = [
5758
(cls: string) => parseSpacing(cls, customTheme?.spacing),
5859
(cls: string) => parseBorder(cls, customTheme?.colors),
60+
parseOutline,
5961
(cls: string) => parseColor(cls, customTheme?.colors),
6062
(cls: string) => parseLayout(cls, customTheme?.spacing),
6163
(cls: string) => parseTypography(cls, customTheme?.fontFamily, customTheme?.fontSize),
@@ -86,6 +88,7 @@ export { parseAspectRatio } from "./aspectRatio";
8688
export { parseBorder } from "./borders";
8789
export { parseColor } from "./colors";
8890
export { parseLayout } from "./layout";
91+
export { parseOutline } from "./outline";
8992
export { parsePlaceholderClass, parsePlaceholderClasses } from "./placeholder";
9093
export { parseShadow } from "./shadows";
9194
export { parseSizing } from "./sizing";

src/parser/outline.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { describe, expect, it } from "vitest";
2+
import { parseOutline } from "./outline";
3+
4+
describe("parseOutline", () => {
5+
it("should parse outline shorthand", () => {
6+
expect(parseOutline("outline")).toEqual({
7+
outlineWidth: 1,
8+
outlineStyle: "solid",
9+
});
10+
});
11+
12+
it("should parse outline-none", () => {
13+
expect(parseOutline("outline-none")).toEqual({ outlineWidth: 0 });
14+
});
15+
16+
it("should parse outline width with preset values", () => {
17+
expect(parseOutline("outline-0")).toEqual({ outlineWidth: 0 });
18+
expect(parseOutline("outline-2")).toEqual({ outlineWidth: 2 });
19+
expect(parseOutline("outline-4")).toEqual({ outlineWidth: 4 });
20+
expect(parseOutline("outline-8")).toEqual({ outlineWidth: 8 });
21+
});
22+
23+
it("should parse outline width with arbitrary values", () => {
24+
expect(parseOutline("outline-[5px]")).toEqual({ outlineWidth: 5 });
25+
expect(parseOutline("outline-[10]")).toEqual({ outlineWidth: 10 });
26+
});
27+
28+
it("should parse outline style", () => {
29+
expect(parseOutline("outline-solid")).toEqual({ outlineStyle: "solid" });
30+
expect(parseOutline("outline-dashed")).toEqual({ outlineStyle: "dashed" });
31+
expect(parseOutline("outline-dotted")).toEqual({ outlineStyle: "dotted" });
32+
});
33+
34+
it("should parse outline offset with preset values", () => {
35+
expect(parseOutline("outline-offset-0")).toEqual({ outlineOffset: 0 });
36+
expect(parseOutline("outline-offset-2")).toEqual({ outlineOffset: 2 });
37+
expect(parseOutline("outline-offset-4")).toEqual({ outlineOffset: 4 });
38+
expect(parseOutline("outline-offset-8")).toEqual({ outlineOffset: 8 });
39+
});
40+
41+
it("should parse outline offset with arbitrary values", () => {
42+
expect(parseOutline("outline-offset-[3px]")).toEqual({ outlineOffset: 3 });
43+
expect(parseOutline("outline-offset-[5]")).toEqual({ outlineOffset: 5 });
44+
});
45+
46+
it("should return null for invalid outline values", () => {
47+
expect(parseOutline("outline-invalid")).toBeNull();
48+
expect(parseOutline("outline-3")).toBeNull(); // Not in scale
49+
expect(parseOutline("outline-offset-3")).toBeNull(); // Not in scale
50+
expect(parseOutline("outline-[5rem]")).toBeNull(); // Unsupported unit
51+
});
52+
53+
it("should return null for outline colors (handled by parseColor)", () => {
54+
expect(parseOutline("outline-red-500")).toBeNull();
55+
expect(parseOutline("outline-[#ff0000]")).toBeNull();
56+
});
57+
});

src/parser/outline.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* Outline utilities (outline width, style, offset)
3+
*/
4+
5+
import type { StyleObject } from "../types";
6+
import { BORDER_WIDTH_SCALE } from "./borders";
7+
8+
/**
9+
* Parse arbitrary outline width/offset value: [8px], [4]
10+
* Returns number for px values, null for unsupported formats
11+
*/
12+
function parseArbitraryOutlineValue(value: string): number | null {
13+
// Match: [8px] or [8] (pixels only)
14+
const pxMatch = value.match(/^\[(\d+)(?:px)?\]$/);
15+
if (pxMatch) {
16+
return parseInt(pxMatch[1], 10);
17+
}
18+
19+
// Warn about unsupported formats
20+
if (value.startsWith("[") && value.endsWith("]")) {
21+
/* v8 ignore next 5 */
22+
if (process.env.NODE_ENV !== "production") {
23+
console.warn(
24+
`[react-native-tailwind] Unsupported arbitrary outline value: ${value}. Only px values are supported (e.g., [8px] or [8]).`,
25+
);
26+
}
27+
return null;
28+
}
29+
30+
return null;
31+
}
32+
33+
/**
34+
* Parse outline classes
35+
* @param cls - The class name to parse
36+
*/
37+
export function parseOutline(cls: string): StyleObject | null {
38+
// Shorthand: outline (width: 1, style: solid)
39+
if (cls === "outline") {
40+
return { outlineWidth: 1, outlineStyle: "solid" };
41+
}
42+
43+
// Outline none
44+
if (cls === "outline-none") {
45+
return { outlineWidth: 0 };
46+
}
47+
48+
// Outline style
49+
if (cls === "outline-solid") return { outlineStyle: "solid" };
50+
if (cls === "outline-dotted") return { outlineStyle: "dotted" };
51+
if (cls === "outline-dashed") return { outlineStyle: "dashed" };
52+
53+
// Outline offset: outline-offset-2, outline-offset-[3px]
54+
if (cls.startsWith("outline-offset-")) {
55+
const valueStr = cls.substring(15); // "outline-offset-".length = 15
56+
57+
// Try arbitrary value first
58+
if (valueStr.startsWith("[")) {
59+
const arbitraryValue = parseArbitraryOutlineValue(valueStr);
60+
if (arbitraryValue !== null) {
61+
return { outlineOffset: arbitraryValue };
62+
}
63+
return null;
64+
}
65+
66+
// Try preset scale (reuse border width scale for consistency with default Tailwind)
67+
const scaleValue = BORDER_WIDTH_SCALE[valueStr];
68+
if (scaleValue !== undefined) {
69+
return { outlineOffset: scaleValue };
70+
}
71+
72+
return null;
73+
}
74+
75+
// Outline width: outline-0, outline-2, outline-[5px]
76+
// Must handle potential collision with outline-red-500 (colors)
77+
// Logic: if it matches width pattern, return width. If it looks like color, return null (let parseColor handle it)
78+
79+
const widthMatch = cls.match(/^outline-(\d+)$/);
80+
if (widthMatch) {
81+
const value = BORDER_WIDTH_SCALE[widthMatch[1]];
82+
if (value !== undefined) {
83+
return { outlineWidth: value };
84+
}
85+
}
86+
87+
const arbMatch = cls.match(/^outline-(\[.+\])$/);
88+
if (arbMatch) {
89+
// Check if it's a color first? No, colors usually look like [#...] or [rgb(...)]
90+
// parseArbitraryOutlineValue only accepts [123] or [123px]
91+
// If it fails, it might be a color, so we return null
92+
const arbitraryValue = parseArbitraryOutlineValue(arbMatch[1]);
93+
if (arbitraryValue !== null) {
94+
return { outlineWidth: arbitraryValue };
95+
}
96+
return null;
97+
}
98+
99+
// If it's outline-{color}, return null so parseColor (called later in index.ts) handles it
100+
return null;
101+
}

0 commit comments

Comments
 (0)