Changelog
All notable changes to propeller-v2-core-ui are documented here.
[0.13.1] - 2026-10-30
Changed
- Docs use the published package names (PWP-1087). Install, import and
peer-dependency lines in the README and docs site now name
@propeller-commerce/propeller-v2-core-uiand@propeller-commerce/propeller-sdk-v2, installed from npm, instead of the unscoped names andgithub:URLs, which do not resolve. - No unpublished CMS adapters in the
CmsAdapterdocs (PWP-1088). The contract page no longer points at adapter packages that are not on npm; it says to write an adapter against the interface or start from the CMS providers in the public Next and Vue boilerplates. - Provider snippet matches the current React API (PWP-1086). The
isAuthenticatedexample passes the scope through<PropellerProvider value={…}>.
[0.13.0] - 2026-10-14
Added
shouldHandlePaginationClick/paginationRel— pagination as links. Grid pagination entries navigate to a different URL, so they are anchors with a realhrefrather than buttons: that is what gives the status-bar preview, open-in-new-tab and copy-link-address, and what makes a screen reader announce a link instead of a button. A plain left click is still handled in the client, soshouldHandlePaginationClickreports whether an event is the plain click to take over or one the browser must keep — a modified or middle click falls through untouched.paginationRelreturnsprev/nextfor the adjacent entries only, since two elements claiming to be the previous document would be wrong.
[0.12.0] - 2026-10-13
Added
incrementOrderableQuantity/decrementOrderableQuantity— directional stepping on the order quantity grid.resolveOrderableQuantityrounds to the nearest grid point, which is right for a typed value on blur but wrong for a stepper button: from a quantity already off the grid the nearest point can lie in the opposite direction, so "+" could move down and "-" could move up. With min 2 and step 3, "+" from 9 sent 8 and "-" from 10 sent 11. These two helpers always move in the requested direction, snapping to the next grid point above or below, and never go belowmin.
[0.11.0] - 2026-10-05
Added
resolveOrderableQuantity(value, min, step)— the order quantity grid. Resolves a requested quantity to one the customer may actually order: at leastmin, on themin + n*stepgrid, rounded to the nearest point. A zero or absent step means any quantity at or aboveminis orderable, and a non-finite value (a cleared input) resolves tomin. The rule existed in five places —AddToCartandQuickOrderin both packages, plus twice inline inCartItem's markup — and the copies had drifted: theCartItemones divided by an unguarded step and rejected a cleared field, so a cart line could sit off the grid and a typed quantity could not be corrected. Every caller now shares this one implementation.
[0.10.0] - 2026-10-02
Added
ImgComponentProps— a contract for replacing the low-level<img>.ImageComponentPropsreceives aproduct/clusterand resolves the URL itself, so it only fits surfaces that still hold the whole entity. Most image sites have already resolved a URL string by render time — cart rows, galleries, autosuggest, bundles — and could not use it, leaving a bare<img src="https://media.…">as the only option. Those images are fetched straight from the media CDN, which sends noX-Robots-Tagand serves norobots.txt, so they sit outside the storefront’s crawler directives and get no framework-level image optimisation. Keyed onsrc/altso a host can route every image through a framework-native component. Additive — existingimageComponentimplementors are unaffected. (PWP-1001)
[0.9.0] - 2026-09-24
Added
isContentHiddentakes an optionalisAuthenticated. Hosts paint from a cached hint before the full profile arrives, souseris null for an authenticated visitor for the first frames and semi-closed surfaces flashed their logged-out state: a signed-in shopper saw the log-in call to action and blanked prices untilgetViewer()resolved. The host’s ownisLoadingis already false by then, so it cannot serve this purpose. Omitting the argument keeps the previous behaviour, so server renders need no change. (PWP-989)AddToCartComponentPropsgainscreateCart,onCartCreatedandincludeTax. The first two decide whether a cart exists at all: with nocartIdandcreateCartdefaulting to false an add fails outright, so a slot component written against the public contract could not serve an anonymous visitor — the click did nothing. All optional, so existing implementors are unaffected. (PWP-986)
[0.8.0] - 2026-09-22
Changed
-
getLabelkeeps an intentionally empty label instead of falling back. It resolvedlabels?.[key] || fallback, so""— the normal way to hide a label — was replaced by the English default and a label could not be blanked. Now??, so only a genuinely absent key falls back.This was live:
AccountIconAndMenurequestsloginSubtitleat two sites, one with an empty fallback and one with'Login to access your account'. nextDemo setsloginSubtitle: ""in bothenandnl, so the blanking worked at the first site and was silently discarded at the second — showing English in the Dutch UI.A locale that relied on
""meaning "use the English default" will now render an empty string. Remove the key instead to get the fallback.
Added
-
isOverAuthorizationLimit(user, companyId, cart), its inverseisCheckoutAllowed, andfindPurchaserPac— the B2B purchase-authorization predicate as plain functions, so an app can gate its own checkout button on the same rule the library'sCartSummaryuses instead of reimplementing it. It previously existed only as a boolean onuseCart, reachable only by mounting the hook, and had drifted into three divergent copies insidepropeller-v2-react-ui.cartis a required argument rather than ambient state: the hook's version read its own internal cart, which is null until the consumer callsaddItem/resolveCart, so it answered "checkout allowed" for an over-limit cart wherever the caller had loaded the cart itself.CartSummarycarried a comment explaining it avoided the hook for exactly that reason. (PWP-988)Reads both plain and underscore-prefixed field shapes (
_authorizationLimit,_company,_items, …). The SDK can serialize class instances with leading underscores on private fields; hosts normally sanitize on login, but any path that bypasses that — or stale storage from a previous session — leaves the underscored shape, and a plain read silently finds no config and reports every cart as within limit. The Vue package had carried hand-rolled tolerant lookups for this; they are now in one place and cover React too. -
getLabelwarns on a missing key in development (NODE_ENV === 'development', once per key). The fallbacks are English, so a key absent from a shipped locale renders English inside an otherwise translated page and reads as a translation rather than a gap. Two live examples found this way:totalExclVatmissing from theCartIconAndSidebarnamespace, andcloseFiltersmissing from every namespace. (PWP-987)
[0.7.0] - 2026-08-26
Added
localeForLanguage(language)— maps a storefront language code to the BCP-47 locale used to format its numbers and dates (EN→en-GB,NL→nl-NL, …). An explicit tag containing a-is returned unchanged, so a shop can pinen-USoveren-GBitself. Currency and number formatting were two independent decisions and only the currency was reachable: every caller leftformatPrice's locale at itsnl-NLdefault, so an English storefront rendered£ 3,45.
Changed
formatPriceplaces the symbol the way the locale does. The explicit- symbol branch hardcoded${symbol} ${amount}, which is right fornl-NL(€ 9,50) and wrong everywhere else (£ 3,45instead of£3.45). It now lays the amount out withIntl.NumberFormat's own currency pattern and substitutes the caller's glyph. Dutch output is byte-identical — the non-breaking space Intl emits is normalised back to a plain space — so only non-Dutch locales change.AddToCartComponentProps.onAddToCartmatches the component it feeds. It declared(product, quantity?, notes?)whileAddToCartcalls(product, clusterId?, quantity?, childItems?, notes?, price?, showModal?): argument 2 meantclusterIdin one andquantityin the other, so a component written against the slot contract could not forward the prop to the real<AddToCart>. Widening is backwards compatible — an implementation taking fewer parameters still satisfies it.
[0.6.2] - 2026-08-10
Added
getNettedBonusItems(items)— nets an order's bonus items against theirincentivesiblings. The API models a bonus as two lines: the product line at its list price, plus a siblingclass: 'incentive'line carrying the negative delta and pointing back viaparentOrderItemId. Consumers that rendered only the product line showed the undiscounted price on order surfaces, while the cart (which receives a pre-nettedbonusItemscollection) showed 0. The helper folds each discount into its parent, so a fully discounted item reaches 0 and a partially discounted one keeps the remainder.
[0.6.1] - 2026-08-07
Fixed
- ENUM cluster configurators render their options again.
extractAttributeValuesresolvedAttributeEnumValueviavalue, but the schema exposes enum values onenumValues: [String](there is novaluefield on that type). Every ENUM attribute therefore returned an empty list, so a cluster spanned by ENUM attributes rendered its dropdown with only the "— Select —" placeholder and no variant could be chosen. Now readsenumValues, with the legacyvaluekept as a fallback. (Originally fixed as 0.4.1, which was never published — the fix was orphaned when 0.5.0/0.6.0 shipped without it.)
[0.6.0] - 2026-08-06
Changed
-
BREAKING —
buildInventoryFilterfilters by a threshold, not two buckets. It now takes a single selection plus an optional minimum quantity, andAvailabilityis'all' | 'in-stock'. The'out-of-stock'bucket is gone.// beforebuildInventoryFilter(['in-stock']) // { totalQuantity: { greaterThan: 1 } }buildInventoryFilter(['out-of-stock']) // { totalQuantity: { equal: 0 } }buildInventoryFilter([]) // undefined// afterbuildInventoryFilter('in-stock') // { totalQuantity: { greaterThan: 1 } }buildInventoryFilter('in-stock', 5) // { totalQuantity: { greaterThan: 5 } }buildInventoryFilter('all') // undefinedThe two-checkbox model could express "in stock or out of stock", which is every product and so had to collapse to no filter — a control with a state that did nothing. It also could not express a quantity, which is the more common question once a shopper knows an item is stocked.
greaterThanstays inclusive, and the quantity defaults to 1, so a caller that passes no quantity gets exactly the previous in-stock behaviour. The value is floored and clamped to a minimum of 1: a lower threshold would match zero-stock products and contradict the control.To upgrade: pass
'in-stock'where you passed['in-stock'], and'all'(orundefined) where you passed[]or both buckets. There is no replacement for['out-of-stock'].
Added
MIN_STOCK_THRESHOLD— the smallest quantity that still means "in stock", exported so hosts can bound their own quantity inputs to the same value the filter clamps to.
[0.5.0] - 2026-08-05
Added
-
buildInventoryFilter(selection)and theAvailabilitytype ('in-stock' | 'out-of-stock') — map a shopper-facing stock selection to the server-sideProductSearchInventoryFilterInputthe SDK exposes onProductSearchInputandCategoryProductSearchInput. The React and Vue packages both call this, so the operator semantics live in one place and cannot drift between them.The operators are inclusive despite their names:
greaterThan: Nmatches stock>= NandlessThan: Nmatches stock<= N. Products that were never stocked have no inventory record and count as 0. So in stock is{ totalQuantity: { greaterThan: 1 } }and out of stock is{ totalQuantity: { equal: 0 } }— deliberately notlessThan: 1, which means<= 1and would also match products with exactly one remaining.Selecting both buckets means "in stock or out of stock", which is every product. There is no OR operator on the filter and the union is the unfiltered set anyway, so both-selected returns
undefined— the same as selecting neither.
Changed
- Bumped the SDK dev dependency to
^0.16.0, where the inventory filter types land. The runtime peer stays*.
[0.4.0] - 2026-07-29
Changed
- Align with
@propeller-commerce/propeller-sdk-v20.14.0, which removed the entire deprecated schema surface.buildProductJsonLd/buildClusterJsonLdnow read the plural localizedcategory.names(wascategory.name), andbuildItemListJsonLdunwraps clusters viaitem.type === ProductClass.CLUSTER(wasitem.class). Cluster SKU is taken fromdefaultProduct?.skuonly (Cluster.skuwas removed). Bumped the SDK dev dependency to^0.14.0; the runtime peer stays*.
[0.3.2] - 2026-07-08
Changed
- Bumped the
@propeller-commerce/propeller-sdk-v2dev dependency to^0.12.0to build and test against the SDK's 0.12.0 release. The runtime peer stays*— consumers pin the SDK version. No API change.
[0.3.1] - 2026-06-24
Documentation
- Added a link to the canonical docs site (https://propeller-commerce.github.io/propeller-v2-core-ui/) at the top of the README, as the source of truth for types and contracts.
[0.3.0] - 2026-06-19
Added
- Rich CMS model +
CmsProvidercontract promoted from the Next boilerplate.propeller-next's in-production CMS layer had grown a much richer contract than core's minimalCmsAdapter(3 methods, opaque blocks). This release promotes that richer model intosrc/types/cms.tsso the accelerator's adapters can express everything a real storefront renders:- New
CmsProviderinterface (8 methods + optionalgetMenu):getPage,getAllPageSlugs,getGlobal,getCategoryBanner,getArticles,getArticle,getAllArticleSlugs,resolveImageUrl. A strict superset ofCmsAdapter. - Typed block catalog
CmsTypedBlock(15 named blocks:CmsHeroBanner,CmsValueProps,CmsProductCards,CmsFeature,CmsFaq, …) discriminated by_type. CmsRichPage(template + typed blocks + typed SEO),CmsArticle/CmsAuthor(blog),CmsCategoryBanner, typedCmsGlobal(header/footer),CmsImage,CmsSeo,CmsPageOptions.
- New
Changed
- Nothing removed or renamed. The original
CmsAdapter, opaqueCmsBlock,CmsPage,CmsMenuItem,CmsGlobals, andCmsFetchOptionsare unchanged and still exported — the genericcms-react/cms-vuerenderers keep dispatching on the opaque block. This is a purely additive minor.
Why
The accelerator's bundled CMS adapters were stuck at the 3-method minimal contract, which could not express category banners, blog articles, static-slug enumeration, or image resolution that the boilerplates depend on — so a scaffolded shop and a boilerplate shop were not interchangeable. Promoting the boilerplate's proven contract into the shared core makes them converge, and is what lets the Strapi / Prepr / generic adapters implement the full surface.
[0.2.4] - 2026-06-04
Changed
- SDK dependency switched from GitHub tarball to npm. Both the
peerDependenciesentry and thedevDependenciestest pin now point at@propeller-commerce/propeller-sdk-v2@^0.11.1instead ofgithub:propeller-commerce/propeller-sdk-v2#master. All 18 source + test files renamed accordingly (from 'propeller-sdk-v2'→from '@propeller-commerce/propeller-sdk-v2', plus the/enumsubpath).
Why
The SDK is now published on npm as a properly scoped package. Pinning via npm removes the GitLab→GitHub mirror dependency from the install chain and gives consumers semver ranges instead of a moving master tip. Behaviour is unchanged — the 0.11.0 github tip and the 0.11.1 npm tarball are export-identical.
[0.2.3] - 2026-06-04
Fixed
getLanguageStringnow treats emptyvalue: ''entries as missing. Previously the function returned the matching entry'svalueverbatim — including the empty string — which short-circuited the fallback and rendered an invisible product name when the SDK returned a placeholder{ language: 'NL', value: '' }. The resolver now tries the other entries before giving up, matching what consumers expect from "localised value missing → use any available translation". Downstream effect:ProductCard,CartItem, and bundle/crossupsell name helpers inpropeller-v2-react-uiandpropeller-v2-vue-uino longer render blank names against datasets with sparse localisation.
0.2.2
Added
TranslationProviderinterface +Locale/Namespacetype aliases (./types/translations). Apps implement this contract to provide translated labels to UI components via the existinglabels?: Record<string, string>prop. Sync by design; the file/CMS/TMS choice is the app's.
[0.2.1] - 2026-06-02
Added
- Docusaurus documentation site under
docs/, deployed to https://propeller-commerce.github.io/propeller-v2-core-ui/ via a new.github/workflows/docs.ymlGitHub Action (build + GitHub Pages deploy). Covers getting-started, the type surface, the utility catalogue, the SDK seam (createServices), and theResult<T>contract design rationale. release_to_githubstage in.gitlab-ci.yml— automatic GitHub Release on everyRelease X.Y.Zpush, mirroring the SDK pattern. Idempotent (skips if thev<version>tag already exists) and gated on a non-"Unreleased" CHANGELOG section. Body is auto-extracted from this file.
Notes
No runtime / public-API changes — this is a tooling release that backfills documentation + release automation for the existing 0.2.0 surface. Consumers do not need to update.
[0.2.0] - 2026-06-01
Adds the framework-agnostic CMS adapter contract and the userMode helper
that consumers (commerce UI packages, CMS packages, accelerator templates)
build on. Additive — no existing exports changed.
Added
CmsAdapterinterface + supporting types (CmsPage,CmsBlock,CmsMenuItem,CmsGlobals,CmsFetchOptions) intypes/cms.ts. Defines the three methods every CMS adapter implementation must expose:getPage(slug, opts?),getMenu(name, opts?),getGlobals(opts?). Framework-agnostic; consumed bypropeller-v2-cms-react,propeller-v2-cms-vue, and downstream adapter packages (propeller-cms-adapter-strapi, etc.).deriveUserMode(user, shopMode)inutils/userMode.ts, returning'anonymous' | 'b2b' | 'b2c'. Built on the existingisContact/isCustomerdiscriminators. Hybrid shops branch on SDK user type at runtime; pureb2b/b2cshops short-circuit regardless of who's logged in. Used bypropeller-v2-react-uiandpropeller-v2-vue-uito expose a deriveduserModefield onPropellerInfraso consumer UI can branch consistently (userMode === 'b2b') instead of re-deriving fromisContact(user)ad hoc.ShopModeandUserModetype exports for downstream packages.
Why this matters
The CMS contract lives in core (not in either UI package) so commerce
components stay focused on commerce, and any new framework binding (Svelte,
Solid, future) inherits the same contract for free. userMode is the
single place that turns SDK user-type discrimination into a shop-mode-aware
gating decision — the upcoming accelerator templates rely on this to gate
B2B-only routes (quotes, authorization, contacts) for Customer sessions
in hybrid shops.
[0.1.0] - 2026-05-20
Initial extraction from the React and Vue UI packages. Pure-TS framework-agnostic
core: domain types, formatting helpers, attribute extractors, language
resolution, user identity discriminators, content-visibility helper, JSON-LD
builders, the Services SDK seam, and the Result<T> contract.