@reblu/site-blocks 0.15.0 → 0.17.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,32 @@
1
1
  # @reblu/site-blocks
2
2
 
3
+ ## 0.17.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a42846c: `MarkdownRenderer` gains an optional `images` prop (`MarkdownImageOptions`): `allowedUrlPrefixes` keeps only images whose normalised URL starts with one of the given prefixes (any other image is dropped), a paragraph holding only an image renders as `<figure>` with the image title as `<figcaption>`, and `render` lets a Next.js consumer render allowed images with `next/image`. Without the prop, images render exactly as before.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [7a6f8c1]
12
+ - @reblu/site-contracts@1.18.0
13
+
14
+ ## 0.16.0
15
+
16
+ ### Minor Changes
17
+
18
+ - af425de: REB-1005 / REB-1002 / REB-1003:
19
+ - `DATA_REQUEST_TYPES` now comes from `@reblu/site-contracts` (same value, still exported from `./client`, now also from the server-safe root entry). New `DATA_REQUEST_FIELD_LIMITS` export.
20
+ - `DataRequestForm`: focus moves to the first invalid field on a validation error and to the success status after a submission; the inputs carry `maxLength` at the server limits (name 200, email 320, details 5000).
21
+ - `TrustBadges`: optional `heading` + `headingId` render a `<section aria-labelledby>` landmark with the group headings one level below; new `sectionHeadingClassName`.
22
+ - `TrustBadges` renders a logo only when its URL is http(s) or root-relative — any other scheme shows the badge name instead.
23
+ - **BREAKING (visual):** `TrustBadges`' `headingClassName` now follows the REB-997 slot rule: it REPLACES the default `text-sm font-semibold uppercase tracking-widest` instead of merging with it. If you pass `headingClassName`, include the full typography you want.
24
+
25
+ ### Patch Changes
26
+
27
+ - Updated dependencies [af425de]
28
+ - @reblu/site-contracts@1.17.0
29
+
3
30
  ## 0.15.0
4
31
 
5
32
  ### Minor Changes
package/README.md CHANGED
@@ -55,14 +55,17 @@ follow one rule:
55
55
  - **A slot className replaces that slot's stylistic defaults.** Passing
56
56
  `CourseDescription`'s `headingClassName` drops its `text-xl font-semibold`;
57
57
  `contentClassName` drops `text-muted-foreground`; `ProductTrustItems`'
58
- `itemClassName` drops `text-sm text-muted-foreground`. Pass the full
58
+ `itemClassName` drops `text-sm text-muted-foreground`; `TrustBadges`'
59
+ `headingClassName` drops `text-sm font-semibold uppercase tracking-widest`
60
+ (and `sectionHeadingClassName` its `text-xl font-semibold`). Pass the full
59
61
  typography you want for that slot.
60
62
  - **Structural classes stay.** Layout an element needs (the trust row's
61
63
  `flex items-center gap-3`, the `space-y-*` of a section or list, the
62
64
  `max-w-none` of `MarkdownRenderer` / `SafeHtml`) is kept, and your layout
63
65
  classes still override it through `cn` (`max-w-prose` beats `max-w-none`).
64
66
  - **Your `text-*` classes reach the DOM verbatim** on those elements
65
- (`CourseDescription`, `ProductTrustItems`, `MarkdownRenderer`, `SafeHtml`):
67
+ (`CourseDescription`, `ProductTrustItems`, `TrustBadges`' headings,
68
+ `MarkdownRenderer`, `SafeHtml`):
66
69
  they never go through tailwind-merge, so `text-h2 text-ink` renders as both.
67
70
  - **Without the prop, nothing changes** — the defaults render exactly as before.
68
71
 
