@reblu/site-blocks 0.18.0 → 0.20.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 +33 -0
- package/README.md +179 -2
- package/dist/_heading-BsNbc1wc.d.cts +4 -0
- package/dist/_heading-BsNbc1wc.d.ts +4 -0
- package/dist/client.cjs +449 -50
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +109 -2
- package/dist/client.d.ts +109 -2
- package/dist/client.js +446 -51
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +374 -309
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -8
- package/dist/index.d.ts +51 -8
- package/dist/index.js +373 -309
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# @reblu/site-blocks
|
|
2
2
|
|
|
3
|
+
## 0.20.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 4ca8816: New `CourseSyllabus` on `@reblu/site-blocks/client`: the programme of a public course page over `AcademyCourseDetail.modules`. The header shows the number of modules and lessons. Each module with lessons is an expandable button (`aria-expanded`, `aria-controls`) showing its lesson count and total duration, and lists its published lessons in order with their duration and free-preview badge. Modules without lessons render as a plain row. Spanish default copy, overridable through `labels` (`DEFAULT_COURSE_SYLLABUS_LABELS`).
|
|
8
|
+
- 25d8189: Automatic staff discount (REB-1131). When the buyer has a server-validated staff session, base-api applies the active «Solo personal» code that gives the largest discount, with no code typed. Buyers without a staff session see no change.
|
|
9
|
+
- `@reblu/site-contracts`: adds the `ShopStaffAutoDiscount` type, `{ code, label, amount }`. It is an optional `autoDiscount` field on `ShopCartValidation`, `ShopDiscountApplication` and `ShopCheckoutSession`, and is absent for everyone else.
|
|
10
|
+
- `@reblu/site-next`: `createShopValidateCartRoute()` now forwards the `sb-*` auth cookies, the same as checkout and apply-discount. Opt out with `{ forwardAuthCookies: false }`. `validateCart()` returns `outcome.autoDiscount` when the server applied one.
|
|
11
|
+
- `@reblu/site-blocks`: `CartSummary` and `CheckoutForm` accept `autoDiscount` and show «Descuento de personal aplicado» with its amount. `CheckoutForm` shows no remove action for it, and the code field stays available. The text can be changed with `labels.staffDiscountApplied`. Without the prop, both components render exactly as before.
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 6a0f8e6: `OrderConfirmation`: the default `foundDescription` now reads «Te hemos enviado un email con tu acceso.» Every shop buyer gets a customer account, and the order confirmation email carries a one-time link into it, where paid digital downloads live. The confirmation page never serves files. A SITE that passes its own `labels.foundDescription` is unaffected.
|
|
16
|
+
- Updated dependencies [25d8189]
|
|
17
|
+
- @reblu/site-contracts@1.20.0
|
|
18
|
+
|
|
19
|
+
## 0.19.0
|
|
20
|
+
|
|
21
|
+
### Minor Changes
|
|
22
|
+
|
|
23
|
+
- 38d27c5: Add `ProfessionalFields`, a drop-in for the tenant-defined professional fields on a team profile (`professional.fields`, e.g. a registration number). `ProfileCard` gains an optional `fields` prop that shows the card fields. Additive: existing usage renders as before.
|
|
24
|
+
- 5ef5630: Add `ProductCarousel` on `@reblu/site-blocks/client`, a drop-in product strip for «Productos relacionados». It scrolls natively with scroll-snap, so touch users swipe it, and has previous/next arrows from `sm` up. The arrows are hidden when everything fits and disabled at the end you have reached. The track is a focusable, labelled region and arrow scrolling respects reduced motion. It renders `ProductCard` by default, or your own card through `renderProduct`, and needs no carousel dependency. Additive: no existing export changes.
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
- 5ef5630: Every button the organisms render shows the pointer cursor on hover under Tailwind v4: `AddToCartButton`, the `CartLineList` quantity stepper and remove button, `CheckoutForm`, `CheckoutLoginPrompt`, `Collection` pagination and facet chips, `DataRequestForm`, `OrderConfirmation` and the `ShareButtons` copy button. Disabled buttons show `cursor-not-allowed`. A source guard (`test/button-cursor.test.ts`) fails on any new `<button>` without the pointer cursor.
|
|
29
|
+
- 49384d1: README: document the `MarkdownRenderer` `images` option (URL-prefix allowlist, custom `render`, `<figure>` captions) with a `next/image` example. No code change.
|
|
30
|
+
- Updated dependencies [95f5b5f]
|
|
31
|
+
- Updated dependencies [5ef5630]
|
|
32
|
+
- Updated dependencies [e27e1b0]
|
|
33
|
+
- @reblu/site-contracts@1.19.0
|
|
34
|
+
- @reblu/site-ui@0.7.2
|
|
35
|
+
|
|
3
36
|
## 0.18.0
|
|
4
37
|
|
|
5
38
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ colour on the red background. See `docs/theming/theming.md`.
|
|
|
26
26
|
| `MarkdownRenderer` | `.` | Canonical Blog renderer: `marked` → **hast-util-sanitize** (XSS) → component map, with a YouTube-domain allowlist. RSC-safe. |
|
|
27
27
|
| `CourseDescription` | `.` | Drop-in for the rich-text course `description` (`client.academy.bySlug()`). Server Component. See "Rich-text descriptions". |
|
|
28
28
|
| `Treatment`, `ParentPage`, `ProfileCard`, `ProductCard` | `.` | Presentational organisms over the SITE API contracts. RSC-safe. `Specialty`: deprecated alias, see below. |
|
|
29
|
+
| `ProfessionalFields` | `.` | The tenant's own professional fields (`professional.fields`) on a team card or profile. RSC-safe. See below. |
|
|
29
30
|
| `ShopFilters` | `.` | Plug-and-play shop filter chrome over `GET /api/site/shop/facets`. RSC-safe. See "Shop filters" below. |
|
|
30
31
|
| `CartLineList`, `CartSummary`, `CartAdjustmentsNotice`, `AddedToCartToast`, `CheckoutLoginPrompt`, `CheckoutForm`, `OrderConfirmation` | `.` | Cart & checkout organisms. RSC-safe by construction. See "Cart & checkout" below. |
|
|
31
32
|
| `PageBlocks`, `FaqAccordion` | `.` | Registry-block renderer (`PageDetail.blocks`) and native `<details>` FAQ. Server Components. See "Page blocks" below. |
|
|
@@ -37,6 +38,8 @@ colour on the red background. See `docs/theming/theming.md`.
|
|
|
37
38
|
| `ShareButtons` | `./client` | Social share (X / LinkedIn / WhatsApp / copy). |
|
|
38
39
|
| `AddToCartButton`, `CartIcon` | `./client` | Add-to-cart feedback (REB-837) — **moved here from `.`**, see below. |
|
|
39
40
|
| `DataRequestForm` | `./client` | Drop-in public GDPR rights form over `POST /api/site/data-requests` (REB-980). See "GDPR rights form" below. |
|
|
41
|
+
| `ProductCarousel` | `./client` | Scroll-snap product strip with prev/next arrows («Productos relacionados»). See "Product carousel" below. |
|
|
42
|
+
| `CourseSyllabus`, `DEFAULT_COURSE_SYLLABUS_LABELS` | `./client` | A course's programme: expandable modules listing their lessons (`client.academy.bySlug()`). See "Course programme" below. |
|
|
40
43
|
|
|
41
44
|
## Boundary
|
|
42
45
|
|
|
@@ -171,6 +174,39 @@ Internal links (`/privacidad`, `#clause-4`) and `mailto:` links open in the same
|
|
|
171
174
|
tab. External links (`https://…`, `//…`) open in a new tab and always carry
|
|
172
175
|
`rel="noopener noreferrer"`.
|
|
173
176
|
|
|
177
|
+
### Images: the `images` option
|
|
178
|
+
|
|
179
|
+
Without `images`, every image the sanitiser keeps renders as a lazy `<img>`
|
|
180
|
+
inside its paragraph. Pass `images` to restrict where they may load from and,
|
|
181
|
+
optionally, how they render:
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
import Image from 'next/image'
|
|
185
|
+
|
|
186
|
+
const MEDIA = 'https://<ref>.supabase.co/storage/v1/object/public/blog-media/'
|
|
187
|
+
|
|
188
|
+
<MarkdownRenderer
|
|
189
|
+
content={post.content}
|
|
190
|
+
headingOffset={1}
|
|
191
|
+
normalizeHeadings
|
|
192
|
+
images={{
|
|
193
|
+
// Absolute URL prefixes (ending in `/`); any other image is dropped.
|
|
194
|
+
allowedUrlPrefixes: [MEDIA],
|
|
195
|
+
// Optional: render an allowed image yourself. Default: a lazy <img>.
|
|
196
|
+
render: ({ src, alt }) => <Image src={src} alt={alt} width={1600} height={900} />,
|
|
197
|
+
}}
|
|
198
|
+
/>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
- The prefix is compared on the parsed, normalised URL (`..` resolved, scheme
|
|
202
|
+
included), so `http://…` never matches an `https://…` prefix.
|
|
203
|
+
- A paragraph holding only an allowed image becomes a `<figure>`; the image
|
|
204
|
+
title (``) is its `<figcaption>`.
|
|
205
|
+
- `render` receives `{ src, alt }` only: give `next/image` a size you know
|
|
206
|
+
(e.g. encoded in the object name) or use `fill` inside a sized box, and allow
|
|
207
|
+
the host in `images.remotePatterns`.
|
|
208
|
+
- `SafeHtml` has no `images` option: its images keep the default rendering.
|
|
209
|
+
|
|
174
210
|
### Which authored attributes survive
|
|
175
211
|
|
|
176
212
|
The renderer rebuilds each element to apply the tenant's design tokens and to
|
|
@@ -289,13 +325,31 @@ for the wiring steps and a full example app).
|
|
|
289
325
|
| `AddedToCartToast` | `.` | `productName`, `cartHref`, `status?`, `quantity?`, `imageUrl?` | Yes |
|
|
290
326
|
| `CartLineList` | `.` | `items`, `onQuantityChange?`, `onRemove?`, `liveMessage?`, `notice?`, `lineMessage?` | Yes |
|
|
291
327
|
| `CartAdjustmentsNotice` | `.` | `removed?`, `quantityChanges?`, `priceChanges?`, `labels?` | Yes |
|
|
292
|
-
| `CartSummary` | `.` | `subtotal`, `discount?`, `shipping?`, `shippingEnabled?`, `total`, `notice?`
|
|
328
|
+
| `CartSummary` | `.` | `subtotal`, `discount?`, `autoDiscount?`, `shipping?`, `shippingEnabled?`, `total`, `notice?` | Yes |
|
|
293
329
|
| `CheckoutLoginPrompt` | `.` | `open`, `email`, `onEmailChange`, `onClose`, `onSubmitEmail`, `onContinueAsGuest` | Yes (renders `null` when `open` is false) |
|
|
294
|
-
| `CheckoutForm` | `.` | `contact`, `onContactChange`, `discountCode`, `onDiscountCodeChange`, `onApplyDiscount`, `onRemoveDiscount`, `onSubmit` | Yes |
|
|
330
|
+
| `CheckoutForm` | `.` | `contact`, `onContactChange`, `discountCode`, `onDiscountCodeChange`, `onApplyDiscount`, `onRemoveDiscount`, `onSubmit`, `autoDiscount?` | Yes |
|
|
295
331
|
| `OrderConfirmation` | `.` | `status`, `order`, `cartHref?`, `onRecheck?` | Yes |
|
|
296
332
|
| `OrderInvoiceLink` | `.` | `invoice` (`order.invoice`), `labels?` | Yes (renders `null` without an invoice) |
|
|
297
333
|
| `FullInvoiceFields` | `.` | `value`, `onChange`, `overCap`, `errors?`, `countries?`, `labels?` | Yes (controlled, no hooks) |
|
|
298
334
|
|
|
335
|
+
**Automatic staff discount (REB-1131).** For a signed-in staff member,
|
|
336
|
+
base-api applies the best «Solo personal» code by itself and reports it as
|
|
337
|
+
`autoDiscount` on validate-cart, apply-discount and checkout. Pass it to both
|
|
338
|
+
components and they show «Descuento de personal aplicado» with its amount
|
|
339
|
+
(override with `labels.staffDiscountApplied`). `CheckoutForm` offers no remove
|
|
340
|
+
action for it and keeps the code field: a typed code replaces it only when the
|
|
341
|
+
server finds it larger. Without `autoDiscount`, both render exactly as before.
|
|
342
|
+
|
|
343
|
+
```tsx
|
|
344
|
+
const outcome = await validateCart() // @reblu/site-next/cart
|
|
345
|
+
// After an apply-discount call, prefer its `autoDiscount` (absent when the
|
|
346
|
+
// typed code won).
|
|
347
|
+
const autoDiscount = discountResponse?.autoDiscount ?? outcome.autoDiscount ?? null
|
|
348
|
+
|
|
349
|
+
<CartSummary subtotal={subtotal} autoDiscount={autoDiscount} total={subtotal - (autoDiscount?.amount ?? 0)} />
|
|
350
|
+
<CheckoutForm {...checkoutProps} autoDiscount={autoDiscount} />
|
|
351
|
+
```
|
|
352
|
+
|
|
299
353
|
**REB-837 breaking move**: `AddToCartButton` and `CartIcon` now carry real
|
|
300
354
|
internal state (the confirmed/error timer, the bump-on-add guard) and ship
|
|
301
355
|
from `@reblu/site-blocks/client` instead of the root `.` entry. Update the
|
|
@@ -764,6 +818,129 @@ export async function ServicesTeaser() {
|
|
|
764
818
|
}
|
|
765
819
|
```
|
|
766
820
|
|
|
821
|
+
## Professional fields — `ProfessionalFields` (REB-1099)
|
|
822
|
+
|
|
823
|
+
Each tenant defines its own fields for team profiles in nexus › Equipo ›
|
|
824
|
+
«Campos profesionales» — for example a registration number, a licence or a
|
|
825
|
+
membership id — and marks which are public and which also go on the card. The
|
|
826
|
+
SITE API serves the public ones as `professional.fields`
|
|
827
|
+
(`{ label, value, showOnCard }[]`): the list (`client.pages.list`) carries only
|
|
828
|
+
the card fields, the profile (`client.pages.bySlug`) every public one. The
|
|
829
|
+
labels are the tenant's; this package names no field.
|
|
830
|
+
|
|
831
|
+
On the team card, `ProfileCard` takes them directly:
|
|
832
|
+
|
|
833
|
+
```tsx
|
|
834
|
+
import { ProfileCard } from '@reblu/site-blocks'
|
|
835
|
+
|
|
836
|
+
const { data: team } = await client.pages.list({ type: ['team'] })
|
|
837
|
+
|
|
838
|
+
{team.map((p) => (
|
|
839
|
+
<ProfileCard
|
|
840
|
+
key={p.slug}
|
|
841
|
+
name={p.title}
|
|
842
|
+
role={p.professional?.role ?? undefined}
|
|
843
|
+
imageUrl={p.coverImageUrl ?? undefined}
|
|
844
|
+
fields={p.professional?.fields}
|
|
845
|
+
href={`/equipo/${p.slug}`}
|
|
846
|
+
/>
|
|
847
|
+
))}
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
On the profile page, drop in `ProfessionalFields` (a `<dl>`):
|
|
851
|
+
|
|
852
|
+
```tsx
|
|
853
|
+
import { ProfessionalFields } from '@reblu/site-blocks'
|
|
854
|
+
|
|
855
|
+
const page = await client.pages.bySlug(slug)
|
|
856
|
+
|
|
857
|
+
<ProfessionalFields fields={page.professional?.fields} />
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
`variant='card'` keeps only `showOnCard` fields. It renders nothing when there
|
|
861
|
+
is nothing to show, so it is safe to place unconditionally. Style it with
|
|
862
|
+
`className` or `[data-slot="professional-fields"]`.
|
|
863
|
+
|
|
864
|
+
## Product carousel — `ProductCarousel` (REB-1120)
|
|
865
|
+
|
|
866
|
+
A horizontal product strip for «Productos relacionados» or «También te puede
|
|
867
|
+
interesar», with no carousel dependency. The track is a native scroll container
|
|
868
|
+
with CSS scroll-snap, so touch users swipe it. From `sm` up, previous/next
|
|
869
|
+
arrows scroll one visible page. The arrows are not rendered when every product
|
|
870
|
+
fits, and the arrow at the end you have reached is disabled.
|
|
871
|
+
|
|
872
|
+
```tsx
|
|
873
|
+
'use client'
|
|
874
|
+
import { ProductCarousel } from '@reblu/site-blocks/client'
|
|
875
|
+
|
|
876
|
+
// `related` is a `ShopProductListItem[]`, e.g. from `client.shop.products({ category })`
|
|
877
|
+
<ProductCarousel
|
|
878
|
+
products={related}
|
|
879
|
+
heading='También te puede interesar'
|
|
880
|
+
productHref={(p) => `/shop/${p.slug}`}
|
|
881
|
+
/>
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
It renders the package's `ProductCard` by default. To use your own card, pass
|
|
885
|
+
`renderProduct`. Any item with an `id` works:
|
|
886
|
+
|
|
887
|
+
```tsx
|
|
888
|
+
<ProductCarousel
|
|
889
|
+
products={related}
|
|
890
|
+
heading='También te puede interesar'
|
|
891
|
+
renderProduct={(p) => <MyProductCard product={p} />}
|
|
892
|
+
/>
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
- **Accessibility.** The track is a focusable `region` with
|
|
896
|
+
`aria-roledescription="carrusel"`. It is named by `heading` when you pass one,
|
|
897
|
+
otherwise by `labels.region`. Once the track has focus, the keyboard arrow
|
|
898
|
+
keys scroll it. The arrows are `<button>`s with `aria-label`s and
|
|
899
|
+
`aria-controls` pointing at the track. Under `prefers-reduced-motion: reduce`
|
|
900
|
+
they jump instead of animating.
|
|
901
|
+
- **Copy.** The defaults are Spanish (`DEFAULT_PRODUCT_CAROUSEL_LABELS`).
|
|
902
|
+
Override them with `labels={{ region, roleDescription, previous, next }}`.
|
|
903
|
+
- **Layout.** About 1.5 cards are visible on mobile, so the next one peeks in,
|
|
904
|
+
then 3 from `sm` and 4 from `lg`. Change the width with `itemClassName`
|
|
905
|
+
(e.g. `basis-64`). Style it with `className` or `[data-slot="product-carousel"]`.
|
|
906
|
+
`headingClassName` replaces the heading's typography (see the className rule
|
|
907
|
+
above). It renders nothing for an empty list.
|
|
908
|
+
|
|
909
|
+
## Course programme — `CourseSyllabus` (REB-1134)
|
|
910
|
+
|
|
911
|
+
The programme of a public course page: every module, and inside each module the
|
|
912
|
+
lessons that make it up. The header reads «Programa» and «3 módulos · 12
|
|
913
|
+
lecciones». Each module row shows its lesson count and, when every lesson in it
|
|
914
|
+
has a duration, the module's total («4 lecciones · 1 h 20 min»). Opening a
|
|
915
|
+
module lists its lessons in order, with each lesson's duration and a «Vista
|
|
916
|
+
previa gratuita» badge on free-preview lessons.
|
|
917
|
+
|
|
918
|
+
```tsx
|
|
919
|
+
import { CourseSyllabus } from '@reblu/site-blocks/client'
|
|
920
|
+
|
|
921
|
+
// `course` is an `AcademyCourseDetail` from `client.academy.bySlug(slug)`.
|
|
922
|
+
// It works from a Server Component page: every prop is serialisable.
|
|
923
|
+
<CourseSyllabus modules={course.modules} />
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
- **Public data only.** It renders what the API serves. The API returns
|
|
927
|
+
published lessons only, and never a video, playback or attachment id, a
|
|
928
|
+
signed URL or the lesson content.
|
|
929
|
+
- **Accessibility.** The block is a `region` named by its heading. A module
|
|
930
|
+
with lessons is a `<button>` with `aria-expanded` and `aria-controls`
|
|
931
|
+
pointing at its lesson list. Tab reaches it; Enter and Space open and close
|
|
932
|
+
it. Modules open independently and start closed.
|
|
933
|
+
- **Modules without lessons** render as a plain row with the title, not a
|
|
934
|
+
button. A payload without `lessons` renders the same way.
|
|
935
|
+
- **Copy.** The defaults are Spanish (`DEFAULT_COURSE_SYLLABUS_LABELS`).
|
|
936
|
+
Override any of them with `labels={{ heading, moduleSingular, modulePlural,
|
|
937
|
+
lessonSingular, lessonPlural, freePreview, hours, minutes }}`.
|
|
938
|
+
- **Styling.** `headingLevel` (default `2`), `headingClassName` (replaces the
|
|
939
|
+
heading's typography, see the className rule above) and `className`. Style
|
|
940
|
+
the parts with `[data-slot="course-syllabus"]`, `course-syllabus-module`,
|
|
941
|
+
`course-syllabus-lessons` and `course-syllabus-lesson`. It renders nothing
|
|
942
|
+
for a course without modules.
|
|
943
|
+
|
|
767
944
|
## Page blocks
|
|
768
945
|
|
|
769
946
|
`PageBlocks` renders a page's registry blocks (`PageDetail.blocks` from
|