@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/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 |