@wix/zero-config-implementation 1.98.0 → 1.100.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.
@@ -5,6 +5,7 @@ import type { MatchedCssData } from '../../css/types'
5
5
  import type { ExtractedElement } from './core/tree-builder'
6
6
  import {
7
7
  enrichContainerProperties,
8
+ enrichFlexProperties,
8
9
  getCssPropertiesForTag,
9
10
  getDefaultDisplayForTag,
10
11
  hasContainerLikeDisplay,
@@ -173,8 +174,8 @@ describe('getCssPropertiesForTag', () => {
173
174
  expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
174
175
  })
175
176
 
176
- it('gives interactive controls both box and text properties', () => {
177
- for (const tag of ['button', 'select', 'textarea']) {
177
+ it('gives controls that render their own text both box and text properties', () => {
178
+ for (const tag of ['select', 'textarea']) {
178
179
  const relevantProperties = getCssPropertiesForTag(tag)
179
180
 
180
181
  expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
@@ -183,6 +184,16 @@ describe('getCssPropertiesForTag', () => {
183
184
  }
184
185
  })
185
186
 
187
+ it('treats a button as box-generating but not text-bearing, except for color', () => {
188
+ const relevantProperties = getCssPropertiesForTag('button')
189
+
190
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
191
+ // `color` resolves `currentColor` in borders, outlines and fills, text or not
192
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.color)
193
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
194
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.textAlign)
195
+ })
196
+
186
197
  it('treats an input with no type as text-bearing', () => {
187
198
  const relevantProperties = getCssPropertiesForTag('input')
188
199
 
@@ -242,6 +253,131 @@ describe('getCssPropertiesForTag', () => {
242
253
  expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
243
254
  expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
244
255
  })
256
+
257
+ it('gives block/replaced elements the full margin family regardless of box/text/media semantics', () => {
258
+ for (const tag of ['div', 'p', 'img']) {
259
+ const relevantProperties = getCssPropertiesForTag(tag)
260
+
261
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginTop)
262
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginBottom)
263
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginInlineStart)
264
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginInlineEnd)
265
+ }
266
+ })
267
+
268
+ it('gives non-replaced inline elements only the horizontal margins, per CSS margin-on-inline-boxes rules', () => {
269
+ for (const tag of ['span', 'a', 'em', 'b', 'i']) {
270
+ const relevantProperties = getCssPropertiesForTag(tag)
271
+
272
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginInlineStart)
273
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginInlineEnd)
274
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginTop)
275
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginBottom)
276
+ }
277
+ })
278
+
279
+ it('gives internal table display elements no margin at all, per CSS 2.1 §17.6.1', () => {
280
+ for (const tag of ['tr', 'thead', 'tbody', 'tfoot', 'col', 'colgroup', 'td', 'th']) {
281
+ const relevantProperties = getCssPropertiesForTag(tag)
282
+
283
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginTop)
284
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginBottom)
285
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginInlineStart)
286
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginInlineEnd)
287
+ }
288
+ })
289
+
290
+ it('gives box-generating elements width, height, overflow and mix-blend-mode', () => {
291
+ const relevantProperties = getCssPropertiesForTag('div')
292
+
293
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.width)
294
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.height)
295
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.overflow)
296
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.mixBlendMode)
297
+ })
298
+
299
+ it('gives media elements sizing, object-fit/position and mix-blend-mode', () => {
300
+ const relevantProperties = getCssPropertiesForTag('img')
301
+
302
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.width)
303
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.height)
304
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.overflow)
305
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.objectFit)
306
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.objectPosition)
307
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.mixBlendMode)
308
+ })
309
+
310
+ it('keeps text-only tags without width/height/overflow/object-fit', () => {
311
+ const relevantProperties = getCssPropertiesForTag('p')
312
+
313
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.width)
314
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.height)
315
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.objectFit)
316
+ })
317
+ })
318
+
319
+ describe('enrichFlexProperties', () => {
320
+ it('adds flexDirection/justifyContent/alignItems to a flex container', () => {
321
+ const elements = [createElementWithCssProperties('div', 'display: flex')]
322
+
323
+ const [enrichedElement] = enrichFlexProperties(elements)
324
+ const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
325
+
326
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.flexDirection)
327
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.justifyContent)
328
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.alignItems)
329
+ expect(cssData.relevant).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.alignSelf)
330
+ })
331
+
332
+ it('does not add flex container properties to a non-flex/grid element', () => {
333
+ const elements = [createElementWithCssProperties('div', 'display: block')]
334
+
335
+ const [enrichedElement] = enrichFlexProperties(elements)
336
+ const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
337
+
338
+ expect(cssData.relevant).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.flexDirection)
339
+ expect(cssData.relevant).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.justifyContent)
340
+ expect(cssData.relevant).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.alignItems)
341
+ })
342
+
343
+ it('adds alignSelf to direct children of a flex/grid container', () => {
344
+ const child = createElementWithCssProperties('span', '')
345
+ const parent: ExtractedElement = {
346
+ ...createElementWithCssProperties('div', 'display: flex'),
347
+ children: [child],
348
+ }
349
+
350
+ const [enrichedParent] = enrichFlexProperties([parent])
351
+ const enrichedChild = enrichedParent.children[0]
352
+ const childCssData = enrichedChild.extractorData.get('css-properties') as { relevant: string[] }
353
+
354
+ expect(childCssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.alignSelf)
355
+ })
356
+
357
+ it('does not add alignSelf to children of a non-flex/grid container', () => {
358
+ const child = createElementWithCssProperties('span', '')
359
+ const parent: ExtractedElement = {
360
+ ...createElementWithCssProperties('div', 'display: block'),
361
+ children: [child],
362
+ }
363
+
364
+ const [enrichedParent] = enrichFlexProperties([parent])
365
+ const enrichedChild = enrichedParent.children[0]
366
+ const childCssData = enrichedChild.extractorData.get('css-properties') as { relevant: string[] }
367
+
368
+ expect(childCssData.relevant).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.alignSelf)
369
+ })
370
+
371
+ it('adds flex container properties to a grid container as well', () => {
372
+ const elements = [createElementWithCssProperties('div', 'display: grid')]
373
+
374
+ const [enrichedElement] = enrichFlexProperties(elements)
375
+ const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
376
+
377
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.flexDirection)
378
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.justifyContent)
379
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.alignItems)
380
+ })
245
381
  })
