Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ Important concepts:
- Theme variants are recognized from known theme names.
- Variant tokens (`:active`, `:focus`, `:disabled`, `:where(.theme)`, `:dir()`, `[data-x]`) are read from two selector shapes: nested under the class as `&:active` (Tailwind < 4.3.3) and flattened into the class selector as `.active\:x:active` (Tailwind >= 4.3.3). A selector carrying any token the runtime cannot observe (e.g. `[aria-disabled="true"]`, alone or stacked with a supported variant) is skipped, never applied under a weaker condition.
- Data attribute variants support boolean `data-x` and exact `data-x="value"` matching against component props.
- Media queries drive dimensions, orientation, color scheme, platform, and native/web-specific metadata.
- Media queries drive dimensions, orientation, color scheme, platform, and native/web-specific metadata. Native exclusive width bounds use the generated artifact's `0.01pt` numeric precision to exclude equality, including bounds expressed with viewport-relative units.
- Important declarations are preserved as `importantProperties`.
- Unsupported CSS features may be silently ignored on native. Prefer documenting support coverage over adding noisy runtime failures for every unsupported CSS construct.
- Tailwind composes `filter` from per-utility `--tw-*` variables and relies on `var(--x,)` empty fallbacks for unset parts, so `Var` resolves those to an empty string. Each filter function compiles to `rt.filterFn(name, amount, unit)` because `addMissingSpaces` would otherwise corrupt an inline `blur(${...}px)` template.
Expand Down
18 changes: 16 additions & 2 deletions packages/uniwind/src/bundler/css-processor/mq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type { MediaQuery, QueryFeatureFor_MediaFeatureId } from 'lightningcss'
import type { ProcessorBuilder } from './processor'
import type { MediaQueryResolver } from './types'

const EXCLUSIVE_BOUND_EPSILON = 0.01

export class MQ {
constructor(private readonly Processor: ProcessorBuilder) {}

Expand Down Expand Up @@ -48,13 +50,25 @@ export class MQ {
const { operator, value } = query
const result = this.Processor.CSS.processValue(value)

if (operator === 'greater-than-equal' || operator === 'greater-than') {
if (operator === 'greater-than-equal') {
mq.minWidth = result
}

if (operator === 'less-than-equal' || operator === 'less-than') {
if (operator === 'greater-than') {
mq.minWidth = typeof result === 'number'
? result + EXCLUSIVE_BOUND_EPSILON
: `(${result}) + ${EXCLUSIVE_BOUND_EPSILON}`
}

if (operator === 'less-than-equal') {
mq.maxWidth = result
}

if (operator === 'less-than') {
mq.maxWidth = typeof result === 'number'
? result - EXCLUSIVE_BOUND_EPSILON
: `(${result}) - ${EXCLUSIVE_BOUND_EPSILON}`
}
}

private processPlainMediaQuery(query: QueryFeatureFor_MediaFeatureId & { type: 'plain' }, mq: MediaQueryResolver) {
Expand Down
40 changes: 40 additions & 0 deletions packages/uniwind/tests/native/styles-parsing/media-queries.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { StyleDependency } from '../../../src/common/consts'
import { UniwindListener } from '../../../src/core/listener'
import { UniwindStore } from '../../../src/core/native/store'

const resolveAtWidth = (className: string, width: number) => {
UniwindStore.runtime.screen = { ...UniwindStore.runtime.screen, width }
UniwindListener.notify([StyleDependency.Dimensions])

return UniwindStore.getStyles(
className,
{},
{},
{ scopedTheme: null, rtl: null, variables: null },
).styles
}

describe('media query boundaries', () => {
const originalScreen = UniwindStore.runtime.screen

afterEach(() => {
UniwindStore.runtime.screen = originalScreen
UniwindListener.notify([StyleDependency.Dimensions])
})

test.each([
[389.99, { top: 0, right: 0, bottom: 1, left: 1 }],
[390, { top: 0, right: 1, bottom: 0, left: 1 }],
[390.01, { top: 1, right: 1, bottom: 0, left: 0 }],
])('resolves width %s', (width, expected) => {
expect(resolveAtWidth('media-query-boundaries', width)).toMatchObject(expected)
})

test('resolves custom width media queries', () => {
expect(resolveAtWidth('min-[50vw]:top-[1px]', 390)).toMatchObject({ top: 1 })
expect(resolveAtWidth('min-[300px]:top-[1px]', 299.99)).toEqual({})
expect(resolveAtWidth('min-[300px]:top-[1px]', 300)).toMatchObject({ top: 1 })
expect(resolveAtWidth('max-[500px]:right-[1px]', 499.99)).toMatchObject({ right: 1 })
expect(resolveAtWidth('max-[500px]:right-[1px]', 500)).toEqual({})
})
})
3 changes: 2 additions & 1 deletion packages/uniwind/tests/native/styles-parsing/meta.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { UniwindBundlerConfig } from '../../../src/bundler/config'
import { compileCSS } from '../../../src/bundler/css-compiler'
import { Platform, StyleDependency } from '../../../src/common/consts'
import { StyleSheets } from '../../../src/core/types'
import { SCREEN_HEIGHT, SCREEN_WIDTH } from '../../consts'

type CompiledResult = {
stylesheet: StyleSheets
Expand All @@ -14,7 +15,7 @@ const compileMetadata = async (): Promise<CompiledResult> => {
const virtualCode = await compileCSS(bundlerConfig)

// oxlint-disable-next-line no-unused-vars
const rt = {}
const rt = { screen: { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } }

// oxlint-disable-next-line no-eval
return eval(`(${virtualCode})`)
Expand Down
31 changes: 31 additions & 0 deletions packages/uniwind/tests/test.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,34 @@
color: var(--inline-after);
--inline-after: #123456;
}

.media-query-boundaries {
top: 0;
right: 0;
bottom: 0;
left: 0;
}

@media (width > 390px) {
.media-query-boundaries {
top: 1px;
}
}

@media (width >= 390px) {
.media-query-boundaries {
right: 1px;
}
}

@media (width < 390px) {
.media-query-boundaries {
bottom: 1px;
}
}

@media (width <= 390px) {
.media-query-boundaries {
left: 1px;
}
}
Loading