@reblu/site-blocks 0.30.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 +25 -0
- package/README.md +185 -5
- 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 +827 -348
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +167 -130
- package/dist/index.d.ts +167 -130
- package/dist/index.js +815 -349
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/dist/_heading-BsNbc1wc.d.cts +0 -4
- package/dist/_heading-BsNbc1wc.d.ts +0 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
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
|
+
|
|
3
28
|
## 0.30.0
|
|
4
29
|
|
|
5
30
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -571,7 +571,7 @@ directly — `loading`/`paid` are drop-in aliases for the original
|
|
|
571
571
|
| `status` | Default copy | Order details shown | Extra UI |
|
|
572
572
|
| --------------------- | ----------------------------------------------- | ---------------------------------------------------- | ----------------------------------------------------- |
|
|
573
573
|
| `loading` / `polling` | «Confirmando tu pedido…» | — | — |
|
|
574
|
-
| `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}`) | — |
|
|
575
575
|
| `failed` | «El pago no se ha completado» | None | "Volver al carrito" link when `cartHref` is given |
|
|
576
576
|
| `refunded` | «Este pedido ha sido reembolsado» | Order number only (neutral reference) | — |
|
|
577
577
|
| `partially_refunded` | «…reembolsado parcialmente» | Order number only | — |
|
|
@@ -591,6 +591,18 @@ reference (`labels.orderReferenceLabel`); `failed`/`not_found`/`error`
|
|
|
591
591
|
render no order details at all, even if an `order` object happens to be
|
|
592
592
|
present.
|
|
593
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
|
+
|
|
594
606
|
Every title/description is overridable via `labels` (see
|
|
595
607
|
`OrderConfirmationLabels`). See `@reblu/site-next`'s README
|
|
596
608
|
"`useShopOrderStatus` — the order-confirmation flow" for the full wiring
|
|
@@ -627,6 +639,7 @@ configuration edited in nexus (`ShopConfig.delivery`, `ShopConfig.checkout`,
|
|
|
627
639
|
| Block | Reads | Renders |
|
|
628
640
|
| ----- | ----- | ------- |
|
|
629
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). |
|
|
630
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. |
|
|
631
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. |
|
|
632
645
|
| `CheckoutPayAction` | `config.checkout.continueToPaymentLabel`, `quote.payNotice` | The submit button and the notice with the amount under it (`text-sm`, the button's description). |
|
|
@@ -644,11 +657,14 @@ import type { ShopConfig } from '@reblu/site-contracts'
|
|
|
644
657
|
import {
|
|
645
658
|
CheckoutDeliveryChoice,
|
|
646
659
|
CheckoutPayAction,
|
|
660
|
+
CheckoutQuoteNotice,
|
|
647
661
|
CheckoutQuoteSummary,
|
|
648
662
|
CheckoutTermsAcceptance,
|
|
649
663
|
EMPTY_CHECKOUT_ADDRESS,
|
|
650
664
|
defaultCheckoutFulfillment,
|
|
665
|
+
checkoutQuoteNotice,
|
|
651
666
|
getShopErrorMessage,
|
|
667
|
+
isCheckoutDeliveryComplete,
|
|
652
668
|
quoteRequiresDelivery,
|
|
653
669
|
toCheckoutFulfillmentFields,
|
|
654
670
|
toCheckoutTermsFields,
|
|
@@ -660,9 +676,13 @@ export function SingleStepCheckout({ config }: { config: ShopConfig }) {
|
|
|
660
676
|
const [address, setAddress] = useState(EMPTY_CHECKOUT_ADDRESS)
|
|
661
677
|
const [acceptsTerms, setAcceptsTerms] = useState(false)
|
|
662
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).
|
|
663
680
|
const { quote, status, error, refresh } = useShopQuote(
|
|
664
|
-
lines.length
|
|
681
|
+
lines.length && isCheckoutDeliveryComplete(fulfillment)
|
|
682
|
+
? { items: lines, fulfillment: toCheckoutFulfillmentFields(fulfillment, address).fulfillment }
|
|
683
|
+
: null,
|
|
665
684
|
)
|
|
685
|
+
const notice = checkoutQuoteNotice({ fulfillment, requiresDelivery: quoteRequiresDelivery(quote), error })
|
|
666
686
|
// A cart that ships nothing hides the choice: send no address (blank = 400).
|
|
667
687
|
const delivery = toCheckoutFulfillmentFields(fulfillment, address, quoteRequiresDelivery(quote))
|
|
668
688
|
|
|
@@ -704,8 +724,9 @@ export function SingleStepCheckout({ config }: { config: ShopConfig }) {
|
|
|
704
724
|
) : null}
|
|
705
725
|
</div>
|
|
706
726
|
<aside className='space-y-5'>
|
|
707
|
-
{
|
|
708
|
-
{
|
|
727
|
+
{notice ? <CheckoutQuoteNotice reason={notice} /> : null}
|
|
728
|
+
{!notice && quote ? <CheckoutQuoteSummary quote={quote} heading='Resumen' /> : null}
|
|
729
|
+
{!notice && status === 'error' ? (
|
|
709
730
|
<p role='alert'>
|
|
710
731
|
{error?.message ?? 'No hemos podido calcular el total.'}{' '}
|
|
711
732
|
<button type='button' onClick={() => void refresh()}>Reintentar</button>
|
|
@@ -751,6 +772,14 @@ Notes:
|
|
|
751
772
|
of the hidden block (the server refuses a blank address with 400): the server records no delivery
|
|
752
773
|
for such a cart, and the request stays on the current contract (REB-1227
|
|
753
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: '…' }}`.
|
|
754
783
|
- The full-invoice texts are the tenant's too: pass
|
|
755
784
|
`labels={{ toggle: config.checkout.fullInvoiceCheckboxLabel, underCapNotice: config.checkout.fullInvoiceHelpText }}`
|
|
756
785
|
to `FullInvoiceFields`.
|
|
@@ -1382,6 +1411,53 @@ const course = await client.academy.bySlug(slug)
|
|
|
1382
1411
|
|
|
1383
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.
|
|
1384
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
|
+
|
|
1385
1461
|
## Page blocks
|
|
1386
1462
|
|
|
1387
1463
|
`PageBlocks` renders a page's registry blocks (`PageDetail.blocks` from
|
|
@@ -1418,6 +1494,9 @@ that applies the parent section's mirrored fields (REB-849):
|
|
|
1418
1494
|
luminance; any other value keeps the theme foreground.
|
|
1419
1495
|
- **`cta`** — rendered after the body as a link styled with `buttonVariants`;
|
|
1420
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.
|
|
1421
1500
|
|
|
1422
1501
|
| Prop | Default | Purpose |
|
|
1423
1502
|
| ------------------- | -------------------- | ---------------------------------------------------------- |
|
|
@@ -1458,9 +1537,110 @@ function TeamCard({ block, headingLevel, embedHref }: BlockRendererProps<'profil
|
|
|
1458
1537
|
<PageBlocks blocks={page.blocks} components={{ profile_embed: TeamCard }} />
|
|
1459
1538
|
```
|
|
1460
1539
|
|
|
1461
|
-
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
|
|
1462
1542
|
async Server Components, or Client Components when they need interactivity.
|
|
1463
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
|
+
|
|
1464
1644
|
### `FaqAccordion`
|
|
1465
1645
|
|
|
1466
1646
|
Native `<details>`/`<summary>` — opens and closes with no JavaScript, keyboard
|