@propeller-commerce/propeller-v2-react-ui 0.4.6

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/CHANGELOG.md ADDED
@@ -0,0 +1,359 @@
1
+ # Changelog
2
+
3
+ All notable changes to `propeller-v2-react-ui` are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and the project aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
7
+ once it reaches 1.0. Until then (the `0.x` line) the public API may change
8
+ between minor versions; breaking changes are called out below and in
9
+ [MIGRATION.md](./MIGRATION.md).
10
+
11
+ ## [0.4.6] - 2026-06-09
12
+
13
+ ### Fixed
14
+
15
+ - **`CartItem` cart actions silently no-op (quantity +/-, delete, notes).**
16
+ The compound-API refactor in 0.3.0 (commit 564f8d0) switched `CartItem`
17
+ from `useInfraProps(rawProps)` to
18
+ `useResolvedProps(rawProps, CART_ITEM_RESOLVE_SPEC)`, but the new spec
19
+ only listed the slot-injection keys (`priceComponent`, `stockComponent`,
20
+ `surchargesComponent`) — the Tier 1 infra keys (`graphqlClient`, `user`,
21
+ `language`, `currency`, `configuration`, `companyId`, `includeTax`)
22
+ weren't declared, so they never got filled in from `<PropellerProvider>`.
23
+ The internal `useCart()` call received `graphqlClient: undefined`, every
24
+ service mutation threw inside the SDK, and the consumer saw no spinner,
25
+ no toast, no change — just buttons that did nothing.
26
+ Added the missing infra entries to `CART_ITEM_RESOLVE_SPEC` so resolution
27
+ matches the precedence the rest of the surface uses
28
+ (`ClusterCard.RESOLVE_SPEC`, `ProductCard.RESOLVE_SPEC`).
29
+
30
+ ## [0.4.5] - 2026-06-04
31
+
32
+ ### Fixed
33
+
34
+ - **List-view / cart row collapse against a consumer's Tailwind cascade.**
35
+ `ProductCard` (row layout), `ClusterCard` (row layout), and `CartItem`
36
+ use responsive override pairs in the markup — `w-full md:w-auto`,
37
+ `border-t md:border-t-0`, `py-2 md:py-0`, `flex-wrap md:flex-nowrap`.
38
+ When a consumer also runs Tailwind v4, its generated stylesheet re-emits
39
+ the base utilities (`.w-full`, `.border-t`, …) but not the `md:` variants
40
+ that only appear inside this package's components. On equal specificity
41
+ the consumer sheet wins, the card footer goes full-width, and the body's
42
+ `flex-1 min-w-0` collapses to zero — title and SKU render at 0px width
43
+ while the placeholder image still occupies space (the symptom looks like
44
+ "missing product name" but the element is actually there).
45
+ Restated the desktop (≥768px) intent on the BEM hook classes scoped under
46
+ the card root in `styles.css` so the selector is specificity (0,2,0) —
47
+ it beats any single-class utility (0,1,0) regardless of sheet order.
48
+ This mirrors the `propeller-v2-vue-ui` fix from May (commit ce7a899).
49
+
50
+ ## [0.4.4] - 2026-06-04
51
+
52
+ ### Changed
53
+
54
+ - **SDK dependency switched from GitHub tarball to npm.** Both the
55
+ `peerDependencies` entry and the `devDependencies` test pin now point
56
+ at `@propeller-commerce/propeller-sdk-v2@^0.11.1` instead of
57
+ `github:propeller-commerce/propeller-sdk-v2#master`. All 113 source +
58
+ test files renamed accordingly (`from 'propeller-sdk-v2'` →
59
+ `from '@propeller-commerce/propeller-sdk-v2'`).
60
+
61
+ ### Dependencies
62
+
63
+ - Bumps `propeller-v2-core-ui` to 0.2.4 (its own SDK switch to npm).
64
+
65
+ ### Why
66
+
67
+ The SDK is now published on npm as a properly scoped package. Pinning
68
+ via npm removes the GitLab→GitHub mirror dependency from the install
69
+ chain and gives consumers semver ranges instead of a moving master tip.
70
+ Behaviour is unchanged.
71
+
72
+ ## [0.4.3] - 2026-06-04
73
+
74
+ ### Fixed
75
+
76
+ - **`CartItem` now resolves product / bundle / crossupsell names via
77
+ `getLanguageString`** (from `propeller-v2-core-ui@0.2.3`), matching
78
+ the active language and walking the localised entries for the first
79
+ non-empty value before falling back to `'Product'`. Previously each
80
+ helper hard-coded `names?.[0]?.value || 'Product'`, which always
81
+ picked the first SDK entry regardless of language and rendered blank
82
+ when that first entry's `value` was empty. Cart pages now show the
83
+ correct localised name and never collapse to invisible rows on
84
+ datasets with sparse localisation.
85
+
86
+ ### Dependencies
87
+
88
+ - Bumps `propeller-v2-core-ui` to 0.2.3 (the resolver fix that powers
89
+ the above).
90
+
91
+ ## [0.4.0] - 2026-06-02
92
+
93
+ Finishes the prop-cascade cleanup started in 0.3.0. Every component on
94
+ the main entry now resolves Tier 1 + Tier 2 infra from
95
+ `<PropellerProvider>` when not passed explicitly — consumer islands can
96
+ drop redundant `graphqlClient` / `user` / `companyId` / `language` /
97
+ `includeTax` / `currency` / `configuration` / `portalMode` passes on
98
+ every component (not just the 31 retrofitted in 0.3.0).
99
+
100
+ ### Changed
101
+
102
+ - **6 client components retrofitted** to call `useInfraProps(rawProps)`:
103
+ `CategoryDescription`, `ProductDescription`, `GridFilters`,
104
+ `GridToolbar`, `AddressSelector`, `CartPaymethods`. Same mechanical
105
+ pattern as the 31 already retrofitted in 0.3.0.
106
+
107
+ ### Added
108
+
109
+ - **Provider-aware client wrappers for the 6 `@rsc-safe` components**:
110
+ `Breadcrumbs`, `GridTitle`, `ProductPrice`, `ProductBulkPrices`,
111
+ `ProductShortDescription`, `CategoryShortDescription`. The pure RSC-safe
112
+ components still exist and are still exported from `/pure` for Server
113
+ Component use. The **main `/` entry now exports the wrapper** under the
114
+ original name (`Breadcrumbs`, etc.), so client islands automatically
115
+ pick up provider-aware versions with no consumer-side import change.
116
+ Server pages that need the pure component should import from
117
+ `propeller-v2-react-ui/pure` (this was already the canonical pattern).
118
+
119
+ ### Migration
120
+
121
+ Additive. Existing call sites that pass explicit infra props keep
122
+ working unchanged. Client islands can now drop those passes. Server
123
+ pages importing from `/pure` are unaffected.
124
+
125
+ ### Verification
126
+
127
+ - `tsc --noEmit` clean.
128
+ - 21 / 21 vitest pass.
129
+ - `tsup` build emits both client (with `"use client"` banner) and `/pure`
130
+ (without banner) shapes correctly; dts unchanged for the pure entry.
131
+
132
+ ## [0.3.0] - 2026-06-02
133
+
134
+ Loosens 5 components' required infra props to optional, plus retrofits
135
+ `UserDetails` to consume `useInfraProps()` like the other 30 retrofitted
136
+ components. Consumers can now drop redundant `:user=` / `:language=`
137
+ passes on these components when `<PropellerProvider>` wraps the subtree.
138
+
139
+ ### Changed
140
+
141
+ - **`UserDetails`** — `user: Contact | Customer` → `user?: Contact |
142
+ Customer | null`. Component now calls `useInfraProps(rawProps)` to
143
+ resolve `user` from the provider when omitted. Existing call sites
144
+ that pass `user` keep working unchanged.
145
+ - **`AddressSelector`** — `user: Contact | Customer | null` →
146
+ `user?: Contact | Customer | null`.
147
+ - **`CartPaymethods`** — `user: Contact | Customer | null` →
148
+ `user?: Contact | Customer | null`.
149
+ - **`GridTitle`** — `language: string` → `language?: string`.
150
+ - **`CategoryDescription`** — `language: string` → `language?: string`.
151
+
152
+ ### Migration
153
+
154
+ Same shape as `propeller-v2-vue-ui@0.3.0`. Additive. Existing call sites
155
+ keep working. A follow-up cleanup in `propeller-next` will prune ~85
156
+ redundant prop-lines from the consumer islands.
157
+
158
+ ### Verification
159
+
160
+ - `tsc --noEmit` clean.
161
+ - 21 / 21 vitest tests pass unchanged.
162
+ - `tsup` build emits identical dts shape.
163
+
164
+ ## 0.2.3
165
+
166
+ ### Fixed
167
+
168
+ - `AccountIconAndMenu` no longer forwards its own `labels` (which contains menu-UI slugs like `accountLabel`, `logoutLabel`) to the embedded `<LoginForm>` — that previously caused LoginForm's `email`, `password`, `forgotPassword` strings to fail translation. Use the new `loginFormLabels?` prop instead.
169
+
170
+ ### Added
171
+
172
+ - `AccountIconAndMenu`: new `loginFormLabels?: Record<string, string>` prop, forwarded to the embedded `<LoginForm>`. Lets consumers pass the LoginForm namespace's translations through to the dropdown sign-in form.
173
+
174
+ ## 0.2.2
175
+
176
+ ### Added
177
+
178
+ - `ProductCard` / `ProductGrid` / `ProductSlider`: new `priceLabels?: Record<string, string>` prop, forwarded through to the embedded `<ProductPrice>` display inside each card. Lets consumers translate `inclTax` / `exclTax` / `loginToSeePrices` strings on grid/slider pages without per-card wiring. `ClusterCard` is intentionally unchanged — it builds its own price span and doesn't render `<ProductPrice>`.
179
+
180
+ ## 0.2.1
181
+
182
+ ### Added
183
+
184
+ - `ProductGrid` / `ProductSlider`: new props `productCardLabels?`, `clusterCardLabels?` for forwarding translations to embedded `<ProductCard>` / `<ClusterCard>`. Mirrors the existing `stockLabels?` / `addToCartLabels?` pattern.
185
+ - `PriceToggle`: new `labels?: Record<string, string>` prop with slugs `pricesLabel`, `inclVat`, `exclVat`. Previously these strings were hardcoded.
186
+ - `OrderList`: filter column field labels and sort-field dropdown options now route through the `labels` prop. Column labels use slug `col<Capitalized>` (e.g. `colTerm`, `colCreatedAt`); sort-field options use the enum value as the slug (e.g. `createdAt`, `price`); sort-order options (ASC/DESC) and order type options also route through `labels`. All fallbacks preserve existing English behavior, so the change is non-breaking for consumers that don't supply the new keys.
187
+
188
+ ### Fixed
189
+
190
+ - `ProductSlider` no longer forwards its own `labels` (which contains slider-UI slugs such as `scrollLeft`, `noProducts`) to embedded `ClusterCard` — that previously caused card-level strings to fail translation. Use the new `clusterCardLabels?` prop instead.
191
+
192
+ ## [0.2.0] - 2026-06-01
193
+
194
+ Adds shop-mode-aware user gating. Existing public API is unchanged; new
195
+ fields are optional and default to backward-compatible behaviour.
196
+
197
+ ### Added
198
+
199
+ - **`shopMode?: ShopMode`** on `PropellerScope`. Declares whether the shop
200
+ is `'b2b'`, `'b2c'`, or `'hybrid'`. Defaults to `'hybrid'` when omitted
201
+ so existing call sites keep their current branching semantics (any
202
+ logged-in Contact treated as B2B).
203
+ - **Derived `userMode: UserMode`** on `PropellerInfra`. Computed via
204
+ `deriveUserMode(user, shopMode)` from `propeller-v2-core-ui`. Values:
205
+ `'anonymous' | 'b2b' | 'b2c'`. B2B-gated UI (company switcher, B2B
206
+ side-nav items, quote/authorization affordances) should read this
207
+ instead of re-deriving from `isContact(user)` ad hoc.
208
+ - **`useUserMode()` hook**. Direct read of `userMode` for components that
209
+ only need that one signal.
210
+
211
+ ### Why this matters
212
+
213
+ Hybrid shops need a single, consistent gate that says "is the current
214
+ viewer behaving as B2B or B2C?" Previously every B2B-gated component
215
+ re-derived this from `isContact(user)` ad hoc, which was correct but
216
+ duplicated and ignored the shop's `mode` (a B2C shop that somehow had a
217
+ Contact session would still light up the B2B surface). Centralising the
218
+ derivation eliminates the duplication and makes the shop mode authoritative.
219
+
220
+ ### Requires
221
+
222
+ - `propeller-v2-core-ui` ≥ 0.2.0 (for `deriveUserMode`, `ShopMode`,
223
+ `UserMode`).
224
+
225
+ ---
226
+
227
+ ## [0.1.0] — Unreleased
228
+
229
+ First version of the package. Extracted from the `propeller-next`
230
+ boilerplate so the Propeller Commerce React surface can be consumed as a
231
+ standalone library. Not yet published to a registry — consumed via a
232
+ `file:` link during stabilization.
233
+
234
+ ### Added
235
+
236
+ - **Initial extraction (Phase E).** 60 components, the React composables
237
+ (hooks), the runtime-agnostic `composables/shared/` layer (utilities and
238
+ domain types), and the two contexts (`PropellerContext`,
239
+ `ProductGridContext`) moved into this package from `propeller-next`.
240
+ - **Build pipeline.** `tsup` produces dual ESM + CJS bundles with `.d.ts`
241
+ declarations. The client bundle (`index`) gets a `"use client";` banner
242
+ prepended in a post-build hook; the `shared` and `pure` bundles are
243
+ runtime-agnostic with no banner.
244
+ - **Three code entry points.** `propeller-v2-react-ui` (components, hooks,
245
+ contexts, `createServices`, `toPlain`), `propeller-v2-react-ui/pure` (the
246
+ 12 pure/presentational components, RSC-safe — see below), and
247
+ `propeller-v2-react-ui/shared` (pure TS — `createServices`, `toPlain`,
248
+ formatters, helpers, all domain types — safe to import from Server
249
+ Components).
250
+ - **`/pure` RSC-safe component entry.** A third `tsup` entry exporting the
251
+ 12 pure/presentational components (`Breadcrumbs`, `ProductPrice`,
252
+ `ItemStock`, `OrderTotals`, `ProductBulkPrices`, `ProductShortDescription`,
253
+ `ProductDownloads`, `ProductVideos`, `OrderItemCard`, `OrderSummary`,
254
+ `GridTitle`, `CategoryShortDescription`). The `pure` bundle is built
255
+ **without** the `"use client"` banner, so a React Server Component can
256
+ import and render these components directly — server-rendering real
257
+ product/price/order markup — without drawing a client boundary or
258
+ shipping the client bundle. Each is verified to use no hooks, state,
259
+ effects, handlers, browser APIs or context reads. The same components
260
+ remain available from the main entry for use inside client boundaries.
261
+ - **Precompiled stylesheet** (`dist/styles.css`). The package's Tailwind v4
262
+ classes are compiled to vanilla CSS at build time and shipped. Consumers
263
+ import it once and do **not** need Tailwind in their own project.
264
+ - **Three styling override surfaces** — theme tokens (CSS variables), BEM
265
+ hooks (`.propeller-product-card__price`, …), and per-instance `className`.
266
+ Documented in [STYLING.md](./STYLING.md).
267
+ - **`PropellerProvider`** — the single integration point. Takes one value
268
+ object (`PropellerInfra`) carrying `graphqlClient`, `services`, `user`,
269
+ `companyId`, `language`, `includeTax`, `currency`, `portalMode`,
270
+ `configuration`. Imports zero host contexts.
271
+ - **`createServices(client)`** — factory that builds the typed `Services`
272
+ bundle (`product`, `cart`, `user`, `order`, …) keyed to a consumer-built
273
+ `GraphQLClient`. Memoized per client via `WeakMap`.
274
+ - **`toPlain(value)`** — recursively strips the SDK's underscore-prefixed
275
+ backing fields from class instances.
276
+ - **`useServices()`** — reads the `Services` bundle from `PropellerProvider`;
277
+ throws a clear error when used outside a provider.
278
+ - Public-grade documentation: [README.md](./README.md), [STYLING.md](./STYLING.md),
279
+ [TECH.md](./TECH.md), [CONTRIBUTING.md](./CONTRIBUTING.md),
280
+ [MIGRATION.md](./MIGRATION.md), [SECURITY.md](./SECURITY.md), and an MIT
281
+ `LICENSE`.
282
+ - **Unit tests.** Vitest suite covering the pure-logic surface — `src/lib/`
283
+ (`createServices`, `toPlain`) and the 11 framework-free shared utilities
284
+ (formatters, truncation, inventory/label/visibility helpers, attribute
285
+ extraction, country lookup, language resolution, product helpers, user
286
+ identity, video URL transforms). 183 tests, ~99% statement / 100%
287
+ function coverage of that surface. Scripts: `test`, `test:watch`,
288
+ `test:coverage`.
289
+ - **CI pipeline** (`.gitlab-ci.yml`). A `verify` stage (typecheck, unit
290
+ tests + coverage, build) and a `downstream` stage that builds the
291
+ package, installs it into a fresh checkout of propeller-next, and runs
292
+ that repo's full Playwright e2e suite — the package's component
293
+ regression gate. Components are intentionally not unit-tested against a
294
+ mock SDK; the consumer's real e2e suite is the verification layer.
295
+ Required GitLab CI/CD variables are documented in
296
+ [CONTRIBUTING.md](./CONTRIBUTING.md).
297
+ - **Storybook** (`.storybook/`). A story per component (60 total),
298
+ rendering each in isolation against fixture data and a mock
299
+ `PropellerProvider`. The "Docs" tab auto-generates each component's
300
+ full `*Props` table from the TypeScript source via
301
+ `react-docgen-typescript`. Mock foundation in `src/__mocks__/`
302
+ (`fixtures.ts`, `mockServices.ts`, `decorators.tsx`). Scripts:
303
+ `storybook`, `build-storybook`.
304
+ - **Documentation site** (`docs/`). A self-contained Docusaurus 3 app —
305
+ its own `package.json` / lockfile, not published to npm, not part of
306
+ the package build. Eight guide pages (introduction, getting started,
307
+ the SDK seam, styling, server components, component reference,
308
+ Storybook, contributing) sourced from the package's own markdown.
309
+ The component prop reference is intentionally not duplicated here —
310
+ the site links out to Storybook's auto-generated prop tables.
311
+
312
+ - **`Menu`: optional pre-fetched `tree` prop.** When the host supplies
313
+ `tree: MenuCategory[]`, `Menu` skips its internal `useMenu` fetch
314
+ entirely and renders the tree directly — mirroring the
315
+ `ProductGrid.products` opt-in. Lets host apps fetch the category tree
316
+ server-side (e.g. in a Next.js layout) and have the menu HTML land in
317
+ the initial response. Omitting the prop preserves the legacy
318
+ client-side fetch behaviour — no breaking change.
319
+
320
+ ### Changed
321
+
322
+ - **Decoupled the SDK seam from Next.js (breaking, pre-publish).** The
323
+ package no longer ships a module-level `graphqlClient` singleton, a
324
+ hardcoded `/api/graphql` endpoint, or `NEXT_PUBLIC_*` environment reads.
325
+ The consumer now constructs its own `GraphQLClient`, calls
326
+ `createServices(client)`, and passes both into `PropellerProvider`.
327
+ `getServices` (singleton-defaulting accessor) was replaced by
328
+ `createServices` (pure factory). See [MIGRATION.md](./MIGRATION.md).
329
+ - **Removed the `/server` entry.** The previous
330
+ `propeller-v2-react-ui/server` subpath (`createServerClient`,
331
+ `getServerInfra`, `fetchProduct`, `fetchCategory`) was deleted —
332
+ server-side GraphQL wiring (endpoint, API keys, cookie names, auth) is
333
+ application-specific. Consumers host their own server module; the package
334
+ exports `createServices` from `/shared` for server-side use.
335
+ - **Dropped the `next` peer dependency.** The package has zero `next/*`
336
+ imports. Next.js apps still work out of the box.
337
+ - `PropellerInfra` gained a required `services: Services` field.
338
+
339
+ ### Fixed
340
+
341
+ - **`OrderActions` button layout.** Buttons no longer wrap mid-word when
342
+ `OrderActions` shares a flex row with `OrderTotals` — added
343
+ `flex-shrink-0` to the wrapper and `whitespace-nowrap` to the buttons.
344
+ - **`CartItem` alignment.** The product image and footer are top-aligned
345
+ with the title row instead of vertically centering against tall content
346
+ (cross-sells) — root changed from `items-center` to `items-start`.
347
+ - **`ProductCard` row layout.** Responsive utilities (`md:flex-nowrap`,
348
+ etc.) buried inside template-literal ternaries were missing from the
349
+ compiled stylesheet; force-included via `@source inline(...)` so list
350
+ view stays single-row at desktop widths.
351
+ - **`GridFilters` — active filters now visible on first render.** A filter
352
+ group with a selected value starts expanded even when `collapsed` is
353
+ `true`, and its checkboxes render checked, on the very first render
354
+ (including SSR — previously this depended on a client-only effect that
355
+ never ran server-side). Restoring a filtered URL no longer hides the
356
+ ticked checkboxes. A group the user explicitly toggles still wins, and a
357
+ user collapsing a group with an active filter is not fought.
358
+
359
+ [0.1.0]: https://github.com/propeller-commerce/propeller-v2-react-ui/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Propeller Commerce
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/MIGRATION.md ADDED
@@ -0,0 +1,217 @@
1
+ # Migration Guide
2
+
3
+ Breaking changes between versions of `propeller-v2-react-ui`, with the steps
4
+ to upgrade. The package is pre-1.0, so breaking changes can land in `0.x`
5
+ minor versions — each one is documented here.
6
+
7
+ ---
8
+
9
+ ## `Menu`: optional pre-fetched `tree` prop (additive)
10
+
11
+ **When:** during `0.1.0` stabilization (pre-publish).
12
+
13
+ `Menu` now accepts an optional `tree?: MenuCategory[]` prop. When supplied,
14
+ the component skips its internal `useMenu` fetch and renders the tree
15
+ directly — mirroring the long-standing `ProductGrid.products` opt-in.
16
+
17
+ **No migration required.** Omitting the prop preserves the legacy
18
+ client-side fetch behaviour. This is purely opt-in for hosts that want to
19
+ move the category tree fetch into a Server Component (e.g. a Next.js
20
+ layout) so the menu HTML lands in the initial response and the host can
21
+ attach framework cache hints — see also `GraphQLFetchOptions` in
22
+ `propeller-sdk-v2` v0.11.0.
23
+
24
+ ### Recommended pattern for Next.js consumers
25
+
26
+ ```tsx
27
+ // app/layout.tsx — Server Component
28
+ import { fetchMenu, getAnonymousInfra } from '@/lib/server';
29
+ import Header from '@/components/layout/Header';
30
+
31
+ export default async function RootLayout({ children }) {
32
+ const menuTree = await fetchMenu(getAnonymousInfra(), BASE_CATEGORY_ID, lang);
33
+ return (
34
+ <html><body>
35
+ <Header menuTree={menuTree} />
36
+ {children}
37
+ </body></html>
38
+ );
39
+ }
40
+
41
+ // components/layout/Header.tsx — 'use client'
42
+ import { Menu } from 'propeller-v2-react-ui';
43
+ export default function Header({ menuTree }) {
44
+ return <Menu categoryId={BASE_CATEGORY_ID} tree={menuTree} onMenuItemClick={...} />;
45
+ }
46
+ ```
47
+
48
+ The internal `useEffect` short-circuits when `tree` is present, so there is
49
+ no avoidable client-side round trip after hydration.
50
+
51
+ See [TECH.md §7 "Pre-fetched data prop pattern"](./TECH.md) for the broader
52
+ context and the same pattern as it applies to `ProductGrid`.
53
+
54
+ ---
55
+
56
+ ## Decoupling the SDK seam from Next.js
57
+
58
+ **When:** during `0.1.0` stabilization (pre-publish).
59
+
60
+ The package used to ship a module-level GraphQL client singleton with a
61
+ hardcoded `/api/graphql` endpoint and `NEXT_PUBLIC_*` environment reads, plus
62
+ a `/server` entry for server-side fetching. Both baked a specific app shape
63
+ (a Next.js app proxying at exactly that path) into the library. They were
64
+ removed.
65
+
66
+ ### What changed
67
+
68
+ | Before | After |
69
+ | ------ | ----- |
70
+ | `import { graphqlClient } from 'propeller-v2-react-ui'` | Consumer constructs its own `GraphQLClient` |
71
+ | `import { getServices } from 'propeller-v2-react-ui'` | `import { createServices } from 'propeller-v2-react-ui'` |
72
+ | `getServices()` (singleton default) | `createServices(client)` — explicit client, required |
73
+ | `propeller-v2-react-ui/server` entry | Removed — host your own server module |
74
+ | `next` peer dependency | Removed — the package has no `next/*` imports |
75
+ | `PropellerInfra` without `services` | `PropellerInfra` requires a `services` field |
76
+
77
+ ### How to upgrade
78
+
79
+ #### 1. Own your GraphQL client
80
+
81
+ Create a small module in your app that constructs the client and the
82
+ services bundle:
83
+
84
+ ```ts
85
+ // lib/api.ts — in YOUR app
86
+ import { GraphQLClient } from 'propeller-sdk-v2';
87
+ import { createServices } from 'propeller-v2-react-ui';
88
+
89
+ export const graphqlClient = new GraphQLClient({
90
+ endpoint: '/api/graphql', // your endpoint / proxy path
91
+ apiKey: '',
92
+ orderEditorApiKey: process.env.NEXT_PUBLIC_ORDER_EDITOR_API_KEY || '',
93
+ timeout: 30_000,
94
+ headers: {},
95
+ });
96
+
97
+ export const services = createServices(graphqlClient);
98
+ ```
99
+
100
+ The endpoint, env-var names, and timeout are now **your** decision — pick
101
+ whatever fits your app (a route-handler proxy, a direct upstream URL, a
102
+ custom rewrite).
103
+
104
+ #### 2. Pass `graphqlClient` and `services` into `PropellerProvider`
105
+
106
+ `PropellerInfra` now requires `services`:
107
+
108
+ ```tsx
109
+ import { PropellerProvider } from 'propeller-v2-react-ui';
110
+ import { graphqlClient, services } from '@/lib/api';
111
+
112
+ <PropellerProvider value={{
113
+ graphqlClient,
114
+ services, // ← newly required
115
+ user,
116
+ companyId,
117
+ language: 'NL',
118
+ includeTax: false,
119
+ currency: '€',
120
+ portalMode: 'OPEN',
121
+ configuration: {},
122
+ }}>
123
+ {children}
124
+ </PropellerProvider>
125
+ ```
126
+
127
+ #### 3. Replace `graphqlClient` / `getServices` imports
128
+
129
+ Anywhere you imported these from the package, import from your own
130
+ `lib/api` instead:
131
+
132
+ ```diff
133
+ - import { graphqlClient, getServices } from 'propeller-v2-react-ui';
134
+ + import { graphqlClient, services } from '@/lib/api';
135
+ ```
136
+
137
+ Call sites collapse — `getServices(graphqlClient).cart` becomes
138
+ `services.cart`:
139
+
140
+ ```diff
141
+ - await getServices(graphqlClient).cart.deleteCart({ id });
142
+ + await services.cart.deleteCart({ id });
143
+ ```
144
+
145
+ Components and composables rendered inside `PropellerProvider` can also use
146
+ the `useServices()` hook instead of importing `services` directly.
147
+
148
+ #### 4. Update the shared cart helpers
149
+
150
+ `initCart`, `fetchActiveCart`, and `mergeAnonymousCart` previously took a
151
+ `graphqlClient` field in their config object. They now take `services`:
152
+
153
+ ```diff
154
+ await fetchActiveCart({
155
+ - graphqlClient,
156
+ + services,
157
+ user,
158
+ language,
159
+ imageSearchFilters,
160
+ imageVariantFilters,
161
+ });
162
+ ```
163
+
164
+ #### 5. Replace the `/server` entry
165
+
166
+ If you imported `createServerClient`, `getServerInfra`, `fetchProduct`, or
167
+ `fetchCategory` from `propeller-v2-react-ui/server`, that entry is gone.
168
+ Host a server module in your own app instead. The pattern:
169
+
170
+ ```ts
171
+ // lib/server.ts — in YOUR app
172
+ import 'server-only';
173
+ import { cookies } from 'next/headers'; // or your framework's equivalent
174
+ import { GraphQLClient } from 'propeller-sdk-v2';
175
+ import { createServices, toPlain } from 'propeller-v2-react-ui/shared';
176
+
177
+ export function createServerClient() {
178
+ return new GraphQLClient({
179
+ endpoint: process.env.PROPELLER_GRAPHQL_ENDPOINT!, // your env name
180
+ apiKey: process.env.PROPELLER_API_KEY!,
181
+ securityMode: 'direct',
182
+ getAccessToken: async () => (await cookies()).get('access_token')?.value,
183
+ });
184
+ }
185
+
186
+ export async function fetchProduct(productId: number, language = 'NL') {
187
+ const services = createServices(createServerClient());
188
+ const result = await services.product.getProduct({
189
+ productId, language, imageSearchFilters: {},
190
+ imageVariantFilters: { transformations: [] },
191
+ });
192
+ return result ? toPlain(result) : null;
193
+ }
194
+ ```
195
+
196
+ `createServices` is exported from `propeller-v2-react-ui/shared` precisely so
197
+ it can be used server-side without pulling the client bundle into the server
198
+ graph. The `propeller-next` repo's `lib/server.ts` is a complete reference
199
+ implementation — copy it and adjust the env-var names and cookie name.
200
+
201
+ #### 6. Drop `next` from your reasoning, not your app
202
+
203
+ The package no longer peer-depends on `next`. Your Next.js app obviously
204
+ still depends on Next — nothing changes there. The point is the package
205
+ itself is now framework-neutral; nothing to do on your side beyond noting it.
206
+
207
+ ### Why this change
208
+
209
+ GraphQL transport — the endpoint URL, whether you proxy, how you resolve
210
+ auth tokens, which environment-variable convention you use — is
211
+ application-specific. A library that hardcodes `/api/graphql` and
212
+ `NEXT_PUBLIC_*` is usable only by an app shaped exactly like the one it was
213
+ extracted from. Moving the client construction into the consumer makes the
214
+ package usable by any React app (Next.js App Router or Pages Router, Vite,
215
+ Remix, CRA) and makes it testable with a mock client.
216
+
217
+ See [TECH.md](./TECH.md) §6 for the full rationale.