@reblu/site-blocks 0.32.0 → 0.34.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 CHANGED
@@ -1,5 +1,46 @@
1
1
  # @reblu/site-blocks
2
2
 
3
+ ## 0.34.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 46b855f: Page links drop-ins (REB-1100). Additive only: no existing component changes.
8
+ - `LinkedPagesGrid` (root entry): a linked card grid over a page's `links` (the pages it picked in nexus, e.g. a profile's areas) or `relatedPages` (derived: the pages of those areas, or the pages that belong to this one). Renders nothing for an empty or absent list.
9
+ - `TeamGrid` (root entry): `ProfileCard`s over a page's `team` (derived: the team profiles linked to this page or to the pages it belongs to). Renders nothing without members.
10
+ - `TeamDirectory` (client entry): the team listing with one filter chip per page the members link to, «Todos» first (labels overridable). `memberHrefBase` (a string) links the cards from a Server Component, where a `memberHref` function cannot be passed. Members without `links` render as a plain listing.
11
+ - Pure helpers `linkKey`, `linkFilterOptions` and `filterByLink` (root entry) for a SITE that builds its own chips.
12
+
13
+ The props are structural, so the drop-ins already accept today's `PageListItem` and `ResolvedEmbed`.
14
+
15
+ - e43b01c: `ReviewsCarousel` (REB-1272), from `@reblu/site-blocks/client`. It shows the tenant's published reviews as an accessible carousel: the same cards and data rules as `Reviews`, on the package's scroll-snap track, with a labelled focusable region and labelled previous/next buttons. It never autoplays, and scrolling is instant under `prefers-reduced-motion`. `Reviews` renders exactly as before; its card moved into a shared internal module. `FeaturedReviews`, also from `/client`, renders the reviews a tenant featured for its home in the layout it chose in nexus: `grid` uses `Reviews`, `carousel` uses `ReviewsCarousel`, and an unknown layout falls back to the grid.
16
+ - 83084d0: «Reseñas» page block (REB-1272). `@reblu/site-contracts` adds the `reviews` block type: `ReviewsBlockData` (`{ scope: 'all' | 'page', limit?, layout?: 'grid' | 'carousel', notice? }`) and an optional `reviews: Review[]` on `PageBlock`, which the site API fills with the published reviews the block selects. `@reblu/site-blocks` renders it in `PageBlocks` with the `Reviews` drop-in (the tenant's notice included) and skips the whole section when there is no review to show. Every existing block type renders exactly as before. The carousel layout renders as the grid in the server-safe `PageBlocks`; `ReviewsBlockRenderer` from `@reblu/site-blocks/client` renders the real carousel through `components.reviews`. `PageDetail` gains an `id` (optional on the wire, always emitted), so a SITE can call `client.reviews.list({ pageId: page.id })`.
17
+
18
+ ### Patch Changes
19
+
20
+ - 1e2f438: REB-1227: docs only. `toCheckoutFulfillmentFields` documentation now says that the server reads a request without `fulfillment` on a cart that ships as home delivery, with the address required (422 `SHIPPING_ADDRESS_REQUIRED`). No code change.
21
+ - Updated dependencies [49d31ea]
22
+ - Updated dependencies [c8bf6c1]
23
+ - Updated dependencies [83084d0]
24
+ - @reblu/site-contracts@1.32.0
25
+
26
+ ## 0.33.0
27
+
28
+ ### Minor Changes
29
+
30
+ - 8c2e641: REB-1236: the order confirmation screen says what happens next for each purchase type.
31
+ - `@reblu/site-contracts`: `ShopOrder.purchaseTexts?: ShopOrderPurchaseText[]` (`{ kind: 'shipping' | 'pickup' | 'digital' | 'service', label, text }`), one entry per purchase type in the order, with the shop's texts from nexus or their Spanish defaults. Optional: older servers do not send it.
32
+ - `@reblu/site-blocks`: `OrderConfirmation` renders `order.purchaseTexts` under a confirmed order, with no extra wiring. New optional prop `showPurchaseTexts` (default `true`). An order without the field renders as before.
33
+
34
+ - fb9bfcc: REB-1236 follow-up: the pickup block of the order confirmation names the pickup point.
35
+ - `@reblu/site-contracts`: `ShopOrderPurchaseText.place?: { name, address: string[] }`, sent on the `pickup` entry with the pickup point frozen on the order (the same name and address lines as the confirmation email). Optional: absent on other kinds and on older servers.
36
+ - `@reblu/site-blocks`: `OrderConfirmation` shows that name and address in the pickup block, with no extra wiring.
37
+
38
+ ### Patch Changes
39
+
40
+ - Updated dependencies [8c2e641]
41
+ - Updated dependencies [fb9bfcc]
42
+ - @reblu/site-contracts@1.31.0
43
+
3
44
  ## 0.32.0
4
45
 
5
46
  ### Minor Changes
package/README.md CHANGED
@@ -334,7 +334,7 @@ for the wiring steps and a full example app).
334
334
  | `CartSummary` | `.` | `subtotal`, `discount?`, `autoDiscount?`, `shipping?`, `shippingEnabled?`, `total`, `notice?` | Yes |
