@wix/zero-config-implementation 1.99.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.
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "registry": "https://registry.npmjs.org/",
5
5
  "access": "public"
6
6
  },
7
- "version": "1.99.0",
7
+ "version": "1.100.0",
8
8
  "description": "Core library for extracting component manifests from JS and CSS files",
9
9
  "type": "module",
10
10
  "main": "dist/index.js",
@@ -106,5 +106,5 @@
106
106
  ]
107
107
  }
108
108
  },
109
- "falconPackageHash": "905a961b05476216ee10b85e56b0d50818e5247abe594c0917619c0a"
109
+ "falconPackageHash": "80ef17ffc1549b6c4d3f51396955cb0517fe8eceb7b20eed65fc7e7d"
110
110
  }
@@ -131,6 +131,41 @@ describe('resolveLonghand', () => {
131
131
  expect(resolveLonghand('paddingTop', new Map())).toBeUndefined()
132
132
  })
133
133
 
134
+ describe('margin family', () => {
135
+ it('marginTop falls back through marginBlockStart → marginBlock → margin', () => {
136
+ expect(resolveLonghand('marginTop', new Map([['marginBlockStart', '1px']]))).toBe('1px')
137
+ expect(resolveLonghand('marginTop', new Map([['marginBlock', '4px 8px']]))).toBe('4px')
138
+ expect(resolveLonghand('marginTop', new Map([['margin', '1rem 2rem']]))).toBe('1rem')
139
+ })
140
+
141
+ it('marginBottom falls back through marginBlockEnd → marginBlock → margin', () => {
142
+ expect(resolveLonghand('marginBottom', new Map([['marginBlockEnd', '2px']]))).toBe('2px')
143
+ expect(resolveLonghand('marginBottom', new Map([['marginBlock', '4px 8px']]))).toBe('8px')
144
+ expect(resolveLonghand('marginBottom', new Map([['margin', '1rem 2rem']]))).toBe('1rem')
145
+ })
146
+
147
+ it('marginInlineStart falls back through marginLeft → marginInline → margin', () => {
148
+ expect(resolveLonghand('marginInlineStart', new Map([['marginLeft', '7px']]))).toBe('7px')
149
+ expect(resolveLonghand('marginInlineStart', new Map([['marginInline', '12px']]))).toBe('12px')
150
+ expect(resolveLonghand('marginInlineStart', new Map([['marginInline', '12px 24px']]))).toBe('12px')
151
+ expect(resolveLonghand('marginInlineStart', new Map([['margin', '1px 2px 3px 4px']]))).toBe('4px')
152
+ })
153
+
154
+ it('marginInlineEnd falls back through marginRight → marginInline → margin', () => {
155
+ expect(resolveLonghand('marginInlineEnd', new Map([['marginRight', '9px']]))).toBe('9px')
156
+ expect(resolveLonghand('marginInlineEnd', new Map([['marginInline', '12px 24px']]))).toBe('24px')
157
+ expect(resolveLonghand('marginInlineEnd', new Map([['margin', '1px 2px 3px 4px']]))).toBe('2px')
158
+ })
159
+
160
+ it('prefers the literal longhand over any fallback', () => {
161
+ const values = new Map([
162
+ ['marginTop', '5px'],
163
+ ['margin', '10px'],
164
+ ])
165
+ expect(resolveLonghand('marginTop', values)).toBe('5px')
166
+ })
167
+ })
168
+
134
169
  describe('padding family', () => {
135
170
  it('paddingTop falls back through paddingBlockStart → paddingBlock → padding', () => {
136
171
  expect(resolveLonghand('paddingTop', new Map([['paddingBlockStart', '1px']]))).toBe('1px')
@@ -215,6 +215,18 @@ const fromTextDecorationLine: Fallback = (values) => {
215
215
  return lineTokens.join(' ')
216
216
  }
217
217
 
218
+ /**
219
+ * `flex-flow: <flex-direction> || <flex-wrap>` — order is free and either
220
+ * part may be omitted. Pick out just the direction keyword for the schema.
221
+ */
222
+ const FLEX_DIRECTION_KEYWORDS = new Set(['row', 'row-reverse', 'column', 'column-reverse'])
223
+
224
+ const fromFlexFlowDirection: Fallback = (values) => {
225
+ const shorthand = values.get('flexFlow')
226
+ if (shorthand === undefined) return undefined
227
+ return splitTopLevelTokens(shorthand).find((token) => FLEX_DIRECTION_KEYWORDS.has(token.toLowerCase()))
228
+ }
229
+
218
230
  // ─────────────────────────────────────────────────────────────────────────────
219
231
  // Resolution table
220
232
  //
@@ -233,6 +245,12 @@ const FALLBACKS: Partial<Record<CssPropertyName, Fallback[]>> = {
233
245
  paddingInlineStart: [fromKey('paddingLeft'), fromAxis('paddingInline', 'start'), fromBox('padding', 'left')],
234
246
  paddingInlineEnd: [fromKey('paddingRight'), fromAxis('paddingInline', 'end'), fromBox('padding', 'right')],
235
247
 
248
+ // Margin — same block/inline convention as padding above.
249
+ marginTop: [fromKey('marginBlockStart'), fromAxis('marginBlock', 'start'), fromBox('margin', 'top')],
250
+ marginBottom: [fromKey('marginBlockEnd'), fromAxis('marginBlock', 'end'), fromBox('margin', 'bottom')],
251
+ marginInlineStart: [fromKey('marginLeft'), fromAxis('marginInline', 'start'), fromBox('margin', 'left')],
252
+ marginInlineEnd: [fromKey('marginRight'), fromAxis('marginInline', 'end'), fromBox('margin', 'right')],
253
+
236
254
  // Border radius — schema uses all-logical corner names; physical names map under LTR/horizontal-tb.
237
255
  borderStartStartRadius: [fromKey('borderTopLeftRadius'), fromCorner('borderRadius', 'tl')],
238
256
  borderStartEndRadius: [fromKey('borderTopRightRadius'), fromCorner('borderRadius', 'tr')],
@@ -255,6 +273,10 @@ const FALLBACKS: Partial<Record<CssPropertyName, Fallback[]>> = {
255
273
  // and color in one declaration. Pick out just the line tokens for the schema.
256
274
  textDecorationLine: [fromTextDecorationLine],
257
275
 
276
+ // Flex — `flex-flow: column wrap` carries both direction and wrap; pick out
277
+ // just the direction keyword for the schema.
278
+ flexDirection: [fromFlexFlowDirection],
279
+
258
280
  // Shorthand targets (e.g. `font`) live in css-shorthand-composer.ts and are
259
281
  // merged in here — composer-driven, conceptually the inverse of the rows above.
260
282
  ...SHORTHAND_FALLBACKS,
@@ -346,6 +346,72 @@ describe('toEditorReactComponent — cssProperties defaultValue', () => {
346
346
  defaultValue: 'var(--wst-primary-background-color)',
347
347
  })
348
348
  })
349
+
350
+ it('excludes width/height from the root element (sizing is handled via layout config)', () => {
351
+ const extractorData = new Map<string, unknown>()
352
+ extractorData.set('css-matcher', matcherDataFromCss('width: 100px; height: 50px; overflow: hidden;'))
353
+ extractorData.set('css-properties', {
354
+ relevant: [
355
+ CSS_PROPERTIES.CSS_PROPERTY_TYPE.width,
356
+ CSS_PROPERTIES.CSS_PROPERTY_TYPE.height,
357
+ CSS_PROPERTIES.CSS_PROPERTY_TYPE.overflow,
358
+ ],
359
+ })
360
+
361
+ const rootElement: ExtractedElement = {
362
+ traceId: 'trace-root',
363
+ name: 'root',
364
+ tag: 'div',
365
+ attributes: {},
366
+ extractorData,
367
+ children: [],
368
+ }
369
+
370
+ const result = toEditorReactComponent(createComponent(rootElement))
371
+
372
+ expect(result.editorElement?.cssProperties?.width).toBeUndefined()
373
+ expect(result.editorElement?.cssProperties?.height).toBeUndefined()
374
+ expect(result.editorElement?.cssProperties?.overflow).toEqual({ defaultValue: 'hidden' })
375
+ })
376
+
377
+ it('keeps width/height on inner (non-root) elements', () => {
378
+ const rootExtractorData = new Map<string, unknown>()
379
+ rootExtractorData.set('css-matcher', matcherDataFromCss(''))
380
+ rootExtractorData.set('css-properties', { relevant: [] })
381
+
382
+ const childExtractorData = new Map<string, unknown>()
383
+ childExtractorData.set('css-matcher', matcherDataFromCss('width: 24px; height: 24px;'))
384
+ childExtractorData.set('css-properties', {
385
+ relevant: [CSS_PROPERTIES.CSS_PROPERTY_TYPE.width, CSS_PROPERTIES.CSS_PROPERTY_TYPE.height],
386
+ })
387
+
388
+ const childElement: ExtractedElement = {
389
+ traceId: 'trace-icon',
390
+ name: 'icon',
391
+ tag: 'img',
392
+ attributes: {},
393
+ extractorData: childExtractorData,
394
+ children: [],
395
+ }
396
+
397
+ const rootElement: ExtractedElement = {
398
+ traceId: 'trace-root',
399
+ name: 'root',
400
+ tag: 'div',
401
+ attributes: {},
402
+ extractorData: rootExtractorData,
403
+ children: [childElement],
404
+ }
405
+
406
+ const result = toEditorReactComponent(createComponent(rootElement))
407
+
408
+ expect(result.editorElement?.elements?.icon?.inlineElement?.cssProperties?.width).toEqual({
409
+ defaultValue: '24px',
410
+ })
411
+ expect(result.editorElement?.elements?.icon?.inlineElement?.cssProperties?.height).toEqual({
412
+ defaultValue: '24px',
413
+ })
414
+ })
349
415
  })
350
416
 
351
417
  describe('toEditorReactComponent selector conversion', () => {
@@ -25,7 +25,11 @@ import type {
25
25
  ExtractedElement,
26
26
  TrackingStores,
27
27
  } from '../information-extractors/react'
28
- import { getDefaultDisplayForTag, resolveDisplayValue } from '../information-extractors/react'
28
+ import {
29
+ ROOT_DISPLAY_CUSTOM_PROPERTY,
30
+ getDefaultDisplayForTag,
31
+ resolveDisplayValue,
32
+ } from '../information-extractors/react'
29
33
  import { findPreferredSemanticClass, normalizeClassNames } from '../utils/css-class'
30
34
  import { buildActiveItemDisplayGroups, buildActiveItemIndexDisplayFilters } from './active-item-index-builder'
31
35
  import { resolveLonghand } from './css-longhand-resolver'
@@ -307,7 +311,7 @@ function buildNearestCommonAncestorCustomProps(
307
311
  const nearestCommonAncestorCustomProps = new Map<string, Record<string, CssCustomPropertyItem>>()
308
312
 
309
313
  for (const varName of varNames) {
310
- if (varName === '--display') continue
314
+ if (varName === ROOT_DISPLAY_CUSTOM_PROPERTY) continue
311
315
 
312
316
  const registration = registeredByVar.get(varName)
313
317
  // Prefer a concrete declared value; fall back to the @property initial-value so a
@@ -563,6 +567,10 @@ function toDisplayEnumValue(cssValue: string): DisplayEnumValue {
563
567
  return (CSS_DISPLAY_TO_ENUM[cssValue] ?? cssValue) as DisplayEnumValue
564
568
  }
565
569
 
570
+ // Root sizing is handled separately via layout config, so `width`/`height`
571
+ // are excluded from the root element's own `cssProperties`.
572
+ const ROOT_EXCLUDED_CSS_PROPERTIES = new Set<string>([CSS_PROPERTY_TYPE.width, CSS_PROPERTY_TYPE.height])
573
+
566
574
  function buildCssProperties(
567
575
  element: ExtractedElement | undefined,
568
576
  isRootElement: boolean,
@@ -576,6 +584,9 @@ function buildCssProperties(
576
584
  }
577
585
  const cssPropertyValues = element ? getMatchedPropertyValues(element) : new Map<string, string>()
578
586
  for (const propName of decidedProperties) {
587
+ if (isRootElement && ROOT_EXCLUDED_CSS_PROPERTIES.has(propName)) {
588
+ continue
589
+ }
579
590
  if (propName === CSS_PROPERTY_TYPE.display && element) {
580
591
  if (!isRootElement) {
581
592
  result[propName] = buildDisplayProperty(element)
@@ -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,
@@ -252,6 +253,131 @@ describe('getCssPropertiesForTag', () => {
252
253
  expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
253
254
  expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
254
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
+ })
255
381
  })
256
382
 
257
383
  describe('getDefaultDisplayForTag', () => {
@@ -404,4 +530,14 @@ describe('enrichContainerProperties', () => {
404
530
  expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
405
531
  expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
406
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
+ })
407
543
  })
@@ -99,6 +99,70 @@ const BOX_CSS_PROPERTIES = [
99
99
  CSS_PROPERTY_TYPE.borderEndStartRadius,
100
100
  CSS_PROPERTY_TYPE.borderEndEndRadius,
101
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,
102
166
  ]
103
167
 
104
168
  /**
@@ -128,6 +192,12 @@ const MEDIA_CSS_PROPERTIES = [
128
192
  CSS_PROPERTY_TYPE.borderEndStartRadius,
129
193
  CSS_PROPERTY_TYPE.borderEndEndRadius,
130
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,
131
201
  ]
132
202
 
133
203
  function hasTextSemantics(tag: string, attributes: SemanticAttributes = {}): boolean {
@@ -180,6 +250,7 @@ export function getCssPropertiesForTag(tag: string, attributes: SemanticAttribut
180
250
  properties.push(...BOX_CSS_PROPERTIES)
181
251
  }
182
252
 
253
+ properties.push(...getMarginPropertiesForTag(tag))
183
254
  properties.push(CSS_PROPERTY_TYPE.display)
184
255
  return [...new Set(properties)]
185
256
  }
@@ -219,6 +290,27 @@ export function addGapProperty(existing: string[]): string[] {
219
290
  return [...existing, CSS_PROPERTY_TYPE.gap]
220
291
  }
221
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
+
222
314
  // ─────────────────────────────────────────────────────────────────────────────
223
315
  // Display Value Resolution
224
316
  // ─────────────────────────────────────────────────────────────────────────────
@@ -412,7 +504,7 @@ function hasUnresolvableDisplayVar(matcherData: MatchedCssData): boolean {
412
504
  return resolveCssPropertyValue(displayProperty, matcherData.customProperties) === undefined
413
505
  }
414
506
 
415
- function hasDisplayDeclaration(matcherData: MatchedCssData): boolean {
507
+ function hasLiteralDisplayDeclaration(matcherData: MatchedCssData): boolean {
416
508
  for (const match of matcherData.matches) {
417
509
  for (const property of match.properties) {
418
510
  if (property.name === 'display') return true
@@ -421,14 +513,30 @@ function hasDisplayDeclaration(matcherData: MatchedCssData): boolean {
421
513
  return false
422
514
  }
423
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
+
424
532
  export function hasFlexOrGridDisplay(matcherData: MatchedCssData): boolean {
425
- const displayValue = resolveDisplayValue(matcherData)
533
+ const displayValue = resolveEffectiveDisplayValue(matcherData)
426
534
  if (displayValue !== undefined) return displayUsesFlexOrGridLayout(displayValue)
427
535
  return hasUnresolvableDisplayVar(matcherData)
428
536
  }
429
537
 
430
538
  export function hasContainerLikeDisplay(matcherData: MatchedCssData): boolean {
431
- const displayValue = resolveDisplayValue(matcherData)
539
+ const displayValue = resolveEffectiveDisplayValue(matcherData)
432
540
  if (displayValue !== undefined) return displayCreatesContainerBox(displayValue)
433
541
  return hasUnresolvableDisplayVar(matcherData)
434
542
  }
@@ -456,7 +564,7 @@ function hasDefaultBoxProperties(tag: string, role?: string): boolean {
456
564
  }
457
565
 
458
566
  function hasEffectiveContainerDisplay(tag: string, role?: string, matcherData?: MatchedCssData): boolean {
459
- if (matcherData && hasDisplayDeclaration(matcherData)) {
567
+ if (matcherData && hasEffectiveDisplayDeclaration(matcherData)) {
460
568
  return hasContainerLikeDisplay(matcherData)
461
569
  }
462
570
 
@@ -488,6 +596,40 @@ export function enrichGapProperties(elements: ExtractedElement[]): ExtractedElem
488
596
  })
489
597
  }
490
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
+
491
633
  /**
492
634
  * Walks the element tree and adds box properties to selected text tags
493
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
 
@@ -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) {