@rudra-js/react 0.2.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.
Files changed (2) hide show
  1. package/README.md +77 -87
  2. package/package.json +4 -4
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 Server
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
- 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.
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: 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.
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,15 +25,13 @@ 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
- This split is what a rendered component rests on. The model decides how things
35
- are arranged and what the words are; every fact about a product is read from
36
- your catalog when the page is served, and the model's own words are rendered as
37
- text and escaped by React.
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.
38
35
 
39
36
  | Decided by the model | Decided by your catalog |
40
37
  | ------------------------------------------------------------------- | ----------------------- |
@@ -42,37 +39,32 @@ text and escaped by React.
42
39
  | Tone, headline, the words in each block | Every price |
43
40
  | Only in per-shopper mode: which products, and how each is described | Every image and link |
44
41
 
45
- In the default cohort mode the products, their order and the reason under each
46
- are filled in per request, not by the model, and a badge the model wrote is
47
- dropped. The model chooses those only under `generation: 'per-shopper'`. The
48
- one thing it still picks in either mode is the product a hero names, and the
49
- full split is in
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
50
46
  [What the model decides, by mode](https://github.com/clivedsouza1010/rudra-js/tree/main/packages/core#what-the-model-decides-by-mode).
51
47
 
52
- The specification has no field carrying a title, a price, an image or a URL.
53
- Product facts are resolved at render time from `products`, keyed by a SKU
54
- reconciliation has already checked. Everything the model writes is rendered as
55
- text and escaped by React.
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.
56
50
 
57
- **Validate `products` with `productSchema` from `@rudra-js/core`** the same
58
- schema your candidates already passed. It is a second door into the framework:
59
- `imageUrl` lands in an `<img src>`, and `productSchema` is what rejects a
60
- protocol-relative `//evil.example/pixel.png` or a `data:` URL. React neutralises
61
- a `javascript:` URL by itself, but not those. A price that is not a finite
62
- number throws rather than rendering the product as free.
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.
63
56
 
64
57
  ## Styling
65
58
 
66
- The package ships no CSS, on purpose a stylesheet would fight whatever your
67
- site already has. Unstyled, the block renders as a run-on line: every card
68
- element is inline, so titles and prices sit together with no separation. That is
69
- 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.
70
62
 
71
- `examples/shop/public/demo-styles.css` is a working stylesheet written against
72
- nothing but the table below copy it as a starting point. The example shop
73
- applies it by default; `?styles=off` shows the raw markup.
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.
74
66
 
75
- Every element it emits carries a class, and this is all of them:
67
+ Every element we emit carries a class. Here's all of them:
76
68
 
77
69
  | Where | Classes |
78
70
  | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -85,17 +77,18 @@ Every element it emits carries a class, and this is all of them:
85
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` |
86
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` |
87
79
 
88
- `.rudra__rationale` only appears under `hasDiagnostics`. `.rudra-bundle__label`
89
- is your own name for the set, and appears only for a bundle you gave a `label`.
90
- `className` is added alongside `rudra` rather than replacing it, so the child
91
- classes keep working.
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.
92
84
 
93
- `.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
94
- rule rather than instead of it.
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.
95
88
 
96
89
  ### Attributes
97
90
 
98
- The same markup carries what the model decided, for styling and for analytics.
91
+ The same markup carries what the model decided, so you can hang styling or analytics off it.
99
92
 
100
93
  | Attribute | On | Value |
101
94
  | ------------------------ | ------------------------------ | ---------------------------------------------------------- |
@@ -105,11 +98,11 @@ The same markup carries what the model decided, for styling and for analytics.
105
98
  | `data-rudra-banner-tone` | a banner | A banner's own tone, a different vocabulary from the above |
106
99
  | `data-rudra-columns` | a grid | The column count the model chose |
107
100
  | `data-rudra-sku` | a card, hero link, bundle item | The product, for click attribution |
108
- | `data-rudra-basis` | a card | Why the product was picked `most_viewed`, `popular`, … |
101
+ | `data-rudra-basis` | a card | Why the product was picked: `most_viewed`, `popular`, … |
109
102
 
110
- `data-rudra-source` is public on purpose: hit rate and fallback share can be
111
- read straight off a rendered page. Everything more specific appears only under
112
- `hasDiagnostics`, since it tells a visitor what you run and when it is failing:
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:
113
106
 
114
107
  | Attribute | On | Value |
115
108
  | ----------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------- |
@@ -120,9 +113,9 @@ read straight off a rendered page. Everything more specific appears only under
120
113
 
121
114
  ## Replacing a renderer
122
115
 
123
- Swap any block for your own design-system component. The model is not involved
124
- and the specification does not change, so this gives it no new ability it can
125
- still only choose from the same fixed vocabulary.
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.
126
119
 
127
120
  ```tsx
128
121
  import { RudraComponent, extendRegistry } from '@rudra-js/react';
@@ -143,50 +136,47 @@ const registry = extendRegistry({
143
136
  | `bundles` | The sets your shop sells together. Only needed if a spec can carry a bundle block. |
144
137
  | `registry` | Replace some or all block renderers. |
145
138
  | `hrefForSku` | Defaults to `/product/{sku}`, URL-encoded. |
146
- | `formatPrice` | Defaults to `Intl.NumberFormat`, which knows each currency's own number of decimal places. It formats products, not bundles. |
139
+ | `formatPrice` | Defaults to `Intl.NumberFormat`, which knows how many decimal places each currency wants. It formats products, not bundles. |
147
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. |
148
- | `locale` | Punctuates prices. Defaults to the **server's** locale, which is rarely the shopper's pass it if you serve more than one. |
149
- | `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. |
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. |
150
143
  | `className` | Added alongside `rudra`. |
151
144
 
152
145
  ### What `products` may be
153
146
 
154
- A list of products, or anything keyed by SKU that answers `get(sku)` and
155
- `has(sku)` a `Map`, or your own index. Those two methods are the only ones the
156
- renderers call, so a shop with a catalog too large to copy into a `Map` on every
157
- request can pass a view over its own store instead.
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.
158
151
 
159
- The check is on those two methods rather than on `instanceof Map`, which is
160
- per-realm: a `Map` arriving from a worker or a `node:vm` sandbox is a perfectly
161
- good catalog and fails `instanceof`. Anything that is neither a list nor keyed —
162
- a `Set` of products, a plain object, a `Map` that has been through JSON — is
163
- refused on the spot with an error naming the prop, rather than quietly rendering
164
- an empty recommendation area.
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.
165
154
 
166
- The component renders nothing at all when there is nothing to show a spec with
167
- no blocks, or one whose every product has left your catalog since it was
168
- generated. An empty recommendation area, or a headline over an empty box, takes
169
- up space and tells the shopper the page is broken.
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.
158
+
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.
170
163
 
171
164
  ### What `bundles` is
172
165
 
173
- The sets your shop sells together, the same way `products` is your catalog. The
174
- model only asks for a bundle block; it never invents one, and it never sees a
175
- price.
176
-
177
- You offer the sets, and the framework picks which one fills each block. It
178
- picks when the page is served after the spec was generated, not before —
179
- inside `reconcileSpec`, from what this shopper has in their basket, has looked
180
- at, or is browsing now. The spec then carries the id it picked, and this prop
181
- supplies that set's members, its price, the currency that price is in and your
182
- name for it so the component can draw it.
183
-
184
- **Validate `bundles` with `bundleSchema` from `@rudra-js/core`, and pass the
185
- same list you sent to `parseTrackingInput`.** Core checked that list every
186
- member in stock, none of them disliked, no repeats, and the whole set inside
187
- the item budget — and then hands the renderer nothing but the id it chose. A
188
- stale or different list here draws a set none of those checks ever saw, under
189
- 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.
190
180
 
191
181
  ## Licence
192
182
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rudra-js/react",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Renders a rudra component specification as React Server Components",
5
5
  "keywords": [
6
6
  "react",
@@ -51,11 +51,11 @@
51
51
  "access": "public"
52
52
  },
53
53
  "peerDependencies": {
54
- "@rudra-js/core": "^0.2.0",
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.0",
58
+ "@rudra-js/core": "0.3.1",
59
59
  "@types/react": "^19.2.18",
60
60
  "@types/react-dom": "^19.2.7",
61
61
  "react": "^19.2.8",