From eabc9cb50bf2316262474fd0e3381e7af3ec9e4e Mon Sep 17 00:00:00 2001 From: Evan Simpson <25159851+e-simpson@users.noreply.github.com> Date: Sat, 24 Jan 2026 12:23:01 -0500 Subject: [PATCH 1/3] Implement outline class for new arch --- docs/src/content/docs/reference/borders.md | 56 ++++++++++- docs/src/content/docs/reference/outlines.md | 59 +++++++++++ src/index.ts | 1 + src/parser/colors.ts | 32 +++++- src/parser/index.ts | 3 + src/parser/outline.test.ts | 57 +++++++++++ src/parser/outline.ts | 102 ++++++++++++++++++++ 7 files changed, 303 insertions(+), 7 deletions(-) create mode 100644 docs/src/content/docs/reference/outlines.md create mode 100644 src/parser/outline.test.ts create mode 100644 src/parser/outline.ts diff --git a/docs/src/content/docs/reference/borders.md b/docs/src/content/docs/reference/borders.md index a63627f..3aeabf6 100644 --- a/docs/src/content/docs/reference/borders.md +++ b/docs/src/content/docs/reference/borders.md @@ -117,10 +117,7 @@ Apply colors to individual border sides. See the [Colors reference](/react-nativ ### Combining Width and Color ```tsx - - // borderLeftWidth: 4 - // borderLeftColor: '#3B82F6' - +// borderLeftWidth: 4 // borderLeftColor: '#3B82F6' ``` ### Example: Accent Border @@ -168,3 +165,54 @@ Apply colors to individual border sides. See the [Colors reference](/react-nativ - [Colors](/react-native-tailwind/reference/colors/) - Border color utilities - [Shadows](/react-native-tailwind/reference/shadows/) - Shadow and elevation + +## Outline + +Utilities for controlling the outline style of an element. + +> **Note**: Outline support requires React Native 0.73+ (New Architecture) and setting the `outline` style property. + +### Outline Width + +```tsx + // outlineWidth: 1, outlineStyle: 'solid' + // outlineWidth: 0 + // outlineWidth: 2 + // outlineWidth: 4 + // outlineWidth: 2 + // outlineWidth: 0 +``` + +### Outline Color + +```tsx + // outlineColor: '#3B82F6' + // outlineColor: '#ff0000' + // outlineColor: '#EF4444' (50% opacity) +``` + +### Outline Style + +```tsx + // outlineStyle: 'solid' + // outlineStyle: 'dashed' + // outlineStyle: 'dotted' +``` + +### Outline Offset + +Utilities for controlling the offset of an element's outline. + +```tsx + // outlineOffset: 0 + // outlineOffset: 1 + // outlineOffset: 2 + // outlineOffset: 4 + // outlineOffset: 3 +``` + +### Example + +```tsx + +``` diff --git a/docs/src/content/docs/reference/outlines.md b/docs/src/content/docs/reference/outlines.md new file mode 100644 index 0000000..7593e08 --- /dev/null +++ b/docs/src/content/docs/reference/outlines.md @@ -0,0 +1,59 @@ +--- +title: Outlines +description: Outline width, style, and offset utilities +--- + +Utilities for controlling the outline style of an element. + +> **Note**: Outline support requires React Native 0.73+ (New Architecture) and setting the `outline` style property. + +## Outline Width + +```tsx + // outlineWidth: 1, outlineStyle: 'solid' + // outlineWidth: 0 + // outlineWidth: 2 + // outlineWidth: 4 + // outlineWidth: 2 + // outlineWidth: 0 +``` + +## Outline Color + +```tsx + // outlineColor: '#3B82F6' + // outlineColor: '#ff0000' + // outlineColor: '#EF4444' (50% opacity) +``` + +## Outline Style + +```tsx + // outlineStyle: 'solid' + // outlineStyle: 'dashed' + // outlineStyle: 'dotted' +``` + +## Outline Offset + +Utilities for controlling the offset of an element's outline. + +```tsx + // outlineOffset: 0 + // outlineOffset: 1 + // outlineOffset: 2 + // outlineOffset: 4 + // outlineOffset: 3 +``` + +## Example + +```tsx + +``` + +## Related + +- [Borders](/react-native-tailwind/reference/borders/) - Border width, radius, and style utilities +- [Colors](/react-native-tailwind/reference/colors/) - Color utilities +- [Shadows](/react-native-tailwind/reference/shadows/) - Shadow and elevation diff --git a/src/index.ts b/src/index.ts index 515af19..0bd46f6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,7 @@ export { parseBorder, parseColor, parseLayout, + parseOutline, parsePlaceholderClass, parsePlaceholderClasses, parseShadow, diff --git a/src/parser/colors.ts b/src/parser/colors.ts index 3966a00..b23e7ab 100644 --- a/src/parser/colors.ts +++ b/src/parser/colors.ts @@ -12,7 +12,10 @@ export { COLORS }; * Parse color classes (background, text, border) * Supports opacity modifier: bg-blue-500/50, text-black/80, border-red-500/30 */ -export function parseColor(cls: string, customColors?: Record): StyleObject | null { +export function parseColor( + cls: string, + customColors?: Record +): StyleObject | null { // Helper to get color with custom override (custom colors take precedence) const getColor = (key: string): string | undefined => { return customColors?.[key] ?? COLORS[key]; @@ -32,7 +35,7 @@ export function parseColor(cls: string, customColors?: Record): /* v8 ignore next 5 */ if (process.env.NODE_ENV !== "production") { console.warn( - `[react-native-tailwind] Invalid opacity value: ${opacity}. Opacity must be between 0 and 100.`, + `[react-native-tailwind] Invalid opacity value: ${opacity}. Opacity must be between 0 and 100.` ); } return null; @@ -65,7 +68,7 @@ export function parseColor(cls: string, customColors?: Record): /* v8 ignore next 5 */ if (process.env.NODE_ENV !== "production") { console.warn( - `[react-native-tailwind] Unsupported arbitrary color value: ${colorKey}. Only hex colors are supported (e.g., [#ff0000], [#f00], or [#ff0000aa]).`, + `[react-native-tailwind] Unsupported arbitrary color value: ${colorKey}. Only hex colors are supported (e.g., [#ff0000], [#f00], or [#ff0000aa]).` ); } return null; @@ -116,6 +119,29 @@ export function parseColor(cls: string, customColors?: Record): } } + // Outline color: outline-blue-500, outline-blue-500/50, outline-[#ff0000]/80 + if ( + cls.startsWith("outline-") && + !cls.match(/^outline-[0-9]/) && + !cls.startsWith("outline-offset-") + ) { + const colorKey = cls.substring(8); // "outline-".length = 8 + + // Skip outline-style values + if (["solid", "dashed", "dotted", "none"].includes(colorKey)) { + return null; + } + + // Skip arbitrary values that don't look like colors (e.g., outline-[3px] is width) + if (colorKey.startsWith("[") && !colorKey.startsWith("[#")) { + return null; + } + const color = parseColorWithOpacity(colorKey); + if (color) { + return { outlineColor: color }; + } + } + // Directional border colors: border-t-red-500, border-l-blue-500/50, border-r-[#ff0000] const dirBorderMatch = cls.match(/^border-([trblxy])-(.+)$/); if (dirBorderMatch) { diff --git a/src/parser/index.ts b/src/parser/index.ts index ebc143a..8599c14 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -9,6 +9,7 @@ import { parseAspectRatio } from "./aspectRatio"; import { parseBorder } from "./borders"; import { parseColor } from "./colors"; import { parseLayout } from "./layout"; +import { parseOutline } from "./outline"; import { parseShadow } from "./shadows"; import { parseSizing } from "./sizing"; import { parseSpacing } from "./spacing"; @@ -56,6 +57,7 @@ export function parseClass(cls: string, customTheme?: CustomTheme): StyleObject const parsers: Array<(cls: string) => StyleObject | null> = [ (cls: string) => parseSpacing(cls, customTheme?.spacing), (cls: string) => parseBorder(cls, customTheme?.colors), + (cls: string) => parseOutline(cls, customTheme?.colors), (cls: string) => parseColor(cls, customTheme?.colors), (cls: string) => parseLayout(cls, customTheme?.spacing), (cls: string) => parseTypography(cls, customTheme?.fontFamily, customTheme?.fontSize), @@ -86,6 +88,7 @@ export { parseAspectRatio } from "./aspectRatio"; export { parseBorder } from "./borders"; export { parseColor } from "./colors"; export { parseLayout } from "./layout"; +export { parseOutline } from "./outline"; export { parsePlaceholderClass, parsePlaceholderClasses } from "./placeholder"; export { parseShadow } from "./shadows"; export { parseSizing } from "./sizing"; diff --git a/src/parser/outline.test.ts b/src/parser/outline.test.ts new file mode 100644 index 0000000..56e396d --- /dev/null +++ b/src/parser/outline.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from "vitest"; +import { parseOutline } from "./outline"; + +describe("parseOutline", () => { + it("should parse outline shorthand", () => { + expect(parseOutline("outline")).toEqual({ + outlineWidth: 1, + outlineStyle: "solid", + }); + }); + + it("should parse outline-none", () => { + expect(parseOutline("outline-none")).toEqual({ outlineWidth: 0 }); + }); + + it("should parse outline width with preset values", () => { + expect(parseOutline("outline-0")).toEqual({ outlineWidth: 0 }); + expect(parseOutline("outline-2")).toEqual({ outlineWidth: 2 }); + expect(parseOutline("outline-4")).toEqual({ outlineWidth: 4 }); + expect(parseOutline("outline-8")).toEqual({ outlineWidth: 8 }); + }); + + it("should parse outline width with arbitrary values", () => { + expect(parseOutline("outline-[5px]")).toEqual({ outlineWidth: 5 }); + expect(parseOutline("outline-[10]")).toEqual({ outlineWidth: 10 }); + }); + + it("should parse outline style", () => { + expect(parseOutline("outline-solid")).toEqual({ outlineStyle: "solid" }); + expect(parseOutline("outline-dashed")).toEqual({ outlineStyle: "dashed" }); + expect(parseOutline("outline-dotted")).toEqual({ outlineStyle: "dotted" }); + }); + + it("should parse outline offset with preset values", () => { + expect(parseOutline("outline-offset-0")).toEqual({ outlineOffset: 0 }); + expect(parseOutline("outline-offset-2")).toEqual({ outlineOffset: 2 }); + expect(parseOutline("outline-offset-4")).toEqual({ outlineOffset: 4 }); + expect(parseOutline("outline-offset-8")).toEqual({ outlineOffset: 8 }); + }); + + it("should parse outline offset with arbitrary values", () => { + expect(parseOutline("outline-offset-[3px]")).toEqual({ outlineOffset: 3 }); + expect(parseOutline("outline-offset-[5]")).toEqual({ outlineOffset: 5 }); + }); + + it("should return null for invalid outline values", () => { + expect(parseOutline("outline-invalid")).toBeNull(); + expect(parseOutline("outline-3")).toBeNull(); // Not in scale + expect(parseOutline("outline-offset-3")).toBeNull(); // Not in scale + expect(parseOutline("outline-[5rem]")).toBeNull(); // Unsupported unit + }); + + it("should return null for outline colors (handled by parseColor)", () => { + expect(parseOutline("outline-red-500")).toBeNull(); + expect(parseOutline("outline-[#ff0000]")).toBeNull(); + }); +}); diff --git a/src/parser/outline.ts b/src/parser/outline.ts new file mode 100644 index 0000000..2ec21c7 --- /dev/null +++ b/src/parser/outline.ts @@ -0,0 +1,102 @@ +/** + * Outline utilities (outline width, style, offset) + */ + +import type { StyleObject } from "../types"; +import { BORDER_WIDTH_SCALE } from "./borders"; + +/** + * Parse arbitrary outline width/offset value: [8px], [4] + * Returns number for px values, null for unsupported formats + */ +function parseArbitraryOutlineValue(value: string): number | null { + // Match: [8px] or [8] (pixels only) + const pxMatch = value.match(/^\[(\d+)(?:px)?\]$/); + if (pxMatch) { + return parseInt(pxMatch[1], 10); + } + + // Warn about unsupported formats + if (value.startsWith("[") && value.endsWith("]")) { + /* v8 ignore next 5 */ + if (process.env.NODE_ENV !== "production") { + console.warn( + `[react-native-tailwind] Unsupported arbitrary outline value: ${value}. Only px values are supported (e.g., [8px] or [8]).`, + ); + } + return null; + } + + return null; +} + +/** + * Parse outline classes + * @param cls - The class name to parse + * @param customColors - Optional custom colors (passed to parseColor for pattern detection) + */ +export function parseOutline(cls: string, customColors?: Record): StyleObject | null { + // Shorthand: outline (width: 1, style: solid) + if (cls === "outline") { + return { outlineWidth: 1, outlineStyle: "solid" }; + } + + // Outline none + if (cls === "outline-none") { + return { outlineWidth: 0 }; + } + + // Outline style + if (cls === "outline-solid") return { outlineStyle: "solid" }; + if (cls === "outline-dotted") return { outlineStyle: "dotted" }; + if (cls === "outline-dashed") return { outlineStyle: "dashed" }; + + // Outline offset: outline-offset-2, outline-offset-[3px] + if (cls.startsWith("outline-offset-")) { + const valueStr = cls.substring(15); // "outline-offset-".length = 15 + + // Try arbitrary value first + if (valueStr.startsWith("[")) { + const arbitraryValue = parseArbitraryOutlineValue(valueStr); + if (arbitraryValue !== null) { + return { outlineOffset: arbitraryValue }; + } + return null; + } + + // Try preset scale (reuse border width scale for consistency with default Tailwind) + const scaleValue = BORDER_WIDTH_SCALE[valueStr]; + if (scaleValue !== undefined) { + return { outlineOffset: scaleValue }; + } + + return null; + } + + // Outline width: outline-0, outline-2, outline-[5px] + // Must handle potential collision with outline-red-500 (colors) + // Logic: if it matches width pattern, return width. If it looks like color, return null (let parseColor handle it) + + const widthMatch = cls.match(/^outline-(\d+)$/); + if (widthMatch) { + const value = BORDER_WIDTH_SCALE[widthMatch[1]]; + if (value !== undefined) { + return { outlineWidth: value }; + } + } + + const arbMatch = cls.match(/^outline-(\[.+\])$/); + if (arbMatch) { + // Check if it's a color first? No, colors usually look like [#...] or [rgb(...)] + // parseArbitraryOutlineValue only accepts [123] or [123px] + // If it fails, it might be a color, so we return null + const arbitraryValue = parseArbitraryOutlineValue(arbMatch[1]); + if (arbitraryValue !== null) { + return { outlineWidth: arbitraryValue }; + } + return null; + } + + // If it's outline-{color}, we return null here so parseColor (called later in index.ts) + return null; +} From fed09f8ed9238b6c66963127ec1fa7d8db0436b3 Mon Sep 17 00:00:00 2001 From: Olivier Louvignes Date: Sun, 25 Jan 2026 16:33:01 +0100 Subject: [PATCH 2/3] refactor: remove unused customColors parameter from parseOutline --- src/parser/index.ts | 2 +- src/parser/outline.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/parser/index.ts b/src/parser/index.ts index 8599c14..0e89fcc 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -57,7 +57,7 @@ export function parseClass(cls: string, customTheme?: CustomTheme): StyleObject const parsers: Array<(cls: string) => StyleObject | null> = [ (cls: string) => parseSpacing(cls, customTheme?.spacing), (cls: string) => parseBorder(cls, customTheme?.colors), - (cls: string) => parseOutline(cls, customTheme?.colors), + parseOutline, (cls: string) => parseColor(cls, customTheme?.colors), (cls: string) => parseLayout(cls, customTheme?.spacing), (cls: string) => parseTypography(cls, customTheme?.fontFamily, customTheme?.fontSize), diff --git a/src/parser/outline.ts b/src/parser/outline.ts index 2ec21c7..d1e04e7 100644 --- a/src/parser/outline.ts +++ b/src/parser/outline.ts @@ -33,9 +33,8 @@ function parseArbitraryOutlineValue(value: string): number | null { /** * Parse outline classes * @param cls - The class name to parse - * @param customColors - Optional custom colors (passed to parseColor for pattern detection) */ -export function parseOutline(cls: string, customColors?: Record): StyleObject | null { +export function parseOutline(cls: string): StyleObject | null { // Shorthand: outline (width: 1, style: solid) if (cls === "outline") { return { outlineWidth: 1, outlineStyle: "solid" }; @@ -97,6 +96,6 @@ export function parseOutline(cls: string, customColors?: Record) return null; } - // If it's outline-{color}, we return null here so parseColor (called later in index.ts) + // If it's outline-{color}, return null so parseColor (called later in index.ts) handles it return null; } From 8433ac66465f3b9ab0e22f7e9b7a5e219d485a5e Mon Sep 17 00:00:00 2001 From: Olivier Louvignes Date: Sun, 25 Jan 2026 16:33:08 +0100 Subject: [PATCH 3/3] docs: add outlines to sidebar and clean up borders page - Add outlines entry to sidebar navigation - Fix formatting regression in borders.md example - Remove duplicate outline documentation from borders.md - Add link to dedicated outlines page --- docs/astro.config.mjs | 1 + docs/src/content/docs/reference/borders.md | 57 ++-------------------- 2 files changed, 6 insertions(+), 52 deletions(-) diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 5df770e..d6e7a0b 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -55,6 +55,7 @@ export default defineConfig({ { label: "Colors", slug: "reference/colors" }, { label: "Typography", slug: "reference/typography" }, { label: "Borders", slug: "reference/borders" }, + { label: "Outlines", slug: "reference/outlines" }, { label: "Shadows & Elevation", slug: "reference/shadows" }, { label: "Aspect Ratio", slug: "reference/aspect-ratio" }, { label: "Transforms", slug: "reference/transforms" }, diff --git a/docs/src/content/docs/reference/borders.md b/docs/src/content/docs/reference/borders.md index 3aeabf6..9362a1c 100644 --- a/docs/src/content/docs/reference/borders.md +++ b/docs/src/content/docs/reference/borders.md @@ -117,7 +117,10 @@ Apply colors to individual border sides. See the [Colors reference](/react-nativ ### Combining Width and Color ```tsx -// borderLeftWidth: 4 // borderLeftColor: '#3B82F6' + + // borderLeftWidth: 4 + // borderLeftColor: '#3B82F6' + ``` ### Example: Accent Border @@ -164,55 +167,5 @@ Apply colors to individual border sides. See the [Colors reference](/react-nativ ## Related - [Colors](/react-native-tailwind/reference/colors/) - Border color utilities +- [Outlines](/react-native-tailwind/reference/outlines/) - Outline width, style, and offset utilities - [Shadows](/react-native-tailwind/reference/shadows/) - Shadow and elevation - -## Outline - -Utilities for controlling the outline style of an element. - -> **Note**: Outline support requires React Native 0.73+ (New Architecture) and setting the `outline` style property. - -### Outline Width - -```tsx - // outlineWidth: 1, outlineStyle: 'solid' - // outlineWidth: 0 - // outlineWidth: 2 - // outlineWidth: 4 - // outlineWidth: 2 - // outlineWidth: 0 -``` - -### Outline Color - -```tsx - // outlineColor: '#3B82F6' - // outlineColor: '#ff0000' - // outlineColor: '#EF4444' (50% opacity) -``` - -### Outline Style - -```tsx - // outlineStyle: 'solid' - // outlineStyle: 'dashed' - // outlineStyle: 'dotted' -``` - -### Outline Offset - -Utilities for controlling the offset of an element's outline. - -```tsx - // outlineOffset: 0 - // outlineOffset: 1 - // outlineOffset: 2 - // outlineOffset: 4 - // outlineOffset: 3 -``` - -### Example - -```tsx - -```