335
335
  | `CheckoutLoginPrompt` | `.` | `open`, `email`, `onEmailChange`, `onClose`, `onSubmitEmail`, `onContinueAsGuest` | Yes (renders `null` when `open` is false) |
336
336
  | `CheckoutForm` | `.` | `contact`, `onContactChange`, `discountCode`, `onDiscountCodeChange`, `onApplyDiscount`, `onRemoveDiscount`, `onSubmit`, `autoDiscount?` | Yes |
337
- | `OrderConfirmation` | `.` | `status`, `order`, `cartHref?`, `onRecheck?` | Yes |
337
+ | `OrderConfirmation` | `.` | `status`, `order`, `cartHref?`, `onRecheck?`, `showPurchaseTexts?` | Yes |
338
338
  | `OrderInvoiceLink` | `.` | `invoice` (`order.invoice`), `labels?` | Yes (renders `null` without an invoice) |
339
339
  | `FullInvoiceFields` | `.` | `value`, `onChange`, `overCap`, `errors?`, `countries?`, `labels?` | Yes (controlled, no hooks) |
340
340
 
@@ -604,6 +604,29 @@ shown instead of a raw placeholder. A SITE that passes its own
604
604
  `labels.foundDescription` is unaffected: a text without `{email}` keeps the
605
605
  confirmation-email line, exactly as before.
606
606
 
607
+ **REB-1236 — what happens next, per purchase type.** The order lookup
608
+ carries `order.purchaseTexts: Array<{ kind, label, text }>`: one entry per
609
+ purchase type in the order (`shipping`, `pickup`, `digital`, `service`; a
610
+ mixed order gets one each, delivery first). `label` is the shop's checkout
611
+ label for that type and `text` the shop's own text from nexus › Tienda ›
612
+ Configuración › Confirmación, or its Spanish default. `OrderConfirmation`
613
+ renders them under a confirmed order (`paid`/`found`/`timeout`) with no extra
614
+ wiring; the SITE writes no copy of its own:
615
+
616
+ ```tsx
617
+ const { status, order, recheck } = useShopOrderStatus(sessionId)
618
+
619
+ // The per-type texts appear on their own. Pass showPurchaseTexts={false} to hide them.
620
+ <OrderConfirmation status={status} order={order} sessionId={sessionId} onRecheck={recheck} />
621
+ ```
622
+
623
+ The `pickup` entry also carries `place: { name, address }`, the pickup point
624
+ frozen on the order, and the block shows it above the text (the same lines as
625
+ the confirmation email).
626
+
627
+ A server older than REB-1236 sends no `purchaseTexts`, and the screen renders
628
+ as before.
629
+
607
630
  Every title/description is overridable via `labels` (see
608
631
  `OrderConfirmationLabels`). See `@reblu/site-next`'s README
609
632
  "`useShopOrderStatus` — the order-confirmation flow" for the full wiring
@@ -771,8 +794,9 @@ Notes:
771
794
  for a cart with nothing to ship (third argument `false`, or no choice) it
772
795
  sends only `fulfillment: { type: 'SHIPPING' }` and never the blank address
773
796
  of the hidden block (the server refuses a blank address with 400): the server records no delivery
774
- for such a cart, and the request stays on the current contract (REB-1227
775
- removes the old shape).
797
+ for such a cart. A request without `fulfillment` on a cart that ships is
798
+ read as home delivery and needs the address (422 `SHIPPING_ADDRESS_REQUIRED`,
799
+ REB-1227).
776
800
  - Pickup point (REB-1276): switching to pickup with exactly one point selects
777
801
  it (`pickupLocationId` is sent); with two or more, none is preselected.
778
802
  While pickup has no point, `isCheckoutDeliveryComplete(fulfillment)` is
@@ -978,6 +1002,62 @@ const { data: pageReviews } = await client.reviews.list({ pageId: page.id })
978
1002
  slot rule and REPLACE their defaults (`text-xl font-semibold`,
979
1003
  `sm:grid-cols-2 lg:grid-cols-3`, `text-sm text-muted-foreground`).
980
1004
 
