@wix/zero-config-implementation 1.105.0 → 1.106.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +29 -0
- package/dist/index.js +7899 -7746
- package/package.json +2 -2
- package/src/converters/states-default-values.test.ts +308 -0
- package/src/converters/to-editor-component.ts +225 -66
- package/src/information-extractors/css/parse.test.ts +49 -0
- package/src/information-extractors/css/parse.ts +113 -8
- package/src/information-extractors/css/selector-matcher.ts +83 -28
- package/src/information-extractors/css/types.ts +29 -0
- package/src/manifest-pipeline.ts +8 -1
|
@@ -17,7 +17,7 @@ import { buildRefElementManifestKey } from '../extensions/ref-elements/path-util
|
|
|
17
17
|
import type { RefElementMatch } from '../extensions/ref-elements/types'
|
|
18
18
|
import type { ComponentInfoWithCss } from '../index'
|
|
19
19
|
import { cssPropertyTypeFromUsages, inferCssDataType, stripVarFallback } from '../information-extractors/css/parse'
|
|
20
|
-
import type { MatchedCssData, RegisteredCustomProperty } from '../information-extractors/css/types'
|
|
20
|
+
import type { CssSelectorMatch, MatchedCssData, RegisteredCustomProperty } from '../information-extractors/css/types'
|
|
21
21
|
import type {
|
|
22
22
|
CoupledComponentInfo,
|
|
23
23
|
CoupledProp,
|
|
@@ -78,15 +78,59 @@ function collectStateClassTriggers(component: ComponentInfoWithCss): StateClassT
|
|
|
78
78
|
const EXCLUDED_PROPS = new Set(['id', 'className', 'elementProps', 'wix'])
|
|
79
79
|
|
|
80
80
|
export function toEditorReactComponent(component: ComponentInfoWithCss): EditorReactComponent {
|
|
81
|
-
const
|
|
81
|
+
const statesByTraceId = buildStatesByTraceId(component)
|
|
82
|
+
const nearestCommonAncestorCustomProps = buildNearestCommonAncestorCustomProps(component, statesByTraceId)
|
|
82
83
|
return {
|
|
83
|
-
editorElement: buildEditorElement(component, nearestCommonAncestorCustomProps),
|
|
84
|
+
editorElement: buildEditorElement(component, nearestCommonAncestorCustomProps, statesByTraceId),
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
/** The `states` block of every emitted element, keyed by traceId. */
|
|
89
|
+
type StatesByTraceId = Map<string, States>
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Builds every element's `states` block up front, so CSS defaults can be split per state
|
|
93
|
+
* before the element entries are assembled. Native states need a semantic block name to
|
|
94
|
+
* synthesize `<block>--<state>`; custom class triggers don't (they carry the className
|
|
95
|
+
* verbatim and match the element directly), so they're built even when the element
|
|
96
|
+
* resolves no semantic class. Prop triggers (`ElementState<>`) are component-level and
|
|
97
|
+
* attach to the root only. Ref elements (and their subtrees) emit no states.
|
|
98
|
+
*/
|
|
99
|
+
function buildStatesByTraceId(component: ComponentInfoWithCss): StatesByTraceId {
|
|
100
|
+
const statesByTraceId: StatesByTraceId = new Map()
|
|
101
|
+
const rootElement = component.elements[0]
|
|
102
|
+
if (!rootElement) return statesByTraceId
|
|
103
|
+
|
|
104
|
+
const stateClassTriggers = collectStateClassTriggers(component)
|
|
105
|
+
const rootPropTriggers = Object.values(component.props).filter((prop) => prop.isStateTrigger)
|
|
106
|
+
|
|
107
|
+
const visit = (element: ExtractedElement, isRootElement: boolean): void => {
|
|
108
|
+
if (!isRootElement && element.extractorData.get('ref-element')) return
|
|
109
|
+
const semanticClass = getSemanticBlockName(element)
|
|
110
|
+
const states = mergeStates(
|
|
111
|
+
semanticClass ? buildStatesBlock(element, semanticClass, stateClassTriggers.focus) : undefined,
|
|
112
|
+
buildCustomStatesBlock({
|
|
113
|
+
element,
|
|
114
|
+
propTriggers: isRootElement ? rootPropTriggers : [],
|
|
115
|
+
classTriggers: stateClassTriggers.custom,
|
|
116
|
+
}),
|
|
117
|
+
)
|
|
118
|
+
if (states) statesByTraceId.set(element.traceId, states)
|
|
119
|
+
for (const child of element.children) visit(child, false)
|
|
120
|
+
}
|
|
121
|
+
visit(rootElement, true)
|
|
122
|
+
|
|
123
|
+
return statesByTraceId
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function stateNamesOf(statesByTraceId: StatesByTraceId, traceId: string): string[] {
|
|
127
|
+
return Object.keys(statesByTraceId.get(traceId) ?? {})
|
|
128
|
+
}
|
|
129
|
+
|
|
87
130
|
function buildEditorElement(
|
|
88
131
|
component: ComponentInfoWithCss,
|
|
89
132
|
nearestCommonAncestorCustomProps: Map<string, Record<string, CssCustomPropertyItem>>,
|
|
133
|
+
statesByTraceId: StatesByTraceId,
|
|
90
134
|
): EditorElement {
|
|
91
135
|
const rootElement = component.elements[0]
|
|
92
136
|
if (!rootElement) {
|
|
@@ -98,23 +142,9 @@ function buildEditorElement(
|
|
|
98
142
|
const childElements = rootElement.children
|
|
99
143
|
const rootCustomProps = nearestCommonAncestorCustomProps.get(rootElement.traceId) ?? {}
|
|
100
144
|
|
|
101
|
-
const stateClassTriggers = collectStateClassTriggers(component)
|
|
102
|
-
|
|
103
|
-
// Native states need a semantic block name to synthesize `<block>--<state>`; custom
|
|
104
|
-
// class triggers don't (they carry the className verbatim and match the element
|
|
105
|
-
// directly), so they're built even when the element resolves no semantic class.
|
|
106
145
|
const rootSemanticClass = getSemanticBlockName(rootElement)
|
|
107
146
|
const rootSemanticClasses = rootSemanticClass ? [rootSemanticClass] : []
|
|
108
|
-
const rootStates = rootElement
|
|
109
|
-
? mergeStates(
|
|
110
|
-
rootSemanticClass ? buildStatesBlock(rootElement, rootSemanticClass, stateClassTriggers.focus) : undefined,
|
|
111
|
-
buildCustomStatesBlock({
|
|
112
|
-
element: rootElement,
|
|
113
|
-
propTriggers: Object.values(component.props).filter((prop) => prop.isStateTrigger),
|
|
114
|
-
classTriggers: stateClassTriggers.custom,
|
|
115
|
-
}),
|
|
116
|
-
)
|
|
117
|
-
: undefined
|
|
147
|
+
const rootStates = statesByTraceId.get(rootElement.traceId)
|
|
118
148
|
|
|
119
149
|
const displayGroups = buildActiveItemDisplayGroups(component.props)
|
|
120
150
|
const displayFilters = buildActiveItemIndexDisplayFilters(component.props)
|
|
@@ -126,12 +156,12 @@ function buildEditorElement(
|
|
|
126
156
|
elements: buildElements(
|
|
127
157
|
childElements,
|
|
128
158
|
nearestCommonAncestorCustomProps,
|
|
129
|
-
|
|
159
|
+
statesByTraceId,
|
|
130
160
|
rootSemanticClasses,
|
|
131
161
|
component.innerElementProps,
|
|
132
162
|
component.propUsages,
|
|
133
163
|
),
|
|
134
|
-
cssProperties: buildCssProperties(rootElement, true),
|
|
164
|
+
cssProperties: buildCssProperties(rootElement, true, stateNamesOf(statesByTraceId, rootElement.traceId)),
|
|
135
165
|
cssCustomProperties: rootCustomProps,
|
|
136
166
|
...(rootStates && { states: rootStates }),
|
|
137
167
|
...(displayGroups && { displayGroups }),
|
|
@@ -202,7 +232,7 @@ function buildData(
|
|
|
202
232
|
function buildElements(
|
|
203
233
|
elements: ExtractedElement[],
|
|
204
234
|
nearestCommonAncestorCustomProps: Map<string, Record<string, CssCustomPropertyItem>>,
|
|
205
|
-
|
|
235
|
+
statesByTraceId: StatesByTraceId,
|
|
206
236
|
ancestorSemanticClasses: string[],
|
|
207
237
|
innerElementProps?: CoupledComponentInfo['innerElementProps'],
|
|
208
238
|
propUsages?: TrackingStores['propUsages'],
|
|
@@ -228,15 +258,9 @@ function buildElements(
|
|
|
228
258
|
const semanticClass = getSemanticBlockName(element)
|
|
229
259
|
|
|
230
260
|
const data = innerEntry && propUsages ? buildData(innerEntry.props, propUsages) : undefined
|
|
231
|
-
const cssProps = buildCssProperties(element, false)
|
|
261
|
+
const cssProps = buildCssProperties(element, false, stateNamesOf(statesByTraceId, element.traceId))
|
|
232
262
|
const cssCustomProps = nearestCommonAncestorCustomProps.get(element.traceId) ?? {}
|
|
233
|
-
|
|
234
|
-
// Inner-element custom states come from custom-class triggers only; prop
|
|
235
|
-
// triggers (`ElementState<>`) are component-level and attach to the root.
|
|
236
|
-
const states = mergeStates(
|
|
237
|
-
semanticClass ? buildStatesBlock(element, semanticClass, stateClassTriggers.focus) : undefined,
|
|
238
|
-
buildCustomStatesBlock({ element, propTriggers: [], classTriggers: stateClassTriggers.custom }),
|
|
239
|
-
)
|
|
263
|
+
const states = statesByTraceId.get(element.traceId)
|
|
240
264
|
|
|
241
265
|
result[element.name] = {
|
|
242
266
|
elementType: ELEMENTS.ELEMENT_TYPE.inlineElement,
|
|
@@ -257,7 +281,7 @@ function buildElements(
|
|
|
257
281
|
? buildElements(
|
|
258
282
|
element.children,
|
|
259
283
|
nearestCommonAncestorCustomProps,
|
|
260
|
-
|
|
284
|
+
statesByTraceId,
|
|
261
285
|
semanticClass ? [...ancestorSemanticClasses, semanticClass] : ancestorSemanticClasses,
|
|
262
286
|
innerElementProps,
|
|
263
287
|
propUsages,
|
|
@@ -283,8 +307,10 @@ function buildElements(
|
|
|
283
307
|
*/
|
|
284
308
|
function buildNearestCommonAncestorCustomProps(
|
|
285
309
|
component: ComponentInfoWithCss,
|
|
310
|
+
statesByTraceId: StatesByTraceId,
|
|
286
311
|
): Map<string, Record<string, CssCustomPropertyItem>> {
|
|
287
312
|
const parentMap = buildParentMap(component.elements)
|
|
313
|
+
const elementsByTraceId = buildElementsByTraceId(component.elements)
|
|
288
314
|
const rootTraceId = component.elements[0]?.traceId
|
|
289
315
|
|
|
290
316
|
// Custom properties registered via the standard @property at-rule — the declared
|
|
@@ -307,7 +333,13 @@ function buildNearestCommonAncestorCustomProps(
|
|
|
307
333
|
|
|
308
334
|
// Emit every declared variable plus every @property registration — a registered var
|
|
309
335
|
// may have no element-level declaration yet still needs a typed control.
|
|
310
|
-
|
|
336
|
+
// Variables declared only under a state (`.root:hover { --x: … }`) have no regular default
|
|
337
|
+
// but still get a control on their declaring element, carrying the state default.
|
|
338
|
+
const varNames = new Set<string>([
|
|
339
|
+
...allCustomPropertyValues.keys(),
|
|
340
|
+
...registeredByVar.keys(),
|
|
341
|
+
...collectStateDeclaredVarNames(component.elements, statesByTraceId),
|
|
342
|
+
])
|
|
311
343
|
|
|
312
344
|
const nearestCommonAncestorCustomProps = new Map<string, Record<string, CssCustomPropertyItem>>()
|
|
313
345
|
|
|
@@ -320,7 +352,7 @@ function buildNearestCommonAncestorCustomProps(
|
|
|
320
352
|
const defaultValue = allCustomPropertyValues.get(varName) ?? registration?.defaultValue
|
|
321
353
|
|
|
322
354
|
// Declaration wins over usage; multiple declaring elements resolve to their NCA.
|
|
323
|
-
const definingTraceIds = findDefiningElements(component.elements, varName)
|
|
355
|
+
const definingTraceIds = findDefiningElements(component.elements, varName, statesByTraceId)
|
|
324
356
|
|
|
325
357
|
let nearestCommonAncestorTraceId: string | undefined
|
|
326
358
|
if (definingTraceIds.size > 0) {
|
|
@@ -342,10 +374,18 @@ function buildNearestCommonAncestorCustomProps(
|
|
|
342
374
|
|
|
343
375
|
if (!nearestCommonAncestorTraceId) continue
|
|
344
376
|
|
|
345
|
-
|
|
377
|
+
const statesDefaultValues = buildCustomPropertyStatesDefaultValues(
|
|
378
|
+
elementsByTraceId.get(nearestCommonAncestorTraceId),
|
|
379
|
+
stateNamesOf(statesByTraceId, nearestCommonAncestorTraceId),
|
|
380
|
+
varName,
|
|
381
|
+
defaultValue,
|
|
382
|
+
)
|
|
383
|
+
// A @property registration is the top-priority type source; otherwise infer from usage,
|
|
384
|
+
// then from the value — a state value when the variable has no regular declaration.
|
|
385
|
+
const valueForTypeInference = defaultValue ?? Object.values(statesDefaultValues ?? {})[0] ?? ''
|
|
346
386
|
const cssPropertyType = registration
|
|
347
387
|
? registration.cssPropertyType
|
|
348
|
-
: getVarPropertyTypeFromCssInfos(varName,
|
|
388
|
+
: getVarPropertyTypeFromCssInfos(varName, valueForTypeInference, component.css)
|
|
349
389
|
const customEnum = buildCustomEnum(registration)
|
|
350
390
|
const numberLimits = buildCssNumber(registration, cssPropertyType)
|
|
351
391
|
const cleanVarName = varName.startsWith('--') ? varName.slice(2) : varName
|
|
@@ -354,6 +394,7 @@ function buildNearestCommonAncestorCustomProps(
|
|
|
354
394
|
existingProps[cleanVarName] = {
|
|
355
395
|
displayName: formatDisplayName(cleanVarName),
|
|
356
396
|
...(defaultValue !== undefined && { defaultValue }),
|
|
397
|
+
...(statesDefaultValues && { statesDefaultValues }),
|
|
357
398
|
...(cssPropertyType !== undefined && { cssPropertyType }),
|
|
358
399
|
...(customEnum && { customEnum }),
|
|
359
400
|
...(numberLimits && { number: numberLimits }),
|
|
@@ -364,6 +405,65 @@ function buildNearestCommonAncestorCustomProps(
|
|
|
364
405
|
return nearestCommonAncestorCustomProps
|
|
365
406
|
}
|
|
366
407
|
|
|
408
|
+
/**
|
|
409
|
+
* Reads the per-state values a variable is declared with on its placement element. Only the
|
|
410
|
+
* placement element's own state declarations count: a value declared under `:hover` on a
|
|
411
|
+
* descendant belongs to that descendant's state, not to the ancestor the variable was placed on.
|
|
412
|
+
* Values equal to the regular default are omitted.
|
|
413
|
+
*/
|
|
414
|
+
function buildCustomPropertyStatesDefaultValues(
|
|
415
|
+
placementElement: ExtractedElement | undefined,
|
|
416
|
+
stateNames: string[],
|
|
417
|
+
varName: string,
|
|
418
|
+
defaultValue: string | undefined,
|
|
419
|
+
): Record<string, string> | undefined {
|
|
420
|
+
const matcherData = placementElement?.extractorData.get('css-matcher') as MatchedCssData | undefined
|
|
421
|
+
const statesDefaultValues: Record<string, string> = {}
|
|
422
|
+
for (const stateName of stateNames) {
|
|
423
|
+
const stateValue = matcherData?.stateCustomProperties?.[stateName]?.[varName]
|
|
424
|
+
if (stateValue !== undefined && stateValue !== defaultValue) statesDefaultValues[stateName] = stateValue
|
|
425
|
+
}
|
|
426
|
+
return Object.keys(statesDefaultValues).length > 0 ? statesDefaultValues : undefined
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* The custom properties an element declares under each state it exposes. Declarations under a
|
|
431
|
+
* CSS state the element has no `states` entry for are left out — there is nowhere to attach them.
|
|
432
|
+
*/
|
|
433
|
+
function exposedStateCustomProperties(
|
|
434
|
+
element: ExtractedElement,
|
|
435
|
+
statesByTraceId: StatesByTraceId,
|
|
436
|
+
): Record<string, string>[] {
|
|
437
|
+
const matcherData = element.extractorData.get('css-matcher') as MatchedCssData | undefined
|
|
438
|
+
return stateNamesOf(statesByTraceId, element.traceId).flatMap((stateName) => {
|
|
439
|
+
const stateCustomProperties = matcherData?.stateCustomProperties?.[stateName]
|
|
440
|
+
return stateCustomProperties ? [stateCustomProperties] : []
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** Collects every variable declared under an exposed state anywhere in the element tree. */
|
|
445
|
+
function collectStateDeclaredVarNames(elements: ExtractedElement[], statesByTraceId: StatesByTraceId): Set<string> {
|
|
446
|
+
const varNames = new Set<string>()
|
|
447
|
+
for (const element of elements) {
|
|
448
|
+
for (const stateCustomProperties of exposedStateCustomProperties(element, statesByTraceId)) {
|
|
449
|
+
for (const varName of Object.keys(stateCustomProperties)) varNames.add(varName)
|
|
450
|
+
}
|
|
451
|
+
for (const varName of collectStateDeclaredVarNames(element.children, statesByTraceId)) varNames.add(varName)
|
|
452
|
+
}
|
|
453
|
+
return varNames
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function buildElementsByTraceId(elements: ExtractedElement[]): Map<string, ExtractedElement> {
|
|
457
|
+
const elementsByTraceId = new Map<string, ExtractedElement>()
|
|
458
|
+
for (const element of elements) {
|
|
459
|
+
elementsByTraceId.set(element.traceId, element)
|
|
460
|
+
for (const [traceId, descendant] of buildElementsByTraceId(element.children)) {
|
|
461
|
+
elementsByTraceId.set(traceId, descendant)
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return elementsByTraceId
|
|
465
|
+
}
|
|
466
|
+
|
|
367
467
|
/**
|
|
368
468
|
* Merges @property registrations across all of the component's CSS files, keyed by the
|
|
369
469
|
* `--`-prefixed variable name. The first file to register a variable wins.
|
|
@@ -516,14 +616,24 @@ function findNearestCommonAncestor(traceIds: Set<string>, parentMap: Map<string,
|
|
|
516
616
|
}
|
|
517
617
|
|
|
518
618
|
/**
|
|
519
|
-
* Finds every surviving element whose matched CSS declares the given custom property
|
|
619
|
+
* Finds every surviving element whose matched CSS declares the given custom property,
|
|
620
|
+
* in its regular appearance or in any state it exposes.
|
|
520
621
|
*/
|
|
521
|
-
function findDefiningElements(
|
|
622
|
+
function findDefiningElements(
|
|
623
|
+
elements: ExtractedElement[],
|
|
624
|
+
varName: string,
|
|
625
|
+
statesByTraceId: StatesByTraceId,
|
|
626
|
+
): Set<string> {
|
|
522
627
|
const definingTraceIds = new Set<string>()
|
|
523
628
|
for (const element of elements) {
|
|
524
629
|
const matcherData = element.extractorData.get('css-matcher') as MatchedCssData | undefined
|
|
525
|
-
|
|
526
|
-
|
|
630
|
+
const declaresInState = exposedStateCustomProperties(element, statesByTraceId).some(
|
|
631
|
+
(stateCustomProperties) => stateCustomProperties[varName] !== undefined,
|
|
632
|
+
)
|
|
633
|
+
if (matcherData?.customProperties[varName] !== undefined || declaresInState) definingTraceIds.add(element.traceId)
|
|
634
|
+
for (const traceId of findDefiningElements(element.children, varName, statesByTraceId)) {
|
|
635
|
+
definingTraceIds.add(traceId)
|
|
636
|
+
}
|
|
527
637
|
}
|
|
528
638
|
return definingTraceIds
|
|
529
639
|
}
|
|
@@ -533,22 +643,39 @@ function findDefiningElements(elements: ExtractedElement[], varName: string): Se
|
|
|
533
643
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
534
644
|
|
|
535
645
|
/**
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
* When multiple rules match (e.g. .foo and .foo.dark), later match wins for same property.
|
|
646
|
+
* The CSS property values (camelCased names) declared by one layer of matches — the regular
|
|
647
|
+
* matches or one state's matches — split by whether each declaration sits inside an at-rule.
|
|
539
648
|
*/
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
649
|
+
interface LayerValues {
|
|
650
|
+
topLevel: Map<string, string>
|
|
651
|
+
inAtRule: Map<string, string>
|
|
652
|
+
}
|
|
544
653
|
|
|
545
|
-
|
|
546
|
-
|
|
654
|
+
/**
|
|
655
|
+
* Collects one layer's property values. When multiple rules match (e.g. .foo and .foo.dark),
|
|
656
|
+
* later match wins for same property within each half of the layer.
|
|
657
|
+
*/
|
|
658
|
+
function collectPropertyValues(matches: CssSelectorMatch[] | undefined): LayerValues {
|
|
659
|
+
const layerValues: LayerValues = { topLevel: new Map(), inAtRule: new Map() }
|
|
660
|
+
for (const match of matches ?? []) {
|
|
547
661
|
for (const prop of match.properties) {
|
|
548
|
-
|
|
662
|
+
const targetValues = prop.inAtRule ? layerValues.inAtRule : layerValues.topLevel
|
|
663
|
+
targetValues.set(camelCase(prop.name), prop.value)
|
|
549
664
|
}
|
|
550
665
|
}
|
|
551
|
-
return
|
|
666
|
+
return layerValues
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Resolves a manifest default from one layer, ignoring any `var()` fallback. Top-level
|
|
671
|
+
* declarations are resolved first, so a conditional at-rule (`@media`) value — even a longhand
|
|
672
|
+
* under a top-level shorthand — only fills in what the top level leaves undeclared: the
|
|
673
|
+
* manifest default describes the unconditional appearance.
|
|
674
|
+
*/
|
|
675
|
+
function resolveLayerDefault(propName: string, layerValues: LayerValues): string | undefined {
|
|
676
|
+
const resolvedValue =
|
|
677
|
+
resolveLonghand(propName, layerValues.topLevel) ?? resolveLonghand(propName, layerValues.inAtRule)
|
|
678
|
+
return resolvedValue !== undefined ? stripVarFallback(resolvedValue) : undefined
|
|
552
679
|
}
|
|
553
680
|
|
|
554
681
|
const { CSS_PROPERTY_TYPE, DISPLAY_VALUE } = CSS_PROPERTIES
|
|
@@ -594,6 +721,7 @@ const ROOT_EXCLUDED_CSS_PROPERTIES = new Set<string>([
|
|
|
594
721
|
function buildCssProperties(
|
|
595
722
|
element: ExtractedElement | undefined,
|
|
596
723
|
isRootElement: boolean,
|
|
724
|
+
stateNames: string[],
|
|
597
725
|
): Record<string, CssPropertyItem> {
|
|
598
726
|
const result: Record<string, CssPropertyItem> = {}
|
|
599
727
|
|
|
@@ -602,47 +730,78 @@ function buildCssProperties(
|
|
|
602
730
|
if (!decidedProperties || decidedProperties.length === 0) {
|
|
603
731
|
return result
|
|
604
732
|
}
|
|
605
|
-
const
|
|
733
|
+
const matcherData = element?.extractorData.get('css-matcher') as MatchedCssData | undefined
|
|
734
|
+
const cssPropertyValues = collectPropertyValues(matcherData?.matches)
|
|
735
|
+
// Each state layer is resolved on its own — merging it over the regular values would let
|
|
736
|
+
// a regular longhand (`padding-top`) beat a state shorthand (`padding`).
|
|
737
|
+
const stateLayers = stateNames.flatMap((stateName) => {
|
|
738
|
+
const stateMatches = matcherData?.stateMatches?.[stateName]
|
|
739
|
+
return stateMatches ? [{ stateName, values: collectPropertyValues(stateMatches) }] : []
|
|
740
|
+
})
|
|
606
741
|
for (const propName of decidedProperties) {
|
|
607
742
|
if (isRootElement && ROOT_EXCLUDED_CSS_PROPERTIES.has(propName)) {
|
|
608
743
|
continue
|
|
609
744
|
}
|
|
610
745
|
if (propName === CSS_PROPERTY_TYPE.display && element) {
|
|
611
746
|
if (!isRootElement) {
|
|
612
|
-
result[propName] = buildDisplayProperty(element)
|
|
747
|
+
result[propName] = buildDisplayProperty(element, stateNames)
|
|
613
748
|
}
|
|
614
749
|
continue
|
|
615
750
|
}
|
|
616
|
-
const
|
|
617
|
-
const
|
|
751
|
+
const defaultValue = resolveLayerDefault(propName, cssPropertyValues)
|
|
752
|
+
const statesDefaultValues: Record<string, string> = {}
|
|
753
|
+
for (const stateLayer of stateLayers) {
|
|
754
|
+
const stateDefaultValue = resolveLayerDefault(propName, stateLayer.values)
|
|
755
|
+
if (stateDefaultValue !== undefined && stateDefaultValue !== defaultValue) {
|
|
756
|
+
statesDefaultValues[stateLayer.stateName] = stateDefaultValue
|
|
757
|
+
}
|
|
758
|
+
}
|
|
618
759
|
result[propName] = {
|
|
619
760
|
...(defaultValue !== undefined && { defaultValue }),
|
|
761
|
+
...(Object.keys(statesDefaultValues).length > 0 && { statesDefaultValues }),
|
|
620
762
|
}
|
|
621
763
|
}
|
|
622
764
|
|
|
623
765
|
return result
|
|
624
766
|
}
|
|
625
767
|
|
|
626
|
-
function buildDisplayProperty(element: ExtractedElement): CssPropertyItem {
|
|
768
|
+
function buildDisplayProperty(element: ExtractedElement, stateNames: string[]): CssPropertyItem {
|
|
627
769
|
const matcherData = element.extractorData.get('css-matcher') as MatchedCssData | undefined
|
|
628
770
|
const resolvedFromCss = matcherData ? resolveDisplayValue(matcherData) : undefined
|
|
629
771
|
const hasExplicitDisplayDeclaration =
|
|
630
772
|
matcherData?.matches.some((match) => match.properties.some((property) => property.name === 'display')) ?? false
|
|
631
773
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
774
|
+
// An explicit but unresolvable regular display leaves only `none` as a known option.
|
|
775
|
+
const isRegularDisplayUnresolved = hasExplicitDisplayDeclaration && resolvedFromCss === undefined
|
|
776
|
+
const currentEnumValue = isRegularDisplayUnresolved
|
|
777
|
+
? undefined
|
|
778
|
+
: toDisplayEnumValue(resolvedFromCss ?? getDefaultDisplayForTag(element.tag))
|
|
779
|
+
const displayValues: DisplayEnumValue[] = currentEnumValue
|
|
780
|
+
? [DISPLAY_VALUE.none, currentEnumValue]
|
|
781
|
+
: [DISPLAY_VALUE.none]
|
|
782
|
+
|
|
783
|
+
// Each state's effective display becomes its state default, and is offered as an option too.
|
|
784
|
+
// The state is resolved over the regular matches with its custom properties merged in, so a
|
|
785
|
+
// state that only changes the variable a regular `display: var(--layout)` reads still counts;
|
|
786
|
+
// a state's own `display` wins on specificity (and, on a tie, by coming later).
|
|
787
|
+
const statesDefaultValues: Record<string, DisplayEnumValue> = {}
|
|
788
|
+
for (const stateName of stateNames) {
|
|
789
|
+
const stateMatches = matcherData?.stateMatches?.[stateName]
|
|
790
|
+
const stateCustomProperties = matcherData?.stateCustomProperties?.[stateName]
|
|
791
|
+
if (!stateMatches && !stateCustomProperties) continue
|
|
792
|
+
const stateDisplayValue = resolveDisplayValue({
|
|
793
|
+
matches: [...(matcherData?.matches ?? []), ...(stateMatches ?? [])],
|
|
794
|
+
customProperties: { ...matcherData?.customProperties, ...stateCustomProperties },
|
|
795
|
+
})
|
|
796
|
+
if (stateDisplayValue === undefined) continue
|
|
797
|
+
const stateEnumValue = toDisplayEnumValue(stateDisplayValue)
|
|
798
|
+
if (stateEnumValue === currentEnumValue) continue
|
|
799
|
+
statesDefaultValues[stateName] = stateEnumValue
|
|
800
|
+
if (!displayValues.includes(stateEnumValue)) displayValues.push(stateEnumValue)
|
|
638
801
|
}
|
|
639
802
|
|
|
640
|
-
const currentValue = resolvedFromCss ?? getDefaultDisplayForTag(element.tag)
|
|
641
|
-
const currentEnumValue = toDisplayEnumValue(currentValue)
|
|
642
|
-
|
|
643
803
|
return {
|
|
644
|
-
display: {
|
|
645
|
-
|
|
646
|
-
},
|
|
804
|
+
display: { displayValues },
|
|
805
|
+
...(Object.keys(statesDefaultValues).length > 0 && { statesDefaultValues }),
|
|
647
806
|
}
|
|
648
807
|
}
|
|
@@ -121,6 +121,55 @@ describe('parseCss — getStateClasses', () => {
|
|
|
121
121
|
})
|
|
122
122
|
})
|
|
123
123
|
|
|
124
|
+
describe('parseCss — getSelectorStateScope', () => {
|
|
125
|
+
// Selectors are keyed by css-tree's generated form, so look the single rule up by its key.
|
|
126
|
+
function scopeOf(selector: string) {
|
|
127
|
+
const api = parseCss(`${selector} { color: red; }`)
|
|
128
|
+
const [generatedSelector] = api.getAllProperties().keys()
|
|
129
|
+
return api.getSelectorStateScope(generatedSelector)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
it.each([
|
|
133
|
+
['.root', { kind: 'regular' }],
|
|
134
|
+
['.root:hover', { kind: 'state', stateName: 'hover' }],
|
|
135
|
+
['.root:global(.button--hover)', { kind: 'state', stateName: 'hover' }],
|
|
136
|
+
['.root:global(.button--active)', { kind: 'state', stateName: 'active' }],
|
|
137
|
+
['.root:active', { kind: 'state', stateName: 'active' }],
|
|
138
|
+
['.root:focus-visible', { kind: 'state', stateName: 'focus' }],
|
|
139
|
+
['.root:global(.button--featured)', { kind: 'state', stateName: 'featured' }],
|
|
140
|
+
['.root:global(.button--hover):hover', { kind: 'state', stateName: 'hover' }],
|
|
141
|
+
['.root:not([dir])', { kind: 'regular' }],
|
|
142
|
+
['.root:not(:disabled)', { kind: 'regular' }],
|
|
143
|
+
['.root:first-child', { kind: 'regular' }],
|
|
144
|
+
['.root:hover .label', { kind: 'unmappedState' }],
|
|
145
|
+
['.root:global(.button--active):hover', { kind: 'unmappedState' }],
|
|
146
|
+
['.root:hover:focus-visible', { kind: 'unmappedState' }],
|
|
147
|
+
['.root:is(:hover, :focus)', { kind: 'unmappedState' }],
|
|
148
|
+
['.root:is(:global(.button--selected))', { kind: 'unmappedState' }],
|
|
149
|
+
['.root:where(:global(.button--selected))', { kind: 'unmappedState' }],
|
|
150
|
+
])('classifies %s', (selector, expectedScope) => {
|
|
151
|
+
expect(scopeOf(selector)).toEqual(expectedScope)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('treats a global ancestor theme class as regular (no local base to modify)', () => {
|
|
155
|
+
expect(scopeOf(':global(.dark) .root')).toEqual({ kind: 'regular' })
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('classifies each selector of a selector list on its own', () => {
|
|
159
|
+
const api = parseCss('.root:global(.button--hover), .root:hover, .root { color: red; }')
|
|
160
|
+
expect(api.getSelectorStateScope('.root:global(.button--hover)')).toEqual({ kind: 'state', stateName: 'hover' })
|
|
161
|
+
expect(api.getSelectorStateScope('.root:hover')).toEqual({ kind: 'state', stateName: 'hover' })
|
|
162
|
+
expect(api.getSelectorStateScope('.root')).toEqual({ kind: 'regular' })
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('flags declarations inside a conditional at-rule', () => {
|
|
166
|
+
const api = parseCss('.root { color: red; } @media (min-width: 600px) { .root { background: blue; } }')
|
|
167
|
+
const properties = api.getPropertiesForSelector('.root')
|
|
168
|
+
expect(properties.find((property) => property.name === 'color')?.inAtRule).toBeUndefined()
|
|
169
|
+
expect(properties.find((property) => property.name === 'background')?.inAtRule).toBe(true)
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
124
173
|
describe('parseCss — @property registration', () => {
|
|
125
174
|
it('maps each syntax type to its cssPropertyType and reads the initial-value default', () => {
|
|
126
175
|
const registered = parseCss(`
|