@rudra-js/react 0.9.0 → 0.9.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.
Files changed (2) hide show
  1. package/README.md +105 -94
  2. package/package.json +3 -3
package/README.md CHANGED
@@ -6,21 +6,21 @@ 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
- 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.
9
+ You can use this package to render a component spec with React 18 or 19. This spec is a description
10
+ of the block generated by
11
+ [`@rudra-js/core`](https://github.com/clivedsouza1010/rudra-js/tree/main/packages/core).
12
12
 
13
- The reference for [`RudraComponent`](https://rudrajs.com/reference/react/RudraComponent) and [`extendRegistry`](https://rudrajs.com/reference/react/extendRegistry) is on rudrajs.com.
13
+ For more on [`RudraComponent`](https://rudrajs.com/reference/react/RudraComponent) and [`extendRegistry`](https://rudrajs.com/reference/react/extendRegistry), see the reference on rudrajs.com.
14
14
 
15
- The components are plain functions with no hooks, state or effects. You can render them:
15
+ All the components are just plain functions with no hooks, state or effects. They can be rendered:
16
16
 
17
17
  - with `renderToString`
18
18
  - in a client-rendered app
19
- - as Server Components, in a setup that supports them, such as the Next.js App Router
19
+ - as Server Components, in a setup that supports them (like the Next.js App Router)
20
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
+ If you render on the server, the recommendations will be in the initial HTML response and won't need
22
+ any client JavaScript. They never show up late like a client-fetched recommendation rail does. Even
23
+ a crawler that doesn't run JavaScript reads the recommendations.
24
24
 
25
25
  ## Install
26
26
 
@@ -28,28 +28,30 @@ doesn't run JavaScript still reads them.
28
28
  npm install @rudra-js/react@latest @rudra-js/core@latest @rudra-js/attested@latest react react-dom
29
29
  ```
30
30
 
31
- Use this same command to install and to upgrade, whether or not your app already uses rudra-js.
31
+ You will need to use this same command to install or to upgrade, even if your app already uses
32
+ rudra-js.
32
33
 
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.
34
+ - For an app with `@rudra-js/anthropic`, you have to add it to the command with `@latest` too.
35
+ - For an app with `@rudra-js/a2ui`, you have to use the install line in that package's README
36
+ instead. It also names the two A2UI packages.
36
37
 
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`.
38
+ You have to keep `@latest` on all the `@rudra-js` packages. These packages are published together.
39
+ For each package, the peer ranges on the others start at its own version. With just a bare name, npm
40
+ will stay inside the range you already have in `package.json`. This means that without `@latest`, an
41
+ app on an older core can get an `ERESOLVE`.
40
42
 
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`.
43
+ You need to leave `react` and `react-dom` without a version. It's how npm and pnpm keep a React 18
44
+ app on 18. But do name both, because npm will then move each to the newest release in its range. In
45
+ React 19, a `react-dom` at a different version from `react` won't load.
44
46
 
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.
47
+ `@rudra-js/core` and `react` are peer dependencies. Your spec comes from your copy of core. This
48
+ package's elements have to come from your app's copy of React. If two copies of core have
49
+ different types, `spec` or another prop can fail its type check. With two copies of React, React
50
+ can refuse to render the component tree.
49
51
 
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.
52
+ `@rudra-js/attested` is core's peer dependency. npm and pnpm will add it for you, but Yarn won't.
53
+ Yarn will move a bare name to its latest version though, so with Yarn, leave `react` and
54
+ `react-dom` out of the command if your app already has React.
53
55
 
54
56
  ```tsx
55
57
  import { RudraComponent } from '@rudra-js/react';
@@ -57,12 +59,13 @@ import { RudraComponent } from '@rudra-js/react';
57
59
  <RudraComponent spec={spec} products={catalog} locale="en-GB" />;
58
60
  ```
59
61
 
60
- `spec` is what `createComponentGenerator().generate()` returned. `products` is your catalog.
62
+ `spec` is the value returned by `createComponentGenerator().generate()`. `products` is your catalog.
61
63
 
62
64
  ## What comes from where
63
65
 
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.
66
+ It's up to the model to decide the layout of the block and the words in it. All of the facts about a
67
+ product will be read from your catalog when the page is served. Anything the model wrote will be
68
+ rendered as escaped text.
66
69
 
67
70
  | Decided by the model | Decided by your catalog |
68
71
  | ------------------------------------------------------------------- | ----------------------- |
@@ -70,41 +73,44 @@ read from your catalog when the page is served. Whatever the model wrote is rend
70
73
  | Tone, headline, the words in each block | Every price |
71
74
  | Only in per-shopper mode: which products, and how each is described | Every image and link |
72
75
 
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
76
+ By default you get cohort mode: shoppers who look alike share a spec. In this mode, core will fill
77
+ in the products and their order for each request. It will also write the reason under each product
78
+ and drop any badge the model wrote. If you set `generation: 'per-shopper'`, the model will make
79
+ those picks itself. In both modes, it's the model that chooses the product a hero names. For the
80
+ full split, see
77
81
  [What the model decides, by mode](https://github.com/clivedsouza1010/rudra-js/tree/main/packages/core#what-the-model-decides-by-mode).
78
82
 
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.
83
+ There is no field in the spec for a title, a price, an image or a URL. Instead, the component will
84
+ look up each product in `products` by a SKU that core has already checked.
81
85
 
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.
86
+ **Validate `products` with `productSchema` from `@rudra-js/core`**. It's the same schema that the
87
+ candidate products you sent to core have already passed. This prop is a second way for data to reach
88
+ the page.
84
89
 
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.
90
+ `imageUrl` will end up in an `<img src>`. `productSchema` rejects a protocol-relative URL such as
91
+ `//evil.example/pixel.png`. It also rejects a `data:` URL. Because it reads a path the way a browser
92
+ does, it will also reject `/\evil.example/pixel.png` and the same trick written with a tab or a
93
+ newline in it. In a browser, all of these resolve to someone else's host. React will block a
94
+ `javascript:` URL on its own, but none of these.
90
95
 
91
- If a price isn't a finite number, rendering throws an error.
96
+ If a price isn't a finite number, rendering will throw an error.
92
97
 
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.
98
+ Any row with `isInStock: false` counts as a row that isn't there. Its cards are dropped. If a hero
99
+ names it, the hero will lose its link. If a bundle has it as a member, the bundle will disappear.
100
+ Core will only name products that were in stock in the data you sent it. But this catalog is read
101
+ later, so it may be the fresher of the two.
96
102
 
97
103
  ## Styling
98
104
 
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.
105
+ This package doesn't ship any CSS. Without styles, the block will render as a single run-on line.
106
+ All of the card elements are inline. This means a card's parts sit on a single line with spaces in
107
+ between. There is nothing to separate a card from the next. You style it from there.
102
108
 
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.
109
+ For a working stylesheet, see `examples/shop/public/demo-styles.css`. It uses just the classes in
110
+ the table below. You can copy it as a starting point. It isn't a supported API though. The example
111
+ shop applies it by default. You can use `?styles=off` to see the raw markup.
106
112
 
107
- Every element the component renders carries a class. Here is the full list:
113
+ All of the elements the component renders have a class. Here is the full list:
108
114
 
109
115
  | Where | Classes |
110
116
  | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -120,26 +126,26 @@ Every element the component renders carries a class. Here is the full list:
120
126
  A few notes on those:
121
127
 
122
128
  - `.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
129
+ - `.rudra-bundle__label` holds your name for the set. It will only show up for a bundle you gave a
124
130
  `label`.
125
- - Your `className` is added alongside `rudra`, never in place of it, so the child classes keep
131
+ - Your `className` will be added alongside `rudra`, never in place of it, to keep the child classes
126
132
  working.
127
- - `.rudra-carousel__track` is meant to scroll horizontally, so give it `overflow-x: auto`. No
128
- JavaScript scrolls it for you.
133
+ - `.rudra-carousel__track` is meant to scroll horizontally, so give it `overflow-x: auto`. There is
134
+ no JavaScript to scroll it for you.
129
135
  - `.rudra-card--featured` is applied alongside `.rudra-card`. Write the `.rudra-card--featured { ... }`
130
- rule after the base rule so it layers on top.
136
+ rule after the base rule to layer it on top.
131
137
 
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.
138
+ `.rudra-banner__cta` and `.rudra-bundle__cta` are plain text. They are not links or buttons. Don't
139
+ style them as buttons unless a renderer of your own wraps them in one. For the hero, the call to
140
+ action sits inside the product link. This means the hero's call to action is clickable.
135
141
 
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.
142
+ All of the cards in a carousel are links. If you tab from card to card, the track will scroll. This
143
+ way you can use the carousel by keyboard, even with the scrollbar hidden.
138
144
 
139
145
  ### Attributes
140
146
 
141
- The markup also carries what the model decided. You can hang styling or analytics off these
142
- attributes.
147
+ In the markup you will also find the model's decisions. You can use these attributes for styling or
148
+ analytics.
143
149
 
144
150
  | Attribute | On | Value |
145
151
  | ------------------------ | ------------------------------ | ---------------------------------------------------------- |
@@ -151,9 +157,9 @@ attributes.
151
157
  | `data-rudra-sku` | a card, hero link, bundle item | The product, for click attribution |
152
158
  | `data-rudra-basis` | a card | Why the product was picked: `most_viewed`, `popular`, … |
153
159
 
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
156
- tells a visitor what you run and when it's failing:
160
+ `data-rudra-source` is always there. This lets you read the cache hit rate and the fallback share
161
+ directly from a rendered page. Anything more specific will only appear under `hasDiagnostics`,
162
+ because it tells a visitor what you run and when it's failing:
157
163
 
158
164
  | Attribute | On | Value |
159
165
  | ----------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------- |
@@ -164,8 +170,8 @@ tells a visitor what you run and when it's failing:
164
170
 
165
171
  ## Replacing a renderer
166
172
 
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.
173
+ You can swap any block's renderer for a component from your design system. Only the drawing changes.
174
+ Neither the spec nor the block kinds the model picks from will change.
169
175
 
170
176
  ```tsx
171
177
  import { RudraComponent, extendRegistry } from '@rudra-js/react';
@@ -177,9 +183,10 @@ const registry = extendRegistry({
177
183
  <RudraComponent spec={spec} products={catalog} registry={registry} />;
178
184
  ```
179
185
 
180
- A grid, carousel or bundle block with nothing left in your catalog is dropped before any renderer
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.
186
+ If a grid, carousel or bundle block has nothing left in your catalog, it will be dropped before any
187
+ renderer runs, yours or ours. But the rest of the blocks will still render. Banner and copy blocks
188
+ will always reach the renderer because they don't depend on the catalog. Heroes without a headline
189
+ or body will need an in-stock SKU in your catalog to get there.
183
190
 
184
191
  ## Props
185
192
 
@@ -198,33 +205,36 @@ depend on the catalog, so they always reach the renderer.
198
205
 
199
206
  ### What `products` may be
200
207
 
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>`.
208
+ `products` has to be a list of products, or anything keyed by SKU that answers `get(sku)` and
209
+ `has(sku)`. This means you can use a `Map` or your own index. The renderers will only ever call
210
+ `get`. `has` is how the component tells a keyed catalog from a list. If your catalog is too big to
211
+ copy into a `Map` on every request, pass a view of your store instead. This prop is typed as a
212
+ `ReadonlyMap`. In TypeScript that means your view has to either implement `ReadonlyMap` or be cast
213
+ with `as unknown as ReadonlyMap<string, Product>`.
206
214
 
207
- A `Map` from a worker or a `node:vm` sandbox works too, even though it fails `instanceof Map`.
215
+ You can also pass a `Map` from a worker or a `node:vm` sandbox, even though it fails
216
+ `instanceof Map`.
208
217
 
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.
218
+ Anything that is neither a list nor keyed will be refused right away, with an error that names the
219
+ prop. This covers a `Set` of products, a plain object or a `Map` that has been through JSON.
211
220
 
212
- When there's nothing left to show, the component renders nothing at all. That covers a spec with no
213
- blocks, and a spec whose products have all left your catalog since it was generated.
221
+ If there's nothing left to show, the component will render nothing at all. This covers a spec with
222
+ no blocks. It also covers a spec whose products have all left your catalog since it was generated.
214
223
 
215
224
  ### What `bundles` is
216
225
 
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.
226
+ `bundles` is a list of the sets your shop sells together, just like `products` is your catalog. As
227
+ for the model, it will only ask for a bundle block. It never invents a set and it never sees a
228
+ price.
219
229
 
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.
230
+ You offer the sets, but it's core that picks which set fills each bundle block. It does this in
231
+ `reconcileSpec` as the page is served, after the spec was generated. It goes by what this shopper
232
+ has in their basket, has looked at or is browsing right now. After this, the spec will carry the id
233
+ of the set core picked. You supply the rest with this prop (the set's members, its price, the
234
+ currency of that price and your name for it) to give the component something to draw.
225
235
 
226
- **Validate `bundles` with `bundleSchema` from `@rudra-js/core`, and pass the same list you sent to
227
- `parseTrackingInput`.** Core checked that list before it picked a set:
236
+ **Validate `bundles` with `bundleSchema` from `@rudra-js/core` and pass the same list you sent to
237
+ `parseTrackingInput`.** It's the list core checked before it picked a set:
228
238
 
229
239
  - every member is in stock
230
240
  - no member is one the shopper disliked
@@ -232,8 +242,9 @@ the currency of that price, and your name for it.
232
242
  - no product shows up twice
233
243
  - the whole set fits in what's left of `context.maxItems`, the most products the component may show
234
244
 
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.
245
+ After that, core will only give the renderer the id it chose. If you pass a stale or different list
246
+ here, you'll draw a set that none of those checks saw, under an id that was checked against another
247
+ list.
237
248
 
238
249
  ## Licence
239
250
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rudra-js/react",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
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,11 +54,11 @@
54
54
  "access": "public"
55
55
  },
56
56
  "peerDependencies": {
57
- "@rudra-js/core": "^0.9.0",
57
+ "@rudra-js/core": "^0.9.1",
58
58
  "react": "^18.2.0 || ^19.0.0"
59
59
  },
60
60
  "devDependencies": {
61
- "@rudra-js/core": "0.9.0",
61
+ "@rudra-js/core": "0.9.1",
62
62
  "@types/react": "^19.3.0",
63
63
  "@types/react-dom": "^19.3.0",
64
64
  "react": "^19.3.1",