@reblu/site-blocks 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +89 -0
- package/README.md +177 -0
- package/dist/client.cjs +198 -0
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +114 -1
- package/dist/client.d.ts +114 -1
- package/dist/client.js +196 -0
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +690 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +351 -2
- package/dist/index.d.ts +351 -2
- package/dist/index.js +679 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/client.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/hooks/use-collection.ts","../src/components/collection.tsx","../src/components/share-buttons.tsx","../src/lib/share-urls.ts"],"sourcesContent":["'use client'\n\n// `@reblu/site-blocks/client` — the client-only surface. Every export here needs\n// browser state or hooks, so the entry carries the `'use client'` directive and\n// each SITE consumes it from a Client Component boundary.\n\nexport {\n useCollection,\n type CollectionFilter,\n type UseCollectionOptions,\n type UseCollectionResult,\n} from './hooks/use-collection'\n\nexport { Collection } from './components/collection'\nexport type {\n CollectionProps,\n CollectionFacet,\n CollectionFacetOption,\n CollectionLabels,\n} from './components/collection'\n\nexport { ShareButtons } from './components/share-buttons'\nexport type { ShareButtonsProps, ShareButtonsLabels } from './components/share-buttons'\n","'use client'\n\nimport { useCallback, useMemo, useState } from 'react'\n\n/**\n * `useCollection` — the headless heart of `@reblu/site-blocks`.\n *\n * A framework-agnostic, presentation-free hook that turns an in-memory array into\n * a filtered / searched / sorted / paginated view. It backs BOTH the page index\n * and the blog index: the facets differ (category, tag, …), the machinery does\n * not. Callers own how items look (`Collection` or their own markup); this hook\n * only owns *which* items and in *what* order.\n *\n * All derivation is memoised and pure — comparators run over `toSorted`, never\n * mutating the caller's array.\n */\n\n/** A named facet: `key` identifies it, `predicate` decides membership for a value. */\nexport interface CollectionFilter<T> {\n key: string\n predicate: (item: T, value: string) => boolean\n}\n\nexport interface UseCollectionOptions<T> {\n items: T[]\n /** Page size. `0`/omitted → no pagination (one page with everything). */\n pageSize?: number\n initialPage?: number\n filters?: CollectionFilter<T>[]\n initialFilters?: Record<string, string | undefined>\n /** Free-text matcher; only applied when a non-empty term is set. */\n search?: (item: T, term: string) => boolean\n initialSearch?: string\n /** Named comparators, selected by `sort`. */\n sorts?: Record<string, (a: T, b: T) => number>\n initialSort?: string\n}\n\nexport interface UseCollectionResult<T> {\n /** Items on the current page (or all items when pagination is off). */\n items: T[]\n /** Full filtered + sorted set, before pagination. */\n filtered: T[]\n /** Size of `filtered`. */\n total: number\n page: number\n pageCount: number\n pageSize: number\n isEmpty: boolean\n activeFilters: Record<string, string | undefined>\n setFilter: (key: string, value: string | undefined) => void\n clearFilters: () => void\n search: string\n setSearch: (term: string) => void\n sort: string | undefined\n setSort: (key: string | undefined) => void\n setPage: (page: number) => void\n nextPage: () => void\n prevPage: () => void\n}\n\nexport function useCollection<T>(options: UseCollectionOptions<T>): UseCollectionResult<T> {\n const { items, pageSize = 0, filters = [], search: searchFn, sorts = {} } = options\n\n const [activeFilters, setActiveFilters] = useState<Record<string, string | undefined>>(\n () => options.initialFilters ?? {},\n )\n const [search, setSearchState] = useState(() => options.initialSearch ?? '')\n const [sort, setSortState] = useState<string | undefined>(() => options.initialSort)\n const [page, setPageState] = useState(() => options.initialPage ?? 1)\n\n const filtered = useMemo(() => {\n let next = items\n\n for (const { key, predicate } of filters) {\n const value = activeFilters[key]\n if (value != null && value !== '') {\n next = next.filter((item) => predicate(item, value))\n }\n }\n\n const term = search.trim()\n if (searchFn && term) {\n next = next.filter((item) => searchFn(item, term))\n }\n\n const comparator = sort ? sorts[sort] : undefined\n if (comparator) {\n next = next.slice().sort(comparator)\n }\n\n return next\n }, [items, filters, activeFilters, search, searchFn, sort, sorts])\n\n const total = filtered.length\n const pageCount = pageSize > 0 ? Math.max(1, Math.ceil(total / pageSize)) : 1\n // Derive the effective page so a shrinking result set never strands us past the\n // last page — state stays as-is, but what we render is always clamped.\n const currentPage = Math.min(Math.max(1, page), pageCount)\n\n const pageItems = useMemo(() => {\n if (pageSize <= 0) return filtered\n const start = (currentPage - 1) * pageSize\n return filtered.slice(start, start + pageSize)\n }, [filtered, pageSize, currentPage])\n\n // Any change to what we're viewing snaps back to the first page.\n const resetToFirstPage = useCallback(() => setPageState(1), [])\n\n const setFilter = useCallback(\n (key: string, value: string | undefined) => {\n setActiveFilters((prev) => ({ ...prev, [key]: value }))\n resetToFirstPage()\n },\n [resetToFirstPage],\n )\n\n const clearFilters = useCallback(() => {\n setActiveFilters({})\n resetToFirstPage()\n }, [resetToFirstPage])\n\n const setSearch = useCallback(\n (term: string) => {\n setSearchState(term)\n resetToFirstPage()\n },\n [resetToFirstPage],\n )\n\n const setSort = useCallback(\n (key: string | undefined) => {\n setSortState(key)\n resetToFirstPage()\n },\n [resetToFirstPage],\n )\n\n // Navigation clamps against the live page bounds so a shrinking result set or\n // an over-shooting jump can never strand the state past the last page.\n const setPage = useCallback(\n (next: number) => setPageState(Math.min(Math.max(1, next), pageCount)),\n [pageCount],\n )\n const nextPage = useCallback(\n () => setPageState(Math.min(currentPage + 1, pageCount)),\n [currentPage, pageCount],\n )\n const prevPage = useCallback(() => setPageState(Math.max(1, currentPage - 1)), [currentPage])\n\n return {\n items: pageItems,\n filtered,\n total,\n page: currentPage,\n pageCount,\n pageSize,\n isEmpty: total === 0,\n activeFilters,\n setFilter,\n clearFilters,\n search,\n setSearch,\n sort,\n setSort,\n setPage,\n nextPage,\n prevPage,\n }\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '@reblu/site-ui/utils'\n\nimport { useCollection, type CollectionFilter } from '../hooks/use-collection'\n\n/**\n * `Collection` — token-driven presentational grid over {@link useCollection}.\n *\n * The headless hook owns filtering/sorting/pagination; this component owns the\n * chrome: a facet chip bar, the item grid (via a `renderItem` prop), and simple\n * pagination. It carries no brand aesthetic — colors come from tokens; a facet\n * option may carry a `color` (tenant hex) which is applied through a CSS custom\n * property, never baked into a class name.\n *\n * Serves both the page index and the blog index; only the facets differ.\n */\nexport interface CollectionFacetOption {\n value: string\n label: string\n /** Optional tenant color (hex). Applied via a CSS var, not a hardcoded class. */\n color?: string\n}\n\nexport interface CollectionFacet {\n key: string\n label: string\n options: CollectionFacetOption[]\n /** Label for the \"clear this facet\" chip. Defaults to \"All\". */\n allLabel?: string\n}\n\nexport interface CollectionLabels {\n previous: string\n next: string\n page: (page: number, pageCount: number) => string\n}\n\nconst DEFAULT_LABELS: CollectionLabels = {\n previous: 'Previous',\n next: 'Next',\n page: (page, pageCount) => `Page ${page} of ${pageCount}`,\n}\n\nexport interface CollectionProps<T> {\n items: T[]\n renderItem: (item: T) => React.ReactNode\n getKey: (item: T) => string\n filters?: CollectionFilter<T>[]\n facets?: CollectionFacet[]\n pageSize?: number\n sorts?: Record<string, (a: T, b: T) => number>\n initialFilters?: Record<string, string | undefined>\n initialSort?: string\n emptyLabel?: string\n className?: string\n /** Tailwind classes for the grid container. */\n gridClassName?: string\n labels?: Partial<CollectionLabels>\n}\n\nexport function Collection<T>({\n items,\n renderItem,\n getKey,\n filters,\n facets = [],\n pageSize = 0,\n sorts,\n initialFilters,\n initialSort,\n emptyLabel = 'No results',\n className,\n gridClassName = 'grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3',\n labels,\n}: CollectionProps<T>) {\n const t = { ...DEFAULT_LABELS, ...labels }\n const collection = useCollection<T>({\n items,\n filters,\n pageSize,\n sorts,\n initialFilters,\n initialSort,\n })\n\n return (\n <div className={cn('space-y-8', className)}>\n {facets.length > 0 && (\n <div className='space-y-4'>\n {facets.map((facet) => {\n const active = collection.activeFilters[facet.key]\n return (\n <div\n key={facet.key}\n role='group'\n aria-label={facet.label}\n className='flex flex-wrap gap-2'\n >\n <FacetChip\n label={facet.allLabel ?? 'All'}\n pressed={active == null || active === ''}\n onClick={() => collection.setFilter(facet.key, undefined)}\n />\n {facet.options.map((opt) => (\n <FacetChip\n key={opt.value}\n label={opt.label}\n color={opt.color}\n pressed={active === opt.value}\n onClick={() => collection.setFilter(facet.key, opt.value)}\n />\n ))}\n </div>\n )\n })}\n </div>\n )}\n\n {collection.isEmpty ? (\n <p className='py-16 text-center text-muted-foreground'>{emptyLabel}</p>\n ) : (\n <ul className={cn('list-none p-0', gridClassName)}>\n {collection.items.map((item) => (\n <li key={getKey(item)}>{renderItem(item)}</li>\n ))}\n </ul>\n )}\n\n {collection.pageCount > 1 && (\n <nav aria-label='Pagination' className='flex items-center justify-center gap-4'>\n <button\n type='button'\n onClick={collection.prevPage}\n disabled={collection.page <= 1}\n className={PAGE_BTN_CLASS}\n >\n {t.previous}\n </button>\n <span aria-current='page' className='text-sm text-muted-foreground'>\n {t.page(collection.page, collection.pageCount)}\n </span>\n <button\n type='button'\n onClick={collection.nextPage}\n disabled={collection.page >= collection.pageCount}\n className={PAGE_BTN_CLASS}\n >\n {t.next}\n </button>\n </nav>\n )}\n </div>\n )\n}\n\nconst PAGE_BTN_CLASS =\n 'inline-flex h-9 items-center rounded-md border border-border px-4 text-sm font-medium transition-colors hover:bg-muted disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'\n\ninterface FacetChipProps {\n label: string\n pressed: boolean\n color?: string\n onClick: () => void\n}\n\nfunction FacetChip({ label, pressed, color, onClick }: FacetChipProps) {\n return (\n <button\n type='button'\n aria-pressed={pressed}\n onClick={onClick}\n // The tenant color rides in a CSS var; the class picks it up when pressed,\n // so no hex string is ever baked into a Tailwind class.\n style={color ? ({ '--facet-color': color } as React.CSSProperties) : undefined}\n className={cn(\n 'inline-flex items-center rounded-full border px-3 py-1 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n pressed\n ? color\n ? 'border-transparent bg-[var(--facet-color)] text-white'\n : 'border-transparent bg-primary text-primary-foreground'\n : 'border-border text-muted-foreground hover:bg-muted hover:text-foreground',\n )}\n >\n {label}\n </button>\n )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '@reblu/site-ui/utils'\n\nimport { buildShareUrls } from '../lib/share-urls'\n\n/**\n * `ShareButtons` — social share row for a blog post (LinkedIn / X / Facebook /\n * WhatsApp) plus a copy-to-clipboard action. Token-driven and icon-only for the\n * providers; the copy action shows a text confirmation.\n *\n * Copy defaults to English (distributable default). A Spanish-or-other SITE can\n * override every visible string via `labels` without forking the component.\n */\nexport interface ShareButtonsLabels {\n share: string\n copy: string\n copied: string\n linkedin: string\n x: string\n facebook: string\n whatsapp: string\n copyStatus: string\n}\n\nconst DEFAULT_LABELS: ShareButtonsLabels = {\n share: 'Share',\n copy: 'Copy link',\n copied: 'Link copied',\n linkedin: 'Share on LinkedIn',\n x: 'Share on X',\n facebook: 'Share on Facebook',\n whatsapp: 'Share on WhatsApp',\n copyStatus: 'Link copied to clipboard',\n}\n\nexport interface ShareButtonsProps {\n /** Canonical, absolute URL of the post being shared. */\n url: string\n /** Post title, used where the provider supports pre-filled copy. */\n title: string\n className?: string\n labels?: Partial<ShareButtonsLabels>\n}\n\nconst LINK_CLASS =\n 'inline-flex h-9 w-9 items-center justify-center rounded-full border border-border text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'\n\nconst ICON = 'h-4 w-4'\n\nfunction LinkedInIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='currentColor' aria-hidden='true' focusable='false'>\n <path d='M19 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2zM8.34 18.34V10.9H5.9v7.44zM7.12 9.82a1.42 1.42 0 1 0 0-2.84 1.42 1.42 0 0 0 0 2.84m11.22 8.52v-4.08c0-2.18-1.16-3.19-2.72-3.19a2.35 2.35 0 0 0-2.13 1.17v-1h-2.43v7.1h2.43v-3.93c0-1 .2-2 1.46-2s1.28 1.16 1.28 2.06v3.87z' />\n </svg>\n )\n}\n\nfunction XIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='currentColor' aria-hidden='true' focusable='false'>\n <path d='M18.244 2.25h3.308l-7.227 8.26 8.502 11.24h-6.66l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z' />\n </svg>\n )\n}\n\nfunction FacebookIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='currentColor' aria-hidden='true' focusable='false'>\n <path d='M22 12a10 10 0 1 0-11.56 9.88v-6.99H7.9V12h2.54V9.8c0-2.5 1.49-3.89 3.78-3.89 1.09 0 2.24.2 2.24.2v2.46h-1.26c-1.24 0-1.63.77-1.63 1.56V12h2.78l-.44 2.89h-2.34v6.99A10 10 0 0 0 22 12' />\n </svg>\n )\n}\n\nfunction WhatsAppIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='currentColor' aria-hidden='true' focusable='false'>\n <path d='M12.04 2C6.58 2 2.13 6.45 2.13 11.91c0 1.75.46 3.45 1.32 4.95L2 22l5.25-1.38a9.9 9.9 0 0 0 4.79 1.22h.01c5.46 0 9.91-4.45 9.91-9.91 0-2.65-1.03-5.14-2.9-7.01A9.82 9.82 0 0 0 12.04 2m0 1.67c2.2 0 4.27.86 5.82 2.42a8.19 8.19 0 0 1 2.42 5.82c0 4.54-3.7 8.24-8.25 8.24a8.2 8.2 0 0 1-4.2-1.15l-.3-.18-3.12.82.83-3.04-.2-.31a8.2 8.2 0 0 1-1.26-4.38c0-4.54 3.7-8.24 8.25-8.24m4.52 10.36c-.06-.1-.22-.16-.46-.28s-1.46-.72-1.68-.8-.39-.12-.55.12-.63.8-.77.96-.28.18-.52.06a6.7 6.7 0 0 1-1.98-1.22 7.4 7.4 0 0 1-1.37-1.7c-.14-.24-.01-.37.11-.49.11-.11.24-.28.37-.42.12-.14.16-.24.24-.4s.04-.3-.02-.42-.55-1.32-.75-1.81c-.2-.48-.4-.41-.55-.42h-.47a.9.9 0 0 0-.65.3c-.22.24-.85.83-.85 2.02s.87 2.35 1 2.51c.12.16 1.71 2.61 4.14 3.66.58.25 1.03.4 1.38.51.58.19 1.11.16 1.53.1.47-.07 1.46-.6 1.66-1.17.21-.58.21-1.07.15-1.17' />\n </svg>\n )\n}\n\nfunction CopyIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='none' stroke='currentColor' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round' aria-hidden='true' focusable='false'>\n <rect x='9' y='9' width='13' height='13' rx='2' ry='2' />\n <path d='M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1' />\n </svg>\n )\n}\n\nfunction CheckIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='none' stroke='currentColor' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round' aria-hidden='true' focusable='false'>\n <polyline points='20 6 9 17 4 12' />\n </svg>\n )\n}\n\nexport function ShareButtons({ url, title, className, labels }: ShareButtonsProps) {\n const t = { ...DEFAULT_LABELS, ...labels }\n const [copied, setCopied] = React.useState(false)\n const share = buildShareUrls(url, title)\n\n async function handleCopy() {\n try {\n await navigator.clipboard.writeText(url)\n setCopied(true)\n window.setTimeout(() => setCopied(false), 2000)\n } catch {\n // Clipboard can be unavailable (permissions, insecure context); fail quietly.\n }\n }\n\n return (\n <div className={cn('flex items-center gap-2', className)} role='group' aria-label={t.share}>\n <span className='mr-1 text-sm font-medium text-muted-foreground'>{t.share}</span>\n\n <a href={share.linkedin} target='_blank' rel='noopener noreferrer' className={LINK_CLASS} aria-label={t.linkedin}>\n <LinkedInIcon />\n </a>\n <a href={share.x} target='_blank' rel='noopener noreferrer' className={LINK_CLASS} aria-label={t.x}>\n <XIcon />\n </a>\n <a href={share.facebook} target='_blank' rel='noopener noreferrer' className={LINK_CLASS} aria-label={t.facebook}>\n <FacebookIcon />\n </a>\n <a href={share.whatsapp} target='_blank' rel='noopener noreferrer' className={LINK_CLASS} aria-label={t.whatsapp}>\n <WhatsAppIcon />\n </a>\n\n <button\n type='button'\n onClick={handleCopy}\n className={cn(LINK_CLASS, 'w-auto gap-2 px-3', copied && 'border-primary text-primary')}\n aria-label={t.copy}\n >\n {copied ? <CheckIcon /> : <CopyIcon />}\n <span className='text-sm'>{copied ? t.copied : t.copy}</span>\n </button>\n\n {/* Polite live region so screen readers announce the copy confirmation. */}\n <span role='status' aria-live='polite' className='sr-only'>\n {copied ? t.copyStatus : ''}\n </span>\n </div>\n )\n}\n","/**\n * Pure builders for the social-share targets used by `ShareButtons`.\n *\n * Framework-free so the URL construction is unit-testable without rendering.\n * Every provider gets the canonical post URL; providers that support custom copy\n * (X, WhatsApp) also receive the post title.\n */\nexport interface ShareUrls {\n linkedin: string\n x: string\n facebook: string\n whatsapp: string\n}\n\nexport function buildShareUrls(url: string, title: string): ShareUrls {\n const encodedUrl = encodeURIComponent(url)\n const encodedTitle = encodeURIComponent(title)\n\n return {\n // LinkedIn derives title/description from the target page's Open Graph tags,\n // so it only takes the URL.\n linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`,\n // The intent endpoint keeps the /intent/tweet path under the x.com domain.\n x: `https://x.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}`,\n facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`,\n whatsapp: `https://wa.me/?text=${encodeURIComponent(`${title} ${url}`)}`,\n }\n}\n"],"mappings":"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","names":["import_utils","import_jsx_runtime","DEFAULT_LABELS"]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/hooks/use-collection.ts","../src/components/collection.tsx","../src/components/share-buttons.tsx","../src/lib/share-urls.ts","../src/components/add-to-cart-button.tsx","../src/components/cart-icon.tsx"],"sourcesContent":["'use client'\n\n// `@reblu/site-blocks/client` — the client-only surface. Every export here needs\n// browser state or hooks, so the entry carries the `'use client'` directive and\n// each SITE consumes it from a Client Component boundary.\n\nexport {\n useCollection,\n type CollectionFilter,\n type UseCollectionOptions,\n type UseCollectionResult,\n} from './hooks/use-collection'\n\nexport { Collection } from './components/collection'\nexport type {\n CollectionProps,\n CollectionFacet,\n CollectionFacetOption,\n CollectionLabels,\n} from './components/collection'\n\nexport { ShareButtons } from './components/share-buttons'\nexport type { ShareButtonsProps, ShareButtonsLabels } from './components/share-buttons'\n\n// ── Cart feedback (REB-837) ──────────────────────────────────────────────────\n// `AddToCartButton`/`CartIcon` moved here from the RSC-safe root: both now\n// carry real internal state (confirmed/error timer, bump-on-add guard).\nexport {\n AddToCartButton,\n type AddToCartButtonProps,\n type AddToCartButtonLabels,\n type AddToCartButtonStatus,\n} from './components/add-to-cart-button'\nexport { CartIcon, type CartIconProps, type CartIconLabels } from './components/cart-icon'\n","'use client'\n\nimport { useCallback, useMemo, useState } from 'react'\n\n/**\n * `useCollection` — the headless heart of `@reblu/site-blocks`.\n *\n * A framework-agnostic, presentation-free hook that turns an in-memory array into\n * a filtered / searched / sorted / paginated view. It backs BOTH the page index\n * and the blog index: the facets differ (category, tag, …), the machinery does\n * not. Callers own how items look (`Collection` or their own markup); this hook\n * only owns *which* items and in *what* order.\n *\n * All derivation is memoised and pure — comparators run over `toSorted`, never\n * mutating the caller's array.\n */\n\n/** A named facet: `key` identifies it, `predicate` decides membership for a value. */\nexport interface CollectionFilter<T> {\n key: string\n predicate: (item: T, value: string) => boolean\n}\n\nexport interface UseCollectionOptions<T> {\n items: T[]\n /** Page size. `0`/omitted → no pagination (one page with everything). */\n pageSize?: number\n initialPage?: number\n filters?: CollectionFilter<T>[]\n initialFilters?: Record<string, string | undefined>\n /** Free-text matcher; only applied when a non-empty term is set. */\n search?: (item: T, term: string) => boolean\n initialSearch?: string\n /** Named comparators, selected by `sort`. */\n sorts?: Record<string, (a: T, b: T) => number>\n initialSort?: string\n}\n\nexport interface UseCollectionResult<T> {\n /** Items on the current page (or all items when pagination is off). */\n items: T[]\n /** Full filtered + sorted set, before pagination. */\n filtered: T[]\n /** Size of `filtered`. */\n total: number\n page: number\n pageCount: number\n pageSize: number\n isEmpty: boolean\n activeFilters: Record<string, string | undefined>\n setFilter: (key: string, value: string | undefined) => void\n clearFilters: () => void\n search: string\n setSearch: (term: string) => void\n sort: string | undefined\n setSort: (key: string | undefined) => void\n setPage: (page: number) => void\n nextPage: () => void\n prevPage: () => void\n}\n\nexport function useCollection<T>(options: UseCollectionOptions<T>): UseCollectionResult<T> {\n const { items, pageSize = 0, filters = [], search: searchFn, sorts = {} } = options\n\n const [activeFilters, setActiveFilters] = useState<Record<string, string | undefined>>(\n () => options.initialFilters ?? {},\n )\n const [search, setSearchState] = useState(() => options.initialSearch ?? '')\n const [sort, setSortState] = useState<string | undefined>(() => options.initialSort)\n const [page, setPageState] = useState(() => options.initialPage ?? 1)\n\n const filtered = useMemo(() => {\n let next = items\n\n for (const { key, predicate } of filters) {\n const value = activeFilters[key]\n if (value != null && value !== '') {\n next = next.filter((item) => predicate(item, value))\n }\n }\n\n const term = search.trim()\n if (searchFn && term) {\n next = next.filter((item) => searchFn(item, term))\n }\n\n const comparator = sort ? sorts[sort] : undefined\n if (comparator) {\n next = next.slice().sort(comparator)\n }\n\n return next\n }, [items, filters, activeFilters, search, searchFn, sort, sorts])\n\n const total = filtered.length\n const pageCount = pageSize > 0 ? Math.max(1, Math.ceil(total / pageSize)) : 1\n // Derive the effective page so a shrinking result set never strands us past the\n // last page — state stays as-is, but what we render is always clamped.\n const currentPage = Math.min(Math.max(1, page), pageCount)\n\n const pageItems = useMemo(() => {\n if (pageSize <= 0) return filtered\n const start = (currentPage - 1) * pageSize\n return filtered.slice(start, start + pageSize)\n }, [filtered, pageSize, currentPage])\n\n // Any change to what we're viewing snaps back to the first page.\n const resetToFirstPage = useCallback(() => setPageState(1), [])\n\n const setFilter = useCallback(\n (key: string, value: string | undefined) => {\n setActiveFilters((prev) => ({ ...prev, [key]: value }))\n resetToFirstPage()\n },\n [resetToFirstPage],\n )\n\n const clearFilters = useCallback(() => {\n setActiveFilters({})\n resetToFirstPage()\n }, [resetToFirstPage])\n\n const setSearch = useCallback(\n (term: string) => {\n setSearchState(term)\n resetToFirstPage()\n },\n [resetToFirstPage],\n )\n\n const setSort = useCallback(\n (key: string | undefined) => {\n setSortState(key)\n resetToFirstPage()\n },\n [resetToFirstPage],\n )\n\n // Navigation clamps against the live page bounds so a shrinking result set or\n // an over-shooting jump can never strand the state past the last page.\n const setPage = useCallback(\n (next: number) => setPageState(Math.min(Math.max(1, next), pageCount)),\n [pageCount],\n )\n const nextPage = useCallback(\n () => setPageState(Math.min(currentPage + 1, pageCount)),\n [currentPage, pageCount],\n )\n const prevPage = useCallback(() => setPageState(Math.max(1, currentPage - 1)), [currentPage])\n\n return {\n items: pageItems,\n filtered,\n total,\n page: currentPage,\n pageCount,\n pageSize,\n isEmpty: total === 0,\n activeFilters,\n setFilter,\n clearFilters,\n search,\n setSearch,\n sort,\n setSort,\n setPage,\n nextPage,\n prevPage,\n }\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '@reblu/site-ui/utils'\n\nimport { useCollection, type CollectionFilter } from '../hooks/use-collection'\n\n/**\n * `Collection` — token-driven presentational grid over {@link useCollection}.\n *\n * The headless hook owns filtering/sorting/pagination; this component owns the\n * chrome: a facet chip bar, the item grid (via a `renderItem` prop), and simple\n * pagination. It carries no brand aesthetic — colors come from tokens; a facet\n * option may carry a `color` (tenant hex) which is applied through a CSS custom\n * property, never baked into a class name.\n *\n * Serves both the page index and the blog index; only the facets differ.\n */\nexport interface CollectionFacetOption {\n value: string\n label: string\n /** Optional tenant color (hex). Applied via a CSS var, not a hardcoded class. */\n color?: string\n}\n\nexport interface CollectionFacet {\n key: string\n label: string\n options: CollectionFacetOption[]\n /** Label for the \"clear this facet\" chip. Defaults to \"All\". */\n allLabel?: string\n}\n\nexport interface CollectionLabels {\n previous: string\n next: string\n page: (page: number, pageCount: number) => string\n}\n\nconst DEFAULT_LABELS: CollectionLabels = {\n previous: 'Previous',\n next: 'Next',\n page: (page, pageCount) => `Page ${page} of ${pageCount}`,\n}\n\nexport interface CollectionProps<T> {\n items: T[]\n renderItem: (item: T) => React.ReactNode\n getKey: (item: T) => string\n filters?: CollectionFilter<T>[]\n facets?: CollectionFacet[]\n pageSize?: number\n sorts?: Record<string, (a: T, b: T) => number>\n initialFilters?: Record<string, string | undefined>\n initialSort?: string\n emptyLabel?: string\n className?: string\n /** Tailwind classes for the grid container. */\n gridClassName?: string\n labels?: Partial<CollectionLabels>\n}\n\nexport function Collection<T>({\n items,\n renderItem,\n getKey,\n filters,\n facets = [],\n pageSize = 0,\n sorts,\n initialFilters,\n initialSort,\n emptyLabel = 'No results',\n className,\n gridClassName = 'grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3',\n labels,\n}: CollectionProps<T>) {\n const t = { ...DEFAULT_LABELS, ...labels }\n const collection = useCollection<T>({\n items,\n filters,\n pageSize,\n sorts,\n initialFilters,\n initialSort,\n })\n\n return (\n <div className={cn('space-y-8', className)}>\n {facets.length > 0 && (\n <div className='space-y-4'>\n {facets.map((facet) => {\n const active = collection.activeFilters[facet.key]\n return (\n <div\n key={facet.key}\n role='group'\n aria-label={facet.label}\n className='flex flex-wrap gap-2'\n >\n <FacetChip\n label={facet.allLabel ?? 'All'}\n pressed={active == null || active === ''}\n onClick={() => collection.setFilter(facet.key, undefined)}\n />\n {facet.options.map((opt) => (\n <FacetChip\n key={opt.value}\n label={opt.label}\n color={opt.color}\n pressed={active === opt.value}\n onClick={() => collection.setFilter(facet.key, opt.value)}\n />\n ))}\n </div>\n )\n })}\n </div>\n )}\n\n {collection.isEmpty ? (\n <p className='py-16 text-center text-muted-foreground'>{emptyLabel}</p>\n ) : (\n <ul className={cn('list-none p-0', gridClassName)}>\n {collection.items.map((item) => (\n <li key={getKey(item)}>{renderItem(item)}</li>\n ))}\n </ul>\n )}\n\n {collection.pageCount > 1 && (\n <nav aria-label='Pagination' className='flex items-center justify-center gap-4'>\n <button\n type='button'\n onClick={collection.prevPage}\n disabled={collection.page <= 1}\n className={PAGE_BTN_CLASS}\n >\n {t.previous}\n </button>\n <span aria-current='page' className='text-sm text-muted-foreground'>\n {t.page(collection.page, collection.pageCount)}\n </span>\n <button\n type='button'\n onClick={collection.nextPage}\n disabled={collection.page >= collection.pageCount}\n className={PAGE_BTN_CLASS}\n >\n {t.next}\n </button>\n </nav>\n )}\n </div>\n )\n}\n\nconst PAGE_BTN_CLASS =\n 'inline-flex h-9 items-center rounded-md border border-border px-4 text-sm font-medium transition-colors hover:bg-muted disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'\n\ninterface FacetChipProps {\n label: string\n pressed: boolean\n color?: string\n onClick: () => void\n}\n\nfunction FacetChip({ label, pressed, color, onClick }: FacetChipProps) {\n return (\n <button\n type='button'\n aria-pressed={pressed}\n onClick={onClick}\n // The tenant color rides in a CSS var; the class picks it up when pressed,\n // so no hex string is ever baked into a Tailwind class.\n style={color ? ({ '--facet-color': color } as React.CSSProperties) : undefined}\n className={cn(\n 'inline-flex items-center rounded-full border px-3 py-1 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n pressed\n ? color\n ? 'border-transparent bg-[var(--facet-color)] text-white'\n : 'border-transparent bg-primary text-primary-foreground'\n : 'border-border text-muted-foreground hover:bg-muted hover:text-foreground',\n )}\n >\n {label}\n </button>\n )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '@reblu/site-ui/utils'\n\nimport { buildShareUrls } from '../lib/share-urls'\n\n/**\n * `ShareButtons` — social share row for a blog post (LinkedIn / X / Facebook /\n * WhatsApp) plus a copy-to-clipboard action. Token-driven and icon-only for the\n * providers; the copy action shows a text confirmation.\n *\n * Copy defaults to English (distributable default). A Spanish-or-other SITE can\n * override every visible string via `labels` without forking the component.\n */\nexport interface ShareButtonsLabels {\n share: string\n copy: string\n copied: string\n linkedin: string\n x: string\n facebook: string\n whatsapp: string\n copyStatus: string\n}\n\nconst DEFAULT_LABELS: ShareButtonsLabels = {\n share: 'Share',\n copy: 'Copy link',\n copied: 'Link copied',\n linkedin: 'Share on LinkedIn',\n x: 'Share on X',\n facebook: 'Share on Facebook',\n whatsapp: 'Share on WhatsApp',\n copyStatus: 'Link copied to clipboard',\n}\n\nexport interface ShareButtonsProps {\n /** Canonical, absolute URL of the post being shared. */\n url: string\n /** Post title, used where the provider supports pre-filled copy. */\n title: string\n className?: string\n labels?: Partial<ShareButtonsLabels>\n}\n\nconst LINK_CLASS =\n 'inline-flex h-9 w-9 items-center justify-center rounded-full border border-border text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'\n\nconst ICON = 'h-4 w-4'\n\nfunction LinkedInIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='currentColor' aria-hidden='true' focusable='false'>\n <path d='M19 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2zM8.34 18.34V10.9H5.9v7.44zM7.12 9.82a1.42 1.42 0 1 0 0-2.84 1.42 1.42 0 0 0 0 2.84m11.22 8.52v-4.08c0-2.18-1.16-3.19-2.72-3.19a2.35 2.35 0 0 0-2.13 1.17v-1h-2.43v7.1h2.43v-3.93c0-1 .2-2 1.46-2s1.28 1.16 1.28 2.06v3.87z' />\n </svg>\n )\n}\n\nfunction XIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='currentColor' aria-hidden='true' focusable='false'>\n <path d='M18.244 2.25h3.308l-7.227 8.26 8.502 11.24h-6.66l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z' />\n </svg>\n )\n}\n\nfunction FacebookIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='currentColor' aria-hidden='true' focusable='false'>\n <path d='M22 12a10 10 0 1 0-11.56 9.88v-6.99H7.9V12h2.54V9.8c0-2.5 1.49-3.89 3.78-3.89 1.09 0 2.24.2 2.24.2v2.46h-1.26c-1.24 0-1.63.77-1.63 1.56V12h2.78l-.44 2.89h-2.34v6.99A10 10 0 0 0 22 12' />\n </svg>\n )\n}\n\nfunction WhatsAppIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='currentColor' aria-hidden='true' focusable='false'>\n <path d='M12.04 2C6.58 2 2.13 6.45 2.13 11.91c0 1.75.46 3.45 1.32 4.95L2 22l5.25-1.38a9.9 9.9 0 0 0 4.79 1.22h.01c5.46 0 9.91-4.45 9.91-9.91 0-2.65-1.03-5.14-2.9-7.01A9.82 9.82 0 0 0 12.04 2m0 1.67c2.2 0 4.27.86 5.82 2.42a8.19 8.19 0 0 1 2.42 5.82c0 4.54-3.7 8.24-8.25 8.24a8.2 8.2 0 0 1-4.2-1.15l-.3-.18-3.12.82.83-3.04-.2-.31a8.2 8.2 0 0 1-1.26-4.38c0-4.54 3.7-8.24 8.25-8.24m4.52 10.36c-.06-.1-.22-.16-.46-.28s-1.46-.72-1.68-.8-.39-.12-.55.12-.63.8-.77.96-.28.18-.52.06a6.7 6.7 0 0 1-1.98-1.22 7.4 7.4 0 0 1-1.37-1.7c-.14-.24-.01-.37.11-.49.11-.11.24-.28.37-.42.12-.14.16-.24.24-.4s.04-.3-.02-.42-.55-1.32-.75-1.81c-.2-.48-.4-.41-.55-.42h-.47a.9.9 0 0 0-.65.3c-.22.24-.85.83-.85 2.02s.87 2.35 1 2.51c.12.16 1.71 2.61 4.14 3.66.58.25 1.03.4 1.38.51.58.19 1.11.16 1.53.1.47-.07 1.46-.6 1.66-1.17.21-.58.21-1.07.15-1.17' />\n </svg>\n )\n}\n\nfunction CopyIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='none' stroke='currentColor' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round' aria-hidden='true' focusable='false'>\n <rect x='9' y='9' width='13' height='13' rx='2' ry='2' />\n <path d='M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1' />\n </svg>\n )\n}\n\nfunction CheckIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='none' stroke='currentColor' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round' aria-hidden='true' focusable='false'>\n <polyline points='20 6 9 17 4 12' />\n </svg>\n )\n}\n\nexport function ShareButtons({ url, title, className, labels }: ShareButtonsProps) {\n const t = { ...DEFAULT_LABELS, ...labels }\n const [copied, setCopied] = React.useState(false)\n const share = buildShareUrls(url, title)\n\n async function handleCopy() {\n try {\n await navigator.clipboard.writeText(url)\n setCopied(true)\n window.setTimeout(() => setCopied(false), 2000)\n } catch {\n // Clipboard can be unavailable (permissions, insecure context); fail quietly.\n }\n }\n\n return (\n <div className={cn('flex items-center gap-2', className)} role='group' aria-label={t.share}>\n <span className='mr-1 text-sm font-medium text-muted-foreground'>{t.share}</span>\n\n <a href={share.linkedin} target='_blank' rel='noopener noreferrer' className={LINK_CLASS} aria-label={t.linkedin}>\n <LinkedInIcon />\n </a>\n <a href={share.x} target='_blank' rel='noopener noreferrer' className={LINK_CLASS} aria-label={t.x}>\n <XIcon />\n </a>\n <a href={share.facebook} target='_blank' rel='noopener noreferrer' className={LINK_CLASS} aria-label={t.facebook}>\n <FacebookIcon />\n </a>\n <a href={share.whatsapp} target='_blank' rel='noopener noreferrer' className={LINK_CLASS} aria-label={t.whatsapp}>\n <WhatsAppIcon />\n </a>\n\n <button\n type='button'\n onClick={handleCopy}\n className={cn(LINK_CLASS, 'w-auto gap-2 px-3', copied && 'border-primary text-primary')}\n aria-label={t.copy}\n >\n {copied ? <CheckIcon /> : <CopyIcon />}\n <span className='text-sm'>{copied ? t.copied : t.copy}</span>\n </button>\n\n {/* Polite live region so screen readers announce the copy confirmation. */}\n <span role='status' aria-live='polite' className='sr-only'>\n {copied ? t.copyStatus : ''}\n </span>\n </div>\n )\n}\n","/**\n * Pure builders for the social-share targets used by `ShareButtons`.\n *\n * Framework-free so the URL construction is unit-testable without rendering.\n * Every provider gets the canonical post URL; providers that support custom copy\n * (X, WhatsApp) also receive the post title.\n */\nexport interface ShareUrls {\n linkedin: string\n x: string\n facebook: string\n whatsapp: string\n}\n\nexport function buildShareUrls(url: string, title: string): ShareUrls {\n const encodedUrl = encodeURIComponent(url)\n const encodedTitle = encodeURIComponent(title)\n\n return {\n // LinkedIn derives title/description from the target page's Open Graph tags,\n // so it only takes the URL.\n linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`,\n // The intent endpoint keeps the /intent/tweet path under the x.com domain.\n x: `https://x.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}`,\n facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`,\n whatsapp: `https://wa.me/?text=${encodeURIComponent(`${title} ${url}`)}`,\n }\n}\n","'use client'\n\nimport { useEffect, useRef, useState } from 'react'\n\nimport { cn } from '@reblu/site-ui/utils'\n\n/**\n * `AddToCartButton` — add-to-cart CTA with a built-in confirmed/error state\n * (REB-837). Moved to the `./client` entry (from the RSC-safe root) because\n * the confirmed-state timer and the `error` prop watcher genuinely need\n * `useState`/`useEffect` — see `client.ts`'s own leading `'use client'`\n * directive (R4: tsup only preserves a directive declared by the entry file\n * itself, not one merely re-exported from a child module).\n *\n * Previously (REB-829) this component had no internal state at all — it was\n * a pure `onAddToCart?: () => void` button exported from the RSC-safe root.\n * A SITE importing `AddToCartButton` from `@reblu/site-blocks` (the root\n * entry) must now import it from `@reblu/site-blocks/client` instead.\n */\nexport interface AddToCartButtonLabels {\n add: string\n adding: string\n outOfStock: string\n /** Shown for ~`successDurationMs` after a successful add. */\n added: string\n /** Fallback shown when `error` is `true` (not a string). */\n error: string\n}\n\nconst DEFAULT_LABELS: AddToCartButtonLabels = {\n add: 'Añadir al carrito',\n adding: 'Añadiendo…',\n outOfStock: 'Agotado',\n added: 'Añadido ✓',\n error: 'No se pudo añadir',\n}\n\nexport type AddToCartButtonStatus = 'idle' | 'success' | 'error'\n\n/**\n * Structural subset of `@reblu/site-next/cart`'s `AddItemOutcome` (REB-837\n * review fix) — `site-blocks` never depends on `site-next`, so this is\n * duplicated shape, not an import; a real `AddItemOutcome` (which also\n * carries `quantity`) satisfies this type as-is.\n */\nexport interface AddToCartOutcome {\n status: 'added' | 'clamped' | 'refused'\n}\n\nexport interface AddToCartButtonProps<TItem = unknown> {\n /**\n * Called on click. MAY return the outcome of the add (synchronously or\n * via a `Promise`) so the button can decide success vs error from the\n * RESULT instead of only from the externally-controlled `error` prop\n * (REB-837 review fix: deciding purely from `error` captured at render\n * time showed \"Añadido ✓\" for a REFUSED add whenever the caller never\n * flips `error` back on). `'refused'` → error state; `'added'`/`'clamped'`\n * → confirmed state; returning nothing (`void`) stays backward compatible\n * with the original fire-and-forget contract — treated as success.\n */\n onAddToCart?: (item?: TItem) => AddToCartOutcome | void | Promise<AddToCartOutcome | void>\n disabled?: boolean\n outOfStock?: boolean\n /** True while the add-to-cart call (or a navigation it triggers) is in flight. */\n pending?: boolean\n /**\n * Controlled error (out-of-stock race, max-quantity reached, a failed\n * mutation…). Set it truthy right after `onAddToCart` fails to trigger the\n * error state; clear it (`undefined`/`false`) to let the button reset. A\n * `string` is shown as the button's label; `true` falls back to\n * `labels.error`.\n */\n error?: string | boolean\n /** Opaque item forwarded verbatim to `onAddToCart`/`onAdded`/`onError` — never read internally. */\n item?: TItem\n /** How long the success/error state shows before reverting to idle, ms. @default 1500 */\n successDurationMs?: number\n /** Fires once per successful add, right after `onAddToCart`. */\n onAdded?: (item?: TItem) => void\n /** Fires once when `error` transitions from falsy to truthy. */\n onError?: (item?: TItem, error?: string | boolean) => void\n labels?: Partial<AddToCartButtonLabels>\n className?: string\n}\n\nexport function AddToCartButton<TItem = unknown>({\n onAddToCart,\n disabled,\n outOfStock,\n pending,\n error,\n item,\n successDurationMs = 1500,\n onAdded,\n onError,\n labels,\n className,\n}: AddToCartButtonProps<TItem>) {\n const t = { ...DEFAULT_LABELS, ...labels }\n const [status, setStatus] = useState<AddToCartButtonStatus>('idle')\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n const prevErrorRef = useRef<string | boolean | undefined>(error)\n // Non-reentrancy guard (REB-837): `disabled`/`aria-disabled` already block\n // both pointer and keyboard activation of a native <button>, but a ref\n // guard is the actual source of truth for \"is a success window open OR an\n // async onAddToCart is still resolving\" — defense in depth against\n // anything that might still dispatch a click, and the only thing that\n // actually blocks a double-submit while a Promise is pending.\n const inSuccessWindowRef = useRef(false)\n const resolvingRef = useRef(false)\n const mountedRef = useRef(true)\n // Mirrors `resolvingRef` into render state so the button is ALSO visibly\n // disabled (not just logically blocked) while an async `onAddToCart` is\n // still pending — `resolvingRef` alone is enough to stop a double-submit,\n // but a sighted user should see the button go inert too.\n const [resolving, setResolving] = useState(false)\n\n const clearTimer = () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current)\n timeoutRef.current = null\n }\n }\n\n useEffect(() => {\n mountedRef.current = true\n return () => {\n mountedRef.current = false\n clearTimer()\n }\n }, [])\n\n const triggerSuccess = () => {\n inSuccessWindowRef.current = true\n clearTimer()\n setStatus('success')\n onAdded?.(item)\n timeoutRef.current = setTimeout(() => {\n inSuccessWindowRef.current = false\n setStatus('idle')\n }, successDurationMs)\n }\n\n const triggerError = (errorValue: string | boolean = true) => {\n clearTimer()\n inSuccessWindowRef.current = false\n setStatus('error')\n onError?.(item, errorValue)\n timeoutRef.current = setTimeout(() => setStatus('idle'), successDurationMs)\n }\n\n useEffect(() => {\n if (error && !prevErrorRef.current) {\n triggerError(error)\n }\n prevErrorRef.current = error\n // eslint-disable-next-line react-hooks/exhaustive-deps -- only re-run on `error` transitions\n }, [error])\n\n /**\n * Applies the RESULT of `onAddToCart` (REB-837 review fix): a `'refused'`\n * outcome always triggers the error state, regardless of the `error` prop\n * — the whole point is that the caller may not control that prop at all\n * (e.g. it derives the outcome from `addItem()`'s own return value, never\n * touching `error`). Anything else (`'added'`, `'clamped'`, or no return\n * value at all — the original fire-and-forget contract) is success,\n * UNLESS the externally-controlled `error` prop is ALSO currently truthy.\n */\n const applyOutcome = (result: AddToCartOutcome | void) => {\n if (!mountedRef.current || outOfStock) return\n if (result?.status === 'refused') {\n triggerError(true)\n return\n }\n if (error) return\n triggerSuccess()\n }\n\n const handleClick = () => {\n if (inSuccessWindowRef.current || resolvingRef.current) return\n const result = onAddToCart?.(item)\n\n if (result && typeof (result as Promise<AddToCartOutcome | void>).then === 'function') {\n resolvingRef.current = true\n setResolving(true)\n ;(result as Promise<AddToCartOutcome | void>)\n .then((resolved) => {\n resolvingRef.current = false\n if (mountedRef.current) setResolving(false)\n applyOutcome(resolved)\n })\n .catch(() => {\n resolvingRef.current = false\n if (mountedRef.current) setResolving(false)\n applyOutcome({ status: 'refused' })\n })\n return\n }\n\n applyOutcome(result as AddToCartOutcome | void)\n }\n\n const isDisabled = Boolean(disabled || outOfStock || pending || status === 'success' || resolving)\n const label = outOfStock\n ? t.outOfStock\n : status === 'success'\n ? t.added\n : status === 'error'\n ? typeof error === 'string'\n ? error\n : t.error\n : pending || resolving\n ? t.adding\n : t.add\n\n return (\n <button\n type='button'\n onClick={handleClick}\n disabled={isDisabled}\n aria-disabled={isDisabled}\n aria-live='polite'\n data-slot='add-to-cart-button'\n data-status={status}\n className={cn(\n 'inline-flex items-center justify-center gap-2 rounded-xl px-4 py-2.5 text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n isDisabled\n ? 'cursor-not-allowed bg-muted text-muted-foreground'\n : 'bg-primary text-primary-foreground hover:bg-primary/90',\n status === 'error' && 'bg-destructive text-destructive-foreground',\n className,\n )}\n >\n {label}\n </button>\n )\n}\n","'use client'\n\nimport { useEffect, useRef, useState } from 'react'\n\nimport { cn } from '@reblu/site-ui/utils'\n\n/**\n * `CartIcon` — cart badge/link, with an opt-in count \"bump\" (REB-837). Moved\n * to the `./client` entry (from the RSC-safe root) because the bump\n * animation needs `useState`/`useEffect` to detect an itemCount increase —\n * see `client.ts`'s own leading `'use client'` directive (R4).\n *\n * The HYDRATION GOTCHA this guards against: `CartProvider` rehydrates\n * AFTER mount, so `itemCount` jumps `0 -> N` on every page load with a\n * non-empty persisted cart. Without a guard, that jump reads exactly like \"N\n * items were just added\" and bumps on every single page load. The fix:\n * `hydrated` (pass `useCart().hydrated` straight through) is compared\n * against its OWN previous value, not just itemCount's — a bump only fires\n * when the cart was ALREADY hydrated on the previous comparison AND\n * itemCount increased since. The simultaneous `false -> true` / `0 -> N`\n * rehydration transition fails that first condition; a real add afterwards\n * (hydrated already `true` on both sides) passes it.\n */\nexport interface CartIconLabels {\n cart: string\n itemsSuffix: (count: number) => string\n}\n\nconst DEFAULT_LABELS: CartIconLabels = {\n cart: 'Carrito',\n itemsSuffix: (count) => `${count} artículo${count === 1 ? '' : 's'}`,\n}\n\n/** How long the bump animation plays, ms. */\nconst BUMP_DURATION_MS = 400\n\nexport interface CartIconProps {\n itemCount: number\n href: string\n /**\n * Pass `useCart().hydrated` through when `bumpOnAdd` is enabled — gates\n * the bump against the rehydration jump. Omitting it ENTIRELY (REB-837\n * review fix — not the same as passing `false`) disables the hydration\n * guard altogether, so the bump fires on every itemCount increase; only\n * reach for that when `itemCount` is computed without any client-side\n * rehydration step (e.g. server-rendered, never changes after mount).\n */\n hydrated?: boolean\n /**\n * Enable the count bump when `itemCount` increases after hydration.\n * Respects `prefers-reduced-motion` (the animation classes are\n * `motion-safe:`-gated). @default false\n */\n bumpOnAdd?: boolean\n labels?: Partial<CartIconLabels>\n className?: string\n}\n\nexport function CartIcon({ itemCount, href, hydrated, bumpOnAdd, labels, className }: CartIconProps) {\n const t = { ...DEFAULT_LABELS, ...labels }\n const ariaLabel = itemCount > 0 ? `${t.cart} (${t.itemsSuffix(itemCount)})` : t.cart\n const displayCount = itemCount > 99 ? '99+' : String(itemCount)\n\n const [bumping, setBumping] = useState(false)\n const prevCountRef = useRef(itemCount)\n const prevHydratedRef = useRef(hydrated)\n\n useEffect(() => {\n // REB-837 review fix: `hydrated` OMITTED entirely (never `=== true` nor\n // `=== false`, i.e. `undefined` on the CURRENT render) means the caller\n // isn't using the hydration guard at all — treat it as always-satisfied\n // rather than permanently failing `prevHydratedRef.current === true`\n // (which stayed `undefined` forever and silently disabled every bump).\n const hydrationGuardSatisfied = hydrated === undefined || prevHydratedRef.current === true\n const shouldBump =\n Boolean(bumpOnAdd) && itemCount > prevCountRef.current && hydrationGuardSatisfied\n\n prevCountRef.current = itemCount\n prevHydratedRef.current = hydrated\n\n if (!shouldBump) {\n // REB-837 review fix: a PREVIOUS bump's own setTimeout gets cancelled\n // by this effect's cleanup (dep change) before this body runs — if\n // this run doesn't start a NEW bump, it must still reset the flag\n // itself, or a bump started just before a quick remove/decrease\n // (within `BUMP_DURATION_MS`) gets stuck on `true` forever.\n setBumping(false)\n return\n }\n\n setBumping(true)\n const id = setTimeout(() => setBumping(false), BUMP_DURATION_MS)\n return () => clearTimeout(id)\n // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally only re-runs when itemCount/hydrated/bumpOnAdd change\n }, [itemCount, hydrated, bumpOnAdd])\n\n return (\n <a\n href={href}\n aria-label={ariaLabel}\n data-slot='cart-icon'\n data-bumping={bumping || undefined}\n className={cn(\n 'relative inline-flex items-center justify-center rounded-lg p-2 text-foreground/80 transition-colors hover:bg-muted/60 hover:text-foreground',\n className,\n )}\n >\n <svg\n viewBox='0 0 24 24'\n fill='none'\n stroke='currentColor'\n strokeWidth={2}\n strokeLinecap='round'\n strokeLinejoin='round'\n className={cn('h-5 w-5', bumping && 'motion-safe:scale-125 motion-safe:transition-transform motion-safe:duration-300')}\n aria-hidden='true'\n >\n <circle cx='9' cy='21' r='1' />\n <circle cx='20' cy='21' r='1' />\n <path d='M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6' />\n </svg>\n {itemCount > 0 && (\n <span\n aria-hidden='true'\n className={cn(\n 'absolute -top-0.5 -right-0.5 flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-primary px-1 text-[10px] font-bold leading-none tabular-nums text-primary-foreground',\n bumping && 'motion-safe:scale-125 motion-safe:transition-transform motion-safe:duration-300',\n )}\n >\n {displayCount}\n </span>\n )}\n </a>\n )\n}\n"],"mappings":"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","names":["import_utils","import_jsx_runtime","DEFAULT_LABELS","import_react","import_utils","import_jsx_runtime","DEFAULT_LABELS","import_react","import_utils","import_jsx_runtime","DEFAULT_LABELS"]}
|
package/dist/client.d.cts
CHANGED
|
@@ -131,4 +131,117 @@ interface ShareButtonsProps {
|
|
|
131
131
|
}
|
|
132
132
|
declare function ShareButtons({ url, title, className, labels }: ShareButtonsProps): react_jsx_runtime.JSX.Element;
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
/**
|
|
135
|
+
* `AddToCartButton` — add-to-cart CTA with a built-in confirmed/error state
|
|
136
|
+
* (REB-837). Moved to the `./client` entry (from the RSC-safe root) because
|
|
137
|
+
* the confirmed-state timer and the `error` prop watcher genuinely need
|
|
138
|
+
* `useState`/`useEffect` — see `client.ts`'s own leading `'use client'`
|
|
139
|
+
* directive (R4: tsup only preserves a directive declared by the entry file
|
|
140
|
+
* itself, not one merely re-exported from a child module).
|
|
141
|
+
*
|
|
142
|
+
* Previously (REB-829) this component had no internal state at all — it was
|
|
143
|
+
* a pure `onAddToCart?: () => void` button exported from the RSC-safe root.
|
|
144
|
+
* A SITE importing `AddToCartButton` from `@reblu/site-blocks` (the root
|
|
145
|
+
* entry) must now import it from `@reblu/site-blocks/client` instead.
|
|
146
|
+
*/
|
|
147
|
+
interface AddToCartButtonLabels {
|
|
148
|
+
add: string;
|
|
149
|
+
adding: string;
|
|
150
|
+
outOfStock: string;
|
|
151
|
+
/** Shown for ~`successDurationMs` after a successful add. */
|
|
152
|
+
added: string;
|
|
153
|
+
/** Fallback shown when `error` is `true` (not a string). */
|
|
154
|
+
error: string;
|
|
155
|
+
}
|
|
156
|
+
type AddToCartButtonStatus = 'idle' | 'success' | 'error';
|
|
157
|
+
/**
|
|
158
|
+
* Structural subset of `@reblu/site-next/cart`'s `AddItemOutcome` (REB-837
|
|
159
|
+
* review fix) — `site-blocks` never depends on `site-next`, so this is
|
|
160
|
+
* duplicated shape, not an import; a real `AddItemOutcome` (which also
|
|
161
|
+
* carries `quantity`) satisfies this type as-is.
|
|
162
|
+
*/
|
|
163
|
+
interface AddToCartOutcome {
|
|
164
|
+
status: 'added' | 'clamped' | 'refused';
|
|
165
|
+
}
|
|
166
|
+
interface AddToCartButtonProps<TItem = unknown> {
|
|
167
|
+
/**
|
|
168
|
+
* Called on click. MAY return the outcome of the add (synchronously or
|
|
169
|
+
* via a `Promise`) so the button can decide success vs error from the
|
|
170
|
+
* RESULT instead of only from the externally-controlled `error` prop
|
|
171
|
+
* (REB-837 review fix: deciding purely from `error` captured at render
|
|
172
|
+
* time showed "Añadido ✓" for a REFUSED add whenever the caller never
|
|
173
|
+
* flips `error` back on). `'refused'` → error state; `'added'`/`'clamped'`
|
|
174
|
+
* → confirmed state; returning nothing (`void`) stays backward compatible
|
|
175
|
+
* with the original fire-and-forget contract — treated as success.
|
|
176
|
+
*/
|
|
177
|
+
onAddToCart?: (item?: TItem) => AddToCartOutcome | void | Promise<AddToCartOutcome | void>;
|
|
178
|
+
disabled?: boolean;
|
|
179
|
+
outOfStock?: boolean;
|
|
180
|
+
/** True while the add-to-cart call (or a navigation it triggers) is in flight. */
|
|
181
|
+
pending?: boolean;
|
|
182
|
+
/**
|
|
183
|
+
* Controlled error (out-of-stock race, max-quantity reached, a failed
|
|
184
|
+
* mutation…). Set it truthy right after `onAddToCart` fails to trigger the
|
|
185
|
+
* error state; clear it (`undefined`/`false`) to let the button reset. A
|
|
186
|
+
* `string` is shown as the button's label; `true` falls back to
|
|
187
|
+
* `labels.error`.
|
|
188
|
+
*/
|
|
189
|
+
error?: string | boolean;
|
|
190
|
+
/** Opaque item forwarded verbatim to `onAddToCart`/`onAdded`/`onError` — never read internally. */
|
|
191
|
+
item?: TItem;
|
|
192
|
+
/** How long the success/error state shows before reverting to idle, ms. @default 1500 */
|
|
193
|
+
successDurationMs?: number;
|
|
194
|
+
/** Fires once per successful add, right after `onAddToCart`. */
|
|
195
|
+
onAdded?: (item?: TItem) => void;
|
|
196
|
+
/** Fires once when `error` transitions from falsy to truthy. */
|
|
197
|
+
onError?: (item?: TItem, error?: string | boolean) => void;
|
|
198
|
+
labels?: Partial<AddToCartButtonLabels>;
|
|
199
|
+
className?: string;
|
|
200
|
+
}
|
|
201
|
+
declare function AddToCartButton<TItem = unknown>({ onAddToCart, disabled, outOfStock, pending, error, item, successDurationMs, onAdded, onError, labels, className, }: AddToCartButtonProps<TItem>): react_jsx_runtime.JSX.Element;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* `CartIcon` — cart badge/link, with an opt-in count "bump" (REB-837). Moved
|
|
205
|
+
* to the `./client` entry (from the RSC-safe root) because the bump
|
|
206
|
+
* animation needs `useState`/`useEffect` to detect an itemCount increase —
|
|
207
|
+
* see `client.ts`'s own leading `'use client'` directive (R4).
|
|
208
|
+
*
|
|
209
|
+
* The HYDRATION GOTCHA this guards against: `CartProvider` rehydrates
|
|
210
|
+
* AFTER mount, so `itemCount` jumps `0 -> N` on every page load with a
|
|
211
|
+
* non-empty persisted cart. Without a guard, that jump reads exactly like "N
|
|
212
|
+
* items were just added" and bumps on every single page load. The fix:
|
|
213
|
+
* `hydrated` (pass `useCart().hydrated` straight through) is compared
|
|
214
|
+
* against its OWN previous value, not just itemCount's — a bump only fires
|
|
215
|
+
* when the cart was ALREADY hydrated on the previous comparison AND
|
|
216
|
+
* itemCount increased since. The simultaneous `false -> true` / `0 -> N`
|
|
217
|
+
* rehydration transition fails that first condition; a real add afterwards
|
|
218
|
+
* (hydrated already `true` on both sides) passes it.
|
|
219
|
+
*/
|
|
220
|
+
interface CartIconLabels {
|
|
221
|
+
cart: string;
|
|
222
|
+
itemsSuffix: (count: number) => string;
|
|
223
|
+
}
|
|
224
|
+
interface CartIconProps {
|
|
225
|
+
itemCount: number;
|
|
226
|
+
href: string;
|
|
227
|
+
/**
|
|
228
|
+
* Pass `useCart().hydrated` through when `bumpOnAdd` is enabled — gates
|
|
229
|
+
* the bump against the rehydration jump. Omitting it ENTIRELY (REB-837
|
|
230
|
+
* review fix — not the same as passing `false`) disables the hydration
|
|
231
|
+
* guard altogether, so the bump fires on every itemCount increase; only
|
|
232
|
+
* reach for that when `itemCount` is computed without any client-side
|
|
233
|
+
* rehydration step (e.g. server-rendered, never changes after mount).
|
|
234
|
+
*/
|
|
235
|
+
hydrated?: boolean;
|
|
236
|
+
/**
|
|
237
|
+
* Enable the count bump when `itemCount` increases after hydration.
|
|
238
|
+
* Respects `prefers-reduced-motion` (the animation classes are
|
|
239
|
+
* `motion-safe:`-gated). @default false
|
|
240
|
+
*/
|
|
241
|
+
bumpOnAdd?: boolean;
|
|
242
|
+
labels?: Partial<CartIconLabels>;
|
|
243
|
+
className?: string;
|
|
244
|
+
}
|
|
245
|
+
declare function CartIcon({ itemCount, href, hydrated, bumpOnAdd, labels, className }: CartIconProps): react_jsx_runtime.JSX.Element;
|
|
246
|
+
|
|
247
|
+
export { AddToCartButton, type AddToCartButtonLabels, type AddToCartButtonProps, type AddToCartButtonStatus, CartIcon, type CartIconLabels, type CartIconProps, Collection, type CollectionFacet, type CollectionFacetOption, type CollectionFilter, type CollectionLabels, type CollectionProps, ShareButtons, type ShareButtonsLabels, type ShareButtonsProps, type UseCollectionOptions, type UseCollectionResult, useCollection };
|
package/dist/client.d.ts
CHANGED
|
@@ -131,4 +131,117 @@ interface ShareButtonsProps {
|
|
|
131
131
|
}
|
|
132
132
|
declare function ShareButtons({ url, title, className, labels }: ShareButtonsProps): react_jsx_runtime.JSX.Element;
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
/**
|
|
135
|
+
* `AddToCartButton` — add-to-cart CTA with a built-in confirmed/error state
|
|
136
|
+
* (REB-837). Moved to the `./client` entry (from the RSC-safe root) because
|
|
137
|
+
* the confirmed-state timer and the `error` prop watcher genuinely need
|
|
138
|
+
* `useState`/`useEffect` — see `client.ts`'s own leading `'use client'`
|
|
139
|
+
* directive (R4: tsup only preserves a directive declared by the entry file
|
|
140
|
+
* itself, not one merely re-exported from a child module).
|
|
141
|
+
*
|
|
142
|
+
* Previously (REB-829) this component had no internal state at all — it was
|
|
143
|
+
* a pure `onAddToCart?: () => void` button exported from the RSC-safe root.
|
|
144
|
+
* A SITE importing `AddToCartButton` from `@reblu/site-blocks` (the root
|
|
145
|
+
* entry) must now import it from `@reblu/site-blocks/client` instead.
|
|
146
|
+
*/
|
|
147
|
+
interface AddToCartButtonLabels {
|
|
148
|
+
add: string;
|
|
149
|
+
adding: string;
|
|
150
|
+
outOfStock: string;
|
|
151
|
+
/** Shown for ~`successDurationMs` after a successful add. */
|
|
152
|
+
added: string;
|
|
153
|
+
/** Fallback shown when `error` is `true` (not a string). */
|
|
154
|
+
error: string;
|
|
155
|
+
}
|
|
156
|
+
type AddToCartButtonStatus = 'idle' | 'success' | 'error';
|
|
157
|
+
/**
|
|
158
|
+
* Structural subset of `@reblu/site-next/cart`'s `AddItemOutcome` (REB-837
|
|
159
|
+
* review fix) — `site-blocks` never depends on `site-next`, so this is
|
|
160
|
+
* duplicated shape, not an import; a real `AddItemOutcome` (which also
|
|
161
|
+
* carries `quantity`) satisfies this type as-is.
|
|
162
|
+
*/
|
|
163
|
+
interface AddToCartOutcome {
|
|
164
|
+
status: 'added' | 'clamped' | 'refused';
|
|
165
|
+
}
|
|
166
|
+
interface AddToCartButtonProps<TItem = unknown> {
|
|
167
|
+
/**
|
|
168
|
+
* Called on click. MAY return the outcome of the add (synchronously or
|
|
169
|
+
* via a `Promise`) so the button can decide success vs error from the
|
|
170
|
+
* RESULT instead of only from the externally-controlled `error` prop
|
|
171
|
+
* (REB-837 review fix: deciding purely from `error` captured at render
|
|
172
|
+
* time showed "Añadido ✓" for a REFUSED add whenever the caller never
|
|
173
|
+
* flips `error` back on). `'refused'` → error state; `'added'`/`'clamped'`
|
|
174
|
+
* → confirmed state; returning nothing (`void`) stays backward compatible
|
|
175
|
+
* with the original fire-and-forget contract — treated as success.
|
|
176
|
+
*/
|
|
177
|
+
onAddToCart?: (item?: TItem) => AddToCartOutcome | void | Promise<AddToCartOutcome | void>;
|
|
178
|
+
disabled?: boolean;
|
|
179
|
+
outOfStock?: boolean;
|
|
180
|
+
/** True while the add-to-cart call (or a navigation it triggers) is in flight. */
|
|
181
|
+
pending?: boolean;
|
|
182
|
+
/**
|
|
183
|
+
* Controlled error (out-of-stock race, max-quantity reached, a failed
|
|
184
|
+
* mutation…). Set it truthy right after `onAddToCart` fails to trigger the
|
|
185
|
+
* error state; clear it (`undefined`/`false`) to let the button reset. A
|
|
186
|
+
* `string` is shown as the button's label; `true` falls back to
|
|
187
|
+
* `labels.error`.
|
|
188
|
+
*/
|
|
189
|
+
error?: string | boolean;
|
|
190
|
+
/** Opaque item forwarded verbatim to `onAddToCart`/`onAdded`/`onError` — never read internally. */
|
|
191
|
+
item?: TItem;
|
|
192
|
+
/** How long the success/error state shows before reverting to idle, ms. @default 1500 */
|
|
193
|
+
successDurationMs?: number;
|
|
194
|
+
/** Fires once per successful add, right after `onAddToCart`. */
|
|
195
|
+
onAdded?: (item?: TItem) => void;
|
|
196
|
+
/** Fires once when `error` transitions from falsy to truthy. */
|
|
197
|
+
onError?: (item?: TItem, error?: string | boolean) => void;
|
|
198
|
+
labels?: Partial<AddToCartButtonLabels>;
|
|
199
|
+
className?: string;
|
|
200
|
+
}
|
|
201
|
+
declare function AddToCartButton<TItem = unknown>({ onAddToCart, disabled, outOfStock, pending, error, item, successDurationMs, onAdded, onError, labels, className, }: AddToCartButtonProps<TItem>): react_jsx_runtime.JSX.Element;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* `CartIcon` — cart badge/link, with an opt-in count "bump" (REB-837). Moved
|
|
205
|
+
* to the `./client` entry (from the RSC-safe root) because the bump
|
|
206
|
+
* animation needs `useState`/`useEffect` to detect an itemCount increase —
|
|
207
|
+
* see `client.ts`'s own leading `'use client'` directive (R4).
|
|
208
|
+
*
|
|
209
|
+
* The HYDRATION GOTCHA this guards against: `CartProvider` rehydrates
|
|
210
|
+
* AFTER mount, so `itemCount` jumps `0 -> N` on every page load with a
|
|
211
|
+
* non-empty persisted cart. Without a guard, that jump reads exactly like "N
|
|
212
|
+
* items were just added" and bumps on every single page load. The fix:
|
|
213
|
+
* `hydrated` (pass `useCart().hydrated` straight through) is compared
|
|
214
|
+
* against its OWN previous value, not just itemCount's — a bump only fires
|
|
215
|
+
* when the cart was ALREADY hydrated on the previous comparison AND
|
|
216
|
+
* itemCount increased since. The simultaneous `false -> true` / `0 -> N`
|
|
217
|
+
* rehydration transition fails that first condition; a real add afterwards
|
|
218
|
+
* (hydrated already `true` on both sides) passes it.
|
|
219
|
+
*/
|
|
220
|
+
interface CartIconLabels {
|
|
221
|
+
cart: string;
|
|
222
|
+
itemsSuffix: (count: number) => string;
|
|
223
|
+
}
|
|
224
|
+
interface CartIconProps {
|
|
225
|
+
itemCount: number;
|
|
226
|
+
href: string;
|
|
227
|
+
/**
|
|
228
|
+
* Pass `useCart().hydrated` through when `bumpOnAdd` is enabled — gates
|
|
229
|
+
* the bump against the rehydration jump. Omitting it ENTIRELY (REB-837
|
|
230
|
+
* review fix — not the same as passing `false`) disables the hydration
|
|
231
|
+
* guard altogether, so the bump fires on every itemCount increase; only
|
|
232
|
+
* reach for that when `itemCount` is computed without any client-side
|
|
233
|
+
* rehydration step (e.g. server-rendered, never changes after mount).
|
|
234
|
+
*/
|
|
235
|
+
hydrated?: boolean;
|
|
236
|
+
/**
|
|
237
|
+
* Enable the count bump when `itemCount` increases after hydration.
|
|
238
|
+
* Respects `prefers-reduced-motion` (the animation classes are
|
|
239
|
+
* `motion-safe:`-gated). @default false
|
|
240
|
+
*/
|
|
241
|
+
bumpOnAdd?: boolean;
|
|
242
|
+
labels?: Partial<CartIconLabels>;
|
|
243
|
+
className?: string;
|
|
244
|
+
}
|
|
245
|
+
declare function CartIcon({ itemCount, href, hydrated, bumpOnAdd, labels, className }: CartIconProps): react_jsx_runtime.JSX.Element;
|
|
246
|
+
|
|
247
|
+
export { AddToCartButton, type AddToCartButtonLabels, type AddToCartButtonProps, type AddToCartButtonStatus, CartIcon, type CartIconLabels, type CartIconProps, Collection, type CollectionFacet, type CollectionFacetOption, type CollectionFilter, type CollectionLabels, type CollectionProps, ShareButtons, type ShareButtonsLabels, type ShareButtonsProps, type UseCollectionOptions, type UseCollectionResult, useCollection };
|
package/dist/client.js
CHANGED
|
@@ -289,7 +289,203 @@ function ShareButtons({ url, title, className, labels }) {
|
|
|
289
289
|
/* @__PURE__ */ jsx2("span", { role: "status", "aria-live": "polite", className: "sr-only", children: copied ? t.copyStatus : "" })
|
|
290
290
|
] });
|
|
291
291
|
}
|
|
292
|
+
|
|
293
|
+
// src/components/add-to-cart-button.tsx
|
|
294
|
+
import { useEffect, useRef, useState as useState3 } from "react";
|
|
295
|
+
import { cn as cn3 } from "@reblu/site-ui/utils";
|
|
296
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
297
|
+
var DEFAULT_LABELS3 = {
|
|
298
|
+
add: "A\xF1adir al carrito",
|
|
299
|
+
adding: "A\xF1adiendo\u2026",
|
|
300
|
+
outOfStock: "Agotado",
|
|
301
|
+
added: "A\xF1adido \u2713",
|
|
302
|
+
error: "No se pudo a\xF1adir"
|
|
303
|
+
};
|
|
304
|
+
function AddToCartButton({
|
|
305
|
+
onAddToCart,
|
|
306
|
+
disabled,
|
|
307
|
+
outOfStock,
|
|
308
|
+
pending,
|
|
309
|
+
error,
|
|
310
|
+
item,
|
|
311
|
+
successDurationMs = 1500,
|
|
312
|
+
onAdded,
|
|
313
|
+
onError,
|
|
314
|
+
labels,
|
|
315
|
+
className
|
|
316
|
+
}) {
|
|
317
|
+
const t = { ...DEFAULT_LABELS3, ...labels };
|
|
318
|
+
const [status, setStatus] = useState3("idle");
|
|
319
|
+
const timeoutRef = useRef(null);
|
|
320
|
+
const prevErrorRef = useRef(error);
|
|
321
|
+
const inSuccessWindowRef = useRef(false);
|
|
322
|
+
const resolvingRef = useRef(false);
|
|
323
|
+
const mountedRef = useRef(true);
|
|
324
|
+
const [resolving, setResolving] = useState3(false);
|
|
325
|
+
const clearTimer = () => {
|
|
326
|
+
if (timeoutRef.current) {
|
|
327
|
+
clearTimeout(timeoutRef.current);
|
|
328
|
+
timeoutRef.current = null;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
useEffect(() => {
|
|
332
|
+
mountedRef.current = true;
|
|
333
|
+
return () => {
|
|
334
|
+
mountedRef.current = false;
|
|
335
|
+
clearTimer();
|
|
336
|
+
};
|
|
337
|
+
}, []);
|
|
338
|
+
const triggerSuccess = () => {
|
|
339
|
+
inSuccessWindowRef.current = true;
|
|
340
|
+
clearTimer();
|
|
341
|
+
setStatus("success");
|
|
342
|
+
onAdded?.(item);
|
|
343
|
+
timeoutRef.current = setTimeout(() => {
|
|
344
|
+
inSuccessWindowRef.current = false;
|
|
345
|
+
setStatus("idle");
|
|
346
|
+
}, successDurationMs);
|
|
347
|
+
};
|
|
348
|
+
const triggerError = (errorValue = true) => {
|
|
349
|
+
clearTimer();
|
|
350
|
+
inSuccessWindowRef.current = false;
|
|
351
|
+
setStatus("error");
|
|
352
|
+
onError?.(item, errorValue);
|
|
353
|
+
timeoutRef.current = setTimeout(() => setStatus("idle"), successDurationMs);
|
|
354
|
+
};
|
|
355
|
+
useEffect(() => {
|
|
356
|
+
if (error && !prevErrorRef.current) {
|
|
357
|
+
triggerError(error);
|
|
358
|
+
}
|
|
359
|
+
prevErrorRef.current = error;
|
|
360
|
+
}, [error]);
|
|
361
|
+
const applyOutcome = (result) => {
|
|
362
|
+
if (!mountedRef.current || outOfStock) return;
|
|
363
|
+
if (result?.status === "refused") {
|
|
364
|
+
triggerError(true);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (error) return;
|
|
368
|
+
triggerSuccess();
|
|
369
|
+
};
|
|
370
|
+
const handleClick = () => {
|
|
371
|
+
if (inSuccessWindowRef.current || resolvingRef.current) return;
|
|
372
|
+
const result = onAddToCart?.(item);
|
|
373
|
+
if (result && typeof result.then === "function") {
|
|
374
|
+
resolvingRef.current = true;
|
|
375
|
+
setResolving(true);
|
|
376
|
+
result.then((resolved) => {
|
|
377
|
+
resolvingRef.current = false;
|
|
378
|
+
if (mountedRef.current) setResolving(false);
|
|
379
|
+
applyOutcome(resolved);
|
|
380
|
+
}).catch(() => {
|
|
381
|
+
resolvingRef.current = false;
|
|
382
|
+
if (mountedRef.current) setResolving(false);
|
|
383
|
+
applyOutcome({ status: "refused" });
|
|
384
|
+
});
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
applyOutcome(result);
|
|
388
|
+
};
|
|
389
|
+
const isDisabled = Boolean(disabled || outOfStock || pending || status === "success" || resolving);
|
|
390
|
+
const label = outOfStock ? t.outOfStock : status === "success" ? t.added : status === "error" ? typeof error === "string" ? error : t.error : pending || resolving ? t.adding : t.add;
|
|
391
|
+
return /* @__PURE__ */ jsx3(
|
|
392
|
+
"button",
|
|
393
|
+
{
|
|
394
|
+
type: "button",
|
|
395
|
+
onClick: handleClick,
|
|
396
|
+
disabled: isDisabled,
|
|
397
|
+
"aria-disabled": isDisabled,
|
|
398
|
+
"aria-live": "polite",
|
|
399
|
+
"data-slot": "add-to-cart-button",
|
|
400
|
+
"data-status": status,
|
|
401
|
+
className: cn3(
|
|
402
|
+
"inline-flex items-center justify-center gap-2 rounded-xl px-4 py-2.5 text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
403
|
+
isDisabled ? "cursor-not-allowed bg-muted text-muted-foreground" : "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
404
|
+
status === "error" && "bg-destructive text-destructive-foreground",
|
|
405
|
+
className
|
|
406
|
+
),
|
|
407
|
+
children: label
|
|
408
|
+
}
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/components/cart-icon.tsx
|
|
413
|
+
import { useEffect as useEffect2, useRef as useRef2, useState as useState4 } from "react";
|
|
414
|
+
import { cn as cn4 } from "@reblu/site-ui/utils";
|
|
415
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
416
|
+
var DEFAULT_LABELS4 = {
|
|
417
|
+
cart: "Carrito",
|
|
418
|
+
itemsSuffix: (count) => `${count} art\xEDculo${count === 1 ? "" : "s"}`
|
|
419
|
+
};
|
|
420
|
+
var BUMP_DURATION_MS = 400;
|
|
421
|
+
function CartIcon({ itemCount, href, hydrated, bumpOnAdd, labels, className }) {
|
|
422
|
+
const t = { ...DEFAULT_LABELS4, ...labels };
|
|
423
|
+
const ariaLabel = itemCount > 0 ? `${t.cart} (${t.itemsSuffix(itemCount)})` : t.cart;
|
|
424
|
+
const displayCount = itemCount > 99 ? "99+" : String(itemCount);
|
|
425
|
+
const [bumping, setBumping] = useState4(false);
|
|
426
|
+
const prevCountRef = useRef2(itemCount);
|
|
427
|
+
const prevHydratedRef = useRef2(hydrated);
|
|
428
|
+
useEffect2(() => {
|
|
429
|
+
const hydrationGuardSatisfied = hydrated === void 0 || prevHydratedRef.current === true;
|
|
430
|
+
const shouldBump = Boolean(bumpOnAdd) && itemCount > prevCountRef.current && hydrationGuardSatisfied;
|
|
431
|
+
prevCountRef.current = itemCount;
|
|
432
|
+
prevHydratedRef.current = hydrated;
|
|
433
|
+
if (!shouldBump) {
|
|
434
|
+
setBumping(false);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
setBumping(true);
|
|
438
|
+
const id = setTimeout(() => setBumping(false), BUMP_DURATION_MS);
|
|
439
|
+
return () => clearTimeout(id);
|
|
440
|
+
}, [itemCount, hydrated, bumpOnAdd]);
|
|
441
|
+
return /* @__PURE__ */ jsxs3(
|
|
442
|
+
"a",
|
|
443
|
+
{
|
|
444
|
+
href,
|
|
445
|
+
"aria-label": ariaLabel,
|
|
446
|
+
"data-slot": "cart-icon",
|
|
447
|
+
"data-bumping": bumping || void 0,
|
|
448
|
+
className: cn4(
|
|
449
|
+
"relative inline-flex items-center justify-center rounded-lg p-2 text-foreground/80 transition-colors hover:bg-muted/60 hover:text-foreground",
|
|
450
|
+
className
|
|
451
|
+
),
|
|
452
|
+
children: [
|
|
453
|
+
/* @__PURE__ */ jsxs3(
|
|
454
|
+
"svg",
|
|
455
|
+
{
|
|
456
|
+
viewBox: "0 0 24 24",
|
|
457
|
+
fill: "none",
|
|
458
|
+
stroke: "currentColor",
|
|
459
|
+
strokeWidth: 2,
|
|
460
|
+
strokeLinecap: "round",
|
|
461
|
+
strokeLinejoin: "round",
|
|
462
|
+
className: cn4("h-5 w-5", bumping && "motion-safe:scale-125 motion-safe:transition-transform motion-safe:duration-300"),
|
|
463
|
+
"aria-hidden": "true",
|
|
464
|
+
children: [
|
|
465
|
+
/* @__PURE__ */ jsx4("circle", { cx: "9", cy: "21", r: "1" }),
|
|
466
|
+
/* @__PURE__ */ jsx4("circle", { cx: "20", cy: "21", r: "1" }),
|
|
467
|
+
/* @__PURE__ */ jsx4("path", { d: "M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6" })
|
|
468
|
+
]
|
|
469
|
+
}
|
|
470
|
+
),
|
|
471
|
+
itemCount > 0 && /* @__PURE__ */ jsx4(
|
|
472
|
+
"span",
|
|
473
|
+
{
|
|
474
|
+
"aria-hidden": "true",
|
|
475
|
+
className: cn4(
|
|
476
|
+
"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",
|
|
477
|
+
bumping && "motion-safe:scale-125 motion-safe:transition-transform motion-safe:duration-300"
|
|
478
|
+
),
|
|
479
|
+
children: displayCount
|
|
480
|
+
}
|
|
481
|
+
)
|
|
482
|
+
]
|
|
483
|
+
}
|
|
484
|
+
);
|
|
485
|
+
}
|
|
292
486
|
export {
|
|
487
|
+
AddToCartButton,
|
|
488
|
+
CartIcon,
|
|
293
489
|
Collection,
|
|
294
490
|
ShareButtons,
|
|
295
491
|
useCollection
|