@reblu/site-blocks 0.14.0 → 0.15.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 +13 -0
- package/README.md +137 -2
- package/dist/client.cjs +259 -0
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +76 -1
- package/dist/client.d.ts +76 -1
- package/dist/client.js +256 -0
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +386 -256
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -7
- package/dist/index.d.ts +66 -7
- package/dist/index.js +383 -256
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/client.js
CHANGED
|
@@ -483,10 +483,266 @@ function CartIcon({ itemCount, href, hydrated, bumpOnAdd, labels, className }) {
|
|
|
483
483
|
}
|
|
484
484
|
);
|
|
485
485
|
}
|
|
486
|
+
|
|
487
|
+
// src/components/data-request-form.tsx
|
|
488
|
+
import { useId, useState as useState5 } from "react";
|
|
489
|
+
import { cn as cn5 } from "@reblu/site-ui/utils";
|
|
490
|
+
import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
491
|
+
var DATA_REQUEST_TYPES = [
|
|
492
|
+
"ACCESS",
|
|
493
|
+
"RECTIFICATION",
|
|
494
|
+
"ERASURE",
|
|
495
|
+
"OBJECTION",
|
|
496
|
+
"PORTABILITY",
|
|
497
|
+
"RESTRICTION",
|
|
498
|
+
"AUTOMATED_DECISION",
|
|
499
|
+
"INFORMATION"
|
|
500
|
+
];
|
|
501
|
+
var DEFAULT_DATA_REQUEST_FORM_LABELS = {
|
|
502
|
+
requestTypeLabel: "Derecho que quieres ejercer",
|
|
503
|
+
requestTypePlaceholder: "Selecciona una opci\xF3n",
|
|
504
|
+
requestTypes: {
|
|
505
|
+
ACCESS: "Acceso",
|
|
506
|
+
RECTIFICATION: "Rectificaci\xF3n",
|
|
507
|
+
ERASURE: "Supresi\xF3n",
|
|
508
|
+
OBJECTION: "Oposici\xF3n",
|
|
509
|
+
PORTABILITY: "Portabilidad",
|
|
510
|
+
RESTRICTION: "Limitaci\xF3n del tratamiento",
|
|
511
|
+
AUTOMATED_DECISION: "No ser objeto de decisiones automatizadas",
|
|
512
|
+
INFORMATION: "Informaci\xF3n"
|
|
513
|
+
},
|
|
514
|
+
nameLabel: "Nombre y apellidos",
|
|
515
|
+
emailLabel: "Email",
|
|
516
|
+
detailsLabel: "Mensaje (opcional)",
|
|
517
|
+
detailsPlaceholder: "Describe tu solicitud",
|
|
518
|
+
consent: "Acepto que mis datos se traten para gestionar esta solicitud.",
|
|
519
|
+
submit: "Enviar solicitud",
|
|
520
|
+
submitting: "Enviando\u2026",
|
|
521
|
+
requestTypeRequired: "Elige el derecho que quieres ejercer.",
|
|
522
|
+
nameRequired: "Indica tu nombre.",
|
|
523
|
+
emailInvalid: "Introduce un email v\xE1lido.",
|
|
524
|
+
consentRequired: "Debes aceptar el tratamiento de tus datos para gestionar la solicitud.",
|
|
525
|
+
success: (deadline) => /* @__PURE__ */ jsxs4(Fragment, { children: [
|
|
526
|
+
"Hemos recibido tu solicitud. Te responderemos como m\xE1ximo el ",
|
|
527
|
+
deadline,
|
|
528
|
+
"."
|
|
529
|
+
] }),
|
|
530
|
+
captchaError: "No hemos podido verificar que no eres un robot. Recarga la p\xE1gina e int\xE9ntalo de nuevo.",
|
|
531
|
+
validationError: "Revisa los datos del formulario e int\xE9ntalo de nuevo.",
|
|
532
|
+
rateLimitError: "Has enviado demasiadas solicitudes. Espera unos minutos e int\xE9ntalo de nuevo.",
|
|
533
|
+
genericError: "No hemos podido enviar tu solicitud. Int\xE9ntalo de nuevo m\xE1s tarde."
|
|
534
|
+
};
|
|
535
|
+
var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
536
|
+
async function postToRoute(endpoint, input) {
|
|
537
|
+
try {
|
|
538
|
+
const res = await fetch(endpoint, {
|
|
539
|
+
method: "POST",
|
|
540
|
+
headers: { "Content-Type": "application/json" },
|
|
541
|
+
body: JSON.stringify(input)
|
|
542
|
+
});
|
|
543
|
+
if (!res.ok) return { ok: false, status: res.status };
|
|
544
|
+
const json = await res.json();
|
|
545
|
+
return json.data ? { ok: true, data: json.data } : { ok: false, status: res.status };
|
|
546
|
+
} catch {
|
|
547
|
+
return { ok: false, status: 0 };
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
function rejectionMessage(status, t) {
|
|
551
|
+
if (status === 400) return t.validationError;
|
|
552
|
+
if (status === 422) return t.captchaError;
|
|
553
|
+
if (status === 429) return t.rateLimitError;
|
|
554
|
+
return t.genericError;
|
|
555
|
+
}
|
|
556
|
+
var FIELD_CLASS = "w-full rounded-lg border bg-background px-3 py-2 text-sm transition focus:outline-none focus:ring-2 focus:ring-primary/20";
|
|
557
|
+
function DataRequestForm({
|
|
558
|
+
getCaptchaToken,
|
|
559
|
+
submit,
|
|
560
|
+
endpoint = "/api/data-requests",
|
|
561
|
+
onInteract,
|
|
562
|
+
onSubmitted,
|
|
563
|
+
requestTypes = DATA_REQUEST_TYPES,
|
|
564
|
+
labels,
|
|
565
|
+
locale = "es-ES",
|
|
566
|
+
className
|
|
567
|
+
}) {
|
|
568
|
+
const t = {
|
|
569
|
+
...DEFAULT_DATA_REQUEST_FORM_LABELS,
|
|
570
|
+
...labels,
|
|
571
|
+
requestTypes: { ...DEFAULT_DATA_REQUEST_FORM_LABELS.requestTypes, ...labels?.requestTypes }
|
|
572
|
+
};
|
|
573
|
+
const id = useId();
|
|
574
|
+
const [requestType, setRequestType] = useState5("");
|
|
575
|
+
const [name, setName] = useState5("");
|
|
576
|
+
const [email, setEmail] = useState5("");
|
|
577
|
+
const [details, setDetails] = useState5("");
|
|
578
|
+
const [consent, setConsent] = useState5(false);
|
|
579
|
+
const [errors, setErrors] = useState5({});
|
|
580
|
+
const [formError, setFormError] = useState5(null);
|
|
581
|
+
const [submitting, setSubmitting] = useState5(false);
|
|
582
|
+
const [ack, setAck] = useState5(null);
|
|
583
|
+
async function handleSubmit(event) {
|
|
584
|
+
event.preventDefault();
|
|
585
|
+
if (submitting) return;
|
|
586
|
+
setFormError(null);
|
|
587
|
+
const next = {};
|
|
588
|
+
if (!requestType) next.requestType = t.requestTypeRequired;
|
|
589
|
+
if (!name.trim()) next.name = t.nameRequired;
|
|
590
|
+
if (!EMAIL_PATTERN.test(email.trim())) next.email = t.emailInvalid;
|
|
591
|
+
if (!consent) next.consent = t.consentRequired;
|
|
592
|
+
setErrors(next);
|
|
593
|
+
if (Object.keys(next).length > 0 || !requestType) return;
|
|
594
|
+
setSubmitting(true);
|
|
595
|
+
try {
|
|
596
|
+
const captcha = await getCaptchaToken().catch(() => void 0);
|
|
597
|
+
if (!captcha) {
|
|
598
|
+
setFormError(t.captchaError);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const input = {
|
|
602
|
+
requestType,
|
|
603
|
+
name: name.trim(),
|
|
604
|
+
email: email.trim(),
|
|
605
|
+
...details.trim() ? { details: details.trim() } : {},
|
|
606
|
+
captcha
|
|
607
|
+
};
|
|
608
|
+
const result = await (submit ?? ((body) => postToRoute(endpoint, body)))(input).catch(
|
|
609
|
+
() => ({ ok: false, status: 0 })
|
|
610
|
+
);
|
|
611
|
+
if (!result.ok) {
|
|
612
|
+
setFormError(rejectionMessage(result.status, t));
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
setAck(result.data);
|
|
616
|
+
onSubmitted?.(result.data);
|
|
617
|
+
} finally {
|
|
618
|
+
setSubmitting(false);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (ack) {
|
|
622
|
+
const deadline = new Intl.DateTimeFormat(locale, {
|
|
623
|
+
dateStyle: "long",
|
|
624
|
+
timeStyle: "short"
|
|
625
|
+
}).format(new Date(ack.slaDueAt));
|
|
626
|
+
return /* @__PURE__ */ jsx5("div", { role: "status", "data-slot": "data-request-form-success", className: cn5("rounded-lg border border-border p-6 text-sm", className), children: t.success(deadline) });
|
|
627
|
+
}
|
|
628
|
+
const errorId = (field) => errors[field] ? `${id}-${field}-error` : void 0;
|
|
629
|
+
const fieldError = (field) => errors[field] ? /* @__PURE__ */ jsx5("p", { id: `${id}-${field}-error`, className: "text-xs text-destructive", children: errors[field] }) : null;
|
|
630
|
+
return /* @__PURE__ */ jsxs4(
|
|
631
|
+
"form",
|
|
632
|
+
{
|
|
633
|
+
noValidate: true,
|
|
634
|
+
onSubmit: handleSubmit,
|
|
635
|
+
onFocusCapture: onInteract,
|
|
636
|
+
onPointerDown: onInteract,
|
|
637
|
+
"data-slot": "data-request-form",
|
|
638
|
+
className: cn5("space-y-4", className),
|
|
639
|
+
children: [
|
|
640
|
+
/* @__PURE__ */ jsxs4("div", { className: "space-y-1", children: [
|
|
641
|
+
/* @__PURE__ */ jsx5("label", { htmlFor: `${id}-type`, className: "text-sm font-medium", children: t.requestTypeLabel }),
|
|
642
|
+
/* @__PURE__ */ jsxs4(
|
|
643
|
+
"select",
|
|
644
|
+
{
|
|
645
|
+
id: `${id}-type`,
|
|
646
|
+
value: requestType,
|
|
647
|
+
onChange: (e) => setRequestType(e.target.value),
|
|
648
|
+
"aria-invalid": errors.requestType ? true : void 0,
|
|
649
|
+
"aria-describedby": errorId("requestType"),
|
|
650
|
+
className: cn5(FIELD_CLASS, errors.requestType ? "border-destructive" : "border-border"),
|
|
651
|
+
children: [
|
|
652
|
+
/* @__PURE__ */ jsx5("option", { value: "", children: t.requestTypePlaceholder }),
|
|
653
|
+
requestTypes.map((type) => /* @__PURE__ */ jsx5("option", { value: type, children: t.requestTypes[type] }, type))
|
|
654
|
+
]
|
|
655
|
+
}
|
|
656
|
+
),
|
|
657
|
+
fieldError("requestType")
|
|
658
|
+
] }),
|
|
659
|
+
/* @__PURE__ */ jsxs4("div", { className: "space-y-1", children: [
|
|
660
|
+
/* @__PURE__ */ jsx5("label", { htmlFor: `${id}-name`, className: "text-sm font-medium", children: t.nameLabel }),
|
|
661
|
+
/* @__PURE__ */ jsx5(
|
|
662
|
+
"input",
|
|
663
|
+
{
|
|
664
|
+
id: `${id}-name`,
|
|
665
|
+
type: "text",
|
|
666
|
+
autoComplete: "name",
|
|
667
|
+
value: name,
|
|
668
|
+
onChange: (e) => setName(e.target.value),
|
|
669
|
+
"aria-invalid": errors.name ? true : void 0,
|
|
670
|
+
"aria-describedby": errorId("name"),
|
|
671
|
+
className: cn5(FIELD_CLASS, errors.name ? "border-destructive" : "border-border")
|
|
672
|
+
}
|
|
673
|
+
),
|
|
674
|
+
fieldError("name")
|
|
675
|
+
] }),
|
|
676
|
+
/* @__PURE__ */ jsxs4("div", { className: "space-y-1", children: [
|
|
677
|
+
/* @__PURE__ */ jsx5("label", { htmlFor: `${id}-email`, className: "text-sm font-medium", children: t.emailLabel }),
|
|
678
|
+
/* @__PURE__ */ jsx5(
|
|
679
|
+
"input",
|
|
680
|
+
{
|
|
681
|
+
id: `${id}-email`,
|
|
682
|
+
type: "email",
|
|
683
|
+
autoComplete: "email",
|
|
684
|
+
value: email,
|
|
685
|
+
onChange: (e) => setEmail(e.target.value),
|
|
686
|
+
"aria-invalid": errors.email ? true : void 0,
|
|
687
|
+
"aria-describedby": errorId("email"),
|
|
688
|
+
className: cn5(FIELD_CLASS, errors.email ? "border-destructive" : "border-border")
|
|
689
|
+
}
|
|
690
|
+
),
|
|
691
|
+
fieldError("email")
|
|
692
|
+
] }),
|
|
693
|
+
/* @__PURE__ */ jsxs4("div", { className: "space-y-1", children: [
|
|
694
|
+
/* @__PURE__ */ jsx5("label", { htmlFor: `${id}-details`, className: "text-sm font-medium", children: t.detailsLabel }),
|
|
695
|
+
/* @__PURE__ */ jsx5(
|
|
696
|
+
"textarea",
|
|
697
|
+
{
|
|
698
|
+
id: `${id}-details`,
|
|
699
|
+
rows: 4,
|
|
700
|
+
value: details,
|
|
701
|
+
onChange: (e) => setDetails(e.target.value),
|
|
702
|
+
placeholder: t.detailsPlaceholder,
|
|
703
|
+
className: cn5(FIELD_CLASS, "border-border")
|
|
704
|
+
}
|
|
705
|
+
)
|
|
706
|
+
] }),
|
|
707
|
+
/* @__PURE__ */ jsxs4("div", { className: "space-y-1", children: [
|
|
708
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex items-start gap-2", children: [
|
|
709
|
+
/* @__PURE__ */ jsx5(
|
|
710
|
+
"input",
|
|
711
|
+
{
|
|
712
|
+
id: `${id}-consent`,
|
|
713
|
+
type: "checkbox",
|
|
714
|
+
checked: consent,
|
|
715
|
+
onChange: (e) => setConsent(e.target.checked),
|
|
716
|
+
"aria-invalid": errors.consent ? true : void 0,
|
|
717
|
+
"aria-describedby": errorId("consent"),
|
|
718
|
+
className: "mt-1"
|
|
719
|
+
}
|
|
720
|
+
),
|
|
721
|
+
/* @__PURE__ */ jsx5("label", { htmlFor: `${id}-consent`, className: "text-sm text-foreground/80", children: t.consent })
|
|
722
|
+
] }),
|
|
723
|
+
fieldError("consent")
|
|
724
|
+
] }),
|
|
725
|
+
formError && /* @__PURE__ */ jsx5("p", { role: "alert", className: "text-sm text-destructive", children: formError }),
|
|
726
|
+
/* @__PURE__ */ jsx5(
|
|
727
|
+
"button",
|
|
728
|
+
{
|
|
729
|
+
type: "submit",
|
|
730
|
+
disabled: submitting,
|
|
731
|
+
className: "rounded-lg bg-primary px-6 py-2 text-sm font-semibold text-primary-foreground disabled:opacity-60",
|
|
732
|
+
children: submitting ? t.submitting : t.submit
|
|
733
|
+
}
|
|
734
|
+
)
|
|
735
|
+
]
|
|
736
|
+
}
|
|
737
|
+
);
|
|
738
|
+
}
|
|
486
739
|
export {
|
|
487
740
|
AddToCartButton,
|
|
488
741
|
CartIcon,
|
|
489
742
|
Collection,
|
|
743
|
+
DATA_REQUEST_TYPES,
|
|
744
|
+
DEFAULT_DATA_REQUEST_FORM_LABELS,
|
|
745
|
+
DataRequestForm,
|
|
490
746
|
ShareButtons,
|
|
491
747
|
useCollection
|
|
492
748
|
};
|
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"],"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"],"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"]}
|
|
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"]}
|