@rudra-js/react 0.1.0 → 0.3.1
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 +84 -84
- package/dist/blocks/product-card.d.ts +3 -3
- package/dist/blocks/product-card.js +3 -3
- package/dist/render-context.d.ts +2 -2
- package/dist/render-context.js +3 -4
- package/dist/render-context.js.map +1 -1
- package/package.json +6 -6
- package/src/blocks/product-card.tsx +3 -3
- package/src/render-context.ts +6 -6
package/README.md
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
# @rudra-js/react
|
|
2
2
|
|
|
3
3
|
Renders a component specification from
|
|
4
|
-
[`@rudra-js/core`](https://github.com/clivedsouza1010/rudra-js/tree/main/packages/core) as React
|
|
5
|
-
Components.
|
|
4
|
+
[`@rudra-js/core`](https://github.com/clivedsouza1010/rudra-js/tree/main/packages/core) as React
|
|
5
|
+
Server Components.
|
|
6
6
|
|
|
7
|
-
No client JavaScript. The recommendation area arrives in the initial HTML
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
crawler that does not run JavaScript.
|
|
7
|
+
No client JavaScript. The recommendation area arrives in the initial HTML response and needs no
|
|
8
|
+
hydration, so it never pops in the way a client-fetched recommendation rail does, and a crawler that
|
|
9
|
+
doesn't run JavaScript still reads it.
|
|
11
10
|
|
|
12
11
|
## Install
|
|
13
12
|
|
|
@@ -15,10 +14,10 @@ crawler that does not run JavaScript.
|
|
|
15
14
|
npm install @rudra-js/react @rudra-js/core react zod@^4
|
|
16
15
|
```
|
|
17
16
|
|
|
18
|
-
Both `@rudra-js/core` and `react` are peer dependencies
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
Both `@rudra-js/core` and `react` are peer dependencies. The specification you pass in comes from
|
|
18
|
+
your copy of core, and the elements this renders have to come from the same React your app renders.
|
|
19
|
+
With two copies of either, you'd get a spec that fails its own type check, or a component tree React
|
|
20
|
+
refuses to render.
|
|
22
21
|
|
|
23
22
|
```tsx
|
|
24
23
|
import { RudraComponent } from '@rudra-js/react';
|
|
@@ -26,43 +25,46 @@ import { RudraComponent } from '@rudra-js/react';
|
|
|
26
25
|
<RudraComponent spec={spec} products={catalog} locale="en-GB" />;
|
|
27
26
|
```
|
|
28
27
|
|
|
29
|
-
`spec` is what `createComponentGenerator().generate()` returned. `products` is
|
|
30
|
-
your catalog.
|
|
28
|
+
`spec` is what `createComponentGenerator().generate()` returned. `products` is your catalog.
|
|
31
29
|
|
|
32
30
|
## What comes from where
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
A rendered component rests on this split. The model decides how things are arranged and what the
|
|
33
|
+
words are. Every fact about a product is read from your catalog as the page is served, and whatever
|
|
34
|
+
the model wrote is rendered as escaped text.
|
|
35
35
|
|
|
36
|
-
| Decided by the model
|
|
37
|
-
|
|
|
38
|
-
| Which layout, in what order
|
|
39
|
-
|
|
|
40
|
-
|
|
|
36
|
+
| Decided by the model | Decided by your catalog |
|
|
37
|
+
| ------------------------------------------------------------------- | ----------------------- |
|
|
38
|
+
| Which layout, in what order | Every product title |
|
|
39
|
+
| Tone, headline, the words in each block | Every price |
|
|
40
|
+
| Only in per-shopper mode: which products, and how each is described | Every image and link |
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
In the default cohort mode the framework fills in the products, their order and the reason under
|
|
43
|
+
each, per request, and a badge the model wrote gets dropped. The model picks those only under
|
|
44
|
+
`generation: 'per-shopper'`. In either mode it still chooses the product a hero names. You'll find
|
|
45
|
+
the full split in
|
|
46
|
+
[What the model decides, by mode](https://github.com/clivedsouza1010/rudra-js/tree/main/packages/core#what-the-model-decides-by-mode).
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
The specification has no field carrying a title, a price, an image or a URL. Product facts are
|
|
49
|
+
resolved at render time from `products`, keyed by a SKU reconciliation has already checked.
|
|
50
|
+
|
|
51
|
+
**Validate `products` with `productSchema` from `@rudra-js/core`**, the same schema your candidates
|
|
52
|
+
already passed. This prop is a second door into the framework. `imageUrl` lands in an `<img src>`,
|
|
53
|
+
and `productSchema` is what rejects a protocol-relative `//evil.example/pixel.png` or a `data:` URL.
|
|
54
|
+
React neutralises a `javascript:` URL by itself, but not those two. And if a price isn't a finite
|
|
55
|
+
number, it throws. A product that looks free is worse than a stack trace.
|
|
53
56
|
|
|
54
57
|
## Styling
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
the starting point, not a fault.
|
|
59
|
+
We ship no CSS. A stylesheet of ours would only fight whatever your site already has. So out of the
|
|
60
|
+
box the block renders as a run-on line — every card element is inline, so titles and prices sit
|
|
61
|
+
together with no separation. That's the starting point you style from.
|
|
60
62
|
|
|
61
|
-
`examples/shop/public/demo-styles.css
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
Check out `examples/shop/public/demo-styles.css`. It's a working stylesheet written against nothing
|
|
64
|
+
but the table below, so copy it as a starting point rather than as a supported API. The example shop applies it by default, and `?styles=off`
|
|
65
|
+
shows you the raw markup.
|
|
64
66
|
|
|
65
|
-
Every element
|
|
67
|
+
Every element we emit carries a class. Here's all of them:
|
|
66
68
|
|
|
67
69
|
| Where | Classes |
|
|
68
70
|
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -75,17 +77,18 @@ Every element it emits carries a class, and this is all of them:
|
|
|
75
77
|
| 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
78
|
| 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
79
|
|
|
78
|
-
`.rudra__rationale` only appears under `hasDiagnostics`.
|
|
79
|
-
is your own name for the set,
|
|
80
|
-
`className` is added alongside `rudra
|
|
81
|
-
|
|
80
|
+
A few notes on those. `.rudra__rationale` only appears under `hasDiagnostics`.
|
|
81
|
+
`.rudra-bundle__label` is your own name for the set, so it shows up only for a bundle you gave a
|
|
82
|
+
`label`. And `className` is added alongside `rudra`, _never_ in place of it, so the child classes
|
|
83
|
+
keep working.
|
|
82
84
|
|
|
83
|
-
`.rudra-carousel__track` is
|
|
84
|
-
|
|
85
|
+
`.rudra-carousel__track` is meant to scroll horizontally, so give it `overflow-x: auto`. Nothing
|
|
86
|
+
here uses JavaScript to scroll it for you. `.rudra-card--featured` is applied alongside
|
|
87
|
+
`.rudra-card`, so write `.rudra-card--featured { ... }` after the base rule and let it layer on top.
|
|
85
88
|
|
|
86
89
|
### Attributes
|
|
87
90
|
|
|
88
|
-
The same markup carries what the model decided,
|
|
91
|
+
The same markup carries what the model decided, so you can hang styling or analytics off it.
|
|
89
92
|
|
|
90
93
|
| Attribute | On | Value |
|
|
91
94
|
| ------------------------ | ------------------------------ | ---------------------------------------------------------- |
|
|
@@ -95,11 +98,11 @@ The same markup carries what the model decided, for styling and for analytics.
|
|
|
95
98
|
| `data-rudra-banner-tone` | a banner | A banner's own tone, a different vocabulary from the above |
|
|
96
99
|
| `data-rudra-columns` | a grid | The column count the model chose |
|
|
97
100
|
| `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
|
|
101
|
+
| `data-rudra-basis` | a card | Why the product was picked: `most_viewed`, `popular`, … |
|
|
99
102
|
|
|
100
|
-
`data-rudra-source`
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
We leave `data-rudra-source` public so you can read hit rate and fallback share straight off a
|
|
104
|
+
rendered page. Anything more specific than that appears only under `hasDiagnostics`, because it
|
|
105
|
+
tells a visitor what you run and when it's failing:
|
|
103
106
|
|
|
104
107
|
| Attribute | On | Value |
|
|
105
108
|
| ----------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -110,9 +113,9 @@ read straight off a rendered page. Everything more specific appears only under
|
|
|
110
113
|
|
|
111
114
|
## Replacing a renderer
|
|
112
115
|
|
|
113
|
-
Swap any block for your own design-system component. The model
|
|
114
|
-
|
|
115
|
-
|
|
116
|
+
Swap any block for your own design-system component. The model isn't involved and the specification
|
|
117
|
+
doesn't change, so it gains nothing here. It still picks from the same fixed vocabulary it always
|
|
118
|
+
did.
|
|
116
119
|
|
|
117
120
|
```tsx
|
|
118
121
|
import { RudraComponent, extendRegistry } from '@rudra-js/react';
|
|
@@ -133,50 +136,47 @@ const registry = extendRegistry({
|
|
|
133
136
|
| `bundles` | The sets your shop sells together. Only needed if a spec can carry a bundle block. |
|
|
134
137
|
| `registry` | Replace some or all block renderers. |
|
|
135
138
|
| `hrefForSku` | Defaults to `/product/{sku}`, URL-encoded. |
|
|
136
|
-
| `formatPrice` | Defaults to `Intl.NumberFormat`, which knows
|
|
139
|
+
| `formatPrice` | Defaults to `Intl.NumberFormat`, which knows how many decimal places each currency wants. It formats products, not bundles. |
|
|
137
140
|
| `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
|
|
139
|
-
| `hasDiagnostics` | Adds the provider, the model name, the latency and the model's own reasoning to the markup. Off by default
|
|
141
|
+
| `locale` | Punctuates prices. Defaults to the **server's** locale, which is rarely the shopper's, so pass it if you serve more than one. |
|
|
142
|
+
| `hasDiagnostics` | Adds the provider, the model name, the latency and the model's own reasoning to the markup. Off by default, since it tells a visitor which model you use and when it's failing. |
|
|
140
143
|
| `className` | Added alongside `rudra`. |
|
|
141
144
|
|
|
142
145
|
### What `products` may be
|
|
143
146
|
|
|
144
|
-
A list of products, or anything keyed by SKU that answers `get(sku)` and
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
A list of products, or anything keyed by SKU that answers `get(sku)` and `has(sku)`. A `Map` does
|
|
148
|
+
it, and so does your own index. Those two methods are the only ones the renderers ever call, so if
|
|
149
|
+
your catalog is too big to copy into a `Map` on every request, hand over a view of your own store
|
|
150
|
+
instead.
|
|
151
|
+
|
|
152
|
+
We look for those two methods rather than for `instanceof Map`, which is per-realm. A `Map` arriving
|
|
153
|
+
from a worker or a `node:vm` sandbox is a perfectly good catalog and fails `instanceof` anyway.
|
|
148
154
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
+
Anything that is neither a list nor keyed gets refused on the spot, with an error naming the prop.
|
|
156
|
+
That's a `Set` of products, a plain object, or a `Map` that has been through JSON. Better a loud
|
|
157
|
+
error than a quietly empty recommendation area.
|
|
155
158
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
When there's nothing left to show, the component renders nothing at all. That covers a spec with no
|
|
160
|
+
blocks, and one whose every product has left your catalog since it was generated. An empty
|
|
161
|
+
recommendation area, or a headline over an empty box, takes up space and tells the shopper the page
|
|
162
|
+
is broken.
|
|
160
163
|
|
|
161
164
|
### What `bundles` is
|
|
162
165
|
|
|
163
|
-
The sets your shop sells together, the same way `products` is your catalog. The
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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.
|
|
166
|
+
The sets your shop sells together, the same way `products` is your catalog. The model only asks for
|
|
167
|
+
a bundle block. It never invents one, and it _never_ sees a price.
|
|
168
|
+
|
|
169
|
+
You offer the sets, and the framework picks which one fills each block. That happens inside
|
|
170
|
+
`reconcileSpec` as the page is served, after the spec was generated rather than before, and it goes on what this
|
|
171
|
+
shopper has in their basket, has looked at, or is browsing right now. The spec then carries the id
|
|
172
|
+
it picked. This prop supplies the rest: that set's members, its price, the currency that price is
|
|
173
|
+
in, and your name for it, so the component has something to draw.
|
|
174
|
+
|
|
175
|
+
**Validate `bundles` with `bundleSchema` from `@rudra-js/core`, and pass the same list you sent to
|
|
176
|
+
`parseTrackingInput`.** Core already checked that list — every member in stock, none of them
|
|
177
|
+
disliked, no repeats, and the whole set inside the item budget — and then hands the renderer nothing
|
|
178
|
+
but the id it chose. Pass a stale or different list here and you'll draw a set none of those checks
|
|
179
|
+
ever saw, under an id that was proved against another one.
|
|
180
180
|
|
|
181
181
|
## Licence
|
|
182
182
|
|
|
@@ -4,9 +4,9 @@ import type { BlockRenderContext } from '../render-context.js';
|
|
|
4
4
|
* One product.
|
|
5
5
|
*
|
|
6
6
|
* Every fact here — the title, the price, the image, the link — is read from
|
|
7
|
-
* the catalog, not from the specification. The
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
10
|
*/
|
|
11
11
|
export declare function ProductCard({ reference, context, }: {
|
|
12
12
|
reference: ProductReference;
|
|
@@ -3,9 +3,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
* One product.
|
|
4
4
|
*
|
|
5
5
|
* Every fact here — the title, the price, the image, the link — is read from
|
|
6
|
-
* the catalog, not from the specification. The
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* the catalog, not from the specification. The specification contributes only
|
|
7
|
+
* `reason`, `badge` and `emphasis`, and React escapes all three on the way
|
|
8
|
+
* into the markup.
|
|
9
9
|
*/
|
|
10
10
|
export function ProductCard({ reference, context, }) {
|
|
11
11
|
const product = context.products.get(reference.sku);
|
package/dist/render-context.d.ts
CHANGED
|
@@ -8,8 +8,8 @@ import type { Bundle, Product } from '@rudra-js/core';
|
|
|
8
8
|
* the model did not invent — both in stock at the time. A bundle member's SKU
|
|
9
9
|
* comes from the set the shop supplied, not from the model. That division
|
|
10
10
|
* is the whole reason a generated component is safe to put in a page — the
|
|
11
|
-
* model decides
|
|
12
|
-
*
|
|
11
|
+
* model decides how the component reads, and the shop decides what is true
|
|
12
|
+
* about a product.
|
|
13
13
|
*/
|
|
14
14
|
export interface BlockRenderContext {
|
|
15
15
|
/** The host's catalog, keyed by SKU. Read-only: it is the caller's own map. */
|
package/dist/render-context.js
CHANGED
|
@@ -53,16 +53,15 @@ export function defaultFormatBundlePrice(bundle, locale) {
|
|
|
53
53
|
throw new TypeError(`price for bundle ${bundle.id} is ${String(bundle.price)}, not a finite number — ` +
|
|
54
54
|
'bundle objects must satisfy bundleSchema from @rudra-js/core');
|
|
55
55
|
}
|
|
56
|
+
let formatter;
|
|
56
57
|
try {
|
|
57
|
-
|
|
58
|
-
style: 'currency',
|
|
59
|
-
currency: bundle.currency,
|
|
60
|
-
}).format(bundle.price);
|
|
58
|
+
formatter = new Intl.NumberFormat(locale, { style: 'currency', currency: bundle.currency });
|
|
61
59
|
}
|
|
62
60
|
catch {
|
|
63
61
|
// A bad locale, or a currency code Intl rejects on a hand-built bundle,
|
|
64
62
|
// should not crash the page. A price nobody can punctuate is still a price.
|
|
65
63
|
return `${bundle.currency} ${bundle.price}`;
|
|
66
64
|
}
|
|
65
|
+
return formatter.format(bundle.price);
|
|
67
66
|
}
|
|
68
67
|
//# sourceMappingURL=render-context.js.map
|
|
@@ -1 +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,
|
|
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,SAA4B,CAAC;IACjC,IAAI,CAAC;QACH,SAAS,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC9F,CAAC;IAAC,MAAM,CAAC;QACP,wEAAwE;QACxE,4EAA4E;QAC5E,OAAO,GAAG,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;IAC9C,CAAC;IAED,OAAO,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACxC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rudra-js/react",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Renders a rudra component specification as React Server Components",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"directory": "packages/react"
|
|
24
24
|
},
|
|
25
25
|
"engines": {
|
|
26
|
-
"node": "
|
|
26
|
+
"node": ">=22.12.0"
|
|
27
27
|
},
|
|
28
28
|
"type": "module",
|
|
29
29
|
"main": "./dist/index.js",
|
|
@@ -51,13 +51,13 @@
|
|
|
51
51
|
"access": "public"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@rudra-js/core": "^0.1
|
|
55
|
-
"react": "^19.0.0"
|
|
54
|
+
"@rudra-js/core": "^0.3.1",
|
|
55
|
+
"react": "^18.2.0 || ^19.0.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@rudra-js/core": "0.1
|
|
58
|
+
"@rudra-js/core": "0.3.1",
|
|
59
59
|
"@types/react": "^19.2.18",
|
|
60
|
-
"@types/react-dom": "^19.2.
|
|
60
|
+
"@types/react-dom": "^19.2.7",
|
|
61
61
|
"react": "^19.2.8",
|
|
62
62
|
"react-dom": "^19.2.8"
|
|
63
63
|
}
|
|
@@ -5,9 +5,9 @@ import type { BlockRenderContext } from '../render-context.js';
|
|
|
5
5
|
* One product.
|
|
6
6
|
*
|
|
7
7
|
* Every fact here — the title, the price, the image, the link — is read from
|
|
8
|
-
* the catalog, not from the specification. The
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
11
|
*/
|
|
12
12
|
export function ProductCard({
|
|
13
13
|
reference,
|
package/src/render-context.ts
CHANGED
|
@@ -9,8 +9,8 @@ import type { Bundle, Product } from '@rudra-js/core';
|
|
|
9
9
|
* the model did not invent — both in stock at the time. A bundle member's SKU
|
|
10
10
|
* comes from the set the shop supplied, not from the model. That division
|
|
11
11
|
* is the whole reason a generated component is safe to put in a page — the
|
|
12
|
-
* model decides
|
|
13
|
-
*
|
|
12
|
+
* model decides how the component reads, and the shop decides what is true
|
|
13
|
+
* about a product.
|
|
14
14
|
*/
|
|
15
15
|
export interface BlockRenderContext {
|
|
16
16
|
/** The host's catalog, keyed by SKU. Read-only: it is the caller's own map. */
|
|
@@ -86,14 +86,14 @@ export function defaultFormatBundlePrice(bundle: Bundle, locale?: string): strin
|
|
|
86
86
|
);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
let formatter: Intl.NumberFormat;
|
|
89
90
|
try {
|
|
90
|
-
|
|
91
|
-
style: 'currency',
|
|
92
|
-
currency: bundle.currency,
|
|
93
|
-
}).format(bundle.price);
|
|
91
|
+
formatter = new Intl.NumberFormat(locale, { style: 'currency', currency: bundle.currency });
|
|
94
92
|
} catch {
|
|
95
93
|
// A bad locale, or a currency code Intl rejects on a hand-built bundle,
|
|
96
94
|
// should not crash the page. A price nobody can punctuate is still a price.
|
|
97
95
|
return `${bundle.currency} ${bundle.price}`;
|
|
98
96
|
}
|
|
97
|
+
|
|
98
|
+
return formatter.format(bundle.price);
|
|
99
99
|
}
|