@rudra-js/react 0.1.0
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/LICENSE +21 -0
- package/README.md +183 -0
- package/dist/blocks/block-renderers.d.ts +30 -0
- package/dist/blocks/block-renderers.d.ts.map +1 -0
- package/dist/blocks/block-renderers.js +19 -0
- package/dist/blocks/block-renderers.js.map +1 -0
- package/dist/blocks/bundle-block.d.ts +7 -0
- package/dist/blocks/bundle-block.d.ts.map +1 -0
- package/dist/blocks/bundle-block.js +16 -0
- package/dist/blocks/bundle-block.js.map +1 -0
- package/dist/blocks/product-card.d.ts +15 -0
- package/dist/blocks/product-card.d.ts.map +1 -0
- package/dist/blocks/product-card.js +19 -0
- package/dist/blocks/product-card.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +33 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +39 -0
- package/dist/registry.js.map +1 -0
- package/dist/render-context.d.ts +46 -0
- package/dist/render-context.d.ts.map +1 -0
- package/dist/render-context.js +68 -0
- package/dist/render-context.js.map +1 -0
- package/dist/rudra-component.d.ts +60 -0
- package/dist/rudra-component.d.ts.map +1 -0
- package/dist/rudra-component.js +132 -0
- package/dist/rudra-component.js.map +1 -0
- package/package.json +64 -0
- package/src/blocks/block-renderers.tsx +102 -0
- package/src/blocks/bundle-block.tsx +43 -0
- package/src/blocks/product-card.tsx +45 -0
- package/src/index.ts +31 -0
- package/src/registry.ts +59 -0
- package/src/render-context.ts +99 -0
- package/src/rudra-component.tsx +234 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Clive Dsouza
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# @rudra-js/react
|
|
2
|
+
|
|
3
|
+
Renders a component specification from
|
|
4
|
+
[`@rudra-js/core`](https://github.com/clivedsouza1010/rudra-js/tree/main/packages/core) as React Server
|
|
5
|
+
Components.
|
|
6
|
+
|
|
7
|
+
No client JavaScript. The recommendation area arrives in the initial HTML
|
|
8
|
+
response and needs no hydration, which is what removes the pop-in of a
|
|
9
|
+
client-fetched recommendation rail and what makes the content visible to a
|
|
10
|
+
crawler that does not run JavaScript.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install @rudra-js/react @rudra-js/core react zod@^4
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Both `@rudra-js/core` and `react` are peer dependencies: the specification you pass
|
|
19
|
+
in comes from your copy of core, and the elements this renders have to come from
|
|
20
|
+
the same React your app renders. Two copies of either would mean a spec that
|
|
21
|
+
fails its own type check, or a component tree React refuses to render.
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { RudraComponent } from '@rudra-js/react';
|
|
25
|
+
|
|
26
|
+
<RudraComponent spec={spec} products={catalog} locale="en-GB" />;
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`spec` is what `createComponentGenerator().generate()` returned. `products` is
|
|
30
|
+
your catalog.
|
|
31
|
+
|
|
32
|
+
## What comes from where
|
|
33
|
+
|
|
34
|
+
This split is the reason a generated component is safe to put in a page.
|
|
35
|
+
|
|
36
|
+
| Decided by the model | Decided by your catalog |
|
|
37
|
+
| ------------------------------------------ | ----------------------- |
|
|
38
|
+
| Which layout, in what order | Every product title |
|
|
39
|
+
| Which products, and how they are described | Every price |
|
|
40
|
+
| Tone, headline, badge text | Every image and link |
|
|
41
|
+
|
|
42
|
+
The specification has no field carrying a title, a price, an image or a URL.
|
|
43
|
+
Product facts are resolved at render time from `products`, keyed by a SKU
|
|
44
|
+
reconciliation has already checked. Everything the model writes is rendered as
|
|
45
|
+
text and escaped by React.
|
|
46
|
+
|
|
47
|
+
**Validate `products` with `productSchema` from `@rudra-js/core`** — the same
|
|
48
|
+
schema your candidates already passed. It is a second door into the framework:
|
|
49
|
+
`imageUrl` lands in an `<img src>`, and `productSchema` is what rejects a
|
|
50
|
+
protocol-relative `//evil.example/pixel.png` or a `data:` URL. React neutralises
|
|
51
|
+
a `javascript:` URL by itself, but not those. A price that is not a finite
|
|
52
|
+
number throws rather than rendering the product as free.
|
|
53
|
+
|
|
54
|
+
## Styling
|
|
55
|
+
|
|
56
|
+
The package ships no CSS, on purpose — a stylesheet would fight whatever your
|
|
57
|
+
site already has. Unstyled, the block renders as a run-on line: every card
|
|
58
|
+
element is inline, so titles and prices sit together with no separation. That is
|
|
59
|
+
the starting point, not a fault.
|
|
60
|
+
|
|
61
|
+
`examples/shop/public/demo-styles.css` is a working stylesheet written against
|
|
62
|
+
nothing but the table below — copy it as a starting point. The example shop
|
|
63
|
+
serves it behind `?styles=on` so you can see both states.
|
|
64
|
+
|
|
65
|
+
Every element it emits carries a class, and this is all of them:
|
|
66
|
+
|
|
67
|
+
| Where | Classes |
|
|
68
|
+
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
69
|
+
| The wrapper | `.rudra`, `.rudra__header`, `.rudra__headline`, `.rudra__subheadline`, `.rudra__rationale` |
|
|
70
|
+
| A hero | `.rudra-hero`, `.rudra-hero__headline`, `.rudra-hero__body`, `.rudra-hero__link`, `.rudra-hero__price`, `.rudra-hero__cta` |
|
|
71
|
+
| A grid | `.rudra-grid`, `.rudra-grid__title`, `.rudra-grid__items` |
|
|
72
|
+
| A carousel | `.rudra-carousel`, `.rudra-carousel__title`, `.rudra-carousel__track` |
|
|
73
|
+
| A banner | `.rudra-banner`, `.rudra-banner__text`, `.rudra-banner__cta` |
|
|
74
|
+
| A copy block | `.rudra-copy`, `.rudra-copy__title`, `.rudra-copy__body` |
|
|
75
|
+
| A bundle | `.rudra-bundle`, `.rudra-bundle__label`, `.rudra-bundle__title`, `.rudra-bundle__body`, `.rudra-bundle__items`, `.rudra-bundle__item`, `.rudra-bundle__link`, `.rudra-bundle__price`, `.rudra-bundle__cta` |
|
|
76
|
+
| A product card | `.rudra-card`, `.rudra-card--featured`, `.rudra-card__image`, `.rudra-card__body`, `.rudra-card__title`, `.rudra-card__price`, `.rudra-card__reason`, `.rudra-card__badge` |
|
|
77
|
+
|
|
78
|
+
`.rudra__rationale` only appears under `hasDiagnostics`. `.rudra-bundle__label`
|
|
79
|
+
is your own name for the set, and appears only for a bundle you gave a `label`.
|
|
80
|
+
`className` is added alongside `rudra` rather than replacing it, so the child
|
|
81
|
+
classes keep working.
|
|
82
|
+
|
|
83
|
+
`.rudra-carousel__track` is expected to scroll horizontally — give it `overflow-x: auto`, since nothing here uses JavaScript to scroll it. `.rudra-card--featured` is applied alongside `.rudra-card`, so write it as `.rudra-card--featured { ... }` after the base
|
|
84
|
+
rule rather than instead of it.
|
|
85
|
+
|
|
86
|
+
### Attributes
|
|
87
|
+
|
|
88
|
+
The same markup carries what the model decided, for styling and for analytics.
|
|
89
|
+
|
|
90
|
+
| Attribute | On | Value |
|
|
91
|
+
| ------------------------ | ------------------------------ | ---------------------------------------------------------- |
|
|
92
|
+
| `data-rudra-slot` | the wrapper | The slot the spec was generated for |
|
|
93
|
+
| `data-rudra-source` | the wrapper | `llm`, `cache` or `fallback` |
|
|
94
|
+
| `data-rudra-tone` | the wrapper | The tone the model chose for the component |
|
|
95
|
+
| `data-rudra-banner-tone` | a banner | A banner's own tone, a different vocabulary from the above |
|
|
96
|
+
| `data-rudra-columns` | a grid | The column count the model chose |
|
|
97
|
+
| `data-rudra-sku` | a card, hero link, bundle item | The product, for click attribution |
|
|
98
|
+
| `data-rudra-basis` | a card | Why the product was picked — `most_viewed`, `popular`, … |
|
|
99
|
+
|
|
100
|
+
`data-rudra-source` is public on purpose: hit rate and fallback share can be
|
|
101
|
+
read straight off a rendered page. Everything more specific appears only under
|
|
102
|
+
`hasDiagnostics`, since it tells a visitor what you run and when it is failing:
|
|
103
|
+
|
|
104
|
+
| Attribute | On | Value |
|
|
105
|
+
| ----------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
106
|
+
| `data-rudra-provider` | the wrapper | The model vendor, or `none` |
|
|
107
|
+
| `data-rudra-model` | the wrapper | The model name, or `none` |
|
|
108
|
+
| `data-rudra-latency-ms` | the wrapper | How long generation took |
|
|
109
|
+
| `data-rudra-degraded` | the wrapper | Why it fell back: `no-provider`, `provider-error`, `timeout`, `invalid-generation`, `unusable-on-serve` or `requested` |
|
|
110
|
+
|
|
111
|
+
## Replacing a renderer
|
|
112
|
+
|
|
113
|
+
Swap any block for your own design-system component. The model is not involved
|
|
114
|
+
and the specification does not change, so this gives it no new ability — it can
|
|
115
|
+
still only choose from the same fixed vocabulary.
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
import { RudraComponent, extendRegistry } from '@rudra-js/react';
|
|
119
|
+
|
|
120
|
+
const registry = extendRegistry({
|
|
121
|
+
grid: ({ block, context }) => <MyProductGrid items={block.items} context={context} />,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
<RudraComponent spec={spec} products={catalog} registry={registry} />;
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Props
|
|
128
|
+
|
|
129
|
+
| Prop | Notes |
|
|
130
|
+
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
131
|
+
| `spec` | Required. From `@rudra-js/core`. |
|
|
132
|
+
| `products` | Required. A list of products, or anything keyed by SKU. See below, and the warning above. |
|
|
133
|
+
| `bundles` | The sets your shop sells together. Only needed if a spec can carry a bundle block. |
|
|
134
|
+
| `registry` | Replace some or all block renderers. |
|
|
135
|
+
| `hrefForSku` | Defaults to `/product/{sku}`, URL-encoded. |
|
|
136
|
+
| `formatPrice` | Defaults to `Intl.NumberFormat`, which knows each currency's own number of decimal places. It formats products, not bundles. |
|
|
137
|
+
| `formatBundlePrice` | The same for a bundle's own price, in the currency the shop put on the set. The set's price and its currency come from the same object, so members priced in another currency change nothing. |
|
|
138
|
+
| `locale` | Punctuates prices. Defaults to the **server's** locale, which is rarely the shopper's — pass it if you serve more than one. |
|
|
139
|
+
| `hasDiagnostics` | Adds the provider, the model name, the latency and the model's own reasoning to the markup. Off by default: it tells a visitor which model you use and when it is failing. |
|
|
140
|
+
| `className` | Added alongside `rudra`. |
|
|
141
|
+
|
|
142
|
+
### What `products` may be
|
|
143
|
+
|
|
144
|
+
A list of products, or anything keyed by SKU that answers `get(sku)` and
|
|
145
|
+
`has(sku)` — a `Map`, or your own index. Those two methods are the only ones the
|
|
146
|
+
renderers call, so a shop with a catalog too large to copy into a `Map` on every
|
|
147
|
+
request can pass a view over its own store instead.
|
|
148
|
+
|
|
149
|
+
The check is on those two methods rather than on `instanceof Map`, which is
|
|
150
|
+
per-realm: a `Map` arriving from a worker or a `node:vm` sandbox is a perfectly
|
|
151
|
+
good catalog and fails `instanceof`. Anything that is neither a list nor keyed —
|
|
152
|
+
a `Set` of products, a plain object, a `Map` that has been through JSON — is
|
|
153
|
+
refused on the spot with an error naming the prop, rather than quietly rendering
|
|
154
|
+
an empty recommendation area.
|
|
155
|
+
|
|
156
|
+
The component renders nothing at all when there is nothing to show — a spec with
|
|
157
|
+
no blocks, or one whose every product has left your catalog since it was
|
|
158
|
+
generated. An empty recommendation area, or a headline over an empty box, takes
|
|
159
|
+
up space and tells the shopper the page is broken.
|
|
160
|
+
|
|
161
|
+
### What `bundles` is
|
|
162
|
+
|
|
163
|
+
The sets your shop sells together, the same way `products` is your catalog. The
|
|
164
|
+
model only asks for a bundle block; it never invents one, and it never sees a
|
|
165
|
+
price.
|
|
166
|
+
|
|
167
|
+
You offer the sets, and the framework picks which one fills each block. It
|
|
168
|
+
picks when the page is served — after the spec was generated, not before —
|
|
169
|
+
inside `reconcileSpec`, from what this shopper has in their basket, has looked
|
|
170
|
+
at, or is browsing now. The spec then carries the id it picked, and this prop
|
|
171
|
+
supplies that set's members, its price, the currency that price is in and your
|
|
172
|
+
name for it so the component can draw it.
|
|
173
|
+
|
|
174
|
+
**Validate `bundles` with `bundleSchema` from `@rudra-js/core`, and pass the
|
|
175
|
+
same list you sent to `parseTrackingInput`.** Core checked that list — every
|
|
176
|
+
member in stock, none of them disliked, no repeats, and the whole set inside
|
|
177
|
+
the item budget — and then hands the renderer nothing but the id it chose. A
|
|
178
|
+
stale or different list here draws a set none of those checks ever saw, under
|
|
179
|
+
an id that was proved against another one.
|
|
180
|
+
|
|
181
|
+
## Licence
|
|
182
|
+
|
|
183
|
+
[MIT](./LICENSE)
|
|
@@ -0,0 +1,30 @@
|
|
|
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';
|
|
11
|
+
import type { BlockRenderContext } from '../render-context.js';
|
|
12
|
+
export declare function HeroRenderer({ block, context, }: {
|
|
13
|
+
block: HeroBlock;
|
|
14
|
+
context: BlockRenderContext;
|
|
15
|
+
}): import("react").JSX.Element;
|
|
16
|
+
export declare function GridRenderer({ block, context, }: {
|
|
17
|
+
block: GridBlock;
|
|
18
|
+
context: BlockRenderContext;
|
|
19
|
+
}): import("react").JSX.Element;
|
|
20
|
+
export declare function CarouselRenderer({ block, context, }: {
|
|
21
|
+
block: CarouselBlock;
|
|
22
|
+
context: BlockRenderContext;
|
|
23
|
+
}): import("react").JSX.Element;
|
|
24
|
+
export declare function BannerRenderer({ block }: {
|
|
25
|
+
block: BannerBlock;
|
|
26
|
+
}): import("react").JSX.Element;
|
|
27
|
+
export declare function CopyRenderer({ block }: {
|
|
28
|
+
block: CopyBlock;
|
|
29
|
+
}): import("react").JSX.Element;
|
|
30
|
+
//# sourceMappingURL=block-renderers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"block-renderers.d.ts","sourceRoot":"","sources":["../../src/blocks/block-renderers.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAClG,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAG/D,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,OAAO,GACR,EAAE;IACD,KAAK,EAAE,SAAS,CAAC;IACjB,OAAO,EAAE,kBAAkB,CAAC;CAC7B,+BAuBA;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,+BAaA;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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ProductCard } from './product-card.js';
|
|
3
|
+
export function HeroRenderer({ block, context, }) {
|
|
4
|
+
const product = block.sku ? context.products.get(block.sku) : undefined;
|
|
5
|
+
return (_jsxs("section", { className: "rudra-hero", children: [_jsx("h3", { className: "rudra-hero__headline", children: block.headline }), block.body ? _jsx("p", { className: "rudra-hero__body", children: block.body }) : null, product ? (_jsxs("a", { className: "rudra-hero__link", href: context.hrefForSku(product.sku), "data-rudra-sku": product.sku, children: [product.title, _jsx("span", { className: "rudra-hero__price", children: context.formatPrice(product) })] })) : null, product && block.ctaLabel ? _jsx("span", { className: "rudra-hero__cta", children: block.ctaLabel }) : null] }));
|
|
6
|
+
}
|
|
7
|
+
export function GridRenderer({ block, context, }) {
|
|
8
|
+
return (_jsxs("section", { className: "rudra-grid", "data-rudra-columns": block.columns, children: [block.title ? _jsx("h3", { className: "rudra-grid__title", children: block.title }) : null, _jsx("div", { className: "rudra-grid__items", children: block.items.map((reference) => (_jsx(ProductCard, { reference: reference, context: context }, reference.sku))) })] }));
|
|
9
|
+
}
|
|
10
|
+
export function CarouselRenderer({ block, context, }) {
|
|
11
|
+
return (_jsxs("section", { className: "rudra-carousel", children: [block.title ? _jsx("h3", { className: "rudra-carousel__title", children: block.title }) : null, _jsx("div", { className: "rudra-carousel__track", children: block.items.map((reference) => (_jsx(ProductCard, { reference: reference, context: context }, reference.sku))) })] }));
|
|
12
|
+
}
|
|
13
|
+
export function BannerRenderer({ block }) {
|
|
14
|
+
return (_jsxs("aside", { className: "rudra-banner", "data-rudra-banner-tone": block.tone, children: [_jsx("span", { className: "rudra-banner__text", children: block.text }), block.ctaLabel ? _jsx("span", { className: "rudra-banner__cta", children: block.ctaLabel }) : null] }));
|
|
15
|
+
}
|
|
16
|
+
export function CopyRenderer({ block }) {
|
|
17
|
+
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 })] }));
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=block-renderers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"block-renderers.js","sourceRoot":"","sources":["../../src/blocks/block-renderers.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE/D,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,OAAO,GACR,EAAE;IACD,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,kBAAkB,CAAC;CAC7B,sCAiCA"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function BundleRenderer({ block, context, }) {
|
|
3
|
+
const bundle = block.bundleId === null ? undefined : context.bundles.get(block.bundleId);
|
|
4
|
+
if (!bundle)
|
|
5
|
+
return null;
|
|
6
|
+
const products = [];
|
|
7
|
+
for (const sku of bundle.skus) {
|
|
8
|
+
const product = context.products.get(sku);
|
|
9
|
+
// A set missing one of its parts is not that set.
|
|
10
|
+
if (!product)
|
|
11
|
+
return null;
|
|
12
|
+
products.push(product);
|
|
13
|
+
}
|
|
14
|
+
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] }));
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=bundle-block.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundle-block.js","sourceRoot":"","sources":["../../src/blocks/bundle-block.tsx"],"names":[],"mappings":";AAGA,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,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC1C,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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ProductReference } from '@rudra-js/core';
|
|
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 only things the model
|
|
8
|
+
* contributes are `reason`, `badge` and `emphasis`, and React escapes all three
|
|
9
|
+
* on the way into the markup.
|
|
10
|
+
*/
|
|
11
|
+
export declare function ProductCard({ reference, context, }: {
|
|
12
|
+
reference: ProductReference;
|
|
13
|
+
context: BlockRenderContext;
|
|
14
|
+
}): import("react").JSX.Element | null;
|
|
15
|
+
//# sourceMappingURL=product-card.d.ts.map
|
|
@@ -0,0 +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,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE/D;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,EAC1B,SAAS,EACT,OAAO,GACR,EAAE;IACD,SAAS,EAAE,gBAAgB,CAAC;IAC5B,OAAO,EAAE,kBAAkB,CAAC;CAC7B,sCA2BA"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* One product.
|
|
4
|
+
*
|
|
5
|
+
* Every fact here — the title, the price, the image, the link — is read from
|
|
6
|
+
* the catalog, not from the specification. The only things the model
|
|
7
|
+
* contributes are `reason`, `badge` and `emphasis`, and React escapes all three
|
|
8
|
+
* on the way into the markup.
|
|
9
|
+
*/
|
|
10
|
+
export function ProductCard({ reference, context, }) {
|
|
11
|
+
const product = context.products.get(reference.sku);
|
|
12
|
+
// Reconciliation drops a SKU the catalog does not have, so this should be
|
|
13
|
+
// unreachable. Rendering nothing beats rendering a card with holes in it.
|
|
14
|
+
if (!product)
|
|
15
|
+
return null;
|
|
16
|
+
const isFeatured = reference.emphasis === 'featured';
|
|
17
|
+
return (_jsxs("a", { href: context.hrefForSku(product.sku), "data-rudra-sku": product.sku, "data-rudra-basis": reference.basis, className: isFeatured ? 'rudra-card rudra-card--featured' : 'rudra-card', children: [product.imageUrl ? (_jsx("img", { className: "rudra-card__image", src: product.imageUrl, alt: "", loading: "lazy" })) : null, _jsxs("span", { className: "rudra-card__body", children: [reference.badge ? _jsx("span", { className: "rudra-card__badge", children: reference.badge }) : null, _jsx("span", { className: "rudra-card__title", children: product.title }), _jsx("span", { className: "rudra-card__price", children: context.formatPrice(product) }), reference.reason ? _jsx("span", { className: "rudra-card__reason", children: reference.reason }) : null] })] }));
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=product-card.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"product-card.js","sourceRoot":"","sources":["../../src/blocks/product-card.tsx"],"names":[],"mappings":";AAGA;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,EAC1B,SAAS,EACT,OAAO,GAIR;IACC,MAAM,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IACpD,0EAA0E;IAC1E,0EAA0E;IAC1E,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,EACtF,eAAM,SAAS,EAAC,mBAAmB,YAAE,OAAO,CAAC,KAAK,GAAQ,EAC1D,eAAM,SAAS,EAAC,mBAAmB,YAAE,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,GAAQ,EACxE,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/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rudra-js/react — renders a component specification as React Server Components.
|
|
3
|
+
*
|
|
4
|
+
* Carries no client JavaScript. The recommendation area arrives in the initial
|
|
5
|
+
* HTML response and needs no hydration.
|
|
6
|
+
*/
|
|
7
|
+
export { RudraComponent, type ProductCatalog, type RudraComponentProps, } from './rudra-component.js';
|
|
8
|
+
export { defaultRegistry, extendRegistry, type BlockRegistry, type BlockRenderer, } from './registry.js';
|
|
9
|
+
export { defaultFormatBundlePrice, defaultFormatPrice, defaultHrefForSku, type BlockRenderContext, } from './render-context.js';
|
|
10
|
+
export { ProductCard } from './blocks/product-card.js';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,cAAc,EACd,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,eAAe,EACf,cAAc,EACd,KAAK,aAAa,EAClB,KAAK,aAAa,GACnB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,iBAAiB,EACjB,KAAK,kBAAkB,GACxB,MAAM,qBAAqB,CAAC;AAK7B,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rudra-js/react — renders a component specification as React Server Components.
|
|
3
|
+
*
|
|
4
|
+
* Carries no client JavaScript. The recommendation area arrives in the initial
|
|
5
|
+
* HTML response and needs no hydration.
|
|
6
|
+
*/
|
|
7
|
+
export { RudraComponent, } from './rudra-component.js';
|
|
8
|
+
export { defaultRegistry, extendRegistry, } from './registry.js';
|
|
9
|
+
export { defaultFormatBundlePrice, defaultFormatPrice, defaultHrefForSku, } from './render-context.js';
|
|
10
|
+
// The six default renderers reach hosts through `defaultRegistry`, which is
|
|
11
|
+
// also the thing they need in order to override one. `ProductCard` is exported
|
|
12
|
+
// on its own because a custom grid renderer still wants the standard card.
|
|
13
|
+
export { ProductCard } from './blocks/product-card.js';
|
|
14
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,cAAc,GAGf,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,eAAe,EACf,cAAc,GAGf,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,iBAAiB,GAElB,MAAM,qBAAqB,CAAC;AAE7B,4EAA4E;AAC5E,+EAA+E;AAC/E,2EAA2E;AAC3E,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each kind of block actually renders.
|
|
3
|
+
*
|
|
4
|
+
* This is the seam that makes a generated component safe *and* usable. The
|
|
5
|
+
* model picks from a fixed vocabulary of block kinds; the registry decides what
|
|
6
|
+
* those kinds look like. A shop can swap any entry for its own design-system
|
|
7
|
+
* component without the model knowing, without the specification changing, and
|
|
8
|
+
* without giving the model any new ability — it still cannot say anything
|
|
9
|
+
* outside the vocabulary.
|
|
10
|
+
*/
|
|
11
|
+
import type { ReactNode } from 'react';
|
|
12
|
+
import type { Block, BlockKind } from '@rudra-js/core';
|
|
13
|
+
import type { BlockRenderContext } from './render-context.js';
|
|
14
|
+
export type BlockRenderer<Kind extends BlockKind> = (props: {
|
|
15
|
+
block: Extract<Block, {
|
|
16
|
+
kind: Kind;
|
|
17
|
+
}>;
|
|
18
|
+
context: BlockRenderContext;
|
|
19
|
+
}) => ReactNode;
|
|
20
|
+
export type BlockRegistry = {
|
|
21
|
+
[Kind in BlockKind]: BlockRenderer<Kind>;
|
|
22
|
+
};
|
|
23
|
+
export declare const defaultRegistry: BlockRegistry;
|
|
24
|
+
/**
|
|
25
|
+
* Replaces some renderers and keeps the rest.
|
|
26
|
+
*
|
|
27
|
+
* Written out kind by kind rather than spread: a spread lets
|
|
28
|
+
* `{ hero: maybeRenderer }` put an explicit `undefined` over the default, and
|
|
29
|
+
* the page then renders every hero as nothing. Here an absent override means
|
|
30
|
+
* the default, however it was written.
|
|
31
|
+
*/
|
|
32
|
+
export declare function extendRegistry(overrides: Partial<BlockRegistry>): BlockRegistry;
|
|
33
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAU9D,MAAM,MAAM,aAAa,CAAC,IAAI,SAAS,SAAS,IAAI,CAAC,KAAK,EAAE;IAC1D,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,IAAI,CAAA;KAAE,CAAC,CAAC;IACtC,OAAO,EAAE,kBAAkB,CAAC;CAC7B,KAAK,SAAS,CAAC;AAEhB,MAAM,MAAM,aAAa,GAAG;KACzB,IAAI,IAAI,SAAS,GAAG,aAAa,CAAC,IAAI,CAAC;CACzC,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,aAO7B,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,aAAa,CAS/E"}
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each kind of block actually renders.
|
|
3
|
+
*
|
|
4
|
+
* This is the seam that makes a generated component safe *and* usable. The
|
|
5
|
+
* model picks from a fixed vocabulary of block kinds; the registry decides what
|
|
6
|
+
* those kinds look like. A shop can swap any entry for its own design-system
|
|
7
|
+
* component without the model knowing, without the specification changing, and
|
|
8
|
+
* without giving the model any new ability — it still cannot say anything
|
|
9
|
+
* outside the vocabulary.
|
|
10
|
+
*/
|
|
11
|
+
import { BannerRenderer, CarouselRenderer, CopyRenderer, GridRenderer, HeroRenderer, } from './blocks/block-renderers.js';
|
|
12
|
+
import { BundleRenderer } from './blocks/bundle-block.js';
|
|
13
|
+
export const defaultRegistry = {
|
|
14
|
+
hero: HeroRenderer,
|
|
15
|
+
grid: GridRenderer,
|
|
16
|
+
carousel: CarouselRenderer,
|
|
17
|
+
banner: BannerRenderer,
|
|
18
|
+
copy: CopyRenderer,
|
|
19
|
+
bundle: BundleRenderer,
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Replaces some renderers and keeps the rest.
|
|
23
|
+
*
|
|
24
|
+
* Written out kind by kind rather than spread: a spread lets
|
|
25
|
+
* `{ hero: maybeRenderer }` put an explicit `undefined` over the default, and
|
|
26
|
+
* the page then renders every hero as nothing. Here an absent override means
|
|
27
|
+
* the default, however it was written.
|
|
28
|
+
*/
|
|
29
|
+
export function extendRegistry(overrides) {
|
|
30
|
+
return {
|
|
31
|
+
hero: overrides.hero ?? defaultRegistry.hero,
|
|
32
|
+
grid: overrides.grid ?? defaultRegistry.grid,
|
|
33
|
+
carousel: overrides.carousel ?? defaultRegistry.carousel,
|
|
34
|
+
banner: overrides.banner ?? defaultRegistry.banner,
|
|
35
|
+
copy: overrides.copy ?? defaultRegistry.copy,
|
|
36
|
+
bundle: overrides.bundle ?? defaultRegistry.bundle,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,YAAY,GACb,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAW1D,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;;;;;;;GAOG;AACH,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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Bundle, Product } from '@rudra-js/core';
|
|
2
|
+
/**
|
|
3
|
+
* Everything a block renderer needs that does not come from the specification.
|
|
4
|
+
*
|
|
5
|
+
* Note what is absent from the spec and present here: a price, a title, an
|
|
6
|
+
* image, a link. Those are resolved from the host's own catalog, keyed by a SKU
|
|
7
|
+
* that `selectProducts` drew from that same catalog and `reconcileSpec` proved
|
|
8
|
+
* the model did not invent — both in stock at the time. A bundle member's SKU
|
|
9
|
+
* comes from the set the shop supplied, not from the model. That division
|
|
10
|
+
* is the whole reason a generated component is safe to put in a page — the
|
|
11
|
+
* model decides what to show and how to describe it, and the shop decides what
|
|
12
|
+
* is true about a product.
|
|
13
|
+
*/
|
|
14
|
+
export interface BlockRenderContext {
|
|
15
|
+
/** The host's catalog, keyed by SKU. Read-only: it is the caller's own map. */
|
|
16
|
+
readonly products: ReadonlyMap<string, Product>;
|
|
17
|
+
/** Sets the shop sells together, keyed by id. */
|
|
18
|
+
readonly bundles: ReadonlyMap<string, Bundle>;
|
|
19
|
+
/** Host-owned link construction. */
|
|
20
|
+
readonly hrefForSku: (sku: string) => string;
|
|
21
|
+
readonly formatPrice: (product: Product) => string;
|
|
22
|
+
readonly formatBundlePrice: (bundle: Bundle) => string;
|
|
23
|
+
}
|
|
24
|
+
export declare function defaultHrefForSku(sku: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Formats a price the way the currency itself is written.
|
|
27
|
+
*
|
|
28
|
+
* No digit count is specified on purpose. Two decimal places is a dollar-and-
|
|
29
|
+
* cent assumption, and forcing it is wrong in both directions: Kuwaiti dinar and
|
|
30
|
+
* Bahraini dinar have three, so a real digit of the price disappears, and yen has
|
|
31
|
+
* none, so a price gains a fraction that does not exist. Intl already knows the
|
|
32
|
+
* right number for each currency.
|
|
33
|
+
*
|
|
34
|
+
* `locale` decides how the number is punctuated and where the symbol sits.
|
|
35
|
+
* Left undefined it falls back to the server's locale, which is almost never
|
|
36
|
+
* the shopper's — a shop serving more than one should pass the shopper's.
|
|
37
|
+
*/
|
|
38
|
+
export declare function defaultFormatPrice(product: Product, locale?: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* Formats a bundle's price, in the currency the shop put on the bundle.
|
|
41
|
+
*
|
|
42
|
+
* The price and the currency come from the same object, so a set whose members
|
|
43
|
+
* are priced in another currency still shows the shop's own price correctly.
|
|
44
|
+
*/
|
|
45
|
+
export declare function defaultFormatBundlePrice(bundle: Bundle, locale?: string): string;
|
|
46
|
+
//# sourceMappingURL=render-context.d.ts.map
|
|
@@ -0,0 +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;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,kBAAkB;IACjC,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChD,iDAAiD;IACjD,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9C,oCAAoC;IACpC,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;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CA8B5E;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAkBhF"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export function defaultHrefForSku(sku) {
|
|
2
|
+
return `/product/${encodeURIComponent(sku)}`;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Formats a price the way the currency itself is written.
|
|
6
|
+
*
|
|
7
|
+
* No digit count is specified on purpose. Two decimal places is a dollar-and-
|
|
8
|
+
* cent assumption, and forcing it is wrong in both directions: Kuwaiti dinar and
|
|
9
|
+
* Bahraini dinar have three, so a real digit of the price disappears, and yen has
|
|
10
|
+
* none, so a price gains a fraction that does not exist. Intl already knows the
|
|
11
|
+
* right number for each currency.
|
|
12
|
+
*
|
|
13
|
+
* `locale` decides how the number is punctuated and where the symbol sits.
|
|
14
|
+
* Left undefined it falls back to the server's locale, which is almost never
|
|
15
|
+
* the shopper's — a shop serving more than one should pass the shopper's.
|
|
16
|
+
*/
|
|
17
|
+
export function defaultFormatPrice(product, locale) {
|
|
18
|
+
// A price that is not a finite number is a broken catalog, not a formatting
|
|
19
|
+
// problem, and Intl will happily render it: null becomes 0 and prints as a
|
|
20
|
+
// free product, undefined prints as NaN. Neither belongs on a shop page, and
|
|
21
|
+
// both mean the catalog skipped the validation this package documents as a
|
|
22
|
+
// precondition. Failing here is a bug report; rendering it is an incident.
|
|
23
|
+
if (!Number.isFinite(product.price)) {
|
|
24
|
+
throw new TypeError(`price for SKU ${product.sku} is ${String(product.price)}, not a finite number — ` +
|
|
25
|
+
'catalog objects must satisfy productSchema from @rudra-js/core');
|
|
26
|
+
}
|
|
27
|
+
let formatter;
|
|
28
|
+
try {
|
|
29
|
+
// Only the constructor is guarded. Wrapping format() as well would swallow
|
|
30
|
+
// a throwing getter on a host's own Product object and render the resulting
|
|
31
|
+
// mess as a price.
|
|
32
|
+
formatter = new Intl.NumberFormat(locale, { style: 'currency', currency: product.currency });
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Two things reach this. A malformed `locale`, which is a host prop nothing
|
|
36
|
+
// validates. And a malformed `currency` — which productSchema would have
|
|
37
|
+
// rejected, but this package takes a catalog directly, so a hand-built
|
|
38
|
+
// object can carry one. Intl accepts any three-letter code it does not
|
|
39
|
+
// know, so 'ZZZ' formats; 'us$' and '' do not. A price nobody can
|
|
40
|
+
// punctuate is still a price worth showing.
|
|
41
|
+
return `${product.currency} ${product.price}`;
|
|
42
|
+
}
|
|
43
|
+
return formatter.format(product.price);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Formats a bundle's price, in the currency the shop put on the bundle.
|
|
47
|
+
*
|
|
48
|
+
* The price and the currency come from the same object, so a set whose members
|
|
49
|
+
* are priced in another currency still shows the shop's own price correctly.
|
|
50
|
+
*/
|
|
51
|
+
export function defaultFormatBundlePrice(bundle, locale) {
|
|
52
|
+
if (!Number.isFinite(bundle.price)) {
|
|
53
|
+
throw new TypeError(`price for bundle ${bundle.id} is ${String(bundle.price)}, not a finite number — ` +
|
|
54
|
+
'bundle objects must satisfy bundleSchema from @rudra-js/core');
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
return new Intl.NumberFormat(locale, {
|
|
58
|
+
style: 'currency',
|
|
59
|
+
currency: bundle.currency,
|
|
60
|
+
}).format(bundle.price);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// A bad locale, or a currency code Intl rejects on a hand-built bundle,
|
|
64
|
+
// should not crash the page. A price nobody can punctuate is still a price.
|
|
65
|
+
return `${bundle.currency} ${bundle.price}`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=render-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render-context.js","sourceRoot":"","sources":["../src/render-context.ts"],"names":[],"mappings":"AAyBA,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,OAAO,YAAY,kBAAkB,CAAC,GAAG,CAAC,EAAE,CAAC;AAC/C,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAgB,EAAE,MAAe;IAClE,4EAA4E;IAC5E,2EAA2E;IAC3E,6EAA6E;IAC7E,2EAA2E;IAC3E,2EAA2E;IAC3E,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;IAED,IAAI,SAA4B,CAAC;IACjC,IAAI,CAAC;QACH,2EAA2E;QAC3E,4EAA4E;QAC5E,mBAAmB;QACnB,SAAS,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC/F,CAAC;IAAC,MAAM,CAAC;QACP,4EAA4E;QAC5E,yEAAyE;QACzE,uEAAuE;QACvE,uEAAuE;QACvE,kEAAkE;QAClE,4CAA4C;QAC5C,OAAO,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;IAChD,CAAC;IAED,OAAO,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AACzC,CAAC;AAED;;;;;GAKG;AACH,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;IAED,IAAI,CAAC;QACH,OAAO,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE;YACnC,KAAK,EAAE,UAAU;YACjB,QAAQ,EAAE,MAAM,CAAC,QAAQ;SAC1B,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,wEAAwE;QACxE,4EAA4E;QAC5E,OAAO,GAAG,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;IAC9C,CAAC;AACH,CAAC"}
|