1005
+ ### `ReviewsCarousel` — the same reviews as a carousel
1006
+
1007
+ The same cards and data rules as `Reviews`, on the package's scroll-snap
1008
+ carousel. A Client Component, from `@reblu/site-blocks/client`:
1009
+
1010
+ ```tsx
1011
+ import { ReviewsCarousel } from '@reblu/site-blocks/client'
1012
+
1013
+ // In a Server Component: fetch, then hand the data to the carousel.
1014
+ const { data: reviews } = await client.reviews.list({ limit: 9 }).catch(() => ({ data: [] }))
1015
+ <ReviewsCarousel
1016
+ reviews={reviews}
1017
+ heading='Reseñas'
1018
+ notice='Cada reseña enlaza a su original.'
1019
+ />
1020
+ ```
1021
+
1022
+ - **Accessible.** The track is a labelled, focusable region
1023
+ (`aria-roledescription="carrusel"`): swipe on touch, arrow keys once it has
1024
+ focus. «Reseña anterior» / «Reseña siguiente» buttons (from `sm` up) scroll one
1025
+ page and are disabled at the ends; they are not rendered when everything fits.
1026
+ - **Never moves by itself.** No autoplay, so there is nothing to pause.
1027
+ Button scrolling is instant under `prefers-reduced-motion: reduce`.
1028
+ - `labels` renames the region and buttons; `itemClassName` changes the slide
1029
+ width; `cardClassName`, `headingClassName` and `noticeClassName` style as in
1030
+ `Reviews`.
1031
+ - `null`, `[]` or no checkable review render nothing.
1032
+
1033
+ ### `FeaturedReviews` — the home's featured reviews, in the tenant's layout
1034
+
1035
+ The tenant chooses in nexus which reviews its home features, in what order,
1036
+ and whether they show as a grid or a carousel. `FeaturedReviews` renders
1037
+ exactly that: `grid` → the `Reviews` grid, `carousel` → `ReviewsCarousel`. An
1038
+ unknown layout falls back to the grid. Client Component, from
1039
+ `@reblu/site-blocks/client`:
1040
+
1041
+ ```tsx
1042
+ import { FeaturedReviews } from '@reblu/site-blocks/client'
1043
+
1044
+ // In a Server Component (`client.reviews.featured()` returns both).
1045
+ const featured = await client.reviews
1046
+ .featured()
1047
+ .catch(() => ({ layout: 'grid' as const, reviews: [] }))
1048
+ <FeaturedReviews reviews={featured.reviews} layout={featured.layout} heading='Reseñas' />
1049
+ ```
1050
+
1051
+ Nothing to show (no featured review, module off) renders nothing.
1052
+
1053
+ ### Why the carousel lives in `./client`
1054
+
1055
+ `ReviewsCarousel` needs browser state, and the root entry must stay loadable in
1056
+ React Server Components: re-exporting it from the root breaks every
1057
+ Server-Component render of the root entry (`test/rsc-safety.test.ts`, verified
1058
+ REB-1272). So the carousel, and everything that may render it, ships from
1059
+ `@reblu/site-blocks/client`.
1060
+
981
1061
  ## Trust badges
982
1062
 
983
1063
  `TrustBadges` renders the tenant's trust badges — partners, insurers, funding,
@@ -1140,6 +1220,65 @@ export async function ServicesTeaser() {
1140
1220
  }
