@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 +359 -0
- package/LICENSE +21 -0
- package/MIGRATION.md +217 -0
- package/README.md +668 -0
- package/STYLING.md +145 -0
- package/dist/index.cjs +17345 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +6478 -0
- package/dist/index.d.ts +6478 -0
- package/dist/index.js +17218 -0
- package/dist/index.js.map +1 -0
- package/dist/pure.cjs +974 -0
- package/dist/pure.cjs.map +1 -0
- package/dist/pure.d.cts +556 -0
- package/dist/pure.d.ts +556 -0
- package/dist/pure.js +938 -0
- package/dist/pure.js.map +1 -0
- package/dist/shared.cjs +14 -0
- package/dist/shared.cjs.map +1 -0
- package/dist/shared.d.cts +24 -0
- package/dist/shared.d.ts +24 -0
- package/dist/shared.js +3 -0
- package/dist/shared.js.map +1 -0
- package/dist/styles.css +2 -0
- package/package.json +93 -0
package/README.md
ADDED
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
# propeller-v2-react-ui
|
|
2
|
+
|
|
3
|
+
A React component library for **Propeller Commerce** storefronts. It ships
|
|
4
|
+
ready-made e-commerce UI — product cards, grids, carts, checkout, account
|
|
5
|
+
pages — together with a set of headless hooks ("composables") that talk to
|
|
6
|
+
the Propeller GraphQL API, plus the shared utilities and types those parts
|
|
7
|
+
build on.
|
|
8
|
+
|
|
9
|
+
The package is framework-agnostic. It runs in any React 18+ app — Next.js
|
|
10
|
+
(App Router or Pages Router), Vite/CRA SPAs, Remix — and ships its own
|
|
11
|
+
precompiled stylesheet, so you do **not** need Tailwind in your project to
|
|
12
|
+
use it.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Table of contents
|
|
17
|
+
|
|
18
|
+
- [Installation](#installation)
|
|
19
|
+
- [Peer dependencies](#peer-dependencies)
|
|
20
|
+
- [Entry points](#entry-points)
|
|
21
|
+
- [Core concept: the SDK seam](#core-concept-the-sdk-seam)
|
|
22
|
+
- [Quick start (Next.js App Router)](#quick-start-nextjs-app-router)
|
|
23
|
+
- [Quick start (Vite / CRA / SPA)](#quick-start-vite--cra--spa)
|
|
24
|
+
- [The PropellerProvider](#the-propellerprovider)
|
|
25
|
+
- [Using components](#using-components)
|
|
26
|
+
- [Using composables (hooks)](#using-composables-hooks)
|
|
27
|
+
- [Server Components & data fetching](#server-components--data-fetching)
|
|
28
|
+
- [Styling](#styling)
|
|
29
|
+
- [API reference](#api-reference)
|
|
30
|
+
- [TypeScript](#typescript)
|
|
31
|
+
- [Building from source](#building-from-source)
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install propeller-v2-react-ui propeller-sdk-v2
|
|
39
|
+
# or
|
|
40
|
+
pnpm add propeller-v2-react-ui propeller-sdk-v2
|
|
41
|
+
# or
|
|
42
|
+
yarn add propeller-v2-react-ui propeller-sdk-v2
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`propeller-sdk-v2` is a peer dependency — install it yourself so the package
|
|
46
|
+
and your application share a single SDK instance.
|
|
47
|
+
|
|
48
|
+
## Peer dependencies
|
|
49
|
+
|
|
50
|
+
| Package | Version | Required |
|
|
51
|
+
| ------------------ | ---------- | ----------------------------------------- |
|
|
52
|
+
| `react` | `>=18` | Yes |
|
|
53
|
+
| `react-dom` | `>=18` | Yes |
|
|
54
|
+
| `propeller-sdk-v2` | `*` | Yes — provides the GraphQL client & types |
|
|
55
|
+
|
|
56
|
+
There is **no dependency on Next.js**. Next.js apps work out of the box, but
|
|
57
|
+
nothing in the package imports `next/*`.
|
|
58
|
+
|
|
59
|
+
## Entry points
|
|
60
|
+
|
|
61
|
+
The package exposes four import paths — three code entries and the stylesheet:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// 1. Main entry — React components, hooks, contexts.
|
|
65
|
+
// The bundle is marked "use client", so in Next.js every re-export is a
|
|
66
|
+
// Client Component and the boundary is drawn automatically.
|
|
67
|
+
import { PropellerProvider, ProductCard, useCart } from 'propeller-v2-react-ui';
|
|
68
|
+
|
|
69
|
+
// 2. Pure entry — the RSC-safe presentational components ONLY.
|
|
70
|
+
// Built WITHOUT the "use client" banner, so a Server Component can
|
|
71
|
+
// render these directly without drawing a client boundary.
|
|
72
|
+
import { ProductPrice, ItemStock, Breadcrumbs } from 'propeller-v2-react-ui/pure';
|
|
73
|
+
|
|
74
|
+
// 3. Shared entry — pure, runtime-agnostic TS. No React, no "use client".
|
|
75
|
+
// Safe to import from a Server Component OR a Client Component.
|
|
76
|
+
// Contains createServices, toPlain, formatters, helpers and all types.
|
|
77
|
+
import { createServices, formatPrice, getLanguageString } from 'propeller-v2-react-ui/shared';
|
|
78
|
+
|
|
79
|
+
// 4. Stylesheet — precompiled CSS, import once at your app root.
|
|
80
|
+
import 'propeller-v2-react-ui/styles.css';
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
| Import path | Contents | Runtime |
|
|
84
|
+
| ----------------------------------- | -------------------------------------------------------------- | --------------- |
|
|
85
|
+
| `propeller-v2-react-ui` | Components, hooks, contexts, `createServices`, `toPlain`, types | Client only |
|
|
86
|
+
| `propeller-v2-react-ui/pure` | The pure/presentational components only (RSC-safe) | Server & Client |
|
|
87
|
+
| `propeller-v2-react-ui/shared` | `createServices`, `toPlain`, formatters, helpers, types | Server & Client |
|
|
88
|
+
| `propeller-v2-react-ui/styles.css` | Precompiled stylesheet | — |
|
|
89
|
+
|
|
90
|
+
> **Why three code entries?** In Next.js App Router, the main entry carries a
|
|
91
|
+
> `"use client"` directive (it bundles interactive components), so importing
|
|
92
|
+
> it into a Server Component pulls that whole tree client-side. The `/shared`
|
|
93
|
+
> entry is plain TypeScript — import the pure helpers and `createServices`
|
|
94
|
+
> from there when you want them in a Server Component without forcing a
|
|
95
|
+
> client boundary. The `/pure` entry is the same idea for *components*: the
|
|
96
|
+
> presentational components (no hooks, state, effects or browser APIs —
|
|
97
|
+
> `ProductPrice`, `ItemStock`, `OrderTotals`, `Breadcrumbs`, …) re-exported
|
|
98
|
+
> from a bundle built without the `"use client"` banner, so a Server
|
|
99
|
+
> Component can render real product/price/order markup server-side.
|
|
100
|
+
|
|
101
|
+
### The `/pure` entry — RSC-safe components
|
|
102
|
+
|
|
103
|
+
These components are pure: they render entirely from their props, with no
|
|
104
|
+
hooks, state, effects, event handlers, browser APIs or context reads. They
|
|
105
|
+
are re-exported from `/pure`, whose bundle has **no** `"use client"` banner,
|
|
106
|
+
so a React Server Component can import and render them directly:
|
|
107
|
+
|
|
108
|
+
`Breadcrumbs`, `CategoryShortDescription`, `GridTitle`, `ItemStock`,
|
|
109
|
+
`OrderItemCard`, `OrderSummary`, `OrderTotals`, `ProductBulkPrices`,
|
|
110
|
+
`ProductDownloads`, `ProductPrice`, `ProductShortDescription`,
|
|
111
|
+
`ProductVideos`.
|
|
112
|
+
|
|
113
|
+
The same component is also available from the main `propeller-v2-react-ui`
|
|
114
|
+
entry — use that inside a `"use client"` boundary, and `/pure` from a Server
|
|
115
|
+
Component.
|
|
116
|
+
|
|
117
|
+
> **`Breadcrumbs` caveat.** `Breadcrumbs` accepts a `configuration` prop. If
|
|
118
|
+
> the object you pass holds function-valued URL builders, it cannot cross the
|
|
119
|
+
> RSC → client serialization boundary — render `Breadcrumbs` inside a client
|
|
120
|
+
> island in that case, or pass only plain data.
|
|
121
|
+
|
|
122
|
+
## Core concept: the SDK seam
|
|
123
|
+
|
|
124
|
+
The package does **not** ship a GraphQL client or a hardcoded API endpoint.
|
|
125
|
+
GraphQL transport is application-specific — a Next.js app may proxy through
|
|
126
|
+
a route handler, a Vite SPA may call the API directly, another app may use a
|
|
127
|
+
custom rewrite or auth resolver. Baking a URL into the library would lock it
|
|
128
|
+
to one app shape.
|
|
129
|
+
|
|
130
|
+
Instead, **you** own the client. The contract is three steps:
|
|
131
|
+
|
|
132
|
+
1. Construct a `GraphQLClient` from `propeller-sdk-v2` with your endpoint,
|
|
133
|
+
headers and auth resolver.
|
|
134
|
+
2. Call `createServices(client)` once to build a `Services` bundle — a typed
|
|
135
|
+
object of all SDK services (`product`, `cart`, `user`, `order`, …) keyed
|
|
136
|
+
to that client.
|
|
137
|
+
3. Pass **both** `graphqlClient` and `services` into `<PropellerProvider>`.
|
|
138
|
+
|
|
139
|
+
Everything inside the provider then reads services via `useServices()`; no
|
|
140
|
+
component or hook ever instantiates the SDK itself.
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { GraphQLClient } from 'propeller-sdk-v2';
|
|
144
|
+
import { createServices } from 'propeller-v2-react-ui';
|
|
145
|
+
|
|
146
|
+
export const graphqlClient = new GraphQLClient({
|
|
147
|
+
endpoint: '/api/graphql', // your endpoint or proxy route
|
|
148
|
+
headers: { /* auth, locale, … */ },
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
export const services = createServices(graphqlClient);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`createServices` is memoized per client (via a `WeakMap`), so calling it
|
|
155
|
+
repeatedly with the same client returns the same bundle. The `GraphQLClient`
|
|
156
|
+
mutates its own config in place — when you update auth headers after a
|
|
157
|
+
login, cached service instances pick up the change automatically.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Quick start (Next.js App Router)
|
|
162
|
+
|
|
163
|
+
### 1. Create the client
|
|
164
|
+
|
|
165
|
+
Create a single shared module so the client is constructed once.
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
// lib/propeller.ts
|
|
169
|
+
import { GraphQLClient } from 'propeller-sdk-v2';
|
|
170
|
+
import { createServices } from 'propeller-v2-react-ui';
|
|
171
|
+
|
|
172
|
+
export const graphqlClient = new GraphQLClient({
|
|
173
|
+
endpoint: '/api/graphql',
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
export const services = createServices(graphqlClient);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 2. Add a providers component
|
|
180
|
+
|
|
181
|
+
`PropellerProvider` reads a `value` object — the `PropellerInfra` shape. Wire
|
|
182
|
+
in your own auth / company / language / price state.
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
// app/providers.tsx
|
|
186
|
+
'use client';
|
|
187
|
+
|
|
188
|
+
import { useMemo, type ReactNode } from 'react';
|
|
189
|
+
import { PropellerProvider, type PropellerInfra } from 'propeller-v2-react-ui';
|
|
190
|
+
import { graphqlClient, services } from '@/lib/propeller';
|
|
191
|
+
|
|
192
|
+
export function Providers({ children }: { children: ReactNode }) {
|
|
193
|
+
// Replace these with your real auth/company/language stores.
|
|
194
|
+
const user = null; // Contact | Customer | null
|
|
195
|
+
const companyId = undefined;
|
|
196
|
+
const language = 'NL';
|
|
197
|
+
const includeTax = false;
|
|
198
|
+
|
|
199
|
+
const value = useMemo<PropellerInfra>(
|
|
200
|
+
() => ({
|
|
201
|
+
graphqlClient,
|
|
202
|
+
services,
|
|
203
|
+
user,
|
|
204
|
+
companyId,
|
|
205
|
+
language,
|
|
206
|
+
includeTax,
|
|
207
|
+
currency: '€',
|
|
208
|
+
configuration: {},
|
|
209
|
+
portalMode: 'open',
|
|
210
|
+
}),
|
|
211
|
+
[user, companyId, language, includeTax],
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
return <PropellerProvider value={value}>{children}</PropellerProvider>;
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### 3. Wire the root layout
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
// app/layout.tsx
|
|
222
|
+
import 'propeller-v2-react-ui/styles.css';
|
|
223
|
+
import { Providers } from './providers';
|
|
224
|
+
|
|
225
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
226
|
+
return (
|
|
227
|
+
<html lang="en">
|
|
228
|
+
<body>
|
|
229
|
+
<Providers>{children}</Providers>
|
|
230
|
+
</body>
|
|
231
|
+
</html>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### 4. Use components and hooks anywhere
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
// app/cart/page.tsx
|
|
240
|
+
'use client';
|
|
241
|
+
|
|
242
|
+
import { CartOverview } from 'propeller-v2-react-ui';
|
|
243
|
+
|
|
244
|
+
export default function CartPage() {
|
|
245
|
+
return <CartOverview />;
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Quick start (Vite / CRA / SPA)
|
|
252
|
+
|
|
253
|
+
There is no `"use client"` concern outside Next.js — import everything from
|
|
254
|
+
the main entry directly.
|
|
255
|
+
|
|
256
|
+
```tsx
|
|
257
|
+
// main.tsx
|
|
258
|
+
import { createRoot } from 'react-dom/client';
|
|
259
|
+
import { GraphQLClient } from 'propeller-sdk-v2';
|
|
260
|
+
import { PropellerProvider, createServices, type PropellerInfra } from 'propeller-v2-react-ui';
|
|
261
|
+
import 'propeller-v2-react-ui/styles.css';
|
|
262
|
+
import App from './App';
|
|
263
|
+
|
|
264
|
+
const graphqlClient = new GraphQLClient({
|
|
265
|
+
endpoint: 'https://your-store.example.com/graphql',
|
|
266
|
+
});
|
|
267
|
+
const services = createServices(graphqlClient);
|
|
268
|
+
|
|
269
|
+
const value: PropellerInfra = {
|
|
270
|
+
graphqlClient,
|
|
271
|
+
services,
|
|
272
|
+
user: null,
|
|
273
|
+
companyId: undefined,
|
|
274
|
+
language: 'NL',
|
|
275
|
+
includeTax: false,
|
|
276
|
+
currency: '€',
|
|
277
|
+
configuration: {},
|
|
278
|
+
portalMode: 'open',
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
createRoot(document.getElementById('root')!).render(
|
|
282
|
+
<PropellerProvider value={value}>
|
|
283
|
+
<App />
|
|
284
|
+
</PropellerProvider>,
|
|
285
|
+
);
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## The PropellerProvider
|
|
291
|
+
|
|
292
|
+
`PropellerProvider` supplies the **infrastructure context** every component
|
|
293
|
+
and hook depends on. It collects the values that would otherwise be drilled
|
|
294
|
+
through ~20 components as repeated props. You construct one `value` object
|
|
295
|
+
and pass it once.
|
|
296
|
+
|
|
297
|
+
### `PropellerInfra` fields
|
|
298
|
+
|
|
299
|
+
| Field | Type | Description |
|
|
300
|
+
| --------------- | ----------------------------- | --------------------------------------------------------------------------- |
|
|
301
|
+
| `graphqlClient` | `GraphQLClient` | The client you constructed. Used by hooks that accept it explicitly. |
|
|
302
|
+
| `services` | `Services` | The bundle from `createServices(graphqlClient)`. Required. |
|
|
303
|
+
| `user` | `Contact \| Customer \| null` | The signed-in user, or `null` when anonymous. |
|
|
304
|
+
| `companyId` | `number \| undefined` | Active company (B2B) — affects pricing, authorization, addresses. |
|
|
305
|
+
| `language` | `string` | Locale code used to resolve localized content (e.g. `'NL'`, `'EN'`). |
|
|
306
|
+
| `includeTax` | `boolean` | Whether displayed prices include tax. |
|
|
307
|
+
| `currency` | `string` | Currency symbol for price formatting. Default: `'€'`. |
|
|
308
|
+
| `configuration` | `unknown` | Free-form config bag forwarded to components — stuff your own settings in. |
|
|
309
|
+
| `portalMode` | `string` | Storefront mode (e.g. `'open'`, `'closed'`). |
|
|
310
|
+
|
|
311
|
+
The `value` object is reactive — when your auth/company/language state
|
|
312
|
+
changes, recompute it (memoize on those dependencies) and the provider
|
|
313
|
+
propagates the new value. Components re-render with fresh infra; service
|
|
314
|
+
instances are stable across the change.
|
|
315
|
+
|
|
316
|
+
> **Make the value reactive.** Wrap `value` in `useMemo` keyed on your
|
|
317
|
+
> auth/company/language/price state so it only changes when something
|
|
318
|
+
> meaningful changes — not on every render.
|
|
319
|
+
|
|
320
|
+
### Accessing the context
|
|
321
|
+
|
|
322
|
+
- `useServices()` — returns the `Services` bundle. **Throws** when called
|
|
323
|
+
outside a provider; that's an integration error, not something to paper
|
|
324
|
+
over. Use this inside your own components/hooks to talk to the API.
|
|
325
|
+
- `usePropellerContext()` — returns the full `PropellerInfra` or `null` when
|
|
326
|
+
outside a provider. Non-throwing, for components that should still render
|
|
327
|
+
standalone (e.g. in isolation tests or Storybook).
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Using components
|
|
332
|
+
|
|
333
|
+
Import any component from the main entry and render it inside the provider.
|
|
334
|
+
|
|
335
|
+
```tsx
|
|
336
|
+
'use client';
|
|
337
|
+
|
|
338
|
+
import {
|
|
339
|
+
ProductGrid,
|
|
340
|
+
ProductCard,
|
|
341
|
+
Breadcrumbs,
|
|
342
|
+
CartIconAndSidebar,
|
|
343
|
+
} from 'propeller-v2-react-ui';
|
|
344
|
+
|
|
345
|
+
export function CategoryPage({ products }) {
|
|
346
|
+
return (
|
|
347
|
+
<>
|
|
348
|
+
<Breadcrumbs categoryPath={[]} currentLabel="Catalog" />
|
|
349
|
+
<CartIconAndSidebar />
|
|
350
|
+
<ProductGrid products={products} columns={4} />
|
|
351
|
+
</>
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Compound API
|
|
357
|
+
|
|
358
|
+
Layout-heavy components such as `ProductCard` support a **compound API** —
|
|
359
|
+
provide subcomponents as children to control exactly what renders and in
|
|
360
|
+
what order. When you omit children, the component falls back to its
|
|
361
|
+
monolithic layout driven by `show*` / `allow*` prop toggles.
|
|
362
|
+
|
|
363
|
+
```tsx
|
|
364
|
+
<ProductCard product={product}>
|
|
365
|
+
<ProductCard.Image variant="grid" />
|
|
366
|
+
<ProductCard.Name linkable />
|
|
367
|
+
<ProductCard.Price />
|
|
368
|
+
<ProductCard.AddToCart />
|
|
369
|
+
</ProductCard>
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Available components
|
|
373
|
+
|
|
374
|
+
Catalog & product:
|
|
375
|
+
`ProductGrid`, `ProductCard`, `ClusterCard`, `ProductInfo`,
|
|
376
|
+
`ProductPrice`, `ProductBulkPrices`, `ProductGallery`, `ProductVideos`,
|
|
377
|
+
`ProductDownloads`, `ProductSpecifications`, `ProductDescription`,
|
|
378
|
+
`ProductShortDescription`, `ProductTabs`, `ProductSlider`, `ProductBundles`,
|
|
379
|
+
`ItemStock`, `PriceToggle`, `DeliveryDate`.
|
|
380
|
+
|
|
381
|
+
Clusters / configurators:
|
|
382
|
+
`ClusterConfigurator`, `ClusterInfo`, `ClusterOptions`.
|
|
383
|
+
|
|
384
|
+
Grid & navigation:
|
|
385
|
+
`GridToolbar`, `GridFilters`, `GridPagination`, `GridTitle`, `Breadcrumbs`,
|
|
386
|
+
`Menu`, `SearchBar`, `CategoryDescription`, `CategoryShortDescription`.
|
|
387
|
+
|
|
388
|
+
Cart & checkout:
|
|
389
|
+
`AddToCart`, `CartIconAndSidebar`, `CartItem`, `CartOverview`,
|
|
390
|
+
`CartSummary`, `CartCarriers`, `CartPaymethods`, `ActionCode`,
|
|
391
|
+
`ItemsOverview`.
|
|
392
|
+
|
|
393
|
+
Orders:
|
|
394
|
+
`OrderList`, `OrderActions`, `OrderItemCard`, `OrderSummary`,
|
|
395
|
+
`OrderTotals`, `OrderShipments`, `QuoteActions`.
|
|
396
|
+
|
|
397
|
+
Account, auth & B2B:
|
|
398
|
+
`LoginForm`, `RegisterForm`, `ForgotPassword`, `UserDetails`,
|
|
399
|
+
`AccountIconAndMenu`, `AddressCard`, `AddressSelector`, `CompanySwitcher`,
|
|
400
|
+
`AddToFavorite`, `FavoriteLists`, `FavoriteListItem`, `FavoriteListDetails`,
|
|
401
|
+
`PurchaseAuthorizationConfigurator`, `PurchaseAuthorizationRequests`.
|
|
402
|
+
|
|
403
|
+
Every component appends `props.className` on its root element, so a one-off
|
|
404
|
+
style override is a regular prop. See [Styling](#styling).
|
|
405
|
+
|
|
406
|
+
## Partner extension API
|
|
407
|
+
|
|
408
|
+
Customise nested components (price, stock, add-to-cart, whole card) without
|
|
409
|
+
forking. See [docs/extension-api.md](docs/extension-api.md) for the full
|
|
410
|
+
guide covering injection slots, cascade rules, before/after iteration slots,
|
|
411
|
+
whole-card swap, ProductInfo expanded shell, and contract types.
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Using composables (hooks)
|
|
416
|
+
|
|
417
|
+
The composables are **headless** — they hold state and talk to the API, but
|
|
418
|
+
render nothing. Use them to build your own UI, or to drive the supplied
|
|
419
|
+
components.
|
|
420
|
+
|
|
421
|
+
Hooks that hit the API take an **options object** containing a
|
|
422
|
+
`graphqlClient` (and other inputs). Read the client from the provider via
|
|
423
|
+
`usePropellerContext()`, or import your shared client module directly.
|
|
424
|
+
|
|
425
|
+
```tsx
|
|
426
|
+
'use client';
|
|
427
|
+
|
|
428
|
+
import { useCart } from 'propeller-v2-react-ui';
|
|
429
|
+
import { graphqlClient } from '@/lib/propeller';
|
|
430
|
+
|
|
431
|
+
export function MiniCart({ user }) {
|
|
432
|
+
const cart = useCart({
|
|
433
|
+
graphqlClient,
|
|
434
|
+
user,
|
|
435
|
+
language: 'NL',
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
if (cart.loading) return <span>Loading…</span>;
|
|
439
|
+
|
|
440
|
+
return (
|
|
441
|
+
<div>
|
|
442
|
+
<span>{cart.cart?.items?.length ?? 0} items</span>
|
|
443
|
+
<button onClick={() => cart.resolveCart()}>Refresh</button>
|
|
444
|
+
</div>
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
`useProductSearch` example — searching the catalog:
|
|
450
|
+
|
|
451
|
+
```tsx
|
|
452
|
+
'use client';
|
|
453
|
+
|
|
454
|
+
import { useProductSearch } from 'propeller-v2-react-ui';
|
|
455
|
+
import { graphqlClient } from '@/lib/propeller';
|
|
456
|
+
|
|
457
|
+
export function Search() {
|
|
458
|
+
const search = useProductSearch({ graphqlClient });
|
|
459
|
+
// search exposes results, loading state and a query setter — drive
|
|
460
|
+
// your own input + result list from it.
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Available composables
|
|
466
|
+
|
|
467
|
+
| Hook | Purpose |
|
|
468
|
+
| ------------------------------------- | ---------------------------------------------------- |
|
|
469
|
+
| `useAuth` | Login, registration, forgot-password flows |
|
|
470
|
+
| `useCart` | Cart resolution, line items, action codes, checkout gate |
|
|
471
|
+
| `useCheckout` | Carriers, pay methods, placing an order |
|
|
472
|
+
| `useCompany` | Company switching and company data (B2B) |
|
|
473
|
+
| `useAddress` | Address CRUD |
|
|
474
|
+
| `useOrders` | Order history search and detail |
|
|
475
|
+
| `useFavorites` | Favorite lists and list items |
|
|
476
|
+
| `useMenu` | Category navigation tree |
|
|
477
|
+
| `useProductInfo` | Single-product detail data |
|
|
478
|
+
| `useProductSearch` | Catalog search and filtering |
|
|
479
|
+
| `useProductSlider` | Cross-sell / up-sell sliders |
|
|
480
|
+
| `useProductSpecs` | Product attribute groups for spec tables |
|
|
481
|
+
| `useProductBundles` | Product bundle composition |
|
|
482
|
+
| `useClusterConfigurator` | Configurable-product (cluster) selection state |
|
|
483
|
+
| `usePurchaseAuthorizationConfigurator`| B2B purchase-authorization configuration |
|
|
484
|
+
| `usePurchaseAuthorizationRequests` | B2B purchase-authorization request handling |
|
|
485
|
+
| `useServices` | Read the `Services` bundle from the provider |
|
|
486
|
+
| `useResolvedProps` / `useInfraProps` | Merge explicit props with provider infra defaults |
|
|
487
|
+
|
|
488
|
+
Each hook exports its own `Use*Options` and `Use*Return` types — import them
|
|
489
|
+
for fully typed integration.
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
## Server Components & data fetching
|
|
494
|
+
|
|
495
|
+
In Next.js App Router you can fetch Propeller data on the server. Build a
|
|
496
|
+
`GraphQLClient` server-side (with your server endpoint, API keys, cookie
|
|
497
|
+
handling), call `createServices`, and use the SDK services directly. Import
|
|
498
|
+
`createServices` and the pure helpers from `propeller-v2-react-ui/shared` so
|
|
499
|
+
no client boundary is forced.
|
|
500
|
+
|
|
501
|
+
```tsx
|
|
502
|
+
// app/product/[id]/page.tsx — a Server Component
|
|
503
|
+
import { GraphQLClient } from 'propeller-sdk-v2';
|
|
504
|
+
import { createServices, getLanguageString } from 'propeller-v2-react-ui/shared';
|
|
505
|
+
import { ProductInfo } from 'propeller-v2-react-ui'; // Client Component
|
|
506
|
+
|
|
507
|
+
async function getServerClient() {
|
|
508
|
+
return new GraphQLClient({
|
|
509
|
+
endpoint: process.env.PROPELLER_GRAPHQL_ENDPOINT!,
|
|
510
|
+
headers: { /* server API key, etc. */ },
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export default async function ProductPage({ params }: { params: { id: string } }) {
|
|
515
|
+
const services = createServices(await getServerClient());
|
|
516
|
+
const product = await services.product /* …fetch by id… */;
|
|
517
|
+
|
|
518
|
+
return (
|
|
519
|
+
<article>
|
|
520
|
+
<h1>{getLanguageString(product.names, 'NL')}</h1>
|
|
521
|
+
{/* ProductInfo is interactive — it renders client-side */}
|
|
522
|
+
<ProductInfo product={product} />
|
|
523
|
+
</article>
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
The `/shared` entry is the safe surface for Server Components: it has no
|
|
529
|
+
React and no `"use client"` directive, so importing `formatPrice`,
|
|
530
|
+
`getLanguageString`, `getStockStatus`, `createServices`, etc. from it does
|
|
531
|
+
**not** pull interactive code into the server bundle.
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
## Styling
|
|
536
|
+
|
|
537
|
+
The package ships a precompiled stylesheet (`dist/styles.css`) that bundles
|
|
538
|
+
every utility class its components reference plus the theme tokens they
|
|
539
|
+
resolve against. Import it once at your app root:
|
|
540
|
+
|
|
541
|
+
```ts
|
|
542
|
+
import 'propeller-v2-react-ui/styles.css';
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
You do **not** need Tailwind in your project — the CSS is plain compiled
|
|
546
|
+
CSS. If you do use Tailwind, the import doesn't conflict; your own output is
|
|
547
|
+
a separate stylesheet.
|
|
548
|
+
|
|
549
|
+
Skip the import entirely and components render unstyled.
|
|
550
|
+
|
|
551
|
+
### Three override surfaces
|
|
552
|
+
|
|
553
|
+
**1. Theme tokens** — the package declares CSS variables (`--primary`,
|
|
554
|
+
`--card`, `--border`, `--radius-container`, …) at low specificity. Redeclare
|
|
555
|
+
any of them and every utility resolving against it updates:
|
|
556
|
+
|
|
557
|
+
```css
|
|
558
|
+
/* your globals.css — reskin the whole package */
|
|
559
|
+
:root {
|
|
560
|
+
--primary: #ff7043;
|
|
561
|
+
--primary-foreground: #ffffff;
|
|
562
|
+
--card: #fafafa;
|
|
563
|
+
--border: #e1e1e1;
|
|
564
|
+
--radius-container: 12px;
|
|
565
|
+
}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
Scope-limited overrides work too — declare the variable on a wrapper class
|
|
569
|
+
and only that subtree changes.
|
|
570
|
+
|
|
571
|
+
**2. BEM hooks** — every styled element carries a BEM class alongside its
|
|
572
|
+
utilities (`.propeller-product-card`, `.propeller-product-card__price`,
|
|
573
|
+
`.propeller-breadcrumbs__separator`, …). The package emits utilities inside
|
|
574
|
+
`@layer utilities`, so any plain consumer rule targeting a BEM class wins by
|
|
575
|
+
cascade order — no `!important` needed:
|
|
576
|
+
|
|
577
|
+
```css
|
|
578
|
+
.propeller-product-card {
|
|
579
|
+
background: #fff8e1;
|
|
580
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
|
581
|
+
}
|
|
582
|
+
.propeller-breadcrumbs__separator { display: none; }
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
**3. Per-instance `className`** — every component appends `props.className`
|
|
586
|
+
on its root, so a one-off override is a regular prop:
|
|
587
|
+
|
|
588
|
+
```tsx
|
|
589
|
+
<ProductCard product={p} className="ring-2 ring-yellow-400" />
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
`className` **adds to** the base classes — it doesn't replace them. To strip
|
|
593
|
+
a default, use a BEM hook.
|
|
594
|
+
|
|
595
|
+
See [STYLING.md](./STYLING.md) for the full token list, the complete BEM
|
|
596
|
+
hook catalog, and the cascade rationale.
|
|
597
|
+
|
|
598
|
+
---
|
|
599
|
+
|
|
600
|
+
## API reference
|
|
601
|
+
|
|
602
|
+
### From `propeller-v2-react-ui`
|
|
603
|
+
|
|
604
|
+
- **SDK glue** — `createServices`, `toPlain`
|
|
605
|
+
- **Contexts** — `PropellerProvider`, `usePropellerContext`,
|
|
606
|
+
`ProductGridConfigProvider`, `useProductGridConfig`
|
|
607
|
+
- **Composables** — all `use*` hooks listed above
|
|
608
|
+
- **Components** — all components listed above
|
|
609
|
+
- **Helpers** — `formatPrice`, `formatDate`, `calcDiscountPercent`,
|
|
610
|
+
`getStockStatus`, `getLabel`, `getLanguageString`, `getCountryName`,
|
|
611
|
+
`getProductImageUrl`, `getClusterImageUrl`, `getProductSku`,
|
|
612
|
+
`getClusterSku`, `getLocalizedValue`, `stripHtml`, `shouldTruncate`,
|
|
613
|
+
`truncateAt`, `isContact`, `isCustomer`, `getUserId`, `getCompany`,
|
|
614
|
+
`getCompanyId`, `getAddresses`, `getDefaultInvoiceAddress`,
|
|
615
|
+
`getDefaultDeliveryAddress`, `isEmbeddable`, `normalizeVideoUrl`,
|
|
616
|
+
`isContentHidden`, `attributeNameMatches`, `getAttributeDisplayName`,
|
|
617
|
+
`extractAttributeValues`, `collectAttributeValues`,
|
|
618
|
+
`filterProductsBySelections`, `initCart`, `fetchActiveCart`,
|
|
619
|
+
`mergeAnonymousCart`, `COUNTRIES`
|
|
620
|
+
- **Types** — `Services`, `PropellerInfra`, `PropellerProviderProps`,
|
|
621
|
+
`ProductGridConfig`, `Country`, `AnyUser`, all `Use*Options` /
|
|
622
|
+
`Use*Return` hook types, and the full domain type set (`auth`, `cart`,
|
|
623
|
+
`company`, `favorites`, `orders`, `pagination`, `product`).
|
|
624
|
+
|
|
625
|
+
### From `propeller-v2-react-ui/shared`
|
|
626
|
+
|
|
627
|
+
A subset of the above with **no React dependency** — `createServices`,
|
|
628
|
+
`toPlain`, all formatters and helpers, `COUNTRIES`, and every domain type.
|
|
629
|
+
Use it from Server Components or any non-React code.
|
|
630
|
+
|
|
631
|
+
## TypeScript
|
|
632
|
+
|
|
633
|
+
The package is written in TypeScript and ships full `.d.ts` declarations.
|
|
634
|
+
Every hook exports its `Use*Options` and `Use*Return` types, every component
|
|
635
|
+
its `*Props` type, and all domain types (`Cart`, `Product`, `Order`, …) flow
|
|
636
|
+
through from `propeller-sdk-v2`.
|
|
637
|
+
|
|
638
|
+
```ts
|
|
639
|
+
import type {
|
|
640
|
+
PropellerInfra,
|
|
641
|
+
UseCartReturn,
|
|
642
|
+
ProductCardProps,
|
|
643
|
+
} from 'propeller-v2-react-ui';
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
## Building from source
|
|
647
|
+
|
|
648
|
+
```bash
|
|
649
|
+
npm install
|
|
650
|
+
npm run build
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
Outputs to `./dist`:
|
|
654
|
+
|
|
655
|
+
- `index.js` / `index.cjs` — client bundle (prefixed with `"use client"`)
|
|
656
|
+
- `shared.js` / `shared.cjs` — runtime-agnostic bundle (no directive)
|
|
657
|
+
- `styles.css` — precompiled, minified stylesheet
|
|
658
|
+
- `*.d.ts` — type declarations
|
|
659
|
+
|
|
660
|
+
Other scripts:
|
|
661
|
+
|
|
662
|
+
| Script | Purpose |
|
|
663
|
+
| ------------------- | ------------------------------------ |
|
|
664
|
+
| `npm run dev` | Rebuild the JS bundle on change |
|
|
665
|
+
| `npm run build:js` | Build JS bundles only |
|
|
666
|
+
| `npm run build:css` | Compile the stylesheet only |
|
|
667
|
+
| `npm run typecheck` | Type-check without emitting |
|
|
668
|
+
| `npm run clean` | Remove the `dist` directory |
|