diff --git a/packages/uniwind/src/bundler/artifacts/css/insets.ts b/packages/uniwind/src/bundler/artifacts/css/insets.ts index 873290ad..2137458d 100644 --- a/packages/uniwind/src/bundler/artifacts/css/insets.ts +++ b/packages/uniwind/src/bundler/artifacts/css/insets.ts @@ -1,10 +1,12 @@ const types = ['margin', 'padding', 'inset'] as const const sides = ['inset', 'x', 'y', 'top', 'bottom', 'left', 'right'] as const +const logicalSides = ['start', 'end'] as const const safeAreaTypes = ['safe', 'safe-or-*', 'safe-offset-*'] as const const spacing = '--spacing(--value(integer))' const length = '--value([length], --spacing-*)' type Side = (typeof sides)[number] +type LogicalSide = (typeof logicalSides)[number] type TypeName = (typeof types)[number] type SafeAreaType = (typeof safeAreaTypes)[number] type Inset = 'top' | 'bottom' | 'left' | 'right' @@ -54,6 +56,30 @@ const generateCSSForInsets = () => { return `${typeName}-${inset}` } + const getLogicalUtilityName = (typeName: TypeName, side: LogicalSide, safeAreaType: SafeAreaType) => { + if (typeName === 'inset') { + return `${side}-${safeAreaType}` + } + + return `${typeName.at(0)}${side.at(0)}-${safeAreaType}` + } + + const getLogicalStyleProperty = (typeName: TypeName, side: LogicalSide) => { + if (typeName === 'inset') { + return `inset-inline-${side}` + } + + return `${typeName}-inline-${side}` + } + + const getLogicalInset = (side: LogicalSide, rtl: boolean): Inset => { + if (side === 'start') { + return rtl ? 'right' : 'left' + } + + return rtl ? 'left' : 'right' + } + const getStylesForSafeAreaType = (safeAreaType: SafeAreaType, styles: Array) => { switch (safeAreaType) { case 'safe': @@ -99,6 +125,28 @@ const generateCSSForInsets = () => { ].join('\n') }) }) + + logicalSides.forEach(side => { + const styleProperty = getLogicalStyleProperty(type, side) + const ltrStyles = [`${styleProperty}: env(safe-area-inset-${getLogicalInset(side, false)});`] + const rtlStyles = [`${styleProperty}: env(safe-area-inset-${getLogicalInset(side, true)});`] + + safeAreaTypes.forEach(safeAreaType => { + const utilityName = getLogicalUtilityName(type, side, safeAreaType) + + css += [ + `@utility ${utilityName} {`, + ...getStylesForSafeAreaType(safeAreaType, ltrStyles).map(style => ` ${style}`), + '', + ' @variant rtl {', + ...getStylesForSafeAreaType(safeAreaType, rtlStyles).map(style => ` ${style}`), + ' }', + '}', + '', + '', + ].join('\n') + }) + }) }) // Remove the last newline character diff --git a/packages/uniwind/src/bundler/css-processor/rn.ts b/packages/uniwind/src/bundler/css-processor/rn.ts index 7785eedb..c1b45753 100644 --- a/packages/uniwind/src/bundler/css-processor/rn.ts +++ b/packages/uniwind/src/bundler/css-processor/rn.ts @@ -196,6 +196,20 @@ export class RN { .replace('Block', 'Vertical') } + if (x.includes('inset')) { + if (x === 'insetInlineStart') { + return 'start' + } + + if (x === 'insetInlineEnd') { + return 'end' + } + + return x + .replace('InlineStart', 'Start') + .replace('InlineEnd', 'End') + } + if (x.includes('border')) { return x .replace('InlineStart', 'Start') diff --git a/packages/uniwind/tests/consts.ts b/packages/uniwind/tests/consts.ts index 0ea07585..49586211 100644 --- a/packages/uniwind/tests/consts.ts +++ b/packages/uniwind/tests/consts.ts @@ -8,6 +8,8 @@ export const TW_SPACING = 4 export const SAFE_AREA_INSET_TOP = 21 export const SAFE_AREA_INSET_BOTTOM = 42 +export const SAFE_AREA_INSET_LEFT = 7 +export const SAFE_AREA_INSET_RIGHT = 13 export const SCREEN_WIDTH = 390 export const SCREEN_HEIGHT = 844 diff --git a/packages/uniwind/tests/native/styles-parsing/safe-area.test.tsx b/packages/uniwind/tests/native/styles-parsing/safe-area.test.tsx index 79079bf6..61e9e6be 100644 --- a/packages/uniwind/tests/native/styles-parsing/safe-area.test.tsx +++ b/packages/uniwind/tests/native/styles-parsing/safe-area.test.tsx @@ -1,6 +1,7 @@ import * as React from 'react' +import { LayoutDirection } from '../../../src' import View from '../../../src/components/native/View' -import { SAFE_AREA_INSET_BOTTOM, SAFE_AREA_INSET_TOP, SCREEN_HEIGHT, TW_SPACING } from '../../consts' +import { SAFE_AREA_INSET_BOTTOM, SAFE_AREA_INSET_LEFT, SAFE_AREA_INSET_RIGHT, SAFE_AREA_INSET_TOP, SCREEN_HEIGHT, TW_SPACING } from '../../consts' import { renderUniwind } from '../utils' describe('Safe Area', () => { @@ -18,13 +19,40 @@ describe('Safe Area', () => { expect(getStylesFromId('p-safe').paddingTop).toBe(SAFE_AREA_INSET_TOP) expect(getStylesFromId('p-safe').paddingBottom).toBe(SAFE_AREA_INSET_BOTTOM) + expect(getStylesFromId('p-safe').paddingLeft).toBe(SAFE_AREA_INSET_LEFT) + expect(getStylesFromId('p-safe').paddingRight).toBe(SAFE_AREA_INSET_RIGHT) expect(getStylesFromId('pt-safe').paddingTop).toBe(SAFE_AREA_INSET_TOP + TW_SPACING * 4) expect(getStylesFromId('mb-safe-or-4').marginBottom).toBe(SAFE_AREA_INSET_BOTTOM) expect(getStylesFromId('h-screen-safe').height).toBe(SCREEN_HEIGHT - SAFE_AREA_INSET_TOP - SAFE_AREA_INSET_BOTTOM) expect(getStylesFromId('inset-safe').top).toBe(SAFE_AREA_INSET_TOP + 10) expect(getStylesFromId('inset-safe').bottom).toBe(SAFE_AREA_INSET_BOTTOM + 10) - expect(getStylesFromId('inset-safe').left).toBe(10) - expect(getStylesFromId('inset-safe').right).toBe(10) + expect(getStylesFromId('inset-safe').left).toBe(SAFE_AREA_INSET_LEFT + 10) + expect(getStylesFromId('inset-safe').right).toBe(SAFE_AREA_INSET_RIGHT + 10) expect(getStylesFromId('p-or-test-spacing').paddingTop).toBe(123) }) + + test('logical Safe Area insets follow layout direction', () => { + const { getStylesFromId } = renderUniwind( + + + + + + , + ) + + expect(getStylesFromId('ltr').marginStart).toBe(SAFE_AREA_INSET_LEFT) + expect(getStylesFromId('ltr').marginEnd).toBe(SAFE_AREA_INSET_RIGHT + TW_SPACING * 4) + expect(getStylesFromId('ltr').paddingStart).toBe(TW_SPACING * 2) + expect(getStylesFromId('ltr').paddingEnd).toBe(SAFE_AREA_INSET_RIGHT) + expect(getStylesFromId('ltr').start).toBe(SAFE_AREA_INSET_LEFT) + expect(getStylesFromId('ltr').end).toBe(SAFE_AREA_INSET_RIGHT + TW_SPACING * 4) + + expect(getStylesFromId('rtl').marginStart).toBe(SAFE_AREA_INSET_RIGHT) + expect(getStylesFromId('rtl').marginEnd).toBe(SAFE_AREA_INSET_LEFT + TW_SPACING * 4) + expect(getStylesFromId('rtl').paddingStart).toBe(SAFE_AREA_INSET_RIGHT) + expect(getStylesFromId('rtl').paddingEnd).toBe(SAFE_AREA_INSET_LEFT) + expect(getStylesFromId('rtl').start).toBe(SAFE_AREA_INSET_RIGHT) + expect(getStylesFromId('rtl').end).toBe(SAFE_AREA_INSET_LEFT + TW_SPACING * 4) + }) }) diff --git a/packages/uniwind/tests/setup.native.ts b/packages/uniwind/tests/setup.native.ts index 13de5af8..9c2b92b8 100644 --- a/packages/uniwind/tests/setup.native.ts +++ b/packages/uniwind/tests/setup.native.ts @@ -3,7 +3,7 @@ import { Dimensions } from 'react-native' import { UniwindBundlerConfig } from '../src/bundler/config' import { compileCSS } from '../src/bundler/css-compiler' import { Platform } from '../src/common/consts' -import { SAFE_AREA_INSET_BOTTOM, SAFE_AREA_INSET_TOP, SCREEN_HEIGHT, SCREEN_WIDTH } from './consts' +import { SAFE_AREA_INSET_BOTTOM, SAFE_AREA_INSET_LEFT, SAFE_AREA_INSET_RIGHT, SAFE_AREA_INSET_TOP, SCREEN_HEIGHT, SCREEN_WIDTH } from './consts' jest.spyOn(Dimensions, 'get').mockReturnValue({ width: SCREEN_WIDTH, height: SCREEN_HEIGHT, scale: 1, fontScale: 1 }) @@ -20,9 +20,9 @@ beforeAll(async () => { Uniwind.__reinit(rt => ${virtualCode}, ['light', 'dark']); Uniwind.updateInsets({ top: ${SAFE_AREA_INSET_TOP}, - left: 0, + left: ${SAFE_AREA_INSET_LEFT}, bottom: ${SAFE_AREA_INSET_BOTTOM}, - right: 0, + right: ${SAFE_AREA_INSET_RIGHT}, }); `, ) diff --git a/packages/uniwind/uniwind.css b/packages/uniwind/uniwind.css index 0589cf66..f941697f 100644 --- a/packages/uniwind/uniwind.css +++ b/packages/uniwind/uniwind.css @@ -133,6 +133,62 @@ margin-right: calc(env(safe-area-inset-right) + --value([length], --spacing-*)); } +@utility ms-safe { + margin-inline-start: env(safe-area-inset-left); + + @variant rtl { + margin-inline-start: env(safe-area-inset-right); + } +} + +@utility ms-safe-or-* { + margin-inline-start: max(env(safe-area-inset-left), --spacing(--value(integer))); + margin-inline-start: max(env(safe-area-inset-left), --value([length], --spacing-*)); + + @variant rtl { + margin-inline-start: max(env(safe-area-inset-right), --spacing(--value(integer))); + margin-inline-start: max(env(safe-area-inset-right), --value([length], --spacing-*)); + } +} + +@utility ms-safe-offset-* { + margin-inline-start: calc(env(safe-area-inset-left) + --spacing(--value(integer))); + margin-inline-start: calc(env(safe-area-inset-left) + --value([length], --spacing-*)); + + @variant rtl { + margin-inline-start: calc(env(safe-area-inset-right) + --spacing(--value(integer))); + margin-inline-start: calc(env(safe-area-inset-right) + --value([length], --spacing-*)); + } +} + +@utility me-safe { + margin-inline-end: env(safe-area-inset-right); + + @variant rtl { + margin-inline-end: env(safe-area-inset-left); + } +} + +@utility me-safe-or-* { + margin-inline-end: max(env(safe-area-inset-right), --spacing(--value(integer))); + margin-inline-end: max(env(safe-area-inset-right), --value([length], --spacing-*)); + + @variant rtl { + margin-inline-end: max(env(safe-area-inset-left), --spacing(--value(integer))); + margin-inline-end: max(env(safe-area-inset-left), --value([length], --spacing-*)); + } +} + +@utility me-safe-offset-* { + margin-inline-end: calc(env(safe-area-inset-right) + --spacing(--value(integer))); + margin-inline-end: calc(env(safe-area-inset-right) + --value([length], --spacing-*)); + + @variant rtl { + margin-inline-end: calc(env(safe-area-inset-left) + --spacing(--value(integer))); + margin-inline-end: calc(env(safe-area-inset-left) + --value([length], --spacing-*)); + } +} + @utility p-safe { padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom); @@ -256,6 +312,62 @@ padding-right: calc(env(safe-area-inset-right) + --value([length], --spacing-*)); } +@utility ps-safe { + padding-inline-start: env(safe-area-inset-left); + + @variant rtl { + padding-inline-start: env(safe-area-inset-right); + } +} + +@utility ps-safe-or-* { + padding-inline-start: max(env(safe-area-inset-left), --spacing(--value(integer))); + padding-inline-start: max(env(safe-area-inset-left), --value([length], --spacing-*)); + + @variant rtl { + padding-inline-start: max(env(safe-area-inset-right), --spacing(--value(integer))); + padding-inline-start: max(env(safe-area-inset-right), --value([length], --spacing-*)); + } +} + +@utility ps-safe-offset-* { + padding-inline-start: calc(env(safe-area-inset-left) + --spacing(--value(integer))); + padding-inline-start: calc(env(safe-area-inset-left) + --value([length], --spacing-*)); + + @variant rtl { + padding-inline-start: calc(env(safe-area-inset-right) + --spacing(--value(integer))); + padding-inline-start: calc(env(safe-area-inset-right) + --value([length], --spacing-*)); + } +} + +@utility pe-safe { + padding-inline-end: env(safe-area-inset-right); + + @variant rtl { + padding-inline-end: env(safe-area-inset-left); + } +} + +@utility pe-safe-or-* { + padding-inline-end: max(env(safe-area-inset-right), --spacing(--value(integer))); + padding-inline-end: max(env(safe-area-inset-right), --value([length], --spacing-*)); + + @variant rtl { + padding-inline-end: max(env(safe-area-inset-left), --spacing(--value(integer))); + padding-inline-end: max(env(safe-area-inset-left), --value([length], --spacing-*)); + } +} + +@utility pe-safe-offset-* { + padding-inline-end: calc(env(safe-area-inset-right) + --spacing(--value(integer))); + padding-inline-end: calc(env(safe-area-inset-right) + --value([length], --spacing-*)); + + @variant rtl { + padding-inline-end: calc(env(safe-area-inset-left) + --spacing(--value(integer))); + padding-inline-end: calc(env(safe-area-inset-left) + --value([length], --spacing-*)); + } +} + @utility inset-safe { top: env(safe-area-inset-top); bottom: env(safe-area-inset-bottom); @@ -379,6 +491,62 @@ right: calc(env(safe-area-inset-right) + --value([length], --spacing-*)); } +@utility start-safe { + inset-inline-start: env(safe-area-inset-left); + + @variant rtl { + inset-inline-start: env(safe-area-inset-right); + } +} + +@utility start-safe-or-* { + inset-inline-start: max(env(safe-area-inset-left), --spacing(--value(integer))); + inset-inline-start: max(env(safe-area-inset-left), --value([length], --spacing-*)); + + @variant rtl { + inset-inline-start: max(env(safe-area-inset-right), --spacing(--value(integer))); + inset-inline-start: max(env(safe-area-inset-right), --value([length], --spacing-*)); + } +} + +@utility start-safe-offset-* { + inset-inline-start: calc(env(safe-area-inset-left) + --spacing(--value(integer))); + inset-inline-start: calc(env(safe-area-inset-left) + --value([length], --spacing-*)); + + @variant rtl { + inset-inline-start: calc(env(safe-area-inset-right) + --spacing(--value(integer))); + inset-inline-start: calc(env(safe-area-inset-right) + --value([length], --spacing-*)); + } +} + +@utility end-safe { + inset-inline-end: env(safe-area-inset-right); + + @variant rtl { + inset-inline-end: env(safe-area-inset-left); + } +} + +@utility end-safe-or-* { + inset-inline-end: max(env(safe-area-inset-right), --spacing(--value(integer))); + inset-inline-end: max(env(safe-area-inset-right), --value([length], --spacing-*)); + + @variant rtl { + inset-inline-end: max(env(safe-area-inset-left), --spacing(--value(integer))); + inset-inline-end: max(env(safe-area-inset-left), --value([length], --spacing-*)); + } +} + +@utility end-safe-offset-* { + inset-inline-end: calc(env(safe-area-inset-right) + --spacing(--value(integer))); + inset-inline-end: calc(env(safe-area-inset-right) + --value([length], --spacing-*)); + + @variant rtl { + inset-inline-end: calc(env(safe-area-inset-left) + --spacing(--value(integer))); + inset-inline-end: calc(env(safe-area-inset-left) + --value([length], --spacing-*)); + } +} + @custom-variant disabled { &:disabled { @slot; diff --git a/skills/uniwind/references/variants-and-selectors.md b/skills/uniwind/references/variants-and-selectors.md index 3a61b0b5..f84e98d0 100644 --- a/skills/uniwind/references/variants-and-selectors.md +++ b/skills/uniwind/references/variants-and-selectors.md @@ -169,6 +169,7 @@ Usage: `xs:p-2 tablet:p-4 3xl:p-8` |-------|-------------| | `p-safe` | All sides | | `pt-safe` / `pb-safe` / `pl-safe` / `pr-safe` | Individual sides | +| `ps-safe` / `pe-safe` | Logical start / end | | `px-safe` / `py-safe` | Horizontal / vertical | ### Margin @@ -177,6 +178,7 @@ Usage: `xs:p-2 tablet:p-4 3xl:p-8` |-------|-------------| | `m-safe` | All sides | | `mt-safe` / `mb-safe` / `ml-safe` / `mr-safe` | Individual sides | +| `ms-safe` / `me-safe` | Logical start / end | | `mx-safe` / `my-safe` | Horizontal / vertical | ### Positioning @@ -185,6 +187,7 @@ Usage: `xs:p-2 tablet:p-4 3xl:p-8` |-------|-------------| | `inset-safe` | All sides | | `top-safe` / `bottom-safe` / `left-safe` / `right-safe` | Individual sides | +| `start-safe` / `end-safe` | Logical start / end | | `x-safe` / `y-safe` | Horizontal / vertical inset | ### Compound Variants