1141
1221
  ```
1142
1222
 
1223
+ ## Page links — `TeamDirectory`, `LinkedPagesGrid`, `TeamGrid` (REB-1100)
1224
+
1225
+ In nexus a page can pick other pages, in order: a team profile picks its
1226
+ areas («Áreas»), and any other page picks the areas it belongs to («Páginas a
1227
+ las que pertenece»). Which category holds the «areas» is the tenant's choice;
1228
+ this package names none. The SITE API serves:
1229
+
1230
+ - `links` on every item of `client.pages.list` and on `client.pages.bySlug`:
1231
+ the published pages this page picked, in the tenant's order;
1232
+ - on `client.pages.bySlug` only, two DERIVED lists:
1233
+ - `team`: the team profiles linked to this page or to any page it links to.
1234
+ On an area page that is the area's team; on a page that belongs to areas,
1235
+ the team of those areas;
1236
+ - `relatedPages`: the other pages linked to this page or to its links. On a
1237
+ profile that is the pages of its areas; on an area, the pages that belong
1238
+ to it.
1239
+
1240
+ All three are optional on the wire: every drop-in below renders nothing (or a
1241
+ plain listing) when they are absent, so it is safe to place before the server
1242
+ emits them.
1243
+
1244
+ The team listing, with one filter chip per area (client entry):
1245
+
1246
+ ```tsx
1247
+ import { TeamDirectory } from '@reblu/site-blocks/client'
1248
+
1249
+ const { data: team } = await client.pages.list({ type: ['team'] })
1250
+
1251
+ <TeamDirectory
1252
+ members={team}
1253
+ memberHrefBase='/equipo' // → `/equipo/${slug}`; from a Client Component, `memberHref={(m) => …}` also works
1254
+ showLinksOnCards // the member's areas as chips on each card
1255
+ labels={{ all: 'Todos', filter: 'Filtrar el equipo' }} // the defaults
1256
+ />
1257
+ ```
1258
+
1259
+ A profile's areas and the pages derived from them, and the team of an area or
1260
+ of a page (RSC):
1261
+
1262
+ ```tsx
1263
+ import { LinkedPagesGrid, TeamGrid } from '@reblu/site-blocks'
1264
+
1265
+ const page = await client.pages.bySlug(slug)
1266
+ const href = (p: { slug: string; category?: { slug: string } | null }) =>
1267
+ `/${p.category?.slug}/${p.slug}`
1268
+
1269
+ // On a team profile
1270
+ <LinkedPagesGrid heading='Áreas' pages={page.links} pageHref={href} />
1271
+ <LinkedPagesGrid heading='Qué hace' pages={page.relatedPages} pageHref={href} />
1272
+
1273
+ // On an area page, or on a page that belongs to areas
1274
+ <TeamGrid heading='Equipo' members={page.team} memberHref={(m) => `/equipo/${m.slug}`} />
1275
+ ```
1276
+
1277
+ Building your own chips? `linkFilterOptions(items)` gives each distinct link
1278
+ once (`{ key, label }`, first-seen order) and `filterByLink(items, key)` the
1279
+ items linked to it (`null` keeps all). Style the drop-ins with `className` or
1280
+ `[data-slot="team-directory" | "linked-pages" | "team-grid"]`.
1281
+
1143
1282
  ## Professional fields — `ProfessionalFields` (REB-1099)
1144
1283
 
1145
1284
  Each tenant defines its own fields for team profiles in nexus › Equipo ›
@@ -1570,6 +1709,8 @@ that applies the parent section's mirrored fields (REB-849):
1570
1709
  render the slot.
1571
1710
  - **Deprecated aliases** `doctor_embed` and `specialty_embed` (REB-852) render
1572
1711
  through the `profile_embed` and `page_embed` renderers respectively.
1712
+ - A **`reviews` block** with no `reviews` (none match, or the tenant's reviews
1713
+ module is off) renders nothing, not even its heading.
1573
1714
 
1574
1715
  ### Overriding a block type
1575
1716
 
@@ -1692,6 +1833,49 @@ import { ImageTextSplit } from '@reblu/site-blocks'
1692
1833
 
1693
1834
  The legacy `sections[]` layouts `TEXT_IMAGE` / `IMAGE_TEXT` are unchanged.
1694
1835
 
1836
+ ### «Reseñas» page block (REB-1272)
1837
+
1838
+ A tenant adds a «Reseñas» section in the nexus page editor and picks which
1839
+ reviews (all published ones, or only those linked to that page), a maximum, a
1840
+ layout and an optional notice. The site API resolves the reviews onto the
1841
+ block (`block.reviews`, same shape as `client.reviews.list()`), so a SITE that
1842
+ renders `PageBlocks` needs nothing else: no extra read, no wiring.
1843
+
1844
+ ```tsx
1845
+ import { PageBlocks } from '@reblu/site-blocks'
1846
+
1847
+ // page = (await client.pages.get(slug)).data
1848
+ <PageBlocks blocks={page.blocks} />
1849
+ // A «Reseñas» block renders <Reviews reviews={block.reviews} notice={block.data.notice} />
1850
+ // inside the section.
1851
+ ```
1852
+
1853
+ **Carousel layout.** The tenant can choose `layout: 'carousel'`. The carousel
1854
+ needs the browser, and `PageBlocks` must stay a Server Component, so on its own
1855
+ it draws the grid (with `data-reviews-layout="carousel"`). To show the real
1856
+ carousel, pass the client renderer:
1857
+
1858
+ ```tsx
1859
+ import { PageBlocks } from '@reblu/site-blocks'
1860
+ import { ReviewsBlockRenderer } from '@reblu/site-blocks/client'
1861
+
1862
+ <PageBlocks blocks={page.blocks} components={{ reviews: ReviewsBlockRenderer }} />
1863
+ ```
1864
+
1865
+ To style it, override the body with the same drop-in:
1866
+
1867
+ ```tsx
1868
+ import { PageBlocks, Reviews, type BlockRendererProps } from '@reblu/site-blocks'
1869
+
1870
+ function BrandedReviews({ block }: BlockRendererProps<'reviews'>) {
1871
+ return <Reviews reviews={block.reviews} notice={block.data.notice} cardClassName='bg-muted' />
1872
+ }
1873
+
1874
+ <PageBlocks blocks={page.blocks} components={{ reviews: BrandedReviews }} />
1875
+ ```
1876
+
1877
+ An SDK older than this one renders nothing for the block (unknown type, fail-closed).
1878
+
1695
1879
  ### `FaqAccordion`
1696
1880
 
1697
1881
  Native `<details>`/`<summary>` — opens and closes with no JavaScript, keyboard