@reblu/site-blocks 0.33.0 → 0.35.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 +36 -0
- package/README.md +207 -2
- package/dist/client.cjs +459 -0
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +142 -3
- package/dist/client.d.ts +142 -3
- package/dist/client.js +453 -0
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +758 -552
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +104 -78
- package/dist/index.d.ts +104 -78
- package/dist/index.js +752 -553
- package/dist/index.js.map +1 -1
- package/dist/{full-invoice-fields-DMX0sjPG.d.cts → page-links-C0nXQzAW.d.cts} +105 -2
- package/dist/{full-invoice-fields-DMX0sjPG.d.ts → page-links-C0nXQzAW.d.ts} +105 -2
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# @reblu/site-blocks
|
|
2
2
|
|
|
3
|
+
## 0.35.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 09fc578: New drop-in `CourseSchedule` (REB-1363): the modality, dates and place of an in-person or webinar course, from `AcademyCourseDetail.schedule`, to show before paying. Each date is a `<time>` in the tenant's time zone; the place of an in-person course is an `<address>`; the tenant's attendance notes follow. It renders nothing when the course has no schedule or against an API that predates the field. Server Component, RSC-safe; Spanish copy, replaceable with `labels` and `locale`.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [84423ed]
|
|
12
|
+
- Updated dependencies [3ff3c90]
|
|
13
|
+
- Updated dependencies [af8e1cf]
|
|
14
|
+
- @reblu/site-contracts@1.33.0
|
|
15
|
+
|
|
16
|
+
## 0.34.0
|
|
17
|
+
|
|
18
|
+
### Minor Changes
|
|
19
|
+
|
|
20
|
+
- 46b855f: Page links drop-ins (REB-1100). Additive only: no existing component changes.
|
|
21
|
+
- `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.
|
|
22
|
+
- `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.
|
|
23
|
+
- `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.
|
|
24
|
+
- Pure helpers `linkKey`, `linkFilterOptions` and `filterByLink` (root entry) for a SITE that builds its own chips.
|
|
25
|
+
|
|
26
|
+
The props are structural, so the drop-ins already accept today's `PageListItem` and `ResolvedEmbed`.
|
|
27
|
+
|
|
28
|
+
- 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.
|
|
29
|
+
- 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 })`.
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- 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.
|
|
34
|
+
- Updated dependencies [49d31ea]
|
|
35
|
+
- Updated dependencies [c8bf6c1]
|
|
36
|
+
- Updated dependencies [83084d0]
|
|
37
|
+
- @reblu/site-contracts@1.32.0
|
|
38
|
+
|
|
3
39
|
## 0.33.0
|
|
4
40
|
|
|
5
41
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -794,8 +794,9 @@ Notes:
|
|
|
794
794
|
for a cart with nothing to ship (third argument `false`, or no choice) it
|
|
795
795
|
sends only `fulfillment: { type: 'SHIPPING' }` and never the blank address
|
|
796
796
|
of the hidden block (the server refuses a blank address with 400): the server records no delivery
|
|
797
|
-
for such a cart
|
|
798
|
-
|
|
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).
|
|
799
800
|
- Pickup point (REB-1276): switching to pickup with exactly one point selects
|
|
800
801
|
it (`pickupLocationId` is sent); with two or more, none is preselected.
|
|
801
802
|
While pickup has no point, `isCheckoutDeliveryComplete(fulfillment)` is
|
|
@@ -1001,6 +1002,62 @@ const { data: pageReviews } = await client.reviews.list({ pageId: page.id })
|
|
|
1001
1002
|
slot rule and REPLACE their defaults (`text-xl font-semibold`,
|
|
1002
1003
|
`sm:grid-cols-2 lg:grid-cols-3`, `text-sm text-muted-foreground`).
|
|
1003
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
|
+
|
|
1004
1061
|
## Trust badges
|
|
1005
1062
|
|
|
1006
1063
|
`TrustBadges` renders the tenant's trust badges — partners, insurers, funding,
|
|
@@ -1163,6 +1220,65 @@ export async function ServicesTeaser() {
|
|
|
1163
1220
|
}
|
|
1164
1221
|
```
|
|
1165
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
|
+
|
|
1166
1282
|
## Professional fields — `ProfessionalFields` (REB-1099)
|
|
1167
1283
|
|
|
1168
1284
|
Each tenant defines its own fields for team profiles in nexus › Equipo ›
|
|
@@ -1485,6 +1601,50 @@ const course = await client.academy.bySlug(slug)
|
|
|
1485
1601
|
|
|
1486
1602
|
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.
|
|
1487
1603
|
|
|
1604
|
+
## Course dates and place — `CourseSchedule` (REB-1363)
|
|
1605
|
+
|
|
1606
|
+
The dates and place of an in-person or webinar course, which a buyer must see
|
|
1607
|
+
before paying, from `client.academy.bySlug()` → `schedule`:
|
|
1608
|
+
|
|
1609
|
+
- **Modality:** «Presencial» or «Webinar, online y en directo».
|
|
1610
|
+
- **Dates** (`sessions`): one `<time>` per date, in the tenant's time zone
|
|
1611
|
+
(`schedule.timeZone`), e.g. «martes, 1 de diciembre de 2026, de 09:00 a 13:00 (WET)».
|
|
1612
|
+
A date without an end shows its start only.
|
|
1613
|
+
- **Place** (`locationName`, `locationAddress`): in-person courses only, in an
|
|
1614
|
+
`<address>`. A webinar never carries a place, and its access link is never
|
|
1615
|
+
part of the API.
|
|
1616
|
+
- **Notes** (`attendanceNotes`): the tenant's text, e.g. how a webinar's
|
|
1617
|
+
access is given.
|
|
1618
|
+
|
|
1619
|
+
`schedule` is `null` for any other modality, and the block then renders
|
|
1620
|
+
nothing, as it does against an API that predates the field. Place it next to
|
|
1621
|
+
the purchase box. It is a Server Component, RSC-safe. The copy is Spanish;
|
|
1622
|
+
replace it with `labels` and set the date language with `locale`.
|
|
1623
|
+
|
|
1624
|
+
```tsx
|
|
1625
|
+
import { CourseSchedule } from '@reblu/site-blocks'
|
|
1626
|
+
|
|
1627
|
+
const course = await client.academy.bySlug(slug)
|
|
1628
|
+
|
|
1629
|
+
<CourseSchedule course={course} heading='Fechas y lugar' />
|
|
1630
|
+
|
|
1631
|
+
// In another language:
|
|
1632
|
+
<CourseSchedule
|
|
1633
|
+
course={course}
|
|
1634
|
+
locale='en-GB'
|
|
1635
|
+
labels={{
|
|
1636
|
+
modality: { IN_PERSON: 'In person', WEBINAR: 'Live online' },
|
|
1637
|
+
dates: 'Dates',
|
|
1638
|
+
place: 'Venue',
|
|
1639
|
+
}}
|
|
1640
|
+
/>
|
|
1641
|
+
```
|
|
1642
|
+
|
|
1643
|
+
Slots: `course-schedule` (with `data-modality`), `course-schedule-modality`,
|
|
1644
|
+
`course-schedule-dates`, `course-schedule-session`, `course-schedule-place`,
|
|
1645
|
+
`course-schedule-notes`. Styling: `className`, `headingClassName`,
|
|
1646
|
+
`bodyClassName`, `headingLevel` (default `2`), `headingId`.
|
|
1647
|
+
|
|
1488
1648
|
## Guest course checkout — `CourseGuestCheckoutForm` (REB-1264)
|
|
1489
1649
|
|
|
1490
1650
|
A course that needs no validation is bought in one step by a visitor without
|
|
@@ -1593,6 +1753,8 @@ that applies the parent section's mirrored fields (REB-849):
|
|
|
1593
1753
|
render the slot.
|
|
1594
1754
|
- **Deprecated aliases** `doctor_embed` and `specialty_embed` (REB-852) render
|
|
1595
1755
|
through the `profile_embed` and `page_embed` renderers respectively.
|
|
1756
|
+
- A **`reviews` block** with no `reviews` (none match, or the tenant's reviews
|
|
1757
|
+
module is off) renders nothing, not even its heading.
|
|
1596
1758
|
|
|
1597
1759
|
### Overriding a block type
|
|
1598
1760
|
|
|
@@ -1715,6 +1877,49 @@ import { ImageTextSplit } from '@reblu/site-blocks'
|
|
|
1715
1877
|
|
|
1716
1878
|
The legacy `sections[]` layouts `TEXT_IMAGE` / `IMAGE_TEXT` are unchanged.
|
|
1717
1879
|
|
|
1880
|
+
### «Reseñas» page block (REB-1272)
|
|
1881
|
+
|
|
1882
|
+
A tenant adds a «Reseñas» section in the nexus page editor and picks which
|
|
1883
|
+
reviews (all published ones, or only those linked to that page), a maximum, a
|
|
1884
|
+
layout and an optional notice. The site API resolves the reviews onto the
|
|
1885
|
+
block (`block.reviews`, same shape as `client.reviews.list()`), so a SITE that
|
|
1886
|
+
renders `PageBlocks` needs nothing else: no extra read, no wiring.
|
|
1887
|
+
|
|
1888
|
+
```tsx
|
|
1889
|
+
import { PageBlocks } from '@reblu/site-blocks'
|
|
1890
|
+
|
|
1891
|
+
// page = (await client.pages.get(slug)).data
|
|
1892
|
+
<PageBlocks blocks={page.blocks} />
|
|
1893
|
+
// A «Reseñas» block renders <Reviews reviews={block.reviews} notice={block.data.notice} />
|
|
1894
|
+
// inside the section.
|
|
1895
|
+
```
|
|
1896
|
+
|
|
1897
|
+
**Carousel layout.** The tenant can choose `layout: 'carousel'`. The carousel
|
|
1898
|
+
needs the browser, and `PageBlocks` must stay a Server Component, so on its own
|
|
1899
|
+
it draws the grid (with `data-reviews-layout="carousel"`). To show the real
|
|
1900
|
+
carousel, pass the client renderer:
|
|
1901
|
+
|
|
1902
|
+
```tsx
|
|
1903
|
+
import { PageBlocks } from '@reblu/site-blocks'
|
|
1904
|
+
import { ReviewsBlockRenderer } from '@reblu/site-blocks/client'
|
|
1905
|
+
|
|
1906
|
+
<PageBlocks blocks={page.blocks} components={{ reviews: ReviewsBlockRenderer }} />
|
|
1907
|
+
```
|
|
1908
|
+
|
|
1909
|
+
To style it, override the body with the same drop-in:
|
|
1910
|
+
|
|
1911
|
+
```tsx
|
|
1912
|
+
import { PageBlocks, Reviews, type BlockRendererProps } from '@reblu/site-blocks'
|
|
1913
|
+
|
|
1914
|
+
function BrandedReviews({ block }: BlockRendererProps<'reviews'>) {
|
|
1915
|
+
return <Reviews reviews={block.reviews} notice={block.data.notice} cardClassName='bg-muted' />
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
<PageBlocks blocks={page.blocks} components={{ reviews: BrandedReviews }} />
|
|
1919
|
+
```
|
|
1920
|
+
|
|
1921
|
+
An SDK older than this one renders nothing for the block (unknown type, fail-closed).
|
|
1922
|
+
|
|
1718
1923
|
### `FaqAccordion`
|
|
1719
1924
|
|
|
1720
1925
|
Native `<details>`/`<summary>` — opens and closes with no JavaScript, keyboard
|