Repository navigation
feat: dynamic build time style matching function #716
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import { serialize } from './serialize' | ||
| import type { MediaQueryResolver } from './types' | ||
|
|
||
| const serializeDimension = (dimension: number | string) => typeof dimension === 'number' ? String(dimension) : serialize(dimension) | ||
|
|
||
| export const generateStyleMatcher = (style: MediaQueryResolver) => { | ||
| const conditions: Array<string> = [] | ||
|
|
||
| if (style.minWidthOperator !== null) { | ||
| conditions.push(`rt.screen.width ${style.minWidthOperator} (${serializeDimension(style.minWidth)})`) | ||
| } | ||
|
|
||
| if (style.maxWidthOperator !== null) { | ||
| conditions.push(`rt.screen.width ${style.maxWidthOperator} (${serializeDimension(style.maxWidth)})`) | ||
| } | ||
|
|
||
| if (style.minHeightOperator !== null) { | ||
| conditions.push(`rt.screen.height ${style.minHeightOperator} (${serializeDimension(style.minHeight)})`) | ||
| } | ||
|
|
||
| if (style.maxHeightOperator !== null) { | ||
| conditions.push(`rt.screen.height ${style.maxHeightOperator} (${serializeDimension(style.maxHeight)})`) | ||
| } | ||
|
|
||
| if (style.theme !== null) { | ||
| conditions.push(`(context.scopedTheme ?? rt.currentThemeName) === ${JSON.stringify(style.theme)}`) | ||
| } | ||
|
|
||
| if (style.orientation !== null) { | ||
| conditions.push(`rt.orientation === ${JSON.stringify(style.orientation)}`) | ||
| } | ||
|
|
||
| if (style.rtl !== null) { | ||
| conditions.push(`(context.rtl ?? rt.rtl) === ${style.rtl}`) | ||
| } | ||
|
|
||
| if (style.active !== null) { | ||
| conditions.push(`state?.isPressed === ${style.active}`) | ||
| } | ||
|
|
||
| if (style.focus !== null) { | ||
| conditions.push(`state?.isFocused === ${style.focus}`) | ||
| } | ||
|
|
||
| if (style.disabled !== null) { | ||
| conditions.push(`state?.isDisabled === ${style.disabled}`) | ||
| } | ||
|
|
||
| for (const [attribute, expectedValue] of Object.entries(style.dataAttributes ?? {})) { | ||
| const value = `props?.[${JSON.stringify(attribute)}]` | ||
|
|
||
| if (expectedValue === '"true"' || expectedValue === '"false"') { | ||
| conditions.push(`(${value} === ${expectedValue.slice(1, -1)} || ${value} === ${expectedValue})`) | ||
| } else { | ||
| conditions.push(`${value} === ${expectedValue}`) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The generated data-value comparison inserts the selector value directly into JavaScript source. If the parsed value contains a quote or backslash, it can change the comparison or break the generated module. Escape the value as a JavaScript string before building Knowledge Base Used: CSS compilation and processing |
||
| } | ||
| } | ||
|
|
||
| return `function(rt, props, state, context) { return ${conditions.join(' && ') || 'true'} }` | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,9 @@ | ||
| import type { ColorScheme, Orientation } from '@/common/consts' | ||
| import { Platform } from '@/common/consts' | ||
| import type { MediaQuery, QueryFeatureFor_MediaFeatureId } from 'lightningcss' | ||
| import type { MediaCondition, 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) {} | ||
|
|
||
|
|
@@ -30,44 +28,62 @@ export class MQ { | |
| return | ||
| } | ||
|
|
||
| if (condition?.type !== 'feature') { | ||
| return | ||
| } | ||
|
|
||
| if (condition.value.type === 'range') { | ||
| this.processWidthMediaQuery(condition.value, mq) | ||
| } | ||
|
|
||
| if (condition.value.type === 'plain') { | ||
| this.processPlainMediaQuery(condition.value, mq) | ||
| if (condition) { | ||
| this.processCondition(condition, mq) | ||
| } | ||
| }) | ||
|
|
||
| return mq | ||
| } | ||
|
|
||
| private processWidthMediaQuery(query: QueryFeatureFor_MediaFeatureId & { type: 'range' }, mq: MediaQueryResolver) { | ||
| const { operator, value } = query | ||
| private processCondition(condition: MediaCondition, mq: MediaQueryResolver) { | ||
| if (condition.type === 'operation' && condition.operator === 'and') { | ||
| condition.conditions.forEach(condition => this.processCondition(condition, mq)) | ||
|
Comment on lines
+40
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an Knowledge Base Used: CSS build pipeline |
||
|
|
||
| return | ||
| } | ||
|
|
||
| if (condition.type !== 'feature') { | ||
| return | ||
| } | ||
|
|
||
| if (condition.value.type === 'range') { | ||
| this.processDimensionMediaQuery(condition.value, mq) | ||
| } | ||
|
|
||
| if (condition.value.type === 'plain') { | ||
| this.processPlainMediaQuery(condition.value, mq) | ||
| } | ||
| } | ||
|
|
||
| private processDimensionMediaQuery(query: QueryFeatureFor_MediaFeatureId & { type: 'range' }, mq: MediaQueryResolver) { | ||
| const { name, operator, value } = query | ||
|
|
||
| if (name !== 'width' && name !== 'height') { | ||
| return | ||
| } | ||
|
|
||
| const dimension = name === 'width' ? 'Width' : 'Height' | ||
| const result = this.Processor.CSS.processValue(value) | ||
|
|
||
| if (operator === 'greater-than-equal') { | ||
| mq.minWidth = result | ||
| mq[`min${dimension}`] = result | ||
| mq[`min${dimension}Operator`] = '>=' | ||
| } | ||
|
|
||
| if (operator === 'greater-than') { | ||
| mq.minWidth = typeof result === 'number' | ||
| ? result + EXCLUSIVE_BOUND_EPSILON | ||
| : `(${result}) + ${EXCLUSIVE_BOUND_EPSILON}` | ||
| mq[`min${dimension}`] = result | ||
| mq[`min${dimension}Operator`] = '>' | ||
| } | ||
|
|
||
| if (operator === 'less-than-equal') { | ||
| mq.maxWidth = result | ||
| mq[`max${dimension}`] = result | ||
| mq[`max${dimension}Operator`] = '<=' | ||
| } | ||
|
|
||
| if (operator === 'less-than') { | ||
| mq.maxWidth = typeof result === 'number' | ||
| ? result - EXCLUSIVE_BOUND_EPSILON | ||
| : `(${result}) - ${EXCLUSIVE_BOUND_EPSILON}` | ||
| mq[`max${dimension}`] = result | ||
| mq[`max${dimension}Operator`] = '<' | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -92,6 +108,12 @@ export class MQ { | |
| return { | ||
| minWidth: 0, | ||
| maxWidth: Number.MAX_VALUE, | ||
| minWidthOperator: null, | ||
| maxWidthOperator: null, | ||
| minHeight: 0, | ||
| maxHeight: Number.MAX_VALUE, | ||
| minHeightOperator: null, | ||
| maxHeightOperator: null, | ||
| platform: null, | ||
| rtl: null, | ||
| important: false, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a media bound uses
em,generateStyleMatcherputs avarslookup insidematches. The function receives novarsargument, so resolving a class with that bound throws instead of returning a style. Pass the needed value into the matcher.Knowledge Base Used: CSS compilation and processing