@reblu/site-blocks 0.17.1 → 0.19.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 +23 -0
- package/README.md +151 -0
- package/dist/_heading-BsNbc1wc.d.cts +4 -0
- package/dist/_heading-BsNbc1wc.d.ts +4 -0
- package/dist/client.cjs +256 -49
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +60 -2
- package/dist/client.d.ts +60 -2
- package/dist/client.js +255 -50
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +381 -308
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -8
- package/dist/index.d.ts +40 -8
- package/dist/index.js +380 -308
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# @reblu/site-blocks
|
|
2
2
|
|
|
3
|
+
## 0.19.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
- 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.
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- 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.
|
|
13
|
+
- 49384d1: README: document the `MarkdownRenderer` `images` option (URL-prefix allowlist, custom `render`, `<figure>` captions) with a `next/image` example. No code change.
|
|
14
|
+
- Updated dependencies [95f5b5f]
|
|
15
|
+
- Updated dependencies [5ef5630]
|
|
16
|
+
- Updated dependencies [e27e1b0]
|
|
17
|
+
- @reblu/site-contracts@1.19.0
|
|
18
|
+
- @reblu/site-ui@0.7.2
|
|
19
|
+
|
|
20
|
+
## 0.18.0
|
|
21
|
+
|
|
22
|
+
### Minor Changes
|
|
23
|
+
|
|
24
|
+
- 801983f: New opt-in `normalizeHeadings` prop on `MarkdownRenderer` and `SafeHtml`: the body's shallowest heading lands at `1 + headingOffset`, every other heading keeps its depth relative to it, and levels clamp at h6. Standard Markdown (`##` sections) then renders `<h2>` under the page `<h1>` just like the Reblu convention (`#` sections), which renders byte-identically. Use it where the body sits directly under the page `<h1>` (a blog post, a legal document); leave it off where the body sits under its own section heading (a product or course description). Default `false`: nothing changes until a SITE opts in.
|
|
25
|
+
|
|
3
26
|
## 0.17.1
|
|
4
27
|
|
|
5
28
|
### Patch 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,7 @@ 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. |
|
|
40
42
|
|
|
41
43
|
## Boundary
|
|
42
44
|
|
|
@@ -105,6 +107,34 @@ To force a visible break, use any of:
|
|
|
105
107
|
| A hard break inside one paragraph | Two spaces at the end of the first line |
|
|
106
108
|
| A hard break, visibly | A trailing `\` at the end of the first line |
|
|
107
109
|
|
|
110
|
+
### Heading levels: `headingOffset` and `normalizeHeadings`
|
|
111
|
+
|
|
112
|
+
`headingOffset` shifts every heading by a fixed amount: with `headingOffset={1}`
|
|
113
|
+
under the page's own `<h1>`, `#` renders `<h2>` and `##` renders `<h3>`. That
|
|
114
|
+
assumes the Reblu convention — `#` is a section — and standard Markdown pasted
|
|
115
|
+
or imported from elsewhere (`##` sections) then renders `<h3>` with no `<h2>`.
|
|
116
|
+
|
|
117
|
+
`normalizeHeadings` lands the body's **shallowest** heading at
|
|
118
|
+
`1 + headingOffset` instead, keeps every other heading's depth relative to it,
|
|
119
|
+
and clamps at h6:
|
|
120
|
+
|
|
121
|
+
| Body | `headingOffset={1}` | `+ normalizeHeadings` |
|
|
122
|
+
| --------------------------- | ------------------- | --------------------- |
|
|
123
|
+
| `#` / `##` (convention) | h2 / h3 | h2 / h3 (identical) |
|
|
124
|
+
| `##` / `###` (standard) | h3 / h4 | h2 / h3 |
|
|
125
|
+
| no headings | — | — (identical) |
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
// A blog post or legal document directly under the page <h1>:
|
|
129
|
+
<MarkdownRenderer content={post.content} headingOffset={1} normalizeHeadings />
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
It is **opt-in**, because the renderer cannot see what sits above the body.
|
|
133
|
+
Leave it off where the body sits under its own section heading — e.g. a product
|
|
134
|
+
description under «Sobre este producto» (`h2`), whose `##` subsections must stay
|
|
135
|
+
one level below that heading. `SafeHtml` takes the same prop; `CourseDescription`
|
|
136
|
+
does not use it (its `##` sits below its own heading by contract).
|
|
137
|
+
|
|
108
138
|
### `hidden` hides a block — it does not withhold it
|
|
109
139
|
|
|
110
140
|
An element marked `hidden` is not rendered and is not announced to a screen
|
|
@@ -143,6 +173,39 @@ Internal links (`/privacidad`, `#clause-4`) and `mailto:` links open in the same
|
|
|
143
173
|
tab. External links (`https://…`, `//…`) open in a new tab and always carry
|
|
144
174
|
`rel="noopener noreferrer"`.
|
|
145
175
|
|
|
176
|
+
### Images: the `images` option
|
|
177
|
+
|
|
178
|
+
Without `images`, every image the sanitiser keeps renders as a lazy `<img>`
|
|
179
|
+
inside its paragraph. Pass `images` to restrict where they may load from and,
|
|
180
|
+
optionally, how they render:
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
import Image from 'next/image'
|
|
184
|
+
|
|
185
|
+
const MEDIA = 'https://<ref>.supabase.co/storage/v1/object/public/blog-media/'
|
|
186
|
+
|
|
187
|
+
<MarkdownRenderer
|
|
188
|
+
content={post.content}
|
|
189
|
+
headingOffset={1}
|
|
190
|
+
normalizeHeadings
|
|
191
|
+
images={{
|
|
192
|
+
// Absolute URL prefixes (ending in `/`); any other image is dropped.
|
|
193
|
+
allowedUrlPrefixes: [MEDIA],
|
|
194
|
+
// Optional: render an allowed image yourself. Default: a lazy <img>.
|
|
195
|
+
render: ({ src, alt }) => <Image src={src} alt={alt} width={1600} height={900} />,
|
|
196
|
+
}}
|
|
197
|
+
/>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
- The prefix is compared on the parsed, normalised URL (`..` resolved, scheme
|
|
201
|
+
included), so `http://…` never matches an `https://…` prefix.
|
|
202
|
+
- A paragraph holding only an allowed image becomes a `<figure>`; the image
|
|
203
|
+
title (``) is its `<figcaption>`.
|
|
204
|
+
- `render` receives `{ src, alt }` only: give `next/image` a size you know
|
|
205
|
+
(e.g. encoded in the object name) or use `fill` inside a sized box, and allow
|
|
206
|
+
the host in `images.remotePatterns`.
|
|
207
|
+
- `SafeHtml` has no `images` option: its images keep the default rendering.
|
|
208
|
+
|
|
146
209
|
### Which authored attributes survive
|
|
147
210
|
|
|
148
211
|
The renderer rebuilds each element to apply the tenant's design tokens and to
|
|
@@ -736,6 +799,94 @@ export async function ServicesTeaser() {
|
|
|
736
799
|
}
|
|
737
800
|
```
|
|
738
801
|
|
|
802
|
+
## Professional fields — `ProfessionalFields` (REB-1099)
|
|
803
|
+
|
|
804
|
+
Each tenant defines its own fields for team profiles in nexus › Equipo ›
|
|
805
|
+
«Campos profesionales» — for example a registration number, a licence or a
|
|
806
|
+
membership id — and marks which are public and which also go on the card. The
|
|
807
|
+
SITE API serves the public ones as `professional.fields`
|
|
808
|
+
(`{ label, value, showOnCard }[]`): the list (`client.pages.list`) carries only
|
|
809
|
+
the card fields, the profile (`client.pages.bySlug`) every public one. The
|
|
810
|
+
labels are the tenant's; this package names no field.
|
|
811
|
+
|
|
812
|
+
On the team card, `ProfileCard` takes them directly:
|
|
813
|
+
|
|
814
|
+
```tsx
|
|
815
|
+
import { ProfileCard } from '@reblu/site-blocks'
|
|
816
|
+
|
|
817
|
+
const { data: team } = await client.pages.list({ type: ['team'] })
|
|
818
|
+
|
|
819
|
+
{team.map((p) => (
|
|
820
|
+
<ProfileCard
|
|
821
|
+
key={p.slug}
|
|
822
|
+
name={p.title}
|
|
823
|
+
role={p.professional?.role ?? undefined}
|
|
824
|
+
imageUrl={p.coverImageUrl ?? undefined}
|
|
825
|
+
fields={p.professional?.fields}
|
|
826
|
+
href={`/equipo/${p.slug}`}
|
|
827
|
+
/>
|
|
828
|
+
))}
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
On the profile page, drop in `ProfessionalFields` (a `<dl>`):
|
|
832
|
+
|
|
833
|
+
```tsx
|
|
834
|
+
import { ProfessionalFields } from '@reblu/site-blocks'
|
|
835
|
+
|
|
836
|
+
const page = await client.pages.bySlug(slug)
|
|
837
|
+
|
|
838
|
+
<ProfessionalFields fields={page.professional?.fields} />
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
`variant='card'` keeps only `showOnCard` fields. It renders nothing when there
|
|
842
|
+
is nothing to show, so it is safe to place unconditionally. Style it with
|
|
843
|
+
`className` or `[data-slot="professional-fields"]`.
|
|
844
|
+
|
|
845
|
+
## Product carousel — `ProductCarousel` (REB-1120)
|
|
846
|
+
|
|
847
|
+
A horizontal product strip for «Productos relacionados» or «También te puede
|
|
848
|
+
interesar», with no carousel dependency. The track is a native scroll container
|
|
849
|
+
with CSS scroll-snap, so touch users swipe it. From `sm` up, previous/next
|
|
850
|
+
arrows scroll one visible page. The arrows are not rendered when every product
|
|
851
|
+
fits, and the arrow at the end you have reached is disabled.
|
|
852
|
+
|
|
853
|
+
```tsx
|
|
854
|
+
'use client'
|
|
855
|
+
import { ProductCarousel } from '@reblu/site-blocks/client'
|
|
856
|
+
|
|
857
|
+
// `related` is a `ShopProductListItem[]`, e.g. from `client.shop.products({ category })`
|
|
858
|
+
<ProductCarousel
|
|
859
|
+
products={related}
|
|
860
|
+
heading='También te puede interesar'
|
|
861
|
+
productHref={(p) => `/shop/${p.slug}`}
|
|
862
|
+
/>
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
It renders the package's `ProductCard` by default. To use your own card, pass
|
|
866
|
+
`renderProduct`. Any item with an `id` works:
|
|
867
|
+
|
|
868
|
+
```tsx
|
|
869
|
+
<ProductCarousel
|
|
870
|
+
products={related}
|
|
871
|
+
heading='También te puede interesar'
|
|
872
|
+
renderProduct={(p) => <MyProductCard product={p} />}
|
|
873
|
+
/>
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
- **Accessibility.** The track is a focusable `region` with
|
|
877
|
+
`aria-roledescription="carrusel"`. It is named by `heading` when you pass one,
|
|
878
|
+
otherwise by `labels.region`. Once the track has focus, the keyboard arrow
|
|
879
|
+
keys scroll it. The arrows are `<button>`s with `aria-label`s and
|
|
880
|
+
`aria-controls` pointing at the track. Under `prefers-reduced-motion: reduce`
|
|
881
|
+
they jump instead of animating.
|
|
882
|
+
- **Copy.** The defaults are Spanish (`DEFAULT_PRODUCT_CAROUSEL_LABELS`).
|
|
883
|
+
Override them with `labels={{ region, roleDescription, previous, next }}`.
|
|
884
|
+
- **Layout.** About 1.5 cards are visible on mobile, so the next one peeks in,
|
|
885
|
+
then 3 from `sm` and 4 from `lg`. Change the width with `itemClassName`
|
|
886
|
+
(e.g. `basis-64`). Style it with `className` or `[data-slot="product-carousel"]`.
|
|
887
|
+
`headingClassName` replaces the heading's typography (see the className rule
|
|
888
|
+
above). It renders nothing for an empty list.
|
|
889
|
+
|
|
739
890
|
## Page blocks
|
|
740
891
|
|
|
741
892
|
`PageBlocks` renders a page's registry blocks (`PageDetail.blocks` from
|