@rudra-js/react 0.8.1 → 0.8.3
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 +126 -102
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -6,29 +6,50 @@ rudra-js is an open-source TypeScript library (MIT) you install from npm. A lang
|
|
|
6
6
|
|
|
7
7
|
It is not a hosted platform, a visual or low-code app builder, or a workspace you sign up for.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
[`@rudra-js/core`](https://github.com/clivedsouza1010/rudra-js/tree/main/packages/core)
|
|
11
|
-
|
|
9
|
+
This package renders a component spec with React 18 or 19. The spec is the description of the block
|
|
10
|
+
that [`@rudra-js/core`](https://github.com/clivedsouza1010/rudra-js/tree/main/packages/core)
|
|
11
|
+
generates.
|
|
12
12
|
|
|
13
|
-
The
|
|
13
|
+
The reference for [`RudraComponent`](https://rudrajs.com/reference/react/RudraComponent) and [`extendRegistry`](https://rudrajs.com/reference/react/extendRegistry) is on rudrajs.com.
|
|
14
14
|
|
|
15
|
-
The components are plain functions with no hooks, state or effects.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
The components are plain functions with no hooks, state or effects. You can render them:
|
|
16
|
+
|
|
17
|
+
- with `renderToString`
|
|
18
|
+
- in a client-rendered app
|
|
19
|
+
- as Server Components, in a setup that supports them, such as the Next.js App Router
|
|
20
|
+
|
|
21
|
+
Rendered on the server, the recommendations arrive in the initial HTML response and need no client
|
|
22
|
+
JavaScript. They never pop in late the way a client-fetched recommendation rail does. A crawler that
|
|
23
|
+
doesn't run JavaScript still reads them.
|
|
21
24
|
|
|
22
25
|
## Install
|
|
23
26
|
|
|
24
27
|
```sh
|
|
25
|
-
npm install @rudra-js/react @rudra-js/core @rudra-js/attested react
|
|
28
|
+
npm install @rudra-js/react@latest @rudra-js/core@latest @rudra-js/attested@latest react react-dom
|
|
26
29
|
```
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
Use this same command to install and to upgrade, whether or not your app already uses rudra-js.
|
|
32
|
+
|
|
33
|
+
- If your app has `@rudra-js/anthropic`, add it to the command with `@latest` too.
|
|
34
|
+
- If your app has `@rudra-js/a2ui`, use the install line in that package's README instead. It also
|
|
35
|
+
names the two A2UI packages.
|
|
36
|
+
|
|
37
|
+
Keep `@latest` on every `@rudra-js` package. The packages are published together. Each one's peer
|
|
38
|
+
ranges on the others start at its own version. With a bare name, npm stays inside the range your
|
|
39
|
+
`package.json` already has. So without `@latest`, an app on an older core can get an `ERESOLVE`.
|
|
40
|
+
|
|
41
|
+
Leave `react` and `react-dom` without a version. Then npm and pnpm keep a React 18 app on 18. Do name
|
|
42
|
+
both of them, so npm moves each one to the newest release in its range. React 19 won't load a
|
|
43
|
+
`react-dom` at a different version from `react`.
|
|
44
|
+
|
|
45
|
+
`@rudra-js/core` and `react` are peer dependencies. The spec you pass in comes from your copy of core.
|
|
46
|
+
The elements this package renders have to come from the same React your app renders. With two copies
|
|
47
|
+
of core, the spec fails its own type check. With two copies of React, React refuses to render the
|
|
48
|
+
component tree.
|
|
49
|
+
|
|
50
|
+
`@rudra-js/attested` is core's peer dependency. npm and pnpm add it for you. Yarn doesn't. Yarn does
|
|
51
|
+
move a bare name to its latest version, so with Yarn, leave `react` and `react-dom` out of the command
|
|
52
|
+
if your app already has React.
|
|
32
53
|
|
|
33
54
|
```tsx
|
|
34
55
|
import { RudraComponent } from '@rudra-js/react';
|
|
@@ -40,9 +61,8 @@ import { RudraComponent } from '@rudra-js/react';
|
|
|
40
61
|
|
|
41
62
|
## What comes from where
|
|
42
63
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
the model wrote is rendered as escaped text.
|
|
64
|
+
The model decides how the block is laid out and what the words say. Every fact about a product is
|
|
65
|
+
read from your catalog when the page is served. Whatever the model wrote is rendered as escaped text.
|
|
46
66
|
|
|
47
67
|
| Decided by the model | Decided by your catalog |
|
|
48
68
|
| ------------------------------------------------------------------- | ----------------------- |
|
|
@@ -50,40 +70,41 @@ the model wrote is rendered as escaped text.
|
|
|
50
70
|
| Tone, headline, the words in each block | Every price |
|
|
51
71
|
| Only in per-shopper mode: which products, and how each is described | Every image and link |
|
|
52
72
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
`generation: 'per-shopper'
|
|
56
|
-
the full split in
|
|
73
|
+
The default is cohort mode, where shoppers who look alike share one spec. In cohort mode, core
|
|
74
|
+
fills in the products and their order for each request. It also writes the reason under each product.
|
|
75
|
+
It drops any badge the model wrote. Under `generation: 'per-shopper'`, the model makes those picks
|
|
76
|
+
itself. In both modes, the model chooses the product a hero names. The full split is in
|
|
57
77
|
[What the model decides, by mode](https://github.com/clivedsouza1010/rudra-js/tree/main/packages/core#what-the-model-decides-by-mode).
|
|
58
78
|
|
|
59
|
-
The
|
|
60
|
-
|
|
79
|
+
The spec has no field for a title, a price, an image or a URL. The component looks up each product in
|
|
80
|
+
`products` by a SKU that core has already checked.
|
|
81
|
+
|
|
82
|
+
**Validate `products` with `productSchema` from `@rudra-js/core`**, the same schema the candidate
|
|
83
|
+
products you sent to core already passed. This prop is a second way for data to reach the page.
|
|
84
|
+
|
|
85
|
+
`imageUrl` ends up in an `<img src>`. `productSchema` rejects a protocol-relative URL such as
|
|
86
|
+
`//evil.example/pixel.png`, and it rejects a `data:` URL. It reads a path the way a browser does, so it
|
|
87
|
+
also rejects `/\evil.example/pixel.png` and the same trick written with a tab or a newline in it. A
|
|
88
|
+
browser resolves each of these to someone else's host. React blocks a `javascript:` URL on its own,
|
|
89
|
+
but none of these.
|
|
61
90
|
|
|
62
|
-
|
|
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.
|
|
65
|
-
It reads a path the way a browser does, so `/\evil.example/pixel.png` and the same trick written
|
|
66
|
-
with a tab or a newline in it are rejected too — each one resolves to someone else's host. React
|
|
67
|
-
neutralises a `javascript:` URL by itself, but not any of those. And if a price isn't a finite
|
|
68
|
-
number, it throws. A product that looks free is worse than a stack trace.
|
|
91
|
+
If a price isn't a finite number, rendering throws an error.
|
|
69
92
|
|
|
70
|
-
A row
|
|
71
|
-
loses its link
|
|
72
|
-
|
|
73
|
-
later and may be the fresher of the two.
|
|
93
|
+
A row with `isInStock: false` counts as a row that isn't there. Its cards are dropped. A hero that
|
|
94
|
+
names it loses its link. A bundle with it as a member disappears. Core only names products that were
|
|
95
|
+
in stock in the data you sent it. This catalog is read later, so it may be the fresher of the two.
|
|
74
96
|
|
|
75
97
|
## Styling
|
|
76
98
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
point you style from.
|
|
99
|
+
This package ships no CSS. Unstyled, the block renders as one run-on line. Every card element is
|
|
100
|
+
inline, so a card's parts sit on one line with spaces between them. Nothing separates one card from
|
|
101
|
+
the next. You style it from there.
|
|
81
102
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
shows you the raw markup.
|
|
103
|
+
For a working stylesheet, see `examples/shop/public/demo-styles.css`. It uses nothing but the classes
|
|
104
|
+
in the table below. Copy it as a starting point. It isn't a supported API. The example shop applies
|
|
105
|
+
it by default, and `?styles=off` shows you the raw markup.
|
|
85
106
|
|
|
86
|
-
Every element
|
|
107
|
+
Every element the component renders carries a class. Here is the full list:
|
|
87
108
|
|
|
88
109
|
| Where | Classes |
|
|
89
110
|
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -96,24 +117,29 @@ Every element we emit carries a class. Here's all of them:
|
|
|
96
117
|
| 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` |
|
|
97
118
|
| 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` |
|
|
98
119
|
|
|
99
|
-
A few notes on those
|
|
100
|
-
`.rudra-bundle__label` is your own name for the set, so it shows up only for a bundle you gave a
|
|
101
|
-
`label`. And `className` is added alongside `rudra`, _never_ in place of it, so the child classes
|
|
102
|
-
keep working.
|
|
120
|
+
A few notes on those:
|
|
103
121
|
|
|
104
|
-
`.
|
|
105
|
-
|
|
106
|
-
`.
|
|
122
|
+
- `.rudra__rationale` appears only under `hasDiagnostics`.
|
|
123
|
+
- `.rudra-bundle__label` holds your own name for the set. It shows up only for a bundle you gave a
|
|
124
|
+
`label`.
|
|
125
|
+
- Your `className` is added alongside `rudra`, never in place of it, so the child classes keep
|
|
126
|
+
working.
|
|
127
|
+
- `.rudra-carousel__track` is meant to scroll horizontally, so give it `overflow-x: auto`. No
|
|
128
|
+
JavaScript scrolls it for you.
|
|
129
|
+
- `.rudra-card--featured` is applied alongside `.rudra-card`. Write the `.rudra-card--featured { ... }`
|
|
130
|
+
rule after the base rule so it layers on top.
|
|
107
131
|
|
|
108
|
-
`.rudra-banner__cta` and `.rudra-bundle__cta` are plain text, not links or buttons
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
132
|
+
`.rudra-banner__cta` and `.rudra-bundle__cta` are plain text, not links or buttons. Don't style
|
|
133
|
+
them as buttons unless a renderer of your own wraps them in one. The hero's call to action sits inside
|
|
134
|
+
the product link, so that one is clickable.
|
|
135
|
+
|
|
136
|
+
Every card in a carousel is a link. Tabbing through the cards scrolls the track, so the carousel
|
|
137
|
+
works by keyboard even with the scrollbar hidden.
|
|
113
138
|
|
|
114
139
|
### Attributes
|
|
115
140
|
|
|
116
|
-
The
|
|
141
|
+
The markup also carries what the model decided. You can hang styling or analytics off these
|
|
142
|
+
attributes.
|
|
117
143
|
|
|
118
144
|
| Attribute | On | Value |
|
|
119
145
|
| ------------------------ | ------------------------------ | ---------------------------------------------------------- |
|
|
@@ -125,8 +151,8 @@ The same markup carries what the model decided, so you can hang styling or analy
|
|
|
125
151
|
| `data-rudra-sku` | a card, hero link, bundle item | The product, for click attribution |
|
|
126
152
|
| `data-rudra-basis` | a card | Why the product was picked: `most_viewed`, `popular`, … |
|
|
127
153
|
|
|
128
|
-
|
|
129
|
-
rendered page. Anything more specific
|
|
154
|
+
`data-rudra-source` is always there, so you can read the cache hit rate and the fallback share
|
|
155
|
+
straight off a rendered page. Anything more specific appears only under `hasDiagnostics`, because it
|
|
130
156
|
tells a visitor what you run and when it's failing:
|
|
131
157
|
|
|
132
158
|
| Attribute | On | Value |
|
|
@@ -138,9 +164,8 @@ tells a visitor what you run and when it's failing:
|
|
|
138
164
|
|
|
139
165
|
## Replacing a renderer
|
|
140
166
|
|
|
141
|
-
Swap any block for your own design
|
|
142
|
-
|
|
143
|
-
did.
|
|
167
|
+
Swap any block's renderer for a component from your own design system. Only the drawing changes. The
|
|
168
|
+
spec, and the block kinds the model picks from, stay the same.
|
|
144
169
|
|
|
145
170
|
```tsx
|
|
146
171
|
import { RudraComponent, extendRegistry } from '@rudra-js/react';
|
|
@@ -153,63 +178,62 @@ const registry = extendRegistry({
|
|
|
153
178
|
```
|
|
154
179
|
|
|
155
180
|
A grid, carousel or bundle block with nothing left in your catalog is dropped before any renderer
|
|
156
|
-
runs
|
|
157
|
-
|
|
158
|
-
reach the renderer, since they don't depend on the catalog.
|
|
181
|
+
runs, yours or ours. The rest of the blocks still render. Hero, banner and copy blocks don't
|
|
182
|
+
depend on the catalog, so they always reach the renderer.
|
|
159
183
|
|
|
160
184
|
## Props
|
|
161
185
|
|
|
162
|
-
| Prop | Notes
|
|
163
|
-
| ------------------- |
|
|
164
|
-
| `spec` | Required. From `@rudra-js/core`.
|
|
165
|
-
| `products` | Required. A list of products, or anything keyed by SKU. See below, and the warning above.
|
|
166
|
-
| `bundles` | The sets your shop sells together. Only needed if a spec can carry a bundle block.
|
|
167
|
-
| `registry` |
|
|
168
|
-
| `hrefForSku` | Defaults to `/product/{sku}`, URL-encoded.
|
|
169
|
-
| `formatPrice` | Defaults to `Intl.NumberFormat`, which knows how many decimal places each currency wants.
|
|
170
|
-
| `formatBundlePrice` |
|
|
171
|
-
| `locale` |
|
|
172
|
-
| `hasDiagnostics` | Adds the provider, the model name, the latency and the model's own reasoning to the markup. Off by default
|
|
173
|
-
| `className` | Added alongside `rudra`.
|
|
186
|
+
| Prop | Notes |
|
|
187
|
+
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
188
|
+
| `spec` | Required. From `@rudra-js/core`. |
|
|
189
|
+
| `products` | Required. A list of products, or anything keyed by SKU. See below, and the `productSchema` warning above. |
|
|
190
|
+
| `bundles` | The sets your shop sells together. Only needed if a spec can carry a bundle block. |
|
|
191
|
+
| `registry` | Replaces some or all of the block renderers. |
|
|
192
|
+
| `hrefForSku` | Defaults to `/product/{sku}`, URL-encoded. |
|
|
193
|
+
| `formatPrice` | Formats product prices, not bundle prices. Defaults to `Intl.NumberFormat`, which knows how many decimal places each currency wants. |
|
|
194
|
+
| `formatBundlePrice` | Formats a bundle's own price, in the currency the shop put on the set. Defaults to `Intl.NumberFormat`, like `formatPrice`. The price and its currency come from the same bundle object, so members priced in another currency change nothing. |
|
|
195
|
+
| `locale` | Sets how prices are punctuated. Defaults to the **server's** locale, which is rarely the shopper's. Pass it if you serve more than one locale. |
|
|
196
|
+
| `hasDiagnostics` | Adds the provider, the model name, the latency and the model's own reasoning to the markup. Off by default. |
|
|
197
|
+
| `className` | Added alongside `rudra`. |
|
|
174
198
|
|
|
175
199
|
### What `products` may be
|
|
176
200
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
catalog from a list
|
|
180
|
-
a view of your own store instead. The prop is typed as a `ReadonlyMap`, so in
|
|
181
|
-
implement `ReadonlyMap` or cast with `as unknown as ReadonlyMap<string, Product>`.
|
|
201
|
+
`products` is a list of products, or anything keyed by SKU that answers `get(sku)` and `has(sku)`. A
|
|
202
|
+
`Map` works, and so does your own index. The renderers only ever call `get`. `has` is how the
|
|
203
|
+
component tells a keyed catalog from a list. If your catalog is too big to copy into a `Map` on every
|
|
204
|
+
request, pass a view of your own store instead. The prop is typed as a `ReadonlyMap`, so in
|
|
205
|
+
TypeScript either implement `ReadonlyMap` or cast with `as unknown as ReadonlyMap<string, Product>`.
|
|
182
206
|
|
|
183
|
-
|
|
184
|
-
from a worker or a `node:vm` sandbox is a perfectly good catalog and fails `instanceof` anyway.
|
|
207
|
+
A `Map` from a worker or a `node:vm` sandbox works too, even though it fails `instanceof Map`.
|
|
185
208
|
|
|
186
|
-
Anything that is neither a list nor keyed
|
|
187
|
-
That
|
|
188
|
-
error than a quietly empty recommendation area.
|
|
209
|
+
Anything that is neither a list nor keyed is refused on the spot, with an error that names the prop.
|
|
210
|
+
That means a `Set` of products, a plain object, or a `Map` that has been through JSON.
|
|
189
211
|
|
|
190
212
|
When there's nothing left to show, the component renders nothing at all. That covers a spec with no
|
|
191
|
-
blocks, and
|
|
192
|
-
goes the same way on its own: it drops out and the rest of the component carries on without it. An
|
|
193
|
-
empty recommendation area, or a headline over an empty box, takes up space and tells the shopper the
|
|
194
|
-
page is broken.
|
|
213
|
+
blocks, and a spec whose products have all left your catalog since it was generated.
|
|
195
214
|
|
|
196
215
|
### What `bundles` is
|
|
197
216
|
|
|
198
|
-
|
|
199
|
-
a bundle block. It never invents
|
|
217
|
+
`bundles` lists the sets your shop sells together, the same way `products` is your catalog. The
|
|
218
|
+
model only asks for a bundle block. It never invents a set, and it never sees a price.
|
|
200
219
|
|
|
201
|
-
You offer the sets, and core picks which one fills each block.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
220
|
+
You offer the sets, and core picks which one fills each bundle block. It does this in `reconcileSpec`
|
|
221
|
+
as the page is served, after the spec was generated. It goes by what this shopper has in their
|
|
222
|
+
basket, has looked at, or is browsing right now. The spec then carries the id of the set core picked.
|
|
223
|
+
This prop supplies the rest, so the component has something to draw: the set's members, its price,
|
|
224
|
+
the currency of that price, and your name for it.
|
|
206
225
|
|
|
207
226
|
**Validate `bundles` with `bundleSchema` from `@rudra-js/core`, and pass the same list you sent to
|
|
208
|
-
`parseTrackingInput`.** Core
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
227
|
+
`parseTrackingInput`.** Core checked that list before it picked a set:
|
|
228
|
+
|
|
229
|
+
- every member is in stock
|
|
230
|
+
- no member is one the shopper disliked
|
|
231
|
+
- no member is the product the shopper is viewing
|
|
232
|
+
- no product shows up twice
|
|
233
|
+
- the whole set fits in what's left of `context.maxItems`, the most products the component may show
|
|
234
|
+
|
|
235
|
+
Core then hands the renderer only the id it chose. Pass a stale or different list here and you'll
|
|
236
|
+
draw a set none of those checks saw, under an id that was checked against another list.
|
|
213
237
|
|
|
214
238
|
## Licence
|
|
215
239
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rudra-js/react",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
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
6
|
"rudra-js",
|
|
@@ -54,14 +54,14 @@
|
|
|
54
54
|
"access": "public"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"@rudra-js/core": "^0.8.
|
|
57
|
+
"@rudra-js/core": "^0.8.3",
|
|
58
58
|
"react": "^18.2.0 || ^19.0.0"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@rudra-js/core": "0.8.
|
|
61
|
+
"@rudra-js/core": "0.8.3",
|
|
62
62
|
"@types/react": "^19.3.0",
|
|
63
63
|
"@types/react-dom": "^19.3.0",
|
|
64
|
-
"react": "^19.3.
|
|
65
|
-
"react-dom": "^19.3.
|
|
64
|
+
"react": "^19.3.1",
|
|
65
|
+
"react-dom": "^19.3.1"
|
|
66
66
|
}
|
|
67
67
|
}
|