@rudra-js/react 0.7.4 → 0.7.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 +11 -5
- package/dist/blocks/block-renderers.d.ts +5 -10
- package/dist/blocks/block-renderers.d.ts.map +1 -1
- package/dist/blocks/block-renderers.js +13 -0
- package/dist/blocks/block-renderers.js.map +1 -1
- package/dist/blocks/product-card.d.ts +0 -8
- package/dist/blocks/product-card.d.ts.map +1 -1
- package/dist/blocks/product-card.js +0 -9
- package/dist/blocks/product-card.js.map +1 -1
- package/dist/registry.js +1 -2
- package/dist/registry.js.map +1 -1
- package/dist/render-context.d.ts.map +1 -1
- package/dist/render-context.js +12 -16
- package/dist/render-context.js.map +1 -1
- package/dist/rudra-component.d.ts +0 -46
- package/dist/rudra-component.d.ts.map +1 -1
- package/dist/rudra-component.js +9 -34
- package/dist/rudra-component.js.map +1 -1
- package/package.json +8 -5
- package/src/blocks/block-renderers.tsx +48 -13
- package/src/blocks/product-card.tsx +0 -10
- package/src/registry.ts +1 -1
- package/src/render-context.ts +12 -20
- package/src/rudra-component.tsx +10 -79
- package/dist/blocks/bundle-block.d.ts +0 -7
- package/dist/blocks/bundle-block.d.ts.map +0 -1
- package/dist/blocks/bundle-block.js +0 -17
- package/dist/blocks/bundle-block.js.map +0 -1
- package/src/blocks/bundle-block.tsx +0 -43
package/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @rudra-js/react
|
|
2
2
|
|
|
3
|
+
This package is part of [rudra-js](https://rudrajs.com).
|
|
4
|
+
|
|
5
|
+
rudra-js is an open-source TypeScript library (MIT) you install from npm. A language model designs one product-recommendation block for a page in your shop, rudra-js checks it against your catalog, and React renders it.
|
|
6
|
+
|
|
7
|
+
It is not a hosted platform, a visual or low-code app builder, or a workspace you sign up for.
|
|
8
|
+
|
|
3
9
|
Renders a component specification from
|
|
4
10
|
[`@rudra-js/core`](https://github.com/clivedsouza1010/rudra-js/tree/main/packages/core) with React
|
|
5
11
|
18 or 19.
|
|
@@ -44,8 +50,8 @@ the model wrote is rendered as escaped text.
|
|
|
44
50
|
| Tone, headline, the words in each block | Every price |
|
|
45
51
|
| Only in per-shopper mode: which products, and how each is described | Every image and link |
|
|
46
52
|
|
|
47
|
-
In the default cohort mode
|
|
48
|
-
|
|
53
|
+
In the default cohort mode core fills in the products, their order and the reason under each,
|
|
54
|
+
per request, and a badge the model wrote gets dropped. The model picks those only under
|
|
49
55
|
`generation: 'per-shopper'`. In either mode it still chooses the product a hero names. You'll find
|
|
50
56
|
the full split in
|
|
51
57
|
[What the model decides, by mode](https://github.com/clivedsouza1010/rudra-js/tree/main/packages/core#what-the-model-decides-by-mode).
|
|
@@ -54,8 +60,8 @@ The specification has no field carrying a title, a price, an image or a URL. Pro
|
|
|
54
60
|
resolved at render time from `products`, keyed by a SKU reconciliation has already checked.
|
|
55
61
|
|
|
56
62
|
**Validate `products` with `productSchema` from `@rudra-js/core`**, the same schema your candidates
|
|
57
|
-
already passed. This prop is a second
|
|
58
|
-
and `productSchema` is what rejects a protocol-relative `//evil.example/pixel.png` or a `data:` URL.
|
|
63
|
+
already passed. This prop is a second way for data to reach the page. `imageUrl` lands in an
|
|
64
|
+
`<img src>`, and `productSchema` is what rejects a protocol-relative `//evil.example/pixel.png` or a `data:` URL.
|
|
59
65
|
It reads a path the way a browser does, so `/\evil.example/pixel.png` and the same trick written
|
|
60
66
|
with a tab or a newline in it are rejected too — each one resolves to someone else's host. React
|
|
61
67
|
neutralises a `javascript:` URL by itself, but not any of those. And if a price isn't a finite
|
|
@@ -192,7 +198,7 @@ page is broken.
|
|
|
192
198
|
The sets your shop sells together, the same way `products` is your catalog. The model only asks for
|
|
193
199
|
a bundle block. It never invents one, and it _never_ sees a price.
|
|
194
200
|
|
|
195
|
-
You offer the sets, and
|
|
201
|
+
You offer the sets, and core picks which one fills each block. That happens inside
|
|
196
202
|
`reconcileSpec` as the page is served, after the spec was generated rather than before, and it goes on what this
|
|
197
203
|
shopper has in their basket, has looked at, or is browsing right now. The spec then carries the id
|
|
198
204
|
it picked. This prop supplies the rest: that set's members, its price, the currency that price is
|
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Five of the six block renderers. The bundle one lives in `bundle-block.tsx`,
|
|
3
|
-
* next door, since it draws a set the shop picked rather than the model.
|
|
4
|
-
*
|
|
5
|
-
* Each takes a block the model produced and a context the host owns. None of
|
|
6
|
-
* them interpolate markup, and none of them read a product fact from the block
|
|
7
|
-
* — that is what keeps generated output to the role of deciding layout, order
|
|
8
|
-
* and wording.
|
|
9
|
-
*/
|
|
10
|
-
import type { BannerBlock, CarouselBlock, CopyBlock, GridBlock, HeroBlock } from '@rudra-js/core';
|
|
1
|
+
import type { BannerBlock, BundleBlock, CarouselBlock, CopyBlock, GridBlock, HeroBlock } from '@rudra-js/core';
|
|
11
2
|
import { type BlockRenderContext } from '../render-context.js';
|
|
12
3
|
export declare function HeroRenderer({ block, context, }: {
|
|
13
4
|
block: HeroBlock;
|
|
@@ -27,4 +18,8 @@ export declare function BannerRenderer({ block }: {
|
|
|
27
18
|
export declare function CopyRenderer({ block }: {
|
|
28
19
|
block: CopyBlock;
|
|
29
20
|
}): import("react").JSX.Element;
|
|
21
|
+
export declare function BundleRenderer({ block, context, }: {
|
|
22
|
+
block: BundleBlock;
|
|
23
|
+
context: BlockRenderContext;
|
|
24
|
+
}): import("react").JSX.Element | null;
|
|
30
25
|
//# sourceMappingURL=block-renderers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"block-renderers.d.ts","sourceRoot":"","sources":["../../src/blocks/block-renderers.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"block-renderers.d.ts","sourceRoot":"","sources":["../../src/blocks/block-renderers.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EACX,aAAa,EACb,SAAS,EACT,SAAS,EACT,SAAS,EAEV,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAmB,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAGhF,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,OAAO,GACR,EAAE;IACD,KAAK,EAAE,SAAS,CAAC;IACjB,OAAO,EAAE,kBAAkB,CAAC;CAC7B,+BAmBA;AAED,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,OAAO,GACR,EAAE;IACD,KAAK,EAAE,SAAS,CAAC;IACjB,OAAO,EAAE,kBAAkB,CAAC;CAC7B,+BAWA;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,OAAO,GACR,EAAE;IACD,KAAK,EAAE,aAAa,CAAC;IACrB,OAAO,EAAE,kBAAkB,CAAC;CAC7B,+BAWA;AAED,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,WAAW,CAAA;CAAE,+BAO/D;AAED,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,SAAS,CAAA;CAAE,+BAO3D;AAED,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,OAAO,GACR,EAAE;IACD,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,kBAAkB,CAAC;CAC7B,sCA+BA"}
|
|
@@ -17,4 +17,17 @@ export function BannerRenderer({ block }) {
|
|
|
17
17
|
export function CopyRenderer({ block }) {
|
|
18
18
|
return (_jsxs("section", { className: "rudra-copy", children: [block.title ? _jsx("h3", { className: "rudra-copy__title", children: block.title }) : null, _jsx("p", { className: "rudra-copy__body", children: block.body })] }));
|
|
19
19
|
}
|
|
20
|
+
export function BundleRenderer({ block, context, }) {
|
|
21
|
+
const bundle = block.bundleId === null ? undefined : context.bundles.get(block.bundleId);
|
|
22
|
+
if (!bundle)
|
|
23
|
+
return null;
|
|
24
|
+
const products = [];
|
|
25
|
+
for (const sku of bundle.skus) {
|
|
26
|
+
const product = sellableProduct(context.products, sku);
|
|
27
|
+
if (!product)
|
|
28
|
+
return null;
|
|
29
|
+
products.push(product);
|
|
30
|
+
}
|
|
31
|
+
return (_jsxs("section", { className: "rudra-bundle", children: [bundle.label ? _jsx("h3", { className: "rudra-bundle__label", children: bundle.label }) : null, block.title ? _jsx("p", { className: "rudra-bundle__title", children: block.title }) : null, block.body ? _jsx("p", { className: "rudra-bundle__body", children: block.body }) : null, _jsx("ul", { className: "rudra-bundle__items", children: products.map((product) => (_jsx("li", { className: "rudra-bundle__item", "data-rudra-sku": product.sku, children: _jsx("a", { className: "rudra-bundle__link", href: context.hrefForSku(product.sku), children: product.title }) }, product.sku))) }), _jsx("p", { className: "rudra-bundle__price", children: context.formatBundlePrice(bundle) }), block.ctaLabel ? _jsx("span", { className: "rudra-bundle__cta", children: block.ctaLabel }) : null] }));
|
|
32
|
+
}
|
|
20
33
|
//# sourceMappingURL=block-renderers.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"block-renderers.js","sourceRoot":"","sources":["../../src/blocks/block-renderers.tsx"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"block-renderers.js","sourceRoot":"","sources":["../../src/blocks/block-renderers.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
import type { ProductReference } from '@rudra-js/core';
|
|
2
2
|
import { type BlockRenderContext } from '../render-context.js';
|
|
3
|
-
/**
|
|
4
|
-
* One product.
|
|
5
|
-
*
|
|
6
|
-
* Every fact here — the title, the price, the image, the link — is read from
|
|
7
|
-
* the catalog, not from the specification. The specification contributes only
|
|
8
|
-
* `reason`, `badge` and `emphasis`, and React escapes all three on the way
|
|
9
|
-
* into the markup.
|
|
10
|
-
*/
|
|
11
3
|
export declare function ProductCard({ reference, context, }: {
|
|
12
4
|
reference: ProductReference;
|
|
13
5
|
context: BlockRenderContext;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"product-card.d.ts","sourceRoot":"","sources":["../../src/blocks/product-card.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAmB,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAEhF
|
|
1
|
+
{"version":3,"file":"product-card.d.ts","sourceRoot":"","sources":["../../src/blocks/product-card.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAmB,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAEhF,wBAAgB,WAAW,CAAC,EAC1B,SAAS,EACT,OAAO,GACR,EAAE;IACD,SAAS,EAAE,gBAAgB,CAAC;IAC5B,OAAO,EAAE,kBAAkB,CAAC;CAC7B,sCAyBA"}
|
|
@@ -1,16 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { sellableProduct } from '../render-context.js';
|
|
3
|
-
/**
|
|
4
|
-
* One product.
|
|
5
|
-
*
|
|
6
|
-
* Every fact here — the title, the price, the image, the link — is read from
|
|
7
|
-
* the catalog, not from the specification. The specification contributes only
|
|
8
|
-
* `reason`, `badge` and `emphasis`, and React escapes all three on the way
|
|
9
|
-
* into the markup.
|
|
10
|
-
*/
|
|
11
3
|
export function ProductCard({ reference, context, }) {
|
|
12
4
|
const product = sellableProduct(context.products, reference.sku);
|
|
13
|
-
// Gone from the catalog, or sold out since the spec was made: render nothing, not half a card.
|
|
14
5
|
if (!product)
|
|
15
6
|
return null;
|
|
16
7
|
const isFeatured = reference.emphasis === 'featured';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"product-card.js","sourceRoot":"","sources":["../../src/blocks/product-card.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,eAAe,EAA2B,MAAM,sBAAsB,CAAC;AAEhF
|
|
1
|
+
{"version":3,"file":"product-card.js","sourceRoot":"","sources":["../../src/blocks/product-card.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,eAAe,EAA2B,MAAM,sBAAsB,CAAC;AAEhF,MAAM,UAAU,WAAW,CAAC,EAC1B,SAAS,EACT,OAAO,GAIR;IACC,MAAM,OAAO,GAAG,eAAe,CAAC,OAAO,CAAC,QAAQ,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;IACjE,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAE1B,MAAM,UAAU,GAAG,SAAS,CAAC,QAAQ,KAAK,UAAU,CAAC;IAErD,OAAO,CACL,aACE,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,oBACrB,OAAO,CAAC,GAAG,sBACT,SAAS,CAAC,KAAK,EACjC,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC,iCAAiC,CAAC,CAAC,CAAC,YAAY,aAEvE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAClB,cAAK,SAAS,EAAC,mBAAmB,EAAC,GAAG,EAAE,OAAO,CAAC,QAAQ,EAAE,GAAG,EAAC,EAAE,EAAC,OAAO,EAAC,MAAM,GAAG,CACnF,CAAC,CAAC,CAAC,IAAI,EAER,gBAAM,SAAS,EAAC,kBAAkB,aAC/B,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,eAAM,SAAS,EAAC,mBAAmB,YAAE,SAAS,CAAC,KAAK,GAAQ,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,EAC3F,eAAM,SAAS,EAAC,mBAAmB,YAAE,OAAO,CAAC,KAAK,GAAQ,EAAC,GAAG,EAC9D,eAAM,SAAS,EAAC,mBAAmB,YAAE,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,GAAQ,EAAC,GAAG,EAC5E,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,eAAM,SAAS,EAAC,oBAAoB,YAAE,SAAS,CAAC,MAAM,GAAQ,CAAC,CAAC,CAAC,IAAI,IACpF,IACL,CACL,CAAC;AACJ,CAAC"}
|
package/dist/registry.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { BannerRenderer, CarouselRenderer, CopyRenderer, GridRenderer, HeroRenderer, } from './blocks/block-renderers.js';
|
|
2
|
-
import { BundleRenderer } from './blocks/bundle-block.js';
|
|
1
|
+
import { BannerRenderer, BundleRenderer, CarouselRenderer, CopyRenderer, GridRenderer, HeroRenderer, } from './blocks/block-renderers.js';
|
|
3
2
|
export const defaultRegistry = {
|
|
4
3
|
hero: HeroRenderer,
|
|
5
4
|
grid: GridRenderer,
|
package/dist/registry.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,YAAY,GACb,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,YAAY,GACb,MAAM,6BAA6B,CAAC;AAWrC,MAAM,CAAC,MAAM,eAAe,GAAkB;IAC5C,IAAI,EAAE,YAAY;IAClB,IAAI,EAAE,YAAY;IAClB,QAAQ,EAAE,gBAAgB;IAC1B,MAAM,EAAE,cAAc;IACtB,IAAI,EAAE,YAAY;IAClB,MAAM,EAAE,cAAc;CACvB,CAAC;AAEF,MAAM,UAAU,cAAc,CAAC,SAAiC;IAC9D,OAAO;QACL,IAAI,EAAE,SAAS,CAAC,IAAI,IAAI,eAAe,CAAC,IAAI;QAC5C,IAAI,EAAE,SAAS,CAAC,IAAI,IAAI,eAAe,CAAC,IAAI;QAC5C,QAAQ,EAAE,SAAS,CAAC,QAAQ,IAAI,eAAe,CAAC,QAAQ;QACxD,MAAM,EAAE,SAAS,CAAC,MAAM,IAAI,eAAe,CAAC,MAAM;QAClD,IAAI,EAAE,SAAS,CAAC,IAAI,IAAI,eAAe,CAAC,IAAI;QAC5C,MAAM,EAAE,SAAS,CAAC,MAAM,IAAI,eAAe,CAAC,MAAM;KACnD,CAAC;AACJ,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-context.d.ts","sourceRoot":"","sources":["../src/render-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAEtD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"render-context.d.ts","sourceRoot":"","sources":["../src/render-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAEtD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChD,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9C,QAAQ,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IAC7C,QAAQ,CAAC,WAAW,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC;IACnD,QAAQ,CAAC,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;CACxD;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,EACtC,GAAG,EAAE,MAAM,GACV,OAAO,GAAG,SAAS,CAGrB;AAYD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAQ5E;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAQhF"}
|
package/dist/render-context.js
CHANGED
|
@@ -5,32 +5,28 @@ export function sellableProduct(products, sku) {
|
|
|
5
5
|
const product = products.get(sku);
|
|
6
6
|
return product?.isInStock === false ? undefined : product;
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
if (!Number.isFinite(product.price)) {
|
|
10
|
-
throw new TypeError(`price for SKU ${product.sku} is ${String(product.price)}, not a finite number — ` +
|
|
11
|
-
'catalog objects must satisfy productSchema from @rudra-js/core');
|
|
12
|
-
}
|
|
8
|
+
function formatMoney(price, currency, locale) {
|
|
13
9
|
let formatter;
|
|
14
10
|
try {
|
|
15
|
-
formatter = new Intl.NumberFormat(locale, { style: 'currency', currency
|
|
11
|
+
formatter = new Intl.NumberFormat(locale, { style: 'currency', currency });
|
|
16
12
|
}
|
|
17
13
|
catch {
|
|
18
|
-
return `${
|
|
14
|
+
return `${currency} ${price}`;
|
|
15
|
+
}
|
|
16
|
+
return formatter.format(price);
|
|
17
|
+
}
|
|
18
|
+
export function defaultFormatPrice(product, locale) {
|
|
19
|
+
if (!Number.isFinite(product.price)) {
|
|
20
|
+
throw new TypeError(`price for SKU ${product.sku} is ${String(product.price)}, not a finite number — ` +
|
|
21
|
+
'catalog objects must satisfy productSchema from @rudra-js/core');
|
|
19
22
|
}
|
|
20
|
-
return
|
|
23
|
+
return formatMoney(product.price, product.currency, locale);
|
|
21
24
|
}
|
|
22
25
|
export function defaultFormatBundlePrice(bundle, locale) {
|
|
23
26
|
if (!Number.isFinite(bundle.price)) {
|
|
24
27
|
throw new TypeError(`price for bundle ${bundle.id} is ${String(bundle.price)}, not a finite number — ` +
|
|
25
28
|
'bundle objects must satisfy bundleSchema from @rudra-js/core');
|
|
26
29
|
}
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
formatter = new Intl.NumberFormat(locale, { style: 'currency', currency: bundle.currency });
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
return `${bundle.currency} ${bundle.price}`;
|
|
33
|
-
}
|
|
34
|
-
return formatter.format(bundle.price);
|
|
30
|
+
return formatMoney(bundle.price, bundle.currency, locale);
|
|
35
31
|
}
|
|
36
32
|
//# sourceMappingURL=render-context.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-context.js","sourceRoot":"","sources":["../src/render-context.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"render-context.js","sourceRoot":"","sources":["../src/render-context.ts"],"names":[],"mappings":"AAUA,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,OAAO,YAAY,kBAAkB,CAAC,GAAG,CAAC,EAAE,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,QAAsC,EACtC,GAAW;IAEX,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAClC,OAAO,OAAO,EAAE,SAAS,KAAK,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;AAC5D,CAAC;AAED,SAAS,WAAW,CAAC,KAAa,EAAE,QAAgB,EAAE,MAAe;IACnE,IAAI,SAA4B,CAAC;IACjC,IAAI,CAAC;QACH,SAAS,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC7E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,QAAQ,IAAI,KAAK,EAAE,CAAC;IAChC,CAAC;IACD,OAAO,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,OAAgB,EAAE,MAAe;IAClE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,SAAS,CACjB,iBAAiB,OAAO,CAAC,GAAG,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,0BAA0B;YAChF,gEAAgE,CACnE,CAAC;IACJ,CAAC;IACD,OAAO,WAAW,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,MAAc,EAAE,MAAe;IACtE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,SAAS,CACjB,oBAAoB,MAAM,CAAC,EAAE,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,0BAA0B;YAChF,8DAA8D,CACjE,CAAC;IACJ,CAAC;IACD,OAAO,WAAW,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;AAC5D,CAAC"}
|
|
@@ -2,62 +2,16 @@ import type { Bundle, ComponentSpec, Product } from '@rudra-js/core';
|
|
|
2
2
|
import { type BlockRegistry } from './registry.js';
|
|
3
3
|
export interface RudraComponentProps {
|
|
4
4
|
spec: ComponentSpec;
|
|
5
|
-
/**
|
|
6
|
-
* The host catalog. Every product fact on the page comes from here rather
|
|
7
|
-
* than from the specification.
|
|
8
|
-
*
|
|
9
|
-
* A list of products, or anything keyed by SKU — a `Map`, or your own view
|
|
10
|
-
* over a catalog too large to hold in one. The renderers only ever call
|
|
11
|
-
* `get(sku)`, and `has` is how a keyed catalog is told apart from a list. The
|
|
12
|
-
* type asks for a `ReadonlyMap`, so a view of your own implements one or is
|
|
13
|
-
* cast with `as unknown as ReadonlyMap<string, Product>`.
|
|
14
|
-
*
|
|
15
|
-
* Validate them with `productSchema` from `@rudra-js/core` — the same schema
|
|
16
|
-
* your candidates already passed — not with `parseTrackingInput`, which
|
|
17
|
-
* parses a whole tracking payload and will reject a bare catalog.
|
|
18
|
-
*
|
|
19
|
-
* This is a second door into the framework. `imageUrl` lands in an
|
|
20
|
-
* `<img src>`, and `productSchema` is the only thing that rejects a
|
|
21
|
-
* protocol-relative `//evil.example/pixel.png` or a `data:` URL — React
|
|
22
|
-
* neutralises `javascript:` on its own, but not those. A price that is not a
|
|
23
|
-
* finite number throws rather than rendering as free.
|
|
24
|
-
*/
|
|
25
5
|
products: ProductCatalog;
|
|
26
|
-
/** Sets the shop sells together. Only needed if a spec can carry a bundle block. */
|
|
27
6
|
bundles?: readonly Bundle[];
|
|
28
7
|
registry?: BlockRegistry;
|
|
29
8
|
hrefForSku?: (sku: string) => string;
|
|
30
9
|
formatPrice?: (product: Product) => string;
|
|
31
|
-
/** Same as `formatPrice`, but for a bundle — the shop's price, not a sum of the parts. */
|
|
32
10
|
formatBundlePrice?: (bundle: Bundle) => string;
|
|
33
|
-
/**
|
|
34
|
-
* The shopper's locale, used to punctuate prices. Defaults to the server's,
|
|
35
|
-
* which is almost never the shopper's — pass it if the shop serves more than
|
|
36
|
-
* one. Ignored when `formatPrice` and `formatBundlePrice` are supplied.
|
|
37
|
-
*/
|
|
38
11
|
locale?: string;
|
|
39
|
-
/**
|
|
40
|
-
* Adds the model's own reasoning, the provider and the model name to the
|
|
41
|
-
* markup. Useful while developing and while benchmarking; it publishes which
|
|
42
|
-
* vendor a shop uses and whether the component is currently degraded, so it
|
|
43
|
-
* is off unless asked for.
|
|
44
|
-
*/
|
|
45
12
|
hasDiagnostics?: boolean;
|
|
46
13
|
className?: string;
|
|
47
14
|
}
|
|
48
|
-
/** A list of products, or a `ReadonlyMap` keyed by SKU, which a view of your own can implement. */
|
|
49
15
|
export type ProductCatalog = readonly Product[] | ReadonlyMap<string, Product>;
|
|
50
|
-
/**
|
|
51
|
-
* Renders a component specification.
|
|
52
|
-
*
|
|
53
|
-
* Plain React with no hooks, no state and no effects, so it renders with
|
|
54
|
-
* `renderToString`, in a client-rendered app, or as a Server Component where the
|
|
55
|
-
* framework supports them. Rendered on the server, the whole component arrives
|
|
56
|
-
* in the initial HTML response, which is what removes the pop-in a
|
|
57
|
-
* client-fetched recommendation rail has — and what makes the content visible
|
|
58
|
-
* to a crawler that does not run JavaScript.
|
|
59
|
-
*
|
|
60
|
-
* Renders nothing at all when no block produced markup.
|
|
61
|
-
*/
|
|
62
16
|
export declare function RudraComponent({ spec, products, bundles, registry, hrefForSku, formatPrice, formatBundlePrice, locale, hasDiagnostics, className, }: RudraComponentProps): import("react").JSX.Element | null;
|
|
63
17
|
//# sourceMappingURL=rudra-component.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rudra-component.d.ts","sourceRoot":"","sources":["../src/rudra-component.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAQ5E,OAAO,EAAmB,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAEpE,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,aAAa,CAAC;IACpB
|
|
1
|
+
{"version":3,"file":"rudra-component.d.ts","sourceRoot":"","sources":["../src/rudra-component.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAQ5E,OAAO,EAAmB,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAEpE,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,aAAa,CAAC;IACpB,QAAQ,EAAE,cAAc,CAAC;IACzB,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IACrC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC;IAC3C,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,cAAc,GAAG,SAAS,OAAO,EAAE,GAAG,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAsE/E,wBAAgB,cAAc,CAAC,EAC7B,IAAI,EACJ,QAAQ,EACR,OAAO,EACP,QAA0B,EAC1B,UAA8B,EAC9B,WAAW,EACX,iBAAiB,EACjB,MAAM,EACN,cAAsB,EACtB,SAAS,GACV,EAAE,mBAAmB,sCAuCrB"}
|
package/dist/rudra-component.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { defaultFormatBundlePrice, defaultFormatPrice, defaultHrefForSku, sellableProduct, } from './render-context.js';
|
|
3
3
|
import { defaultRegistry } from './registry.js';
|
|
4
|
-
// Asks what the renderers call rather than which class the host built, since
|
|
5
|
-
// `instanceof Map` is per-realm. Checked before the list branch because a
|
|
6
|
-
// collection can answer both: an Immutable.js map has `map`, and converting
|
|
7
|
-
// through it yields `[sku, product]` pairs rather than products.
|
|
8
4
|
function isKeyedBySku(catalog) {
|
|
9
5
|
const candidate = catalog;
|
|
10
6
|
return typeof candidate.get === 'function' && typeof candidate.has === 'function';
|
|
@@ -18,28 +14,24 @@ function toProductMap(catalog) {
|
|
|
18
14
|
}
|
|
19
15
|
return new Map(catalog.map((product) => [product.sku, product]));
|
|
20
16
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// is flagged out of stock, and both read the same here: grid and carousel lose
|
|
24
|
-
// the products that did, a bundle loses itself if any one member did.
|
|
25
|
-
function hasContent(block, products, bundles) {
|
|
17
|
+
function hasContent(block, context) {
|
|
18
|
+
const isSellable = (sku) => sellableProduct(context.products, sku) !== undefined;
|
|
26
19
|
switch (block.kind) {
|
|
27
20
|
case 'grid':
|
|
28
21
|
case 'carousel':
|
|
29
|
-
return block.items.some((reference) =>
|
|
22
|
+
return block.items.some((reference) => isSellable(reference.sku));
|
|
30
23
|
case 'hero':
|
|
31
24
|
return (block.headline.length > 0 ||
|
|
32
25
|
(block.body !== null && block.body.length > 0) ||
|
|
33
|
-
(block.sku !== null &&
|
|
26
|
+
(block.sku !== null && isSellable(block.sku)));
|
|
34
27
|
case 'banner':
|
|
35
28
|
case 'copy':
|
|
36
29
|
return true;
|
|
37
30
|
case 'bundle': {
|
|
38
31
|
if (block.bundleId === null)
|
|
39
32
|
return false;
|
|
40
|
-
const bundle = bundles.get(block.bundleId);
|
|
41
|
-
return
|
|
42
|
-
bundle.skus.every((sku) => sellableProduct(products, sku) !== undefined));
|
|
33
|
+
const bundle = context.bundles.get(block.bundleId);
|
|
34
|
+
return bundle !== undefined && bundle.skus.every((sku) => isSellable(sku));
|
|
43
35
|
}
|
|
44
36
|
default:
|
|
45
37
|
block;
|
|
@@ -61,38 +53,21 @@ function renderBlock(block, context, registry, index) {
|
|
|
61
53
|
case 'bundle':
|
|
62
54
|
return _jsx(registry.bundle, { block: block, context: context }, index);
|
|
63
55
|
default:
|
|
64
|
-
// A block kind this renderer predates loses that block, not the page.
|
|
65
56
|
block;
|
|
66
57
|
return null;
|
|
67
58
|
}
|
|
68
59
|
}
|
|
69
|
-
/**
|
|
70
|
-
* Renders a component specification.
|
|
71
|
-
*
|
|
72
|
-
* Plain React with no hooks, no state and no effects, so it renders with
|
|
73
|
-
* `renderToString`, in a client-rendered app, or as a Server Component where the
|
|
74
|
-
* framework supports them. Rendered on the server, the whole component arrives
|
|
75
|
-
* in the initial HTML response, which is what removes the pop-in a
|
|
76
|
-
* client-fetched recommendation rail has — and what makes the content visible
|
|
77
|
-
* to a crawler that does not run JavaScript.
|
|
78
|
-
*
|
|
79
|
-
* Renders nothing at all when no block produced markup.
|
|
80
|
-
*/
|
|
81
60
|
export function RudraComponent({ spec, products, bundles, registry = defaultRegistry, hrefForSku = defaultHrefForSku, formatPrice, formatBundlePrice, locale, hasDiagnostics = false, className, }) {
|
|
82
|
-
const productMap = toProductMap(products);
|
|
83
|
-
const bundlesById = new Map((bundles ?? []).map((bundle) => [bundle.id, bundle]));
|
|
84
61
|
const context = {
|
|
85
|
-
products:
|
|
86
|
-
bundles:
|
|
62
|
+
products: toProductMap(products),
|
|
63
|
+
bundles: new Map((bundles ?? []).map((bundle) => [bundle.id, bundle])),
|
|
87
64
|
hrefForSku,
|
|
88
65
|
formatPrice: formatPrice ?? ((product) => defaultFormatPrice(product, locale)),
|
|
89
66
|
formatBundlePrice: formatBundlePrice ?? ((bundle) => defaultFormatBundlePrice(bundle, locale)),
|
|
90
67
|
};
|
|
91
|
-
const visible = spec.blocks.filter((block) => hasContent(block, context
|
|
68
|
+
const visible = spec.blocks.filter((block) => hasContent(block, context));
|
|
92
69
|
if (visible.length === 0)
|
|
93
70
|
return null;
|
|
94
|
-
// React drops a data-* attribute whose value is undefined, so degradedReason
|
|
95
|
-
// needs no branch of its own.
|
|
96
71
|
const diagnosticAttributes = hasDiagnostics
|
|
97
72
|
? {
|
|
98
73
|
'data-rudra-provider': spec.provider ?? 'none',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rudra-component.js","sourceRoot":"","sources":["../src/rudra-component.tsx"],"names":[],"mappings":";AACA,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,GAEhB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,eAAe,EAAsB,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"rudra-component.js","sourceRoot":"","sources":["../src/rudra-component.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rudra-js/react",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.7.6",
|
|
4
|
+
"description": "React renderer for rudra-js: renders an LLM-designed product-recommendation block from your own catalog, on the server or in the browser",
|
|
5
5
|
"keywords": [
|
|
6
|
+
"rudra-js",
|
|
7
|
+
"product-recommendations",
|
|
6
8
|
"react",
|
|
7
9
|
"react-server-components",
|
|
8
10
|
"rsc",
|
|
9
11
|
"llm",
|
|
10
12
|
"recommendations",
|
|
11
13
|
"ecommerce",
|
|
12
|
-
"ssr"
|
|
14
|
+
"ssr",
|
|
15
|
+
"typescript"
|
|
13
16
|
],
|
|
14
17
|
"license": "MIT",
|
|
15
18
|
"author": "Clive Dsouza",
|
|
@@ -51,11 +54,11 @@
|
|
|
51
54
|
"access": "public"
|
|
52
55
|
},
|
|
53
56
|
"peerDependencies": {
|
|
54
|
-
"@rudra-js/core": "^0.7.
|
|
57
|
+
"@rudra-js/core": "^0.7.6",
|
|
55
58
|
"react": "^18.2.0 || ^19.0.0"
|
|
56
59
|
},
|
|
57
60
|
"devDependencies": {
|
|
58
|
-
"@rudra-js/core": "0.7.
|
|
61
|
+
"@rudra-js/core": "0.7.6",
|
|
59
62
|
"@types/react": "^19.3.0",
|
|
60
63
|
"@types/react-dom": "^19.3.0",
|
|
61
64
|
"react": "^19.3.0",
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import type { BannerBlock, CarouselBlock, CopyBlock, GridBlock, HeroBlock } from '@rudra-js/core';
|
|
1
|
+
import type {
|
|
2
|
+
BannerBlock,
|
|
3
|
+
BundleBlock,
|
|
4
|
+
CarouselBlock,
|
|
5
|
+
CopyBlock,
|
|
6
|
+
GridBlock,
|
|
7
|
+
HeroBlock,
|
|
8
|
+
Product,
|
|
9
|
+
} from '@rudra-js/core';
|
|
12
10
|
import { sellableProduct, type BlockRenderContext } from '../render-context.js';
|
|
13
11
|
import { ProductCard } from './product-card.js';
|
|
14
12
|
|
|
@@ -68,8 +66,6 @@ export function CarouselRenderer({
|
|
|
68
66
|
return (
|
|
69
67
|
<section className="rudra-carousel">
|
|
70
68
|
{block.title ? <h3 className="rudra-carousel__title">{block.title}</h3> : null}
|
|
71
|
-
{/* Scrolls with CSS overflow rather than JavaScript, so the whole
|
|
72
|
-
component still needs no client bundle. */}
|
|
73
69
|
<div className="rudra-carousel__track">
|
|
74
70
|
{block.items.map((reference) => (
|
|
75
71
|
<ProductCard key={reference.sku} reference={reference} context={context} />
|
|
@@ -96,3 +92,42 @@ export function CopyRenderer({ block }: { block: CopyBlock }) {
|
|
|
96
92
|
</section>
|
|
97
93
|
);
|
|
98
94
|
}
|
|
95
|
+
|
|
96
|
+
export function BundleRenderer({
|
|
97
|
+
block,
|
|
98
|
+
context,
|
|
99
|
+
}: {
|
|
100
|
+
block: BundleBlock;
|
|
101
|
+
context: BlockRenderContext;
|
|
102
|
+
}) {
|
|
103
|
+
const bundle = block.bundleId === null ? undefined : context.bundles.get(block.bundleId);
|
|
104
|
+
if (!bundle) return null;
|
|
105
|
+
|
|
106
|
+
const products: Product[] = [];
|
|
107
|
+
for (const sku of bundle.skus) {
|
|
108
|
+
const product = sellableProduct(context.products, sku);
|
|
109
|
+
if (!product) return null;
|
|
110
|
+
products.push(product);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<section className="rudra-bundle">
|
|
115
|
+
{bundle.label ? <h3 className="rudra-bundle__label">{bundle.label}</h3> : null}
|
|
116
|
+
{block.title ? <p className="rudra-bundle__title">{block.title}</p> : null}
|
|
117
|
+
{block.body ? <p className="rudra-bundle__body">{block.body}</p> : null}
|
|
118
|
+
|
|
119
|
+
<ul className="rudra-bundle__items">
|
|
120
|
+
{products.map((product) => (
|
|
121
|
+
<li key={product.sku} className="rudra-bundle__item" data-rudra-sku={product.sku}>
|
|
122
|
+
<a className="rudra-bundle__link" href={context.hrefForSku(product.sku)}>
|
|
123
|
+
{product.title}
|
|
124
|
+
</a>
|
|
125
|
+
</li>
|
|
126
|
+
))}
|
|
127
|
+
</ul>
|
|
128
|
+
|
|
129
|
+
<p className="rudra-bundle__price">{context.formatBundlePrice(bundle)}</p>
|
|
130
|
+
{block.ctaLabel ? <span className="rudra-bundle__cta">{block.ctaLabel}</span> : null}
|
|
131
|
+
</section>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
import type { ProductReference } from '@rudra-js/core';
|
|
2
2
|
import { sellableProduct, type BlockRenderContext } from '../render-context.js';
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* One product.
|
|
6
|
-
*
|
|
7
|
-
* Every fact here — the title, the price, the image, the link — is read from
|
|
8
|
-
* the catalog, not from the specification. The specification contributes only
|
|
9
|
-
* `reason`, `badge` and `emphasis`, and React escapes all three on the way
|
|
10
|
-
* into the markup.
|
|
11
|
-
*/
|
|
12
4
|
export function ProductCard({
|
|
13
5
|
reference,
|
|
14
6
|
context,
|
|
@@ -17,7 +9,6 @@ export function ProductCard({
|
|
|
17
9
|
context: BlockRenderContext;
|
|
18
10
|
}) {
|
|
19
11
|
const product = sellableProduct(context.products, reference.sku);
|
|
20
|
-
// Gone from the catalog, or sold out since the spec was made: render nothing, not half a card.
|
|
21
12
|
if (!product) return null;
|
|
22
13
|
|
|
23
14
|
const isFeatured = reference.emphasis === 'featured';
|
|
@@ -33,7 +24,6 @@ export function ProductCard({
|
|
|
33
24
|
<img className="rudra-card__image" src={product.imageUrl} alt="" loading="lazy" />
|
|
34
25
|
) : null}
|
|
35
26
|
|
|
36
|
-
{/* The spaces are the link's accessible name: "Shoe $53.67 Popular", not "Shoe$53.67Popular". */}
|
|
37
27
|
<span className="rudra-card__body">
|
|
38
28
|
{reference.badge ? <span className="rudra-card__badge">{reference.badge}</span> : null}{' '}
|
|
39
29
|
<span className="rudra-card__title">{product.title}</span>{' '}
|
package/src/registry.ts
CHANGED
|
@@ -3,12 +3,12 @@ import type { Block, BlockKind } from '@rudra-js/core';
|
|
|
3
3
|
import type { BlockRenderContext } from './render-context.js';
|
|
4
4
|
import {
|
|
5
5
|
BannerRenderer,
|
|
6
|
+
BundleRenderer,
|
|
6
7
|
CarouselRenderer,
|
|
7
8
|
CopyRenderer,
|
|
8
9
|
GridRenderer,
|
|
9
10
|
HeroRenderer,
|
|
10
11
|
} from './blocks/block-renderers.js';
|
|
11
|
-
import { BundleRenderer } from './blocks/bundle-block.js';
|
|
12
12
|
|
|
13
13
|
export type BlockRenderer<Kind extends BlockKind> = (props: {
|
|
14
14
|
block: Extract<Block, { kind: Kind }>;
|
package/src/render-context.ts
CHANGED
|
@@ -2,9 +2,7 @@ import type { Bundle, Product } from '@rudra-js/core';
|
|
|
2
2
|
|
|
3
3
|
export interface BlockRenderContext {
|
|
4
4
|
readonly products: ReadonlyMap<string, Product>;
|
|
5
|
-
|
|
6
5
|
readonly bundles: ReadonlyMap<string, Bundle>;
|
|
7
|
-
|
|
8
6
|
readonly hrefForSku: (sku: string) => string;
|
|
9
7
|
readonly formatPrice: (product: Product) => string;
|
|
10
8
|
readonly formatBundlePrice: (bundle: Bundle) => string;
|
|
@@ -22,6 +20,16 @@ export function sellableProduct(
|
|
|
22
20
|
return product?.isInStock === false ? undefined : product;
|
|
23
21
|
}
|
|
24
22
|
|
|
23
|
+
function formatMoney(price: number, currency: string, locale?: string): string {
|
|
24
|
+
let formatter: Intl.NumberFormat;
|
|
25
|
+
try {
|
|
26
|
+
formatter = new Intl.NumberFormat(locale, { style: 'currency', currency });
|
|
27
|
+
} catch {
|
|
28
|
+
return `${currency} ${price}`;
|
|
29
|
+
}
|
|
30
|
+
return formatter.format(price);
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
export function defaultFormatPrice(product: Product, locale?: string): string {
|
|
26
34
|
if (!Number.isFinite(product.price)) {
|
|
27
35
|
throw new TypeError(
|
|
@@ -29,15 +37,7 @@ export function defaultFormatPrice(product: Product, locale?: string): string {
|
|
|
29
37
|
'catalog objects must satisfy productSchema from @rudra-js/core',
|
|
30
38
|
);
|
|
31
39
|
}
|
|
32
|
-
|
|
33
|
-
let formatter: Intl.NumberFormat;
|
|
34
|
-
try {
|
|
35
|
-
formatter = new Intl.NumberFormat(locale, { style: 'currency', currency: product.currency });
|
|
36
|
-
} catch {
|
|
37
|
-
return `${product.currency} ${product.price}`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return formatter.format(product.price);
|
|
40
|
+
return formatMoney(product.price, product.currency, locale);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
export function defaultFormatBundlePrice(bundle: Bundle, locale?: string): string {
|
|
@@ -47,13 +47,5 @@ export function defaultFormatBundlePrice(bundle: Bundle, locale?: string): strin
|
|
|
47
47
|
'bundle objects must satisfy bundleSchema from @rudra-js/core',
|
|
48
48
|
);
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
let formatter: Intl.NumberFormat;
|
|
52
|
-
try {
|
|
53
|
-
formatter = new Intl.NumberFormat(locale, { style: 'currency', currency: bundle.currency });
|
|
54
|
-
} catch {
|
|
55
|
-
return `${bundle.currency} ${bundle.price}`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return formatter.format(bundle.price);
|
|
50
|
+
return formatMoney(bundle.price, bundle.currency, locale);
|
|
59
51
|
}
|
package/src/rudra-component.tsx
CHANGED
|
@@ -10,57 +10,19 @@ import { defaultRegistry, type BlockRegistry } from './registry.js';
|
|
|
10
10
|
|
|
11
11
|
export interface RudraComponentProps {
|
|
12
12
|
spec: ComponentSpec;
|
|
13
|
-
/**
|
|
14
|
-
* The host catalog. Every product fact on the page comes from here rather
|
|
15
|
-
* than from the specification.
|
|
16
|
-
*
|
|
17
|
-
* A list of products, or anything keyed by SKU — a `Map`, or your own view
|
|
18
|
-
* over a catalog too large to hold in one. The renderers only ever call
|
|
19
|
-
* `get(sku)`, and `has` is how a keyed catalog is told apart from a list. The
|
|
20
|
-
* type asks for a `ReadonlyMap`, so a view of your own implements one or is
|
|
21
|
-
* cast with `as unknown as ReadonlyMap<string, Product>`.
|
|
22
|
-
*
|
|
23
|
-
* Validate them with `productSchema` from `@rudra-js/core` — the same schema
|
|
24
|
-
* your candidates already passed — not with `parseTrackingInput`, which
|
|
25
|
-
* parses a whole tracking payload and will reject a bare catalog.
|
|
26
|
-
*
|
|
27
|
-
* This is a second door into the framework. `imageUrl` lands in an
|
|
28
|
-
* `<img src>`, and `productSchema` is the only thing that rejects a
|
|
29
|
-
* protocol-relative `//evil.example/pixel.png` or a `data:` URL — React
|
|
30
|
-
* neutralises `javascript:` on its own, but not those. A price that is not a
|
|
31
|
-
* finite number throws rather than rendering as free.
|
|
32
|
-
*/
|
|
33
13
|
products: ProductCatalog;
|
|
34
|
-
/** Sets the shop sells together. Only needed if a spec can carry a bundle block. */
|
|
35
14
|
bundles?: readonly Bundle[];
|
|
36
15
|
registry?: BlockRegistry;
|
|
37
16
|
hrefForSku?: (sku: string) => string;
|
|
38
17
|
formatPrice?: (product: Product) => string;
|
|
39
|
-
/** Same as `formatPrice`, but for a bundle — the shop's price, not a sum of the parts. */
|
|
40
18
|
formatBundlePrice?: (bundle: Bundle) => string;
|
|
41
|
-
/**
|
|
42
|
-
* The shopper's locale, used to punctuate prices. Defaults to the server's,
|
|
43
|
-
* which is almost never the shopper's — pass it if the shop serves more than
|
|
44
|
-
* one. Ignored when `formatPrice` and `formatBundlePrice` are supplied.
|
|
45
|
-
*/
|
|
46
19
|
locale?: string;
|
|
47
|
-
/**
|
|
48
|
-
* Adds the model's own reasoning, the provider and the model name to the
|
|
49
|
-
* markup. Useful while developing and while benchmarking; it publishes which
|
|
50
|
-
* vendor a shop uses and whether the component is currently degraded, so it
|
|
51
|
-
* is off unless asked for.
|
|
52
|
-
*/
|
|
53
20
|
hasDiagnostics?: boolean;
|
|
54
21
|
className?: string;
|
|
55
22
|
}
|
|
56
23
|
|
|
57
|
-
/** A list of products, or a `ReadonlyMap` keyed by SKU, which a view of your own can implement. */
|
|
58
24
|
export type ProductCatalog = readonly Product[] | ReadonlyMap<string, Product>;
|
|
59
25
|
|
|
60
|
-
// Asks what the renderers call rather than which class the host built, since
|
|
61
|
-
// `instanceof Map` is per-realm. Checked before the list branch because a
|
|
62
|
-
// collection can answer both: an Immutable.js map has `map`, and converting
|
|
63
|
-
// through it yields `[sku, product]` pairs rather than products.
|
|
64
26
|
function isKeyedBySku(catalog: ProductCatalog): catalog is ReadonlyMap<string, Product> {
|
|
65
27
|
const candidate = catalog as { get?: unknown; has?: unknown };
|
|
66
28
|
return typeof candidate.get === 'function' && typeof candidate.has === 'function';
|
|
@@ -77,37 +39,26 @@ function toProductMap(catalog: ProductCatalog): ReadonlyMap<string, Product> {
|
|
|
77
39
|
return new Map(catalog.map((product) => [product.sku, product]));
|
|
78
40
|
}
|
|
79
41
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
// the products that did, a bundle loses itself if any one member did.
|
|
84
|
-
function hasContent(
|
|
85
|
-
block: Block,
|
|
86
|
-
products: ReadonlyMap<string, Product>,
|
|
87
|
-
bundles: ReadonlyMap<string, Bundle>,
|
|
88
|
-
): boolean {
|
|
42
|
+
function hasContent(block: Block, context: BlockRenderContext): boolean {
|
|
43
|
+
const isSellable = (sku: string) => sellableProduct(context.products, sku) !== undefined;
|
|
44
|
+
|
|
89
45
|
switch (block.kind) {
|
|
90
46
|
case 'grid':
|
|
91
47
|
case 'carousel':
|
|
92
|
-
return block.items.some(
|
|
93
|
-
(reference) => sellableProduct(products, reference.sku) !== undefined,
|
|
94
|
-
);
|
|
48
|
+
return block.items.some((reference) => isSellable(reference.sku));
|
|
95
49
|
case 'hero':
|
|
96
50
|
return (
|
|
97
51
|
block.headline.length > 0 ||
|
|
98
52
|
(block.body !== null && block.body.length > 0) ||
|
|
99
|
-
(block.sku !== null &&
|
|
53
|
+
(block.sku !== null && isSellable(block.sku))
|
|
100
54
|
);
|
|
101
55
|
case 'banner':
|
|
102
56
|
case 'copy':
|
|
103
57
|
return true;
|
|
104
58
|
case 'bundle': {
|
|
105
59
|
if (block.bundleId === null) return false;
|
|
106
|
-
const bundle = bundles.get(block.bundleId);
|
|
107
|
-
return (
|
|
108
|
-
bundle !== undefined &&
|
|
109
|
-
bundle.skus.every((sku) => sellableProduct(products, sku) !== undefined)
|
|
110
|
-
);
|
|
60
|
+
const bundle = context.bundles.get(block.bundleId);
|
|
61
|
+
return bundle !== undefined && bundle.skus.every((sku) => isSellable(sku));
|
|
111
62
|
}
|
|
112
63
|
default:
|
|
113
64
|
block satisfies never;
|
|
@@ -135,24 +86,11 @@ function renderBlock(
|
|
|
135
86
|
case 'bundle':
|
|
136
87
|
return <registry.bundle key={index} block={block} context={context} />;
|
|
137
88
|
default:
|
|
138
|
-
// A block kind this renderer predates loses that block, not the page.
|
|
139
89
|
block satisfies never;
|
|
140
90
|
return null;
|
|
141
91
|
}
|
|
142
92
|
}
|
|
143
93
|
|
|
144
|
-
/**
|
|
145
|
-
* Renders a component specification.
|
|
146
|
-
*
|
|
147
|
-
* Plain React with no hooks, no state and no effects, so it renders with
|
|
148
|
-
* `renderToString`, in a client-rendered app, or as a Server Component where the
|
|
149
|
-
* framework supports them. Rendered on the server, the whole component arrives
|
|
150
|
-
* in the initial HTML response, which is what removes the pop-in a
|
|
151
|
-
* client-fetched recommendation rail has — and what makes the content visible
|
|
152
|
-
* to a crawler that does not run JavaScript.
|
|
153
|
-
*
|
|
154
|
-
* Renders nothing at all when no block produced markup.
|
|
155
|
-
*/
|
|
156
94
|
export function RudraComponent({
|
|
157
95
|
spec,
|
|
158
96
|
products,
|
|
@@ -165,24 +103,17 @@ export function RudraComponent({
|
|
|
165
103
|
hasDiagnostics = false,
|
|
166
104
|
className,
|
|
167
105
|
}: RudraComponentProps) {
|
|
168
|
-
const productMap = toProductMap(products);
|
|
169
|
-
const bundlesById = new Map((bundles ?? []).map((bundle) => [bundle.id, bundle]));
|
|
170
|
-
|
|
171
106
|
const context: BlockRenderContext = {
|
|
172
|
-
products:
|
|
173
|
-
bundles:
|
|
107
|
+
products: toProductMap(products),
|
|
108
|
+
bundles: new Map((bundles ?? []).map((bundle) => [bundle.id, bundle])),
|
|
174
109
|
hrefForSku,
|
|
175
110
|
formatPrice: formatPrice ?? ((product) => defaultFormatPrice(product, locale)),
|
|
176
111
|
formatBundlePrice: formatBundlePrice ?? ((bundle) => defaultFormatBundlePrice(bundle, locale)),
|
|
177
112
|
};
|
|
178
113
|
|
|
179
|
-
const visible = spec.blocks.filter((block) =>
|
|
180
|
-
hasContent(block, context.products, context.bundles),
|
|
181
|
-
);
|
|
114
|
+
const visible = spec.blocks.filter((block) => hasContent(block, context));
|
|
182
115
|
if (visible.length === 0) return null;
|
|
183
116
|
|
|
184
|
-
// React drops a data-* attribute whose value is undefined, so degradedReason
|
|
185
|
-
// needs no branch of its own.
|
|
186
117
|
const diagnosticAttributes = hasDiagnostics
|
|
187
118
|
? {
|
|
188
119
|
'data-rudra-provider': spec.provider ?? 'none',
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { BundleBlock } from '@rudra-js/core';
|
|
2
|
-
import { type BlockRenderContext } from '../render-context.js';
|
|
3
|
-
export declare function BundleRenderer({ block, context, }: {
|
|
4
|
-
block: BundleBlock;
|
|
5
|
-
context: BlockRenderContext;
|
|
6
|
-
}): import("react").JSX.Element | null;
|
|
7
|
-
//# sourceMappingURL=bundle-block.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"bundle-block.d.ts","sourceRoot":"","sources":["../../src/blocks/bundle-block.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAW,MAAM,gBAAgB,CAAC;AAC3D,OAAO,EAAmB,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAEhF,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,OAAO,GACR,EAAE;IACD,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,kBAAkB,CAAC;CAC7B,sCAiCA"}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { sellableProduct } from '../render-context.js';
|
|
3
|
-
export function BundleRenderer({ block, context, }) {
|
|
4
|
-
const bundle = block.bundleId === null ? undefined : context.bundles.get(block.bundleId);
|
|
5
|
-
if (!bundle)
|
|
6
|
-
return null;
|
|
7
|
-
const products = [];
|
|
8
|
-
for (const sku of bundle.skus) {
|
|
9
|
-
const product = sellableProduct(context.products, sku);
|
|
10
|
-
// A set missing one of its parts is not that set.
|
|
11
|
-
if (!product)
|
|
12
|
-
return null;
|
|
13
|
-
products.push(product);
|
|
14
|
-
}
|
|
15
|
-
return (_jsxs("section", { className: "rudra-bundle", children: [bundle.label ? _jsx("h3", { className: "rudra-bundle__label", children: bundle.label }) : null, block.title ? _jsx("p", { className: "rudra-bundle__title", children: block.title }) : null, block.body ? _jsx("p", { className: "rudra-bundle__body", children: block.body }) : null, _jsx("ul", { className: "rudra-bundle__items", children: products.map((product) => (_jsx("li", { className: "rudra-bundle__item", "data-rudra-sku": product.sku, children: _jsx("a", { className: "rudra-bundle__link", href: context.hrefForSku(product.sku), children: product.title }) }, product.sku))) }), _jsx("p", { className: "rudra-bundle__price", children: context.formatBundlePrice(bundle) }), block.ctaLabel ? _jsx("span", { className: "rudra-bundle__cta", children: block.ctaLabel }) : null] }));
|
|
16
|
-
}
|
|
17
|
-
//# sourceMappingURL=bundle-block.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"bundle-block.js","sourceRoot":"","sources":["../../src/blocks/bundle-block.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,eAAe,EAA2B,MAAM,sBAAsB,CAAC;AAEhF,MAAM,UAAU,cAAc,CAAC,EAC7B,KAAK,EACL,OAAO,GAIR;IACC,MAAM,MAAM,GAAG,KAAK,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACzF,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAEzB,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;QAC9B,MAAM,OAAO,GAAG,eAAe,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QACvD,kDAAkD;QAClD,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAC;QAC1B,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACzB,CAAC;IAED,OAAO,CACL,mBAAS,SAAS,EAAC,cAAc,aAE9B,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,aAAI,SAAS,EAAC,qBAAqB,YAAE,MAAM,CAAC,KAAK,GAAM,CAAC,CAAC,CAAC,IAAI,EAC7E,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,YAAG,SAAS,EAAC,qBAAqB,YAAE,KAAK,CAAC,KAAK,GAAK,CAAC,CAAC,CAAC,IAAI,EACzE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,YAAG,SAAS,EAAC,oBAAoB,YAAE,KAAK,CAAC,IAAI,GAAK,CAAC,CAAC,CAAC,IAAI,EAEvE,aAAI,SAAS,EAAC,qBAAqB,YAChC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CACzB,aAAsB,SAAS,EAAC,oBAAoB,oBAAiB,OAAO,CAAC,GAAG,YAC9E,YAAG,SAAS,EAAC,oBAAoB,EAAC,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,YACpE,OAAO,CAAC,KAAK,GACZ,IAHG,OAAO,CAAC,GAAG,CAIf,CACN,CAAC,GACC,EAEL,YAAG,SAAS,EAAC,qBAAqB,YAAE,OAAO,CAAC,iBAAiB,CAAC,MAAM,CAAC,GAAK,EACzE,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,eAAM,SAAS,EAAC,mBAAmB,YAAE,KAAK,CAAC,QAAQ,GAAQ,CAAC,CAAC,CAAC,IAAI,IAC5E,CACX,CAAC;AACJ,CAAC"}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { BundleBlock, Product } from '@rudra-js/core';
|
|
2
|
-
import { sellableProduct, type BlockRenderContext } from '../render-context.js';
|
|
3
|
-
|
|
4
|
-
export function BundleRenderer({
|
|
5
|
-
block,
|
|
6
|
-
context,
|
|
7
|
-
}: {
|
|
8
|
-
block: BundleBlock;
|
|
9
|
-
context: BlockRenderContext;
|
|
10
|
-
}) {
|
|
11
|
-
const bundle = block.bundleId === null ? undefined : context.bundles.get(block.bundleId);
|
|
12
|
-
if (!bundle) return null;
|
|
13
|
-
|
|
14
|
-
const products: Product[] = [];
|
|
15
|
-
for (const sku of bundle.skus) {
|
|
16
|
-
const product = sellableProduct(context.products, sku);
|
|
17
|
-
// A set missing one of its parts is not that set.
|
|
18
|
-
if (!product) return null;
|
|
19
|
-
products.push(product);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return (
|
|
23
|
-
<section className="rudra-bundle">
|
|
24
|
-
{/* The shop's label is the one name here anyone can check, so it leads. */}
|
|
25
|
-
{bundle.label ? <h3 className="rudra-bundle__label">{bundle.label}</h3> : null}
|
|
26
|
-
{block.title ? <p className="rudra-bundle__title">{block.title}</p> : null}
|
|
27
|
-
{block.body ? <p className="rudra-bundle__body">{block.body}</p> : null}
|
|
28
|
-
|
|
29
|
-
<ul className="rudra-bundle__items">
|
|
30
|
-
{products.map((product) => (
|
|
31
|
-
<li key={product.sku} className="rudra-bundle__item" data-rudra-sku={product.sku}>
|
|
32
|
-
<a className="rudra-bundle__link" href={context.hrefForSku(product.sku)}>
|
|
33
|
-
{product.title}
|
|
34
|
-
</a>
|
|
35
|
-
</li>
|
|
36
|
-
))}
|
|
37
|
-
</ul>
|
|
38
|
-
|
|
39
|
-
<p className="rudra-bundle__price">{context.formatBundlePrice(bundle)}</p>
|
|
40
|
-
{block.ctaLabel ? <span className="rudra-bundle__cta">{block.ctaLabel}</span> : null}
|
|
41
|
-
</section>
|
|
42
|
-
);
|
|
43
|
-
}
|