@reblu/site-blocks 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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/product-carousel.tsx","../src/components/_heading.tsx","../src/components/product-card.tsx","../src/lib/format-price.ts","../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 cursor-pointer items-center rounded-md border border-border px-4 text-sm font-medium transition-colors hover:bg-muted disabled:pointer-events-none disabled:cursor-not-allowed 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 cursor-pointer 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 cursor-pointer 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 cursor-pointer 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","'use client'\n\nimport { useCallback, useEffect, useId, useRef, useState, type ReactNode } from 'react'\n\nimport { buttonVariants } from '@reblu/site-ui/button-variants'\nimport { cn } from '@reblu/site-ui/utils'\nimport type { ShopProductListItem } from '@reblu/site-contracts'\n\nimport { Heading, type HeadingLevel } from './_heading'\nimport { ProductCard } from './product-card'\n\n/**\n * `ProductCarousel` — a horizontal product strip («Productos relacionados»,\n * «También te puede interesar»…) with no carousel dependency.\n *\n * - The track is a native scroll container with CSS scroll-snap, so touch users swipe\n * it and keyboard users scroll it once it has focus (it is a labelled, focusable region).\n * - Previous/next arrows (from `sm` up) scroll by one visible page. They are not rendered\n * when everything fits, and the arrow of the end the track is at is disabled.\n * - Arrow scrolling is instant under `prefers-reduced-motion: reduce`.\n * - Cards: the package's `ProductCard` by default, or the consumer's via `renderProduct`.\n */\nexport interface ProductCarouselLabels {\n /** Accessible name of the track when there is no `heading`. */\n region: string\n /** `aria-roledescription` of the track. */\n roleDescription: string\n previous: string\n next: string\n}\n\nexport const DEFAULT_PRODUCT_CAROUSEL_LABELS: ProductCarouselLabels = {\n region: 'Productos relacionados',\n roleDescription: 'carrusel',\n previous: 'Anterior',\n next: 'Siguiente',\n}\n\ninterface ProductCarouselCommonProps {\n /** Visible heading; when set it also names the track. */\n heading?: string\n /** Default: 2. */\n headingLevel?: HeadingLevel\n /** Replaces the heading's typography (see the className rule in the README). */\n headingClassName?: string\n labels?: Partial<ProductCarouselLabels>\n className?: string\n /** Classes for each slide (`<li>`), e.g. a different card width. */\n itemClassName?: string\n}\n\nexport interface ProductCarouselDefaultCardProps extends ProductCarouselCommonProps {\n products: readonly ShopProductListItem[]\n renderProduct?: undefined\n /** Link target per product (e.g. `(p) => \\`/shop/${p.slug}\\``). Omit for non-linking cards. */\n productHref?: (product: ShopProductListItem) => string | undefined\n currencyCode?: string\n locale?: string\n}\n\nexport interface ProductCarouselCustomCardProps<T extends { id: string | number }>\n extends ProductCarouselCommonProps {\n products: readonly T[]\n /** Renders one slide's card — your own product card. */\n renderProduct: (product: T, index: number) => ReactNode\n productHref?: undefined\n currencyCode?: undefined\n locale?: undefined\n}\n\nexport type ProductCarouselProps<T extends { id: string | number } = ShopProductListItem> =\n | ProductCarouselDefaultCardProps\n | ProductCarouselCustomCardProps<T>\n\n/** Tolerance for sub-pixel scroll positions. */\nconst EDGE_EPSILON = 1\n\nfunction prefersReducedMotion(): boolean {\n return typeof window !== 'undefined' && typeof window.matchMedia === 'function'\n ? window.matchMedia('(prefers-reduced-motion: reduce)').matches\n : false\n}\n\nfunction ChevronIcon({ direction }: { direction: 'left' | 'right' }) {\n return (\n <svg\n aria-hidden='true'\n viewBox='0 0 24 24'\n fill='none'\n stroke='currentColor'\n strokeWidth={2}\n strokeLinecap='round'\n strokeLinejoin='round'\n className='rtl:-scale-x-100'\n >\n <path d={direction === 'left' ? 'm15 18-6-6 6-6' : 'm9 18 6-6-6-6'} />\n </svg>\n )\n}\n\nexport function ProductCarousel<T extends { id: string | number } = ShopProductListItem>(\n props: ProductCarouselProps<T>,\n) {\n const { products, heading, headingLevel = 2, headingClassName, labels, className, itemClassName } = props\n const t = { ...DEFAULT_PRODUCT_CAROUSEL_LABELS, ...labels }\n\n const trackId = useId()\n const headingId = useId()\n const trackRef = useRef<HTMLDivElement>(null)\n const [canPrev, setCanPrev] = useState(false)\n const [canNext, setCanNext] = useState(false)\n\n const update = useCallback(() => {\n const el = trackRef.current\n if (!el) return\n const max = el.scrollWidth - el.clientWidth\n // RTL tracks report a negative scrollLeft; distance from the start is its magnitude.\n const pos = Math.abs(el.scrollLeft)\n setCanPrev(pos > EDGE_EPSILON)\n setCanNext(pos < max - EDGE_EPSILON)\n }, [])\n\n useEffect(() => {\n const el = trackRef.current\n if (!el) return\n update()\n el.addEventListener('scroll', update, { passive: true })\n let observer: ResizeObserver | undefined\n if (typeof ResizeObserver !== 'undefined') {\n observer = new ResizeObserver(update)\n observer.observe(el)\n } else {\n window.addEventListener('resize', update)\n }\n return () => {\n el.removeEventListener('scroll', update)\n if (observer) observer.disconnect()\n else window.removeEventListener('resize', update)\n }\n }, [update, products.length])\n\n const scrollByPage = (direction: -1 | 1) => {\n const el = trackRef.current\n if (!el) return\n const rtl = typeof getComputedStyle === 'function' && getComputedStyle(el).direction === 'rtl'\n el.scrollBy({\n left: direction * el.clientWidth * (rtl ? -1 : 1),\n behavior: prefersReducedMotion() ? 'auto' : 'smooth',\n })\n }\n\n if (products.length === 0) return null\n\n const scrollable = canPrev || canNext\n\n return (\n <section data-slot='product-carousel' className={cn('flex flex-col gap-4', className)}>\n {(heading || scrollable) && (\n <div className='flex items-center justify-between gap-4'>\n {heading ? (\n <div id={headingId}>\n <Heading level={headingLevel} className={headingClassName ?? 'text-xl font-semibold'}>\n {heading}\n </Heading>\n </div>\n ) : (\n <span />\n )}\n {scrollable && (\n <div className='hidden gap-2 sm:flex' data-slot='product-carousel-controls'>\n <button\n type='button'\n className={cn(buttonVariants({ variant: 'outline', size: 'icon' }), 'rounded-full')}\n aria-label={t.previous}\n aria-controls={trackId}\n disabled={!canPrev}\n onClick={() => scrollByPage(-1)}\n >\n <ChevronIcon direction='left' />\n </button>\n <button\n type='button'\n className={cn(buttonVariants({ variant: 'outline', size: 'icon' }), 'rounded-full')}\n aria-label={t.next}\n aria-controls={trackId}\n disabled={!canNext}\n onClick={() => scrollByPage(1)}\n >\n <ChevronIcon direction='right' />\n </button>\n </div>\n )}\n </div>\n )}\n\n <div\n ref={trackRef}\n id={trackId}\n role='region'\n aria-roledescription={t.roleDescription}\n {...(heading ? { 'aria-labelledby': headingId } : { 'aria-label': t.region })}\n tabIndex={0}\n data-slot='product-carousel-track'\n className='snap-x snap-mandatory overflow-x-auto overscroll-x-contain scroll-smooth rounded-lg pb-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:scroll-auto'\n >\n <ul role='list' className='m-0 flex list-none gap-4 p-0'>\n {products.map((product, index) => (\n <li\n key={product.id}\n className={cn(\n 'shrink-0 snap-start basis-[calc((100%-1rem)/1.5)] sm:basis-[calc((100%-2rem)/3)] lg:basis-[calc((100%-3rem)/4)]',\n itemClassName,\n )}\n >\n {props.renderProduct ? (\n props.renderProduct(product as T, index)\n ) : (\n <ProductCard\n product={product as ShopProductListItem}\n href={props.productHref?.(product as ShopProductListItem)}\n currencyCode={props.currencyCode}\n locale={props.locale}\n className='h-full'\n />\n )}\n </li>\n ))}\n </ul>\n </div>\n </section>\n )\n}\n","import type { ReactNode } from 'react'\n\n/** A heading level a caller can configure — `h1`…`h6`. */\nexport type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6\n\n/** One level deeper, clamped at `h6`. */\nexport function nextHeadingLevel(level: HeadingLevel): HeadingLevel {\n return Math.min(level + 1, 6) as HeadingLevel\n}\n\n/** Internal: renders `<h{level}>`. Server-safe (no hooks). */\nexport function Heading({\n level,\n className,\n children,\n}: {\n level: HeadingLevel\n className?: string\n children: ReactNode\n}) {\n const Tag = `h${level}` as const\n return <Tag className={className}>{children}</Tag>\n}\n","import { cn } from '@reblu/site-ui/utils'\nimport type { ShopProductListItem } from '@reblu/site-contracts'\n\nimport { formatPrice } from '../lib/format-price'\n\n/**\n * `ProductCard` — presentational shop product tile over `ShopProductListItem`.\n *\n * Token-driven, RSC-safe (no hooks). Prices arrive as integer cents and are\n * formatted with {@link formatPrice}. A `comparePrice` higher than `price` renders\n * as a struck-through \"was\" amount.\n */\nexport interface ProductCardProps {\n product: ShopProductListItem\n /** Link target for the product (e.g. `/shop/{slug}`). Omit to render non-linking. */\n href?: string\n currencyCode?: string\n locale?: string\n className?: string\n}\n\nexport function ProductCard({ product, href, currencyCode = 'EUR', locale, className }: ProductCardProps) {\n const image = product.images.find((i) => i.isPrimary) ?? product.images[0]\n const hasDiscount = product.comparePrice != null && product.comparePrice > product.price\n\n return (\n <article\n className={cn(\n 'group flex flex-col overflow-hidden rounded-lg border bg-card transition-shadow hover:shadow-lg',\n className,\n )}\n data-slot='product-card'\n >\n <div className='relative aspect-square overflow-hidden bg-muted'>\n {image ? (\n <img\n src={image.url}\n alt={image.alt ?? product.name}\n loading='lazy'\n className='h-full w-full object-cover transition-transform group-hover:scale-105'\n />\n ) : (\n <div className='flex h-full items-center justify-center text-4xl text-muted-foreground/30' aria-hidden='true'>\n 🛍️\n </div>\n )}\n </div>\n\n <div className='flex flex-1 flex-col gap-2 p-4'>\n {product.category && (\n <span className='inline-flex w-fit items-center rounded-full bg-secondary px-2.5 py-0.5 text-xs font-medium text-secondary-foreground'>\n {product.category.name}\n </span>\n )}\n\n <h3 className='text-base font-semibold leading-tight'>\n {href ? (\n <a href={href} className='transition-colors hover:text-primary'>\n {product.name}\n </a>\n ) : (\n product.name\n )}\n </h3>\n\n {product.shortDescription && (\n <p className='line-clamp-2 text-sm text-muted-foreground'>{product.shortDescription}</p>\n )}\n\n <div className='mt-auto flex items-baseline gap-2 pt-2'>\n <span className='text-lg font-bold text-foreground'>\n {formatPrice(product.price, currencyCode, locale)}\n </span>\n {hasDiscount && (\n <span className='text-sm text-muted-foreground line-through'>\n {formatPrice(product.comparePrice as number, currencyCode, locale)}\n </span>\n )}\n </div>\n </div>\n </article>\n )\n}\n","/**\n * Format an integer-cents amount as a localized currency string.\n *\n * All Reblu shop monetary values travel as integer cents (Prisma `Int`, never\n * Decimal), so the SITE never sees floats. This helper is the single place that\n * converts cents → major units and applies `Intl.NumberFormat`, keeping price\n * rendering consistent across `ProductCard` and any future shop organism.\n */\nexport function formatPrice(cents: number, currency = 'EUR', locale = 'es-ES'): string {\n return new Intl.NumberFormat(locale, {\n style: 'currency',\n currency,\n }).format(cents / 100)\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='cursor-pointer rounded-lg bg-primary px-6 py-2 text-sm font-semibold text-primary-foreground disabled:cursor-not-allowed disabled:opacity-60'\n >\n {submitting ? t.submitting : t.submit}\n </button>\n </form>\n )\n}\n"],"mappings":";;;AAEA,SAAS,aAAa,SAAS,gBAAgB;AA2DxC,SAAS,cAAiB,SAA0D;AACzF,QAAM,EAAE,OAAO,WAAW,GAAG,UAAU,CAAC,GAAG,QAAQ,UAAU,QAAQ,CAAC,EAAE,IAAI;AAE5E,QAAM,CAAC,eAAe,gBAAgB,IAAI;AAAA,IACxC,MAAM,QAAQ,kBAAkB,CAAC;AAAA,EACnC;AACA,QAAM,CAAC,QAAQ,cAAc,IAAI,SAAS,MAAM,QAAQ,iBAAiB,EAAE;AAC3E,QAAM,CAAC,MAAM,YAAY,IAAI,SAA6B,MAAM,QAAQ,WAAW;AACnF,QAAM,CAAC,MAAM,YAAY,IAAI,SAAS,MAAM,QAAQ,eAAe,CAAC;AAEpE,QAAM,WAAW,QAAQ,MAAM;AAC7B,QAAI,OAAO;AAEX,eAAW,EAAE,KAAK,UAAU,KAAK,SAAS;AACxC,YAAM,QAAQ,cAAc,GAAG;AAC/B,UAAI,SAAS,QAAQ,UAAU,IAAI;AACjC,eAAO,KAAK,OAAO,CAAC,SAAS,UAAU,MAAM,KAAK,CAAC;AAAA,MACrD;AAAA,IACF;AAEA,UAAM,OAAO,OAAO,KAAK;AACzB,QAAI,YAAY,MAAM;AACpB,aAAO,KAAK,OAAO,CAAC,SAAS,SAAS,MAAM,IAAI,CAAC;AAAA,IACnD;AAEA,UAAM,aAAa,OAAO,MAAM,IAAI,IAAI;AACxC,QAAI,YAAY;AACd,aAAO,KAAK,MAAM,EAAE,KAAK,UAAU;AAAA,IACrC;AAEA,WAAO;AAAA,EACT,GAAG,CAAC,OAAO,SAAS,eAAe,QAAQ,UAAU,MAAM,KAAK,CAAC;AAEjE,QAAM,QAAQ,SAAS;AACvB,QAAM,YAAY,WAAW,IAAI,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,QAAQ,CAAC,IAAI;AAG5E,QAAM,cAAc,KAAK,IAAI,KAAK,IAAI,GAAG,IAAI,GAAG,SAAS;AAEzD,QAAM,YAAY,QAAQ,MAAM;AAC9B,QAAI,YAAY,EAAG,QAAO;AAC1B,UAAM,SAAS,cAAc,KAAK;AAClC,WAAO,SAAS,MAAM,OAAO,QAAQ,QAAQ;AAAA,EAC/C,GAAG,CAAC,UAAU,UAAU,WAAW,CAAC;AAGpC,QAAM,mBAAmB,YAAY,MAAM,aAAa,CAAC,GAAG,CAAC,CAAC;AAE9D,QAAM,YAAY;AAAA,IAChB,CAAC,KAAa,UAA8B;AAC1C,uBAAiB,CAAC,UAAU,EAAE,GAAG,MAAM,CAAC,GAAG,GAAG,MAAM,EAAE;AACtD,uBAAiB;AAAA,IACnB;AAAA,IACA,CAAC,gBAAgB;AAAA,EACnB;AAEA,QAAM,eAAe,YAAY,MAAM;AACrC,qBAAiB,CAAC,CAAC;AACnB,qBAAiB;AAAA,EACnB,GAAG,CAAC,gBAAgB,CAAC;AAErB,QAAM,YAAY;AAAA,IAChB,CAAC,SAAiB;AAChB,qBAAe,IAAI;AACnB,uBAAiB;AAAA,IACnB;AAAA,IACA,CAAC,gBAAgB;AAAA,EACnB;AAEA,QAAM,UAAU;AAAA,IACd,CAAC,QAA4B;AAC3B,mBAAa,GAAG;AAChB,uBAAiB;AAAA,IACnB;AAAA,IACA,CAAC,gBAAgB;AAAA,EACnB;AAIA,QAAM,UAAU;AAAA,IACd,CAAC,SAAiB,aAAa,KAAK,IAAI,KAAK,IAAI,GAAG,IAAI,GAAG,SAAS,CAAC;AAAA,IACrE,CAAC,SAAS;AAAA,EACZ;AACA,QAAM,WAAW;AAAA,IACf,MAAM,aAAa,KAAK,IAAI,cAAc,GAAG,SAAS,CAAC;AAAA,IACvD,CAAC,aAAa,SAAS;AAAA,EACzB;AACA,QAAM,WAAW,YAAY,MAAM,aAAa,KAAK,IAAI,GAAG,cAAc,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC;AAE5F,SAAO;AAAA,IACL,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA,SAAS,UAAU;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;;;ACtKA,SAAS,UAAU;AA2FL,SAME,KANF;AAvDd,IAAM,iBAAmC;AAAA,EACvC,UAAU;AAAA,EACV,MAAM;AAAA,EACN,MAAM,CAAC,MAAM,cAAc,QAAQ,IAAI,OAAO,SAAS;AACzD;AAmBO,SAAS,WAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS,CAAC;AAAA,EACV,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb;AAAA,EACA,gBAAgB;AAAA,EAChB;AACF,GAAuB;AACrB,QAAM,IAAI,EAAE,GAAG,gBAAgB,GAAG,OAAO;AACzC,QAAM,aAAa,cAAiB;AAAA,IAClC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,SACE,qBAAC,SAAI,WAAW,GAAG,aAAa,SAAS,GACtC;AAAA,WAAO,SAAS,KACf,oBAAC,SAAI,WAAU,aACZ,iBAAO,IAAI,CAAC,UAAU;AACrB,YAAM,SAAS,WAAW,cAAc,MAAM,GAAG;AACjD,aACE;AAAA,QAAC;AAAA;AAAA,UAEC,MAAK;AAAA,UACL,cAAY,MAAM;AAAA,UAClB,WAAU;AAAA,UAEV;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,OAAO,MAAM,YAAY;AAAA,gBACzB,SAAS,UAAU,QAAQ,WAAW;AAAA,gBACtC,SAAS,MAAM,WAAW,UAAU,MAAM,KAAK,MAAS;AAAA;AAAA,YAC1D;AAAA,YACC,MAAM,QAAQ,IAAI,CAAC,QAClB;AAAA,cAAC;AAAA;AAAA,gBAEC,OAAO,IAAI;AAAA,gBACX,OAAO,IAAI;AAAA,gBACX,SAAS,WAAW,IAAI;AAAA,gBACxB,SAAS,MAAM,WAAW,UAAU,MAAM,KAAK,IAAI,KAAK;AAAA;AAAA,cAJnD,IAAI;AAAA,YAKX,CACD;AAAA;AAAA;AAAA,QAlBI,MAAM;AAAA,MAmBb;AAAA,IAEJ,CAAC,GACH;AAAA,IAGD,WAAW,UACV,oBAAC,OAAE,WAAU,2CAA2C,sBAAW,IAEnE,oBAAC,QAAG,WAAW,GAAG,iBAAiB,aAAa,GAC7C,qBAAW,MAAM,IAAI,CAAC,SACrB,oBAAC,QAAuB,qBAAW,IAAI,KAA9B,OAAO,IAAI,CAAqB,CAC1C,GACH;AAAA,IAGD,WAAW,YAAY,KACtB,qBAAC,SAAI,cAAW,cAAa,WAAU,0CACrC;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS,WAAW;AAAA,UACpB,UAAU,WAAW,QAAQ;AAAA,UAC7B,WAAW;AAAA,UAEV,YAAE;AAAA;AAAA,MACL;AAAA,MACA,oBAAC,UAAK,gBAAa,QAAO,WAAU,iCACjC,YAAE,KAAK,WAAW,MAAM,WAAW,SAAS,GAC/C;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS,WAAW;AAAA,UACpB,UAAU,WAAW,QAAQ,WAAW;AAAA,UACxC,WAAW;AAAA,UAEV,YAAE;AAAA;AAAA,MACL;AAAA,OACF;AAAA,KAEJ;AAEJ;AAEA,IAAM,iBACJ;AASF,SAAS,UAAU,EAAE,OAAO,SAAS,OAAO,QAAQ,GAAmB;AACrE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,gBAAc;AAAA,MACd;AAAA,MAGA,OAAO,QAAS,EAAE,iBAAiB,MAAM,IAA4B;AAAA,MACrE,WAAW;AAAA,QACT;AAAA,QACA,UACI,QACE,0DACA,0DACF;AAAA,MACN;AAAA,MAEC;AAAA;AAAA,EACH;AAEJ;;;AC1LA,YAAY,WAAW;AACvB,SAAS,MAAAA,WAAU;;;ACWZ,SAAS,eAAe,KAAa,OAA0B;AACpE,QAAM,aAAa,mBAAmB,GAAG;AACzC,QAAM,eAAe,mBAAmB,KAAK;AAE7C,SAAO;AAAA;AAAA;AAAA,IAGL,UAAU,uDAAuD,UAAU;AAAA;AAAA,IAE3E,GAAG,kCAAkC,UAAU,SAAS,YAAY;AAAA,IACpE,UAAU,gDAAgD,UAAU;AAAA,IACpE,UAAU,uBAAuB,mBAAmB,GAAG,KAAK,IAAI,GAAG,EAAE,CAAC;AAAA,EACxE;AACF;;;AD2BM,gBAAAC,MA+BF,QAAAC,aA/BE;AA5BN,IAAMC,kBAAqC;AAAA,EACzC,OAAO;AAAA,EACP,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,GAAG;AAAA,EACH,UAAU;AAAA,EACV,UAAU;AAAA,EACV,YAAY;AACd;AAWA,IAAM,aACJ;AAEF,IAAM,OAAO;AAEb,SAAS,eAAe;AACtB,SACE,gBAAAF,KAAC,SAAI,WAAW,MAAM,SAAQ,aAAY,MAAK,gBAAe,eAAY,QAAO,WAAU,SACzF,0BAAAA,KAAC,UAAK,GAAE,mSAAkS,GAC5S;AAEJ;AAEA,SAAS,QAAQ;AACf,SACE,gBAAAA,KAAC,SAAI,WAAW,MAAM,SAAQ,aAAY,MAAK,gBAAe,eAAY,QAAO,WAAU,SACzF,0BAAAA,KAAC,UAAK,GAAE,+JAA8J,GACxK;AAEJ;AAEA,SAAS,eAAe;AACtB,SACE,gBAAAA,KAAC,SAAI,WAAW,MAAM,SAAQ,aAAY,MAAK,gBAAe,eAAY,QAAO,WAAU,SACzF,0BAAAA,KAAC,UAAK,GAAE,0LAAyL,GACnM;AAEJ;AAEA,SAAS,eAAe;AACtB,SACE,gBAAAA,KAAC,SAAI,WAAW,MAAM,SAAQ,aAAY,MAAK,gBAAe,eAAY,QAAO,WAAU,SACzF,0BAAAA,KAAC,UAAK,GAAE,8yBAA6yB,GACvzB;AAEJ;AAEA,SAAS,WAAW;AAClB,SACE,gBAAAC,MAAC,SAAI,WAAW,MAAM,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,KAAI,eAAc,SAAQ,gBAAe,SAAQ,eAAY,QAAO,WAAU,SACpK;AAAA,oBAAAD,KAAC,UAAK,GAAE,KAAI,GAAE,KAAI,OAAM,MAAK,QAAO,MAAK,IAAG,KAAI,IAAG,KAAI;AAAA,IACvD,gBAAAA,KAAC,UAAK,GAAE,2DAA0D;AAAA,KACpE;AAEJ;AAEA,SAAS,YAAY;AACnB,SACE,gBAAAA,KAAC,SAAI,WAAW,MAAM,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,KAAI,eAAc,SAAQ,gBAAe,SAAQ,eAAY,QAAO,WAAU,SACpK,0BAAAA,KAAC,cAAS,QAAO,kBAAiB,GACpC;AAEJ;AAEO,SAAS,aAAa,EAAE,KAAK,OAAO,WAAW,OAAO,GAAsB;AACjF,QAAM,IAAI,EAAE,GAAGE,iBAAgB,GAAG,OAAO;AACzC,QAAM,CAAC,QAAQ,SAAS,IAAU,eAAS,KAAK;AAChD,QAAM,QAAQ,eAAe,KAAK,KAAK;AAEvC,iBAAe,aAAa;AAC1B,QAAI;AACF,YAAM,UAAU,UAAU,UAAU,GAAG;AACvC,gBAAU,IAAI;AACd,aAAO,WAAW,MAAM,UAAU,KAAK,GAAG,GAAI;AAAA,IAChD,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SACE,gBAAAD,MAAC,SAAI,WAAWE,IAAG,2BAA2B,SAAS,GAAG,MAAK,SAAQ,cAAY,EAAE,OACnF;AAAA,oBAAAH,KAAC,UAAK,WAAU,kDAAkD,YAAE,OAAM;AAAA,IAE1E,gBAAAA,KAAC,OAAE,MAAM,MAAM,UAAU,QAAO,UAAS,KAAI,uBAAsB,WAAW,YAAY,cAAY,EAAE,UACtG,0BAAAA,KAAC,gBAAa,GAChB;AAAA,IACA,gBAAAA,KAAC,OAAE,MAAM,MAAM,GAAG,QAAO,UAAS,KAAI,uBAAsB,WAAW,YAAY,cAAY,EAAE,GAC/F,0BAAAA,KAAC,SAAM,GACT;AAAA,IACA,gBAAAA,KAAC,OAAE,MAAM,MAAM,UAAU,QAAO,UAAS,KAAI,uBAAsB,WAAW,YAAY,cAAY,EAAE,UACtG,0BAAAA,KAAC,gBAAa,GAChB;AAAA,IACA,gBAAAA,KAAC,OAAE,MAAM,MAAM,UAAU,QAAO,UAAS,KAAI,uBAAsB,WAAW,YAAY,cAAY,EAAE,UACtG,0BAAAA,KAAC,gBAAa,GAChB;AAAA,IAEA,gBAAAC;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS;AAAA,QACT,WAAWE,IAAG,YAAY,oCAAoC,UAAU,6BAA6B;AAAA,QACrG,cAAY,EAAE;AAAA,QAEb;AAAA,mBAAS,gBAAAH,KAAC,aAAU,IAAK,gBAAAA,KAAC,YAAS;AAAA,UACpC,gBAAAA,KAAC,UAAK,WAAU,WAAW,mBAAS,EAAE,SAAS,EAAE,MAAK;AAAA;AAAA;AAAA,IACxD;AAAA,IAGA,gBAAAA,KAAC,UAAK,MAAK,UAAS,aAAU,UAAS,WAAU,WAC9C,mBAAS,EAAE,aAAa,IAC3B;AAAA,KACF;AAEJ;;;AElJA,SAAS,WAAW,QAAQ,YAAAI,iBAAgB;AAE5C,SAAS,MAAAC,WAAU;AAoNf,gBAAAC,YAAA;AA3LJ,IAAMC,kBAAwC;AAAA,EAC5C,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,OAAO;AACT;AAkDO,SAAS,gBAAiC;AAAA,EAC/C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,oBAAoB;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAgC;AAC9B,QAAM,IAAI,EAAE,GAAGA,iBAAgB,GAAG,OAAO;AACzC,QAAM,CAAC,QAAQ,SAAS,IAAIH,UAAgC,MAAM;AAClE,QAAM,aAAa,OAA6C,IAAI;AACpE,QAAM,eAAe,OAAqC,KAAK;AAO/D,QAAM,qBAAqB,OAAO,KAAK;AACvC,QAAM,eAAe,OAAO,KAAK;AACjC,QAAM,aAAa,OAAO,IAAI;AAK9B,QAAM,CAAC,WAAW,YAAY,IAAIA,UAAS,KAAK;AAEhD,QAAM,aAAa,MAAM;AACvB,QAAI,WAAW,SAAS;AACtB,mBAAa,WAAW,OAAO;AAC/B,iBAAW,UAAU;AAAA,IACvB;AAAA,EACF;AAEA,YAAU,MAAM;AACd,eAAW,UAAU;AACrB,WAAO,MAAM;AACX,iBAAW,UAAU;AACrB,iBAAW;AAAA,IACb;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,QAAM,iBAAiB,MAAM;AAC3B,uBAAmB,UAAU;AAC7B,eAAW;AACX,cAAU,SAAS;AACnB,cAAU,IAAI;AACd,eAAW,UAAU,WAAW,MAAM;AACpC,yBAAmB,UAAU;AAC7B,gBAAU,MAAM;AAAA,IAClB,GAAG,iBAAiB;AAAA,EACtB;AAEA,QAAM,eAAe,CAAC,aAA+B,SAAS;AAC5D,eAAW;AACX,uBAAmB,UAAU;AAC7B,cAAU,OAAO;AACjB,cAAU,MAAM,UAAU;AAC1B,eAAW,UAAU,WAAW,MAAM,UAAU,MAAM,GAAG,iBAAiB;AAAA,EAC5E;AAEA,YAAU,MAAM;AACd,QAAI,SAAS,CAAC,aAAa,SAAS;AAClC,mBAAa,KAAK;AAAA,IACpB;AACA,iBAAa,UAAU;AAAA,EAEzB,GAAG,CAAC,KAAK,CAAC;AAWV,QAAM,eAAe,CAAC,WAAoC;AACxD,QAAI,CAAC,WAAW,WAAW,WAAY;AACvC,QAAI,QAAQ,WAAW,WAAW;AAChC,mBAAa,IAAI;AACjB;AAAA,IACF;AACA,QAAI,MAAO;AACX,mBAAe;AAAA,EACjB;AAEA,QAAM,cAAc,MAAM;AACxB,QAAI,mBAAmB,WAAW,aAAa,QAAS;AACxD,UAAM,SAAS,cAAc,IAAI;AAEjC,QAAI,UAAU,OAAQ,OAA4C,SAAS,YAAY;AACrF,mBAAa,UAAU;AACvB,mBAAa,IAAI;AAChB,MAAC,OACC,KAAK,CAAC,aAAa;AAClB,qBAAa,UAAU;AACvB,YAAI,WAAW,QAAS,cAAa,KAAK;AAC1C,qBAAa,QAAQ;AAAA,MACvB,CAAC,EACA,MAAM,MAAM;AACX,qBAAa,UAAU;AACvB,YAAI,WAAW,QAAS,cAAa,KAAK;AAC1C,qBAAa,EAAE,QAAQ,UAAU,CAAC;AAAA,MACpC,CAAC;AACH;AAAA,IACF;AAEA,iBAAa,MAAiC;AAAA,EAChD;AAEA,QAAM,aAAa,QAAQ,YAAY,cAAc,WAAW,WAAW,aAAa,SAAS;AACjG,QAAM,QAAQ,aACV,EAAE,aACF,WAAW,YACT,EAAE,QACF,WAAW,UACT,OAAO,UAAU,WACf,QACA,EAAE,QACJ,WAAW,YACT,EAAE,SACF,EAAE;AAEZ,SACE,gBAAAE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAS;AAAA,MACT,UAAU;AAAA,MACV,iBAAe;AAAA,MACf,aAAU;AAAA,MACV,aAAU;AAAA,MACV,eAAa;AAAA,MACb,WAAWD;AAAA,QACT;AAAA,QACA,aACI,sDACA;AAAA,QACJ,WAAW,WAAW;AAAA,QACtB;AAAA,MACF;AAAA,MAEC;AAAA;AAAA,EACH;AAEJ;;;AC1OA,SAAS,aAAAG,YAAW,UAAAC,SAAQ,YAAAC,iBAAgB;AAE5C,SAAS,MAAAC,WAAU;AAuGb,SAUE,OAAAC,MAVF,QAAAC,aAAA;AA/EN,IAAMC,kBAAiC;AAAA,EACrC,MAAM;AAAA,EACN,aAAa,CAAC,UAAU,GAAG,KAAK,eAAY,UAAU,IAAI,KAAK,GAAG;AACpE;AAGA,IAAM,mBAAmB;AAwBlB,SAAS,SAAS,EAAE,WAAW,MAAM,UAAU,WAAW,QAAQ,UAAU,GAAkB;AACnG,QAAM,IAAI,EAAE,GAAGA,iBAAgB,GAAG,OAAO;AACzC,QAAM,YAAY,YAAY,IAAI,GAAG,EAAE,IAAI,KAAK,EAAE,YAAY,SAAS,CAAC,MAAM,EAAE;AAChF,QAAM,eAAe,YAAY,KAAK,QAAQ,OAAO,SAAS;AAE9D,QAAM,CAAC,SAAS,UAAU,IAAIJ,UAAS,KAAK;AAC5C,QAAM,eAAeD,QAAO,SAAS;AACrC,QAAM,kBAAkBA,QAAO,QAAQ;AAEvC,EAAAD,WAAU,MAAM;AAMd,UAAM,0BAA0B,aAAa,UAAa,gBAAgB,YAAY;AACtF,UAAM,aACJ,QAAQ,SAAS,KAAK,YAAY,aAAa,WAAW;AAE5D,iBAAa,UAAU;AACvB,oBAAgB,UAAU;AAE1B,QAAI,CAAC,YAAY;AAMf,iBAAW,KAAK;AAChB;AAAA,IACF;AAEA,eAAW,IAAI;AACf,UAAM,KAAK,WAAW,MAAM,WAAW,KAAK,GAAG,gBAAgB;AAC/D,WAAO,MAAM,aAAa,EAAE;AAAA,EAE9B,GAAG,CAAC,WAAW,UAAU,SAAS,CAAC;AAEnC,SACE,gBAAAK;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,cAAY;AAAA,MACZ,aAAU;AAAA,MACV,gBAAc,WAAW;AAAA,MACzB,WAAWF;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MAEA;AAAA,wBAAAE;AAAA,UAAC;AAAA;AAAA,YACC,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,QAAO;AAAA,YACP,aAAa;AAAA,YACb,eAAc;AAAA,YACd,gBAAe;AAAA,YACf,WAAWF,IAAG,WAAW,WAAW,iFAAiF;AAAA,YACrH,eAAY;AAAA,YAEZ;AAAA,8BAAAC,KAAC,YAAO,IAAG,KAAI,IAAG,MAAK,GAAE,KAAI;AAAA,cAC7B,gBAAAA,KAAC,YAAO,IAAG,MAAK,IAAG,MAAK,GAAE,KAAI;AAAA,cAC9B,gBAAAA,KAAC,UAAK,GAAE,mEAAkE;AAAA;AAAA;AAAA,QAC5E;AAAA,QACC,YAAY,KACX,gBAAAA;AAAA,UAAC;AAAA;AAAA,YACC,eAAY;AAAA,YACZ,WAAWD;AAAA,cACT;AAAA,cACA,WAAW;AAAA,YACb;AAAA,YAEC;AAAA;AAAA,QACH;AAAA;AAAA;AAAA,EAEJ;AAEJ;;;ACpIA,SAAS,eAAAI,cAAa,aAAAC,YAAW,OAAO,UAAAC,SAAQ,YAAAC,iBAAgC;AAEhF,SAAS,sBAAsB;AAC/B,SAAS,MAAAC,WAAU;;;ACgBV,gBAAAC,YAAA;AAVF,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,MAAM,IAAI,KAAK;AACrB,SAAO,gBAAAC,KAAC,OAAI,WAAuB,UAAS;AAC9C;;;ACtBA,SAAS,MAAAC,WAAU;;;ACQZ,SAAS,YAAY,OAAe,WAAW,OAAO,SAAS,SAAiB;AACrF,SAAO,IAAI,KAAK,aAAa,QAAQ;AAAA,IACnC,OAAO;AAAA,IACP;AAAA,EACF,CAAC,EAAE,OAAO,QAAQ,GAAG;AACvB;;;ADsBU,gBAAAC,MAkCF,QAAAC,aAlCE;AAdH,SAAS,YAAY,EAAE,SAAS,MAAM,eAAe,OAAO,QAAQ,UAAU,GAAqB;AACxG,QAAM,QAAQ,QAAQ,OAAO,KAAK,CAAC,MAAM,EAAE,SAAS,KAAK,QAAQ,OAAO,CAAC;AACzE,QAAM,cAAc,QAAQ,gBAAgB,QAAQ,QAAQ,eAAe,QAAQ;AAEnF,SACE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,WAAWC;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACA,aAAU;AAAA,MAEV;AAAA,wBAAAF,KAAC,SAAI,WAAU,mDACZ,kBACC,gBAAAA;AAAA,UAAC;AAAA;AAAA,YACC,KAAK,MAAM;AAAA,YACX,KAAK,MAAM,OAAO,QAAQ;AAAA,YAC1B,SAAQ;AAAA,YACR,WAAU;AAAA;AAAA,QACZ,IAEA,gBAAAA,KAAC,SAAI,WAAU,6EAA4E,eAAY,QAAO,6BAE9G,GAEJ;AAAA,QAEA,gBAAAC,MAAC,SAAI,WAAU,kCACZ;AAAA,kBAAQ,YACP,gBAAAD,KAAC,UAAK,WAAU,wHACb,kBAAQ,SAAS,MACpB;AAAA,UAGF,gBAAAA,KAAC,QAAG,WAAU,yCACX,iBACC,gBAAAA,KAAC,OAAE,MAAY,WAAU,wCACtB,kBAAQ,MACX,IAEA,QAAQ,MAEZ;AAAA,UAEC,QAAQ,oBACP,gBAAAA,KAAC,OAAE,WAAU,8CAA8C,kBAAQ,kBAAiB;AAAA,UAGtF,gBAAAC,MAAC,SAAI,WAAU,0CACb;AAAA,4BAAAD,KAAC,UAAK,WAAU,qCACb,sBAAY,QAAQ,OAAO,cAAc,MAAM,GAClD;AAAA,YACC,eACC,gBAAAA,KAAC,UAAK,WAAU,8CACb,sBAAY,QAAQ,cAAwB,cAAc,MAAM,GACnE;AAAA,aAEJ;AAAA,WACF;AAAA;AAAA;AAAA,EACF;AAEJ;;;AFaM,gBAAAG,MA0EM,QAAAC,aA1EN;AAhEC,IAAM,kCAAyD;AAAA,EACpE,QAAQ;AAAA,EACR,iBAAiB;AAAA,EACjB,UAAU;AAAA,EACV,MAAM;AACR;AAuCA,IAAM,eAAe;AAErB,SAAS,uBAAgC;AACvC,SAAO,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,aACjE,OAAO,WAAW,kCAAkC,EAAE,UACtD;AACN;AAEA,SAAS,YAAY,EAAE,UAAU,GAAoC;AACnE,SACE,gBAAAD;AAAA,IAAC;AAAA;AAAA,MACC,eAAY;AAAA,MACZ,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,QAAO;AAAA,MACP,aAAa;AAAA,MACb,eAAc;AAAA,MACd,gBAAe;AAAA,MACf,WAAU;AAAA,MAEV,0BAAAA,KAAC,UAAK,GAAG,cAAc,SAAS,mBAAmB,iBAAiB;AAAA;AAAA,EACtE;AAEJ;AAEO,SAAS,gBACd,OACA;AACA,QAAM,EAAE,UAAU,SAAS,eAAe,GAAG,kBAAkB,QAAQ,WAAW,cAAc,IAAI;AACpG,QAAM,IAAI,EAAE,GAAG,iCAAiC,GAAG,OAAO;AAE1D,QAAM,UAAU,MAAM;AACtB,QAAM,YAAY,MAAM;AACxB,QAAM,WAAWE,QAAuB,IAAI;AAC5C,QAAM,CAAC,SAAS,UAAU,IAAIC,UAAS,KAAK;AAC5C,QAAM,CAAC,SAAS,UAAU,IAAIA,UAAS,KAAK;AAE5C,QAAM,SAASC,aAAY,MAAM;AAC/B,UAAM,KAAK,SAAS;AACpB,QAAI,CAAC,GAAI;AACT,UAAM,MAAM,GAAG,cAAc,GAAG;AAEhC,UAAM,MAAM,KAAK,IAAI,GAAG,UAAU;AAClC,eAAW,MAAM,YAAY;AAC7B,eAAW,MAAM,MAAM,YAAY;AAAA,EACrC,GAAG,CAAC,CAAC;AAEL,EAAAC,WAAU,MAAM;AACd,UAAM,KAAK,SAAS;AACpB,QAAI,CAAC,GAAI;AACT,WAAO;AACP,OAAG,iBAAiB,UAAU,QAAQ,EAAE,SAAS,KAAK,CAAC;AACvD,QAAI;AACJ,QAAI,OAAO,mBAAmB,aAAa;AACzC,iBAAW,IAAI,eAAe,MAAM;AACpC,eAAS,QAAQ,EAAE;AAAA,IACrB,OAAO;AACL,aAAO,iBAAiB,UAAU,MAAM;AAAA,IAC1C;AACA,WAAO,MAAM;AACX,SAAG,oBAAoB,UAAU,MAAM;AACvC,UAAI,SAAU,UAAS,WAAW;AAAA,UAC7B,QAAO,oBAAoB,UAAU,MAAM;AAAA,IAClD;AAAA,EACF,GAAG,CAAC,QAAQ,SAAS,MAAM,CAAC;AAE5B,QAAM,eAAe,CAAC,cAAsB;AAC1C,UAAM,KAAK,SAAS;AACpB,QAAI,CAAC,GAAI;AACT,UAAM,MAAM,OAAO,qBAAqB,cAAc,iBAAiB,EAAE,EAAE,cAAc;AACzF,OAAG,SAAS;AAAA,MACV,MAAM,YAAY,GAAG,eAAe,MAAM,KAAK;AAAA,MAC/C,UAAU,qBAAqB,IAAI,SAAS;AAAA,IAC9C,CAAC;AAAA,EACH;AAEA,MAAI,SAAS,WAAW,EAAG,QAAO;AAElC,QAAM,aAAa,WAAW;AAE9B,SACE,gBAAAJ,MAAC,aAAQ,aAAU,oBAAmB,WAAWK,IAAG,uBAAuB,SAAS,GAChF;AAAA,gBAAW,eACX,gBAAAL,MAAC,SAAI,WAAU,2CACZ;AAAA,gBACC,gBAAAD,KAAC,SAAI,IAAI,WACP,0BAAAA,KAAC,WAAQ,OAAO,cAAc,WAAW,oBAAoB,yBAC1D,mBACH,GACF,IAEA,gBAAAA,KAAC,UAAK;AAAA,MAEP,cACC,gBAAAC,MAAC,SAAI,WAAU,wBAAuB,aAAU,6BAC9C;AAAA,wBAAAD;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,WAAWM,IAAG,eAAe,EAAE,SAAS,WAAW,MAAM,OAAO,CAAC,GAAG,cAAc;AAAA,YAClF,cAAY,EAAE;AAAA,YACd,iBAAe;AAAA,YACf,UAAU,CAAC;AAAA,YACX,SAAS,MAAM,aAAa,EAAE;AAAA,YAE9B,0BAAAN,KAAC,eAAY,WAAU,QAAO;AAAA;AAAA,QAChC;AAAA,QACA,gBAAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,WAAWM,IAAG,eAAe,EAAE,SAAS,WAAW,MAAM,OAAO,CAAC,GAAG,cAAc;AAAA,YAClF,cAAY,EAAE;AAAA,YACd,iBAAe;AAAA,YACf,UAAU,CAAC;AAAA,YACX,SAAS,MAAM,aAAa,CAAC;AAAA,YAE7B,0BAAAN,KAAC,eAAY,WAAU,SAAQ;AAAA;AAAA,QACjC;AAAA,SACF;AAAA,OAEJ;AAAA,IAGF,gBAAAA;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,IAAI;AAAA,QACJ,MAAK;AAAA,QACL,wBAAsB,EAAE;AAAA,QACvB,GAAI,UAAU,EAAE,mBAAmB,UAAU,IAAI,EAAE,cAAc,EAAE,OAAO;AAAA,QAC3E,UAAU;AAAA,QACV,aAAU;AAAA,QACV,WAAU;AAAA,QAEV,0BAAAA,KAAC,QAAG,MAAK,QAAO,WAAU,gCACvB,mBAAS,IAAI,CAAC,SAAS,UACtB,gBAAAA;AAAA,UAAC;AAAA;AAAA,YAEC,WAAWM;AAAA,cACT;AAAA,cACA;AAAA,YACF;AAAA,YAEC,gBAAM,gBACL,MAAM,cAAc,SAAc,KAAK,IAEvC,gBAAAN;AAAA,cAAC;AAAA;AAAA,gBACC;AAAA,gBACA,MAAM,MAAM,cAAc,OAA8B;AAAA,gBACxD,cAAc,MAAM;AAAA,gBACpB,QAAQ,MAAM;AAAA,gBACd,WAAU;AAAA;AAAA,YACZ;AAAA;AAAA,UAfG,QAAQ;AAAA,QAiBf,CACD,GACH;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;;;AIvOA,SAAS,aAAAO,YAAW,SAAAC,QAAO,UAAAC,SAAQ,YAAAC,iBAAgD;AACnF,SAAS,MAAAC,WAAU;AACnB;AAAA,EACE;AAAA,OAIK;AAqFH,mBAsJE,OAAAC,MAtJF,QAAAC,aAAA;AAlDG,IAAM,4BAA4B,OAAO,OAAO,EAAE,MAAM,KAAK,OAAO,KAAK,SAAS,IAAK,CAAC;AAyBxF,IAAM,mCAA0D;AAAA,EACrE,kBAAkB;AAAA,EAClB,wBAAwB;AAAA,EACxB,cAAc;AAAA,IACZ,QAAQ;AAAA,IACR,eAAe;AAAA,IACf,SAAS;AAAA,IACT,WAAW;AAAA,IACX,aAAa;AAAA,IACb,aAAa;AAAA,IACb,oBAAoB;AAAA,IACpB,aAAa;AAAA,EACf;AAAA,EACA,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,oBAAoB;AAAA,EACpB,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,qBAAqB;AAAA,EACrB,cAAc;AAAA,EACd,cAAc;AAAA,EACd,iBAAiB;AAAA,EACjB,SAAS,CAAC,aACR,gBAAAC,MAAA,YAAE;AAAA;AAAA,IAC8D;AAAA,IAAS;AAAA,KACzE;AAAA,EAEF,cACE;AAAA,EACF,iBAAiB;AAAA,EACjB,gBAAgB;AAAA,EAChB,cAAc;AAChB;AAyBA,IAAM,cAAoC,CAAC,eAAe,QAAQ,SAAS,SAAS;AAEpF,IAAM,gBAAgB;AAEtB,eAAe,YAAY,UAAkB,OAA2D;AACtG,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,UAAU;AAAA,MAChC,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU,KAAK;AAAA,IAC5B,CAAC;AACD,QAAI,CAAC,IAAI,GAAI,QAAO,EAAE,IAAI,OAAO,QAAQ,IAAI,OAAO;AACpD,UAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,WAAO,KAAK,OAAO,EAAE,IAAI,MAAM,MAAM,KAAK,KAAK,IAAI,EAAE,IAAI,OAAO,QAAQ,IAAI,OAAO;AAAA,EACrF,QAAQ;AACN,WAAO,EAAE,IAAI,OAAO,QAAQ,EAAE;AAAA,EAChC;AACF;AAEA,SAAS,iBAAiB,QAAgB,GAAkC;AAC1E,MAAI,WAAW,IAAK,QAAO,EAAE;AAC7B,MAAI,WAAW,IAAK,QAAO,EAAE;AAC7B,MAAI,WAAW,IAAK,QAAO,EAAE;AAC7B,SAAO,EAAE;AACX;AAEA,IAAM,cACJ;AAEK,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA,eAAe;AAAA,EACf;AAAA,EACA,SAAS;AAAA,EACT;AACF,GAAyB;AACvB,QAAM,IAA2B;AAAA,IAC/B,GAAG;AAAA,IACH,GAAG;AAAA,IACH,cAAc,EAAE,GAAG,iCAAiC,cAAc,GAAG,QAAQ,aAAa;AAAA,EAC5F;AACA,QAAM,KAAKC,OAAM;AACjB,QAAM,CAAC,aAAa,cAAc,IAAIC,UAA+B,EAAE;AACvE,QAAM,CAAC,MAAM,OAAO,IAAIA,UAAS,EAAE;AACnC,QAAM,CAAC,OAAO,QAAQ,IAAIA,UAAS,EAAE;AACrC,QAAM,CAAC,SAAS,UAAU,IAAIA,UAAS,EAAE;AACzC,QAAM,CAAC,SAAS,UAAU,IAAIA,UAAS,KAAK;AAC5C,QAAM,CAAC,QAAQ,SAAS,IAAIA,UAAsB,CAAC,CAAC;AACpD,QAAM,CAAC,WAAW,YAAY,IAAIA,UAAwB,IAAI;AAC9D,QAAM,CAAC,YAAY,aAAa,IAAIA,UAAS,KAAK;AAClD,QAAM,CAAC,KAAK,MAAM,IAAIA,UAAuC,IAAI;AACjE,QAAM,YAAYC,QAAuB,IAAI;AAI7C,EAAAC,WAAU,MAAM;AACd,QAAI,IAAK,WAAU,SAAS,MAAM;AAAA,EACpC,GAAG,CAAC,GAAG,CAAC;AAER,iBAAe,aAAa,OAAmC;AAC7D,UAAM,eAAe;AACrB,QAAI,WAAY;AAChB,iBAAa,IAAI;AAEjB,UAAM,OAAoB,CAAC;AAC3B,QAAI,CAAC,YAAa,MAAK,cAAc,EAAE;AACvC,QAAI,CAAC,KAAK,KAAK,EAAG,MAAK,OAAO,EAAE;AAChC,QAAI,CAAC,cAAc,KAAK,MAAM,KAAK,CAAC,EAAG,MAAK,QAAQ,EAAE;AACtD,QAAI,CAAC,QAAS,MAAK,UAAU,EAAE;AAC/B,cAAU,IAAI;AACd,UAAM,eAAe,YAAY,KAAK,CAAC,UAAU,KAAK,KAAK,CAAC;AAC5D,QAAI,cAAc;AAEhB,eAAS,eAAe,GAAG,EAAE,IAAI,iBAAiB,gBAAgB,SAAS,YAAY,EAAE,GAAG,MAAM;AAClG;AAAA,IACF;AACA,QAAI,CAAC,YAAa;AAElB,kBAAc,IAAI;AAClB,QAAI;AACF,YAAM,UAAU,MAAM,gBAAgB,EAAE,MAAM,MAAM,MAAS;AAC7D,UAAI,CAAC,SAAS;AACZ,qBAAa,EAAE,YAAY;AAC3B;AAAA,MACF;AACA,YAAM,QAA0B;AAAA,QAC9B;AAAA,QACA,MAAM,KAAK,KAAK;AAAA,QAChB,OAAO,MAAM,KAAK;AAAA,QAClB,GAAI,QAAQ,KAAK,IAAI,EAAE,SAAS,QAAQ,KAAK,EAAE,IAAI,CAAC;AAAA,QACpD;AAAA,MACF;AACA,YAAM,SAAS,OAAO,WAAW,CAAC,SAAS,YAAY,UAAU,IAAI,IAAI,KAAK,EAAE;AAAA,QAC9E,OAAgC,EAAE,IAAI,OAAO,QAAQ,EAAE;AAAA,MACzD;AACA,UAAI,CAAC,OAAO,IAAI;AACd,qBAAa,iBAAiB,OAAO,QAAQ,CAAC,CAAC;AAC/C;AAAA,MACF;AACA,aAAO,OAAO,IAAI;AAClB,oBAAc,OAAO,IAAI;AAAA,IAC3B,UAAE;AACA,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF;AAEA,MAAI,KAAK;AACP,UAAM,WAAW,IAAI,KAAK,eAAe,QAAQ;AAAA,MAC/C,WAAW;AAAA,MACX,WAAW;AAAA,IACb,CAAC,EAAE,OAAO,IAAI,KAAK,IAAI,QAAQ,CAAC;AAChC,WACE,gBAAAC;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,UAAU;AAAA,QACV,MAAK;AAAA,QACL,aAAU;AAAA,QACV,WAAWC,IAAG,+CAA+C,SAAS;AAAA,QACrE,YAAE,QAAQ,QAAQ;AAAA;AAAA,IACrB;AAAA,EAEJ;AAEA,QAAM,UAAU,CAAC,UAA8B,OAAO,KAAK,IAAI,GAAG,EAAE,IAAI,KAAK,WAAW;AACxF,QAAM,aAAa,CAAC,UAClB,OAAO,KAAK,IACV,gBAAAD,KAAC,OAAE,IAAI,GAAG,EAAE,IAAI,KAAK,UAAU,WAAU,4BACtC,iBAAO,KAAK,GACf,IACE;AAEN,SACE,gBAAAL;AAAA,IAAC;AAAA;AAAA,MACC,YAAU;AAAA,MACV,UAAU;AAAA,MACV,gBAAgB;AAAA,MAChB,eAAe;AAAA,MACf,aAAU;AAAA,MACV,WAAWM,IAAG,aAAa,SAAS;AAAA,MAEpC;AAAA,wBAAAN,MAAC,SAAI,WAAU,aACb;AAAA,0BAAAK,KAAC,WAAM,SAAS,GAAG,EAAE,SAAS,WAAU,uBACrC,YAAE,kBACL;AAAA,UACA,gBAAAL;AAAA,YAAC;AAAA;AAAA,cACC,IAAI,GAAG,EAAE;AAAA,cACT,OAAO;AAAA,cACP,UAAU,CAAC,MAAM,eAAe,EAAE,OAAO,KAA6B;AAAA,cACtE,gBAAc,OAAO,cAAc,OAAO;AAAA,cAC1C,oBAAkB,QAAQ,aAAa;AAAA,cACvC,WAAWM,IAAG,aAAa,OAAO,cAAc,uBAAuB,eAAe;AAAA,cAEtF;AAAA,gCAAAD,KAAC,YAAO,OAAM,IAAI,YAAE,wBAAuB;AAAA,gBAC1C,aAAa,IAAI,CAAC,SACjB,gBAAAA,KAAC,YAAkB,OAAO,MACvB,YAAE,aAAa,IAAI,KADT,IAEb,CACD;AAAA;AAAA;AAAA,UACH;AAAA,UACC,WAAW,aAAa;AAAA,WAC3B;AAAA,QAEA,gBAAAL,MAAC,SAAI,WAAU,aACb;AAAA,0BAAAK,KAAC,WAAM,SAAS,GAAG,EAAE,SAAS,WAAU,uBACrC,YAAE,WACL;AAAA,UACA,gBAAAA;AAAA,YAAC;AAAA;AAAA,cACC,IAAI,GAAG,EAAE;AAAA,cACT,MAAK;AAAA,cACL,cAAa;AAAA,cACb,WAAW,0BAA0B;AAAA,cACrC,OAAO;AAAA,cACP,UAAU,CAAC,MAAM,QAAQ,EAAE,OAAO,KAAK;AAAA,cACvC,gBAAc,OAAO,OAAO,OAAO;AAAA,cACnC,oBAAkB,QAAQ,MAAM;AAAA,cAChC,WAAWC,IAAG,aAAa,OAAO,OAAO,uBAAuB,eAAe;AAAA;AAAA,UACjF;AAAA,UACC,WAAW,MAAM;AAAA,WACpB;AAAA,QAEA,gBAAAN,MAAC,SAAI,WAAU,aACb;AAAA,0BAAAK,KAAC,WAAM,SAAS,GAAG,EAAE,UAAU,WAAU,uBACtC,YAAE,YACL;AAAA,UACA,gBAAAA;AAAA,YAAC;AAAA;AAAA,cACC,IAAI,GAAG,EAAE;AAAA,cACT,MAAK;AAAA,cACL,cAAa;AAAA,cACb,WAAW,0BAA0B;AAAA,cACrC,OAAO;AAAA,cACP,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK;AAAA,cACxC,gBAAc,OAAO,QAAQ,OAAO;AAAA,cACpC,oBAAkB,QAAQ,OAAO;AAAA,cACjC,WAAWC,IAAG,aAAa,OAAO,QAAQ,uBAAuB,eAAe;AAAA;AAAA,UAClF;AAAA,UACC,WAAW,OAAO;AAAA,WACrB;AAAA,QAEA,gBAAAN,MAAC,SAAI,WAAU,aACb;AAAA,0BAAAK,KAAC,WAAM,SAAS,GAAG,EAAE,YAAY,WAAU,uBACxC,YAAE,cACL;AAAA,UACA,gBAAAA;AAAA,YAAC;AAAA;AAAA,cACC,IAAI,GAAG,EAAE;AAAA,cACT,MAAM;AAAA,cACN,WAAW,0BAA0B;AAAA,cACrC,OAAO;AAAA,cACP,UAAU,CAAC,MAAM,WAAW,EAAE,OAAO,KAAK;AAAA,cAC1C,aAAa,EAAE;AAAA,cACf,WAAWC,IAAG,aAAa,eAAe;AAAA;AAAA,UAC5C;AAAA,WACF;AAAA,QAEA,gBAAAN,MAAC,SAAI,WAAU,aACb;AAAA,0BAAAA,MAAC,SAAI,WAAU,0BACb;AAAA,4BAAAK;AAAA,cAAC;AAAA;AAAA,gBACC,IAAI,GAAG,EAAE;AAAA,gBACT,MAAK;AAAA,gBACL,SAAS;AAAA,gBACT,UAAU,CAAC,MAAM,WAAW,EAAE,OAAO,OAAO;AAAA,gBAC5C,gBAAc,OAAO,UAAU,OAAO;AAAA,gBACtC,oBAAkB,QAAQ,SAAS;AAAA,gBACnC,WAAU;AAAA;AAAA,YACZ;AAAA,YACA,gBAAAA,KAAC,WAAM,SAAS,GAAG,EAAE,YAAY,WAAU,8BACxC,YAAE,SACL;AAAA,aACF;AAAA,UACC,WAAW,SAAS;AAAA,WACvB;AAAA,QAEC,aACC,gBAAAA,KAAC,OAAE,MAAK,SAAQ,WAAU,4BACvB,qBACH;AAAA,QAGF,gBAAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,UAAU;AAAA,YACV,WAAU;AAAA,YAET,uBAAa,EAAE,aAAa,EAAE;AAAA;AAAA,QACjC;AAAA;AAAA;AAAA,EACF;AAEJ;","names":["cn","jsx","jsxs","DEFAULT_LABELS","cn","useState","cn","jsx","DEFAULT_LABELS","useEffect","useRef","useState","cn","jsx","jsxs","DEFAULT_LABELS","useCallback","useEffect","useRef","useState","cn","jsx","jsx","cn","jsx","jsxs","cn","jsx","jsxs","useRef","useState","useCallback","useEffect","cn","useEffect","useId","useRef","useState","cn","jsx","jsxs","jsxs","useId","useState","useRef","useEffect","jsx","cn"]}
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/product-carousel.tsx","../src/components/_heading.tsx","../src/components/product-card.tsx","../src/lib/format-price.ts","../src/components/course-syllabus.tsx","../src/lib/slot-class-name.ts","../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 cursor-pointer items-center rounded-md border border-border px-4 text-sm font-medium transition-colors hover:bg-muted disabled:pointer-events-none disabled:cursor-not-allowed 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 cursor-pointer 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 cursor-pointer 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 cursor-pointer 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","'use client'\n\nimport { useCallback, useEffect, useId, useRef, useState, type ReactNode } from 'react'\n\nimport { buttonVariants } from '@reblu/site-ui/button-variants'\nimport { cn } from '@reblu/site-ui/utils'\nimport type { ShopProductListItem } from '@reblu/site-contracts'\n\nimport { Heading, type HeadingLevel } from './_heading'\nimport { ProductCard } from './product-card'\n\n/**\n * `ProductCarousel` — a horizontal product strip («Productos relacionados»,\n * «También te puede interesar»…) with no carousel dependency.\n *\n * - The track is a native scroll container with CSS scroll-snap, so touch users swipe\n * it and keyboard users scroll it once it has focus (it is a labelled, focusable region).\n * - Previous/next arrows (from `sm` up) scroll by one visible page. They are not rendered\n * when everything fits, and the arrow of the end the track is at is disabled.\n * - Arrow scrolling is instant under `prefers-reduced-motion: reduce`.\n * - Cards: the package's `ProductCard` by default, or the consumer's via `renderProduct`.\n */\nexport interface ProductCarouselLabels {\n /** Accessible name of the track when there is no `heading`. */\n region: string\n /** `aria-roledescription` of the track. */\n roleDescription: string\n previous: string\n next: string\n}\n\nexport const DEFAULT_PRODUCT_CAROUSEL_LABELS: ProductCarouselLabels = {\n region: 'Productos relacionados',\n roleDescription: 'carrusel',\n previous: 'Anterior',\n next: 'Siguiente',\n}\n\ninterface ProductCarouselCommonProps {\n /** Visible heading; when set it also names the track. */\n heading?: string\n /** Default: 2. */\n headingLevel?: HeadingLevel\n /** Replaces the heading's typography (see the className rule in the README). */\n headingClassName?: string\n labels?: Partial<ProductCarouselLabels>\n className?: string\n /** Classes for each slide (`<li>`), e.g. a different card width. */\n itemClassName?: string\n}\n\nexport interface ProductCarouselDefaultCardProps extends ProductCarouselCommonProps {\n products: readonly ShopProductListItem[]\n renderProduct?: undefined\n /** Link target per product (e.g. `(p) => \\`/shop/${p.slug}\\``). Omit for non-linking cards. */\n productHref?: (product: ShopProductListItem) => string | undefined\n currencyCode?: string\n locale?: string\n}\n\nexport interface ProductCarouselCustomCardProps<T extends { id: string | number }>\n extends ProductCarouselCommonProps {\n products: readonly T[]\n /** Renders one slide's card — your own product card. */\n renderProduct: (product: T, index: number) => ReactNode\n productHref?: undefined\n currencyCode?: undefined\n locale?: undefined\n}\n\nexport type ProductCarouselProps<T extends { id: string | number } = ShopProductListItem> =\n | ProductCarouselDefaultCardProps\n | ProductCarouselCustomCardProps<T>\n\n/** Tolerance for sub-pixel scroll positions. */\nconst EDGE_EPSILON = 1\n\nfunction prefersReducedMotion(): boolean {\n return typeof window !== 'undefined' && typeof window.matchMedia === 'function'\n ? window.matchMedia('(prefers-reduced-motion: reduce)').matches\n : false\n}\n\nfunction ChevronIcon({ direction }: { direction: 'left' | 'right' }) {\n return (\n <svg\n aria-hidden='true'\n viewBox='0 0 24 24'\n fill='none'\n stroke='currentColor'\n strokeWidth={2}\n strokeLinecap='round'\n strokeLinejoin='round'\n className='rtl:-scale-x-100'\n >\n <path d={direction === 'left' ? 'm15 18-6-6 6-6' : 'm9 18 6-6-6-6'} />\n </svg>\n )\n}\n\nexport function ProductCarousel<T extends { id: string | number } = ShopProductListItem>(\n props: ProductCarouselProps<T>,\n) {\n const { products, heading, headingLevel = 2, headingClassName, labels, className, itemClassName } = props\n const t = { ...DEFAULT_PRODUCT_CAROUSEL_LABELS, ...labels }\n\n const trackId = useId()\n const headingId = useId()\n const trackRef = useRef<HTMLDivElement>(null)\n const [canPrev, setCanPrev] = useState(false)\n const [canNext, setCanNext] = useState(false)\n\n const update = useCallback(() => {\n const el = trackRef.current\n if (!el) return\n const max = el.scrollWidth - el.clientWidth\n // RTL tracks report a negative scrollLeft; distance from the start is its magnitude.\n const pos = Math.abs(el.scrollLeft)\n setCanPrev(pos > EDGE_EPSILON)\n setCanNext(pos < max - EDGE_EPSILON)\n }, [])\n\n useEffect(() => {\n const el = trackRef.current\n if (!el) return\n update()\n el.addEventListener('scroll', update, { passive: true })\n let observer: ResizeObserver | undefined\n if (typeof ResizeObserver !== 'undefined') {\n observer = new ResizeObserver(update)\n observer.observe(el)\n } else {\n window.addEventListener('resize', update)\n }\n return () => {\n el.removeEventListener('scroll', update)\n if (observer) observer.disconnect()\n else window.removeEventListener('resize', update)\n }\n }, [update, products.length])\n\n const scrollByPage = (direction: -1 | 1) => {\n const el = trackRef.current\n if (!el) return\n const rtl = typeof getComputedStyle === 'function' && getComputedStyle(el).direction === 'rtl'\n el.scrollBy({\n left: direction * el.clientWidth * (rtl ? -1 : 1),\n behavior: prefersReducedMotion() ? 'auto' : 'smooth',\n })\n }\n\n if (products.length === 0) return null\n\n const scrollable = canPrev || canNext\n\n return (\n <section data-slot='product-carousel' className={cn('flex flex-col gap-4', className)}>\n {(heading || scrollable) && (\n <div className='flex items-center justify-between gap-4'>\n {heading ? (\n <div id={headingId}>\n <Heading level={headingLevel} className={headingClassName ?? 'text-xl font-semibold'}>\n {heading}\n </Heading>\n </div>\n ) : (\n <span />\n )}\n {scrollable && (\n <div className='hidden gap-2 sm:flex' data-slot='product-carousel-controls'>\n <button\n type='button'\n className={cn(buttonVariants({ variant: 'outline', size: 'icon' }), 'rounded-full')}\n aria-label={t.previous}\n aria-controls={trackId}\n disabled={!canPrev}\n onClick={() => scrollByPage(-1)}\n >\n <ChevronIcon direction='left' />\n </button>\n <button\n type='button'\n className={cn(buttonVariants({ variant: 'outline', size: 'icon' }), 'rounded-full')}\n aria-label={t.next}\n aria-controls={trackId}\n disabled={!canNext}\n onClick={() => scrollByPage(1)}\n >\n <ChevronIcon direction='right' />\n </button>\n </div>\n )}\n </div>\n )}\n\n <div\n ref={trackRef}\n id={trackId}\n role='region'\n aria-roledescription={t.roleDescription}\n {...(heading ? { 'aria-labelledby': headingId } : { 'aria-label': t.region })}\n tabIndex={0}\n data-slot='product-carousel-track'\n className='snap-x snap-mandatory overflow-x-auto overscroll-x-contain scroll-smooth rounded-lg pb-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:scroll-auto'\n >\n <ul role='list' className='m-0 flex list-none gap-4 p-0'>\n {products.map((product, index) => (\n <li\n key={product.id}\n className={cn(\n 'shrink-0 snap-start basis-[calc((100%-1rem)/1.5)] sm:basis-[calc((100%-2rem)/3)] lg:basis-[calc((100%-3rem)/4)]',\n itemClassName,\n )}\n >\n {props.renderProduct ? (\n props.renderProduct(product as T, index)\n ) : (\n <ProductCard\n product={product as ShopProductListItem}\n href={props.productHref?.(product as ShopProductListItem)}\n currencyCode={props.currencyCode}\n locale={props.locale}\n className='h-full'\n />\n )}\n </li>\n ))}\n </ul>\n </div>\n </section>\n )\n}\n","import type { ReactNode } from 'react'\n\n/** A heading level a caller can configure — `h1`…`h6`. */\nexport type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6\n\n/** One level deeper, clamped at `h6`. */\nexport function nextHeadingLevel(level: HeadingLevel): HeadingLevel {\n return Math.min(level + 1, 6) as HeadingLevel\n}\n\n/** Internal: renders `<h{level}>`. Server-safe (no hooks). */\nexport function Heading({\n level,\n className,\n children,\n}: {\n level: HeadingLevel\n className?: string\n children: ReactNode\n}) {\n const Tag = `h${level}` as const\n return <Tag className={className}>{children}</Tag>\n}\n","import { cn } from '@reblu/site-ui/utils'\nimport type { ShopProductListItem } from '@reblu/site-contracts'\n\nimport { formatPrice } from '../lib/format-price'\n\n/**\n * `ProductCard` — presentational shop product tile over `ShopProductListItem`.\n *\n * Token-driven, RSC-safe (no hooks). Prices arrive as integer cents and are\n * formatted with {@link formatPrice}. A `comparePrice` higher than `price` renders\n * as a struck-through \"was\" amount.\n */\nexport interface ProductCardProps {\n product: ShopProductListItem\n /** Link target for the product (e.g. `/shop/{slug}`). Omit to render non-linking. */\n href?: string\n currencyCode?: string\n locale?: string\n className?: string\n}\n\nexport function ProductCard({ product, href, currencyCode = 'EUR', locale, className }: ProductCardProps) {\n const image = product.images.find((i) => i.isPrimary) ?? product.images[0]\n const hasDiscount = product.comparePrice != null && product.comparePrice > product.price\n\n return (\n <article\n className={cn(\n 'group flex flex-col overflow-hidden rounded-lg border bg-card transition-shadow hover:shadow-lg',\n className,\n )}\n data-slot='product-card'\n >\n <div className='relative aspect-square overflow-hidden bg-muted'>\n {image ? (\n <img\n src={image.url}\n alt={image.alt ?? product.name}\n loading='lazy'\n className='h-full w-full object-cover transition-transform group-hover:scale-105'\n />\n ) : (\n <div className='flex h-full items-center justify-center text-4xl text-muted-foreground/30' aria-hidden='true'>\n 🛍️\n </div>\n )}\n </div>\n\n <div className='flex flex-1 flex-col gap-2 p-4'>\n {product.category && (\n <span className='inline-flex w-fit items-center rounded-full bg-secondary px-2.5 py-0.5 text-xs font-medium text-secondary-foreground'>\n {product.category.name}\n </span>\n )}\n\n <h3 className='text-base font-semibold leading-tight'>\n {href ? (\n <a href={href} className='transition-colors hover:text-primary'>\n {product.name}\n </a>\n ) : (\n product.name\n )}\n </h3>\n\n {product.shortDescription && (\n <p className='line-clamp-2 text-sm text-muted-foreground'>{product.shortDescription}</p>\n )}\n\n <div className='mt-auto flex items-baseline gap-2 pt-2'>\n <span className='text-lg font-bold text-foreground'>\n {formatPrice(product.price, currencyCode, locale)}\n </span>\n {hasDiscount && (\n <span className='text-sm text-muted-foreground line-through'>\n {formatPrice(product.comparePrice as number, currencyCode, locale)}\n </span>\n )}\n </div>\n </div>\n </article>\n )\n}\n","/**\n * Format an integer-cents amount as a localized currency string.\n *\n * All Reblu shop monetary values travel as integer cents (Prisma `Int`, never\n * Decimal), so the SITE never sees floats. This helper is the single place that\n * converts cents → major units and applies `Intl.NumberFormat`, keeping price\n * rendering consistent across `ProductCard` and any future shop organism.\n */\nexport function formatPrice(cents: number, currency = 'EUR', locale = 'es-ES'): string {\n return new Intl.NumberFormat(locale, {\n style: 'currency',\n currency,\n }).format(cents / 100)\n}\n","'use client'\n\nimport { useId, useState } from 'react'\n\nimport { cn } from '@reblu/site-ui/utils'\nimport type { AcademyLessonPublic, AcademyModulePublic } from '@reblu/site-contracts'\n\nimport type { HeadingLevel } from './_heading'\nimport { slotClassName } from '../lib/slot-class-name'\n\n/**\n * `CourseSyllabus` — the drop-in programme of a public course page, over\n * `AcademyCourseDetail.modules` (`client.academy.bySlug()`), REB-1134.\n *\n * - The header reads the heading, then «N módulos · M lecciones».\n * - Each module with lessons is a `<button>` with `aria-expanded` and\n * `aria-controls`, showing its lesson count and, when every lesson has one,\n * the module's total duration. Expanding it lists the lessons in the order\n * the API returns them, with their duration and the free-preview flag.\n * - A module without lessons is a plain row (title only), never a button.\n *\n * It renders only what the API serves. The API returns published lessons only\n * and no video, playback or attachment data, so nothing here can leak it.\n *\n * Client Component: it holds the open/closed state of each module. Every prop\n * is serialisable (copy is plain strings), so a Server Component page can\n * render it directly.\n */\nexport interface CourseSyllabusLabels {\n heading: string\n moduleSingular: string\n modulePlural: string\n lessonSingular: string\n lessonPlural: string\n /** Badge on a lesson with `isFreePreview`. */\n freePreview: string\n /** Hours unit in durations («1 h 15 min»). */\n hours: string\n /** Minutes unit in durations. */\n minutes: string\n}\n\nexport const DEFAULT_COURSE_SYLLABUS_LABELS: CourseSyllabusLabels = {\n heading: 'Programa',\n moduleSingular: 'módulo',\n modulePlural: 'módulos',\n lessonSingular: 'lección',\n lessonPlural: 'lecciones',\n freePreview: 'Vista previa gratuita',\n hours: 'h',\n minutes: 'min',\n}\n\n/** A module as served. `lessons` is optional so an older payload still renders. */\nexport type CourseSyllabusModule = Pick<AcademyModulePublic, 'id' | 'title'> & {\n description?: string | null\n lessons?: readonly AcademyLessonPublic[]\n}\n\nexport interface CourseSyllabusProps {\n /** `AcademyCourseDetail.modules`, in the order the API returns them. */\n modules: readonly CourseSyllabusModule[]\n labels?: Partial<CourseSyllabusLabels>\n /** Level of the heading. Default `2`. */\n headingLevel?: HeadingLevel\n /** Replaces the heading's default typography (see the className rule in the README). */\n headingClassName?: string\n className?: string\n}\n\nconst SEPARATOR = ' · '\n\nfunction count(n: number, singular: string, plural: string): string {\n return `${n} ${n === 1 ? singular : plural}`\n}\n\nfunction formatDuration(seconds: number, labels: CourseSyllabusLabels): string {\n const totalMinutes = Math.max(1, Math.round(seconds / 60))\n const hours = Math.floor(totalMinutes / 60)\n const minutes = totalMinutes % 60\n if (hours === 0) return `${minutes} ${labels.minutes}`\n return minutes === 0\n ? `${hours} ${labels.hours}`\n : `${hours} ${labels.hours} ${minutes} ${labels.minutes}`\n}\n\n/** Sum of the lessons' durations, or `null` unless every lesson has one. */\nfunction moduleSeconds(lessons: readonly AcademyLessonPublic[]): number | null {\n let total = 0\n for (const l of lessons) {\n if (typeof l.durationSeconds !== 'number' || l.durationSeconds <= 0) return null\n total += l.durationSeconds\n }\n return total\n}\n\nexport function CourseSyllabus({\n modules,\n labels: labelOverrides,\n headingLevel = 2,\n headingClassName,\n className,\n}: CourseSyllabusProps) {\n const labels = { ...DEFAULT_COURSE_SYLLABUS_LABELS, ...labelOverrides }\n const baseId = useId()\n const [open, setOpen] = useState<ReadonlySet<string>>(() => new Set())\n\n if (modules.length === 0) return null\n\n const totalLessons = modules.reduce((sum, m) => sum + (m.lessons?.length ?? 0), 0)\n const summary = [count(modules.length, labels.moduleSingular, labels.modulePlural)]\n if (totalLessons > 0)\n summary.push(count(totalLessons, labels.lessonSingular, labels.lessonPlural))\n\n const headingId = `${baseId}-heading`\n const Tag = `h${headingLevel}` as const\n\n function toggle(id: string) {\n setOpen((prev) => {\n const next = new Set(prev)\n if (next.has(id)) next.delete(id)\n else next.add(id)\n return next\n })\n }\n\n return (\n <section\n data-slot='course-syllabus'\n aria-labelledby={headingId}\n className={cn('space-y-4', className)}\n >\n <div className='space-y-1'>\n <Tag\n id={headingId}\n className={slotClassName('', 'text-xl font-semibold', headingClassName)}\n >\n {labels.heading}\n </Tag>\n <p data-slot='course-syllabus-summary' className='text-sm text-muted-foreground'>\n {summary.join(SEPARATOR)}\n </p>\n </div>\n\n <ol className='flex list-none flex-col gap-2.5 p-0'>\n {modules.map((module, index) => {\n const lessons = module.lessons ?? []\n const number = (\n <span\n aria-hidden='true'\n className='flex size-7 shrink-0 items-center justify-center rounded-full bg-muted text-sm font-semibold text-primary'\n >\n {index + 1}\n </span>\n )\n\n if (lessons.length === 0) {\n return (\n <li\n key={module.id}\n data-slot='course-syllabus-module'\n className='flex items-center gap-3.5 rounded-lg border border-border bg-card px-4 py-3.5'\n >\n {number}\n <span className='font-semibold leading-snug'>{module.title}</span>\n </li>\n )\n }\n\n const isOpen = open.has(module.id)\n const panelId = `${baseId}-module-${index}`\n const seconds = moduleSeconds(lessons)\n const meta = [count(lessons.length, labels.lessonSingular, labels.lessonPlural)]\n if (seconds !== null) meta.push(formatDuration(seconds, labels))\n\n return (\n <li\n key={module.id}\n data-slot='course-syllabus-module'\n className='rounded-lg border border-border bg-card'\n >\n <button\n type='button'\n aria-expanded={isOpen}\n aria-controls={panelId}\n onClick={() => toggle(module.id)}\n className='flex w-full cursor-pointer items-center gap-3.5 rounded-lg px-4 py-3.5 text-left focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'\n >\n {number}\n <span className='flex min-w-0 flex-1 flex-col gap-0.5'>\n <span className='font-semibold leading-snug'>{module.title}</span>\n <span className='text-sm text-muted-foreground'>{meta.join(SEPARATOR)}</span>\n </span>\n <span\n aria-hidden='true'\n className={cn(\n 'shrink-0 text-muted-foreground transition-transform motion-reduce:transition-none',\n isOpen && 'rotate-180',\n )}\n >\n ▾\n </span>\n </button>\n <ol\n id={panelId}\n hidden={!isOpen}\n data-slot='course-syllabus-lessons'\n className='list-none space-y-2 border-t border-border px-4 py-3 pl-[3.75rem]'\n >\n {lessons.map((lesson) => (\n <li\n key={lesson.id}\n data-slot='course-syllabus-lesson'\n className='flex flex-wrap items-baseline gap-x-3 gap-y-1 text-sm'\n >\n <span data-slot='course-syllabus-lesson-title' className='flex-1'>\n {lesson.title}\n </span>\n {lesson.isFreePreview && (\n <span className='rounded-full bg-muted px-2 py-0.5 text-xs font-medium'>\n {labels.freePreview}\n </span>\n )}\n {typeof lesson.durationSeconds === 'number' && lesson.durationSeconds > 0 && (\n <span className='text-muted-foreground tabular-nums'>\n {formatDuration(lesson.durationSeconds, labels)}\n </span>\n )}\n </li>\n ))}\n </ol>\n </li>\n )\n })}\n </ol>\n </section>\n )\n}\n","import { cn } from '@reblu/site-ui/utils'\n\n/**\n * `text-*` utilities tailwind-merge classifies correctly (alignment, wrapping,\n * overflow). Every other `text-*` may be a SITE theme token it misreads.\n */\nconst MERGE_SAFE_TEXT = /(?:^|:)!?text-(?:left|center|right|justify|start|end|wrap|nowrap|balance|pretty|ellipsis|clip)$/\nconst TEXT_UTILITY = /(?:^|:)!?-?text-/\n\nfunction bypassesMerge(token: string): boolean {\n return TEXT_UTILITY.test(token) && !MERGE_SAFE_TEXT.test(token)\n}\n\n/**\n * Classes for a drop-in element a consumer can style (REB-997).\n *\n * tailwind-merge only knows Tailwind's default scale: a SITE's custom font-size\n * token such as `text-h2` is read as a text COLOUR and silently dropped against\n * a real colour (`twMerge('text-h2 text-ink')` → `text-ink`). So:\n *\n * - no consumer class (`undefined` or blank) → `cn(structural, defaults)`,\n * exactly what the component rendered before;\n * - with a consumer class → the stylistic `defaults` are NOT applied (the\n * consumer owns the look of that slot); `structural` — the classes the\n * element needs to work — is kept and merged with the consumer's layout\n * classes, while the consumer's `text-*` classes skip tailwind-merge and are\n * appended verbatim, so custom tokens reach the DOM intact.\n */\nexport function slotClassName(structural: string, defaults: string, consumer: string | undefined): string {\n if (consumer === undefined || consumer.trim() === '') return cn(structural, defaults)\n const tokens = consumer.split(/\\s+/).filter(Boolean)\n const verbatim = tokens.filter(bypassesMerge)\n const merged = cn(structural, tokens.filter((token) => !bypassesMerge(token)))\n return [merged, ...verbatim].filter(Boolean).join(' ')\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='cursor-pointer rounded-lg bg-primary px-6 py-2 text-sm font-semibold text-primary-foreground disabled:cursor-not-allowed disabled:opacity-60'\n >\n {submitting ? t.submitting : t.submit}\n </button>\n </form>\n )\n}\n"],"mappings":";;;AAEA,SAAS,aAAa,SAAS,gBAAgB;AA2DxC,SAAS,cAAiB,SAA0D;AACzF,QAAM,EAAE,OAAO,WAAW,GAAG,UAAU,CAAC,GAAG,QAAQ,UAAU,QAAQ,CAAC,EAAE,IAAI;AAE5E,QAAM,CAAC,eAAe,gBAAgB,IAAI;AAAA,IACxC,MAAM,QAAQ,kBAAkB,CAAC;AAAA,EACnC;AACA,QAAM,CAAC,QAAQ,cAAc,IAAI,SAAS,MAAM,QAAQ,iBAAiB,EAAE;AAC3E,QAAM,CAAC,MAAM,YAAY,IAAI,SAA6B,MAAM,QAAQ,WAAW;AACnF,QAAM,CAAC,MAAM,YAAY,IAAI,SAAS,MAAM,QAAQ,eAAe,CAAC;AAEpE,QAAM,WAAW,QAAQ,MAAM;AAC7B,QAAI,OAAO;AAEX,eAAW,EAAE,KAAK,UAAU,KAAK,SAAS;AACxC,YAAM,QAAQ,cAAc,GAAG;AAC/B,UAAI,SAAS,QAAQ,UAAU,IAAI;AACjC,eAAO,KAAK,OAAO,CAAC,SAAS,UAAU,MAAM,KAAK,CAAC;AAAA,MACrD;AAAA,IACF;AAEA,UAAM,OAAO,OAAO,KAAK;AACzB,QAAI,YAAY,MAAM;AACpB,aAAO,KAAK,OAAO,CAAC,SAAS,SAAS,MAAM,IAAI,CAAC;AAAA,IACnD;AAEA,UAAM,aAAa,OAAO,MAAM,IAAI,IAAI;AACxC,QAAI,YAAY;AACd,aAAO,KAAK,MAAM,EAAE,KAAK,UAAU;AAAA,IACrC;AAEA,WAAO;AAAA,EACT,GAAG,CAAC,OAAO,SAAS,eAAe,QAAQ,UAAU,MAAM,KAAK,CAAC;AAEjE,QAAM,QAAQ,SAAS;AACvB,QAAM,YAAY,WAAW,IAAI,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,QAAQ,CAAC,IAAI;AAG5E,QAAM,cAAc,KAAK,IAAI,KAAK,IAAI,GAAG,IAAI,GAAG,SAAS;AAEzD,QAAM,YAAY,QAAQ,MAAM;AAC9B,QAAI,YAAY,EAAG,QAAO;AAC1B,UAAM,SAAS,cAAc,KAAK;AAClC,WAAO,SAAS,MAAM,OAAO,QAAQ,QAAQ;AAAA,EAC/C,GAAG,CAAC,UAAU,UAAU,WAAW,CAAC;AAGpC,QAAM,mBAAmB,YAAY,MAAM,aAAa,CAAC,GAAG,CAAC,CAAC;AAE9D,QAAM,YAAY;AAAA,IAChB,CAAC,KAAa,UAA8B;AAC1C,uBAAiB,CAAC,UAAU,EAAE,GAAG,MAAM,CAAC,GAAG,GAAG,MAAM,EAAE;AACtD,uBAAiB;AAAA,IACnB;AAAA,IACA,CAAC,gBAAgB;AAAA,EACnB;AAEA,QAAM,eAAe,YAAY,MAAM;AACrC,qBAAiB,CAAC,CAAC;AACnB,qBAAiB;AAAA,EACnB,GAAG,CAAC,gBAAgB,CAAC;AAErB,QAAM,YAAY;AAAA,IAChB,CAAC,SAAiB;AAChB,qBAAe,IAAI;AACnB,uBAAiB;AAAA,IACnB;AAAA,IACA,CAAC,gBAAgB;AAAA,EACnB;AAEA,QAAM,UAAU;AAAA,IACd,CAAC,QAA4B;AAC3B,mBAAa,GAAG;AAChB,uBAAiB;AAAA,IACnB;AAAA,IACA,CAAC,gBAAgB;AAAA,EACnB;AAIA,QAAM,UAAU;AAAA,IACd,CAAC,SAAiB,aAAa,KAAK,IAAI,KAAK,IAAI,GAAG,IAAI,GAAG,SAAS,CAAC;AAAA,IACrE,CAAC,SAAS;AAAA,EACZ;AACA,QAAM,WAAW;AAAA,IACf,MAAM,aAAa,KAAK,IAAI,cAAc,GAAG,SAAS,CAAC;AAAA,IACvD,CAAC,aAAa,SAAS;AAAA,EACzB;AACA,QAAM,WAAW,YAAY,MAAM,aAAa,KAAK,IAAI,GAAG,cAAc,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC;AAE5F,SAAO;AAAA,IACL,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA,SAAS,UAAU;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;;;ACtKA,SAAS,UAAU;AA2FL,SAME,KANF;AAvDd,IAAM,iBAAmC;AAAA,EACvC,UAAU;AAAA,EACV,MAAM;AAAA,EACN,MAAM,CAAC,MAAM,cAAc,QAAQ,IAAI,OAAO,SAAS;AACzD;AAmBO,SAAS,WAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS,CAAC;AAAA,EACV,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb;AAAA,EACA,gBAAgB;AAAA,EAChB;AACF,GAAuB;AACrB,QAAM,IAAI,EAAE,GAAG,gBAAgB,GAAG,OAAO;AACzC,QAAM,aAAa,cAAiB;AAAA,IAClC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,SACE,qBAAC,SAAI,WAAW,GAAG,aAAa,SAAS,GACtC;AAAA,WAAO,SAAS,KACf,oBAAC,SAAI,WAAU,aACZ,iBAAO,IAAI,CAAC,UAAU;AACrB,YAAM,SAAS,WAAW,cAAc,MAAM,GAAG;AACjD,aACE;AAAA,QAAC;AAAA;AAAA,UAEC,MAAK;AAAA,UACL,cAAY,MAAM;AAAA,UAClB,WAAU;AAAA,UAEV;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,OAAO,MAAM,YAAY;AAAA,gBACzB,SAAS,UAAU,QAAQ,WAAW;AAAA,gBACtC,SAAS,MAAM,WAAW,UAAU,MAAM,KAAK,MAAS;AAAA;AAAA,YAC1D;AAAA,YACC,MAAM,QAAQ,IAAI,CAAC,QAClB;AAAA,cAAC;AAAA;AAAA,gBAEC,OAAO,IAAI;AAAA,gBACX,OAAO,IAAI;AAAA,gBACX,SAAS,WAAW,IAAI;AAAA,gBACxB,SAAS,MAAM,WAAW,UAAU,MAAM,KAAK,IAAI,KAAK;AAAA;AAAA,cAJnD,IAAI;AAAA,YAKX,CACD;AAAA;AAAA;AAAA,QAlBI,MAAM;AAAA,MAmBb;AAAA,IAEJ,CAAC,GACH;AAAA,IAGD,WAAW,UACV,oBAAC,OAAE,WAAU,2CAA2C,sBAAW,IAEnE,oBAAC,QAAG,WAAW,GAAG,iBAAiB,aAAa,GAC7C,qBAAW,MAAM,IAAI,CAAC,SACrB,oBAAC,QAAuB,qBAAW,IAAI,KAA9B,OAAO,IAAI,CAAqB,CAC1C,GACH;AAAA,IAGD,WAAW,YAAY,KACtB,qBAAC,SAAI,cAAW,cAAa,WAAU,0CACrC;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS,WAAW;AAAA,UACpB,UAAU,WAAW,QAAQ;AAAA,UAC7B,WAAW;AAAA,UAEV,YAAE;AAAA;AAAA,MACL;AAAA,MACA,oBAAC,UAAK,gBAAa,QAAO,WAAU,iCACjC,YAAE,KAAK,WAAW,MAAM,WAAW,SAAS,GAC/C;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS,WAAW;AAAA,UACpB,UAAU,WAAW,QAAQ,WAAW;AAAA,UACxC,WAAW;AAAA,UAEV,YAAE;AAAA;AAAA,MACL;AAAA,OACF;AAAA,KAEJ;AAEJ;AAEA,IAAM,iBACJ;AASF,SAAS,UAAU,EAAE,OAAO,SAAS,OAAO,QAAQ,GAAmB;AACrE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,gBAAc;AAAA,MACd;AAAA,MAGA,OAAO,QAAS,EAAE,iBAAiB,MAAM,IAA4B;AAAA,MACrE,WAAW;AAAA,QACT;AAAA,QACA,UACI,QACE,0DACA,0DACF;AAAA,MACN;AAAA,MAEC;AAAA;AAAA,EACH;AAEJ;;;AC1LA,YAAY,WAAW;AACvB,SAAS,MAAAA,WAAU;;;ACWZ,SAAS,eAAe,KAAa,OAA0B;AACpE,QAAM,aAAa,mBAAmB,GAAG;AACzC,QAAM,eAAe,mBAAmB,KAAK;AAE7C,SAAO;AAAA;AAAA;AAAA,IAGL,UAAU,uDAAuD,UAAU;AAAA;AAAA,IAE3E,GAAG,kCAAkC,UAAU,SAAS,YAAY;AAAA,IACpE,UAAU,gDAAgD,UAAU;AAAA,IACpE,UAAU,uBAAuB,mBAAmB,GAAG,KAAK,IAAI,GAAG,EAAE,CAAC;AAAA,EACxE;AACF;;;AD2BM,gBAAAC,MA+BF,QAAAC,aA/BE;AA5BN,IAAMC,kBAAqC;AAAA,EACzC,OAAO;AAAA,EACP,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,GAAG;AAAA,EACH,UAAU;AAAA,EACV,UAAU;AAAA,EACV,YAAY;AACd;AAWA,IAAM,aACJ;AAEF,IAAM,OAAO;AAEb,SAAS,eAAe;AACtB,SACE,gBAAAF,KAAC,SAAI,WAAW,MAAM,SAAQ,aAAY,MAAK,gBAAe,eAAY,QAAO,WAAU,SACzF,0BAAAA,KAAC,UAAK,GAAE,mSAAkS,GAC5S;AAEJ;AAEA,SAAS,QAAQ;AACf,SACE,gBAAAA,KAAC,SAAI,WAAW,MAAM,SAAQ,aAAY,MAAK,gBAAe,eAAY,QAAO,WAAU,SACzF,0BAAAA,KAAC,UAAK,GAAE,+JAA8J,GACxK;AAEJ;AAEA,SAAS,eAAe;AACtB,SACE,gBAAAA,KAAC,SAAI,WAAW,MAAM,SAAQ,aAAY,MAAK,gBAAe,eAAY,QAAO,WAAU,SACzF,0BAAAA,KAAC,UAAK,GAAE,0LAAyL,GACnM;AAEJ;AAEA,SAAS,eAAe;AACtB,SACE,gBAAAA,KAAC,SAAI,WAAW,MAAM,SAAQ,aAAY,MAAK,gBAAe,eAAY,QAAO,WAAU,SACzF,0BAAAA,KAAC,UAAK,GAAE,8yBAA6yB,GACvzB;AAEJ;AAEA,SAAS,WAAW;AAClB,SACE,gBAAAC,MAAC,SAAI,WAAW,MAAM,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,KAAI,eAAc,SAAQ,gBAAe,SAAQ,eAAY,QAAO,WAAU,SACpK;AAAA,oBAAAD,KAAC,UAAK,GAAE,KAAI,GAAE,KAAI,OAAM,MAAK,QAAO,MAAK,IAAG,KAAI,IAAG,KAAI;AAAA,IACvD,gBAAAA,KAAC,UAAK,GAAE,2DAA0D;AAAA,KACpE;AAEJ;AAEA,SAAS,YAAY;AACnB,SACE,gBAAAA,KAAC,SAAI,WAAW,MAAM,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,KAAI,eAAc,SAAQ,gBAAe,SAAQ,eAAY,QAAO,WAAU,SACpK,0BAAAA,KAAC,cAAS,QAAO,kBAAiB,GACpC;AAEJ;AAEO,SAAS,aAAa,EAAE,KAAK,OAAO,WAAW,OAAO,GAAsB;AACjF,QAAM,IAAI,EAAE,GAAGE,iBAAgB,GAAG,OAAO;AACzC,QAAM,CAAC,QAAQ,SAAS,IAAU,eAAS,KAAK;AAChD,QAAM,QAAQ,eAAe,KAAK,KAAK;AAEvC,iBAAe,aAAa;AAC1B,QAAI;AACF,YAAM,UAAU,UAAU,UAAU,GAAG;AACvC,gBAAU,IAAI;AACd,aAAO,WAAW,MAAM,UAAU,KAAK,GAAG,GAAI;AAAA,IAChD,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SACE,gBAAAD,MAAC,SAAI,WAAWE,IAAG,2BAA2B,SAAS,GAAG,MAAK,SAAQ,cAAY,EAAE,OACnF;AAAA,oBAAAH,KAAC,UAAK,WAAU,kDAAkD,YAAE,OAAM;AAAA,IAE1E,gBAAAA,KAAC,OAAE,MAAM,MAAM,UAAU,QAAO,UAAS,KAAI,uBAAsB,WAAW,YAAY,cAAY,EAAE,UACtG,0BAAAA,KAAC,gBAAa,GAChB;AAAA,IACA,gBAAAA,KAAC,OAAE,MAAM,MAAM,GAAG,QAAO,UAAS,KAAI,uBAAsB,WAAW,YAAY,cAAY,EAAE,GAC/F,0BAAAA,KAAC,SAAM,GACT;AAAA,IACA,gBAAAA,KAAC,OAAE,MAAM,MAAM,UAAU,QAAO,UAAS,KAAI,uBAAsB,WAAW,YAAY,cAAY,EAAE,UACtG,0BAAAA,KAAC,gBAAa,GAChB;AAAA,IACA,gBAAAA,KAAC,OAAE,MAAM,MAAM,UAAU,QAAO,UAAS,KAAI,uBAAsB,WAAW,YAAY,cAAY,EAAE,UACtG,0BAAAA,KAAC,gBAAa,GAChB;AAAA,IAEA,gBAAAC;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS;AAAA,QACT,WAAWE,IAAG,YAAY,oCAAoC,UAAU,6BAA6B;AAAA,QACrG,cAAY,EAAE;AAAA,QAEb;AAAA,mBAAS,gBAAAH,KAAC,aAAU,IAAK,gBAAAA,KAAC,YAAS;AAAA,UACpC,gBAAAA,KAAC,UAAK,WAAU,WAAW,mBAAS,EAAE,SAAS,EAAE,MAAK;AAAA;AAAA;AAAA,IACxD;AAAA,IAGA,gBAAAA,KAAC,UAAK,MAAK,UAAS,aAAU,UAAS,WAAU,WAC9C,mBAAS,EAAE,aAAa,IAC3B;AAAA,KACF;AAEJ;;;AElJA,SAAS,WAAW,QAAQ,YAAAI,iBAAgB;AAE5C,SAAS,MAAAC,WAAU;AAoNf,gBAAAC,YAAA;AA3LJ,IAAMC,kBAAwC;AAAA,EAC5C,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,OAAO;AACT;AAkDO,SAAS,gBAAiC;AAAA,EAC/C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,oBAAoB;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAgC;AAC9B,QAAM,IAAI,EAAE,GAAGA,iBAAgB,GAAG,OAAO;AACzC,QAAM,CAAC,QAAQ,SAAS,IAAIH,UAAgC,MAAM;AAClE,QAAM,aAAa,OAA6C,IAAI;AACpE,QAAM,eAAe,OAAqC,KAAK;AAO/D,QAAM,qBAAqB,OAAO,KAAK;AACvC,QAAM,eAAe,OAAO,KAAK;AACjC,QAAM,aAAa,OAAO,IAAI;AAK9B,QAAM,CAAC,WAAW,YAAY,IAAIA,UAAS,KAAK;AAEhD,QAAM,aAAa,MAAM;AACvB,QAAI,WAAW,SAAS;AACtB,mBAAa,WAAW,OAAO;AAC/B,iBAAW,UAAU;AAAA,IACvB;AAAA,EACF;AAEA,YAAU,MAAM;AACd,eAAW,UAAU;AACrB,WAAO,MAAM;AACX,iBAAW,UAAU;AACrB,iBAAW;AAAA,IACb;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,QAAM,iBAAiB,MAAM;AAC3B,uBAAmB,UAAU;AAC7B,eAAW;AACX,cAAU,SAAS;AACnB,cAAU,IAAI;AACd,eAAW,UAAU,WAAW,MAAM;AACpC,yBAAmB,UAAU;AAC7B,gBAAU,MAAM;AAAA,IAClB,GAAG,iBAAiB;AAAA,EACtB;AAEA,QAAM,eAAe,CAAC,aAA+B,SAAS;AAC5D,eAAW;AACX,uBAAmB,UAAU;AAC7B,cAAU,OAAO;AACjB,cAAU,MAAM,UAAU;AAC1B,eAAW,UAAU,WAAW,MAAM,UAAU,MAAM,GAAG,iBAAiB;AAAA,EAC5E;AAEA,YAAU,MAAM;AACd,QAAI,SAAS,CAAC,aAAa,SAAS;AAClC,mBAAa,KAAK;AAAA,IACpB;AACA,iBAAa,UAAU;AAAA,EAEzB,GAAG,CAAC,KAAK,CAAC;AAWV,QAAM,eAAe,CAAC,WAAoC;AACxD,QAAI,CAAC,WAAW,WAAW,WAAY;AACvC,QAAI,QAAQ,WAAW,WAAW;AAChC,mBAAa,IAAI;AACjB;AAAA,IACF;AACA,QAAI,MAAO;AACX,mBAAe;AAAA,EACjB;AAEA,QAAM,cAAc,MAAM;AACxB,QAAI,mBAAmB,WAAW,aAAa,QAAS;AACxD,UAAM,SAAS,cAAc,IAAI;AAEjC,QAAI,UAAU,OAAQ,OAA4C,SAAS,YAAY;AACrF,mBAAa,UAAU;AACvB,mBAAa,IAAI;AAChB,MAAC,OACC,KAAK,CAAC,aAAa;AAClB,qBAAa,UAAU;AACvB,YAAI,WAAW,QAAS,cAAa,KAAK;AAC1C,qBAAa,QAAQ;AAAA,MACvB,CAAC,EACA,MAAM,MAAM;AACX,qBAAa,UAAU;AACvB,YAAI,WAAW,QAAS,cAAa,KAAK;AAC1C,qBAAa,EAAE,QAAQ,UAAU,CAAC;AAAA,MACpC,CAAC;AACH;AAAA,IACF;AAEA,iBAAa,MAAiC;AAAA,EAChD;AAEA,QAAM,aAAa,QAAQ,YAAY,cAAc,WAAW,WAAW,aAAa,SAAS;AACjG,QAAM,QAAQ,aACV,EAAE,aACF,WAAW,YACT,EAAE,QACF,WAAW,UACT,OAAO,UAAU,WACf,QACA,EAAE,QACJ,WAAW,YACT,EAAE,SACF,EAAE;AAEZ,SACE,gBAAAE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAS;AAAA,MACT,UAAU;AAAA,MACV,iBAAe;AAAA,MACf,aAAU;AAAA,MACV,aAAU;AAAA,MACV,eAAa;AAAA,MACb,WAAWD;AAAA,QACT;AAAA,QACA,aACI,sDACA;AAAA,QACJ,WAAW,WAAW;AAAA,QACtB;AAAA,MACF;AAAA,MAEC;AAAA;AAAA,EACH;AAEJ;;;AC1OA,SAAS,aAAAG,YAAW,UAAAC,SAAQ,YAAAC,iBAAgB;AAE5C,SAAS,MAAAC,WAAU;AAuGb,SAUE,OAAAC,MAVF,QAAAC,aAAA;AA/EN,IAAMC,kBAAiC;AAAA,EACrC,MAAM;AAAA,EACN,aAAa,CAACC,WAAU,GAAGA,MAAK,eAAYA,WAAU,IAAI,KAAK,GAAG;AACpE;AAGA,IAAM,mBAAmB;AAwBlB,SAAS,SAAS,EAAE,WAAW,MAAM,UAAU,WAAW,QAAQ,UAAU,GAAkB;AACnG,QAAM,IAAI,EAAE,GAAGD,iBAAgB,GAAG,OAAO;AACzC,QAAM,YAAY,YAAY,IAAI,GAAG,EAAE,IAAI,KAAK,EAAE,YAAY,SAAS,CAAC,MAAM,EAAE;AAChF,QAAM,eAAe,YAAY,KAAK,QAAQ,OAAO,SAAS;AAE9D,QAAM,CAAC,SAAS,UAAU,IAAIJ,UAAS,KAAK;AAC5C,QAAM,eAAeD,QAAO,SAAS;AACrC,QAAM,kBAAkBA,QAAO,QAAQ;AAEvC,EAAAD,WAAU,MAAM;AAMd,UAAM,0BAA0B,aAAa,UAAa,gBAAgB,YAAY;AACtF,UAAM,aACJ,QAAQ,SAAS,KAAK,YAAY,aAAa,WAAW;AAE5D,iBAAa,UAAU;AACvB,oBAAgB,UAAU;AAE1B,QAAI,CAAC,YAAY;AAMf,iBAAW,KAAK;AAChB;AAAA,IACF;AAEA,eAAW,IAAI;AACf,UAAM,KAAK,WAAW,MAAM,WAAW,KAAK,GAAG,gBAAgB;AAC/D,WAAO,MAAM,aAAa,EAAE;AAAA,EAE9B,GAAG,CAAC,WAAW,UAAU,SAAS,CAAC;AAEnC,SACE,gBAAAK;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,cAAY;AAAA,MACZ,aAAU;AAAA,MACV,gBAAc,WAAW;AAAA,MACzB,WAAWF;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MAEA;AAAA,wBAAAE;AAAA,UAAC;AAAA;AAAA,YACC,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,QAAO;AAAA,YACP,aAAa;AAAA,YACb,eAAc;AAAA,YACd,gBAAe;AAAA,YACf,WAAWF,IAAG,WAAW,WAAW,iFAAiF;AAAA,YACrH,eAAY;AAAA,YAEZ;AAAA,8BAAAC,KAAC,YAAO,IAAG,KAAI,IAAG,MAAK,GAAE,KAAI;AAAA,cAC7B,gBAAAA,KAAC,YAAO,IAAG,MAAK,IAAG,MAAK,GAAE,KAAI;AAAA,cAC9B,gBAAAA,KAAC,UAAK,GAAE,mEAAkE;AAAA;AAAA;AAAA,QAC5E;AAAA,QACC,YAAY,KACX,gBAAAA;AAAA,UAAC;AAAA;AAAA,YACC,eAAY;AAAA,YACZ,WAAWD;AAAA,cACT;AAAA,cACA,WAAW;AAAA,YACb;AAAA,YAEC;AAAA;AAAA,QACH;AAAA;AAAA;AAAA,EAEJ;AAEJ;;;ACpIA,SAAS,eAAAK,cAAa,aAAAC,YAAW,OAAO,UAAAC,SAAQ,YAAAC,iBAAgC;AAEhF,SAAS,sBAAsB;AAC/B,SAAS,MAAAC,WAAU;;;ACgBV,gBAAAC,YAAA;AAVF,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,MAAM,IAAI,KAAK;AACrB,SAAO,gBAAAC,KAAC,OAAI,WAAuB,UAAS;AAC9C;;;ACtBA,SAAS,MAAAC,WAAU;;;ACQZ,SAAS,YAAY,OAAe,WAAW,OAAO,SAAS,SAAiB;AACrF,SAAO,IAAI,KAAK,aAAa,QAAQ;AAAA,IACnC,OAAO;AAAA,IACP;AAAA,EACF,CAAC,EAAE,OAAO,QAAQ,GAAG;AACvB;;;ADsBU,gBAAAC,MAkCF,QAAAC,aAlCE;AAdH,SAAS,YAAY,EAAE,SAAS,MAAM,eAAe,OAAO,QAAQ,UAAU,GAAqB;AACxG,QAAM,QAAQ,QAAQ,OAAO,KAAK,CAAC,MAAM,EAAE,SAAS,KAAK,QAAQ,OAAO,CAAC;AACzE,QAAM,cAAc,QAAQ,gBAAgB,QAAQ,QAAQ,eAAe,QAAQ;AAEnF,SACE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,WAAWC;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACA,aAAU;AAAA,MAEV;AAAA,wBAAAF,KAAC,SAAI,WAAU,mDACZ,kBACC,gBAAAA;AAAA,UAAC;AAAA;AAAA,YACC,KAAK,MAAM;AAAA,YACX,KAAK,MAAM,OAAO,QAAQ;AAAA,YAC1B,SAAQ;AAAA,YACR,WAAU;AAAA;AAAA,QACZ,IAEA,gBAAAA,KAAC,SAAI,WAAU,6EAA4E,eAAY,QAAO,6BAE9G,GAEJ;AAAA,QAEA,gBAAAC,MAAC,SAAI,WAAU,kCACZ;AAAA,kBAAQ,YACP,gBAAAD,KAAC,UAAK,WAAU,wHACb,kBAAQ,SAAS,MACpB;AAAA,UAGF,gBAAAA,KAAC,QAAG,WAAU,yCACX,iBACC,gBAAAA,KAAC,OAAE,MAAY,WAAU,wCACtB,kBAAQ,MACX,IAEA,QAAQ,MAEZ;AAAA,UAEC,QAAQ,oBACP,gBAAAA,KAAC,OAAE,WAAU,8CAA8C,kBAAQ,kBAAiB;AAAA,UAGtF,gBAAAC,MAAC,SAAI,WAAU,0CACb;AAAA,4BAAAD,KAAC,UAAK,WAAU,qCACb,sBAAY,QAAQ,OAAO,cAAc,MAAM,GAClD;AAAA,YACC,eACC,gBAAAA,KAAC,UAAK,WAAU,8CACb,sBAAY,QAAQ,cAAwB,cAAc,MAAM,GACnE;AAAA,aAEJ;AAAA,WACF;AAAA;AAAA;AAAA,EACF;AAEJ;;;AFaM,gBAAAG,MA0EM,QAAAC,aA1EN;AAhEC,IAAM,kCAAyD;AAAA,EACpE,QAAQ;AAAA,EACR,iBAAiB;AAAA,EACjB,UAAU;AAAA,EACV,MAAM;AACR;AAuCA,IAAM,eAAe;AAErB,SAAS,uBAAgC;AACvC,SAAO,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,aACjE,OAAO,WAAW,kCAAkC,EAAE,UACtD;AACN;AAEA,SAAS,YAAY,EAAE,UAAU,GAAoC;AACnE,SACE,gBAAAD;AAAA,IAAC;AAAA;AAAA,MACC,eAAY;AAAA,MACZ,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,QAAO;AAAA,MACP,aAAa;AAAA,MACb,eAAc;AAAA,MACd,gBAAe;AAAA,MACf,WAAU;AAAA,MAEV,0BAAAA,KAAC,UAAK,GAAG,cAAc,SAAS,mBAAmB,iBAAiB;AAAA;AAAA,EACtE;AAEJ;AAEO,SAAS,gBACd,OACA;AACA,QAAM,EAAE,UAAU,SAAS,eAAe,GAAG,kBAAkB,QAAQ,WAAW,cAAc,IAAI;AACpG,QAAM,IAAI,EAAE,GAAG,iCAAiC,GAAG,OAAO;AAE1D,QAAM,UAAU,MAAM;AACtB,QAAM,YAAY,MAAM;AACxB,QAAM,WAAWE,QAAuB,IAAI;AAC5C,QAAM,CAAC,SAAS,UAAU,IAAIC,UAAS,KAAK;AAC5C,QAAM,CAAC,SAAS,UAAU,IAAIA,UAAS,KAAK;AAE5C,QAAM,SAASC,aAAY,MAAM;AAC/B,UAAM,KAAK,SAAS;AACpB,QAAI,CAAC,GAAI;AACT,UAAM,MAAM,GAAG,cAAc,GAAG;AAEhC,UAAM,MAAM,KAAK,IAAI,GAAG,UAAU;AAClC,eAAW,MAAM,YAAY;AAC7B,eAAW,MAAM,MAAM,YAAY;AAAA,EACrC,GAAG,CAAC,CAAC;AAEL,EAAAC,WAAU,MAAM;AACd,UAAM,KAAK,SAAS;AACpB,QAAI,CAAC,GAAI;AACT,WAAO;AACP,OAAG,iBAAiB,UAAU,QAAQ,EAAE,SAAS,KAAK,CAAC;AACvD,QAAI;AACJ,QAAI,OAAO,mBAAmB,aAAa;AACzC,iBAAW,IAAI,eAAe,MAAM;AACpC,eAAS,QAAQ,EAAE;AAAA,IACrB,OAAO;AACL,aAAO,iBAAiB,UAAU,MAAM;AAAA,IAC1C;AACA,WAAO,MAAM;AACX,SAAG,oBAAoB,UAAU,MAAM;AACvC,UAAI,SAAU,UAAS,WAAW;AAAA,UAC7B,QAAO,oBAAoB,UAAU,MAAM;AAAA,IAClD;AAAA,EACF,GAAG,CAAC,QAAQ,SAAS,MAAM,CAAC;AAE5B,QAAM,eAAe,CAAC,cAAsB;AAC1C,UAAM,KAAK,SAAS;AACpB,QAAI,CAAC,GAAI;AACT,UAAM,MAAM,OAAO,qBAAqB,cAAc,iBAAiB,EAAE,EAAE,cAAc;AACzF,OAAG,SAAS;AAAA,MACV,MAAM,YAAY,GAAG,eAAe,MAAM,KAAK;AAAA,MAC/C,UAAU,qBAAqB,IAAI,SAAS;AAAA,IAC9C,CAAC;AAAA,EACH;AAEA,MAAI,SAAS,WAAW,EAAG,QAAO;AAElC,QAAM,aAAa,WAAW;AAE9B,SACE,gBAAAJ,MAAC,aAAQ,aAAU,oBAAmB,WAAWK,IAAG,uBAAuB,SAAS,GAChF;AAAA,gBAAW,eACX,gBAAAL,MAAC,SAAI,WAAU,2CACZ;AAAA,gBACC,gBAAAD,KAAC,SAAI,IAAI,WACP,0BAAAA,KAAC,WAAQ,OAAO,cAAc,WAAW,oBAAoB,yBAC1D,mBACH,GACF,IAEA,gBAAAA,KAAC,UAAK;AAAA,MAEP,cACC,gBAAAC,MAAC,SAAI,WAAU,wBAAuB,aAAU,6BAC9C;AAAA,wBAAAD;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,WAAWM,IAAG,eAAe,EAAE,SAAS,WAAW,MAAM,OAAO,CAAC,GAAG,cAAc;AAAA,YAClF,cAAY,EAAE;AAAA,YACd,iBAAe;AAAA,YACf,UAAU,CAAC;AAAA,YACX,SAAS,MAAM,aAAa,EAAE;AAAA,YAE9B,0BAAAN,KAAC,eAAY,WAAU,QAAO;AAAA;AAAA,QAChC;AAAA,QACA,gBAAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,WAAWM,IAAG,eAAe,EAAE,SAAS,WAAW,MAAM,OAAO,CAAC,GAAG,cAAc;AAAA,YAClF,cAAY,EAAE;AAAA,YACd,iBAAe;AAAA,YACf,UAAU,CAAC;AAAA,YACX,SAAS,MAAM,aAAa,CAAC;AAAA,YAE7B,0BAAAN,KAAC,eAAY,WAAU,SAAQ;AAAA;AAAA,QACjC;AAAA,SACF;AAAA,OAEJ;AAAA,IAGF,gBAAAA;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,IAAI;AAAA,QACJ,MAAK;AAAA,QACL,wBAAsB,EAAE;AAAA,QACvB,GAAI,UAAU,EAAE,mBAAmB,UAAU,IAAI,EAAE,cAAc,EAAE,OAAO;AAAA,QAC3E,UAAU;AAAA,QACV,aAAU;AAAA,QACV,WAAU;AAAA,QAEV,0BAAAA,KAAC,QAAG,MAAK,QAAO,WAAU,gCACvB,mBAAS,IAAI,CAAC,SAAS,UACtB,gBAAAA;AAAA,UAAC;AAAA;AAAA,YAEC,WAAWM;AAAA,cACT;AAAA,cACA;AAAA,YACF;AAAA,YAEC,gBAAM,gBACL,MAAM,cAAc,SAAc,KAAK,IAEvC,gBAAAN;AAAA,cAAC;AAAA;AAAA,gBACC;AAAA,gBACA,MAAM,MAAM,cAAc,OAA8B;AAAA,gBACxD,cAAc,MAAM;AAAA,gBACpB,QAAQ,MAAM;AAAA,gBACd,WAAU;AAAA;AAAA,YACZ;AAAA;AAAA,UAfG,QAAQ;AAAA,QAiBf,CACD,GACH;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;;;AIrOA,SAAS,SAAAO,QAAO,YAAAC,iBAAgB;AAEhC,SAAS,MAAAC,WAAU;;;ACJnB,SAAS,MAAAC,WAAU;AAMnB,IAAM,kBAAkB;AACxB,IAAM,eAAe;AAErB,SAAS,cAAc,OAAwB;AAC7C,SAAO,aAAa,KAAK,KAAK,KAAK,CAAC,gBAAgB,KAAK,KAAK;AAChE;AAiBO,SAAS,cAAc,YAAoB,UAAkB,UAAsC;AACxG,MAAI,aAAa,UAAa,SAAS,KAAK,MAAM,GAAI,QAAOA,IAAG,YAAY,QAAQ;AACpF,QAAM,SAAS,SAAS,MAAM,KAAK,EAAE,OAAO,OAAO;AACnD,QAAM,WAAW,OAAO,OAAO,aAAa;AAC5C,QAAM,SAASA,IAAG,YAAY,OAAO,OAAO,CAAC,UAAU,CAAC,cAAc,KAAK,CAAC,CAAC;AAC7E,SAAO,CAAC,QAAQ,GAAG,QAAQ,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AACvD;;;ADkGM,SACE,OAAAC,MADF,QAAAC,aAAA;AA1FC,IAAM,iCAAuD;AAAA,EAClE,SAAS;AAAA,EACT,gBAAgB;AAAA,EAChB,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,cAAc;AAAA,EACd,aAAa;AAAA,EACb,OAAO;AAAA,EACP,SAAS;AACX;AAmBA,IAAM,YAAY;AAElB,SAAS,MAAM,GAAW,UAAkB,QAAwB;AAClE,SAAO,GAAG,CAAC,IAAI,MAAM,IAAI,WAAW,MAAM;AAC5C;AAEA,SAAS,eAAe,SAAiB,QAAsC;AAC7E,QAAM,eAAe,KAAK,IAAI,GAAG,KAAK,MAAM,UAAU,EAAE,CAAC;AACzD,QAAM,QAAQ,KAAK,MAAM,eAAe,EAAE;AAC1C,QAAM,UAAU,eAAe;AAC/B,MAAI,UAAU,EAAG,QAAO,GAAG,OAAO,IAAI,OAAO,OAAO;AACpD,SAAO,YAAY,IACf,GAAG,KAAK,IAAI,OAAO,KAAK,KACxB,GAAG,KAAK,IAAI,OAAO,KAAK,IAAI,OAAO,IAAI,OAAO,OAAO;AAC3D;AAGA,SAAS,cAAc,SAAwD;AAC7E,MAAI,QAAQ;AACZ,aAAW,KAAK,SAAS;AACvB,QAAI,OAAO,EAAE,oBAAoB,YAAY,EAAE,mBAAmB,EAAG,QAAO;AAC5E,aAAS,EAAE;AAAA,EACb;AACA,SAAO;AACT;AAEO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA,QAAQ;AAAA,EACR,eAAe;AAAA,EACf;AAAA,EACA;AACF,GAAwB;AACtB,QAAM,SAAS,EAAE,GAAG,gCAAgC,GAAG,eAAe;AACtE,QAAM,SAASC,OAAM;AACrB,QAAM,CAAC,MAAM,OAAO,IAAIC,UAA8B,MAAM,oBAAI,IAAI,CAAC;AAErE,MAAI,QAAQ,WAAW,EAAG,QAAO;AAEjC,QAAM,eAAe,QAAQ,OAAO,CAAC,KAAK,MAAM,OAAO,EAAE,SAAS,UAAU,IAAI,CAAC;AACjF,QAAM,UAAU,CAAC,MAAM,QAAQ,QAAQ,OAAO,gBAAgB,OAAO,YAAY,CAAC;AAClF,MAAI,eAAe;AACjB,YAAQ,KAAK,MAAM,cAAc,OAAO,gBAAgB,OAAO,YAAY,CAAC;AAE9E,QAAM,YAAY,GAAG,MAAM;AAC3B,QAAM,MAAM,IAAI,YAAY;AAE5B,WAAS,OAAO,IAAY;AAC1B,YAAQ,CAAC,SAAS;AAChB,YAAM,OAAO,IAAI,IAAI,IAAI;AACzB,UAAI,KAAK,IAAI,EAAE,EAAG,MAAK,OAAO,EAAE;AAAA,UAC3B,MAAK,IAAI,EAAE;AAChB,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAEA,SACE,gBAAAF;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,mBAAiB;AAAA,MACjB,WAAWG,IAAG,aAAa,SAAS;AAAA,MAEpC;AAAA,wBAAAH,MAAC,SAAI,WAAU,aACb;AAAA,0BAAAD;AAAA,YAAC;AAAA;AAAA,cACC,IAAI;AAAA,cACJ,WAAW,cAAc,IAAI,yBAAyB,gBAAgB;AAAA,cAErE,iBAAO;AAAA;AAAA,UACV;AAAA,UACA,gBAAAA,KAAC,OAAE,aAAU,2BAA0B,WAAU,iCAC9C,kBAAQ,KAAK,SAAS,GACzB;AAAA,WACF;AAAA,QAEA,gBAAAA,KAAC,QAAG,WAAU,uCACX,kBAAQ,IAAI,CAAC,QAAQ,UAAU;AAC9B,gBAAM,UAAU,OAAO,WAAW,CAAC;AACnC,gBAAM,SACJ,gBAAAA;AAAA,YAAC;AAAA;AAAA,cACC,eAAY;AAAA,cACZ,WAAU;AAAA,cAET,kBAAQ;AAAA;AAAA,UACX;AAGF,cAAI,QAAQ,WAAW,GAAG;AACxB,mBACE,gBAAAC;AAAA,cAAC;AAAA;AAAA,gBAEC,aAAU;AAAA,gBACV,WAAU;AAAA,gBAET;AAAA;AAAA,kBACD,gBAAAD,KAAC,UAAK,WAAU,8BAA8B,iBAAO,OAAM;AAAA;AAAA;AAAA,cALtD,OAAO;AAAA,YAMd;AAAA,UAEJ;AAEA,gBAAM,SAAS,KAAK,IAAI,OAAO,EAAE;AACjC,gBAAM,UAAU,GAAG,MAAM,WAAW,KAAK;AACzC,gBAAM,UAAU,cAAc,OAAO;AACrC,gBAAM,OAAO,CAAC,MAAM,QAAQ,QAAQ,OAAO,gBAAgB,OAAO,YAAY,CAAC;AAC/E,cAAI,YAAY,KAAM,MAAK,KAAK,eAAe,SAAS,MAAM,CAAC;AAE/D,iBACE,gBAAAC;AAAA,YAAC;AAAA;AAAA,cAEC,aAAU;AAAA,cACV,WAAU;AAAA,cAEV;AAAA,gCAAAA;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,iBAAe;AAAA,oBACf,iBAAe;AAAA,oBACf,SAAS,MAAM,OAAO,OAAO,EAAE;AAAA,oBAC/B,WAAU;AAAA,oBAET;AAAA;AAAA,sBACD,gBAAAA,MAAC,UAAK,WAAU,wCACd;AAAA,wCAAAD,KAAC,UAAK,WAAU,8BAA8B,iBAAO,OAAM;AAAA,wBAC3D,gBAAAA,KAAC,UAAK,WAAU,iCAAiC,eAAK,KAAK,SAAS,GAAE;AAAA,yBACxE;AAAA,sBACA,gBAAAA;AAAA,wBAAC;AAAA;AAAA,0BACC,eAAY;AAAA,0BACZ,WAAWI;AAAA,4BACT;AAAA,4BACA,UAAU;AAAA,0BACZ;AAAA,0BACD;AAAA;AAAA,sBAED;AAAA;AAAA;AAAA,gBACF;AAAA,gBACA,gBAAAJ;AAAA,kBAAC;AAAA;AAAA,oBACC,IAAI;AAAA,oBACJ,QAAQ,CAAC;AAAA,oBACT,aAAU;AAAA,oBACV,WAAU;AAAA,oBAET,kBAAQ,IAAI,CAAC,WACZ,gBAAAC;AAAA,sBAAC;AAAA;AAAA,wBAEC,aAAU;AAAA,wBACV,WAAU;AAAA,wBAEV;AAAA,0CAAAD,KAAC,UAAK,aAAU,gCAA+B,WAAU,UACtD,iBAAO,OACV;AAAA,0BACC,OAAO,iBACN,gBAAAA,KAAC,UAAK,WAAU,yDACb,iBAAO,aACV;AAAA,0BAED,OAAO,OAAO,oBAAoB,YAAY,OAAO,kBAAkB,KACtE,gBAAAA,KAAC,UAAK,WAAU,sCACb,yBAAe,OAAO,iBAAiB,MAAM,GAChD;AAAA;AAAA;AAAA,sBAfG,OAAO;AAAA,oBAiBd,CACD;AAAA;AAAA,gBACH;AAAA;AAAA;AAAA,YArDK,OAAO;AAAA,UAsDd;AAAA,QAEJ,CAAC,GACH;AAAA;AAAA;AAAA,EACF;AAEJ;;;AE7OA,SAAS,aAAAK,YAAW,SAAAC,QAAO,UAAAC,SAAQ,YAAAC,iBAAgD;AACnF,SAAS,MAAAC,WAAU;AACnB;AAAA,EACE;AAAA,OAIK;AAqFH,mBAsJE,OAAAC,MAtJF,QAAAC,aAAA;AAlDG,IAAM,4BAA4B,OAAO,OAAO,EAAE,MAAM,KAAK,OAAO,KAAK,SAAS,IAAK,CAAC;AAyBxF,IAAM,mCAA0D;AAAA,EACrE,kBAAkB;AAAA,EAClB,wBAAwB;AAAA,EACxB,cAAc;AAAA,IACZ,QAAQ;AAAA,IACR,eAAe;AAAA,IACf,SAAS;AAAA,IACT,WAAW;AAAA,IACX,aAAa;AAAA,IACb,aAAa;AAAA,IACb,oBAAoB;AAAA,IACpB,aAAa;AAAA,EACf;AAAA,EACA,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,oBAAoB;AAAA,EACpB,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,qBAAqB;AAAA,EACrB,cAAc;AAAA,EACd,cAAc;AAAA,EACd,iBAAiB;AAAA,EACjB,SAAS,CAAC,aACR,gBAAAC,MAAA,YAAE;AAAA;AAAA,IAC8D;AAAA,IAAS;AAAA,KACzE;AAAA,EAEF,cACE;AAAA,EACF,iBAAiB;AAAA,EACjB,gBAAgB;AAAA,EAChB,cAAc;AAChB;AAyBA,IAAM,cAAoC,CAAC,eAAe,QAAQ,SAAS,SAAS;AAEpF,IAAM,gBAAgB;AAEtB,eAAe,YAAY,UAAkB,OAA2D;AACtG,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,UAAU;AAAA,MAChC,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU,KAAK;AAAA,IAC5B,CAAC;AACD,QAAI,CAAC,IAAI,GAAI,QAAO,EAAE,IAAI,OAAO,QAAQ,IAAI,OAAO;AACpD,UAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,WAAO,KAAK,OAAO,EAAE,IAAI,MAAM,MAAM,KAAK,KAAK,IAAI,EAAE,IAAI,OAAO,QAAQ,IAAI,OAAO;AAAA,EACrF,QAAQ;AACN,WAAO,EAAE,IAAI,OAAO,QAAQ,EAAE;AAAA,EAChC;AACF;AAEA,SAAS,iBAAiB,QAAgB,GAAkC;AAC1E,MAAI,WAAW,IAAK,QAAO,EAAE;AAC7B,MAAI,WAAW,IAAK,QAAO,EAAE;AAC7B,MAAI,WAAW,IAAK,QAAO,EAAE;AAC7B,SAAO,EAAE;AACX;AAEA,IAAM,cACJ;AAEK,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA,eAAe;AAAA,EACf;AAAA,EACA,SAAS;AAAA,EACT;AACF,GAAyB;AACvB,QAAM,IAA2B;AAAA,IAC/B,GAAG;AAAA,IACH,GAAG;AAAA,IACH,cAAc,EAAE,GAAG,iCAAiC,cAAc,GAAG,QAAQ,aAAa;AAAA,EAC5F;AACA,QAAM,KAAKC,OAAM;AACjB,QAAM,CAAC,aAAa,cAAc,IAAIC,UAA+B,EAAE;AACvE,QAAM,CAAC,MAAM,OAAO,IAAIA,UAAS,EAAE;AACnC,QAAM,CAAC,OAAO,QAAQ,IAAIA,UAAS,EAAE;AACrC,QAAM,CAAC,SAAS,UAAU,IAAIA,UAAS,EAAE;AACzC,QAAM,CAAC,SAAS,UAAU,IAAIA,UAAS,KAAK;AAC5C,QAAM,CAAC,QAAQ,SAAS,IAAIA,UAAsB,CAAC,CAAC;AACpD,QAAM,CAAC,WAAW,YAAY,IAAIA,UAAwB,IAAI;AAC9D,QAAM,CAAC,YAAY,aAAa,IAAIA,UAAS,KAAK;AAClD,QAAM,CAAC,KAAK,MAAM,IAAIA,UAAuC,IAAI;AACjE,QAAM,YAAYC,QAAuB,IAAI;AAI7C,EAAAC,WAAU,MAAM;AACd,QAAI,IAAK,WAAU,SAAS,MAAM;AAAA,EACpC,GAAG,CAAC,GAAG,CAAC;AAER,iBAAe,aAAa,OAAmC;AAC7D,UAAM,eAAe;AACrB,QAAI,WAAY;AAChB,iBAAa,IAAI;AAEjB,UAAM,OAAoB,CAAC;AAC3B,QAAI,CAAC,YAAa,MAAK,cAAc,EAAE;AACvC,QAAI,CAAC,KAAK,KAAK,EAAG,MAAK,OAAO,EAAE;AAChC,QAAI,CAAC,cAAc,KAAK,MAAM,KAAK,CAAC,EAAG,MAAK,QAAQ,EAAE;AACtD,QAAI,CAAC,QAAS,MAAK,UAAU,EAAE;AAC/B,cAAU,IAAI;AACd,UAAM,eAAe,YAAY,KAAK,CAAC,UAAU,KAAK,KAAK,CAAC;AAC5D,QAAI,cAAc;AAEhB,eAAS,eAAe,GAAG,EAAE,IAAI,iBAAiB,gBAAgB,SAAS,YAAY,EAAE,GAAG,MAAM;AAClG;AAAA,IACF;AACA,QAAI,CAAC,YAAa;AAElB,kBAAc,IAAI;AAClB,QAAI;AACF,YAAM,UAAU,MAAM,gBAAgB,EAAE,MAAM,MAAM,MAAS;AAC7D,UAAI,CAAC,SAAS;AACZ,qBAAa,EAAE,YAAY;AAC3B;AAAA,MACF;AACA,YAAM,QAA0B;AAAA,QAC9B;AAAA,QACA,MAAM,KAAK,KAAK;AAAA,QAChB,OAAO,MAAM,KAAK;AAAA,QAClB,GAAI,QAAQ,KAAK,IAAI,EAAE,SAAS,QAAQ,KAAK,EAAE,IAAI,CAAC;AAAA,QACpD;AAAA,MACF;AACA,YAAM,SAAS,OAAO,WAAW,CAAC,SAAS,YAAY,UAAU,IAAI,IAAI,KAAK,EAAE;AAAA,QAC9E,OAAgC,EAAE,IAAI,OAAO,QAAQ,EAAE;AAAA,MACzD;AACA,UAAI,CAAC,OAAO,IAAI;AACd,qBAAa,iBAAiB,OAAO,QAAQ,CAAC,CAAC;AAC/C;AAAA,MACF;AACA,aAAO,OAAO,IAAI;AAClB,oBAAc,OAAO,IAAI;AAAA,IAC3B,UAAE;AACA,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF;AAEA,MAAI,KAAK;AACP,UAAM,WAAW,IAAI,KAAK,eAAe,QAAQ;AAAA,MAC/C,WAAW;AAAA,MACX,WAAW;AAAA,IACb,CAAC,EAAE,OAAO,IAAI,KAAK,IAAI,QAAQ,CAAC;AAChC,WACE,gBAAAC;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,UAAU;AAAA,QACV,MAAK;AAAA,QACL,aAAU;AAAA,QACV,WAAWC,IAAG,+CAA+C,SAAS;AAAA,QACrE,YAAE,QAAQ,QAAQ;AAAA;AAAA,IACrB;AAAA,EAEJ;AAEA,QAAM,UAAU,CAAC,UAA8B,OAAO,KAAK,IAAI,GAAG,EAAE,IAAI,KAAK,WAAW;AACxF,QAAM,aAAa,CAAC,UAClB,OAAO,KAAK,IACV,gBAAAD,KAAC,OAAE,IAAI,GAAG,EAAE,IAAI,KAAK,UAAU,WAAU,4BACtC,iBAAO,KAAK,GACf,IACE;AAEN,SACE,gBAAAL;AAAA,IAAC;AAAA;AAAA,MACC,YAAU;AAAA,MACV,UAAU;AAAA,MACV,gBAAgB;AAAA,MAChB,eAAe;AAAA,MACf,aAAU;AAAA,MACV,WAAWM,IAAG,aAAa,SAAS;AAAA,MAEpC;AAAA,wBAAAN,MAAC,SAAI,WAAU,aACb;AAAA,0BAAAK,KAAC,WAAM,SAAS,GAAG,EAAE,SAAS,WAAU,uBACrC,YAAE,kBACL;AAAA,UACA,gBAAAL;AAAA,YAAC;AAAA;AAAA,cACC,IAAI,GAAG,EAAE;AAAA,cACT,OAAO;AAAA,cACP,UAAU,CAAC,MAAM,eAAe,EAAE,OAAO,KAA6B;AAAA,cACtE,gBAAc,OAAO,cAAc,OAAO;AAAA,cAC1C,oBAAkB,QAAQ,aAAa;AAAA,cACvC,WAAWM,IAAG,aAAa,OAAO,cAAc,uBAAuB,eAAe;AAAA,cAEtF;AAAA,gCAAAD,KAAC,YAAO,OAAM,IAAI,YAAE,wBAAuB;AAAA,gBAC1C,aAAa,IAAI,CAAC,SACjB,gBAAAA,KAAC,YAAkB,OAAO,MACvB,YAAE,aAAa,IAAI,KADT,IAEb,CACD;AAAA;AAAA;AAAA,UACH;AAAA,UACC,WAAW,aAAa;AAAA,WAC3B;AAAA,QAEA,gBAAAL,MAAC,SAAI,WAAU,aACb;AAAA,0BAAAK,KAAC,WAAM,SAAS,GAAG,EAAE,SAAS,WAAU,uBACrC,YAAE,WACL;AAAA,UACA,gBAAAA;AAAA,YAAC;AAAA;AAAA,cACC,IAAI,GAAG,EAAE;AAAA,cACT,MAAK;AAAA,cACL,cAAa;AAAA,cACb,WAAW,0BAA0B;AAAA,cACrC,OAAO;AAAA,cACP,UAAU,CAAC,MAAM,QAAQ,EAAE,OAAO,KAAK;AAAA,cACvC,gBAAc,OAAO,OAAO,OAAO;AAAA,cACnC,oBAAkB,QAAQ,MAAM;AAAA,cAChC,WAAWC,IAAG,aAAa,OAAO,OAAO,uBAAuB,eAAe;AAAA;AAAA,UACjF;AAAA,UACC,WAAW,MAAM;AAAA,WACpB;AAAA,QAEA,gBAAAN,MAAC,SAAI,WAAU,aACb;AAAA,0BAAAK,KAAC,WAAM,SAAS,GAAG,EAAE,UAAU,WAAU,uBACtC,YAAE,YACL;AAAA,UACA,gBAAAA;AAAA,YAAC;AAAA;AAAA,cACC,IAAI,GAAG,EAAE;AAAA,cACT,MAAK;AAAA,cACL,cAAa;AAAA,cACb,WAAW,0BAA0B;AAAA,cACrC,OAAO;AAAA,cACP,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK;AAAA,cACxC,gBAAc,OAAO,QAAQ,OAAO;AAAA,cACpC,oBAAkB,QAAQ,OAAO;AAAA,cACjC,WAAWC,IAAG,aAAa,OAAO,QAAQ,uBAAuB,eAAe;AAAA;AAAA,UAClF;AAAA,UACC,WAAW,OAAO;AAAA,WACrB;AAAA,QAEA,gBAAAN,MAAC,SAAI,WAAU,aACb;AAAA,0BAAAK,KAAC,WAAM,SAAS,GAAG,EAAE,YAAY,WAAU,uBACxC,YAAE,cACL;AAAA,UACA,gBAAAA;AAAA,YAAC;AAAA;AAAA,cACC,IAAI,GAAG,EAAE;AAAA,cACT,MAAM;AAAA,cACN,WAAW,0BAA0B;AAAA,cACrC,OAAO;AAAA,cACP,UAAU,CAAC,MAAM,WAAW,EAAE,OAAO,KAAK;AAAA,cAC1C,aAAa,EAAE;AAAA,cACf,WAAWC,IAAG,aAAa,eAAe;AAAA;AAAA,UAC5C;AAAA,WACF;AAAA,QAEA,gBAAAN,MAAC,SAAI,WAAU,aACb;AAAA,0BAAAA,MAAC,SAAI,WAAU,0BACb;AAAA,4BAAAK;AAAA,cAAC;AAAA;AAAA,gBACC,IAAI,GAAG,EAAE;AAAA,gBACT,MAAK;AAAA,gBACL,SAAS;AAAA,gBACT,UAAU,CAAC,MAAM,WAAW,EAAE,OAAO,OAAO;AAAA,gBAC5C,gBAAc,OAAO,UAAU,OAAO;AAAA,gBACtC,oBAAkB,QAAQ,SAAS;AAAA,gBACnC,WAAU;AAAA;AAAA,YACZ;AAAA,YACA,gBAAAA,KAAC,WAAM,SAAS,GAAG,EAAE,YAAY,WAAU,8BACxC,YAAE,SACL;AAAA,aACF;AAAA,UACC,WAAW,SAAS;AAAA,WACvB;AAAA,QAEC,aACC,gBAAAA,KAAC,OAAE,MAAK,SAAQ,WAAU,4BACvB,qBACH;AAAA,QAGF,gBAAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,UAAU;AAAA,YACV,WAAU;AAAA,YAET,uBAAa,EAAE,aAAa,EAAE;AAAA;AAAA,QACjC;AAAA;AAAA;AAAA,EACF;AAEJ;","names":["cn","jsx","jsxs","DEFAULT_LABELS","cn","useState","cn","jsx","DEFAULT_LABELS","useEffect","useRef","useState","cn","jsx","jsxs","DEFAULT_LABELS","count","useCallback","useEffect","useRef","useState","cn","jsx","jsx","cn","jsx","jsxs","cn","jsx","jsxs","useRef","useState","useCallback","useEffect","cn","useId","useState","cn","cn","jsx","jsxs","useId","useState","cn","useEffect","useId","useRef","useState","cn","jsx","jsxs","jsxs","useId","useState","useRef","useEffect","jsx","cn"]}
package/dist/index.cjs CHANGED
@@ -14809,12 +14809,14 @@ var DEFAULT_LABELS3 = {
14809
14809
  freeShipping: "Env\xEDo gratis",
14810
14810
  shippingCalculated: "Se calcula en el pago",
14811
14811
  total: "Total",
14812
- cancelledNotice: "Pago cancelado: tu carrito sigue aqu\xED"
14812
+ cancelledNotice: "Pago cancelado: tu carrito sigue aqu\xED",
14813
+ staffDiscountApplied: "Descuento de personal aplicado"
14813
14814
  };
14814
14815
  function CartSummary({
14815
14816
  subtotal,
14816
14817
  discount,
14817
14818
  discountCode,
14819
+ autoDiscount,
14818
14820
  shipping,
14819
14821
  shippingEnabled,
14820
14822
  total,
@@ -14825,7 +14827,8 @@ function CartSummary({
14825
14827
  className
14826
14828
  }) {
14827
14829
  const t = { ...DEFAULT_LABELS3, ...labels };
14828
- const hasDiscount = Boolean(discount && discount > 0);
14830
+ const discountAmount = discount ?? autoDiscount?.amount;
14831
+ const hasDiscount = Boolean(discountAmount && discountAmount > 0);
14829
14832
  const noticeContent = notice === true ? t.cancelledNotice : notice;
14830
14833
  const hasNotice = noticeContent != null && noticeContent !== false;
14831
14834
  const breakdown = /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
@@ -14839,13 +14842,10 @@ function CartSummary({
14839
14842
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("dd", { children: formatPrice(subtotal, currencyCode, locale) })
14840
14843
  ] }),
14841
14844
  hasDiscount && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex justify-between text-green-600 dark:text-green-400", children: [
14842
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("dt", { children: [
14843
- t.discountPrefix,
14844
- discountCode ? ` (${discountCode})` : ""
14845
- ] }),
14845
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("dt", { children: autoDiscount ? t.staffDiscountApplied : `${t.discountPrefix}${discountCode ? ` (${discountCode})` : ""}` }),
14846
14846
  /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("dd", { children: [
14847
14847
  "-",
14848
- formatPrice(discount, currencyCode, locale)
14848
+ formatPrice(discountAmount, currencyCode, locale)
14849
14849
  ] })
14850
14850
  ] }),
14851
14851
  shippingEnabled && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex justify-between text-muted-foreground", children: [
@@ -15094,6 +15094,7 @@ var DEFAULT_LABELS7 = {
15094
15094
  applyingDiscount: "Aplicando\u2026",
15095
15095
  removeDiscount: "Quitar",
15096
15096
  discountAppliedSuffix: (amount) => `\u2014 ${amount} de descuento`,
15097
+ staffDiscountApplied: "Descuento de personal aplicado",
15097
15098
  submit: "Confirmar y pagar",
15098
15099
  submitting: "Procesando\u2026"
15099
15100
  };
@@ -15108,6 +15109,7 @@ function CheckoutForm({
15108
15109
  onApplyDiscount,
15109
15110
  onRemoveDiscount,
15110
15111
  appliedDiscount,
15112
+ autoDiscount,
15111
15113
  discountApplying,
15112
15114
  discountError,
15113
15115
  loginPrompt,
@@ -15202,7 +15204,18 @@ function CheckoutForm({
15202
15204
  generalError ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { role: "alert", className: "text-sm text-destructive", children: generalError }) : null,
15203
15205
  /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("section", { className: "space-y-3", children: [
15204
15206
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("h2", { className: "border-b border-border pb-2 text-lg font-semibold", children: t.discountTitle }),
15205
- appliedDiscount ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center justify-between rounded-lg border border-green-200 bg-green-50 px-4 py-3 dark:border-green-800 dark:bg-green-900/20", children: [
15207
+ autoDiscount ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
15208
+ "div",
15209
+ {
15210
+ className: "flex items-center gap-2 rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-700 dark:border-green-800 dark:bg-green-900/20 dark:text-green-400",
15211
+ "data-slot": "checkout-staff-discount",
15212
+ children: [
15213
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "font-semibold", children: t.staffDiscountApplied }),
15214
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { children: t.discountAppliedSuffix(formatPrice(autoDiscount.amount, currencyCode, locale)) })
15215
+ ]
15216
+ }
15217
+ ) : null,
15218
+ appliedDiscount && !autoDiscount ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center justify-between rounded-lg border border-green-200 bg-green-50 px-4 py-3 dark:border-green-800 dark:bg-green-900/20", children: [
15206
15219
  /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center gap-2 text-sm text-green-700 dark:text-green-400", children: [
15207
15220
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "font-semibold", children: appliedDiscount.code }),
15208
15221
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { children: t.discountAppliedSuffix(formatPrice(appliedDiscount.amount, currencyCode, locale)) })
@@ -15294,7 +15307,9 @@ var DEFAULT_LABELS9 = {
15294
15307
  pollingTitle: "Confirmando tu pedido\u2026",
15295
15308
  pollingDescription: "Estamos procesando el pago, un momento\u2026",
15296
15309
  foundTitle: "\xA1Pedido confirmado!",
15297
- foundDescription: "Recibir\xE1s un email de confirmaci\xF3n con todos los detalles.",
15310
+ // REB-1085: every buyer gets a customer account; the order email carries
15311
+ // the access link. Files are never served from this page.
15312
+ foundDescription: "Te hemos enviado un email con tu acceso.",
15298
15313
  timeoutTitle: "Pago recibido",
15299
15314
  timeoutDescription: "Tu pago se ha procesado. Recibir\xE1s un email de confirmaci\xF3n en breve.",
15300
15315
  orderNumberLabel: "Pedido confirmado",