@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.
Files changed (2) hide show
  1. package/README.md +126 -102
  2. 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
- Renders a component specification from
10
- [`@rudra-js/core`](https://github.com/clivedsouza1010/rudra-js/tree/main/packages/core) with React
11
- 18 or 19.
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 renderer and its registry are documented at [rudrajs.com](https://rudrajs.com/docs#RudraComponent).
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. They work with
16
- `renderToString`, in a client-rendered app, and as Server Components in a setup that supports them,
17
- such as the Next.js App Router. Rendered on the server, the
18
- recommendation area arrives in the initial HTML response and needs no client JavaScript, so it never
19
- pops in the way a client-fetched recommendation rail does, and a crawler that doesn't run JavaScript
20
- still reads it.
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
- Both `@rudra-js/core` and `react` are peer dependencies. The specification you pass in comes from
29
- your copy of core, and the elements this renders have to come from the same React your app renders.
30
- With two copies of either, you'd get a spec that fails its own type check, or a component tree React
31
- refuses to render. `@rudra-js/attested` is core's peer: npm and pnpm add it for you, Yarn doesn't.
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
- A rendered component rests on this split. The model decides how things are arranged and what the
44
- words are. Every fact about a product is read from your catalog as the page is served, and whatever
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
- 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
55
- `generation: 'per-shopper'`. In either mode it still chooses the product a hero names. You'll find
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 specification has no field carrying a title, a price, an image or a URL. Product facts are
60
- resolved at render time from `products`, keyed by a SKU reconciliation has already checked.
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
- **Validate `products` with `productSchema` from `@rudra-js/core`**, the same schema your candidates
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 flagged `isInStock: false` counts as a row that isn't there. Cards for it are dropped, a hero
71
- loses its link, and a bundle with one sold-out member disappears — the same as if you'd left the
72
- row out. Core only ever names a product that was in stock in the payload, but this catalog is read
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
- We ship no CSS. A stylesheet of ours would only fight whatever your site already has. So out of the
78
- box the block renders as a run-on line — every card element is inline, so a card's parts sit on one
79
- line with spaces between them, and nothing separates one card from the next. That's the starting
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
- Check out `examples/shop/public/demo-styles.css`. It's a working stylesheet written against nothing
83
- 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`
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 we emit carries a class. Here's all of them:
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. `.rudra__rationale` only appears under `hasDiagnostics`.
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
- `.rudra-carousel__track` is meant to scroll horizontally, so give it `overflow-x: auto`. Nothing
105
- here uses JavaScript to scroll it for you. `.rudra-card--featured` is applied alongside
106
- `.rudra-card`, so write `.rudra-card--featured { ... }` after the base rule and let it layer on top.
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, because there is
109
- nothing for them to do. Don't style them as buttons unless a renderer of your own wraps them in one.
110
- The hero's call to action sits inside the product link, so that one is clickable. Every card in a
111
- carousel is a link, so tabbing through them scrolls the track, which keeps it usable by keyboard
112
- even with the scrollbar hidden.
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 same markup carries what the model decided, so you can hang styling or analytics off it.
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
- We leave `data-rudra-source` public so you can read hit rate and fallback share straight off a
129
- rendered page. Anything more specific than that appears only under `hasDiagnostics`, because it
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-system component. The model isn't involved and the specification
142
- doesn't change, so it gains nothing here. It still picks from the same fixed vocabulary it always
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 — yours as well as ours. The component has to decide whether to draw the wrapper and its
157
- headline at all, and the only way to decide is to ask each block first. Hero, banner and copy always
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` | Replace some or all block renderers. |
168
- | `hrefForSku` | Defaults to `/product/{sku}`, URL-encoded. |
169
- | `formatPrice` | Defaults to `Intl.NumberFormat`, which knows how many decimal places each currency wants. It formats products, not bundles. |
170
- | `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. |
171
- | `locale` | Punctuates prices. Defaults to the **server's** locale, which is rarely the shopper's, so pass it if you serve more than one. |
172
- | `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. |
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
- A list of products, or anything keyed by SKU that answers `get(sku)` and `has(sku)`. A `Map` does
178
- it, and so does your own index. The renderers only ever call `get`, and `has` is how we tell a keyed
179
- catalog from a list, so if your catalog is too big to copy into a `Map` on every request, hand over
180
- a view of your own store instead. The prop is typed as a `ReadonlyMap`, so in TypeScript either
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
- We look for those two methods rather than for `instanceof Map`, which is per-realm. A `Map` arriving
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 gets refused on the spot, with an error naming the prop.
187
- That's a `Set` of products, a plain object, or a `Map` that has been through JSON. Better a loud
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 one whose every product has left your catalog since it was generated. A single block
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
- The sets your shop sells together, the same way `products` is your catalog. The model only asks for
199
- a bundle block. It never invents one, and it _never_ sees a price.
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. That happens inside
202
- `reconcileSpec` as the page is served, after the spec was generated rather than before, and it goes on what this
203
- shopper has in their basket, has looked at, or is browsing right now. The spec then carries the id
204
- it picked. This prop supplies the rest: that set's members, its price, the currency that price is
205
- in, and your name for it, so the component has something to draw.
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 already checked that list — every member in stock, none of them
209
- disliked, none of them the product being looked at, no repeats, and the whole set inside the item
210
- budget — and then hands the renderer nothing but the id it chose. Pass a stale or different list
211
- here and you'll draw a set none of those checks ever saw, under an id that was proved against
212
- another one.
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.1",
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.1",
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.1",
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.0",
65
- "react-dom": "^19.3.0"
64
+ "react": "^19.3.1",
65
+ "react-dom": "^19.3.1"
66
66
  }
67
67
  }