@wix/zero-config-implementation 1.105.0 → 1.107.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 -17
- package/dist/index.js +8019 -7895
- package/package.json +2 -2
- package/src/converters/states-default-values.test.ts +308 -0
- package/src/converters/to-editor-component.test.ts +0 -85
- package/src/converters/to-editor-component.ts +225 -105
- package/src/information-extractors/css/parse.test.ts +49 -0
- package/src/information-extractors/css/parse.ts +113 -24
- package/src/information-extractors/css/selector-matcher.ts +83 -28
- package/src/information-extractors/css/types.ts +29 -17
- package/src/manifest-pipeline.ts +8 -1
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
CssDataType,
|
|
10
10
|
NativePseudoClass,
|
|
11
11
|
RegisteredCustomProperty,
|
|
12
|
-
|
|
12
|
+
SelectorStateScope,
|
|
13
13
|
StateClassRule,
|
|
14
14
|
} from './types'
|
|
15
15
|
|
|
@@ -29,6 +29,7 @@ function isInsideKeyframes(currentContext: WalkContext): boolean {
|
|
|
29
29
|
export function parseCss(cssString: string): CSSParserAPI {
|
|
30
30
|
const parsedSelectors = new Map<string, Selector>()
|
|
31
31
|
const { propertiesMap: allProperties, varUsagesByProperty } = parseAllProperties(cssString, parsedSelectors)
|
|
32
|
+
const stateScopeBySelector = new Map<string, SelectorStateScope>()
|
|
32
33
|
const stateClasses = parseStateClasses(cssString)
|
|
33
34
|
const registeredProperties = parseRegisteredProperties(cssString)
|
|
34
35
|
|
|
@@ -85,6 +86,15 @@ export function parseCss(cssString: string): CSSParserAPI {
|
|
|
85
86
|
return getSelectorSpecificity(parsed)
|
|
86
87
|
},
|
|
87
88
|
|
|
89
|
+
getSelectorStateScope(selector: string): SelectorStateScope {
|
|
90
|
+
const cachedScope = stateScopeBySelector.get(selector)
|
|
91
|
+
if (cachedScope) return cachedScope
|
|
92
|
+
const parsed = parsedSelectors.get(selector)
|
|
93
|
+
const scope = parsed ? classifySelectorStateScope(parsed) : REGULAR_SCOPE
|
|
94
|
+
stateScopeBySelector.set(selector, scope)
|
|
95
|
+
return scope
|
|
96
|
+
},
|
|
97
|
+
|
|
88
98
|
getVarPropertyType(varName: string): string | undefined {
|
|
89
99
|
// A `@property` registration is the declared type and wins over usage inference.
|
|
90
100
|
const normalizedVarName = varName.startsWith('--') ? varName : `--${varName}`
|
|
@@ -145,6 +155,10 @@ function parseAllProperties(
|
|
|
145
155
|
|
|
146
156
|
const selectorStrings: string[] = []
|
|
147
157
|
|
|
158
|
+
if (this.atrule && !isInsideKeyframes(this)) {
|
|
159
|
+
for (const property of properties) property.inAtRule = true
|
|
160
|
+
}
|
|
161
|
+
|
|
148
162
|
if (isInsideKeyframes(this)) {
|
|
149
163
|
const keyframeName = this.atrule?.prelude ? generate(this.atrule.prelude).trim() : ''
|
|
150
164
|
if (!keyframeName) return
|
|
@@ -522,19 +536,6 @@ function extractAtruleDescriptors(block: CssNode): Map<string, string> {
|
|
|
522
536
|
return descriptors
|
|
523
537
|
}
|
|
524
538
|
|
|
525
|
-
/** Picks the range descriptors out of the block; css-tree collects them like any other declaration. */
|
|
526
|
-
function extractRangeDescriptors(descriptors: Map<string, string>): RegisteredPropertyRange | undefined {
|
|
527
|
-
const min = descriptors.get('--min')
|
|
528
|
-
const max = descriptors.get('--max')
|
|
529
|
-
const step = descriptors.get('--step')
|
|
530
|
-
if (min === undefined && max === undefined && step === undefined) return undefined
|
|
531
|
-
return {
|
|
532
|
-
...(min !== undefined && { min }),
|
|
533
|
-
...(max !== undefined && { max }),
|
|
534
|
-
...(step !== undefined && { step }),
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
539
|
/**
|
|
539
540
|
* Walks the stylesheet for `@property` at-rules and builds a map of registered custom
|
|
540
541
|
* properties keyed by the `--`-prefixed variable name.
|
|
@@ -562,13 +563,11 @@ function parseRegisteredProperties(cssString: string): Map<string, RegisteredCus
|
|
|
562
563
|
if (!resolved) return
|
|
563
564
|
const { cssPropertyType, enumOptions } = resolved
|
|
564
565
|
const initialValue = descriptors.get('initial-value')
|
|
565
|
-
const range = extractRangeDescriptors(descriptors)
|
|
566
566
|
|
|
567
567
|
registered.set(varName, {
|
|
568
568
|
cssPropertyType,
|
|
569
569
|
...(enumOptions && { enumOptions }),
|
|
570
570
|
...(initialValue !== undefined && { defaultValue: initialValue }),
|
|
571
|
-
...(range && { range }),
|
|
572
571
|
})
|
|
573
572
|
},
|
|
574
573
|
})
|
|
@@ -664,14 +663,8 @@ function analyzeCompound(componentNodes: CssNode[]): AnalyzedCompound | null {
|
|
|
664
663
|
return { base, pseudoClass, modifiers }
|
|
665
664
|
}
|
|
666
665
|
|
|
667
|
-
/**
|
|
668
|
-
|
|
669
|
-
* `:global(.modifier)` (or native pseudo) belongs to the compound it is attached to,
|
|
670
|
-
* so ancestor/descendant compounds in the same selector are ignored for the base —
|
|
671
|
-
* e.g. `.header:global(.x--selected) .label` resolves to base `.header`, not
|
|
672
|
-
* `.header .label`. Returns one entry per compound that carries a modifier or pseudo.
|
|
673
|
-
*/
|
|
674
|
-
function analyzeStateSelector(selector: Selector): AnalyzedCompound[] {
|
|
666
|
+
/** Splits a selector into its compound selectors (the parts between combinators), in order. */
|
|
667
|
+
function splitIntoCompounds(selector: Selector): CssNode[][] {
|
|
675
668
|
const compounds: CssNode[][] = [[]]
|
|
676
669
|
for (const component of selector.children) {
|
|
677
670
|
if (component.type === 'Combinator') {
|
|
@@ -680,6 +673,18 @@ function analyzeStateSelector(selector: Selector): AnalyzedCompound[] {
|
|
|
680
673
|
compounds[compounds.length - 1].push(component)
|
|
681
674
|
}
|
|
682
675
|
}
|
|
676
|
+
return compounds
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Splits a selector into compound selectors at combinators and analyzes each. A
|
|
681
|
+
* `:global(.modifier)` (or native pseudo) belongs to the compound it is attached to,
|
|
682
|
+
* so ancestor/descendant compounds in the same selector are ignored for the base —
|
|
683
|
+
* e.g. `.header:global(.x--selected) .label` resolves to base `.header`, not
|
|
684
|
+
* `.header .label`. Returns one entry per compound that carries a modifier or pseudo.
|
|
685
|
+
*/
|
|
686
|
+
function analyzeStateSelector(selector: Selector): AnalyzedCompound[] {
|
|
687
|
+
const compounds = splitIntoCompounds(selector)
|
|
683
688
|
|
|
684
689
|
const results: AnalyzedCompound[] = []
|
|
685
690
|
for (const compoundNodes of compounds) {
|
|
@@ -747,6 +752,90 @@ function parseStateClasses(cssString: string): StateClassRule[] {
|
|
|
747
752
|
return results
|
|
748
753
|
}
|
|
749
754
|
|
|
755
|
+
// Dynamic pseudo-classes mapped to the design-state name a selector styles. Broader than
|
|
756
|
+
// NATIVE_PSEUDO_BY_NAME: `:active`/`:checked` aren't native editor states, but a rule using
|
|
757
|
+
// them still styles a state (e.g. the custom `active` state of `.root:global(.x--active), .root:active`)
|
|
758
|
+
// and must not leak into the regular default. These names follow the pseudo-class, so they do
|
|
759
|
+
// not line up with prop-derived custom states (`isActive` → `is-active`). `:focus-within` (a
|
|
760
|
+
// descendant has focus) also maps to `focus`: the schema has a single focus state per element.
|
|
761
|
+
const STATE_NAME_BY_PSEUDO: Record<string, string> = {
|
|
762
|
+
hover: 'hover',
|
|
763
|
+
active: 'active',
|
|
764
|
+
focus: 'focus',
|
|
765
|
+
'focus-visible': 'focus',
|
|
766
|
+
'focus-within': 'focus',
|
|
767
|
+
disabled: 'disabled',
|
|
768
|
+
invalid: 'invalid',
|
|
769
|
+
checked: 'checked',
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Pseudo-classes whose selector-list argument matches the element itself, so a state
|
|
773
|
+
// condition inside them (`:is(:hover, :focus)`, `:is(:global(.x--selected))`) makes the
|
|
774
|
+
// rule state-dependent.
|
|
775
|
+
const SELECTOR_LIST_PSEUDO_NAMES = new Set(['is', 'where', 'has', 'matches'])
|
|
776
|
+
|
|
777
|
+
const REGULAR_SCOPE: SelectorStateScope = { kind: 'regular' }
|
|
778
|
+
const UNMAPPED_STATE_SCOPE: SelectorStateScope = { kind: 'unmappedState' }
|
|
779
|
+
|
|
780
|
+
/** True when a node contains a state pseudo-class or a `:global(.modifier)` state class. */
|
|
781
|
+
function containsStateCondition(node: CssNode): boolean {
|
|
782
|
+
let found = false
|
|
783
|
+
walk(node, (childNode) => {
|
|
784
|
+
if (
|
|
785
|
+
childNode.type === 'PseudoClassSelector' &&
|
|
786
|
+
(STATE_NAME_BY_PSEUDO[childNode.name] !== undefined || childNode.name === 'global')
|
|
787
|
+
) {
|
|
788
|
+
found = true
|
|
789
|
+
}
|
|
790
|
+
})
|
|
791
|
+
return found
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Collects the design-state names a single compound selector is conditioned on: its
|
|
796
|
+
* `:global(.modifier)` classes (split from the base exactly as `parseStateClasses` does)
|
|
797
|
+
* and its dynamic pseudo-classes. Returns null when a state condition is nested in a
|
|
798
|
+
* selector-list pseudo (`:is(:hover)`, `:is(:global(.x--selected))`), which cannot be
|
|
799
|
+
* attributed to a single state.
|
|
800
|
+
*/
|
|
801
|
+
function collectCompoundStateNames(compoundNodes: CssNode[]): Set<string> | null {
|
|
802
|
+
const stateNames = new Set<string>()
|
|
803
|
+
for (const modifier of analyzeCompound(compoundNodes)?.modifiers ?? []) {
|
|
804
|
+
stateNames.add(stateNameFromModifier(modifier))
|
|
805
|
+
}
|
|
806
|
+
for (const component of compoundNodes) {
|
|
807
|
+
if (component.type !== 'PseudoClassSelector') continue
|
|
808
|
+
const pseudoStateName = STATE_NAME_BY_PSEUDO[component.name]
|
|
809
|
+
if (pseudoStateName) {
|
|
810
|
+
stateNames.add(pseudoStateName)
|
|
811
|
+
} else if (SELECTOR_LIST_PSEUDO_NAMES.has(component.name) && containsStateCondition(component)) {
|
|
812
|
+
return null
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return stateNames
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Classifies which appearance of its subject (last) compound a selector styles. A state is
|
|
820
|
+
* attributed only when the subject alone carries exactly one state name; a state on an
|
|
821
|
+
* ancestor compound, several distinct states, or a nested state pseudo is `unmappedState`.
|
|
822
|
+
* `:not(...)` is ignored — `:not([dir])` and `:not(:disabled)` hold in the regular appearance.
|
|
823
|
+
*/
|
|
824
|
+
function classifySelectorStateScope(selector: Selector): SelectorStateScope {
|
|
825
|
+
const compounds = splitIntoCompounds(selector)
|
|
826
|
+
|
|
827
|
+
const subjectCompound = compounds[compounds.length - 1]
|
|
828
|
+
for (const ancestorCompound of compounds.slice(0, -1)) {
|
|
829
|
+
const ancestorStateNames = collectCompoundStateNames(ancestorCompound)
|
|
830
|
+
if (!ancestorStateNames || ancestorStateNames.size > 0) return UNMAPPED_STATE_SCOPE
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const subjectStateNames = collectCompoundStateNames(subjectCompound)
|
|
834
|
+
if (!subjectStateNames || subjectStateNames.size > 1) return UNMAPPED_STATE_SCOPE
|
|
835
|
+
const [stateName] = subjectStateNames
|
|
836
|
+
return stateName ? { kind: 'state', stateName } : REGULAR_SCOPE
|
|
837
|
+
}
|
|
838
|
+
|
|
750
839
|
/**
|
|
751
840
|
* Builds a DOM-queryable selector by filtering out pseudo-classes and pseudo-elements.
|
|
752
841
|
* Returns null if the selector contains pseudo-elements (unmatchable against real DOM).
|
|
@@ -50,9 +50,9 @@ export function matchCssSelectors(
|
|
|
50
50
|
): { elements: ExtractedElement[]; varUsedByTraceId: Map<string, Set<string>> } {
|
|
51
51
|
const $ = cheerio.load(html)
|
|
52
52
|
|
|
53
|
-
// Build traceId -> matches map
|
|
54
|
-
const
|
|
55
|
-
const
|
|
53
|
+
// Build traceId -> matches map, split by the appearance (regular / per-state) each selector styles
|
|
54
|
+
const regularByTraceId = new Map<string, ScopedCssData>()
|
|
55
|
+
const stateByTraceId = new Map<string, Map<string, ScopedCssData>>()
|
|
56
56
|
const varUsedByTraceId = new Map<string, Set<string>>()
|
|
57
57
|
|
|
58
58
|
for (const cssInfo of cssInfos) {
|
|
@@ -66,6 +66,8 @@ export function matchCssSelectors(
|
|
|
66
66
|
const domSelector = cssInfo.api.getDomSelector(selector)
|
|
67
67
|
if (!domSelector) continue
|
|
68
68
|
|
|
69
|
+
const stateScope = cssInfo.api.getSelectorStateScope(selector)
|
|
70
|
+
|
|
69
71
|
try {
|
|
70
72
|
const selectorSpecificity = cssInfo.api.getSelectorSpecificity(selector) ?? [0, 0, 0]
|
|
71
73
|
|
|
@@ -76,39 +78,46 @@ export function matchCssSelectors(
|
|
|
76
78
|
// Use reduce to separate regular and custom properties in one pass
|
|
77
79
|
const { regular, custom } = properties.reduce<{
|
|
78
80
|
regular: CSSProperty[]
|
|
79
|
-
custom:
|
|
81
|
+
custom: CSSProperty[]
|
|
80
82
|
}>(
|
|
81
83
|
(accumulator, prop) => {
|
|
82
84
|
if (prop.name.startsWith('--')) {
|
|
83
|
-
accumulator.custom
|
|
85
|
+
accumulator.custom.push(prop)
|
|
84
86
|
} else {
|
|
85
87
|
accumulator.regular.push(prop)
|
|
86
88
|
}
|
|
87
89
|
return accumulator
|
|
88
90
|
},
|
|
89
|
-
{ regular: [], custom:
|
|
91
|
+
{ regular: [], custom: [] },
|
|
90
92
|
)
|
|
91
93
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
for (const varName of regularProp.varRefs ?? []) {
|
|
100
|
-
const traceIdSet = varUsedByTraceId.get(varName) ?? new Set()
|
|
101
|
-
traceIdSet.add(traceId)
|
|
102
|
-
varUsedByTraceId.set(varName, traceIdSet)
|
|
103
|
-
}
|
|
94
|
+
// Track which CSS custom properties are used (via var()) by this element — in every
|
|
95
|
+
// scope, so a variable read only under `:hover` is still placed on its element.
|
|
96
|
+
for (const regularProp of regular) {
|
|
97
|
+
for (const varName of regularProp.varRefs ?? []) {
|
|
98
|
+
const traceIdSet = varUsedByTraceId.get(varName) ?? new Set()
|
|
99
|
+
traceIdSet.add(traceId)
|
|
100
|
+
varUsedByTraceId.set(varName, traceIdSet)
|
|
104
101
|
}
|
|
105
102
|
}
|
|
106
103
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
104
|
+
// State-dependent rules that can't be attributed to one state of this element
|
|
105
|
+
// style neither its regular appearance nor any of its states.
|
|
106
|
+
if (stateScope.kind === 'unmappedState') return
|
|
107
|
+
|
|
108
|
+
const scopedData =
|
|
109
|
+
stateScope.kind === 'state'
|
|
110
|
+
? getOrCreate(
|
|
111
|
+
getOrCreate(stateByTraceId, traceId, () => new Map()),
|
|
112
|
+
stateScope.stateName,
|
|
113
|
+
createScopedData,
|
|
114
|
+
)
|
|
115
|
+
: getOrCreate(regularByTraceId, traceId, createScopedData)
|
|
116
|
+
|
|
117
|
+
if (regular.length > 0) {
|
|
118
|
+
scopedData.matches.push({ selector, properties: regular, specificity: selectorSpecificity })
|
|
111
119
|
}
|
|
120
|
+
for (const customProp of custom) assignCustomProperty(scopedData, customProp)
|
|
112
121
|
})
|
|
113
122
|
} catch {
|
|
114
123
|
// Invalid selector for Cheerio, skip
|
|
@@ -117,26 +126,72 @@ export function matchCssSelectors(
|
|
|
117
126
|
}
|
|
118
127
|
|
|
119
128
|
return {
|
|
120
|
-
elements: enrichElements(elements,
|
|
129
|
+
elements: enrichElements(elements, regularByTraceId, stateByTraceId),
|
|
121
130
|
varUsedByTraceId,
|
|
122
131
|
}
|
|
123
132
|
}
|
|
124
133
|
|
|
134
|
+
interface ScopedCssData {
|
|
135
|
+
matches: CssSelectorMatch[]
|
|
136
|
+
customProperties: Record<string, string>
|
|
137
|
+
/** Custom properties declared outside any at-rule — a conditional (`@media`) value never overrides them. */
|
|
138
|
+
topLevelCustomPropertyNames: Set<string>
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function createScopedData(): ScopedCssData {
|
|
142
|
+
return { matches: [], customProperties: {}, topLevelCustomPropertyNames: new Set() }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Later declaration wins, except that an at-rule declaration never overrides a top-level one. */
|
|
146
|
+
function assignCustomProperty(scopedData: ScopedCssData, customProp: CSSProperty): void {
|
|
147
|
+
if (customProp.inAtRule && scopedData.topLevelCustomPropertyNames.has(customProp.name)) return
|
|
148
|
+
if (!customProp.inAtRule) scopedData.topLevelCustomPropertyNames.add(customProp.name)
|
|
149
|
+
scopedData.customProperties[customProp.name] = customProp.value
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function getOrCreate<Key, Value>(map: Map<Key, Value>, key: Key, create: () => Value): Value {
|
|
153
|
+
const existing = map.get(key)
|
|
154
|
+
if (existing !== undefined) return existing
|
|
155
|
+
const created = create()
|
|
156
|
+
map.set(key, created)
|
|
157
|
+
return created
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function buildStateData(
|
|
161
|
+
stateData: Map<string, ScopedCssData> | undefined,
|
|
162
|
+
): Pick<MatchedCssData, 'stateMatches' | 'stateCustomProperties'> {
|
|
163
|
+
if (!stateData || stateData.size === 0) return {}
|
|
164
|
+
const stateMatches: Record<string, CssSelectorMatch[]> = {}
|
|
165
|
+
const stateCustomProperties: Record<string, Record<string, string>> = {}
|
|
166
|
+
for (const [stateName, scopedData] of stateData) {
|
|
167
|
+
if (scopedData.matches.length > 0) stateMatches[stateName] = scopedData.matches
|
|
168
|
+
if (Object.keys(scopedData.customProperties).length > 0) {
|
|
169
|
+
stateCustomProperties[stateName] = scopedData.customProperties
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
...(Object.keys(stateMatches).length > 0 && { stateMatches }),
|
|
174
|
+
...(Object.keys(stateCustomProperties).length > 0 && { stateCustomProperties }),
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
125
178
|
function enrichElements(
|
|
126
179
|
elements: ExtractedElement[],
|
|
127
|
-
|
|
128
|
-
|
|
180
|
+
regularByTraceId: Map<string, ScopedCssData>,
|
|
181
|
+
stateByTraceId: Map<string, Map<string, ScopedCssData>>,
|
|
129
182
|
): ExtractedElement[] {
|
|
130
183
|
return elements.map((element) => {
|
|
184
|
+
const regularData = regularByTraceId.get(element.traceId)
|
|
131
185
|
const data: MatchedCssData = {
|
|
132
|
-
matches:
|
|
133
|
-
customProperties:
|
|
186
|
+
matches: regularData?.matches ?? [],
|
|
187
|
+
customProperties: regularData?.customProperties ?? {},
|
|
188
|
+
...buildStateData(stateByTraceId.get(element.traceId)),
|
|
134
189
|
}
|
|
135
190
|
element.extractorData.set('css-matcher', data)
|
|
136
191
|
|
|
137
192
|
return {
|
|
138
193
|
...element,
|
|
139
|
-
children: enrichElements(element.children,
|
|
194
|
+
children: enrichElements(element.children, regularByTraceId, stateByTraceId),
|
|
140
195
|
}
|
|
141
196
|
})
|
|
142
197
|
}
|
|
@@ -15,6 +15,13 @@ export interface CSSProperty {
|
|
|
15
15
|
valueAst?: CssNode
|
|
16
16
|
/** CSS variable names referenced via var() in this property's value */
|
|
17
17
|
varRefs?: string[]
|
|
18
|
+
/**
|
|
19
|
+
* True when the declaration sits inside an at-rule block (e.g. `@media`, `@supports`, `@layer`),
|
|
20
|
+
* other than `@keyframes`. A top-level declaration wins over one in an at-rule when resolving a
|
|
21
|
+
* default value — conditional rules don't describe the default appearance, and unlayered rules
|
|
22
|
+
* beat layered ones in the cascade.
|
|
23
|
+
*/
|
|
24
|
+
inAtRule?: boolean
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
export interface CssSelectorMatch {
|
|
@@ -24,10 +31,25 @@ export interface CssSelectorMatch {
|
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
export interface MatchedCssData {
|
|
34
|
+
/** Matches whose selectors apply in the element's regular (non-state) appearance. */
|
|
27
35
|
matches: CssSelectorMatch[]
|
|
36
|
+
/** Custom properties declared by regular-scope matches. */
|
|
28
37
|
customProperties: Record<string, string>
|
|
38
|
+
/** Matches whose selectors apply only in one design state, keyed by state name (e.g. `hover`). */
|
|
39
|
+
stateMatches?: Record<string, CssSelectorMatch[]>
|
|
40
|
+
/** Custom properties declared by state-scope matches, keyed by state name. */
|
|
41
|
+
stateCustomProperties?: Record<string, Record<string, string>>
|
|
29
42
|
}
|
|
30
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Which appearance of its subject element a selector styles:
|
|
46
|
+
* - `regular` — the element's default appearance (no state condition on the subject).
|
|
47
|
+
* - `state` — exactly one design state on the subject (`.root:hover`, `.root:global(.x--hover)`).
|
|
48
|
+
* - `unmappedState` — state-dependent, but not attributable to a single state of the subject
|
|
49
|
+
* (an ancestor's state, several states combined, a state inside `:is()`).
|
|
50
|
+
*/
|
|
51
|
+
export type SelectorStateScope = { kind: 'regular' } | { kind: 'state'; stateName: string } | { kind: 'unmappedState' }
|
|
52
|
+
|
|
31
53
|
/**
|
|
32
54
|
* A CSS custom property registered via the standard `@property` at-rule.
|
|
33
55
|
* The `syntax` descriptor gives the type (and, for a pipe-separated ident list, a
|
|
@@ -47,23 +69,6 @@ export interface RegisteredCustomProperty {
|
|
|
47
69
|
* (`a | b | c`). Present only when `cssPropertyType` is `customEnum`.
|
|
48
70
|
*/
|
|
49
71
|
enumOptions?: string[]
|
|
50
|
-
/**
|
|
51
|
-
* The numeric range for the control, as raw descriptor strings. Validated at
|
|
52
|
-
* conversion time. Meaningful only when `cssPropertyType` is `number`.
|
|
53
|
-
*/
|
|
54
|
-
range?: RegisteredPropertyRange
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* `--min` / `--max` / `--step` descriptors an importer may emit inside an `@property`
|
|
59
|
-
* block. The `--` prefix keeps them in the author namespace: browsers ignore descriptors
|
|
60
|
-
* they don't know, so the CSS stays valid and CSS can't standardise these names out from
|
|
61
|
-
* under us.
|
|
62
|
-
*/
|
|
63
|
-
export interface RegisteredPropertyRange {
|
|
64
|
-
min?: string
|
|
65
|
-
max?: string
|
|
66
|
-
step?: string
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
export type NativePseudoClass = 'hover' | 'focus' | 'disabled' | 'invalid'
|
|
@@ -133,6 +138,13 @@ export interface CSSParserAPI {
|
|
|
133
138
|
*/
|
|
134
139
|
getSelectorSpecificity: (selector: string) => [number, number, number] | null
|
|
135
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Classifies which appearance of its subject element a selector styles — regular, a single
|
|
143
|
+
* design state, or a state that cannot be attributed to the subject. Returns `regular` for
|
|
144
|
+
* selectors that were not parsed (e.g. keyframe selectors).
|
|
145
|
+
*/
|
|
146
|
+
getSelectorStateScope: (selector: string) => SelectorStateScope
|
|
147
|
+
|
|
136
148
|
/**
|
|
137
149
|
* Determines the CSS property type this one stylesheet can attribute to a custom property.
|
|
138
150
|
* A `@property` registration is the top-priority source (declared type). Otherwise falls back
|
package/src/manifest-pipeline.ts
CHANGED
|
@@ -373,10 +373,17 @@ function extractCssInfo(cssImportPaths: string[], componentName: string): Extrac
|
|
|
373
373
|
|
|
374
374
|
const properties = new Map<string, string>()
|
|
375
375
|
const customProperties = new Map<string, string>()
|
|
376
|
+
const topLevelCustomPropertyNames = new Set<string>()
|
|
376
377
|
|
|
377
|
-
for (const [, props] of allProps) {
|
|
378
|
+
for (const [selector, props] of allProps) {
|
|
379
|
+
// A state rule (`.root:hover { --x: … }`) must not supply the regular default.
|
|
380
|
+
const isRegularSelector = api.getSelectorStateScope(selector).kind === 'regular'
|
|
378
381
|
for (const prop of props) {
|
|
379
382
|
if (prop.name.startsWith('--')) {
|
|
383
|
+
if (!isRegularSelector) continue
|
|
384
|
+
// An at-rule (`@media`) value never overrides a top-level one.
|
|
385
|
+
if (prop.inAtRule && topLevelCustomPropertyNames.has(prop.name)) continue
|
|
386
|
+
if (!prop.inAtRule) topLevelCustomPropertyNames.add(prop.name)
|
|
380
387
|
customProperties.set(prop.name, prop.value)
|
|
381
388
|
} else {
|
|
382
389
|
properties.set(prop.name, prop.value)
|