@reblu/site-blocks 0.7.0 → 0.8.0
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/CHANGELOG.md +11 -0
- package/README.md +63 -43
- package/dist/index.cjs +19 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +19 -18
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @reblu/site-blocks
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- c40434a: Add a distributable controlled phone input and E.164 normalization. Preserve and validate the additive checkout `details.field` in typed API errors so CheckoutForm can mark the rejected phone while retaining generic error fallback.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [c40434a]
|
|
12
|
+
- @reblu/site-ui@0.7.0
|
|
13
|
+
|
|
3
14
|
## 0.7.0
|
|
4
15
|
|
|
5
16
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -10,18 +10,18 @@ a different-looking site for every tenant, with zero forks.
|
|
|
10
10
|
|
|
11
11
|
## What's inside
|
|
12
12
|
|
|
13
|
-
| Export
|
|
14
|
-
|
|
|
15
|
-
| `readingTime`, `formatPrice`, `buildShareUrls`, `markdownToSafeHtml`
|
|
16
|
-
| `sanitizeHtml`
|
|
17
|
-
| `MarkdownRenderer`
|
|
18
|
-
| `Treatment`, `Specialty`, `ProfileCard`, `ProductCard`
|
|
19
|
-
| `ShopFilters`
|
|
20
|
-
| `CartLineList`, `CartSummary`, `AddedToCartToast`, `CheckoutLoginPrompt`, `CheckoutForm`, `OrderConfirmation` | `.`
|
|
21
|
-
| `useCollection`
|
|
22
|
-
| `Collection`
|
|
23
|
-
| `ShareButtons`
|
|
24
|
-
| `AddToCartButton`, `CartIcon`
|
|
13
|
+
| Export | Entry | Purpose |
|
|
14
|
+
| ------------------------------------------------------------------------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
15
|
+
| `readingTime`, `formatPrice`, `buildShareUrls`, `markdownToSafeHtml` | `.` | Pure, framework-agnostic helpers (RSC-safe). |
|
|
16
|
+
| `sanitizeHtml` | `.` | Sanitises an HTML string before `dangerouslySetInnerHTML` — the **HTML boundary**. See "Security boundaries" below. |
|
|
17
|
+
| `MarkdownRenderer` | `.` | Canonical Blog renderer: `marked` → **hast-util-sanitize** (XSS) → component map, with a YouTube-domain allowlist. RSC-safe. |
|
|
18
|
+
| `Treatment`, `Specialty`, `ProfileCard`, `ProductCard` | `.` | Presentational organisms over the SITE API contracts. RSC-safe. |
|
|
19
|
+
| `ShopFilters` | `.` | Plug-and-play shop filter chrome over `GET /api/site/shop/facets`. RSC-safe. See "Shop filters" below. |
|
|
20
|
+
| `CartLineList`, `CartSummary`, `AddedToCartToast`, `CheckoutLoginPrompt`, `CheckoutForm`, `OrderConfirmation` | `.` | Cart & checkout organisms. RSC-safe by construction. See "Cart & checkout" below. |
|
|
21
|
+
| `useCollection` | `./client` | Headless filterable/sortable/paginated collection hook (facets, category, tag, search). Serves both page and blog indexes. |
|
|
22
|
+
| `Collection` | `./client` | Token-driven presentational grid over `useCollection`. |
|
|
23
|
+
| `ShareButtons` | `./client` | Social share (X / LinkedIn / WhatsApp / copy). |
|
|
24
|
+
| `AddToCartButton`, `CartIcon` | `./client` | Add-to-cart feedback (REB-837) — **moved here from `.`**, see below. |
|
|
25
25
|
|
|
26
26
|
## Boundary
|
|
27
27
|
|
|
@@ -44,7 +44,7 @@ Calle Mayor 1
|
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
renders as one paragraph reading `Calle Mayor 1 28001 Madrid`. This is standard
|
|
47
|
-
CommonMark — a lone newline is a
|
|
47
|
+
CommonMark — a lone newline is a _soft_ break, i.e. whitespace — and it is what
|
|
48
48
|
this renderer has done since it replaced the interim, non-standard one that
|
|
49
49
|
emitted a `<br>` for every newline. **Text written under the old behaviour reads
|
|
50
50
|
differently now**, so addresses, signature blocks and enumerated clauses typed on
|
|
@@ -52,11 +52,11 @@ consecutive lines are worth re-reading.
|
|
|
52
52
|
|
|
53
53
|
To force a visible break, use any of:
|
|
54
54
|
|
|
55
|
-
| Want
|
|
56
|
-
|
|
|
57
|
-
| A new paragraph
|
|
58
|
-
| A hard break inside one paragraph | Two spaces at the end of the first line
|
|
59
|
-
| A hard break, visibly
|
|
55
|
+
| Want | Write |
|
|
56
|
+
| --------------------------------- | ------------------------------------------- |
|
|
57
|
+
| A new paragraph | A blank line between the two lines |
|
|
58
|
+
| A hard break inside one paragraph | Two spaces at the end of the first line |
|
|
59
|
+
| A hard break, visibly | A trailing `\` at the end of the first line |
|
|
60
60
|
|
|
61
61
|
### `hidden` hides a block — it does not withhold it
|
|
62
62
|
|
|
@@ -191,16 +191,16 @@ dependency of their own. Wire them to `@reblu/site-next/cart`'s `useCart()`
|
|
|
191
191
|
and the `./shop-route-handlers` Route Handlers (see that package's README
|
|
192
192
|
for the wiring steps and a full example app).
|
|
193
193
|
|
|
194
|
-
| Component
|
|
195
|
-
|
|
|
196
|
-
| `AddToCartButton`
|
|
197
|
-
| `CartIcon`
|
|
198
|
-
| `AddedToCartToast`
|
|
199
|
-
| `CartLineList`
|
|
200
|
-
| `CartSummary`
|
|
201
|
-
| `CheckoutLoginPrompt` | `.`
|
|
202
|
-
| `CheckoutForm`
|
|
203
|
-
| `OrderConfirmation`
|
|
194
|
+
| Component | Entry | Props it needs | RSC-safe without callbacks? |
|
|
195
|
+
| --------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
196
|
+
| `AddToCartButton` | `./client` | `onAddToCart?`, `disabled?`, `outOfStock?`, `pending?`, `error?`, `item?`, `successDurationMs?`, `onAdded?`, `onError?` | No — carries a built-in confirmed/error timer (`useState`/`useEffect`), see below |
|
|
197
|
+
| `CartIcon` | `./client` | `itemCount`, `href`, `hydrated?`, `bumpOnAdd?` | No — the bump animation needs state, see below |
|
|
198
|
+
| `AddedToCartToast` | `.` | `productName`, `cartHref`, `status?`, `quantity?`, `imageUrl?` | Yes |
|
|
199
|
+
| `CartLineList` | `.` | `items`, `onQuantityChange?`, `onRemove?`, `liveMessage?`, `notice?` | Yes |
|
|
200
|
+
| `CartSummary` | `.` | `subtotal`, `discount?`, `shipping?`, `shippingEnabled?`, `total`, `notice?` | Yes |
|
|
201
|
+
| `CheckoutLoginPrompt` | `.` | `open`, `email`, `onEmailChange`, `onClose`, `onSubmitEmail`, `onContinueAsGuest` | Yes (renders `null` when `open` is false) |
|
|
202
|
+
| `CheckoutForm` | `.` | `contact`, `onContactChange`, `discountCode`, `onDiscountCodeChange`, `onApplyDiscount`, `onRemoveDiscount`, `onSubmit` | Yes |
|
|
203
|
+
| `OrderConfirmation` | `.` | `status`, `order`, `cartHref?`, `onRecheck?` | Yes |
|
|
204
204
|
|
|
205
205
|
**REB-837 breaking move**: `AddToCartButton` and `CartIcon` now carry real
|
|
206
206
|
internal state (the confirmed/error timer, the bump-on-add guard) and ship
|
|
@@ -316,6 +316,26 @@ discount-code flow. Render them via `CheckoutForm`'s `children` slot
|
|
|
316
316
|
(rendered between the contact and discount sections) if your shop has
|
|
317
317
|
shipping enabled.
|
|
318
318
|
|
|
319
|
+
For phone normalization, pass `phoneRegion` from the tenant's configured country:
|
|
320
|
+
the controlled `contact.phone` callback receives E.164 when valid and raw text
|
|
321
|
+
when incomplete. On a checkout API rejection pass the parsed response body to
|
|
322
|
+
`checkoutError` (not the transport error's prefixed `message`). Only an
|
|
323
|
+
`INVALID_INPUT` body with `details.field === 'customerPhone'` and a nonblank
|
|
324
|
+
`error` marks the phone field. Unknown/missing/malformed details show the
|
|
325
|
+
existing generic code message instead; other code paths are unchanged. Clear
|
|
326
|
+
the response on edit/retry in the consuming SITE to remove a stale server error.
|
|
327
|
+
|
|
328
|
+
```tsx
|
|
329
|
+
<CheckoutForm {...checkoutProps} phoneRegion='ES' checkoutError={checkoutErrorBody} />
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Pass `error.body` after catching `error instanceof ApiError` from
|
|
333
|
+
`@reblu/site-client`. Its `body` parser now types and validates the optional
|
|
334
|
+
string `details.field`; never parse the transport-prefixed `error.message`.
|
|
335
|
+
For malformed bodies (which parse as `undefined`), pass a generic body such as
|
|
336
|
+
`error.body ?? { error: 'Checkout failed' }` to show the fallback banner.
|
|
337
|
+
An external SITE (including Dermamedicin) must adopt this wiring separately.
|
|
338
|
+
|
|
319
339
|
`validate-cart`'s rejection path (out-of-stock, price-changed, empty cart)
|
|
320
340
|
surfaces through `CheckoutForm`'s own error-display conventions: pass the
|
|
321
341
|
first `ShopCartValidationError.message` from `client.shop.validateCart(...)`
|
|
@@ -330,17 +350,17 @@ directly — `loading`/`paid` are drop-in aliases for the original
|
|
|
330
350
|
`polling`/`found` states, so existing callers on the pre-REB-837 3-state API
|
|
331
351
|
(`'polling' | 'found' | 'timeout'`) are completely unaffected.
|
|
332
352
|
|
|
333
|
-
| `status`
|
|
334
|
-
|
|
|
335
|
-
| `loading` / `polling` | «Confirmando tu pedido…»
|
|
336
|
-
| `paid` / `found`
|
|
337
|
-
| `failed`
|
|
338
|
-
| `refunded`
|
|
339
|
-
| `partially_refunded`
|
|
340
|
-
| `pending_timeout`
|
|
341
|
-
| `not_found`
|
|
342
|
-
| `error`
|
|
343
|
-
| `timeout` (legacy)
|
|
353
|
+
| `status` | Default copy | Order details shown | Extra UI |
|
|
354
|
+
| --------------------- | ----------------------------------------------- | ---------------------------------------------------- | ----------------------------------------------------- |
|
|
355
|
+
| `loading` / `polling` | «Confirmando tu pedido…» | — | — |
|
|
356
|
+
| `paid` / `found` | «¡Pedido confirmado!» | Full summary (items, total, confirmation-email line) | — |
|
|
357
|
+
| `failed` | «El pago no se ha completado» | None | "Volver al carrito" link when `cartHref` is given |
|
|
358
|
+
| `refunded` | «Este pedido ha sido reembolsado» | Order number only (neutral reference) | — |
|
|
359
|
+
| `partially_refunded` | «…reembolsado parcialmente» | Order number only | — |
|
|
360
|
+
| `pending_timeout` | «Pedido recibido. Estamos confirmando el pago…» | Order number only | "Volver a comprobar" button when `onRecheck` is given |
|
|
361
|
+
| `not_found` | «No encontramos ningún pedido» | None | — |
|
|
362
|
+
| `error` | «Ha ocurrido un error» | None | "Volver a comprobar" button when `onRecheck` is given |
|
|
363
|
+
| `timeout` (legacy) | «Pago recibido» — UNCHANGED | Full summary (unchanged pre-REB-837 behavior) | — |
|
|
344
364
|
|
|
345
365
|
**REB-837 review fix**: the full order summary (items, total, "te hemos
|
|
346
366
|
enviado un email de confirmación a…") renders ONLY for `paid`/`found` and
|
|
@@ -363,10 +383,10 @@ recipe.
|
|
|
363
383
|
Two sinks, two different helpers, in two different packages — do not reach for
|
|
364
384
|
one where the other belongs:
|
|
365
385
|
|
|
366
|
-
| Sink
|
|
367
|
-
|
|
|
368
|
-
| HTML rendered via `dangerouslySetInnerHTML` | `sanitizeHtml` (strip/allowlist sanitiser)
|
|
369
|
-
| `<script type="application/ld+json">` body
|
|
386
|
+
| Sink | Helper | Package |
|
|
387
|
+
| ------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------ |
|
|
388
|
+
| HTML rendered via `dangerouslySetInnerHTML` | `sanitizeHtml` (strip/allowlist sanitiser) | `@reblu/site-blocks` (here) |
|
|
389
|
+
| `<script type="application/ld+json">` body | `jsonLdScriptContent` / `<JsonLd>` (escaper) | [`@reblu/site-next/json-ld`](../site-next#structured-data-json-ld) |
|
|
370
390
|
|
|
371
391
|
A `<script>` body is HTML's **raw-text** context: the parser ends it on the
|
|
372
392
|
first `</script` byte sequence regardless of whether that sequence sits inside
|
package/dist/index.cjs
CHANGED
|
@@ -13990,6 +13990,7 @@ function CheckoutLoginPrompt({
|
|
|
13990
13990
|
|
|
13991
13991
|
// src/components/checkout-form.tsx
|
|
13992
13992
|
var import_utils12 = require("@reblu/site-ui/utils");
|
|
13993
|
+
var import_phone_input = require("@reblu/site-ui/phone-input");
|
|
13993
13994
|
var import_jsx_runtime12 = require("react/jsx-runtime");
|
|
13994
13995
|
var DEFAULT_LABELS6 = {
|
|
13995
13996
|
contactTitle: "Datos de contacto",
|
|
@@ -14011,6 +14012,8 @@ function CheckoutForm({
|
|
|
14011
14012
|
contact,
|
|
14012
14013
|
onContactChange,
|
|
14013
14014
|
contactErrors,
|
|
14015
|
+
phoneRegion,
|
|
14016
|
+
checkoutError,
|
|
14014
14017
|
discountCode,
|
|
14015
14018
|
onDiscountCodeChange,
|
|
14016
14019
|
onApplyDiscount,
|
|
@@ -14029,6 +14032,10 @@ function CheckoutForm({
|
|
|
14029
14032
|
className
|
|
14030
14033
|
}) {
|
|
14031
14034
|
const t = { ...DEFAULT_LABELS6, ...labels };
|
|
14035
|
+
const details = checkoutError?.details;
|
|
14036
|
+
const rejectedPhone = checkoutError?.code === "INVALID_INPUT" && typeof details === "object" && details !== null && !Array.isArray(details) && "field" in details && details.field === "customerPhone" && typeof checkoutError.error === "string" && checkoutError.error.trim().length > 0;
|
|
14037
|
+
const phoneError = contactErrors?.phone ?? (rejectedPhone ? checkoutError.error : void 0);
|
|
14038
|
+
const generalError = checkoutError && !rejectedPhone ? getShopErrorMessage(typeof checkoutError.code === "string" ? checkoutError.code : void 0) : null;
|
|
14032
14039
|
function patchContact(patch3) {
|
|
14033
14040
|
onContactChange({ ...contact, ...patch3 });
|
|
14034
14041
|
}
|
|
@@ -14089,27 +14096,21 @@ function CheckoutForm({
|
|
|
14089
14096
|
),
|
|
14090
14097
|
contactErrors?.email && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "text-xs text-destructive", children: contactErrors.email })
|
|
14091
14098
|
] }),
|
|
14092
|
-
/* @__PURE__ */ (0, import_jsx_runtime12.
|
|
14093
|
-
|
|
14094
|
-
|
|
14095
|
-
"
|
|
14096
|
-
|
|
14097
|
-
|
|
14098
|
-
|
|
14099
|
-
|
|
14100
|
-
|
|
14101
|
-
|
|
14102
|
-
|
|
14103
|
-
"w-full rounded-lg border bg-background px-3 py-2 text-sm transition focus:outline-none focus:ring-2 focus:ring-primary/20",
|
|
14104
|
-
contactErrors?.phone ? "border-destructive" : "border-border focus:border-primary"
|
|
14105
|
-
)
|
|
14106
|
-
}
|
|
14107
|
-
),
|
|
14108
|
-
contactErrors?.phone && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "text-xs text-destructive", children: contactErrors.phone })
|
|
14109
|
-
] })
|
|
14099
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "space-y-1", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
14100
|
+
import_phone_input.PhoneInput,
|
|
14101
|
+
{
|
|
14102
|
+
id: "checkout-form-phone",
|
|
14103
|
+
label: t.phoneLabel,
|
|
14104
|
+
region: phoneRegion,
|
|
14105
|
+
value: contact.phone ?? "",
|
|
14106
|
+
onChange: (phone) => patchContact({ phone }),
|
|
14107
|
+
error: phoneError
|
|
14108
|
+
}
|
|
14109
|
+
) })
|
|
14110
14110
|
] })
|
|
14111
14111
|
] }),
|
|
14112
14112
|
children2,
|
|
14113
|
+
generalError ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { role: "alert", className: "text-sm text-destructive", children: generalError }) : null,
|
|
14113
14114
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("section", { className: "space-y-3", children: [
|
|
14114
14115
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("h2", { className: "border-b border-border pb-2 text-lg font-semibold", children: t.discountTitle }),
|
|
14115
14116
|
appliedDiscount ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex items-center justify-between rounded-lg border border-green-200 bg-green-50 px-4 py-3 dark:border-green-800 dark:bg-green-900/20", children: [
|