Skip to content

feat(ep-commerce): tiered designer data surface for catalog-search (ADR-0011)#356

Open
field123 wants to merge 1 commit into
masterfrom
feat/catalog-search-dx-355
Open

feat(ep-commerce): tiered designer data surface for catalog-search (ADR-0011)#356
field123 wants to merge 1 commit into
masterfrom
feat/catalog-search-dx-355

Conversation

@field123
Copy link
Copy Markdown
Collaborator

@field123 field123 commented Jun 5, 2026

Summary

Brings the catalog-search components up to the ADR-0006/0007 insulation floor under one governing principle — progressive disclosure: the common case is a discoverable, zero-lookup, zero-wiring action; every advanced path stays reachable underneath; and no enhancement removes a capability (every convenience opt-outs to the exact prior behaviour).

Implements ADR-0011 ("Catalog-search designer ergonomics: a tiered, progressively-disclosed data surface"). Closes #355 (under the #304 umbrella).

Slice 1 — discoverable, tiered hit data surface

  • D7 (shared base Product contract): extract the hit normalizer into utils/normalize-hit.ts (normalizeSearchHitSearchHitProduct extends Product), built from the same primitives the PDP path uses (formatCurrency, buildExtensionsMap, normalizeExtensions). A hit's currentProduct is shape-identical to the PDP's, so the ADR-0006 field components and bindings work unchanged across PDP + search. Absent base fields read as absent (never throw).
  • D1 (discoverable hit fields): EPSearchHits publishes per hit
    • Tier 1 — $ctx.currentProduct.fields.<key>: flat, slug-free, null-safe, flattening a configurable primaryExtensionTemplate prop (autocompletes in the picker, no (… || {}), no slug brackets);
    • Tier 2 — $ctx.productExtensions["<slug>"].field: the ADR-0007 slug-keyed Proxy map, scoped per hit;
    • Tier 3 — raw extensions / rawData retained.
  • D3 (fields): fold highlight: auto | on | off and a titlecase format into EPProductField — one field-render surface across PDP + search. Only the backend's <mark> markup is ever rendered as HTML, never the raw value, so the html:true footgun is removed rather than handed to designers.
  • D8/D9: EPSearchHits ships a batteries-included-but-unstyled default card built from the field components, and collapses to a single render (pure inner fed items, the InstantSearch hook in the data wrapper) per the PRD: EP catalog-search components must honour designer styling (headless styling contract) #305 headless-styling contract.

Slice 2 — zero-wiring facets & pagination

  • D2 (auto-wiring): refinement items and single-select items are auto-wired via the prop-getter pattern the autocomplete already uses (onClick + keyboard + role + selection aria-*). A defaulted-on, disableable autoWire prop; OFF is byte-for-byte today's function-on-context behaviour, and toggle stays on context as the escape.
  • D5/D6 (EPSingleSelectFacet): new component (useMenu, replace semantics, radiogroup a11y) splitting singleSelect out of EPRefinementList. Per-item context is identical, so slot content ports across a mode change. Opt-in includeAllOption emits an "All" pseudo-item (refined when nothing is, clears on select) flagged isAllOption. EPRefinementList + EPClearRefinements keep working unchanged.
  • D4 (pagination windowing): EPSearchPagination exposes a windowed pageItems model — ellipsis sentinels, isFirst/isLast/isCurrent flags, a per-item pre-bound goTo, and a windowSize prop. Raw pages: number[] + goTo retained.

Cross-cutting

  • D3 (provider): EPCatalogSearchProvider gains excludeVariationChildren (default on) emitting meta.product_types:!=child, AND'd with baseFilter. Deliberate non-additive default — variation children near-universally don't belong in hits, and the package has no external consumers depending on the old behaviour. Turn off to include them.
  • D8 (insert section): every catalog-search component groups under one "EP Catalog Search" insert-menu section; the provider's default slot is a complete, working unstyled scaffold (search box + scope facet + hits + empty + pagination).

Verification

  • 1816 package tests pass (catalog-search-components jest suite + new unit/acceptance tests: normalize-hit, pagination-window, product-field-highlight, format-value titlecase, provider excludeVariationChildren, EPSingleSelectFacet).
  • assertHeadlessStylingContract passes for the new EPSingleSelectFacet and the refactored EPRefinementList / EPSearchHits.
  • tsc --noEmit clean across all touched files.
  • All enhancements opt-out to prior behaviour; existing component instances render without re-authoring.

Behaviour change to call out

excludeVariationChildren defaults on — searches that previously returned PXM variation children will stop doing so. This is intentional and documented above; set the prop to false to restore the old behaviour.

…DR-0011)

Bring the catalog-search components up to the ADR-0006/0007 insulation floor
with one governing principle — progressive disclosure: the common case is a
discoverable, zero-lookup, zero-wiring action, every advanced path reachable
underneath, and no enhancement removes a capability.

Slice 1 — discoverable, tiered hit data surface
- D7: shared base Product contract. Extract the hit normalizer into
  utils/normalize-hit.ts (normalizeSearchHit → SearchHitProduct extends Product),
  built from the shared primitives the PDP path uses (formatCurrency,
  buildExtensionsMap, normalizeExtensions). Absent base fields read as absent,
  never throw.
- D1: EPSearchHits publishes per hit currentProduct.fields.<key> (Tier 1, flat,
  slug-free, from a configurable primaryExtensionTemplate prop) and the
  slug-keyed $ctx.productExtensions Proxy map (Tier 2). Raw extensions/rawData
  retained (Tier 3).
- D3 (fields): fold highlight (auto|on|off) and a titlecase format into
  EPProductField — one field-render surface across PDP and search; the
  html:true footgun is contained, not handed to designers.
- D8/D9: EPSearchHits ships a working unstyled default card built from the field
  components and collapses to one render path (pure inner fed items, hook in the
  data wrapper) per the #305 contract.

Slice 2 — zero-wiring facets and pagination
- D2: auto-wire refinement and single-select items via the prop-getter pattern
  (click + keyboard + role + selection aria), a defaulted-on disableable prop;
  off is byte-for-byte the prior function-on-context behaviour.
- D5/D6: new EPSingleSelectFacet (useMenu, replace semantics, radiogroup a11y)
  with an opt-in All pseudo-item; EPRefinementList loses singleSelect but its
  per-item context is unchanged so slot content ports.
- D4: EPSearchPagination exposes a windowed pageItems model (ellipsis sentinels,
  first/last/current flags, per-item bound goTo, window-size prop); raw pages +
  goTo retained.

Cross-cutting
- D3 (provider): EPCatalogSearchProvider excludeVariationChildren (default on)
  emits meta.product_types:!=child, AND'd with baseFilter.
- D8: group the family under one "EP Catalog Search" insert-menu section;
  batteries-included provider scaffold.

All enhancements opt-out to prior behaviour; existing instances keep working.
Closes #355.
@field123 field123 force-pushed the feat/catalog-search-dx-355 branch from edda7c1 to eac0654 Compare June 8, 2026 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

EP Catalog Search: tiered, progressively-disclosed designer data surface (ADR-0011)

1 participant