246
382
 
247
383
  describe('getDefaultDisplayForTag', () => {
@@ -394,4 +530,14 @@ describe('enrichContainerProperties', () => {
394
530
  expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
395
531
  expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
396
532
  })
533
+
534
+ it('adds box properties to span when display is signaled only via the --display root convention, not a literal display declaration', () => {
535
+ const elements = [createElementWithCssProperties('span', '--display: block')]
536
+
537
+ const [enrichedElement] = enrichContainerProperties(elements)
538
+ const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
539
+
540
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
541
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
542
+ })
397
543
  })
@@ -30,7 +30,8 @@ export interface SemanticAttributes {
30
30
 
31
31
  /**
32
32
  * Text & Typography - elements where readability and hierarchy are primary.
33
- * Form controls render their own text; `input` varies by type (see below).
33
+ * `select`, `textarea`, and `input` render their own text; `input` varies by type
34
+ * (see below). `button` is absent on purpose - see CONTENT_DEPENDENT_TEXT_TAGS.
34
35
  */
35
36
  const TEXT_TAGS = new Set([
36
37
  'h1',
@@ -48,11 +49,21 @@ const TEXT_TAGS = new Set([
48
49
  'em',
49
50
  'b',
50
51
  'i',
51
- 'button',
52
52
  'select',
53
53
  'textarea',
54
54
  ])
55
55
 
56
+ /**
57
+ * Tags whose text comes from their content rather than their own rendering, so
58
+ * tree building decides once the rendered subtree is known.
59
+ */
60
+ const CONTENT_DEPENDENT_TEXT_TAGS = new Set(['button'])
61
+
62
+ /** Whether the tag's content, rather than the tag itself, decides typography. */
63
+ export function rendersTextThroughContent(tag: string): boolean {
64
+ return CONTENT_DEPENDENT_TEXT_TAGS.has(tag.toLowerCase())
65
+ }
66
+
56
67
  /** Anything else - including a missing or invalid `type` - defaults to text. */
57
68
  const NON_TEXT_INPUT_TYPES = new Set(['checkbox', 'radio', 'range', 'color', 'file', 'image', 'hidden'])
58
69
 
@@ -88,6 +99,70 @@ const BOX_CSS_PROPERTIES = [
88
99
  CSS_PROPERTY_TYPE.borderEndStartRadius,
89
100
  CSS_PROPERTY_TYPE.borderEndEndRadius,
90
101
  CSS_PROPERTY_TYPE.boxShadow,
102
+ CSS_PROPERTY_TYPE.width,
103
+ CSS_PROPERTY_TYPE.height,
104
+ CSS_PROPERTY_TYPE.overflow,
105
+ CSS_PROPERTY_TYPE.mixBlendMode,
106
+ ]
107
+
108
+ /**
109
+ * Margin applies to most DOM elements regardless of box/text/media
110
+ * semantics, so it's appended in `getCssPropertiesForTag` rather than gated
111
+ * to one of the trait-based sets above — except for the display-dependent
112
+ * carve-outs in `getMarginPropertiesForTag` below.
113
+ */
114
+ const MARGIN_CSS_PROPERTIES = [
115
+ CSS_PROPERTY_TYPE.marginTop,
116
+ CSS_PROPERTY_TYPE.marginBottom,
117
+ CSS_PROPERTY_TYPE.marginInlineStart,
118
+ CSS_PROPERTY_TYPE.marginInlineEnd,
119
+ ]
120
+
121
+ const HORIZONTAL_MARGIN_CSS_PROPERTIES = [CSS_PROPERTY_TYPE.marginInlineStart, CSS_PROPERTY_TYPE.marginInlineEnd]
122
+
123
+ /**
124
+ * Per CSS 2.1 §17.6.1, margin does not apply to elements with these internal
125
+ * table display values — the table layout algorithm ignores it entirely.
126
+ */
127
+ const MARGIN_UNSUPPORTED_DISPLAY_VALUES = new Set([
128
+ 'table-row',
129
+ 'table-row-group',
130
+ 'table-header-group',
131
+ 'table-footer-group',
132
+ 'table-column',
133
+ 'table-column-group',
134
+ 'table-cell',
135
+ ])
136
+
137
+ /**
138
+ * Margin properties relevant to a tag's default display. Internal table
139
+ * display values (`tr`, `thead`/`tbody`/`tfoot`, `col`/`colgroup`, `td`/`th`)
140
+ * don't honor margin at all. Non-replaced inline elements (`span`, `a`, …)
141
+ * only honor the horizontal (inline) margins — `margin-top`/`margin-bottom`
142
+ * have no effect on them. Replaced inline elements (`img`, `video`, …) honor
143
+ * margin on every side, same as a block box.
144
+ */
145
+ function getMarginPropertiesForTag(tag: string): string[] {
146
+ const normalizedTag = tag.toLowerCase()
147
+ const defaultDisplay = getDefaultDisplayForTag(normalizedTag)
148
+
149
+ if (MARGIN_UNSUPPORTED_DISPLAY_VALUES.has(defaultDisplay)) {
150
+ return []
151
+ }
152
+ if (defaultDisplay === 'inline' && !MEDIA_TAGS.has(normalizedTag)) {
153
+ return HORIZONTAL_MARGIN_CSS_PROPERTIES
154
+ }
155
+ return MARGIN_CSS_PROPERTIES
156
+ }
157
+
158
+ /**
159
+ * Child-layout controls for flex/grid containers, added post-hoc by
160
+ * `enrichFlexProperties` once the element's resolved `display` is known.
161
+ */
162
+ const FLEX_CONTAINER_CSS_PROPERTIES = [
163
+ CSS_PROPERTY_TYPE.flexDirection,
164
+ CSS_PROPERTY_TYPE.justifyContent,
165
+ CSS_PROPERTY_TYPE.alignItems,
91
166
  ]
92
167
 
93
168
  /**
@@ -117,6 +192,12 @@ const MEDIA_CSS_PROPERTIES = [
117
192
  CSS_PROPERTY_TYPE.borderEndStartRadius,
118
193
  CSS_PROPERTY_TYPE.borderEndEndRadius,
119
194
  CSS_PROPERTY_TYPE.boxShadow,
195
+ CSS_PROPERTY_TYPE.width,
196
+ CSS_PROPERTY_TYPE.height,
197
+ CSS_PROPERTY_TYPE.overflow,
198
+ CSS_PROPERTY_TYPE.objectFit,
199
+ CSS_PROPERTY_TYPE.objectPosition,
200
+ CSS_PROPERTY_TYPE.mixBlendMode,
120
201
  ]
121
202
 
122
203
  function hasTextSemantics(tag: string, attributes: SemanticAttributes = {}): boolean {
@@ -157,6 +238,10 @@ export function getCssPropertiesForTag(tag: string, attributes: SemanticAttribut
157
238
 
158
239
  if (hasTextProperties) {
159
240
  properties.push(...TEXT_CSS_PROPERTIES)
241
+ } else if (rendersTextThroughContent(tag)) {
242
+ // `color` resolves `currentColor` in borders, outlines and fills even with no
243
+ // text; the rest of the typography waits until tree building sees the content.
244
+ properties.push(CSS_PROPERTY_TYPE.color)
160
245
  }
161
246
  if (hasMediaProperties) {
162
247
  properties.push(...MEDIA_CSS_PROPERTIES)
@@ -165,6 +250,7 @@ export function getCssPropertiesForTag(tag: string, attributes: SemanticAttribut
165
250
  properties.push(...BOX_CSS_PROPERTIES)
166
251
  }
167
252
 
253
+ properties.push(...getMarginPropertiesForTag(tag))
168
254
  properties.push(CSS_PROPERTY_TYPE.display)
169
255
  return [...new Set(properties)]
170
256
  }
@@ -204,6 +290,27 @@ export function addGapProperty(existing: string[]): string[] {
204
290
  return [...existing, CSS_PROPERTY_TYPE.gap]
205
291
  }
206
292
 
293
+ /**
294
+ * Adds flex/grid container CSS properties to an existing property list without duplicates.
295
+ */
296
+ export function addFlexContainerProperties(existing: string[]): string[] {
297
+ const result = [...existing]
298
+ for (const property of FLEX_CONTAINER_CSS_PROPERTIES) {
299
+ if (!result.includes(property)) {
300
+ result.push(property)
301
+ }
302
+ }
303
+ return result
304
+ }
305
+
306
+ /**
307
+ * Adds the `alignSelf` CSS property to an existing property list if not already present.
308
+ */
309
+ export function addAlignSelfProperty(existing: string[]): string[] {
310
+ if (existing.includes(CSS_PROPERTY_TYPE.alignSelf)) return existing
311
+ return [...existing, CSS_PROPERTY_TYPE.alignSelf]
312
+ }
313
+
207
314
  // ─────────────────────────────────────────────────────────────────────────────
208
315
  // Display Value Resolution
209
316
  // ─────────────────────────────────────────────────────────────────────────────
@@ -397,7 +504,7 @@ function hasUnresolvableDisplayVar(matcherData: MatchedCssData): boolean {
397
504
  return resolveCssPropertyValue(displayProperty, matcherData.customProperties) === undefined
398
505
  }
399
506
 
400
- function hasDisplayDeclaration(matcherData: MatchedCssData): boolean {
507
+ function hasLiteralDisplayDeclaration(matcherData: MatchedCssData): boolean {
401
508
  for (const match of matcherData.matches) {
402
509
  for (const property of match.properties) {
403
510
  if (property.name === 'display') return true
@@ -406,14 +513,30 @@ function hasDisplayDeclaration(matcherData: MatchedCssData): boolean {
406
513
  return false
407
514
  }
408
515
 
516
+ export const ROOT_DISPLAY_CUSTOM_PROPERTY = '--display'
517
+
518
+ function hasEffectiveDisplayDeclaration(matcherData: MatchedCssData): boolean {
519
+ return (
520
+ hasLiteralDisplayDeclaration(matcherData) ||
521
+ matcherData.customProperties[ROOT_DISPLAY_CUSTOM_PROPERTY] !== undefined
522
+ )
523
+ }
524
+
525
+ function resolveEffectiveDisplayValue(matcherData: MatchedCssData): string | undefined {
526
+ const resolved = resolveDisplayValue(matcherData)
527
+ if (resolved !== undefined) return resolved
528
+ if (hasLiteralDisplayDeclaration(matcherData)) return undefined
529
+ return matcherData.customProperties[ROOT_DISPLAY_CUSTOM_PROPERTY]
530
+ }
531
+
409
532
  export function hasFlexOrGridDisplay(matcherData: MatchedCssData): boolean {
410
- const displayValue = resolveDisplayValue(matcherData)
533
+ const displayValue = resolveEffectiveDisplayValue(matcherData)
411
534
  if (displayValue !== undefined) return displayUsesFlexOrGridLayout(displayValue)
412
535
  return hasUnresolvableDisplayVar(matcherData)
413
536
  }
414
537
 
415
538
  export function hasContainerLikeDisplay(matcherData: MatchedCssData): boolean {
416
- const displayValue = resolveDisplayValue(matcherData)
539
+ const displayValue = resolveEffectiveDisplayValue(matcherData)
417
540
  if (displayValue !== undefined) return displayCreatesContainerBox(displayValue)
418
541
  return hasUnresolvableDisplayVar(matcherData)
419
542
  }
@@ -441,7 +564,7 @@ function hasDefaultBoxProperties(tag: string, role?: string): boolean {
441
564
  }
442
565
 
443
566
  function hasEffectiveContainerDisplay(tag: string, role?: string, matcherData?: MatchedCssData): boolean {
444
- if (matcherData && hasDisplayDeclaration(matcherData)) {
567
+ if (matcherData && hasEffectiveDisplayDeclaration(matcherData)) {
445
568
  return hasContainerLikeDisplay(matcherData)
446
569
  }
447
570
 
@@ -473,6 +596,40 @@ export function enrichGapProperties(elements: ExtractedElement[]): ExtractedElem
473
596
  })
474
597
  }
475
598
 
599
+ /**
600
+ * Walks the element tree and adds flex/grid layout controls to relevant CSS
601
+ * properties: `flexDirection`/`justifyContent`/`alignItems` on elements whose
602
+ * own display is flex|grid, and `alignSelf` on their direct children (the
603
+ * property that actually targets flex/grid items per the CSS spec).
604
+ * Must be called after CSS selector matching (requires css-matcher data).
605
+ */
606
+ export function enrichFlexProperties(
607
+ elements: ExtractedElement[],
608
+ parentIsFlexOrGridContainer = false,
609
+ ): ExtractedElement[] {
610
+ return elements.map((element) => {
611
+ const matcherData = element.extractorData.get('css-matcher') as MatchedCssData | undefined
612
+ const isFlexOrGridContainer = matcherData ? hasFlexOrGridDisplay(matcherData) : false
613
+ const cssData = element.extractorData.get('css-properties') as CssPropertiesData | undefined
614
+
615
+ if (cssData) {
616
+ let relevant = cssData.relevant
617
+ if (isFlexOrGridContainer) {
618
+ relevant = addFlexContainerProperties(relevant)
619
+ }
620
+ if (parentIsFlexOrGridContainer) {
621
+ relevant = addAlignSelfProperty(relevant)
622
+ }
623
+ element.extractorData.set('css-properties', { relevant })
624
+ }
625
+
626
+ return {
627
+ ...element,
628
+ children: enrichFlexProperties(element.children, isFlexOrGridContainer),
629
+ }
630
+ })
631
+ }
632
+
476
633
  /**
477
634
  * Walks the element tree and adds box properties to selected text tags
478
635
  * when CSS overrides `display` or default UA display creates a box.
@@ -28,8 +28,10 @@ export {
28
28
  createCssPropertiesExtractor,
29
29
  enrichGapProperties,
30
30
  enrichContainerProperties,
31
+ enrichFlexProperties,
31
32
  resolveDisplayValue,
32
33
  getDefaultDisplayForTag,
34
+ ROOT_DISPLAY_CUSTOM_PROPERTY,
33
35
  } from './css-properties'
34
36
  export type { CssPropertiesData } from './css-properties'
35
37
 
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
2
2
  import { createRefElementMarker } from '../../../extensions/ref-elements/path-utils'
3
3
  import type { RefElementMatch } from '../../../extensions/ref-elements/types'
4
4
  import { ExtractorStore } from './core/store'
5
- import { createPropTrackerExtractor } from './prop-tracker'
5
+ import { type PropTrackerData, createPropTrackerExtractor } from './prop-tracker'
6
6
 
7
7
  function markRefElementComponent(runtimeComponent: (...args: unknown[]) => unknown, refComponentType: string): void {
8
8
  Object.defineProperty(
@@ -248,4 +248,22 @@ describe('createPropTrackerExtractor', () => {
248
248
  selector: '.card-cta',
249
249
  })
250
250
  })
251
+
252
+ it('persists the empty-slot signal onto the element data', () => {
253
+ const store = new ExtractorStore()
254
+ const { extractor } = createPropTrackerExtractor()
255
+
256
+ extractor.onCreateElement?.({
257
+ type: 'button',
258
+ isDomElement: true,
259
+ tag: 'button',
260
+ traceId: 'trace-1',
261
+ props: { className: 'trigger' },
262
+ children: [],
263
+ hasEmptySlot: true,
264
+ store,
265
+ })
266
+
267
+ expect(store.get<PropTrackerData>('trace-1', 'prop-tracker')?.hasEmptySlot).toBe(true)
268
+ })
251
269
  })
@@ -26,6 +26,8 @@ export interface PropTrackerData {
26
26
  concatenatedAttrs: Map<string, string> // attrName → propName (for concatenated values)
27
27
  /** on*-handler prop names whose value is a function on this element (e.g. ['onClick', 'onChange']). */
28
28
  eventHandlers: string[]
29
+ /** True when a child slot rendered nothing, so the content is unknown. */
30
+ hasEmptySlot: boolean
29
31
  }
30
32
 
31
33
  export interface PropTrackerExtractorState {
@@ -170,6 +172,7 @@ export function createPropTrackerExtractor(options?: CreatePropTrackerExtractorO
170
172
  boundProps: [...boundProps],
171
173
  concatenatedAttrs,
172
174
  eventHandlers,
175
+ hasEmptySlot: event.hasEmptySlot ?? false,
173
176
  } satisfies PropTrackerData)
174
177
 
175
178
  attachMatchedRefElement(traceId, props, store)
@@ -15,6 +15,7 @@ function makeElement(input: {
15
15
  boundProps: [],
16
16
  concatenatedAttrs: new Map(),
17
17
  eventHandlers: input.eventHandlers ?? [],
18
+ hasEmptySlot: false,
18
19
  }
19
20
  return {
20
21
  traceId: 'trace-0',
@@ -18,8 +18,10 @@ export {
18
18
  createCssPropertiesExtractor,
19
19
  enrichGapProperties,
20
20
  enrichContainerProperties,
21
+ enrichFlexProperties,
21
22
  resolveDisplayValue,
22
23
  getDefaultDisplayForTag,
24
+ ROOT_DISPLAY_CUSTOM_PROPERTY,
23
25
  inferSupportedNativeStates,
24
26
  } from './extractors'
25
27
 
@@ -10,6 +10,7 @@ import {
10
10
  createCssPropertiesExtractor,
11
11
  createPropTrackerExtractor,
12
12
  enrichContainerProperties,
13
+ enrichFlexProperties,
13
14
  enrichGapProperties,
14
15
  runExtractors,
15
16
  } from './information-extractors/react'
@@ -151,9 +152,10 @@ export function processComponent(
151
152
  const matchResult = matchCssSelectors(html, extractedElements, css)
152
153
  const containerEnrichedElements = enrichContainerProperties(matchResult.elements)
153
154
  const gapEnrichedElements = enrichGapProperties(containerEnrichedElements)
155
+ const flexEnrichedElements = enrichFlexProperties(gapEnrichedElements)
154
156
  enhancedInfo = {
155
157
  ...enhancedInfo,
156
- elements: convertElements(gapEnrichedElements),
158
+ elements: convertElements(flexEnrichedElements),
157
159
  }
158
160
  varUsedByTraceId = matchResult.varUsedByTraceId
159
161
  } catch (thrownError) {