@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.
@@ -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 nearestCommonAncestorCustomProps = buildNearestCommonAncestorCustomProps(component)
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
- stateClassTriggers,
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
- stateClassTriggers: StateClassTriggers,
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
- stateClassTriggers,
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
- const varNames = new Set<string>([...allCustomPropertyValues.keys(), ...registeredByVar.keys()])
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
- // A @property registration is the top-priority type source; otherwise infer from usage.
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, defaultValue ?? '', component.css)
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(elements: ExtractedElement[], varName: string): Set<string> {
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
- if (matcherData?.customProperties[varName] !== undefined) definingTraceIds.add(element.traceId)
526
- for (const traceId of findDefiningElements(element.children, varName)) definingTraceIds.add(traceId)
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
- * Returns the CSS property values matched for this specific element's selector(s).
537
- * Reads from css-matcher.matches which is populated by matchCssSelectors.
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
- function getMatchedPropertyValues(element: ExtractedElement): Map<string, string> {
541
- const matcherData = element.extractorData.get('css-matcher') as MatchedCssData | undefined
542
- const matches = matcherData?.matches
543
- if (!matches || matches.length === 0) return new Map()
649
+ interface LayerValues {
650
+ topLevel: Map<string, string>
651
+ inAtRule: Map<string, string>
652
+ }
544
653
 
545
- const values = new Map<string, string>()
546
- for (const match of matches) {
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
- values.set(camelCase(prop.name), prop.value)
662
+ const targetValues = prop.inAtRule ? layerValues.inAtRule : layerValues.topLevel
663
+ targetValues.set(camelCase(prop.name), prop.value)
549
664
  }
550
665
  }
551
- return values
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 cssPropertyValues = element ? getMatchedPropertyValues(element) : new Map<string, string>()
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 resolvedValue = resolveLonghand(propName, cssPropertyValues)
617
- const defaultValue = resolvedValue !== undefined ? stripVarFallback(resolvedValue) : undefined
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
- if (hasExplicitDisplayDeclaration && resolvedFromCss === undefined) {
633
- return {
634
- display: {
635
- displayValues: [DISPLAY_VALUE.none],
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
- displayValues: [DISPLAY_VALUE.none, currentEnumValue],
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(`