@reblu/site-blocks 0.29.0 → 0.31.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 +31 -0
- package/README.md +217 -6
- package/dist/client.cjs +632 -0
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +105 -3
- package/dist/client.d.ts +105 -3
- package/dist/client.js +629 -0
- package/dist/client.js.map +1 -1
- package/dist/full-invoice-fields-DMX0sjPG.d.cts +140 -0
- package/dist/full-invoice-fields-DMX0sjPG.d.ts +140 -0
- package/dist/index.cjs +859 -342
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +202 -131
- package/dist/index.d.ts +202 -131
- package/dist/index.js +844 -342
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/dist/_heading-BsNbc1wc.d.cts +0 -4
- package/dist/_heading-BsNbc1wc.d.ts +0 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# @reblu/site-blocks
|
|
2
2
|
|
|
3
|
+
## 0.31.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 9a83295: Guest course checkout (REB-1264). Additive only: every existing export behaves as before.
|
|
8
|
+
- `@reblu/site-contracts` exports `AcademyCheckoutRequest`, `AcademyCheckoutBilling`, `AcademyCheckoutSession` and `AcademyCheckoutErrorCode` (`POST /api/site/academy/checkout`).
|
|
9
|
+
- `@reblu/site-client` adds `client.academy.checkout(body, options?)` → `{ url, sessionId }` (Stripe URL re-checked like `shop.checkout()`); a refusal throws `AcademyCheckoutError` (an `ApiError`) with the stable `code` and `details`.
|
|
10
|
+
- `@reblu/site-next` adds `createAcademyCheckoutRoute()` in `./route-handlers`: forwards `x-reblu-visitor-ip`, fills `successUrl`/`cancelUrl` on `coursePath` and re-checks the Stripe URL. `createShopCheckoutRoute` now shares that code, unchanged in behaviour.
|
|
11
|
+
- `@reblu/site-blocks` adds the drop-in `CourseGuestCheckoutForm` (`./client`): name, surname, email, terms box, the course's consent box, optional full invoice, total and pay notice; it gives way to the request form on `ENROLLMENT_REQUEST_REQUIRED`. The root entry adds `courseSellsAsGuest` and `toAcademyCheckoutInvoiceFields`.
|
|
12
|
+
|
|
13
|
+
- faf78e8: Checkout pickup point (REB-1276). `CheckoutDeliveryChoice`: switching to pickup with exactly one pickup point now selects it (`pickupLocationId` is sent); with two or more points none is preselected, as before. New `isCheckoutDeliveryComplete(value, requiresDelivery?)` tells whether the choice can be quoted (pickup needs a point), so the SITE passes `null` to `useShopQuote` instead of asking for a quote known to answer 422. New `checkoutQuoteNotice({ fulfillment, requiresDelivery?, error? })` and `<CheckoutQuoteNotice reason labels? />` render «Elige un punto de recogida» (`role=status`) for an incomplete pickup and for the `PICKUP_LOCATION_INVALID` quote error, instead of the generic total error. Also exports `DEFAULT_CHECKOUT_QUOTE_NOTICE_LABELS` and the `CheckoutQuoteNotice*` types. All additive.
|
|
14
|
+
- faf78e8: `OrderConfirmation` default copy change (REB-1200, approved by the owner). The confirmed-order line no longer says «Te hemos enviado un email con tu acceso.» (wrong for physical orders and for buyers who already have an account). The default `foundDescription` is now «Tu pedido {number} está confirmado. Te hemos enviado el resumen a {email}.»: `{number}` and `{email}` are filled from the order. When the text carries `{email}`, the separate «Te hemos enviado un email de confirmación a…» line is not shown. New label `foundWithoutOrderDescription` («Tu pedido está confirmado.») is used while no order is loaded, so a placeholder is never shown raw. A SITE that passes its own `labels.foundDescription` is unaffected: a text without placeholders renders as is, with the confirmation-email line as before.
|
|
15
|
+
- 49f8f4b: New page content blocks (REB-1265..1268). Additive only: every existing block type renders exactly as before.
|
|
16
|
+
- `before_after` («Antes y después»): before/after image pairs with an opt-in text under them (`showCaption` + `caption`). `@reblu/site-blocks` adds the drop-in `BeforeAfterPairs` and renders the block in `PageBlocks`. `@reblu/site-contracts` exports `BeforeAfterData`.
|
|
17
|
+
- `chips` («Etiquetas»): a short list of texts shown as chips. Drop-in `ChipList`; type `ChipsData`.
|
|
18
|
+
- `icon_cards` («Tarjetas con icono»): a responsive grid of cards with an icon from a fixed, curated set, a title and a short text. Drop-in `IconCardGrid` and `CardIconSvg`, `CARD_ICON_NAMES`; types `IconCardsData` and `CardIcon`. `feature_list` is unchanged.
|
|
19
|
+
- `image_text` («Imagen y texto»): an image beside rich text, image left or right, configurable mobile order. Drop-in `ImageTextSplit`; type `ImageTextData`. `BlockRendererProps` gains an optional `headingId`, set only for `image_text`, which draws the section heading in its text column. The legacy `TEXT_IMAGE`/`IMAGE_TEXT` layouts are unchanged.
|
|
20
|
+
- `PageBlocks` renders `data.kicker` above the section heading for the new content blocks only.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Updated dependencies [9a83295]
|
|
25
|
+
- Updated dependencies [49f8f4b]
|
|
26
|
+
- @reblu/site-contracts@1.29.0
|
|
27
|
+
|
|
28
|
+
## 0.30.0
|
|
29
|
+
|
|
30
|
+
### Minor Changes
|
|
31
|
+
|
|
32
|
+
- 3b096d4: `CartLineList` gains an optional `lineIncludes` render prop (REB-1208): it adds a collapsed «Qué incluye» disclosure under a line (`labels.includesSummary`). New drop-in `CartLineIncludes` (term/description list plus optional `note`) and `bundleComponentsToIncludes`, which maps `ShopProductDetail.bundleComponents` to its entries. Omitting the prop renders the cart exactly as before.
|
|
33
|
+
|
|
3
34
|
## 0.29.0
|
|
4
35
|
|
|
5
36
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -327,7 +327,8 @@ for the wiring steps and a full example app).
|
|
|
327
327
|
| `AddToCartButton` | `./client` | `onAddToCart?`, `disabled?`, `outOfStock?`, `pending?`, `error?`, `item?`, `successDurationMs?`, `onAdded?`, `onError?` | No — carries a built-in confirmed/error timer (`useState`/`useEffect`), see below |
|
|
328
328
|
| `CartIcon` | `./client` | `itemCount`, `href`, `hydrated?`, `bumpOnAdd?` | No — the bump animation needs state, see below |
|
|
329
329
|
| `AddedToCartToast` | `.` | `productName`, `cartHref`, `status?`, `quantity?`, `imageUrl?` | Yes |
|
|
330
|
-
| `CartLineList` | `.` | `items`, `onQuantityChange?`, `onRemove?`, `liveMessage?`, `notice?`, `lineMessage?`
|
|
330
|
+
| `CartLineList` | `.` | `items`, `onQuantityChange?`, `onRemove?`, `liveMessage?`, `notice?`, `lineMessage?`, `lineIncludes?` | Yes |
|
|
331
|
+
| `CartLineIncludes` | `.` | `entries`, `note?` | Yes |
|
|
331
332
|
| `CartAdjustmentsNotice` | `.` | `removed?`, `quantityChanges?`, `priceChanges?`, `labels?` | Yes |
|
|
332
333
|
| `CartSummary` | `.` | `subtotal`, `discount?`, `autoDiscount?`, `shipping?`, `shippingEnabled?`, `total`, `notice?` | Yes |
|
|
333
334
|
| `CheckoutLoginPrompt` | `.` | `open`, `email`, `onEmailChange`, `onClose`, `onSubmitEmail`, `onContinueAsGuest` | Yes (renders `null` when `open` is false) |
|
|
@@ -530,6 +531,36 @@ function). `currencyCode`/`locale` format the prices.
|
|
|
530
531
|
`CartLineList`'s `lineMessage={(item) => …}` renders a message under a line,
|
|
531
532
|
e.g. `validation.getLineError(item.productId, item.variantId)?.message`.
|
|
532
533
|
|
|
534
|
+
### «Qué incluye» per line — `lineIncludes` + `CartLineIncludes` (REB-1208)
|
|
535
|
+
|
|
536
|
+
`CartLineList`'s `lineIncludes={(item) => …}` adds a collapsed
|
|
537
|
+
`<details>` under a line whose summary reads «Qué incluye»
|
|
538
|
+
(`labels.includesSummary`). Return `null`/`undefined` for lines with nothing
|
|
539
|
+
to show; omit the prop and the cart renders exactly as before.
|
|
540
|
+
|
|
541
|
+
`CartLineIncludes` is the drop-in body: a term/description list plus an
|
|
542
|
+
optional `note`. Build its entries from the product's own data — nothing is
|
|
543
|
+
fixed per product type. For a bundle, `bundleComponentsToIncludes` maps
|
|
544
|
+
`ShopProductDetail.bundleComponents` to name / `x{quantity}` rows:
|
|
545
|
+
|
|
546
|
+
```tsx
|
|
547
|
+
import { CartLineIncludes, CartLineList, bundleComponentsToIncludes } from '@reblu/site-blocks'
|
|
548
|
+
|
|
549
|
+
// `detailsById`: the SITE's ShopProductDetail per productId (already fetched for the ficha).
|
|
550
|
+
<CartLineList
|
|
551
|
+
items={items}
|
|
552
|
+
lineIncludes={(item) => {
|
|
553
|
+
const product = detailsById[item.productId]
|
|
554
|
+
if (!product) return null
|
|
555
|
+
const entries = [
|
|
556
|
+
...bundleComponentsToIncludes(product.bundleComponents),
|
|
557
|
+
// …plus any other `{ label, value }` row built from the product's own data.
|
|
558
|
+
]
|
|
559
|
+
return entries.length > 0 ? <CartLineIncludes entries={entries} /> : null
|
|
560
|
+
}}
|
|
561
|
+
/>
|
|
562
|
+
```
|
|
563
|
+
|
|
533
564
|
### `OrderConfirmation` states (REB-837)
|
|
534
565
|
|
|
535
566
|
`status` accepts `@reblu/site-next/shop`'s `useShopOrderStatus` hook result
|
|
@@ -540,7 +571,7 @@ directly — `loading`/`paid` are drop-in aliases for the original
|
|
|
540
571
|
| `status` | Default copy | Order details shown | Extra UI |
|
|
541
572
|
| --------------------- | ----------------------------------------------- | ---------------------------------------------------- | ----------------------------------------------------- |
|
|
542
573
|
| `loading` / `polling` | «Confirmando tu pedido…» | — | — |
|
|
543
|
-
| `paid` / `found` | «¡Pedido confirmado!»
|
|
574
|
+
| `paid` / `found` | «¡Pedido confirmado!» + «Tu pedido {number} está confirmado. Te hemos enviado el resumen a {email}.» | Full summary (items, total; the confirmation-email line only when `foundDescription` has no `{email}`) | — |
|
|
544
575
|
| `failed` | «El pago no se ha completado» | None | "Volver al carrito" link when `cartHref` is given |
|
|
545
576
|
| `refunded` | «Este pedido ha sido reembolsado» | Order number only (neutral reference) | — |
|
|
546
577
|
| `partially_refunded` | «…reembolsado parcialmente» | Order number only | — |
|
|
@@ -560,6 +591,18 @@ reference (`labels.orderReferenceLabel`); `failed`/`not_found`/`error`
|
|
|
560
591
|
render no order details at all, even if an `order` object happens to be
|
|
561
592
|
present.
|
|
562
593
|
|
|
594
|
+
**REB-1200 — default copy change.** The confirmed line used to read «Te
|
|
595
|
+
hemos enviado un email con tu acceso.» for every order, physical ones too, and
|
|
596
|
+
buyers who already had an account got no new access. The default
|
|
597
|
+
`foundDescription` is now «Tu pedido {number} está confirmado. Te hemos
|
|
598
|
+
enviado el resumen a {email}.» (`{number}` = `order.orderNumber`, `{email}` =
|
|
599
|
+
`order.customerEmail`). Because it names the email, the separate «Te hemos
|
|
600
|
+
enviado un email de confirmación a…» line is not shown under it. With no order
|
|
601
|
+
loaded yet, `foundWithoutOrderDescription` («Tu pedido está confirmado.») is
|
|
602
|
+
shown instead of a raw placeholder. A SITE that passes its own
|
|
603
|
+
`labels.foundDescription` is unaffected: a text without `{email}` keeps the
|
|
604
|
+
confirmation-email line, exactly as before.
|
|
605
|
+
|
|
563
606
|
Every title/description is overridable via `labels` (see
|
|
564
607
|
`OrderConfirmationLabels`). See `@reblu/site-next`'s README
|
|
565
608
|
"`useShopOrderStatus` — the order-confirmation flow" for the full wiring
|
|
@@ -596,6 +639,7 @@ configuration edited in nexus (`ShopConfig.delivery`, `ShopConfig.checkout`,
|
|
|
596
639
|
| Block | Reads | Renders |
|
|
597
640
|
| ----- | ----- | ------- |
|
|
598
641
|
| `CheckoutDeliveryChoice` | `config.delivery`, `config.checkout` | Home delivery (cost, estimate, address form) or free pickup (points with address, hours, ready-in text). Only the methods the tenant offers; nothing for a cart that ships nothing. |
|
|
642
|
+
| `CheckoutQuoteNotice` | `checkoutQuoteNotice(...)` | «Elige un punto de recogida» (`role=status`) in place of the summary and of the total error while pickup has no point, or when the quote answers `PICKUP_LOCATION_INVALID` (REB-1276). |
|
|
599
643
|
| `CheckoutQuoteSummary` | `ShopQuote` | Lines (kind, `qty × unit`), subtotal, discount, delivery row, tax rows, total, «Impuestos incluidos», remaining for free shipping, delivery term; `role=status` for an unusable code, `role=alert` when the order cannot be paid. |
|
|
600
644
|
| `CheckoutTermsAcceptance` | `config.checkout` | The required, unticked box with `{terms}` as the link to the full document and `{tenant}` filled. Nothing when no terms are published. |
|
|
601
645
|
| `CheckoutPayAction` | `config.checkout.continueToPaymentLabel`, `quote.payNotice` | The submit button and the notice with the amount under it (`text-sm`, the button's description). |
|
|
@@ -613,11 +657,14 @@ import type { ShopConfig } from '@reblu/site-contracts'
|
|
|
613
657
|
import {
|
|
614
658
|
CheckoutDeliveryChoice,
|
|
615
659
|
CheckoutPayAction,
|
|
660
|
+
CheckoutQuoteNotice,
|
|
616
661
|
CheckoutQuoteSummary,
|
|
617
662
|
CheckoutTermsAcceptance,
|
|
618
663
|
EMPTY_CHECKOUT_ADDRESS,
|
|
619
664
|
defaultCheckoutFulfillment,
|
|
665
|
+
checkoutQuoteNotice,
|
|
620
666
|
getShopErrorMessage,
|
|
667
|
+
isCheckoutDeliveryComplete,
|
|
621
668
|
quoteRequiresDelivery,
|
|
622
669
|
toCheckoutFulfillmentFields,
|
|
623
670
|
toCheckoutTermsFields,
|
|
@@ -629,9 +676,13 @@ export function SingleStepCheckout({ config }: { config: ShopConfig }) {
|
|
|
629
676
|
const [address, setAddress] = useState(EMPTY_CHECKOUT_ADDRESS)
|
|
630
677
|
const [acceptsTerms, setAcceptsTerms] = useState(false)
|
|
631
678
|
const lines = items.map(({ productId, variantId, quantity }) => ({ productId, variantId, quantity }))
|
|
679
|
+
// Pickup with no point is known to be incomplete: ask no quote for it (REB-1276).
|
|
632
680
|
const { quote, status, error, refresh } = useShopQuote(
|
|
633
|
-
lines.length
|
|
681
|
+
lines.length && isCheckoutDeliveryComplete(fulfillment)
|
|
682
|
+
? { items: lines, fulfillment: toCheckoutFulfillmentFields(fulfillment, address).fulfillment }
|
|
683
|
+
: null,
|
|
634
684
|
)
|
|
685
|
+
const notice = checkoutQuoteNotice({ fulfillment, requiresDelivery: quoteRequiresDelivery(quote), error })
|
|
635
686
|
// A cart that ships nothing hides the choice: send no address (blank = 400).
|
|
636
687
|
const delivery = toCheckoutFulfillmentFields(fulfillment, address, quoteRequiresDelivery(quote))
|
|
637
688
|
|
|
@@ -673,8 +724,9 @@ export function SingleStepCheckout({ config }: { config: ShopConfig }) {
|
|
|
673
724
|
) : null}
|
|
674
725
|
</div>
|
|
675
726
|
<aside className='space-y-5'>
|
|
676
|
-
{
|
|
677
|
-
{
|
|
727
|
+
{notice ? <CheckoutQuoteNotice reason={notice} /> : null}
|
|
728
|
+
{!notice && quote ? <CheckoutQuoteSummary quote={quote} heading='Resumen' /> : null}
|
|
729
|
+
{!notice && status === 'error' ? (
|
|
678
730
|
<p role='alert'>
|
|
679
731
|
{error?.message ?? 'No hemos podido calcular el total.'}{' '}
|
|
680
732
|
<button type='button' onClick={() => void refresh()}>Reintentar</button>
|
|
@@ -720,6 +772,14 @@ Notes:
|
|
|
720
772
|
of the hidden block (the server refuses a blank address with 400): the server records no delivery
|
|
721
773
|
for such a cart, and the request stays on the current contract (REB-1227
|
|
722
774
|
removes the old shape).
|
|
775
|
+
- Pickup point (REB-1276): switching to pickup with exactly one point selects
|
|
776
|
+
it (`pickupLocationId` is sent); with two or more, none is preselected.
|
|
777
|
+
While pickup has no point, `isCheckoutDeliveryComplete(fulfillment)` is
|
|
778
|
+
`false`: pass `null` to `useShopQuote` (no request that is known to answer
|
|
779
|
+
422) and render `CheckoutQuoteNotice` with `checkoutQuoteNotice(...)`. The
|
|
780
|
+
same notice covers a `PICKUP_LOCATION_INVALID` quote error (a point
|
|
781
|
+
deactivated meanwhile), never the generic total error. Replace its text with
|
|
782
|
+
`labels={{ pickupLocationRequired: '…' }}`.
|
|
723
783
|
- The full-invoice texts are the tenant's too: pass
|
|
724
784
|
`labels={{ toggle: config.checkout.fullInvoiceCheckboxLabel, underCapNotice: config.checkout.fullInvoiceHelpText }}`
|
|
725
785
|
to `FullInvoiceFields`.
|
|
@@ -1351,6 +1411,53 @@ const course = await client.academy.bySlug(slug)
|
|
|
1351
1411
|
|
|
1352
1412
|
The waiver item keeps `data-slot='course-purchase-terms-withdrawal-waiver'` (as in REB-1199); the express request to start uses `data-slot='course-purchase-terms-service-start-request'`. Both carry `data-kind` with the kind.
|
|
1353
1413
|
|
|
1414
|
+
## Guest course checkout — `CourseGuestCheckoutForm` (REB-1264)
|
|
1415
|
+
|
|
1416
|
+
A course that needs no validation is bought in one step by a visitor without
|
|
1417
|
+
a session: name, surname, email, the tenant's terms box, the course's consent
|
|
1418
|
+
box (`withdrawalConsent`, required on the `course` prop — `null` when there
|
|
1419
|
+
is none — and its `version` is sent), the optional full invoice
|
|
1420
|
+
(fields hidden until the buyer opts in, or required over the cap), the total
|
|
1421
|
+
and the pay notice. On success the buyer goes to Stripe; the account is
|
|
1422
|
+
created after payment. On `ENROLLMENT_REQUEST_REQUIRED` the form gives way to
|
|
1423
|
+
`requestForm`. It says the same for every email and never mentions an account.
|
|
1424
|
+
|
|
1425
|
+
```tsx
|
|
1426
|
+
// Server Component — the course page
|
|
1427
|
+
import { courseSellsAsGuest } from '@reblu/site-blocks'
|
|
1428
|
+
import { CourseGuestCheckoutForm } from '@reblu/site-blocks/client'
|
|
1429
|
+
import { AcademyEnrollmentForm } from '@reblu/site-next/academy'
|
|
1430
|
+
|
|
1431
|
+
const [course, shop] = await Promise.all([client.academy.bySlug(slug), client.shop.config()])
|
|
1432
|
+
const requestForm = (
|
|
1433
|
+
<AcademyEnrollmentForm enrollmentRequestUrl='/api/academy/enrollment-request' courseSlug={course.slug} />
|
|
1434
|
+
)
|
|
1435
|
+
|
|
1436
|
+
return courseSellsAsGuest(course) ? (
|
|
1437
|
+
<CourseGuestCheckoutForm
|
|
1438
|
+
// `withdrawalConsent` is required: `null` = the course has no box
|
|
1439
|
+
course={{ ...course, withdrawalConsent: course.withdrawalConsent ?? null }}
|
|
1440
|
+
copy={shop.checkout} // terms box, «Continuar al pago», pay notice, invoice texts
|
|
1441
|
+
termsHref='/condiciones'
|
|
1442
|
+
simplifiedInvoiceCapCents={shop.simplifiedInvoiceCapCents}
|
|
1443
|
+
issuesInvoices={shop.simplifiedInvoiceCapCents != null}
|
|
1444
|
+
requestForm={requestForm}
|
|
1445
|
+
/>
|
|
1446
|
+
) : (
|
|
1447
|
+
requestForm
|
|
1448
|
+
)
|
|
1449
|
+
```
|
|
1450
|
+
|
|
1451
|
+
It posts to the SITE's own `/api/academy/checkout` (`endpoint`) — mount
|
|
1452
|
+
`createAcademyCheckoutRoute()` from `@reblu/site-next/route-handlers` there —
|
|
1453
|
+
or pass `submit` to send it yourself (`client.academy.checkout`). `{total}`
|
|
1454
|
+
and `{tenant}` in `copy.payNotice` are filled with the course price. Every
|
|
1455
|
+
refusal code has a default message (`DEFAULT_COURSE_GUEST_CHECKOUT_ERROR_MESSAGES`,
|
|
1456
|
+
override with `errorMessages`); `INVALID_BILLING` field errors land on their
|
|
1457
|
+
invoice field; `onError` gets each refusal (e.g. reload on
|
|
1458
|
+
`TERMS_VERSION_CHANGED` / `CONSENT_VERSION_CHANGED`). `toAcademyCheckoutInvoiceFields`
|
|
1459
|
+
(root entry) builds the `billing` block for a SITE with its own form.
|
|
1460
|
+
|
|
1354
1461
|
## Page blocks
|
|
1355
1462
|
|
|
1356
1463
|
`PageBlocks` renders a page's registry blocks (`PageDetail.blocks` from
|
|
@@ -1387,6 +1494,9 @@ that applies the parent section's mirrored fields (REB-849):
|
|
|
1387
1494
|
luminance; any other value keeps the theme foreground.
|
|
1388
1495
|
- **`cta`** — rendered after the body as a link styled with `buttonVariants`;
|
|
1389
1496
|
external CTAs open in a new tab with `rel="noopener noreferrer"`.
|
|
1497
|
+
- **`data.kicker`** — on the content blocks below (REB-1265..1268), a short
|
|
1498
|
+
line rendered above the heading (`data-slot="page-block-kicker"`). Only
|
|
1499
|
+
those block types render it; every other block's markup is unchanged.
|
|
1390
1500
|
|
|
1391
1501
|
| Prop | Default | Purpose |
|
|
1392
1502
|
| ------------------- | -------------------- | ---------------------------------------------------------- |
|
|
@@ -1427,9 +1537,110 @@ function TeamCard({ block, headingLevel, embedHref }: BlockRendererProps<'profil
|
|
|
1427
1537
|
<PageBlocks blocks={page.blocks} components={{ profile_embed: TeamCard }} />
|
|
1428
1538
|
```
|
|
1429
1539
|
|
|
1430
|
-
Every renderer receives `{ block, headingLevel, embedHref }
|
|
1540
|
+
Every renderer receives `{ block, headingLevel, embedHref }` (plus
|
|
1541
|
+
`headingId` for `image_text`, which draws the section heading itself). Overrides may be
|
|
1431
1542
|
async Server Components, or Client Components when they need interactivity.
|
|
1432
1543
|
|
|
1544
|
+
### Content blocks (REB-1265..1268)
|
|
1545
|
+
|
|
1546
|
+
Drop-in components for the content blocks a tenant builds in the nexus page
|
|
1547
|
+
editor. `PageBlocks` uses each one as the default renderer of its block type;
|
|
1548
|
+
you can also place them on any page yourself, passing the block's `data`.
|
|
1549
|
+
All are Server Components, take colours from theme tokens only, and lazy-load
|
|
1550
|
+
every image. The platform ships no tenant copy: the only built-in strings are
|
|
1551
|
+
neutral labels you can override.
|
|
1552
|
+
|
|
1553
|
+
#### `BeforeAfterPairs` — block `before_after` («Antes y después»)
|
|
1554
|
+
|
|
1555
|
+
Before/after image pairs. Each pair stacks on mobile and sits side by side from
|
|
1556
|
+
`sm`; pairs flow two per row from `lg`. The text under the pairs is opt-in: it
|
|
1557
|
+
renders **only** when `showCaption` is `true` **and** `caption` is not blank.
|
|
1558
|
+
|
|
1559
|
+
```tsx
|
|
1560
|
+
import { BeforeAfterPairs } from '@reblu/site-blocks'
|
|
1561
|
+
|
|
1562
|
+
// `block.data` of a `before_after` block from `page.blocks`
|
|
1563
|
+
<BeforeAfterPairs
|
|
1564
|
+
pairs={block.data.pairs}
|
|
1565
|
+
showCaption={block.data.showCaption}
|
|
1566
|
+
caption={block.data.caption}
|
|
1567
|
+
labels={{ before: 'Before', after: 'After' }} // default: DEFAULT_BEFORE_AFTER_LABELS
|
|
1568
|
+
/>
|
|
1569
|
+
```
|
|
1570
|
+
|
|
1571
|
+
| Prop | Default | Purpose |
|
|
1572
|
+
| ------------- | ----------------------------- | -------------------------------------------------------- |
|
|
1573
|
+
| `pairs` | — | `{ beforeUrl, afterUrl, alt? }[]`. Empty with no caption renders nothing. |
|
|
1574
|
+
| `showCaption` | `false` | The tenant's «Mostrar texto al pie» toggle. |
|
|
1575
|
+
| `caption` | — | Free tenant text (legal note, consent…), line breaks kept. |
|
|
1576
|
+
| `labels` | `DEFAULT_BEFORE_AFTER_LABELS` | `{ before, after }` shown under each image. |
|
|
1577
|
+
|
|
1578
|
+
#### `ChipList` — block `chips` («Etiquetas»)
|
|
1579
|
+
|
|
1580
|
+
A short list of texts shown as chips. They wrap onto new lines on small
|
|
1581
|
+
screens and use the `primary` theme token (tinted background, primary text).
|
|
1582
|
+
Blank items are skipped; an empty list renders nothing.
|
|
1583
|
+
|
|
1584
|
+
```tsx
|
|
1585
|
+
import { ChipList } from '@reblu/site-blocks'
|
|
1586
|
+
|
|
1587
|
+
<ChipList items={block.data.items} />
|
|
1588
|
+
```
|
|
1589
|
+
|
|
1590
|
+
#### `IconCardGrid` — block `icon_cards` («Tarjetas con icono»)
|
|
1591
|
+
|
|
1592
|
+
Cards with an icon, a title and a short text, in a responsive grid: one column
|
|
1593
|
+
on mobile, two from `sm`, three from `lg`. The icon is a name from a fixed,
|
|
1594
|
+
curated set (`CARD_ICON_NAMES`, the `CardIcon` type) — tenants pick it in nexus,
|
|
1595
|
+
never upload one. The SVGs are inlined (Lucide geometry, ISC) and drawn in
|
|
1596
|
+
`currentColor` on a `primary`-tinted tile. An icon name this version does not
|
|
1597
|
+
know renders the card without an icon.
|
|
1598
|
+
|
|
1599
|
+
```tsx
|
|
1600
|
+
import { IconCardGrid, CardIconSvg } from '@reblu/site-blocks'
|
|
1601
|
+
|
|
1602
|
+
<IconCardGrid intro={block.data.intro} items={block.data.items} headingLevel={3} />
|
|
1603
|
+
|
|
1604
|
+
// One icon on its own, e.g. in a custom card:
|
|
1605
|
+
<CardIconSvg icon='shield-check' className='h-6 w-6 text-primary' />
|
|
1606
|
+
```
|
|
1607
|
+
|
|
1608
|
+
| Prop | Default | Purpose |
|
|
1609
|
+
| -------------- | ------- | ------------------------------------------------ |
|
|
1610
|
+
| `items` | — | `{ icon, title, text? }[]` — `data.items`. |
|
|
1611
|
+
| `intro` | — | `data.intro`, a line above the grid. |
|
|
1612
|
+
| `headingLevel` | `3` | Level of each card title. |
|
|
1613
|
+
|
|
1614
|
+
`feature_list` is unchanged: it still renders its plain list.
|
|
1615
|
+
|
|
1616
|
+
#### `ImageTextSplit` — block `image_text` («Imagen y texto»)
|
|
1617
|
+
|
|
1618
|
+
An image beside rich text. Stacked on mobile — the image first, or the text
|
|
1619
|
+
first with `mobileOrder='text_first'` — and side by side from `md`, with the
|
|
1620
|
+
image on `imageSide` (`'left'` or `'right'`). The kicker and the section
|
|
1621
|
+
heading sit at the top of the text column, so in `PageBlocks` this block draws
|
|
1622
|
+
the section heading itself: its renderer receives `headingId` (the
|
|
1623
|
+
`aria-labelledby` target) and the section's own `headingLevel`. `html` is the
|
|
1624
|
+
section's rich text, derived by base-api on save; it is sanitised here and its
|
|
1625
|
+
headings nest below the heading.
|
|
1626
|
+
|
|
1627
|
+
```tsx
|
|
1628
|
+
import { ImageTextSplit } from '@reblu/site-blocks'
|
|
1629
|
+
|
|
1630
|
+
<ImageTextSplit
|
|
1631
|
+
imageUrl={block.data.imageUrl}
|
|
1632
|
+
alt={block.data.alt}
|
|
1633
|
+
imageSide={block.data.imageSide}
|
|
1634
|
+
mobileOrder={block.data.mobileOrder}
|
|
1635
|
+
html={block.data.html}
|
|
1636
|
+
kicker={block.data.kicker}
|
|
1637
|
+
heading={block.heading}
|
|
1638
|
+
headingLevel={2}
|
|
1639
|
+
/>
|
|
1640
|
+
```
|
|
1641
|
+
|
|
1642
|
+
The legacy `sections[]` layouts `TEXT_IMAGE` / `IMAGE_TEXT` are unchanged.
|
|
1643
|
+
|
|
1433
1644
|
### `FaqAccordion`
|
|
1434
1645
|
|
|
1435
1646
|
Native `<details>`/`<summary>` — opens and closes with no JavaScript, keyboard
|