@@ -630,13 +633,31 @@ export async function Footer() {
630
633
  - `types` picks which groups render and in what order, e.g. only the funding
631
634
  logos: `<TrustBadges badges={badges} types={['SUBVENTION']} />`. To fetch
632
635
  only one type, `client.trustBadges.list({ type: 'SUBVENTION' })`.
633
- - A logo is an `<img>` with `alt` = badge name; a badge without a logo shows
634
- its name as text. A badge `url` (http/https only) opens in a new window with
635
- `rel="noopener noreferrer"` and a visually hidden «(se abre en una ventana
636
- nueva)» — override it with `newWindowLabel`.
636
+ - A logo is an `<img>` with `alt` = badge name, rendered only when its URL is
637
+ http(s) or root-relative (`/logos/x.svg`); any other scheme (`javascript:`,
638
+ `data:`, `//host`) is dropped and the badge shows its name as text, like a
639
+ badge without a logo. A badge `url` (http/https only) opens in a new window
640
+ with `rel="noopener noreferrer"` and a visually hidden «(se abre en una
641
+ ventana nueva)» — override it with `newWindowLabel`.
637
642
  - `null`, `undefined` or an empty list render nothing — never an empty heading.
643
+ - **Section landmark.** Pass `heading` (e.g. «Nos avalan») and the drop-in
644
+ renders a `<section aria-labelledby={headingId}>` (default id
645
+ `trust-badges-heading`) with that heading at `headingLevel` and the group
646
+ headings one level below — so you keep the landmark without wrapping it:
647
+
648
+ ```tsx
649
+ <TrustBadges badges={badges} heading='Nos avalan' headingId='avales' headingLevel={2} />
650
+ ```
651
+
638
652
  - Styling hooks: `className`, `groupClassName`, `headingClassName`,
639
- `listClassName`, `logoClassName` (e.g. `brightness-0 invert` on a dark footer).
653
+ `sectionHeadingClassName`, `listClassName`, `logoClassName` (e.g.
654
+ `brightness-0 invert` on a dark footer). **The REB-997 slot rule applies**:
655
+ `headingClassName` replaces the group headings' `text-sm font-semibold
656
+ uppercase tracking-widest` and `sectionHeadingClassName` replaces the section
657
+ heading's `text-xl font-semibold` — pass the full typography you want; custom
658
+ `text-*` tokens reach the DOM verbatim. `className`, `groupClassName`,
659
+ `listClassName` and `logoClassName` are layout/sizing slots and still merge
660
+ with `cn`.
640
661
 
641
662
  ## `ParentPage` (formerly `Specialty`)
642
663
 
@@ -895,18 +916,29 @@ export function DataRequestSection({ siteKey }: { siteKey: string }) {
895
916
 
896
917
  ```ts
897
918
  // app/api/data-requests/route.ts — server side, the API key never reaches the browser
898
- import { createRebluClient } from '@reblu/site-client'
919
+ import { createDataRequestRoute } from '@reblu/site-next/route-handlers'
899
920
 
900
- export async function POST(request: Request) {
901
- const client = createRebluClient({ baseUrl: process.env.REBLU_API_BASE_URL!, apiKey: process.env.REBLU_API_KEY! })
902
- try {
903
- return Response.json({ data: await client.dataRequests.submit(await request.json()) })
904
- } catch (e) {
905
- return Response.json({ error: 'rejected' }, { status: (e as { status?: number }).status ?? 502 })
906
- }
907
- }
921
+ export const POST = createDataRequestRoute()
908
922
  ```
909
923
 
924
+ `createDataRequestRoute` (REB-1005) validates the body, caps it at 16 KiB,
925
+ forwards the visitor IP for base-api's per-IP limit and keeps exactly the
926
+ statuses this form maps — see the `@reblu/site-next` README.
927
+
928
+ - **Render it on the reCAPTCHA key alone — not on the contact form.** The rights
929
+ form is a legal channel the tenant must keep open even when its contact form
930
+ is `DISABLED` or `NOT_CONFIGURED`; base-api's data-request endpoint ignores
931
+ `contactFormMode` and needs only a captcha. Gate on
932
+ `isSafeRecaptchaSiteKey(analytics.recaptchaSiteKey)` (`@reblu/site-next/recaptcha`),
933
+ never on `resolveContactFormAvailability`.
934
+ - **The list of rights** is `DATA_REQUEST_TYPES` — exported from
935
+ `@reblu/site-contracts` (server-safe, one source with base-api), and
936
+ re-exported from this package's root and `./client` entries.
937
+ - **Accessibility.** On a validation error focus moves to the first invalid
938
+ field; on success to the `role="status"` confirmation. The inputs carry
939
+ `maxLength` at the server limits (`DATA_REQUEST_FIELD_LIMITS`: name 200,
940
+ email 320, details 5000), so overlong text cannot be typed.
941
+
910
942
  - **reCAPTCHA is required.** base-api rejects a request without a token, so the
911
943
  form never submits when `getCaptchaToken` resolves `undefined`.
912
944
  - **No server text reaches the visitor.** Rejections are mapped by status to
package/dist/client.cjs CHANGED
@@ -34,7 +34,8 @@ __export(client_exports, {
34
34
  AddToCartButton: () => AddToCartButton,
35
35
  CartIcon: () => CartIcon,
36
36
  Collection: () => Collection,
37
- DATA_REQUEST_TYPES: () => DATA_REQUEST_TYPES,
37
+ DATA_REQUEST_FIELD_LIMITS: () => DATA_REQUEST_FIELD_LIMITS,
38
+ DATA_REQUEST_TYPES: () => import_site_contracts.DATA_REQUEST_TYPES,
38
39
  DEFAULT_DATA_REQUEST_FORM_LABELS: () => DEFAULT_DATA_REQUEST_FORM_LABELS,
39
40
  DataRequestForm: () => DataRequestForm,
40
41
  ShareButtons: () => ShareButtons,
@@ -529,17 +530,9 @@ function CartIcon({ itemCount, href, hydrated, bumpOnAdd, labels, className }) {
529
530
  // src/components/data-request-form.tsx
530
531
  var import_react4 = require("react");
531
532
  var import_utils5 = require("@reblu/site-ui/utils");
533
+ var import_site_contracts = require("@reblu/site-contracts");
532
534
  var import_jsx_runtime5 = require("react/jsx-runtime");
533
- var DATA_REQUEST_TYPES = [
534
- "ACCESS",
535
- "RECTIFICATION",
536
- "ERASURE",
537
- "OBJECTION",
538
- "PORTABILITY",
539
- "RESTRICTION",
540
- "AUTOMATED_DECISION",
541
- "INFORMATION"
542
- ];
535
+ var DATA_REQUEST_FIELD_LIMITS = Object.freeze({ name: 200, email: 320, details: 5e3 });
543
536
  var DEFAULT_DATA_REQUEST_FORM_LABELS = {
544
537
  requestTypeLabel: "Derecho que quieres ejercer",
545
538
  requestTypePlaceholder: "Selecciona una opci\xF3n",
@@ -574,6 +567,7 @@ var DEFAULT_DATA_REQUEST_FORM_LABELS = {
574
567
  rateLimitError: "Has enviado demasiadas solicitudes. Espera unos minutos e int\xE9ntalo de nuevo.",
575
568
  genericError: "No hemos podido enviar tu solicitud. Int\xE9ntalo de nuevo m\xE1s tarde."
576
569
  };
570
+ var FIELD_ORDER = ["requestType", "name", "email", "consent"];
577
571
  var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
578
572
  async function postToRoute(endpoint, input) {
579
573
  try {
@@ -602,7 +596,7 @@ function DataRequestForm({
602
596
  endpoint = "/api/data-requests",
603
597
  onInteract,
604
598
  onSubmitted,
605
- requestTypes = DATA_REQUEST_TYPES,
599
+ requestTypes = import_site_contracts.DATA_REQUEST_TYPES,
606
600
  labels,
607
601
  locale = "es-ES",
608
602
  className
@@ -622,6 +616,10 @@ function DataRequestForm({
622
616
  const [formError, setFormError] = (0, import_react4.useState)(null);
623
617
  const [submitting, setSubmitting] = (0, import_react4.useState)(false);
624
618
  const [ack, setAck] = (0, import_react4.useState)(null);
619
+ const statusRef = (0, import_react4.useRef)(null);
620
+ (0, import_react4.useEffect)(() => {
621
+ if (ack) statusRef.current?.focus();
622
+ }, [ack]);
625
623
  async function handleSubmit(event) {
626
624
  event.preventDefault();
627
625
  if (submitting) return;
@@ -632,7 +630,12 @@ function DataRequestForm({
632
630
  if (!EMAIL_PATTERN.test(email.trim())) next.email = t.emailInvalid;
633
631
  if (!consent) next.consent = t.consentRequired;
634
632
  setErrors(next);
635
- if (Object.keys(next).length > 0 || !requestType) return;
633
+ const firstInvalid = FIELD_ORDER.find((field) => next[field]);
634
+ if (firstInvalid) {
635
+ document.getElementById(`${id}-${firstInvalid === "requestType" ? "type" : firstInvalid}`)?.focus();
636
+ return;
637
+ }
638
+ if (!requestType) return;
636
639
  setSubmitting(true);
637
640
  try {
638
641
  const captcha = await getCaptchaToken().catch(() => void 0);
@@ -665,7 +668,17 @@ function DataRequestForm({
665
668
  dateStyle: "long",
666
669
  timeStyle: "short"
667
670
  }).format(new Date(ack.slaDueAt));
668
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { role: "status", "data-slot": "data-request-form-success", className: (0, import_utils5.cn)("rounded-lg border border-border p-6 text-sm", className), children: t.success(deadline) });
671
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
672
+ "div",
673
+ {
674
+ ref: statusRef,
675
+ tabIndex: -1,
676
+ role: "status",
677
+ "data-slot": "data-request-form-success",
678
+ className: (0, import_utils5.cn)("rounded-lg border border-border p-6 text-sm", className),
679
+ children: t.success(deadline)
680
+ }
681
+ );
669
682
  }
670
683
  const errorId = (field) => errors[field] ? `${id}-${field}-error` : void 0;
671
684
  const fieldError = (field) => errors[field] ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { id: `${id}-${field}-error`, className: "text-xs text-destructive", children: errors[field] }) : null;
@@ -706,6 +719,7 @@ function DataRequestForm({
706
719
  id: `${id}-name`,
707
720
  type: "text",
708
721
  autoComplete: "name",
722
+ maxLength: DATA_REQUEST_FIELD_LIMITS.name,
709
723
  value: name,
710
724
  onChange: (e) => setName(e.target.value),
711
725
  "aria-invalid": errors.name ? true : void 0,
@@ -723,6 +737,7 @@ function DataRequestForm({
723
737
  id: `${id}-email`,
724
738
  type: "email",
725
739
  autoComplete: "email",
740
+ maxLength: DATA_REQUEST_FIELD_LIMITS.email,
726
741
  value: email,
727
742
  onChange: (e) => setEmail(e.target.value),
728
743
  "aria-invalid": errors.email ? true : void 0,
@@ -739,6 +754,7 @@ function DataRequestForm({
739
754
  {
740
755
  id: `${id}-details`,
741
756
  rows: 4,
757
+ maxLength: DATA_REQUEST_FIELD_LIMITS.details,
742
758
  value: details,
743
759
  onChange: (e) => setDetails(e.target.value),
744
760
  placeholder: t.detailsPlaceholder,
@@ -783,6 +799,7 @@ function DataRequestForm({
783
799
  AddToCartButton,
784
800
  CartIcon,
785
801
  Collection,
802
+ DATA_REQUEST_FIELD_LIMITS,
786
803
  DATA_REQUEST_TYPES,
787
804
  DEFAULT_DATA_REQUEST_FORM_LABELS,
788
805
  DataRequestForm,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/hooks/use-collection.ts","../src/components/collection.tsx","../src/components/share-buttons.tsx","../src/lib/share-urls.ts","../src/components/add-to-cart-button.tsx","../src/components/cart-icon.tsx","../src/components/data-request-form.tsx"],"sourcesContent":["'use client'\n\n// `@reblu/site-blocks/client` — the client-only surface. Every export here needs\n// browser state or hooks, so the entry carries the `'use client'` directive and\n// each SITE consumes it from a Client Component boundary.\n\nexport {\n useCollection,\n type CollectionFilter,\n type UseCollectionOptions,\n type UseCollectionResult,\n} from './hooks/use-collection'\n\nexport { Collection } from './components/collection'\nexport type {\n CollectionProps,\n CollectionFacet,\n CollectionFacetOption,\n CollectionLabels,\n} from './components/collection'\n\nexport { ShareButtons } from './components/share-buttons'\nexport type { ShareButtonsProps, ShareButtonsLabels } from './components/share-buttons'\n\n// ── Cart feedback (REB-837) ──────────────────────────────────────────────────\n// `AddToCartButton`/`CartIcon` moved here from the RSC-safe root: both now\n// carry real internal state (confirmed/error timer, bump-on-add guard).\nexport {\n AddToCartButton,\n type AddToCartButtonProps,\n type AddToCartButtonLabels,\n type AddToCartButtonStatus,\n} from './components/add-to-cart-button'\nexport { CartIcon, type CartIconProps, type CartIconLabels } from './components/cart-icon'\n\n// ── GDPR rights form (REB-980) ───────────────────────────────────────────────\n// Drop-in public form over `POST /api/site/data-requests` (REB-77).\nexport {\n DataRequestForm,\n DATA_REQUEST_TYPES,\n DEFAULT_DATA_REQUEST_FORM_LABELS,\n type DataRequestFormProps,\n type DataRequestFormLabels,\n type DataRequestSubmitResult,\n} from './components/data-request-form'\n","'use client'\n\nimport { useCallback, useMemo, useState } from 'react'\n\n/**\n * `useCollection` — the headless heart of `@reblu/site-blocks`.\n *\n * A framework-agnostic, presentation-free hook that turns an in-memory array into\n * a filtered / searched / sorted / paginated view. It backs BOTH the page index\n * and the blog index: the facets differ (category, tag, …), the machinery does\n * not. Callers own how items look (`Collection` or their own markup); this hook\n * only owns *which* items and in *what* order.\n *\n * All derivation is memoised and pure — comparators run over `toSorted`, never\n * mutating the caller's array.\n */\n\n/** A named facet: `key` identifies it, `predicate` decides membership for a value. */\nexport interface CollectionFilter<T> {\n key: string\n predicate: (item: T, value: string) => boolean\n}\n\nexport interface UseCollectionOptions<T> {\n items: T[]\n /** Page size. `0`/omitted → no pagination (one page with everything). */\n pageSize?: number\n initialPage?: number\n filters?: CollectionFilter<T>[]\n initialFilters?: Record<string, string | undefined>\n /** Free-text matcher; only applied when a non-empty term is set. */\n search?: (item: T, term: string) => boolean\n initialSearch?: string\n /** Named comparators, selected by `sort`. */\n sorts?: Record<string, (a: T, b: T) => number>\n initialSort?: string\n}\n\nexport interface UseCollectionResult<T> {\n /** Items on the current page (or all items when pagination is off). */\n items: T[]\n /** Full filtered + sorted set, before pagination. */\n filtered: T[]\n /** Size of `filtered`. */\n total: number\n page: number\n pageCount: number\n pageSize: number\n isEmpty: boolean\n activeFilters: Record<string, string | undefined>\n setFilter: (key: string, value: string | undefined) => void\n clearFilters: () => void\n search: string\n setSearch: (term: string) => void\n sort: string | undefined\n setSort: (key: string | undefined) => void\n setPage: (page: number) => void\n nextPage: () => void\n prevPage: () => void\n}\n\nexport function useCollection<T>(options: UseCollectionOptions<T>): UseCollectionResult<T> {\n const { items, pageSize = 0, filters = [], search: searchFn, sorts = {} } = options\n\n const [activeFilters, setActiveFilters] = useState<Record<string, string | undefined>>(\n () => options.initialFilters ?? {},\n )\n const [search, setSearchState] = useState(() => options.initialSearch ?? '')\n const [sort, setSortState] = useState<string | undefined>(() => options.initialSort)\n const [page, setPageState] = useState(() => options.initialPage ?? 1)\n\n const filtered = useMemo(() => {\n let next = items\n\n for (const { key, predicate } of filters) {\n const value = activeFilters[key]\n if (value != null && value !== '') {\n next = next.filter((item) => predicate(item, value))\n }\n }\n\n const term = search.trim()\n if (searchFn && term) {\n next = next.filter((item) => searchFn(item, term))\n }\n\n const comparator = sort ? sorts[sort] : undefined\n if (comparator) {\n next = next.slice().sort(comparator)\n }\n\n return next\n }, [items, filters, activeFilters, search, searchFn, sort, sorts])\n\n const total = filtered.length\n const pageCount = pageSize > 0 ? Math.max(1, Math.ceil(total / pageSize)) : 1\n // Derive the effective page so a shrinking result set never strands us past the\n // last page — state stays as-is, but what we render is always clamped.\n const currentPage = Math.min(Math.max(1, page), pageCount)\n\n const pageItems = useMemo(() => {\n if (pageSize <= 0) return filtered\n const start = (currentPage - 1) * pageSize\n return filtered.slice(start, start + pageSize)\n }, [filtered, pageSize, currentPage])\n\n // Any change to what we're viewing snaps back to the first page.\n const resetToFirstPage = useCallback(() => setPageState(1), [])\n\n const setFilter = useCallback(\n (key: string, value: string | undefined) => {\n setActiveFilters((prev) => ({ ...prev, [key]: value }))\n resetToFirstPage()\n },\n [resetToFirstPage],\n )\n\n const clearFilters = useCallback(() => {\n setActiveFilters({})\n resetToFirstPage()\n }, [resetToFirstPage])\n\n const setSearch = useCallback(\n (term: string) => {\n setSearchState(term)\n resetToFirstPage()\n },\n [resetToFirstPage],\n )\n\n const setSort = useCallback(\n (key: string | undefined) => {\n setSortState(key)\n resetToFirstPage()\n },\n [resetToFirstPage],\n )\n\n // Navigation clamps against the live page bounds so a shrinking result set or\n // an over-shooting jump can never strand the state past the last page.\n const setPage = useCallback(\n (next: number) => setPageState(Math.min(Math.max(1, next), pageCount)),\n [pageCount],\n )\n const nextPage = useCallback(\n () => setPageState(Math.min(currentPage + 1, pageCount)),\n [currentPage, pageCount],\n )\n const prevPage = useCallback(() => setPageState(Math.max(1, currentPage - 1)), [currentPage])\n\n return {\n items: pageItems,\n filtered,\n total,\n page: currentPage,\n pageCount,\n pageSize,\n isEmpty: total === 0,\n activeFilters,\n setFilter,\n clearFilters,\n search,\n setSearch,\n sort,\n setSort,\n setPage,\n nextPage,\n prevPage,\n }\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '@reblu/site-ui/utils'\n\nimport { useCollection, type CollectionFilter } from '../hooks/use-collection'\n\n/**\n * `Collection` — token-driven presentational grid over {@link useCollection}.\n *\n * The headless hook owns filtering/sorting/pagination; this component owns the\n * chrome: a facet chip bar, the item grid (via a `renderItem` prop), and simple\n * pagination. It carries no brand aesthetic — colors come from tokens; a facet\n * option may carry a `color` (tenant hex) which is applied through a CSS custom\n * property, never baked into a class name.\n *\n * Serves both the page index and the blog index; only the facets differ.\n */\nexport interface CollectionFacetOption {\n value: string\n label: string\n /** Optional tenant color (hex). Applied via a CSS var, not a hardcoded class. */\n color?: string\n}\n\nexport interface CollectionFacet {\n key: string\n label: string\n options: CollectionFacetOption[]\n /** Label for the \"clear this facet\" chip. Defaults to \"All\". */\n allLabel?: string\n}\n\nexport interface CollectionLabels {\n previous: string\n next: string\n page: (page: number, pageCount: number) => string\n}\n\nconst DEFAULT_LABELS: CollectionLabels = {\n previous: 'Previous',\n next: 'Next',\n page: (page, pageCount) => `Page ${page} of ${pageCount}`,\n}\n\nexport interface CollectionProps<T> {\n items: T[]\n renderItem: (item: T) => React.ReactNode\n getKey: (item: T) => string\n filters?: CollectionFilter<T>[]\n facets?: CollectionFacet[]\n pageSize?: number\n sorts?: Record<string, (a: T, b: T) => number>\n initialFilters?: Record<string, string | undefined>\n initialSort?: string\n emptyLabel?: string\n className?: string\n /** Tailwind classes for the grid container. */\n gridClassName?: string\n labels?: Partial<CollectionLabels>\n}\n\nexport function Collection<T>({\n items,\n renderItem,\n getKey,\n filters,\n facets = [],\n pageSize = 0,\n sorts,\n initialFilters,\n initialSort,\n emptyLabel = 'No results',\n className,\n gridClassName = 'grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3',\n labels,\n}: CollectionProps<T>) {\n const t = { ...DEFAULT_LABELS, ...labels }\n const collection = useCollection<T>({\n items,\n filters,\n pageSize,\n sorts,\n initialFilters,\n initialSort,\n })\n\n return (\n <div className={cn('space-y-8', className)}>\n {facets.length > 0 && (\n <div className='space-y-4'>\n {facets.map((facet) => {\n const active = collection.activeFilters[facet.key]\n return (\n <div\n key={facet.key}\n role='group'\n aria-label={facet.label}\n className='flex flex-wrap gap-2'\n >\n <FacetChip\n label={facet.allLabel ?? 'All'}\n pressed={active == null || active === ''}\n onClick={() => collection.setFilter(facet.key, undefined)}\n />\n {facet.options.map((opt) => (\n <FacetChip\n key={opt.value}\n label={opt.label}\n color={opt.color}\n pressed={active === opt.value}\n onClick={() => collection.setFilter(facet.key, opt.value)}\n />\n ))}\n </div>\n )\n })}\n </div>\n )}\n\n {collection.isEmpty ? (\n <p className='py-16 text-center text-muted-foreground'>{emptyLabel}</p>\n ) : (\n <ul className={cn('list-none p-0', gridClassName)}>\n {collection.items.map((item) => (\n <li key={getKey(item)}>{renderItem(item)}</li>\n ))}\n </ul>\n )}\n\n {collection.pageCount > 1 && (\n <nav aria-label='Pagination' className='flex items-center justify-center gap-4'>\n <button\n type='button'\n onClick={collection.prevPage}\n disabled={collection.page <= 1}\n className={PAGE_BTN_CLASS}\n >\n {t.previous}\n </button>\n <span aria-current='page' className='text-sm text-muted-foreground'>\n {t.page(collection.page, collection.pageCount)}\n </span>\n <button\n type='button'\n onClick={collection.nextPage}\n disabled={collection.page >= collection.pageCount}\n className={PAGE_BTN_CLASS}\n >\n {t.next}\n </button>\n </nav>\n )}\n </div>\n )\n}\n\nconst PAGE_BTN_CLASS =\n 'inline-flex h-9 items-center rounded-md border border-border px-4 text-sm font-medium transition-colors hover:bg-muted disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'\n\ninterface FacetChipProps {\n label: string\n pressed: boolean\n color?: string\n onClick: () => void\n}\n\nfunction FacetChip({ label, pressed, color, onClick }: FacetChipProps) {\n return (\n <button\n type='button'\n aria-pressed={pressed}\n onClick={onClick}\n // The tenant color rides in a CSS var; the class picks it up when pressed,\n // so no hex string is ever baked into a Tailwind class.\n style={color ? ({ '--facet-color': color } as React.CSSProperties) : undefined}\n className={cn(\n 'inline-flex items-center rounded-full border px-3 py-1 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n pressed\n ? color\n ? 'border-transparent bg-[var(--facet-color)] text-white'\n : 'border-transparent bg-primary text-primary-foreground'\n : 'border-border text-muted-foreground hover:bg-muted hover:text-foreground',\n )}\n >\n {label}\n </button>\n )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '@reblu/site-ui/utils'\n\nimport { buildShareUrls } from '../lib/share-urls'\n\n/**\n * `ShareButtons` — social share row for a blog post (LinkedIn / X / Facebook /\n * WhatsApp) plus a copy-to-clipboard action. Token-driven and icon-only for the\n * providers; the copy action shows a text confirmation.\n *\n * Copy defaults to English (distributable default). A Spanish-or-other SITE can\n * override every visible string via `labels` without forking the component.\n */\nexport interface ShareButtonsLabels {\n share: string\n copy: string\n copied: string\n linkedin: string\n x: string\n facebook: string\n whatsapp: string\n copyStatus: string\n}\n\nconst DEFAULT_LABELS: ShareButtonsLabels = {\n share: 'Share',\n copy: 'Copy link',\n copied: 'Link copied',\n linkedin: 'Share on LinkedIn',\n x: 'Share on X',\n facebook: 'Share on Facebook',\n whatsapp: 'Share on WhatsApp',\n copyStatus: 'Link copied to clipboard',\n}\n\nexport interface ShareButtonsProps {\n /** Canonical, absolute URL of the post being shared. */\n url: string\n /** Post title, used where the provider supports pre-filled copy. */\n title: string\n className?: string\n labels?: Partial<ShareButtonsLabels>\n}\n\nconst LINK_CLASS =\n 'inline-flex h-9 w-9 items-center justify-center rounded-full border border-border text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'\n\nconst ICON = 'h-4 w-4'\n\nfunction LinkedInIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='currentColor' aria-hidden='true' focusable='false'>\n <path d='M19 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2zM8.34 18.34V10.9H5.9v7.44zM7.12 9.82a1.42 1.42 0 1 0 0-2.84 1.42 1.42 0 0 0 0 2.84m11.22 8.52v-4.08c0-2.18-1.16-3.19-2.72-3.19a2.35 2.35 0 0 0-2.13 1.17v-1h-2.43v7.1h2.43v-3.93c0-1 .2-2 1.46-2s1.28 1.16 1.28 2.06v3.87z' />\n </svg>\n )\n}\n\nfunction XIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='currentColor' aria-hidden='true' focusable='false'>\n <path d='M18.244 2.25h3.308l-7.227 8.26 8.502 11.24h-6.66l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z' />\n </svg>\n )\n}\n\nfunction FacebookIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='currentColor' aria-hidden='true' focusable='false'>\n <path d='M22 12a10 10 0 1 0-11.56 9.88v-6.99H7.9V12h2.54V9.8c0-2.5 1.49-3.89 3.78-3.89 1.09 0 2.24.2 2.24.2v2.46h-1.26c-1.24 0-1.63.77-1.63 1.56V12h2.78l-.44 2.89h-2.34v6.99A10 10 0 0 0 22 12' />\n </svg>\n )\n}\n\nfunction WhatsAppIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='currentColor' aria-hidden='true' focusable='false'>\n <path d='M12.04 2C6.58 2 2.13 6.45 2.13 11.91c0 1.75.46 3.45 1.32 4.95L2 22l5.25-1.38a9.9 9.9 0 0 0 4.79 1.22h.01c5.46 0 9.91-4.45 9.91-9.91 0-2.65-1.03-5.14-2.9-7.01A9.82 9.82 0 0 0 12.04 2m0 1.67c2.2 0 4.27.86 5.82 2.42a8.19 8.19 0 0 1 2.42 5.82c0 4.54-3.7 8.24-8.25 8.24a8.2 8.2 0 0 1-4.2-1.15l-.3-.18-3.12.82.83-3.04-.2-.31a8.2 8.2 0 0 1-1.26-4.38c0-4.54 3.7-8.24 8.25-8.24m4.52 10.36c-.06-.1-.22-.16-.46-.28s-1.46-.72-1.68-.8-.39-.12-.55.12-.63.8-.77.96-.28.18-.52.06a6.7 6.7 0 0 1-1.98-1.22 7.4 7.4 0 0 1-1.37-1.7c-.14-.24-.01-.37.11-.49.11-.11.24-.28.37-.42.12-.14.16-.24.24-.4s.04-.3-.02-.42-.55-1.32-.75-1.81c-.2-.48-.4-.41-.55-.42h-.47a.9.9 0 0 0-.65.3c-.22.24-.85.83-.85 2.02s.87 2.35 1 2.51c.12.16 1.71 2.61 4.14 3.66.58.25 1.03.4 1.38.51.58.19 1.11.16 1.53.1.47-.07 1.46-.6 1.66-1.17.21-.58.21-1.07.15-1.17' />\n </svg>\n )\n}\n\nfunction CopyIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='none' stroke='currentColor' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round' aria-hidden='true' focusable='false'>\n <rect x='9' y='9' width='13' height='13' rx='2' ry='2' />\n <path d='M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1' />\n </svg>\n )\n}\n\nfunction CheckIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='none' stroke='currentColor' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round' aria-hidden='true' focusable='false'>\n <polyline points='20 6 9 17 4 12' />\n </svg>\n )\n}\n\nexport function ShareButtons({ url, title, className, labels }: ShareButtonsProps) {\n const t = { ...DEFAULT_LABELS, ...labels }\n const [copied, setCopied] = React.useState(false)\n const share = buildShareUrls(url, title)\n\n async function handleCopy() {\n try {\n await navigator.clipboard.writeText(url)\n setCopied(true)\n window.setTimeout(() => setCopied(false), 2000)\n } catch {\n // Clipboard can be unavailable (permissions, insecure context); fail quietly.\n }\n }\n\n return (\n <div className={cn('flex items-center gap-2', className)} role='group' aria-label={t.share}>\n <span className='mr-1 text-sm font-medium text-muted-foreground'>{t.share}</span>\n\n <a href={share.linkedin} target='_blank' rel='noopener noreferrer' className={LINK_CLASS} aria-label={t.linkedin}>\n <LinkedInIcon />\n </a>\n <a href={share.x} target='_blank' rel='noopener noreferrer' className={LINK_CLASS} aria-label={t.x}>\n <XIcon />\n </a>\n <a href={share.facebook} target='_blank' rel='noopener noreferrer' className={LINK_CLASS} aria-label={t.facebook}>\n <FacebookIcon />\n </a>\n <a href={share.whatsapp} target='_blank' rel='noopener noreferrer' className={LINK_CLASS} aria-label={t.whatsapp}>\n <WhatsAppIcon />\n </a>\n\n <button\n type='button'\n onClick={handleCopy}\n className={cn(LINK_CLASS, 'w-auto gap-2 px-3', copied && 'border-primary text-primary')}\n aria-label={t.copy}\n >\n {copied ? <CheckIcon /> : <CopyIcon />}\n <span className='text-sm'>{copied ? t.copied : t.copy}</span>\n </button>\n\n {/* Polite live region so screen readers announce the copy confirmation. */}\n <span role='status' aria-live='polite' className='sr-only'>\n {copied ? t.copyStatus : ''}\n </span>\n </div>\n )\n}\n","/**\n * Pure builders for the social-share targets used by `ShareButtons`.\n *\n * Framework-free so the URL construction is unit-testable without rendering.\n * Every provider gets the canonical post URL; providers that support custom copy\n * (X, WhatsApp) also receive the post title.\n */\nexport interface ShareUrls {\n linkedin: string\n x: string\n facebook: string\n whatsapp: string\n}\n\nexport function buildShareUrls(url: string, title: string): ShareUrls {\n const encodedUrl = encodeURIComponent(url)\n const encodedTitle = encodeURIComponent(title)\n\n return {\n // LinkedIn derives title/description from the target page's Open Graph tags,\n // so it only takes the URL.\n linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`,\n // The intent endpoint keeps the /intent/tweet path under the x.com domain.\n x: `https://x.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}`,\n facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`,\n whatsapp: `https://wa.me/?text=${encodeURIComponent(`${title} ${url}`)}`,\n }\n}\n","'use client'\n\nimport { useEffect, useRef, useState } from 'react'\n\nimport { cn } from '@reblu/site-ui/utils'\n\n/**\n * `AddToCartButton` — add-to-cart CTA with a built-in confirmed/error state\n * (REB-837). Moved to the `./client` entry (from the RSC-safe root) because\n * the confirmed-state timer and the `error` prop watcher genuinely need\n * `useState`/`useEffect` — see `client.ts`'s own leading `'use client'`\n * directive (R4: tsup only preserves a directive declared by the entry file\n * itself, not one merely re-exported from a child module).\n *\n * Previously (REB-829) this component had no internal state at all — it was\n * a pure `onAddToCart?: () => void` button exported from the RSC-safe root.\n * A SITE importing `AddToCartButton` from `@reblu/site-blocks` (the root\n * entry) must now import it from `@reblu/site-blocks/client` instead.\n */\nexport interface AddToCartButtonLabels {\n add: string\n adding: string\n outOfStock: string\n /** Shown for ~`successDurationMs` after a successful add. */\n added: string\n /** Fallback shown when `error` is `true` (not a string). */\n error: string\n}\n\nconst DEFAULT_LABELS: AddToCartButtonLabels = {\n add: 'Añadir al carrito',\n adding: 'Añadiendo…',\n outOfStock: 'Agotado',\n added: 'Añadido ✓',\n error: 'No se pudo añadir',\n}\n\nexport type AddToCartButtonStatus = 'idle' | 'success' | 'error'\n\n/**\n * Structural subset of `@reblu/site-next/cart`'s `AddItemOutcome` (REB-837\n * review fix) — `site-blocks` never depends on `site-next`, so this is\n * duplicated shape, not an import; a real `AddItemOutcome` (which also\n * carries `quantity`) satisfies this type as-is.\n */\nexport interface AddToCartOutcome {\n status: 'added' | 'clamped' | 'refused'\n}\n\nexport interface AddToCartButtonProps<TItem = unknown> {\n /**\n * Called on click. MAY return the outcome of the add (synchronously or\n * via a `Promise`) so the button can decide success vs error from the\n * RESULT instead of only from the externally-controlled `error` prop\n * (REB-837 review fix: deciding purely from `error` captured at render\n * time showed \"Añadido ✓\" for a REFUSED add whenever the caller never\n * flips `error` back on). `'refused'` → error state; `'added'`/`'clamped'`\n * → confirmed state; returning nothing (`void`) stays backward compatible\n * with the original fire-and-forget contract — treated as success.\n */\n onAddToCart?: (item?: TItem) => AddToCartOutcome | void | Promise<AddToCartOutcome | void>\n disabled?: boolean\n outOfStock?: boolean\n /** True while the add-to-cart call (or a navigation it triggers) is in flight. */\n pending?: boolean\n /**\n * Controlled error (out-of-stock race, max-quantity reached, a failed\n * mutation…). Set it truthy right after `onAddToCart` fails to trigger the\n * error state; clear it (`undefined`/`false`) to let the button reset. A\n * `string` is shown as the button's label; `true` falls back to\n * `labels.error`.\n */\n error?: string | boolean\n /** Opaque item forwarded verbatim to `onAddToCart`/`onAdded`/`onError` — never read internally. */\n item?: TItem\n /** How long the success/error state shows before reverting to idle, ms. @default 1500 */\n successDurationMs?: number\n /** Fires once per successful add, right after `onAddToCart`. */\n onAdded?: (item?: TItem) => void\n /** Fires once when `error` transitions from falsy to truthy. */\n onError?: (item?: TItem, error?: string | boolean) => void\n labels?: Partial<AddToCartButtonLabels>\n className?: string\n}\n\nexport function AddToCartButton<TItem = unknown>({\n onAddToCart,\n disabled,\n outOfStock,\n pending,\n error,\n item,\n successDurationMs = 1500,\n onAdded,\n onError,\n labels,\n className,\n}: AddToCartButtonProps<TItem>) {\n const t = { ...DEFAULT_LABELS, ...labels }\n const [status, setStatus] = useState<AddToCartButtonStatus>('idle')\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n const prevErrorRef = useRef<string | boolean | undefined>(error)\n // Non-reentrancy guard (REB-837): `disabled`/`aria-disabled` already block\n // both pointer and keyboard activation of a native <button>, but a ref\n // guard is the actual source of truth for \"is a success window open OR an\n // async onAddToCart is still resolving\" — defense in depth against\n // anything that might still dispatch a click, and the only thing that\n // actually blocks a double-submit while a Promise is pending.\n const inSuccessWindowRef = useRef(false)\n const resolvingRef = useRef(false)\n const mountedRef = useRef(true)\n // Mirrors `resolvingRef` into render state so the button is ALSO visibly\n // disabled (not just logically blocked) while an async `onAddToCart` is\n // still pending — `resolvingRef` alone is enough to stop a double-submit,\n // but a sighted user should see the button go inert too.\n const [resolving, setResolving] = useState(false)\n\n const clearTimer = () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current)\n timeoutRef.current = null\n }\n }\n\n useEffect(() => {\n mountedRef.current = true\n return () => {\n mountedRef.current = false\n clearTimer()\n }\n }, [])\n\n const triggerSuccess = () => {\n inSuccessWindowRef.current = true\n clearTimer()\n setStatus('success')\n onAdded?.(item)\n timeoutRef.current = setTimeout(() => {\n inSuccessWindowRef.current = false\n setStatus('idle')\n }, successDurationMs)\n }\n\n const triggerError = (errorValue: string | boolean = true) => {\n clearTimer()\n inSuccessWindowRef.current = false\n setStatus('error')\n onError?.(item, errorValue)\n timeoutRef.current = setTimeout(() => setStatus('idle'), successDurationMs)\n }\n\n useEffect(() => {\n if (error && !prevErrorRef.current) {\n triggerError(error)\n }\n prevErrorRef.current = error\n // eslint-disable-next-line react-hooks/exhaustive-deps -- only re-run on `error` transitions\n }, [error])\n\n /**\n * Applies the RESULT of `onAddToCart` (REB-837 review fix): a `'refused'`\n * outcome always triggers the error state, regardless of the `error` prop\n * — the whole point is that the caller may not control that prop at all\n * (e.g. it derives the outcome from `addItem()`'s own return value, never\n * touching `error`). Anything else (`'added'`, `'clamped'`, or no return\n * value at all — the original fire-and-forget contract) is success,\n * UNLESS the externally-controlled `error` prop is ALSO currently truthy.\n */\n const applyOutcome = (result: AddToCartOutcome | void) => {\n if (!mountedRef.current || outOfStock) return\n if (result?.status === 'refused') {\n triggerError(true)\n return\n }\n if (error) return\n triggerSuccess()\n }\n\n const handleClick = () => {\n if (inSuccessWindowRef.current || resolvingRef.current) return\n const result = onAddToCart?.(item)\n\n if (result && typeof (result as Promise<AddToCartOutcome | void>).then === 'function') {\n resolvingRef.current = true\n setResolving(true)\n ;(result as Promise<AddToCartOutcome | void>)\n .then((resolved) => {\n resolvingRef.current = false\n if (mountedRef.current) setResolving(false)\n applyOutcome(resolved)\n })\n .catch(() => {\n resolvingRef.current = false\n if (mountedRef.current) setResolving(false)\n applyOutcome({ status: 'refused' })\n })\n return\n }\n\n applyOutcome(result as AddToCartOutcome | void)\n }\n\n const isDisabled = Boolean(disabled || outOfStock || pending || status === 'success' || resolving)\n const label = outOfStock\n ? t.outOfStock\n : status === 'success'\n ? t.added\n : status === 'error'\n ? typeof error === 'string'\n ? error\n : t.error\n : pending || resolving\n ? t.adding\n : t.add\n\n return (\n <button\n type='button'\n onClick={handleClick}\n disabled={isDisabled}\n aria-disabled={isDisabled}\n aria-live='polite'\n data-slot='add-to-cart-button'\n data-status={status}\n className={cn(\n 'inline-flex items-center justify-center gap-2 rounded-xl px-4 py-2.5 text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n isDisabled\n ? 'cursor-not-allowed bg-muted text-muted-foreground'\n : 'bg-primary text-primary-foreground hover:bg-primary/90',\n status === 'error' && 'bg-destructive text-destructive-foreground',\n className,\n )}\n >\n {label}\n </button>\n )\n}\n","'use client'\n\nimport { useEffect, useRef, useState } from 'react'\n\nimport { cn } from '@reblu/site-ui/utils'\n\n/**\n * `CartIcon` — cart badge/link, with an opt-in count \"bump\" (REB-837). Moved\n * to the `./client` entry (from the RSC-safe root) because the bump\n * animation needs `useState`/`useEffect` to detect an itemCount increase —\n * see `client.ts`'s own leading `'use client'` directive (R4).\n *\n * The HYDRATION GOTCHA this guards against: `CartProvider` rehydrates\n * AFTER mount, so `itemCount` jumps `0 -> N` on every page load with a\n * non-empty persisted cart. Without a guard, that jump reads exactly like \"N\n * items were just added\" and bumps on every single page load. The fix:\n * `hydrated` (pass `useCart().hydrated` straight through) is compared\n * against its OWN previous value, not just itemCount's — a bump only fires\n * when the cart was ALREADY hydrated on the previous comparison AND\n * itemCount increased since. The simultaneous `false -> true` / `0 -> N`\n * rehydration transition fails that first condition; a real add afterwards\n * (hydrated already `true` on both sides) passes it.\n */\nexport interface CartIconLabels {\n cart: string\n itemsSuffix: (count: number) => string\n}\n\nconst DEFAULT_LABELS: CartIconLabels = {\n cart: 'Carrito',\n itemsSuffix: (count) => `${count} artículo${count === 1 ? '' : 's'}`,\n}\n\n/** How long the bump animation plays, ms. */\nconst BUMP_DURATION_MS = 400\n\nexport interface CartIconProps {\n itemCount: number\n href: string\n /**\n * Pass `useCart().hydrated` through when `bumpOnAdd` is enabled — gates\n * the bump against the rehydration jump. Omitting it ENTIRELY (REB-837\n * review fix — not the same as passing `false`) disables the hydration\n * guard altogether, so the bump fires on every itemCount increase; only\n * reach for that when `itemCount` is computed without any client-side\n * rehydration step (e.g. server-rendered, never changes after mount).\n */\n hydrated?: boolean\n /**\n * Enable the count bump when `itemCount` increases after hydration.\n * Respects `prefers-reduced-motion` (the animation classes are\n * `motion-safe:`-gated). @default false\n */\n bumpOnAdd?: boolean\n labels?: Partial<CartIconLabels>\n className?: string\n}\n\nexport function CartIcon({ itemCount, href, hydrated, bumpOnAdd, labels, className }: CartIconProps) {\n const t = { ...DEFAULT_LABELS, ...labels }\n const ariaLabel = itemCount > 0 ? `${t.cart} (${t.itemsSuffix(itemCount)})` : t.cart\n const displayCount = itemCount > 99 ? '99+' : String(itemCount)\n\n const [bumping, setBumping] = useState(false)\n const prevCountRef = useRef(itemCount)\n const prevHydratedRef = useRef(hydrated)\n\n useEffect(() => {\n // REB-837 review fix: `hydrated` OMITTED entirely (never `=== true` nor\n // `=== false`, i.e. `undefined` on the CURRENT render) means the caller\n // isn't using the hydration guard at all — treat it as always-satisfied\n // rather than permanently failing `prevHydratedRef.current === true`\n // (which stayed `undefined` forever and silently disabled every bump).\n const hydrationGuardSatisfied = hydrated === undefined || prevHydratedRef.current === true\n const shouldBump =\n Boolean(bumpOnAdd) && itemCount > prevCountRef.current && hydrationGuardSatisfied\n\n prevCountRef.current = itemCount\n prevHydratedRef.current = hydrated\n\n if (!shouldBump) {\n // REB-837 review fix: a PREVIOUS bump's own setTimeout gets cancelled\n // by this effect's cleanup (dep change) before this body runs — if\n // this run doesn't start a NEW bump, it must still reset the flag\n // itself, or a bump started just before a quick remove/decrease\n // (within `BUMP_DURATION_MS`) gets stuck on `true` forever.\n setBumping(false)\n return\n }\n\n setBumping(true)\n const id = setTimeout(() => setBumping(false), BUMP_DURATION_MS)\n return () => clearTimeout(id)\n // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally only re-runs when itemCount/hydrated/bumpOnAdd change\n }, [itemCount, hydrated, bumpOnAdd])\n\n return (\n <a\n href={href}\n aria-label={ariaLabel}\n data-slot='cart-icon'\n data-bumping={bumping || undefined}\n className={cn(\n 'relative inline-flex items-center justify-center rounded-lg p-2 text-foreground/80 transition-colors hover:bg-muted/60 hover:text-foreground',\n className,\n )}\n >\n <svg\n viewBox='0 0 24 24'\n fill='none'\n stroke='currentColor'\n strokeWidth={2}\n strokeLinecap='round'\n strokeLinejoin='round'\n className={cn('h-5 w-5', bumping && 'motion-safe:scale-125 motion-safe:transition-transform motion-safe:duration-300')}\n aria-hidden='true'\n >\n <circle cx='9' cy='21' r='1' />\n <circle cx='20' cy='21' r='1' />\n <path d='M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6' />\n </svg>\n {itemCount > 0 && (\n <span\n aria-hidden='true'\n className={cn(\n 'absolute -top-0.5 -right-0.5 flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-primary px-1 text-[10px] font-bold leading-none tabular-nums text-primary-foreground',\n bumping && 'motion-safe:scale-125 motion-safe:transition-transform motion-safe:duration-300',\n )}\n >\n {displayCount}\n </span>\n )}\n </a>\n )\n}\n","import { useId, useState, type FormEvent, type ReactNode } from 'react'\nimport { cn } from '@reblu/site-ui/utils'\nimport type {\n DataRequestInput,\n DataRequestSubmission,\n DataRequestType,\n} from '@reblu/site-contracts'\n\n/**\n * `DataRequestForm` — drop-in public GDPR rights form (REB-980) over\n * `POST /api/site/data-requests` (REB-77). The visitor picks the right they\n * exercise, identifies themselves, accepts the processing notice and submits;\n * the request lands in the tenant's nexus queue with source `PUBLIC_FORM` and\n * its 72h clock running, and the form shows the response deadline.\n *\n * Stateful (client-only — ships from `./client`) but transport-agnostic:\n * - `getCaptchaToken` mints the reCAPTCHA v3 token. base-api rejects a\n * request without one, so the form refuses to submit when it resolves\n * `undefined`. Pair it with `useDeferredRecaptchaToken` from\n * `@reblu/site-next/recaptcha-provider`.\n * - `submit` sends the body. By default it POSTs JSON to the SITE's own\n * Route Handler (`endpoint`, default `/api/data-requests`) and unwraps the\n * `{ data }` envelope — the API key never reaches the browser.\n *\n * Errors never show server text: a rejection is mapped by HTTP status to the\n * fixed (overridable) copy in `labels`, so nothing upstream — validation\n * details, provider state — reaches the visitor.\n */\nexport type DataRequestSubmitResult =\n | { ok: true; data: DataRequestSubmission }\n /** `status` is the HTTP status, or `0` when the request never got an answer. */\n | { ok: false; status: number }\n\nexport const DATA_REQUEST_TYPES: readonly DataRequestType[] = [\n 'ACCESS',\n 'RECTIFICATION',\n 'ERASURE',\n 'OBJECTION',\n 'PORTABILITY',\n 'RESTRICTION',\n 'AUTOMATED_DECISION',\n 'INFORMATION',\n]\n\nexport interface DataRequestFormLabels {\n requestTypeLabel: string\n requestTypePlaceholder: string\n requestTypes: Record<DataRequestType, string>\n nameLabel: string\n emailLabel: string\n detailsLabel: string\n detailsPlaceholder: string\n consent: ReactNode\n submit: string\n submitting: string\n requestTypeRequired: string\n nameRequired: string\n emailInvalid: string\n consentRequired: string\n /** Shown on success; receives the formatted 72h deadline. */\n success: (deadline: string) => ReactNode\n captchaError: string\n validationError: string\n rateLimitError: string\n genericError: string\n}\n\nexport const DEFAULT_DATA_REQUEST_FORM_LABELS: DataRequestFormLabels = {\n requestTypeLabel: 'Derecho que quieres ejercer',\n requestTypePlaceholder: 'Selecciona una opción',\n requestTypes: {\n ACCESS: 'Acceso',\n RECTIFICATION: 'Rectificación',\n ERASURE: 'Supresión',\n OBJECTION: 'Oposición',\n PORTABILITY: 'Portabilidad',\n RESTRICTION: 'Limitación del tratamiento',\n AUTOMATED_DECISION: 'No ser objeto de decisiones automatizadas',\n INFORMATION: 'Información',\n },\n nameLabel: 'Nombre y apellidos',\n emailLabel: 'Email',\n detailsLabel: 'Mensaje (opcional)',\n detailsPlaceholder: 'Describe tu solicitud',\n consent: 'Acepto que mis datos se traten para gestionar esta solicitud.',\n submit: 'Enviar solicitud',\n submitting: 'Enviando…',\n requestTypeRequired: 'Elige el derecho que quieres ejercer.',\n nameRequired: 'Indica tu nombre.',\n emailInvalid: 'Introduce un email válido.',\n consentRequired: 'Debes aceptar el tratamiento de tus datos para gestionar la solicitud.',\n success: (deadline) => (\n <>\n Hemos recibido tu solicitud. Te responderemos como máximo el {deadline}.\n </>\n ),\n captchaError:\n 'No hemos podido verificar que no eres un robot. Recarga la página e inténtalo de nuevo.',\n validationError: 'Revisa los datos del formulario e inténtalo de nuevo.',\n rateLimitError: 'Has enviado demasiadas solicitudes. Espera unos minutos e inténtalo de nuevo.',\n genericError: 'No hemos podido enviar tu solicitud. Inténtalo de nuevo más tarde.',\n}\n\nexport interface DataRequestFormProps {\n /** Mints the reCAPTCHA v3 token; `undefined` blocks the submission. */\n getCaptchaToken: () => Promise<string | undefined>\n /** Sends the body. Defaults to a JSON POST to `endpoint`. */\n submit?: (input: DataRequestInput) => Promise<DataRequestSubmitResult>\n /** The SITE's Route Handler used by the default `submit`. @default '/api/data-requests' */\n endpoint?: string\n /** First interaction with the form — wire it to the deferred reCAPTCHA `arm`. */\n onInteract?: () => void\n /** Called with the acknowledgement after a successful submission. */\n onSubmitted?: (ack: DataRequestSubmission) => void\n /** Rights offered, in order. @default DATA_REQUEST_TYPES */\n requestTypes?: readonly DataRequestType[]\n labels?: Partial<DataRequestFormLabels>\n /** Locale for the deadline. @default 'es-ES' */\n locale?: string\n className?: string\n}\n\ntype FieldErrors = Partial<Record<'requestType' | 'name' | 'email' | 'consent', string>>\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nasync function postToRoute(endpoint: string, input: DataRequestInput): Promise<DataRequestSubmitResult> {\n try {\n const res = await fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(input),\n })\n if (!res.ok) return { ok: false, status: res.status }\n const json = (await res.json()) as { data?: DataRequestSubmission }\n return json.data ? { ok: true, data: json.data } : { ok: false, status: res.status }\n } catch {\n return { ok: false, status: 0 }\n }\n}\n\nfunction rejectionMessage(status: number, t: DataRequestFormLabels): string {\n if (status === 400) return t.validationError\n if (status === 422) return t.captchaError\n if (status === 429) return t.rateLimitError\n return t.genericError\n}\n\nconst FIELD_CLASS =\n 'w-full rounded-lg border bg-background px-3 py-2 text-sm transition focus:outline-none focus:ring-2 focus:ring-primary/20'\n\nexport function DataRequestForm({\n getCaptchaToken,\n submit,\n endpoint = '/api/data-requests',\n onInteract,\n onSubmitted,\n requestTypes = DATA_REQUEST_TYPES,\n labels,\n locale = 'es-ES',\n className,\n}: DataRequestFormProps) {\n const t: DataRequestFormLabels = {\n ...DEFAULT_DATA_REQUEST_FORM_LABELS,\n ...labels,\n requestTypes: { ...DEFAULT_DATA_REQUEST_FORM_LABELS.requestTypes, ...labels?.requestTypes },\n }\n const id = useId()\n const [requestType, setRequestType] = useState<DataRequestType | ''>('')\n const [name, setName] = useState('')\n const [email, setEmail] = useState('')\n const [details, setDetails] = useState('')\n const [consent, setConsent] = useState(false)\n const [errors, setErrors] = useState<FieldErrors>({})\n const [formError, setFormError] = useState<string | null>(null)\n const [submitting, setSubmitting] = useState(false)\n const [ack, setAck] = useState<DataRequestSubmission | null>(null)\n\n async function handleSubmit(event: FormEvent<HTMLFormElement>) {\n event.preventDefault()\n if (submitting) return\n setFormError(null)\n\n const next: FieldErrors = {}\n if (!requestType) next.requestType = t.requestTypeRequired\n if (!name.trim()) next.name = t.nameRequired\n if (!EMAIL_PATTERN.test(email.trim())) next.email = t.emailInvalid\n if (!consent) next.consent = t.consentRequired\n setErrors(next)\n if (Object.keys(next).length > 0 || !requestType) return\n\n setSubmitting(true)\n try {\n const captcha = await getCaptchaToken().catch(() => undefined)\n if (!captcha) {\n setFormError(t.captchaError)\n return\n }\n const input: DataRequestInput = {\n requestType,\n name: name.trim(),\n email: email.trim(),\n ...(details.trim() ? { details: details.trim() } : {}),\n captcha,\n }\n const result = await (submit ?? ((body) => postToRoute(endpoint, body)))(input).catch(\n (): DataRequestSubmitResult => ({ ok: false, status: 0 }),\n )\n if (!result.ok) {\n setFormError(rejectionMessage(result.status, t))\n return\n }\n setAck(result.data)\n onSubmitted?.(result.data)\n } finally {\n setSubmitting(false)\n }\n }\n\n if (ack) {\n const deadline = new Intl.DateTimeFormat(locale, {\n dateStyle: 'long',\n timeStyle: 'short',\n }).format(new Date(ack.slaDueAt))\n return (\n <div role='status' data-slot='data-request-form-success' className={cn('rounded-lg border border-border p-6 text-sm', className)}>\n {t.success(deadline)}\n </div>\n )\n }\n\n const errorId = (field: keyof FieldErrors) => (errors[field] ? `${id}-${field}-error` : undefined)\n const fieldError = (field: keyof FieldErrors) =>\n errors[field] ? (\n <p id={`${id}-${field}-error`} className='text-xs text-destructive'>\n {errors[field]}\n </p>\n ) : null\n\n return (\n <form\n noValidate\n onSubmit={handleSubmit}\n onFocusCapture={onInteract}\n onPointerDown={onInteract}\n data-slot='data-request-form'\n className={cn('space-y-4', className)}\n >\n <div className='space-y-1'>\n <label htmlFor={`${id}-type`} className='text-sm font-medium'>\n {t.requestTypeLabel}\n </label>\n <select\n id={`${id}-type`}\n value={requestType}\n onChange={(e) => setRequestType(e.target.value as DataRequestType | '')}\n aria-invalid={errors.requestType ? true : undefined}\n aria-describedby={errorId('requestType')}\n className={cn(FIELD_CLASS, errors.requestType ? 'border-destructive' : 'border-border')}\n >\n <option value=''>{t.requestTypePlaceholder}</option>\n {requestTypes.map((type) => (\n <option key={type} value={type}>\n {t.requestTypes[type]}\n </option>\n ))}\n </select>\n {fieldError('requestType')}\n </div>\n\n <div className='space-y-1'>\n <label htmlFor={`${id}-name`} className='text-sm font-medium'>\n {t.nameLabel}\n </label>\n <input\n id={`${id}-name`}\n type='text'\n autoComplete='name'\n value={name}\n onChange={(e) => setName(e.target.value)}\n aria-invalid={errors.name ? true : undefined}\n aria-describedby={errorId('name')}\n className={cn(FIELD_CLASS, errors.name ? 'border-destructive' : 'border-border')}\n />\n {fieldError('name')}\n </div>\n\n <div className='space-y-1'>\n <label htmlFor={`${id}-email`} className='text-sm font-medium'>\n {t.emailLabel}\n </label>\n <input\n id={`${id}-email`}\n type='email'\n autoComplete='email'\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n aria-invalid={errors.email ? true : undefined}\n aria-describedby={errorId('email')}\n className={cn(FIELD_CLASS, errors.email ? 'border-destructive' : 'border-border')}\n />\n {fieldError('email')}\n </div>\n\n <div className='space-y-1'>\n <label htmlFor={`${id}-details`} className='text-sm font-medium'>\n {t.detailsLabel}\n </label>\n <textarea\n id={`${id}-details`}\n rows={4}\n value={details}\n onChange={(e) => setDetails(e.target.value)}\n placeholder={t.detailsPlaceholder}\n className={cn(FIELD_CLASS, 'border-border')}\n />\n </div>\n\n <div className='space-y-1'>\n <div className='flex items-start gap-2'>\n <input\n id={`${id}-consent`}\n type='checkbox'\n checked={consent}\n onChange={(e) => setConsent(e.target.checked)}\n aria-invalid={errors.consent ? true : undefined}\n aria-describedby={errorId('consent')}\n className='mt-1'\n />\n <label htmlFor={`${id}-consent`} className='text-sm text-foreground/80'>\n {t.consent}\n </label>\n </div>\n {fieldError('consent')}\n </div>\n\n {formError && (\n <p role='alert' className='text-sm text-destructive'>\n {formError}\n </p>\n )}\n\n <button\n type='submit'\n disabled={submitting}\n className='rounded-lg bg-primary px-6 py-2 text-sm font-semibold text-primary-foreground disabled:opacity-60'\n >\n {submitting ? t.submitting : t.submit}\n </button>\n </form>\n )\n}\n"],"mappings":"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","names":["import_utils","import_jsx_runtime","DEFAULT_LABELS","import_react","import_utils","import_jsx_runtime","DEFAULT_LABELS","import_react","import_utils","import_jsx_runtime","DEFAULT_LABELS","import_react","import_utils","import_jsx_runtime"]}
1
+ {"version":3,"sources":["../src/client.ts","../src/hooks/use-collection.ts","../src/components/collection.tsx","../src/components/share-buttons.tsx","../src/lib/share-urls.ts","../src/components/add-to-cart-button.tsx","../src/components/cart-icon.tsx","../src/components/data-request-form.tsx"],"sourcesContent":["'use client'\n\n// `@reblu/site-blocks/client` — the client-only surface. Every export here needs\n// browser state or hooks, so the entry carries the `'use client'` directive and\n// each SITE consumes it from a Client Component boundary.\n\nexport {\n useCollection,\n type CollectionFilter,\n type UseCollectionOptions,\n type UseCollectionResult,\n} from './hooks/use-collection'\n\nexport { Collection } from './components/collection'\nexport type {\n CollectionProps,\n CollectionFacet,\n CollectionFacetOption,\n CollectionLabels,\n} from './components/collection'\n\nexport { ShareButtons } from './components/share-buttons'\nexport type { ShareButtonsProps, ShareButtonsLabels } from './components/share-buttons'\n\n// ── Cart feedback (REB-837) ──────────────────────────────────────────────────\n// `AddToCartButton`/`CartIcon` moved here from the RSC-safe root: both now\n// carry real internal state (confirmed/error timer, bump-on-add guard).\nexport {\n AddToCartButton,\n type AddToCartButtonProps,\n type AddToCartButtonLabels,\n type AddToCartButtonStatus,\n} from './components/add-to-cart-button'\nexport { CartIcon, type CartIconProps, type CartIconLabels } from './components/cart-icon'\n\n// ── GDPR rights form (REB-980) ───────────────────────────────────────────────\n// Drop-in public form over `POST /api/site/data-requests` (REB-77).\nexport {\n DataRequestForm,\n DATA_REQUEST_TYPES,\n DATA_REQUEST_FIELD_LIMITS,\n DEFAULT_DATA_REQUEST_FORM_LABELS,\n type DataRequestFormProps,\n type DataRequestFormLabels,\n type DataRequestSubmitResult,\n} from './components/data-request-form'\n","'use client'\n\nimport { useCallback, useMemo, useState } from 'react'\n\n/**\n * `useCollection` — the headless heart of `@reblu/site-blocks`.\n *\n * A framework-agnostic, presentation-free hook that turns an in-memory array into\n * a filtered / searched / sorted / paginated view. It backs BOTH the page index\n * and the blog index: the facets differ (category, tag, …), the machinery does\n * not. Callers own how items look (`Collection` or their own markup); this hook\n * only owns *which* items and in *what* order.\n *\n * All derivation is memoised and pure — comparators run over `toSorted`, never\n * mutating the caller's array.\n */\n\n/** A named facet: `key` identifies it, `predicate` decides membership for a value. */\nexport interface CollectionFilter<T> {\n key: string\n predicate: (item: T, value: string) => boolean\n}\n\nexport interface UseCollectionOptions<T> {\n items: T[]\n /** Page size. `0`/omitted → no pagination (one page with everything). */\n pageSize?: number\n initialPage?: number\n filters?: CollectionFilter<T>[]\n initialFilters?: Record<string, string | undefined>\n /** Free-text matcher; only applied when a non-empty term is set. */\n search?: (item: T, term: string) => boolean\n initialSearch?: string\n /** Named comparators, selected by `sort`. */\n sorts?: Record<string, (a: T, b: T) => number>\n initialSort?: string\n}\n\nexport interface UseCollectionResult<T> {\n /** Items on the current page (or all items when pagination is off). */\n items: T[]\n /** Full filtered + sorted set, before pagination. */\n filtered: T[]\n /** Size of `filtered`. */\n total: number\n page: number\n pageCount: number\n pageSize: number\n isEmpty: boolean\n activeFilters: Record<string, string | undefined>\n setFilter: (key: string, value: string | undefined) => void\n clearFilters: () => void\n search: string\n setSearch: (term: string) => void\n sort: string | undefined\n setSort: (key: string | undefined) => void\n setPage: (page: number) => void\n nextPage: () => void\n prevPage: () => void\n}\n\nexport function useCollection<T>(options: UseCollectionOptions<T>): UseCollectionResult<T> {\n const { items, pageSize = 0, filters = [], search: searchFn, sorts = {} } = options\n\n const [activeFilters, setActiveFilters] = useState<Record<string, string | undefined>>(\n () => options.initialFilters ?? {},\n )\n const [search, setSearchState] = useState(() => options.initialSearch ?? '')\n const [sort, setSortState] = useState<string | undefined>(() => options.initialSort)\n const [page, setPageState] = useState(() => options.initialPage ?? 1)\n\n const filtered = useMemo(() => {\n let next = items\n\n for (const { key, predicate } of filters) {\n const value = activeFilters[key]\n if (value != null && value !== '') {\n next = next.filter((item) => predicate(item, value))\n }\n }\n\n const term = search.trim()\n if (searchFn && term) {\n next = next.filter((item) => searchFn(item, term))\n }\n\n const comparator = sort ? sorts[sort] : undefined\n if (comparator) {\n next = next.slice().sort(comparator)\n }\n\n return next\n }, [items, filters, activeFilters, search, searchFn, sort, sorts])\n\n const total = filtered.length\n const pageCount = pageSize > 0 ? Math.max(1, Math.ceil(total / pageSize)) : 1\n // Derive the effective page so a shrinking result set never strands us past the\n // last page — state stays as-is, but what we render is always clamped.\n const currentPage = Math.min(Math.max(1, page), pageCount)\n\n const pageItems = useMemo(() => {\n if (pageSize <= 0) return filtered\n const start = (currentPage - 1) * pageSize\n return filtered.slice(start, start + pageSize)\n }, [filtered, pageSize, currentPage])\n\n // Any change to what we're viewing snaps back to the first page.\n const resetToFirstPage = useCallback(() => setPageState(1), [])\n\n const setFilter = useCallback(\n (key: string, value: string | undefined) => {\n setActiveFilters((prev) => ({ ...prev, [key]: value }))\n resetToFirstPage()\n },\n [resetToFirstPage],\n )\n\n const clearFilters = useCallback(() => {\n setActiveFilters({})\n resetToFirstPage()\n }, [resetToFirstPage])\n\n const setSearch = useCallback(\n (term: string) => {\n setSearchState(term)\n resetToFirstPage()\n },\n [resetToFirstPage],\n )\n\n const setSort = useCallback(\n (key: string | undefined) => {\n setSortState(key)\n resetToFirstPage()\n },\n [resetToFirstPage],\n )\n\n // Navigation clamps against the live page bounds so a shrinking result set or\n // an over-shooting jump can never strand the state past the last page.\n const setPage = useCallback(\n (next: number) => setPageState(Math.min(Math.max(1, next), pageCount)),\n [pageCount],\n )\n const nextPage = useCallback(\n () => setPageState(Math.min(currentPage + 1, pageCount)),\n [currentPage, pageCount],\n )\n const prevPage = useCallback(() => setPageState(Math.max(1, currentPage - 1)), [currentPage])\n\n return {\n items: pageItems,\n filtered,\n total,\n page: currentPage,\n pageCount,\n pageSize,\n isEmpty: total === 0,\n activeFilters,\n setFilter,\n clearFilters,\n search,\n setSearch,\n sort,\n setSort,\n setPage,\n nextPage,\n prevPage,\n }\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '@reblu/site-ui/utils'\n\nimport { useCollection, type CollectionFilter } from '../hooks/use-collection'\n\n/**\n * `Collection` — token-driven presentational grid over {@link useCollection}.\n *\n * The headless hook owns filtering/sorting/pagination; this component owns the\n * chrome: a facet chip bar, the item grid (via a `renderItem` prop), and simple\n * pagination. It carries no brand aesthetic — colors come from tokens; a facet\n * option may carry a `color` (tenant hex) which is applied through a CSS custom\n * property, never baked into a class name.\n *\n * Serves both the page index and the blog index; only the facets differ.\n */\nexport interface CollectionFacetOption {\n value: string\n label: string\n /** Optional tenant color (hex). Applied via a CSS var, not a hardcoded class. */\n color?: string\n}\n\nexport interface CollectionFacet {\n key: string\n label: string\n options: CollectionFacetOption[]\n /** Label for the \"clear this facet\" chip. Defaults to \"All\". */\n allLabel?: string\n}\n\nexport interface CollectionLabels {\n previous: string\n next: string\n page: (page: number, pageCount: number) => string\n}\n\nconst DEFAULT_LABELS: CollectionLabels = {\n previous: 'Previous',\n next: 'Next',\n page: (page, pageCount) => `Page ${page} of ${pageCount}`,\n}\n\nexport interface CollectionProps<T> {\n items: T[]\n renderItem: (item: T) => React.ReactNode\n getKey: (item: T) => string\n filters?: CollectionFilter<T>[]\n facets?: CollectionFacet[]\n pageSize?: number\n sorts?: Record<string, (a: T, b: T) => number>\n initialFilters?: Record<string, string | undefined>\n initialSort?: string\n emptyLabel?: string\n className?: string\n /** Tailwind classes for the grid container. */\n gridClassName?: string\n labels?: Partial<CollectionLabels>\n}\n\nexport function Collection<T>({\n items,\n renderItem,\n getKey,\n filters,\n facets = [],\n pageSize = 0,\n sorts,\n initialFilters,\n initialSort,\n emptyLabel = 'No results',\n className,\n gridClassName = 'grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3',\n labels,\n}: CollectionProps<T>) {\n const t = { ...DEFAULT_LABELS, ...labels }\n const collection = useCollection<T>({\n items,\n filters,\n pageSize,\n sorts,\n initialFilters,\n initialSort,\n })\n\n return (\n <div className={cn('space-y-8', className)}>\n {facets.length > 0 && (\n <div className='space-y-4'>\n {facets.map((facet) => {\n const active = collection.activeFilters[facet.key]\n return (\n <div\n key={facet.key}\n role='group'\n aria-label={facet.label}\n className='flex flex-wrap gap-2'\n >\n <FacetChip\n label={facet.allLabel ?? 'All'}\n pressed={active == null || active === ''}\n onClick={() => collection.setFilter(facet.key, undefined)}\n />\n {facet.options.map((opt) => (\n <FacetChip\n key={opt.value}\n label={opt.label}\n color={opt.color}\n pressed={active === opt.value}\n onClick={() => collection.setFilter(facet.key, opt.value)}\n />\n ))}\n </div>\n )\n })}\n </div>\n )}\n\n {collection.isEmpty ? (\n <p className='py-16 text-center text-muted-foreground'>{emptyLabel}</p>\n ) : (\n <ul className={cn('list-none p-0', gridClassName)}>\n {collection.items.map((item) => (\n <li key={getKey(item)}>{renderItem(item)}</li>\n ))}\n </ul>\n )}\n\n {collection.pageCount > 1 && (\n <nav aria-label='Pagination' className='flex items-center justify-center gap-4'>\n <button\n type='button'\n onClick={collection.prevPage}\n disabled={collection.page <= 1}\n className={PAGE_BTN_CLASS}\n >\n {t.previous}\n </button>\n <span aria-current='page' className='text-sm text-muted-foreground'>\n {t.page(collection.page, collection.pageCount)}\n </span>\n <button\n type='button'\n onClick={collection.nextPage}\n disabled={collection.page >= collection.pageCount}\n className={PAGE_BTN_CLASS}\n >\n {t.next}\n </button>\n </nav>\n )}\n </div>\n )\n}\n\nconst PAGE_BTN_CLASS =\n 'inline-flex h-9 items-center rounded-md border border-border px-4 text-sm font-medium transition-colors hover:bg-muted disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'\n\ninterface FacetChipProps {\n label: string\n pressed: boolean\n color?: string\n onClick: () => void\n}\n\nfunction FacetChip({ label, pressed, color, onClick }: FacetChipProps) {\n return (\n <button\n type='button'\n aria-pressed={pressed}\n onClick={onClick}\n // The tenant color rides in a CSS var; the class picks it up when pressed,\n // so no hex string is ever baked into a Tailwind class.\n style={color ? ({ '--facet-color': color } as React.CSSProperties) : undefined}\n className={cn(\n 'inline-flex items-center rounded-full border px-3 py-1 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n pressed\n ? color\n ? 'border-transparent bg-[var(--facet-color)] text-white'\n : 'border-transparent bg-primary text-primary-foreground'\n : 'border-border text-muted-foreground hover:bg-muted hover:text-foreground',\n )}\n >\n {label}\n </button>\n )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '@reblu/site-ui/utils'\n\nimport { buildShareUrls } from '../lib/share-urls'\n\n/**\n * `ShareButtons` — social share row for a blog post (LinkedIn / X / Facebook /\n * WhatsApp) plus a copy-to-clipboard action. Token-driven and icon-only for the\n * providers; the copy action shows a text confirmation.\n *\n * Copy defaults to English (distributable default). A Spanish-or-other SITE can\n * override every visible string via `labels` without forking the component.\n */\nexport interface ShareButtonsLabels {\n share: string\n copy: string\n copied: string\n linkedin: string\n x: string\n facebook: string\n whatsapp: string\n copyStatus: string\n}\n\nconst DEFAULT_LABELS: ShareButtonsLabels = {\n share: 'Share',\n copy: 'Copy link',\n copied: 'Link copied',\n linkedin: 'Share on LinkedIn',\n x: 'Share on X',\n facebook: 'Share on Facebook',\n whatsapp: 'Share on WhatsApp',\n copyStatus: 'Link copied to clipboard',\n}\n\nexport interface ShareButtonsProps {\n /** Canonical, absolute URL of the post being shared. */\n url: string\n /** Post title, used where the provider supports pre-filled copy. */\n title: string\n className?: string\n labels?: Partial<ShareButtonsLabels>\n}\n\nconst LINK_CLASS =\n 'inline-flex h-9 w-9 items-center justify-center rounded-full border border-border text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'\n\nconst ICON = 'h-4 w-4'\n\nfunction LinkedInIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='currentColor' aria-hidden='true' focusable='false'>\n <path d='M19 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2zM8.34 18.34V10.9H5.9v7.44zM7.12 9.82a1.42 1.42 0 1 0 0-2.84 1.42 1.42 0 0 0 0 2.84m11.22 8.52v-4.08c0-2.18-1.16-3.19-2.72-3.19a2.35 2.35 0 0 0-2.13 1.17v-1h-2.43v7.1h2.43v-3.93c0-1 .2-2 1.46-2s1.28 1.16 1.28 2.06v3.87z' />\n </svg>\n )\n}\n\nfunction XIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='currentColor' aria-hidden='true' focusable='false'>\n <path d='M18.244 2.25h3.308l-7.227 8.26 8.502 11.24h-6.66l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z' />\n </svg>\n )\n}\n\nfunction FacebookIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='currentColor' aria-hidden='true' focusable='false'>\n <path d='M22 12a10 10 0 1 0-11.56 9.88v-6.99H7.9V12h2.54V9.8c0-2.5 1.49-3.89 3.78-3.89 1.09 0 2.24.2 2.24.2v2.46h-1.26c-1.24 0-1.63.77-1.63 1.56V12h2.78l-.44 2.89h-2.34v6.99A10 10 0 0 0 22 12' />\n </svg>\n )\n}\n\nfunction WhatsAppIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='currentColor' aria-hidden='true' focusable='false'>\n <path d='M12.04 2C6.58 2 2.13 6.45 2.13 11.91c0 1.75.46 3.45 1.32 4.95L2 22l5.25-1.38a9.9 9.9 0 0 0 4.79 1.22h.01c5.46 0 9.91-4.45 9.91-9.91 0-2.65-1.03-5.14-2.9-7.01A9.82 9.82 0 0 0 12.04 2m0 1.67c2.2 0 4.27.86 5.82 2.42a8.19 8.19 0 0 1 2.42 5.82c0 4.54-3.7 8.24-8.25 8.24a8.2 8.2 0 0 1-4.2-1.15l-.3-.18-3.12.82.83-3.04-.2-.31a8.2 8.2 0 0 1-1.26-4.38c0-4.54 3.7-8.24 8.25-8.24m4.52 10.36c-.06-.1-.22-.16-.46-.28s-1.46-.72-1.68-.8-.39-.12-.55.12-.63.8-.77.96-.28.18-.52.06a6.7 6.7 0 0 1-1.98-1.22 7.4 7.4 0 0 1-1.37-1.7c-.14-.24-.01-.37.11-.49.11-.11.24-.28.37-.42.12-.14.16-.24.24-.4s.04-.3-.02-.42-.55-1.32-.75-1.81c-.2-.48-.4-.41-.55-.42h-.47a.9.9 0 0 0-.65.3c-.22.24-.85.83-.85 2.02s.87 2.35 1 2.51c.12.16 1.71 2.61 4.14 3.66.58.25 1.03.4 1.38.51.58.19 1.11.16 1.53.1.47-.07 1.46-.6 1.66-1.17.21-.58.21-1.07.15-1.17' />\n </svg>\n )\n}\n\nfunction CopyIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='none' stroke='currentColor' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round' aria-hidden='true' focusable='false'>\n <rect x='9' y='9' width='13' height='13' rx='2' ry='2' />\n <path d='M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1' />\n </svg>\n )\n}\n\nfunction CheckIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='none' stroke='currentColor' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round' aria-hidden='true' focusable='false'>\n <polyline points='20 6 9 17 4 12' />\n </svg>\n )\n}\n\nexport function ShareButtons({ url, title, className, labels }: ShareButtonsProps) {\n const t = { ...DEFAULT_LABELS, ...labels }\n const [copied, setCopied] = React.useState(false)\n const share = buildShareUrls(url, title)\n\n async function handleCopy() {\n try {\n await navigator.clipboard.writeText(url)\n setCopied(true)\n window.setTimeout(() => setCopied(false), 2000)\n } catch {\n // Clipboard can be unavailable (permissions, insecure context); fail quietly.\n }\n }\n\n return (\n <div className={cn('flex items-center gap-2', className)} role='group' aria-label={t.share}>\n <span className='mr-1 text-sm font-medium text-muted-foreground'>{t.share}</span>\n\n <a href={share.linkedin} target='_blank' rel='noopener noreferrer' className={LINK_CLASS} aria-label={t.linkedin}>\n <LinkedInIcon />\n </a>\n <a href={share.x} target='_blank' rel='noopener noreferrer' className={LINK_CLASS} aria-label={t.x}>\n <XIcon />\n </a>\n <a href={share.facebook} target='_blank' rel='noopener noreferrer' className={LINK_CLASS} aria-label={t.facebook}>\n <FacebookIcon />\n </a>\n <a href={share.whatsapp} target='_blank' rel='noopener noreferrer' className={LINK_CLASS} aria-label={t.whatsapp}>\n <WhatsAppIcon />\n </a>\n\n <button\n type='button'\n onClick={handleCopy}\n className={cn(LINK_CLASS, 'w-auto gap-2 px-3', copied && 'border-primary text-primary')}\n aria-label={t.copy}\n >\n {copied ? <CheckIcon /> : <CopyIcon />}\n <span className='text-sm'>{copied ? t.copied : t.copy}</span>\n </button>\n\n {/* Polite live region so screen readers announce the copy confirmation. */}\n <span role='status' aria-live='polite' className='sr-only'>\n {copied ? t.copyStatus : ''}\n </span>\n </div>\n )\n}\n","/**\n * Pure builders for the social-share targets used by `ShareButtons`.\n *\n * Framework-free so the URL construction is unit-testable without rendering.\n * Every provider gets the canonical post URL; providers that support custom copy\n * (X, WhatsApp) also receive the post title.\n */\nexport interface ShareUrls {\n linkedin: string\n x: string\n facebook: string\n whatsapp: string\n}\n\nexport function buildShareUrls(url: string, title: string): ShareUrls {\n const encodedUrl = encodeURIComponent(url)\n const encodedTitle = encodeURIComponent(title)\n\n return {\n // LinkedIn derives title/description from the target page's Open Graph tags,\n // so it only takes the URL.\n linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`,\n // The intent endpoint keeps the /intent/tweet path under the x.com domain.\n x: `https://x.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}`,\n facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`,\n whatsapp: `https://wa.me/?text=${encodeURIComponent(`${title} ${url}`)}`,\n }\n}\n","'use client'\n\nimport { useEffect, useRef, useState } from 'react'\n\nimport { cn } from '@reblu/site-ui/utils'\n\n/**\n * `AddToCartButton` — add-to-cart CTA with a built-in confirmed/error state\n * (REB-837). Moved to the `./client` entry (from the RSC-safe root) because\n * the confirmed-state timer and the `error` prop watcher genuinely need\n * `useState`/`useEffect` — see `client.ts`'s own leading `'use client'`\n * directive (R4: tsup only preserves a directive declared by the entry file\n * itself, not one merely re-exported from a child module).\n *\n * Previously (REB-829) this component had no internal state at all — it was\n * a pure `onAddToCart?: () => void` button exported from the RSC-safe root.\n * A SITE importing `AddToCartButton` from `@reblu/site-blocks` (the root\n * entry) must now import it from `@reblu/site-blocks/client` instead.\n */\nexport interface AddToCartButtonLabels {\n add: string\n adding: string\n outOfStock: string\n /** Shown for ~`successDurationMs` after a successful add. */\n added: string\n /** Fallback shown when `error` is `true` (not a string). */\n error: string\n}\n\nconst DEFAULT_LABELS: AddToCartButtonLabels = {\n add: 'Añadir al carrito',\n adding: 'Añadiendo…',\n outOfStock: 'Agotado',\n added: 'Añadido ✓',\n error: 'No se pudo añadir',\n}\n\nexport type AddToCartButtonStatus = 'idle' | 'success' | 'error'\n\n/**\n * Structural subset of `@reblu/site-next/cart`'s `AddItemOutcome` (REB-837\n * review fix) — `site-blocks` never depends on `site-next`, so this is\n * duplicated shape, not an import; a real `AddItemOutcome` (which also\n * carries `quantity`) satisfies this type as-is.\n */\nexport interface AddToCartOutcome {\n status: 'added' | 'clamped' | 'refused'\n}\n\nexport interface AddToCartButtonProps<TItem = unknown> {\n /**\n * Called on click. MAY return the outcome of the add (synchronously or\n * via a `Promise`) so the button can decide success vs error from the\n * RESULT instead of only from the externally-controlled `error` prop\n * (REB-837 review fix: deciding purely from `error` captured at render\n * time showed \"Añadido ✓\" for a REFUSED add whenever the caller never\n * flips `error` back on). `'refused'` → error state; `'added'`/`'clamped'`\n * → confirmed state; returning nothing (`void`) stays backward compatible\n * with the original fire-and-forget contract — treated as success.\n */\n onAddToCart?: (item?: TItem) => AddToCartOutcome | void | Promise<AddToCartOutcome | void>\n disabled?: boolean\n outOfStock?: boolean\n /** True while the add-to-cart call (or a navigation it triggers) is in flight. */\n pending?: boolean\n /**\n * Controlled error (out-of-stock race, max-quantity reached, a failed\n * mutation…). Set it truthy right after `onAddToCart` fails to trigger the\n * error state; clear it (`undefined`/`false`) to let the button reset. A\n * `string` is shown as the button's label; `true` falls back to\n * `labels.error`.\n */\n error?: string | boolean\n /** Opaque item forwarded verbatim to `onAddToCart`/`onAdded`/`onError` — never read internally. */\n item?: TItem\n /** How long the success/error state shows before reverting to idle, ms. @default 1500 */\n successDurationMs?: number\n /** Fires once per successful add, right after `onAddToCart`. */\n onAdded?: (item?: TItem) => void\n /** Fires once when `error` transitions from falsy to truthy. */\n onError?: (item?: TItem, error?: string | boolean) => void\n labels?: Partial<AddToCartButtonLabels>\n className?: string\n}\n\nexport function AddToCartButton<TItem = unknown>({\n onAddToCart,\n disabled,\n outOfStock,\n pending,\n error,\n item,\n successDurationMs = 1500,\n onAdded,\n onError,\n labels,\n className,\n}: AddToCartButtonProps<TItem>) {\n const t = { ...DEFAULT_LABELS, ...labels }\n const [status, setStatus] = useState<AddToCartButtonStatus>('idle')\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n const prevErrorRef = useRef<string | boolean | undefined>(error)\n // Non-reentrancy guard (REB-837): `disabled`/`aria-disabled` already block\n // both pointer and keyboard activation of a native <button>, but a ref\n // guard is the actual source of truth for \"is a success window open OR an\n // async onAddToCart is still resolving\" — defense in depth against\n // anything that might still dispatch a click, and the only thing that\n // actually blocks a double-submit while a Promise is pending.\n const inSuccessWindowRef = useRef(false)\n const resolvingRef = useRef(false)\n const mountedRef = useRef(true)\n // Mirrors `resolvingRef` into render state so the button is ALSO visibly\n // disabled (not just logically blocked) while an async `onAddToCart` is\n // still pending — `resolvingRef` alone is enough to stop a double-submit,\n // but a sighted user should see the button go inert too.\n const [resolving, setResolving] = useState(false)\n\n const clearTimer = () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current)\n timeoutRef.current = null\n }\n }\n\n useEffect(() => {\n mountedRef.current = true\n return () => {\n mountedRef.current = false\n clearTimer()\n }\n }, [])\n\n const triggerSuccess = () => {\n inSuccessWindowRef.current = true\n clearTimer()\n setStatus('success')\n onAdded?.(item)\n timeoutRef.current = setTimeout(() => {\n inSuccessWindowRef.current = false\n setStatus('idle')\n }, successDurationMs)\n }\n\n const triggerError = (errorValue: string | boolean = true) => {\n clearTimer()\n inSuccessWindowRef.current = false\n setStatus('error')\n onError?.(item, errorValue)\n timeoutRef.current = setTimeout(() => setStatus('idle'), successDurationMs)\n }\n\n useEffect(() => {\n if (error && !prevErrorRef.current) {\n triggerError(error)\n }\n prevErrorRef.current = error\n // eslint-disable-next-line react-hooks/exhaustive-deps -- only re-run on `error` transitions\n }, [error])\n\n /**\n * Applies the RESULT of `onAddToCart` (REB-837 review fix): a `'refused'`\n * outcome always triggers the error state, regardless of the `error` prop\n * — the whole point is that the caller may not control that prop at all\n * (e.g. it derives the outcome from `addItem()`'s own return value, never\n * touching `error`). Anything else (`'added'`, `'clamped'`, or no return\n * value at all — the original fire-and-forget contract) is success,\n * UNLESS the externally-controlled `error` prop is ALSO currently truthy.\n */\n const applyOutcome = (result: AddToCartOutcome | void) => {\n if (!mountedRef.current || outOfStock) return\n if (result?.status === 'refused') {\n triggerError(true)\n return\n }\n if (error) return\n triggerSuccess()\n }\n\n const handleClick = () => {\n if (inSuccessWindowRef.current || resolvingRef.current) return\n const result = onAddToCart?.(item)\n\n if (result && typeof (result as Promise<AddToCartOutcome | void>).then === 'function') {\n resolvingRef.current = true\n setResolving(true)\n ;(result as Promise<AddToCartOutcome | void>)\n .then((resolved) => {\n resolvingRef.current = false\n if (mountedRef.current) setResolving(false)\n applyOutcome(resolved)\n })\n .catch(() => {\n resolvingRef.current = false\n if (mountedRef.current) setResolving(false)\n applyOutcome({ status: 'refused' })\n })\n return\n }\n\n applyOutcome(result as AddToCartOutcome | void)\n }\n\n const isDisabled = Boolean(disabled || outOfStock || pending || status === 'success' || resolving)\n const label = outOfStock\n ? t.outOfStock\n : status === 'success'\n ? t.added\n : status === 'error'\n ? typeof error === 'string'\n ? error\n : t.error\n : pending || resolving\n ? t.adding\n : t.add\n\n return (\n <button\n type='button'\n onClick={handleClick}\n disabled={isDisabled}\n aria-disabled={isDisabled}\n aria-live='polite'\n data-slot='add-to-cart-button'\n data-status={status}\n className={cn(\n 'inline-flex items-center justify-center gap-2 rounded-xl px-4 py-2.5 text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n isDisabled\n ? 'cursor-not-allowed bg-muted text-muted-foreground'\n : 'bg-primary text-primary-foreground hover:bg-primary/90',\n status === 'error' && 'bg-destructive text-destructive-foreground',\n className,\n )}\n >\n {label}\n </button>\n )\n}\n","'use client'\n\nimport { useEffect, useRef, useState } from 'react'\n\nimport { cn } from '@reblu/site-ui/utils'\n\n/**\n * `CartIcon` — cart badge/link, with an opt-in count \"bump\" (REB-837). Moved\n * to the `./client` entry (from the RSC-safe root) because the bump\n * animation needs `useState`/`useEffect` to detect an itemCount increase —\n * see `client.ts`'s own leading `'use client'` directive (R4).\n *\n * The HYDRATION GOTCHA this guards against: `CartProvider` rehydrates\n * AFTER mount, so `itemCount` jumps `0 -> N` on every page load with a\n * non-empty persisted cart. Without a guard, that jump reads exactly like \"N\n * items were just added\" and bumps on every single page load. The fix:\n * `hydrated` (pass `useCart().hydrated` straight through) is compared\n * against its OWN previous value, not just itemCount's — a bump only fires\n * when the cart was ALREADY hydrated on the previous comparison AND\n * itemCount increased since. The simultaneous `false -> true` / `0 -> N`\n * rehydration transition fails that first condition; a real add afterwards\n * (hydrated already `true` on both sides) passes it.\n */\nexport interface CartIconLabels {\n cart: string\n itemsSuffix: (count: number) => string\n}\n\nconst DEFAULT_LABELS: CartIconLabels = {\n cart: 'Carrito',\n itemsSuffix: (count) => `${count} artículo${count === 1 ? '' : 's'}`,\n}\n\n/** How long the bump animation plays, ms. */\nconst BUMP_DURATION_MS = 400\n\nexport interface CartIconProps {\n itemCount: number\n href: string\n /**\n * Pass `useCart().hydrated` through when `bumpOnAdd` is enabled — gates\n * the bump against the rehydration jump. Omitting it ENTIRELY (REB-837\n * review fix — not the same as passing `false`) disables the hydration\n * guard altogether, so the bump fires on every itemCount increase; only\n * reach for that when `itemCount` is computed without any client-side\n * rehydration step (e.g. server-rendered, never changes after mount).\n */\n hydrated?: boolean\n /**\n * Enable the count bump when `itemCount` increases after hydration.\n * Respects `prefers-reduced-motion` (the animation classes are\n * `motion-safe:`-gated). @default false\n */\n bumpOnAdd?: boolean\n labels?: Partial<CartIconLabels>\n className?: string\n}\n\nexport function CartIcon({ itemCount, href, hydrated, bumpOnAdd, labels, className }: CartIconProps) {\n const t = { ...DEFAULT_LABELS, ...labels }\n const ariaLabel = itemCount > 0 ? `${t.cart} (${t.itemsSuffix(itemCount)})` : t.cart\n const displayCount = itemCount > 99 ? '99+' : String(itemCount)\n\n const [bumping, setBumping] = useState(false)\n const prevCountRef = useRef(itemCount)\n const prevHydratedRef = useRef(hydrated)\n\n useEffect(() => {\n // REB-837 review fix: `hydrated` OMITTED entirely (never `=== true` nor\n // `=== false`, i.e. `undefined` on the CURRENT render) means the caller\n // isn't using the hydration guard at all — treat it as always-satisfied\n // rather than permanently failing `prevHydratedRef.current === true`\n // (which stayed `undefined` forever and silently disabled every bump).\n const hydrationGuardSatisfied = hydrated === undefined || prevHydratedRef.current === true\n const shouldBump =\n Boolean(bumpOnAdd) && itemCount > prevCountRef.current && hydrationGuardSatisfied\n\n prevCountRef.current = itemCount\n prevHydratedRef.current = hydrated\n\n if (!shouldBump) {\n // REB-837 review fix: a PREVIOUS bump's own setTimeout gets cancelled\n // by this effect's cleanup (dep change) before this body runs — if\n // this run doesn't start a NEW bump, it must still reset the flag\n // itself, or a bump started just before a quick remove/decrease\n // (within `BUMP_DURATION_MS`) gets stuck on `true` forever.\n setBumping(false)\n return\n }\n\n setBumping(true)\n const id = setTimeout(() => setBumping(false), BUMP_DURATION_MS)\n return () => clearTimeout(id)\n // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally only re-runs when itemCount/hydrated/bumpOnAdd change\n }, [itemCount, hydrated, bumpOnAdd])\n\n return (\n <a\n href={href}\n aria-label={ariaLabel}\n data-slot='cart-icon'\n data-bumping={bumping || undefined}\n className={cn(\n 'relative inline-flex items-center justify-center rounded-lg p-2 text-foreground/80 transition-colors hover:bg-muted/60 hover:text-foreground',\n className,\n )}\n >\n <svg\n viewBox='0 0 24 24'\n fill='none'\n stroke='currentColor'\n strokeWidth={2}\n strokeLinecap='round'\n strokeLinejoin='round'\n className={cn('h-5 w-5', bumping && 'motion-safe:scale-125 motion-safe:transition-transform motion-safe:duration-300')}\n aria-hidden='true'\n >\n <circle cx='9' cy='21' r='1' />\n <circle cx='20' cy='21' r='1' />\n <path d='M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6' />\n </svg>\n {itemCount > 0 && (\n <span\n aria-hidden='true'\n className={cn(\n 'absolute -top-0.5 -right-0.5 flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-primary px-1 text-[10px] font-bold leading-none tabular-nums text-primary-foreground',\n bumping && 'motion-safe:scale-125 motion-safe:transition-transform motion-safe:duration-300',\n )}\n >\n {displayCount}\n </span>\n )}\n </a>\n )\n}\n","import { useEffect, useId, useRef, useState, type FormEvent, type ReactNode } from 'react'\nimport { cn } from '@reblu/site-ui/utils'\nimport {\n DATA_REQUEST_TYPES,\n type DataRequestInput,\n type DataRequestSubmission,\n type DataRequestType,\n} from '@reblu/site-contracts'\n\n/**\n * `DataRequestForm` — drop-in public GDPR rights form (REB-980) over\n * `POST /api/site/data-requests` (REB-77). The visitor picks the right they\n * exercise, identifies themselves, accepts the processing notice and submits;\n * the request lands in the tenant's nexus queue with source `PUBLIC_FORM` and\n * its 72h clock running, and the form shows the response deadline.\n *\n * Stateful (client-only — ships from `./client`) but transport-agnostic:\n * - `getCaptchaToken` mints the reCAPTCHA v3 token. base-api rejects a\n * request without one, so the form refuses to submit when it resolves\n * `undefined`. Pair it with `useDeferredRecaptchaToken` from\n * `@reblu/site-next/recaptcha-provider`.\n * - `submit` sends the body. By default it POSTs JSON to the SITE's own\n * Route Handler (`endpoint`, default `/api/data-requests`) and unwraps the\n * `{ data }` envelope — the API key never reaches the browser.\n *\n * Errors never show server text: a rejection is mapped by HTTP status to the\n * fixed (overridable) copy in `labels`, so nothing upstream — validation\n * details, provider state — reaches the visitor.\n */\nexport type DataRequestSubmitResult =\n | { ok: true; data: DataRequestSubmission }\n /** `status` is the HTTP status, or `0` when the request never got an answer. */\n | { ok: false; status: number }\n\n/**\n * Every GDPR right, in base-api's order. Lives in `@reblu/site-contracts`\n * since REB-1005 (server code needs it too); re-exported here so existing\n * `@reblu/site-blocks/client` imports keep working.\n */\nexport { DATA_REQUEST_TYPES }\n\n/** The server's field limits (base-api `POST /api/site/data-requests`). */\nexport const DATA_REQUEST_FIELD_LIMITS = Object.freeze({ name: 200, email: 320, details: 5000 })\n\nexport interface DataRequestFormLabels {\n requestTypeLabel: string\n requestTypePlaceholder: string\n requestTypes: Record<DataRequestType, string>\n nameLabel: string\n emailLabel: string\n detailsLabel: string\n detailsPlaceholder: string\n consent: ReactNode\n submit: string\n submitting: string\n requestTypeRequired: string\n nameRequired: string\n emailInvalid: string\n consentRequired: string\n /** Shown on success; receives the formatted 72h deadline. */\n success: (deadline: string) => ReactNode\n captchaError: string\n validationError: string\n rateLimitError: string\n genericError: string\n}\n\nexport const DEFAULT_DATA_REQUEST_FORM_LABELS: DataRequestFormLabels = {\n requestTypeLabel: 'Derecho que quieres ejercer',\n requestTypePlaceholder: 'Selecciona una opción',\n requestTypes: {\n ACCESS: 'Acceso',\n RECTIFICATION: 'Rectificación',\n ERASURE: 'Supresión',\n OBJECTION: 'Oposición',\n PORTABILITY: 'Portabilidad',\n RESTRICTION: 'Limitación del tratamiento',\n AUTOMATED_DECISION: 'No ser objeto de decisiones automatizadas',\n INFORMATION: 'Información',\n },\n nameLabel: 'Nombre y apellidos',\n emailLabel: 'Email',\n detailsLabel: 'Mensaje (opcional)',\n detailsPlaceholder: 'Describe tu solicitud',\n consent: 'Acepto que mis datos se traten para gestionar esta solicitud.',\n submit: 'Enviar solicitud',\n submitting: 'Enviando…',\n requestTypeRequired: 'Elige el derecho que quieres ejercer.',\n nameRequired: 'Indica tu nombre.',\n emailInvalid: 'Introduce un email válido.',\n consentRequired: 'Debes aceptar el tratamiento de tus datos para gestionar la solicitud.',\n success: (deadline) => (\n <>\n Hemos recibido tu solicitud. Te responderemos como máximo el {deadline}.\n </>\n ),\n captchaError:\n 'No hemos podido verificar que no eres un robot. Recarga la página e inténtalo de nuevo.',\n validationError: 'Revisa los datos del formulario e inténtalo de nuevo.',\n rateLimitError: 'Has enviado demasiadas solicitudes. Espera unos minutos e inténtalo de nuevo.',\n genericError: 'No hemos podido enviar tu solicitud. Inténtalo de nuevo más tarde.',\n}\n\nexport interface DataRequestFormProps {\n /** Mints the reCAPTCHA v3 token; `undefined` blocks the submission. */\n getCaptchaToken: () => Promise<string | undefined>\n /** Sends the body. Defaults to a JSON POST to `endpoint`. */\n submit?: (input: DataRequestInput) => Promise<DataRequestSubmitResult>\n /** The SITE's Route Handler used by the default `submit`. @default '/api/data-requests' */\n endpoint?: string\n /** First interaction with the form — wire it to the deferred reCAPTCHA `arm`. */\n onInteract?: () => void\n /** Called with the acknowledgement after a successful submission. */\n onSubmitted?: (ack: DataRequestSubmission) => void\n /** Rights offered, in order. @default DATA_REQUEST_TYPES */\n requestTypes?: readonly DataRequestType[]\n labels?: Partial<DataRequestFormLabels>\n /** Locale for the deadline. @default 'es-ES' */\n locale?: string\n className?: string\n}\n\ntype FieldName = 'requestType' | 'name' | 'email' | 'consent'\ntype FieldErrors = Partial<Record<FieldName, string>>\n\n/** Form order — focus goes to the first invalid field in this order. */\nconst FIELD_ORDER: readonly FieldName[] = ['requestType', 'name', 'email', 'consent']\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nasync function postToRoute(endpoint: string, input: DataRequestInput): Promise<DataRequestSubmitResult> {\n try {\n const res = await fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(input),\n })\n if (!res.ok) return { ok: false, status: res.status }\n const json = (await res.json()) as { data?: DataRequestSubmission }\n return json.data ? { ok: true, data: json.data } : { ok: false, status: res.status }\n } catch {\n return { ok: false, status: 0 }\n }\n}\n\nfunction rejectionMessage(status: number, t: DataRequestFormLabels): string {\n if (status === 400) return t.validationError\n if (status === 422) return t.captchaError\n if (status === 429) return t.rateLimitError\n return t.genericError\n}\n\nconst FIELD_CLASS =\n 'w-full rounded-lg border bg-background px-3 py-2 text-sm transition focus:outline-none focus:ring-2 focus:ring-primary/20'\n\nexport function DataRequestForm({\n getCaptchaToken,\n submit,\n endpoint = '/api/data-requests',\n onInteract,\n onSubmitted,\n requestTypes = DATA_REQUEST_TYPES,\n labels,\n locale = 'es-ES',\n className,\n}: DataRequestFormProps) {\n const t: DataRequestFormLabels = {\n ...DEFAULT_DATA_REQUEST_FORM_LABELS,\n ...labels,\n requestTypes: { ...DEFAULT_DATA_REQUEST_FORM_LABELS.requestTypes, ...labels?.requestTypes },\n }\n const id = useId()\n const [requestType, setRequestType] = useState<DataRequestType | ''>('')\n const [name, setName] = useState('')\n const [email, setEmail] = useState('')\n const [details, setDetails] = useState('')\n const [consent, setConsent] = useState(false)\n const [errors, setErrors] = useState<FieldErrors>({})\n const [formError, setFormError] = useState<string | null>(null)\n const [submitting, setSubmitting] = useState(false)\n const [ack, setAck] = useState<DataRequestSubmission | null>(null)\n const statusRef = useRef<HTMLDivElement>(null)\n\n // REB-1003: the success message replaces the form, so focus would otherwise\n // fall back to <body> — move it to the status the screen reader announces.\n useEffect(() => {\n if (ack) statusRef.current?.focus()\n }, [ack])\n\n async function handleSubmit(event: FormEvent<HTMLFormElement>) {\n event.preventDefault()\n if (submitting) return\n setFormError(null)\n\n const next: FieldErrors = {}\n if (!requestType) next.requestType = t.requestTypeRequired\n if (!name.trim()) next.name = t.nameRequired\n if (!EMAIL_PATTERN.test(email.trim())) next.email = t.emailInvalid\n if (!consent) next.consent = t.consentRequired\n setErrors(next)\n const firstInvalid = FIELD_ORDER.find((field) => next[field])\n if (firstInvalid) {\n // REB-1003: take the visitor to the first field to fix.\n document.getElementById(`${id}-${firstInvalid === 'requestType' ? 'type' : firstInvalid}`)?.focus()\n return\n }\n if (!requestType) return\n\n setSubmitting(true)\n try {\n const captcha = await getCaptchaToken().catch(() => undefined)\n if (!captcha) {\n setFormError(t.captchaError)\n return\n }\n const input: DataRequestInput = {\n requestType,\n name: name.trim(),\n email: email.trim(),\n ...(details.trim() ? { details: details.trim() } : {}),\n captcha,\n }\n const result = await (submit ?? ((body) => postToRoute(endpoint, body)))(input).catch(\n (): DataRequestSubmitResult => ({ ok: false, status: 0 }),\n )\n if (!result.ok) {\n setFormError(rejectionMessage(result.status, t))\n return\n }\n setAck(result.data)\n onSubmitted?.(result.data)\n } finally {\n setSubmitting(false)\n }\n }\n\n if (ack) {\n const deadline = new Intl.DateTimeFormat(locale, {\n dateStyle: 'long',\n timeStyle: 'short',\n }).format(new Date(ack.slaDueAt))\n return (\n <div\n ref={statusRef}\n tabIndex={-1}\n role='status'\n data-slot='data-request-form-success'\n className={cn('rounded-lg border border-border p-6 text-sm', className)}>\n {t.success(deadline)}\n </div>\n )\n }\n\n const errorId = (field: keyof FieldErrors) => (errors[field] ? `${id}-${field}-error` : undefined)\n const fieldError = (field: keyof FieldErrors) =>\n errors[field] ? (\n <p id={`${id}-${field}-error`} className='text-xs text-destructive'>\n {errors[field]}\n </p>\n ) : null\n\n return (\n <form\n noValidate\n onSubmit={handleSubmit}\n onFocusCapture={onInteract}\n onPointerDown={onInteract}\n data-slot='data-request-form'\n className={cn('space-y-4', className)}\n >\n <div className='space-y-1'>\n <label htmlFor={`${id}-type`} className='text-sm font-medium'>\n {t.requestTypeLabel}\n </label>\n <select\n id={`${id}-type`}\n value={requestType}\n onChange={(e) => setRequestType(e.target.value as DataRequestType | '')}\n aria-invalid={errors.requestType ? true : undefined}\n aria-describedby={errorId('requestType')}\n className={cn(FIELD_CLASS, errors.requestType ? 'border-destructive' : 'border-border')}\n >\n <option value=''>{t.requestTypePlaceholder}</option>\n {requestTypes.map((type) => (\n <option key={type} value={type}>\n {t.requestTypes[type]}\n </option>\n ))}\n </select>\n {fieldError('requestType')}\n </div>\n\n <div className='space-y-1'>\n <label htmlFor={`${id}-name`} className='text-sm font-medium'>\n {t.nameLabel}\n </label>\n <input\n id={`${id}-name`}\n type='text'\n autoComplete='name'\n maxLength={DATA_REQUEST_FIELD_LIMITS.name}\n value={name}\n onChange={(e) => setName(e.target.value)}\n aria-invalid={errors.name ? true : undefined}\n aria-describedby={errorId('name')}\n className={cn(FIELD_CLASS, errors.name ? 'border-destructive' : 'border-border')}\n />\n {fieldError('name')}\n </div>\n\n <div className='space-y-1'>\n <label htmlFor={`${id}-email`} className='text-sm font-medium'>\n {t.emailLabel}\n </label>\n <input\n id={`${id}-email`}\n type='email'\n autoComplete='email'\n maxLength={DATA_REQUEST_FIELD_LIMITS.email}\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n aria-invalid={errors.email ? true : undefined}\n aria-describedby={errorId('email')}\n className={cn(FIELD_CLASS, errors.email ? 'border-destructive' : 'border-border')}\n />\n {fieldError('email')}\n </div>\n\n <div className='space-y-1'>\n <label htmlFor={`${id}-details`} className='text-sm font-medium'>\n {t.detailsLabel}\n </label>\n <textarea\n id={`${id}-details`}\n rows={4}\n maxLength={DATA_REQUEST_FIELD_LIMITS.details}\n value={details}\n onChange={(e) => setDetails(e.target.value)}\n placeholder={t.detailsPlaceholder}\n className={cn(FIELD_CLASS, 'border-border')}\n />\n </div>\n\n <div className='space-y-1'>\n <div className='flex items-start gap-2'>\n <input\n id={`${id}-consent`}\n type='checkbox'\n checked={consent}\n onChange={(e) => setConsent(e.target.checked)}\n aria-invalid={errors.consent ? true : undefined}\n aria-describedby={errorId('consent')}\n className='mt-1'\n />\n <label htmlFor={`${id}-consent`} className='text-sm text-foreground/80'>\n {t.consent}\n </label>\n </div>\n {fieldError('consent')}\n </div>\n\n {formError && (\n <p role='alert' className='text-sm text-destructive'>\n {formError}\n </p>\n )}\n\n <button\n type='submit'\n disabled={submitting}\n className='rounded-lg bg-primary px-6 py-2 text-sm font-semibold text-primary-foreground disabled:opacity-60'\n >\n {submitting ? t.submitting : t.submit}\n </button>\n </form>\n )\n}\n"],"mappings":"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","names":["import_utils","import_jsx_runtime","DEFAULT_LABELS","import_react","import_utils","import_jsx_runtime","DEFAULT_LABELS","import_react","import_utils","import_jsx_runtime","DEFAULT_LABELS","import_react","import_utils","import_jsx_runtime"]}
package/dist/client.d.cts CHANGED
@@ -2,6 +2,7 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as React from 'react';
3
3
  import { ReactNode } from 'react';
4
4
  import { DataRequestType, DataRequestInput, DataRequestSubmission } from '@reblu/site-contracts';
5
+ export { DATA_REQUEST_TYPES } from '@reblu/site-contracts';
5
6
 
6
7
  /**
7
8
  * `useCollection` — the headless heart of `@reblu/site-blocks`.
@@ -275,7 +276,13 @@ type DataRequestSubmitResult = {
275
276
  ok: false;
276
277
  status: number;
277
278
  };
278
- declare const DATA_REQUEST_TYPES: readonly DataRequestType[];
279
+
280
+ /** The server's field limits (base-api `POST /api/site/data-requests`). */
281
+ declare const DATA_REQUEST_FIELD_LIMITS: Readonly<{
282
+ name: 200;
283
+ email: 320;
284
+ details: 5000;
285
+ }>;
279
286
  interface DataRequestFormLabels {
280
287
  requestTypeLabel: string;
281
288
  requestTypePlaceholder: string;
@@ -319,4 +326,4 @@ interface DataRequestFormProps {
319
326
  }
320
327
  declare function DataRequestForm({ getCaptchaToken, submit, endpoint, onInteract, onSubmitted, requestTypes, labels, locale, className, }: DataRequestFormProps): react_jsx_runtime.JSX.Element;
321
328
 
322
- export { AddToCartButton, type AddToCartButtonLabels, type AddToCartButtonProps, type AddToCartButtonStatus, CartIcon, type CartIconLabels, type CartIconProps, Collection, type CollectionFacet, type CollectionFacetOption, type CollectionFilter, type CollectionLabels, type CollectionProps, DATA_REQUEST_TYPES, DEFAULT_DATA_REQUEST_FORM_LABELS, DataRequestForm, type DataRequestFormLabels, type DataRequestFormProps, type DataRequestSubmitResult, ShareButtons, type ShareButtonsLabels, type ShareButtonsProps, type UseCollectionOptions, type UseCollectionResult, useCollection };
329
+ export { AddToCartButton, type AddToCartButtonLabels, type AddToCartButtonProps, type AddToCartButtonStatus, CartIcon, type CartIconLabels, type CartIconProps, Collection, type CollectionFacet, type CollectionFacetOption, type CollectionFilter, type CollectionLabels, type CollectionProps, DATA_REQUEST_FIELD_LIMITS, DEFAULT_DATA_REQUEST_FORM_LABELS, DataRequestForm, type DataRequestFormLabels, type DataRequestFormProps, type DataRequestSubmitResult, ShareButtons, type ShareButtonsLabels, type ShareButtonsProps, type UseCollectionOptions, type UseCollectionResult, useCollection };