@reblu/site-blocks 0.3.1 → 0.4.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/dist/client.cjs +7 -7
- package/dist/client.cjs.map +1 -1
- package/dist/client.js +2 -2
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +26 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +23 -9
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/client.cjs
CHANGED
|
@@ -130,7 +130,7 @@ function useCollection(options) {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
// src/components/collection.tsx
|
|
133
|
-
var
|
|
133
|
+
var import_utils = require("@reblu/site-ui/utils");
|
|
134
134
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
135
135
|
var DEFAULT_LABELS = {
|
|
136
136
|
previous: "Previous",
|
|
@@ -161,7 +161,7 @@ function Collection({
|
|
|
161
161
|
initialFilters,
|
|
162
162
|
initialSort
|
|
163
163
|
});
|
|
164
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: (0,
|
|
164
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: (0, import_utils.cn)("space-y-8", className), children: [
|
|
165
165
|
facets.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "space-y-4", children: facets.map((facet) => {
|
|
166
166
|
const active = collection.activeFilters[facet.key];
|
|
167
167
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
@@ -194,7 +194,7 @@ function Collection({
|
|
|
194
194
|
facet.key
|
|
195
195
|
);
|
|
196
196
|
}) }),
|
|
197
|
-
collection.isEmpty ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "py-16 text-center text-muted-foreground", children: emptyLabel }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: (0,
|
|
197
|
+
collection.isEmpty ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "py-16 text-center text-muted-foreground", children: emptyLabel }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: (0, import_utils.cn)("list-none p-0", gridClassName), children: collection.items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: renderItem(item) }, getKey(item))) }),
|
|
198
198
|
collection.pageCount > 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("nav", { "aria-label": "Pagination", className: "flex items-center justify-center gap-4", children: [
|
|
199
199
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
200
200
|
"button",
|
|
@@ -229,7 +229,7 @@ function FacetChip({ label, pressed, color, onClick }) {
|
|
|
229
229
|
"aria-pressed": pressed,
|
|
230
230
|
onClick,
|
|
231
231
|
style: color ? { "--facet-color": color } : void 0,
|
|
232
|
-
className: (0,
|
|
232
|
+
className: (0, import_utils.cn)(
|
|
233
233
|
"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",
|
|
234
234
|
pressed ? color ? "border-transparent bg-[var(--facet-color)] text-white" : "border-transparent bg-primary text-primary-foreground" : "border-border text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
235
235
|
),
|
|
@@ -240,7 +240,7 @@ function FacetChip({ label, pressed, color, onClick }) {
|
|
|
240
240
|
|
|
241
241
|
// src/components/share-buttons.tsx
|
|
242
242
|
var React = __toESM(require("react"), 1);
|
|
243
|
-
var
|
|
243
|
+
var import_utils2 = require("@reblu/site-ui/utils");
|
|
244
244
|
|
|
245
245
|
// src/lib/share-urls.ts
|
|
246
246
|
function buildShareUrls(url, title) {
|
|
@@ -304,7 +304,7 @@ function ShareButtons({ url, title, className, labels }) {
|
|
|
304
304
|
} catch {
|
|
305
305
|
}
|
|
306
306
|
}
|
|
307
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: (0,
|
|
307
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: (0, import_utils2.cn)("flex items-center gap-2", className), role: "group", "aria-label": t.share, children: [
|
|
308
308
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "mr-1 text-sm font-medium text-muted-foreground", children: t.share }),
|
|
309
309
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("a", { href: share.linkedin, target: "_blank", rel: "noopener noreferrer", className: LINK_CLASS, "aria-label": t.linkedin, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(LinkedInIcon, {}) }),
|
|
310
310
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("a", { href: share.x, target: "_blank", rel: "noopener noreferrer", className: LINK_CLASS, "aria-label": t.x, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(XIcon, {}) }),
|
|
@@ -315,7 +315,7 @@ function ShareButtons({ url, title, className, labels }) {
|
|
|
315
315
|
{
|
|
316
316
|
type: "button",
|
|
317
317
|
onClick: handleCopy,
|
|
318
|
-
className: (0,
|
|
318
|
+
className: (0, import_utils2.cn)(LINK_CLASS, "w-auto gap-2 px-3", copied && "border-primary text-primary"),
|
|
319
319
|
"aria-label": t.copy,
|
|
320
320
|
children: [
|
|
321
321
|
copied ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CheckIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CopyIcon, {}),
|
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'\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'\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_site_ui","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"],"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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACEA,mBAA+C;AA2DxC,SAAS,cAAiB,SAA0D;AACzF,QAAM,EAAE,OAAO,WAAW,GAAG,UAAU,CAAC,GAAG,QAAQ,UAAU,QAAQ,CAAC,EAAE,IAAI;AAE5E,QAAM,CAAC,eAAe,gBAAgB,QAAI;AAAA,IACxC,MAAM,QAAQ,kBAAkB,CAAC;AAAA,EACnC;AACA,QAAM,CAAC,QAAQ,cAAc,QAAI,uBAAS,MAAM,QAAQ,iBAAiB,EAAE;AAC3E,QAAM,CAAC,MAAM,YAAY,QAAI,uBAA6B,MAAM,QAAQ,WAAW;AACnF,QAAM,CAAC,MAAM,YAAY,QAAI,uBAAS,MAAM,QAAQ,eAAe,CAAC;AAEpE,QAAM,eAAW,sBAAQ,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,gBAAY,sBAAQ,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,uBAAmB,0BAAY,MAAM,aAAa,CAAC,GAAG,CAAC,CAAC;AAE9D,QAAM,gBAAY;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,mBAAe,0BAAY,MAAM;AACrC,qBAAiB,CAAC,CAAC;AACnB,qBAAiB;AAAA,EACnB,GAAG,CAAC,gBAAgB,CAAC;AAErB,QAAM,gBAAY;AAAA,IAChB,CAAC,SAAiB;AAChB,qBAAe,IAAI;AACnB,uBAAiB;AAAA,IACnB;AAAA,IACA,CAAC,gBAAgB;AAAA,EACnB;AAEA,QAAM,cAAU;AAAA,IACd,CAAC,QAA4B;AAC3B,mBAAa,GAAG;AAChB,uBAAiB;AAAA,IACnB;AAAA,IACA,CAAC,gBAAgB;AAAA,EACnB;AAIA,QAAM,cAAU;AAAA,IACd,CAAC,SAAiB,aAAa,KAAK,IAAI,KAAK,IAAI,GAAG,IAAI,GAAG,SAAS,CAAC;AAAA,IACrE,CAAC,SAAS;AAAA,EACZ;AACA,QAAM,eAAW;AAAA,IACf,MAAM,aAAa,KAAK,IAAI,cAAc,GAAG,SAAS,CAAC;AAAA,IACvD,CAAC,aAAa,SAAS;AAAA,EACzB;AACA,QAAM,eAAW,0BAAY,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,mBAAmB;AA2FL;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,6CAAC,SAAI,eAAW,iBAAG,aAAa,SAAS,GACtC;AAAA,WAAO,SAAS,KACf,4CAAC,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,4CAAC,OAAE,WAAU,2CAA2C,sBAAW,IAEnE,4CAAC,QAAG,eAAW,iBAAG,iBAAiB,aAAa,GAC7C,qBAAW,MAAM,IAAI,CAAC,SACrB,4CAAC,QAAuB,qBAAW,IAAI,KAA9B,OAAO,IAAI,CAAqB,CAC1C,GACH;AAAA,IAGD,WAAW,YAAY,KACtB,6CAAC,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,4CAAC,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,eAAW;AAAA,QACT;AAAA,QACA,UACI,QACE,0DACA,0DACF;AAAA,MACN;AAAA,MAEC;AAAA;AAAA,EACH;AAEJ;;;AC1LA,YAAuB;AACvB,IAAAA,gBAAmB;;;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,IAAAC,sBAAA;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,6CAAC,SAAI,WAAW,MAAM,SAAQ,aAAY,MAAK,gBAAe,eAAY,QAAO,WAAU,SACzF,uDAAC,UAAK,GAAE,mSAAkS,GAC5S;AAEJ;AAEA,SAAS,QAAQ;AACf,SACE,6CAAC,SAAI,WAAW,MAAM,SAAQ,aAAY,MAAK,gBAAe,eAAY,QAAO,WAAU,SACzF,uDAAC,UAAK,GAAE,+JAA8J,GACxK;AAEJ;AAEA,SAAS,eAAe;AACtB,SACE,6CAAC,SAAI,WAAW,MAAM,SAAQ,aAAY,MAAK,gBAAe,eAAY,QAAO,WAAU,SACzF,uDAAC,UAAK,GAAE,0LAAyL,GACnM;AAEJ;AAEA,SAAS,eAAe;AACtB,SACE,6CAAC,SAAI,WAAW,MAAM,SAAQ,aAAY,MAAK,gBAAe,eAAY,QAAO,WAAU,SACzF,uDAAC,UAAK,GAAE,8yBAA6yB,GACvzB;AAEJ;AAEA,SAAS,WAAW;AAClB,SACE,8CAAC,SAAI,WAAW,MAAM,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,KAAI,eAAc,SAAQ,gBAAe,SAAQ,eAAY,QAAO,WAAU,SACpK;AAAA,iDAAC,UAAK,GAAE,KAAI,GAAE,KAAI,OAAM,MAAK,QAAO,MAAK,IAAG,KAAI,IAAG,KAAI;AAAA,IACvD,6CAAC,UAAK,GAAE,2DAA0D;AAAA,KACpE;AAEJ;AAEA,SAAS,YAAY;AACnB,SACE,6CAAC,SAAI,WAAW,MAAM,SAAQ,aAAY,MAAK,QAAO,QAAO,gBAAe,aAAY,KAAI,eAAc,SAAQ,gBAAe,SAAQ,eAAY,QAAO,WAAU,SACpK,uDAAC,cAAS,QAAO,kBAAiB,GACpC;AAEJ;AAEO,SAAS,aAAa,EAAE,KAAK,OAAO,WAAW,OAAO,GAAsB;AACjF,QAAM,IAAI,EAAE,GAAGA,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,8CAAC,SAAI,eAAW,kBAAG,2BAA2B,SAAS,GAAG,MAAK,SAAQ,cAAY,EAAE,OACnF;AAAA,iDAAC,UAAK,WAAU,kDAAkD,YAAE,OAAM;AAAA,IAE1E,6CAAC,OAAE,MAAM,MAAM,UAAU,QAAO,UAAS,KAAI,uBAAsB,WAAW,YAAY,cAAY,EAAE,UACtG,uDAAC,gBAAa,GAChB;AAAA,IACA,6CAAC,OAAE,MAAM,MAAM,GAAG,QAAO,UAAS,KAAI,uBAAsB,WAAW,YAAY,cAAY,EAAE,GAC/F,uDAAC,SAAM,GACT;AAAA,IACA,6CAAC,OAAE,MAAM,MAAM,UAAU,QAAO,UAAS,KAAI,uBAAsB,WAAW,YAAY,cAAY,EAAE,UACtG,uDAAC,gBAAa,GAChB;AAAA,IACA,6CAAC,OAAE,MAAM,MAAM,UAAU,QAAO,UAAS,KAAI,uBAAsB,WAAW,YAAY,cAAY,EAAE,UACtG,uDAAC,gBAAa,GAChB;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS;AAAA,QACT,eAAW,kBAAG,YAAY,qBAAqB,UAAU,6BAA6B;AAAA,QACtF,cAAY,EAAE;AAAA,QAEb;AAAA,mBAAS,6CAAC,aAAU,IAAK,6CAAC,YAAS;AAAA,UACpC,6CAAC,UAAK,WAAU,WAAW,mBAAS,EAAE,SAAS,EAAE,MAAK;AAAA;AAAA;AAAA,IACxD;AAAA,IAGA,6CAAC,UAAK,MAAK,UAAS,aAAU,UAAS,WAAU,WAC9C,mBAAS,EAAE,aAAa,IAC3B;AAAA,KACF;AAEJ;","names":["import_utils","import_jsx_runtime","DEFAULT_LABELS"]}
|
package/dist/client.js
CHANGED
|
@@ -93,7 +93,7 @@ function useCollection(options) {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
// src/components/collection.tsx
|
|
96
|
-
import { cn } from "@reblu/site-ui";
|
|
96
|
+
import { cn } from "@reblu/site-ui/utils";
|
|
97
97
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
98
98
|
var DEFAULT_LABELS = {
|
|
99
99
|
previous: "Previous",
|
|
@@ -203,7 +203,7 @@ function FacetChip({ label, pressed, color, onClick }) {
|
|
|
203
203
|
|
|
204
204
|
// src/components/share-buttons.tsx
|
|
205
205
|
import * as React from "react";
|
|
206
|
-
import { cn as cn2 } from "@reblu/site-ui";
|
|
206
|
+
import { cn as cn2 } from "@reblu/site-ui/utils";
|
|
207
207
|
|
|
208
208
|
// src/lib/share-urls.ts
|
|
209
209
|
function buildShareUrls(url, title) {
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/hooks/use-collection.ts","../src/components/collection.tsx","../src/components/share-buttons.tsx","../src/lib/share-urls.ts"],"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'\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'\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":";;;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,qBAAqB,UAAU,6BAA6B;AAAA,QACtF,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;","names":["cn","jsx","jsxs","DEFAULT_LABELS","cn"]}
|
|
1
|
+
{"version":3,"sources":["../src/hooks/use-collection.ts","../src/components/collection.tsx","../src/components/share-buttons.tsx","../src/lib/share-urls.ts"],"sourcesContent":["'use client'\n\nimport { useCallback, useMemo, useState } from 'react'\n\n/**\n * `useCollection` — the headless heart of `@reblu/site-blocks`.\n *\n * A framework-agnostic, presentation-free hook that turns an in-memory array into\n * a filtered / searched / sorted / paginated view. It backs BOTH the page index\n * and the blog index: the facets differ (category, tag, …), the machinery does\n * not. Callers own how items look (`Collection` or their own markup); this hook\n * only owns *which* items and in *what* order.\n *\n * All derivation is memoised and pure — comparators run over `toSorted`, never\n * mutating the caller's array.\n */\n\n/** A named facet: `key` identifies it, `predicate` decides membership for a value. */\nexport interface CollectionFilter<T> {\n key: string\n predicate: (item: T, value: string) => boolean\n}\n\nexport interface UseCollectionOptions<T> {\n items: T[]\n /** Page size. `0`/omitted → no pagination (one page with everything). */\n pageSize?: number\n initialPage?: number\n filters?: CollectionFilter<T>[]\n initialFilters?: Record<string, string | undefined>\n /** Free-text matcher; only applied when a non-empty term is set. */\n search?: (item: T, term: string) => boolean\n initialSearch?: string\n /** Named comparators, selected by `sort`. */\n sorts?: Record<string, (a: T, b: T) => number>\n initialSort?: string\n}\n\nexport interface UseCollectionResult<T> {\n /** Items on the current page (or all items when pagination is off). */\n items: T[]\n /** Full filtered + sorted set, before pagination. */\n filtered: T[]\n /** Size of `filtered`. */\n total: number\n page: number\n pageCount: number\n pageSize: number\n isEmpty: boolean\n activeFilters: Record<string, string | undefined>\n setFilter: (key: string, value: string | undefined) => void\n clearFilters: () => void\n search: string\n setSearch: (term: string) => void\n sort: string | undefined\n setSort: (key: string | undefined) => void\n setPage: (page: number) => void\n nextPage: () => void\n prevPage: () => void\n}\n\nexport function useCollection<T>(options: UseCollectionOptions<T>): UseCollectionResult<T> {\n const { items, pageSize = 0, filters = [], search: searchFn, sorts = {} } = options\n\n const [activeFilters, setActiveFilters] = useState<Record<string, string | undefined>>(\n () => options.initialFilters ?? {},\n )\n const [search, setSearchState] = useState(() => options.initialSearch ?? '')\n const [sort, setSortState] = useState<string | undefined>(() => options.initialSort)\n const [page, setPageState] = useState(() => options.initialPage ?? 1)\n\n const filtered = useMemo(() => {\n let next = items\n\n for (const { key, predicate } of filters) {\n const value = activeFilters[key]\n if (value != null && value !== '') {\n next = next.filter((item) => predicate(item, value))\n }\n }\n\n const term = search.trim()\n if (searchFn && term) {\n next = next.filter((item) => searchFn(item, term))\n }\n\n const comparator = sort ? sorts[sort] : undefined\n if (comparator) {\n next = next.slice().sort(comparator)\n }\n\n return next\n }, [items, filters, activeFilters, search, searchFn, sort, sorts])\n\n const total = filtered.length\n const pageCount = pageSize > 0 ? Math.max(1, Math.ceil(total / pageSize)) : 1\n // Derive the effective page so a shrinking result set never strands us past the\n // last page — state stays as-is, but what we render is always clamped.\n const currentPage = Math.min(Math.max(1, page), pageCount)\n\n const pageItems = useMemo(() => {\n if (pageSize <= 0) return filtered\n const start = (currentPage - 1) * pageSize\n return filtered.slice(start, start + pageSize)\n }, [filtered, pageSize, currentPage])\n\n // Any change to what we're viewing snaps back to the first page.\n const resetToFirstPage = useCallback(() => setPageState(1), [])\n\n const setFilter = useCallback(\n (key: string, value: string | undefined) => {\n setActiveFilters((prev) => ({ ...prev, [key]: value }))\n resetToFirstPage()\n },\n [resetToFirstPage],\n )\n\n const clearFilters = useCallback(() => {\n setActiveFilters({})\n resetToFirstPage()\n }, [resetToFirstPage])\n\n const setSearch = useCallback(\n (term: string) => {\n setSearchState(term)\n resetToFirstPage()\n },\n [resetToFirstPage],\n )\n\n const setSort = useCallback(\n (key: string | undefined) => {\n setSortState(key)\n resetToFirstPage()\n },\n [resetToFirstPage],\n )\n\n // Navigation clamps against the live page bounds so a shrinking result set or\n // an over-shooting jump can never strand the state past the last page.\n const setPage = useCallback(\n (next: number) => setPageState(Math.min(Math.max(1, next), pageCount)),\n [pageCount],\n )\n const nextPage = useCallback(\n () => setPageState(Math.min(currentPage + 1, pageCount)),\n [currentPage, pageCount],\n )\n const prevPage = useCallback(() => setPageState(Math.max(1, currentPage - 1)), [currentPage])\n\n return {\n items: pageItems,\n filtered,\n total,\n page: currentPage,\n pageCount,\n pageSize,\n isEmpty: total === 0,\n activeFilters,\n setFilter,\n clearFilters,\n search,\n setSearch,\n sort,\n setSort,\n setPage,\n nextPage,\n prevPage,\n }\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '@reblu/site-ui/utils'\n\nimport { useCollection, type CollectionFilter } from '../hooks/use-collection'\n\n/**\n * `Collection` — token-driven presentational grid over {@link useCollection}.\n *\n * The headless hook owns filtering/sorting/pagination; this component owns the\n * chrome: a facet chip bar, the item grid (via a `renderItem` prop), and simple\n * pagination. It carries no brand aesthetic — colors come from tokens; a facet\n * option may carry a `color` (tenant hex) which is applied through a CSS custom\n * property, never baked into a class name.\n *\n * Serves both the page index and the blog index; only the facets differ.\n */\nexport interface CollectionFacetOption {\n value: string\n label: string\n /** Optional tenant color (hex). Applied via a CSS var, not a hardcoded class. */\n color?: string\n}\n\nexport interface CollectionFacet {\n key: string\n label: string\n options: CollectionFacetOption[]\n /** Label for the \"clear this facet\" chip. Defaults to \"All\". */\n allLabel?: string\n}\n\nexport interface CollectionLabels {\n previous: string\n next: string\n page: (page: number, pageCount: number) => string\n}\n\nconst DEFAULT_LABELS: CollectionLabels = {\n previous: 'Previous',\n next: 'Next',\n page: (page, pageCount) => `Page ${page} of ${pageCount}`,\n}\n\nexport interface CollectionProps<T> {\n items: T[]\n renderItem: (item: T) => React.ReactNode\n getKey: (item: T) => string\n filters?: CollectionFilter<T>[]\n facets?: CollectionFacet[]\n pageSize?: number\n sorts?: Record<string, (a: T, b: T) => number>\n initialFilters?: Record<string, string | undefined>\n initialSort?: string\n emptyLabel?: string\n className?: string\n /** Tailwind classes for the grid container. */\n gridClassName?: string\n labels?: Partial<CollectionLabels>\n}\n\nexport function Collection<T>({\n items,\n renderItem,\n getKey,\n filters,\n facets = [],\n pageSize = 0,\n sorts,\n initialFilters,\n initialSort,\n emptyLabel = 'No results',\n className,\n gridClassName = 'grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3',\n labels,\n}: CollectionProps<T>) {\n const t = { ...DEFAULT_LABELS, ...labels }\n const collection = useCollection<T>({\n items,\n filters,\n pageSize,\n sorts,\n initialFilters,\n initialSort,\n })\n\n return (\n <div className={cn('space-y-8', className)}>\n {facets.length > 0 && (\n <div className='space-y-4'>\n {facets.map((facet) => {\n const active = collection.activeFilters[facet.key]\n return (\n <div\n key={facet.key}\n role='group'\n aria-label={facet.label}\n className='flex flex-wrap gap-2'\n >\n <FacetChip\n label={facet.allLabel ?? 'All'}\n pressed={active == null || active === ''}\n onClick={() => collection.setFilter(facet.key, undefined)}\n />\n {facet.options.map((opt) => (\n <FacetChip\n key={opt.value}\n label={opt.label}\n color={opt.color}\n pressed={active === opt.value}\n onClick={() => collection.setFilter(facet.key, opt.value)}\n />\n ))}\n </div>\n )\n })}\n </div>\n )}\n\n {collection.isEmpty ? (\n <p className='py-16 text-center text-muted-foreground'>{emptyLabel}</p>\n ) : (\n <ul className={cn('list-none p-0', gridClassName)}>\n {collection.items.map((item) => (\n <li key={getKey(item)}>{renderItem(item)}</li>\n ))}\n </ul>\n )}\n\n {collection.pageCount > 1 && (\n <nav aria-label='Pagination' className='flex items-center justify-center gap-4'>\n <button\n type='button'\n onClick={collection.prevPage}\n disabled={collection.page <= 1}\n className={PAGE_BTN_CLASS}\n >\n {t.previous}\n </button>\n <span aria-current='page' className='text-sm text-muted-foreground'>\n {t.page(collection.page, collection.pageCount)}\n </span>\n <button\n type='button'\n onClick={collection.nextPage}\n disabled={collection.page >= collection.pageCount}\n className={PAGE_BTN_CLASS}\n >\n {t.next}\n </button>\n </nav>\n )}\n </div>\n )\n}\n\nconst PAGE_BTN_CLASS =\n 'inline-flex h-9 items-center rounded-md border border-border px-4 text-sm font-medium transition-colors hover:bg-muted disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'\n\ninterface FacetChipProps {\n label: string\n pressed: boolean\n color?: string\n onClick: () => void\n}\n\nfunction FacetChip({ label, pressed, color, onClick }: FacetChipProps) {\n return (\n <button\n type='button'\n aria-pressed={pressed}\n onClick={onClick}\n // The tenant color rides in a CSS var; the class picks it up when pressed,\n // so no hex string is ever baked into a Tailwind class.\n style={color ? ({ '--facet-color': color } as React.CSSProperties) : undefined}\n className={cn(\n 'inline-flex items-center rounded-full border px-3 py-1 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n pressed\n ? color\n ? 'border-transparent bg-[var(--facet-color)] text-white'\n : 'border-transparent bg-primary text-primary-foreground'\n : 'border-border text-muted-foreground hover:bg-muted hover:text-foreground',\n )}\n >\n {label}\n </button>\n )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '@reblu/site-ui/utils'\n\nimport { buildShareUrls } from '../lib/share-urls'\n\n/**\n * `ShareButtons` — social share row for a blog post (LinkedIn / X / Facebook /\n * WhatsApp) plus a copy-to-clipboard action. Token-driven and icon-only for the\n * providers; the copy action shows a text confirmation.\n *\n * Copy defaults to English (distributable default). A Spanish-or-other SITE can\n * override every visible string via `labels` without forking the component.\n */\nexport interface ShareButtonsLabels {\n share: string\n copy: string\n copied: string\n linkedin: string\n x: string\n facebook: string\n whatsapp: string\n copyStatus: string\n}\n\nconst DEFAULT_LABELS: ShareButtonsLabels = {\n share: 'Share',\n copy: 'Copy link',\n copied: 'Link copied',\n linkedin: 'Share on LinkedIn',\n x: 'Share on X',\n facebook: 'Share on Facebook',\n whatsapp: 'Share on WhatsApp',\n copyStatus: 'Link copied to clipboard',\n}\n\nexport interface ShareButtonsProps {\n /** Canonical, absolute URL of the post being shared. */\n url: string\n /** Post title, used where the provider supports pre-filled copy. */\n title: string\n className?: string\n labels?: Partial<ShareButtonsLabels>\n}\n\nconst LINK_CLASS =\n 'inline-flex h-9 w-9 items-center justify-center rounded-full border border-border text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'\n\nconst ICON = 'h-4 w-4'\n\nfunction LinkedInIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='currentColor' aria-hidden='true' focusable='false'>\n <path d='M19 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2zM8.34 18.34V10.9H5.9v7.44zM7.12 9.82a1.42 1.42 0 1 0 0-2.84 1.42 1.42 0 0 0 0 2.84m11.22 8.52v-4.08c0-2.18-1.16-3.19-2.72-3.19a2.35 2.35 0 0 0-2.13 1.17v-1h-2.43v7.1h2.43v-3.93c0-1 .2-2 1.46-2s1.28 1.16 1.28 2.06v3.87z' />\n </svg>\n )\n}\n\nfunction XIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='currentColor' aria-hidden='true' focusable='false'>\n <path d='M18.244 2.25h3.308l-7.227 8.26 8.502 11.24h-6.66l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z' />\n </svg>\n )\n}\n\nfunction FacebookIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='currentColor' aria-hidden='true' focusable='false'>\n <path d='M22 12a10 10 0 1 0-11.56 9.88v-6.99H7.9V12h2.54V9.8c0-2.5 1.49-3.89 3.78-3.89 1.09 0 2.24.2 2.24.2v2.46h-1.26c-1.24 0-1.63.77-1.63 1.56V12h2.78l-.44 2.89h-2.34v6.99A10 10 0 0 0 22 12' />\n </svg>\n )\n}\n\nfunction WhatsAppIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='currentColor' aria-hidden='true' focusable='false'>\n <path d='M12.04 2C6.58 2 2.13 6.45 2.13 11.91c0 1.75.46 3.45 1.32 4.95L2 22l5.25-1.38a9.9 9.9 0 0 0 4.79 1.22h.01c5.46 0 9.91-4.45 9.91-9.91 0-2.65-1.03-5.14-2.9-7.01A9.82 9.82 0 0 0 12.04 2m0 1.67c2.2 0 4.27.86 5.82 2.42a8.19 8.19 0 0 1 2.42 5.82c0 4.54-3.7 8.24-8.25 8.24a8.2 8.2 0 0 1-4.2-1.15l-.3-.18-3.12.82.83-3.04-.2-.31a8.2 8.2 0 0 1-1.26-4.38c0-4.54 3.7-8.24 8.25-8.24m4.52 10.36c-.06-.1-.22-.16-.46-.28s-1.46-.72-1.68-.8-.39-.12-.55.12-.63.8-.77.96-.28.18-.52.06a6.7 6.7 0 0 1-1.98-1.22 7.4 7.4 0 0 1-1.37-1.7c-.14-.24-.01-.37.11-.49.11-.11.24-.28.37-.42.12-.14.16-.24.24-.4s.04-.3-.02-.42-.55-1.32-.75-1.81c-.2-.48-.4-.41-.55-.42h-.47a.9.9 0 0 0-.65.3c-.22.24-.85.83-.85 2.02s.87 2.35 1 2.51c.12.16 1.71 2.61 4.14 3.66.58.25 1.03.4 1.38.51.58.19 1.11.16 1.53.1.47-.07 1.46-.6 1.66-1.17.21-.58.21-1.07.15-1.17' />\n </svg>\n )\n}\n\nfunction CopyIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='none' stroke='currentColor' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round' aria-hidden='true' focusable='false'>\n <rect x='9' y='9' width='13' height='13' rx='2' ry='2' />\n <path d='M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1' />\n </svg>\n )\n}\n\nfunction CheckIcon() {\n return (\n <svg className={ICON} viewBox='0 0 24 24' fill='none' stroke='currentColor' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round' aria-hidden='true' focusable='false'>\n <polyline points='20 6 9 17 4 12' />\n </svg>\n )\n}\n\nexport function ShareButtons({ url, title, className, labels }: ShareButtonsProps) {\n const t = { ...DEFAULT_LABELS, ...labels }\n const [copied, setCopied] = React.useState(false)\n const share = buildShareUrls(url, title)\n\n async function handleCopy() {\n try {\n await navigator.clipboard.writeText(url)\n setCopied(true)\n window.setTimeout(() => setCopied(false), 2000)\n } catch {\n // Clipboard can be unavailable (permissions, insecure context); fail quietly.\n }\n }\n\n return (\n <div className={cn('flex items-center gap-2', className)} role='group' aria-label={t.share}>\n <span className='mr-1 text-sm font-medium text-muted-foreground'>{t.share}</span>\n\n <a href={share.linkedin} target='_blank' rel='noopener noreferrer' className={LINK_CLASS} aria-label={t.linkedin}>\n <LinkedInIcon />\n </a>\n <a href={share.x} target='_blank' rel='noopener noreferrer' className={LINK_CLASS} aria-label={t.x}>\n <XIcon />\n </a>\n <a href={share.facebook} target='_blank' rel='noopener noreferrer' className={LINK_CLASS} aria-label={t.facebook}>\n <FacebookIcon />\n </a>\n <a href={share.whatsapp} target='_blank' rel='noopener noreferrer' className={LINK_CLASS} aria-label={t.whatsapp}>\n <WhatsAppIcon />\n </a>\n\n <button\n type='button'\n onClick={handleCopy}\n className={cn(LINK_CLASS, 'w-auto gap-2 px-3', copied && 'border-primary text-primary')}\n aria-label={t.copy}\n >\n {copied ? <CheckIcon /> : <CopyIcon />}\n <span className='text-sm'>{copied ? t.copied : t.copy}</span>\n </button>\n\n {/* Polite live region so screen readers announce the copy confirmation. */}\n <span role='status' aria-live='polite' className='sr-only'>\n {copied ? t.copyStatus : ''}\n </span>\n </div>\n )\n}\n","/**\n * Pure builders for the social-share targets used by `ShareButtons`.\n *\n * Framework-free so the URL construction is unit-testable without rendering.\n * Every provider gets the canonical post URL; providers that support custom copy\n * (X, WhatsApp) also receive the post title.\n */\nexport interface ShareUrls {\n linkedin: string\n x: string\n facebook: string\n whatsapp: string\n}\n\nexport function buildShareUrls(url: string, title: string): ShareUrls {\n const encodedUrl = encodeURIComponent(url)\n const encodedTitle = encodeURIComponent(title)\n\n return {\n // LinkedIn derives title/description from the target page's Open Graph tags,\n // so it only takes the URL.\n linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`,\n // The intent endpoint keeps the /intent/tweet path under the x.com domain.\n x: `https://x.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}`,\n facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`,\n whatsapp: `https://wa.me/?text=${encodeURIComponent(`${title} ${url}`)}`,\n }\n}\n"],"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,qBAAqB,UAAU,6BAA6B;AAAA,QACtF,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;","names":["cn","jsx","jsxs","DEFAULT_LABELS","cn"]}
|
package/dist/index.cjs
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
2
|
var __defProp = Object.defineProperty;
|
|
4
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
6
|
var __export = (target, all3) => {
|
|
9
7
|
for (var name in all3)
|
|
@@ -17,14 +15,6 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
17
15
|
}
|
|
18
16
|
return to;
|
|
19
17
|
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
19
|
|
|
30
20
|
// src/index.ts
|
|
@@ -13053,9 +13043,13 @@ function markdownToSafeHtml(markdown) {
|
|
|
13053
13043
|
}
|
|
13054
13044
|
|
|
13055
13045
|
// src/components/markdown-renderer.tsx
|
|
13056
|
-
var
|
|
13057
|
-
var import_html_react_parser =
|
|
13046
|
+
var import_utils = require("@reblu/site-ui/utils");
|
|
13047
|
+
var import_html_react_parser = require("html-react-parser");
|
|
13058
13048
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
13049
|
+
var HTML_TO_DOM_OPTIONS = { lowerCaseAttributeNames: false };
|
|
13050
|
+
function parse3(html5, options) {
|
|
13051
|
+
return (0, import_html_react_parser.domToReact)((0, import_html_react_parser.htmlToDOM)(html5, HTML_TO_DOM_OPTIONS), options);
|
|
13052
|
+
}
|
|
13059
13053
|
var HEADING_CLASSES = {
|
|
13060
13054
|
1: "text-3xl font-bold mt-8 mb-4 first:mt-0",
|
|
13061
13055
|
2: "text-2xl font-bold mt-7 mb-3 first:mt-0",
|
|
@@ -13158,18 +13152,19 @@ function buildOptions(headingOffset) {
|
|
|
13158
13152
|
function MarkdownRenderer({ content, className, headingOffset = 0 }) {
|
|
13159
13153
|
const html5 = markdownToSafeHtml(content);
|
|
13160
13154
|
if (!html5) return null;
|
|
13161
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: (0,
|
|
13155
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: (0, import_utils.cn)("max-w-none", className), "data-slot": "markdown", children: parse3(html5, buildOptions(headingOffset)) });
|
|
13162
13156
|
}
|
|
13163
13157
|
function SafeHtml({ html: html5, className, headingOffset = 0 }) {
|
|
13164
13158
|
if (!html5) return null;
|
|
13165
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: (0,
|
|
13159
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: (0, import_utils.cn)("max-w-none", className), "data-slot": "safe-html", children: parse3(html5, buildOptions(headingOffset)) });
|
|
13166
13160
|
}
|
|
13167
13161
|
|
|
13168
13162
|
// src/components/treatment.tsx
|
|
13169
|
-
var
|
|
13163
|
+
var import_utils3 = require("@reblu/site-ui/utils");
|
|
13170
13164
|
|
|
13171
13165
|
// src/components/_page-parts.tsx
|
|
13172
|
-
var
|
|
13166
|
+
var import_utils2 = require("@reblu/site-ui/utils");
|
|
13167
|
+
var import_button_variants = require("@reblu/site-ui/button-variants");
|
|
13173
13168
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
13174
13169
|
var KNOWN_BUTTON_VARIANTS = /* @__PURE__ */ new Set([
|
|
13175
13170
|
"default",
|
|
@@ -13200,18 +13195,23 @@ function PageHero({ page }) {
|
|
|
13200
13195
|
}
|
|
13201
13196
|
function SectionCta({ section }) {
|
|
13202
13197
|
if (!section.ctaLabel || !section.ctaUrl) return null;
|
|
13203
|
-
|
|
13198
|
+
const variant = toButtonVariant(section.ctaVariant);
|
|
13199
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "pt-2", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
13204
13200
|
"a",
|
|
13205
13201
|
{
|
|
13206
13202
|
href: section.ctaUrl,
|
|
13203
|
+
"data-slot": "button",
|
|
13204
|
+
"data-variant": variant,
|
|
13205
|
+
"data-size": "default",
|
|
13206
|
+
className: (0, import_utils2.cn)((0, import_button_variants.buttonVariants)({ variant, size: "default" })),
|
|
13207
13207
|
...section.ctaIsExternal ? { target: "_blank", rel: "noopener noreferrer" } : {},
|
|
13208
13208
|
children: section.ctaLabel
|
|
13209
13209
|
}
|
|
13210
|
-
) })
|
|
13210
|
+
) });
|
|
13211
13211
|
}
|
|
13212
13212
|
function SectionImages({ section }) {
|
|
13213
13213
|
if (section.images.length === 0) return null;
|
|
13214
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: (0,
|
|
13214
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: (0, import_utils2.cn)("grid gap-4", section.images.length > 1 ? "sm:grid-cols-2" : "grid-cols-1"), children: section.images.map((img) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("figure", { className: "space-y-1", children: [
|
|
13215
13215
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "relative aspect-video w-full overflow-hidden rounded-lg bg-muted", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("img", { src: img.url, alt: img.alt, loading: "lazy", className: "h-full w-full object-cover" }) }),
|
|
13216
13216
|
img.caption && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("figcaption", { className: "text-sm text-muted-foreground", children: img.caption })
|
|
13217
13217
|
] }, `${img.url}-${img.order}`)) });
|
|
@@ -13248,14 +13248,14 @@ function PageSectionList({ sections }) {
|
|
|
13248
13248
|
// src/components/treatment.tsx
|
|
13249
13249
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
13250
13250
|
function Treatment({ page, className }) {
|
|
13251
|
-
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("article", { className: (0,
|
|
13251
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("article", { className: (0, import_utils3.cn)("space-y-12", className), "data-slot": "treatment", children: [
|
|
13252
13252
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PageHero, { page }),
|
|
13253
13253
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PageSectionList, { sections: page.sections })
|
|
13254
13254
|
] });
|
|
13255
13255
|
}
|
|
13256
13256
|
|
|
13257
13257
|
// src/components/specialty.tsx
|
|
13258
|
-
var
|
|
13258
|
+
var import_utils4 = require("@reblu/site-ui/utils");
|
|
13259
13259
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
13260
13260
|
function Specialty({
|
|
13261
13261
|
page,
|
|
@@ -13264,7 +13264,7 @@ function Specialty({
|
|
|
13264
13264
|
treatmentsLabel = "Treatments",
|
|
13265
13265
|
className
|
|
13266
13266
|
}) {
|
|
13267
|
-
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("article", { className: (0,
|
|
13267
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("article", { className: (0, import_utils4.cn)("space-y-12", className), "data-slot": "specialty", children: [
|
|
13268
13268
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PageHero, { page }),
|
|
13269
13269
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PageSectionList, { sections: page.sections }),
|
|
13270
13270
|
treatments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("section", { className: "space-y-6", children: [
|
|
@@ -13296,7 +13296,7 @@ function Specialty({
|
|
|
13296
13296
|
}
|
|
13297
13297
|
|
|
13298
13298
|
// src/components/profile-card.tsx
|
|
13299
|
-
var
|
|
13299
|
+
var import_utils5 = require("@reblu/site-ui/utils");
|
|
13300
13300
|
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
13301
13301
|
function initials(name) {
|
|
13302
13302
|
return name.split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]?.toUpperCase() ?? "").join("");
|
|
@@ -13306,7 +13306,7 @@ function ProfileCard({ name, role, imageUrl, specialties = [], bio, href, classN
|
|
|
13306
13306
|
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
13307
13307
|
"article",
|
|
13308
13308
|
{
|
|
13309
|
-
className: (0,
|
|
13309
|
+
className: (0, import_utils5.cn)(
|
|
13310
13310
|
"flex flex-col items-center gap-3 rounded-lg border bg-card p-6 text-center",
|
|
13311
13311
|
className
|
|
13312
13312
|
),
|
|
@@ -13338,7 +13338,7 @@ function ProfileCard({ name, role, imageUrl, specialties = [], bio, href, classN
|
|
|
13338
13338
|
}
|
|
13339
13339
|
|
|
13340
13340
|
// src/components/product-card.tsx
|
|
13341
|
-
var
|
|
13341
|
+
var import_utils6 = require("@reblu/site-ui/utils");
|
|
13342
13342
|
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
13343
13343
|
function ProductCard({ product, href, currencyCode = "EUR", locale, className }) {
|
|
13344
13344
|
const image = product.images.find((i) => i.isPrimary) ?? product.images[0];
|
|
@@ -13346,7 +13346,7 @@ function ProductCard({ product, href, currencyCode = "EUR", locale, className })
|
|
|
13346
13346
|
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
13347
13347
|
"article",
|
|
13348
13348
|
{
|
|
13349
|
-
className: (0,
|
|
13349
|
+
className: (0, import_utils6.cn)(
|
|
13350
13350
|
"group flex flex-col overflow-hidden rounded-lg border bg-card transition-shadow hover:shadow-lg",
|
|
13351
13351
|
className
|
|
13352
13352
|
),
|