From 2f2a1a2f224f95ec7ad90199c8fc6bbe45e90f58 Mon Sep 17 00:00:00 2001 From: Bartosz Kaszubowski Date: Fri, 19 Dec 2025 18:06:25 +0100 Subject: [PATCH] feat: support `outline*` style props --- src/UtilityParser.ts | 17 ++++++++ src/__tests__/outline.spec.ts | 43 ++++++++++++++++++ src/__tests__/simple-mappings.spec.ts | 2 + src/resolve/color.ts | 1 + src/resolve/outline.ts | 63 +++++++++++++++++++++++++++ src/styles.ts | 2 + src/tw-config.ts | 4 ++ src/types.ts | 3 +- 8 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/outline.spec.ts create mode 100644 src/resolve/outline.ts diff --git a/src/UtilityParser.ts b/src/UtilityParser.ts index e8f7333..d8f229d 100644 --- a/src/UtilityParser.ts +++ b/src/UtilityParser.ts @@ -27,6 +27,7 @@ import { import pointerEvents from './resolve/pointer-events'; import userSelect from './resolve/user-select'; import textDecorationStyle from './resolve/text-decoration-style'; +import { outlineOffset, outlineStyle, outlineWidth } from './resolve/outline'; export default class UtilityParser { private position = 0; @@ -359,6 +360,22 @@ export default class UtilityParser { if (style) return style; } + if (this.consumePeeked(`outline-`)) { + if (this.consumePeeked(`offset-`)) { + style = outlineOffset(this.rest, this.isNegative, theme?.outlineOffset); + if (style) return style; + } + + style = outlineWidth(this.rest, theme?.outlineWidth); + if (style) return style; + + style = outlineStyle(this.rest); + if (style) return style; + + style = color(`outline`, this.rest, theme?.colors); + if (style) return style; + } + h.warn(`\`${this.isNegative ? `-` : ``}${this.rest}\` unknown or invalid utility`); return null; } diff --git a/src/__tests__/outline.spec.ts b/src/__tests__/outline.spec.ts new file mode 100644 index 0000000..ea626ac --- /dev/null +++ b/src/__tests__/outline.spec.ts @@ -0,0 +1,43 @@ +import { describe, test, expect } from '@jest/globals'; +import { create } from '../'; + +describe(`text-decoration utilities`, () => { + let tw = create(); + beforeEach(() => (tw = create())); + + const cases: Array<[string, Record]> = [ + // outline style + [`outline`, { outlineStyle: `solid` }], + [`outline-dotted`, { outlineStyle: `dotted` }], + [`outline-dashed`, { outlineStyle: `dashed` }], + // outline width + [`outline-4`, { outlineWidth: 4 }], + [`outline-[5px]`, { outlineWidth: 5 }], + // outline offset + [`outline-offset-0`, { outlineOffset: 0 }], + [`outline-offset-[-3px]`, { outlineOffset: -3 }], + // all values mix + [ + `outline outline-1 outline-[#58c4dc] outline-offset-2`, + { + outlineWidth: 1, + outlineStyle: `solid`, + outlineOffset: 2, + outlineColor: `#58c4dc`, + }, + ], + [ + `outline-dashed outline-[6px] outline-offset-[-2px] outline-black/50`, + { + outlineWidth: 6, + outlineStyle: `dashed`, + outlineOffset: -2, + outlineColor: `rgba(0, 0, 0, 0.5)`, + }, + ], + ]; + + test.each(cases)(`tw\`%s\` -> %s`, (utility, expected) => { + expect(tw.style(utility)).toEqual(expected); + }); +}); diff --git a/src/__tests__/simple-mappings.spec.ts b/src/__tests__/simple-mappings.spec.ts index 6fa4614..5f97db6 100644 --- a/src/__tests__/simple-mappings.spec.ts +++ b/src/__tests__/simple-mappings.spec.ts @@ -98,6 +98,8 @@ describe(`simple style mappings`, () => { [`box-border`, { boxSizing: `border-box` }], [`box-content`, { boxSizing: `content-box` }], + [`outline`, { outlineStyle: `solid` }], + // default box-shadow implementations [ `shadow-sm`, diff --git a/src/resolve/color.ts b/src/resolve/color.ts index ca60786..766f887 100644 --- a/src/resolve/color.ts +++ b/src/resolve/color.ts @@ -107,6 +107,7 @@ const STYLE_PROPS = { borderRight: { opacity: `__opacity_border`, color: `borderRightColor` }, shadow: { opacity: `__opacity_shadow`, color: `shadowColor` }, decoration: { opacity: `__opacity_decoration`, color: `textDecorationColor` }, + outline: { opacity: `__opacity_decoration`, color: `outlineColor` }, tint: { opacity: `__opacity_tint`, color: `tintColor` }, }; diff --git a/src/resolve/outline.ts b/src/resolve/outline.ts new file mode 100644 index 0000000..b5d54c9 --- /dev/null +++ b/src/resolve/outline.ts @@ -0,0 +1,63 @@ +import type { StyleIR } from '../types'; +import type { TwTheme } from '../tw-config'; +import { complete, parseStyleVal, parseUnconfigged } from '../helpers'; + +const ALLOWED_STYLE_VALUES = [`dotted`, `dashed`]; +export function outlineStyle(value: string): StyleIR | null { + if (!ALLOWED_STYLE_VALUES.includes(value)) return null; + + return complete({ + outlineStyle: value, + }); +} + +export function outlineWidth( + value: string, + config?: TwTheme['outlineWidth'], +): StyleIR | null { + const configValue = config?.[value]; + + if (configValue) { + const parsedConfigValue = parseStyleVal(configValue); + if (parsedConfigValue !== null) { + return complete({ + outlineWidth: parsedConfigValue, + }); + } + } + + const parsedValue = parseUnconfigged(value); + if (parsedValue !== null) { + return complete({ + outlineWidth: parsedValue, + }); + } + + return null; +} + +export function outlineOffset( + value: string, + isNegative: boolean, + config?: TwTheme['outlineOffset'], +): StyleIR | null { + const configValue = config?.[value]; + + if (configValue) { + const parsedConfigValue = parseStyleVal(configValue); + if (parsedConfigValue !== null) { + return complete({ + outlineOffset: isNegative ? -parsedConfigValue : parsedConfigValue, + }); + } + } + + const parsedValue = parseUnconfigged(value); + if (parsedValue !== null) { + return complete({ + outlineOffset: isNegative ? -parsedValue : parsedValue, + }); + } + + return null; +} diff --git a/src/styles.ts b/src/styles.ts index 59ca438..b36f9a4 100644 --- a/src/styles.ts +++ b/src/styles.ts @@ -121,6 +121,8 @@ const defaultStyles: Array<[string, StyleIR]> = [ [`box-border`, complete({ boxSizing: `border-box` })], [`box-content`, complete({ boxSizing: `content-box` })], + [`outline`, complete({ outlineStyle: `solid` })], + // default box-shadow implementations [ `shadow-sm`, diff --git a/src/tw-config.ts b/src/tw-config.ts index a0a6282..4ca2c32 100644 --- a/src/tw-config.ts +++ b/src/tw-config.ts @@ -43,6 +43,8 @@ export interface TwTheme { skew?: Record; translate?: Record; transformOrigin?: Record; + outlineOffset?: Record; + outlineWidth?: Record; extend?: Omit; // colors?: TwColors; @@ -50,6 +52,7 @@ export interface TwTheme { borderColor?: TwColors; // border- textColor?: TwColors; // text- textDecorationColor?: TwColors; // decoration- + outlineColor?: TwColors; // outline- } export const PREFIX_COLOR_PROP_MAP = { @@ -57,6 +60,7 @@ export const PREFIX_COLOR_PROP_MAP = { 'border-': `borderColor`, 'text-': `textColor`, 'decoration-': `textDecorationColor`, + 'outline-': `outlineColor`, } as const; export interface TwConfig { diff --git a/src/types.ts b/src/types.ts index b42cab4..4e5d706 100644 --- a/src/types.ts +++ b/src/types.ts @@ -95,7 +95,8 @@ export type ColorStyleType = | 'borderBottom' | 'shadow' | 'tint' - | 'decoration'; + | 'decoration' + | 'outline'; export type Direction = | 'All'