You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Part of #304 (catalog-search component coverage gaps).
Spec: ADR-0011 — "Catalog-search designer ergonomics: a tiered, progressively-disclosed data surface" (lives in the ISO storefront repo, which has no remote; decisions quoted inline below). Companion to the ADR-0006 field-display and ADR-0007 curated-extensions-map work already shipped via #339 / #350.
What to build
Bring the catalog-search components up to the ADR-0006/0007 insulation floor, applying one governing principle — progressive disclosure: make the common case a discoverable, zero-lookup, zero-wiring action; keep every advanced path reachable underneath.
Governing guarantee — no enhancement removes a capability. Every convenience is opt-out to the exact prior behaviour, and every underlying value/function (raw extensions/rawData, toggle/goTo, EPClearRefinements, raw pages) remains. The simple path is added beside the powerful one, never on top of it.
The work is two independent, end-to-end vertical slices (each cuts normalizer → component API → mock/defaults → demoable on a search page). Either may be grabbed first; they touch different components.
Slice 1 — Discoverable, tiered hit data surface
Today a designer must type ($ctx.currentProduct.extensions["products(iso-standard)"] || {}).title — a throw-guarded, slug-bracketed lookup that doesn't autocomplete in the data picker. Replace with a tiered surface:
(D7, prerequisite) Shared base Product contract both normalizers (normalizeHitToCurrentProduct and the PDP shopper-SDK path) produce identically via shared utils/ primitives (extensions map, formatCurrency, image/highlight resolution). Each source extends it with a typed superset (SearchHitProduct extends Product adds _highlighted*/_score/_rawTypesenseHit). Input-key mapping stays per-source. A hit that lacks a base field omits it (presence reads absent) — never synthesises an empty.
(D1)EPSearchHits publishes per hit: a flat, null-safe, slug-free currentProduct.fields.<key> namespace flattening a configurable primaryExtensionTemplate prop (Tier 1, discoverable), and the ADR-0007 slug-keyed $ctx.productExtensions Proxy map (Tier 2). Raw extensions/rawData retained (Tier 3).
(D3, fields) Add highlight: auto | on | off and a titlecase option to EPProductField's format enum. auto renders the highlighted/snippet variant when present (in a hit) and the plain value otherwise (inert on the PDP). The ADR-0006 field components are the Tier-0 GUI-selector for hit fields — they already resolve in hit cards via the rawData bridge.
Today scope pills and pagination are hand-wired with customFunction calls ($ctx.currentRefinement.toggle(), $ctx.searchPaginationData.goTo()) and the "All" pill is a second glued-on component.
(D2) Auto-wire interactive items via the prop-getter pattern the autocomplete already uses (getItemProps() spreads onClick + role + aria-*): the component owns click + active/current state + a11y; the designer only styles the slot. Auto-wiring is a defaulted-on prop whose OFF state is byte-for-byte today's behaviour; toggle/goTo remain on context as the Tier-1 escape. (Supersedes the earlier "rename goTo vs goToPage" idea.)
(D5) Split single-select out of EPRefinementList into a dedicated EPSingleSelectFacet (useMenu-backed, replace semantics, radio a11y). Per-item context stays identical (value/label/count/isRefined/toggle) so slot content ports across a mode change. Generic name (no ISO knowledge in the package).
(D4) Enrich EPSearchPagination's exposed page-item model with ellipsis sentinels and isFirst/isLast/isCurrent flags (window size as a prop). Keep raw pages: number[] + goTo.
(D3, provider)EPCatalogSearchProvider gains excludeVariationChildren (default true) emitting meta.product_types:!=child, AND'd with any baseFilter. Deliberate non-additive default — the common case is near-universally correct; no external consumers depend on the old behaviour.
(D8) Add a single flat "EP Catalog Search"section to group the family in the insert menu (parentComponentName already handles in-context surfacing).
Acceptance criteria
Slice 1
Both normalizers produce an identical shared base Product; SearchHitProduct superset is typed; a base field absent from a hit reads as absent (presence false), never throws.
A designer binds currentProduct.fields.<key> (autocompletes in the picker) with no slug bracket and no (… || {}).
EPProductField with highlight: auto renders the <mark> name/abstract in a hit and the plain value on a PDP; titlecase renders product_kind/lifecycle_status without a ternary.
EPSearchHits default slot renders a working card from the field components against mock data; mock and runtime produce the same DOM tree (assertHeadlessStylingContract passes).
Slice 2
Refinement items and page items refine/navigate on click with nocustomFunction wired; active/current state and ARIA come for free; auto-wiring disables to today's function-on-context behaviour.
EPSingleSelectFacet exists (radio/menu semantics) with opt-in includeAllOption; EPRefinementList + EPClearRefinements still work unchanged.
Parent PRD
Part of #304 (catalog-search component coverage gaps).
Spec: ADR-0011 — "Catalog-search designer ergonomics: a tiered, progressively-disclosed data surface" (lives in the ISO storefront repo, which has no remote; decisions quoted inline below). Companion to the ADR-0006 field-display and ADR-0007 curated-extensions-map work already shipped via #339 / #350.
What to build
Bring the catalog-search components up to the ADR-0006/0007 insulation floor, applying one governing principle — progressive disclosure: make the common case a discoverable, zero-lookup, zero-wiring action; keep every advanced path reachable underneath.
The work is two independent, end-to-end vertical slices (each cuts normalizer → component API → mock/defaults → demoable on a search page). Either may be grabbed first; they touch different components.
Slice 1 — Discoverable, tiered hit data surface
Today a designer must type
($ctx.currentProduct.extensions["products(iso-standard)"] || {}).title— a throw-guarded, slug-bracketed lookup that doesn't autocomplete in the data picker. Replace with a tiered surface:Productcontract both normalizers (normalizeHitToCurrentProductand the PDP shopper-SDK path) produce identically via sharedutils/primitives (extensions map,formatCurrency, image/highlight resolution). Each source extends it with a typed superset (SearchHitProduct extends Productadds_highlighted*/_score/_rawTypesenseHit). Input-key mapping stays per-source. A hit that lacks a base field omits it (presence reads absent) — never synthesises an empty.EPSearchHitspublishes per hit: a flat, null-safe, slug-freecurrentProduct.fields.<key>namespace flattening a configurableprimaryExtensionTemplateprop (Tier 1, discoverable), and the ADR-0007 slug-keyed$ctx.productExtensionsProxy map (Tier 2). Rawextensions/rawDataretained (Tier 3).highlight: auto | on | offand atitlecaseoption toEPProductField'sformatenum.autorenders the highlighted/snippet variant when present (in a hit) and the plain value otherwise (inert on the PDP). The ADR-0006 field components are the Tier-0 GUI-selector for hit fields — they already resolve in hit cards via therawDatabridge.EPSearchHitsships a batteries-included-but-unstyled default card built from the field components; collapse its divergent mock tree to a single render (data wrapper holds the hook, pure inner takesitems) per the PRD: EP catalog-search components must honour designer styling (headless styling contract) #305 contract.Slice 2 — Zero-wiring facets & pagination
Today scope pills and pagination are hand-wired with
customFunctioncalls ($ctx.currentRefinement.toggle(),$ctx.searchPaginationData.goTo()) and the "All" pill is a second glued-on component.getItemProps()spreadsonClick+role+aria-*): the component owns click + active/current state + a11y; the designer only styles the slot. Auto-wiring is a defaulted-on prop whose OFF state is byte-for-byte today's behaviour;toggle/goToremain on context as the Tier-1 escape. (Supersedes the earlier "renamegoTovsgoToPage" idea.)EPRefinementListinto a dedicatedEPSingleSelectFacet(useMenu-backed, replace semantics, radio a11y). Per-item context stays identical (value/label/count/isRefined/toggle) so slot content ports across a mode change. Generic name (no ISO knowledge in the package).EPSingleSelectFacetgains opt-inincludeAllOptionemitting an "All" pseudo-item (isRefinedwhen nothing refined;toggleclears), flaggedisAllOption. Default off;EPRefinementList+EPClearRefinementstwo-component path stays working.EPSearchPagination's exposed page-item model with ellipsis sentinels andisFirst/isLast/isCurrentflags (window size as a prop). Keep rawpages: number[]+goTo.EPRefinementList's divergent mock tree per the PRD: EP catalog-search components must honour designer styling (headless styling contract) #305 contract.Cross-cutting (do in whichever slice lands first)
EPCatalogSearchProvidergainsexcludeVariationChildren(default true) emittingmeta.product_types:!=child, AND'd with anybaseFilter. Deliberate non-additive default — the common case is near-universally correct; no external consumers depend on the old behaviour."EP Catalog Search"sectionto group the family in the insert menu (parentComponentNamealready handles in-context surfacing).Acceptance criteria
Slice 1
Product;SearchHitProductsuperset is typed; a base field absent from a hit reads as absent (presence false), never throws.currentProduct.fields.<key>(autocompletes in the picker) with no slug bracket and no(… || {}).EPProductFieldwithhighlight: autorenders the<mark>name/abstract in a hit and the plain value on a PDP;titlecaserendersproduct_kind/lifecycle_statuswithout a ternary.EPSearchHitsdefault slot renders a working card from the field components against mock data; mock and runtime produce the same DOM tree (assertHeadlessStylingContractpasses).Slice 2
customFunctionwired; active/current state and ARIA come for free; auto-wiring disables to today's function-on-context behaviour.EPSingleSelectFacetexists (radio/menu semantics) with opt-inincludeAllOption;EPRefinementList+EPClearRefinementsstill work unchanged.EPSearchPaginationexposes windowed page items (ellipsis + first/last/current); rawpages/goToretained.EPRefinementListmock collapses to a single render;assertHeadlessStylingContractpasses.Cross-cutting
excludeVariationChildrendefaults on, emits the correct filter, AND's withbaseFilter; documented as a behaviour change."EP Catalog Search"insert-menu section.Blocked by
None — can start immediately. The two slices are independent (different components); if doing one first, Slice 1 is the higher-value foundation.
ADR decisions addressed