@reblu/site-blocks 0.15.0 → 0.16.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 +16 -0
- package/README.md +48 -16
- package/dist/client.cjs +31 -14
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +9 -2
- package/dist/client.d.ts +9 -2
- package/dist/client.js +31 -13
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +58 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -4
- package/dist/index.d.ts +30 -4
- package/dist/index.js +57 -24
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/client.d.ts
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
|
-
|
|
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,
|
|
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 };
|
package/dist/client.js
CHANGED
|
@@ -485,19 +485,13 @@ function CartIcon({ itemCount, href, hydrated, bumpOnAdd, labels, className }) {
|
|
|
485
485
|
}
|
|
486
486
|
|
|
487
487
|
// src/components/data-request-form.tsx
|
|
488
|
-
import { useId, useState as useState5 } from "react";
|
|
488
|
+
import { useEffect as useEffect3, useId, useRef as useRef3, useState as useState5 } from "react";
|
|
489
489
|
import { cn as cn5 } from "@reblu/site-ui/utils";
|
|
490
|
+
import {
|
|
491
|
+
DATA_REQUEST_TYPES
|
|
492
|
+
} from "@reblu/site-contracts";
|
|
490
493
|
import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
491
|
-
var
|
|
492
|
-
"ACCESS",
|
|
493
|
-
"RECTIFICATION",
|
|
494
|
-
"ERASURE",
|
|
495
|
-
"OBJECTION",
|
|
496
|
-
"PORTABILITY",
|
|
497
|
-
"RESTRICTION",
|
|
498
|
-
"AUTOMATED_DECISION",
|
|
499
|
-
"INFORMATION"
|
|
500
|
-
];
|
|
494
|
+
var DATA_REQUEST_FIELD_LIMITS = Object.freeze({ name: 200, email: 320, details: 5e3 });
|
|
501
495
|
var DEFAULT_DATA_REQUEST_FORM_LABELS = {
|
|
502
496
|
requestTypeLabel: "Derecho que quieres ejercer",
|
|
503
497
|
requestTypePlaceholder: "Selecciona una opci\xF3n",
|
|
@@ -532,6 +526,7 @@ var DEFAULT_DATA_REQUEST_FORM_LABELS = {
|
|
|
532
526
|
rateLimitError: "Has enviado demasiadas solicitudes. Espera unos minutos e int\xE9ntalo de nuevo.",
|
|
533
527
|
genericError: "No hemos podido enviar tu solicitud. Int\xE9ntalo de nuevo m\xE1s tarde."
|
|
534
528
|
};
|
|
529
|
+
var FIELD_ORDER = ["requestType", "name", "email", "consent"];
|
|
535
530
|
var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
536
531
|
async function postToRoute(endpoint, input) {
|
|
537
532
|
try {
|
|
@@ -580,6 +575,10 @@ function DataRequestForm({
|
|
|
580
575
|
const [formError, setFormError] = useState5(null);
|
|
581
576
|
const [submitting, setSubmitting] = useState5(false);
|
|
582
577
|
const [ack, setAck] = useState5(null);
|
|
578
|
+
const statusRef = useRef3(null);
|
|
579
|
+
useEffect3(() => {
|
|
580
|
+
if (ack) statusRef.current?.focus();
|
|
581
|
+
}, [ack]);
|
|
583
582
|
async function handleSubmit(event) {
|
|
584
583
|
event.preventDefault();
|
|
585
584
|
if (submitting) return;
|
|
@@ -590,7 +589,12 @@ function DataRequestForm({
|
|
|
590
589
|
if (!EMAIL_PATTERN.test(email.trim())) next.email = t.emailInvalid;
|
|
591
590
|
if (!consent) next.consent = t.consentRequired;
|
|
592
591
|
setErrors(next);
|
|
593
|
-
|
|
592
|
+
const firstInvalid = FIELD_ORDER.find((field) => next[field]);
|
|
593
|
+
if (firstInvalid) {
|
|
594
|
+
document.getElementById(`${id}-${firstInvalid === "requestType" ? "type" : firstInvalid}`)?.focus();
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if (!requestType) return;
|
|
594
598
|
setSubmitting(true);
|
|
595
599
|
try {
|
|
596
600
|
const captcha = await getCaptchaToken().catch(() => void 0);
|
|
@@ -623,7 +627,17 @@ function DataRequestForm({
|
|
|
623
627
|
dateStyle: "long",
|
|
624
628
|
timeStyle: "short"
|
|
625
629
|
}).format(new Date(ack.slaDueAt));
|
|
626
|
-
return /* @__PURE__ */ jsx5(
|
|
630
|
+
return /* @__PURE__ */ jsx5(
|
|
631
|
+
"div",
|
|
632
|
+
{
|
|
633
|
+
ref: statusRef,
|
|
634
|
+
tabIndex: -1,
|
|
635
|
+
role: "status",
|
|
636
|
+
"data-slot": "data-request-form-success",
|
|
637
|
+
className: cn5("rounded-lg border border-border p-6 text-sm", className),
|
|
638
|
+
children: t.success(deadline)
|
|
639
|
+
}
|
|
640
|
+
);
|
|
627
641
|
}
|
|
628
642
|
const errorId = (field) => errors[field] ? `${id}-${field}-error` : void 0;
|
|
629
643
|
const fieldError = (field) => errors[field] ? /* @__PURE__ */ jsx5("p", { id: `${id}-${field}-error`, className: "text-xs text-destructive", children: errors[field] }) : null;
|
|
@@ -664,6 +678,7 @@ function DataRequestForm({
|
|
|
664
678
|
id: `${id}-name`,
|
|
665
679
|
type: "text",
|
|
666
680
|
autoComplete: "name",
|
|
681
|
+
maxLength: DATA_REQUEST_FIELD_LIMITS.name,
|
|
667
682
|
value: name,
|
|
668
683
|
onChange: (e) => setName(e.target.value),
|
|
669
684
|
"aria-invalid": errors.name ? true : void 0,
|
|
@@ -681,6 +696,7 @@ function DataRequestForm({
|
|
|
681
696
|
id: `${id}-email`,
|
|
682
697
|
type: "email",
|
|
683
698
|
autoComplete: "email",
|
|
699
|
+
maxLength: DATA_REQUEST_FIELD_LIMITS.email,
|
|
684
700
|
value: email,
|
|
685
701
|
onChange: (e) => setEmail(e.target.value),
|
|
686
702
|
"aria-invalid": errors.email ? true : void 0,
|
|
@@ -697,6 +713,7 @@ function DataRequestForm({
|
|
|
697
713
|
{
|
|
698
714
|
id: `${id}-details`,
|
|
699
715
|
rows: 4,
|
|
716
|
+
maxLength: DATA_REQUEST_FIELD_LIMITS.details,
|
|
700
717
|
value: details,
|
|
701
718
|
onChange: (e) => setDetails(e.target.value),
|
|
702
719
|
placeholder: t.detailsPlaceholder,
|
|
@@ -740,6 +757,7 @@ export {
|
|
|
740
757
|
AddToCartButton,
|
|
741
758
|
CartIcon,
|
|
742
759
|
Collection,
|
|
760
|
+
DATA_REQUEST_FIELD_LIMITS,
|
|
743
761
|
DATA_REQUEST_TYPES,
|
|
744
762
|
DEFAULT_DATA_REQUEST_FORM_LABELS,
|
|
745
763
|
DataRequestForm,
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../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\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":["cn","jsx","jsxs","DEFAULT_LABELS","cn","useState","cn","jsx","DEFAULT_LABELS","useEffect","useRef","useState","cn","jsx","jsxs","DEFAULT_LABELS","useState","cn","jsx","jsxs"]}
|
|
1
|
+
{"version":3,"sources":["../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\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":["cn","jsx","jsxs","DEFAULT_LABELS","cn","useState","cn","jsx","DEFAULT_LABELS","useEffect","useRef","useState","cn","jsx","jsxs","DEFAULT_LABELS","useEffect","useRef","useState","cn","jsx","jsxs","jsxs","useState","useRef","useEffect","jsx","cn"]}
|
package/dist/index.cjs
CHANGED
|
@@ -28,6 +28,7 @@ __export(src_exports, {
|
|
|
28
28
|
CheckoutForm: () => CheckoutForm,
|
|
29
29
|
CheckoutLoginPrompt: () => CheckoutLoginPrompt,
|
|
30
30
|
CourseDescription: () => CourseDescription,
|
|
31
|
+
DATA_REQUEST_TYPES: () => import_site_contracts.DATA_REQUEST_TYPES,
|
|
31
32
|
DEFAULT_INVOICE_COUNTRIES: () => DEFAULT_INVOICE_COUNTRIES,
|
|
32
33
|
DEFAULT_SHOP_ERROR_FALLBACK_MESSAGE: () => DEFAULT_SHOP_ERROR_FALLBACK_MESSAGE,
|
|
33
34
|
DEFAULT_SHOP_ERROR_MESSAGES: () => DEFAULT_SHOP_ERROR_MESSAGES,
|
|
@@ -13989,12 +13990,18 @@ function safeHref(url) {
|
|
|
13989
13990
|
return null;
|
|
13990
13991
|
}
|
|
13991
13992
|
}
|
|
13993
|
+
function safeLogoSrc(url) {
|
|
13994
|
+
if (typeof url !== "string" || url === "") return null;
|
|
13995
|
+
if (url.startsWith("/")) return url.startsWith("//") || url.startsWith("/\\") ? null : url;
|
|
13996
|
+
return safeHref(url);
|
|
13997
|
+
}
|
|
13992
13998
|
function BadgeContent({ badge, logoClassName }) {
|
|
13993
|
-
|
|
13999
|
+
const src = safeLogoSrc(badge.logoUrl);
|
|
14000
|
+
if (src) {
|
|
13994
14001
|
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
13995
14002
|
"img",
|
|
13996
14003
|
{
|
|
13997
|
-
src
|
|
14004
|
+
src,
|
|
13998
14005
|
alt: badge.name,
|
|
13999
14006
|
loading: "lazy",
|
|
14000
14007
|
className: (0, import_utils11.cn)("h-12 w-auto object-contain", logoClassName)
|
|
@@ -14007,11 +14014,14 @@ function TrustBadges({
|
|
|
14007
14014
|
badges,
|
|
14008
14015
|
headings,
|
|
14009
14016
|
types = DEFAULT_TRUST_BADGE_TYPE_ORDER,
|
|
14017
|
+
heading,
|
|
14018
|
+
headingId = "trust-badges-heading",
|
|
14010
14019
|
headingLevel = 2,
|
|
14011
14020
|
newWindowLabel = "(se abre en una ventana nueva)",
|
|
14012
14021
|
className,
|
|
14013
14022
|
groupClassName,
|
|
14014
14023
|
headingClassName,
|
|
14024
|
+
sectionHeadingClassName,
|
|
14015
14025
|
listClassName,
|
|
14016
14026
|
logoClassName
|
|
14017
14027
|
}) {
|
|
@@ -14020,46 +14030,66 @@ function TrustBadges({
|
|
|
14020
14030
|
const groups = types.flatMap((type) => {
|
|
14021
14031
|
const items = usable.filter((b2) => b2.type === type).map((b2, index) => ({ b: b2, index })).sort((x2, y2) => (x2.b.sortOrder ?? 0) - (y2.b.sortOrder ?? 0) || x2.index - y2.index).map(({ b: b2 }) => b2);
|
|
14022
14032
|
if (items.length === 0) return [];
|
|
14023
|
-
const
|
|
14024
|
-
return [{ type,
|
|
14033
|
+
const groupHeading = headings && Object.prototype.hasOwnProperty.call(headings, type) ? headings[type] : DEFAULT_TRUST_BADGE_HEADINGS[type];
|
|
14034
|
+
return [{ type, groupHeading, items }];
|
|
14025
14035
|
});
|
|
14026
14036
|
if (groups.length === 0) return null;
|
|
14027
|
-
|
|
14037
|
+
const hasHeading = heading !== void 0 && heading !== null && heading !== "";
|
|
14038
|
+
const groupHeadingLevel = hasHeading ? nextHeadingLevel(headingLevel) : headingLevel;
|
|
14039
|
+
const content = groups.map(({ type, groupHeading, items }) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
|
|
14028
14040
|
"div",
|
|
14029
14041
|
{
|
|
14030
14042
|
"data-slot": "trust-badge-group",
|
|
14031
14043
|
"data-type": type,
|
|
14032
14044
|
className: (0, import_utils11.cn)("space-y-3", groupClassName),
|
|
14033
14045
|
children: [
|
|
14034
|
-
|
|
14046
|
+
groupHeading ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
14035
14047
|
Heading,
|
|
14036
14048
|
{
|
|
14037
|
-
level:
|
|
14038
|
-
className: (
|
|
14039
|
-
|
|
14049
|
+
level: groupHeadingLevel,
|
|
14050
|
+
className: slotClassName(
|
|
14051
|
+
"",
|
|
14052
|
+
"text-sm font-semibold uppercase tracking-widest",
|
|
14053
|
+
headingClassName
|
|
14054
|
+
),
|
|
14055
|
+
children: groupHeading
|
|
14040
14056
|
}
|
|
14041
14057
|
) : null,
|
|
14042
14058
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("ul", { className: (0, import_utils11.cn)("flex flex-wrap items-center gap-6", listClassName), children: items.map((badge) => {
|
|
14043
14059
|
const href = safeHref(badge.url);
|
|
14044
|
-
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("li", { children: href ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
|
|
14045
|
-
|
|
14046
|
-
|
|
14047
|
-
|
|
14048
|
-
|
|
14049
|
-
rel: "noopener noreferrer",
|
|
14050
|
-
className: "inline-flex",
|
|
14051
|
-
children: [
|
|
14052
|
-
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(BadgeContent, { badge, logoClassName }),
|
|
14053
|
-
" ",
|
|
14054
|
-
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "sr-only", children: newWindowLabel })
|
|
14055
|
-
]
|
|
14056
|
-
}
|
|
14057
|
-
) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(BadgeContent, { badge, logoClassName }) }, badge.id);
|
|
14060
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("li", { children: href ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("a", { href, target: "_blank", rel: "noopener noreferrer", className: "inline-flex", children: [
|
|
14061
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(BadgeContent, { badge, logoClassName }),
|
|
14062
|
+
" ",
|
|
14063
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "sr-only", children: newWindowLabel })
|
|
14064
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(BadgeContent, { badge, logoClassName }) }, badge.id);
|
|
14058
14065
|
}) })
|
|
14059
14066
|
]
|
|
14060
14067
|
},
|
|
14061
14068
|
type
|
|
14062
|
-
))
|
|
14069
|
+
));
|
|
14070
|
+
if (!hasHeading) {
|
|
14071
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { "data-slot": "trust-badges", className: (0, import_utils11.cn)("space-y-6", className), children: content });
|
|
14072
|
+
}
|
|
14073
|
+
const Tag = `h${headingLevel}`;
|
|
14074
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
|
|
14075
|
+
"section",
|
|
14076
|
+
{
|
|
14077
|
+
"data-slot": "trust-badges",
|
|
14078
|
+
"aria-labelledby": headingId,
|
|
14079
|
+
className: (0, import_utils11.cn)("space-y-6", className),
|
|
14080
|
+
children: [
|
|
14081
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
14082
|
+
Tag,
|
|
14083
|
+
{
|
|
14084
|
+
id: headingId,
|
|
14085
|
+
className: slotClassName("", "text-xl font-semibold", sectionHeadingClassName),
|
|
14086
|
+
children: heading
|
|
14087
|
+
}
|
|
14088
|
+
),
|
|
14089
|
+
content
|
|
14090
|
+
]
|
|
14091
|
+
}
|
|
14092
|
+
);
|
|
14063
14093
|
}
|
|
14064
14094
|
|
|
14065
14095
|
// src/components/page-blocks.tsx
|
|
@@ -15490,6 +15520,9 @@ function FullInvoiceFields({
|
|
|
15490
15520
|
] })
|
|
15491
15521
|
] });
|
|
15492
15522
|
}
|
|
15523
|
+
|
|
15524
|
+
// src/index.ts
|
|
15525
|
+
var import_site_contracts = require("@reblu/site-contracts");
|
|
15493
15526
|
// Annotate the CommonJS export names for ESM import in node:
|
|
15494
15527
|
0 && (module.exports = {
|
|
15495
15528
|
ALLOWED_YOUTUBE_HOSTS,
|
|
@@ -15500,6 +15533,7 @@ function FullInvoiceFields({
|
|
|
15500
15533
|
CheckoutForm,
|
|
15501
15534
|
CheckoutLoginPrompt,
|
|
15502
15535
|
CourseDescription,
|
|
15536
|
+
DATA_REQUEST_TYPES,
|
|
15503
15537
|
DEFAULT_INVOICE_COUNTRIES,
|
|
15504
15538
|
DEFAULT_SHOP_ERROR_FALLBACK_MESSAGE,
|
|
15505
15539
|
DEFAULT_SHOP_ERROR_MESSAGES,
|