@reblu/site-blocks 0.2.1

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/index.js ADDED
@@ -0,0 +1,415 @@
1
+ // src/lib/reading-time.ts
2
+ var WORDS_PER_MINUTE = 200;
3
+ function readingTime(content) {
4
+ if (!content?.trim()) return null;
5
+ const words = content.replace(/<[^>]*>/g, " ").split(/\s+/).filter(Boolean).length;
6
+ if (words === 0) return null;
7
+ return Math.max(1, Math.round(words / WORDS_PER_MINUTE));
8
+ }
9
+
10
+ // src/lib/format-price.ts
11
+ function formatPrice(cents, currency = "EUR", locale = "es-ES") {
12
+ return new Intl.NumberFormat(locale, {
13
+ style: "currency",
14
+ currency
15
+ }).format(cents / 100);
16
+ }
17
+
18
+ // src/lib/share-urls.ts
19
+ function buildShareUrls(url, title) {
20
+ const encodedUrl = encodeURIComponent(url);
21
+ const encodedTitle = encodeURIComponent(title);
22
+ return {
23
+ // LinkedIn derives title/description from the target page's Open Graph tags,
24
+ // so it only takes the URL.
25
+ linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`,
26
+ // The intent endpoint keeps the /intent/tweet path under the x.com domain.
27
+ x: `https://x.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}`,
28
+ facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`,
29
+ whatsapp: `https://wa.me/?text=${encodeURIComponent(`${title} ${url}`)}`
30
+ };
31
+ }
32
+
33
+ // src/lib/markdown.ts
34
+ import DOMPurify from "isomorphic-dompurify";
35
+ import { Marked } from "marked";
36
+ var ALLOWED_YOUTUBE_HOSTS = [
37
+ "youtube.com",
38
+ "www.youtube.com",
39
+ "youtube-nocookie.com",
40
+ "www.youtube-nocookie.com"
41
+ ];
42
+ function isAllowedYouTubeHost(src) {
43
+ if (!src) return false;
44
+ try {
45
+ const { host } = new URL(src);
46
+ return ALLOWED_YOUTUBE_HOSTS.includes(host);
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
51
+ var marked = new Marked({ gfm: true, breaks: false });
52
+ var PURIFY_CONFIG = {
53
+ ADD_TAGS: ["iframe"],
54
+ ADD_ATTR: ["allow", "allowfullscreen", "frameborder", "scrolling", "title", "loading", "target"]
55
+ };
56
+ function dropDisallowedIframes(node) {
57
+ if (node.nodeName?.toLowerCase() !== "iframe") return;
58
+ const src = node.getAttribute("src");
59
+ if (!isAllowedYouTubeHost(src)) {
60
+ node.remove();
61
+ }
62
+ }
63
+ function sanitizeHtml(html) {
64
+ if (!html?.trim()) return "";
65
+ DOMPurify.addHook("uponSanitizeElement", dropDisallowedIframes);
66
+ try {
67
+ return DOMPurify.sanitize(html, PURIFY_CONFIG);
68
+ } finally {
69
+ DOMPurify.removeHook("uponSanitizeElement");
70
+ }
71
+ }
72
+ function markdownToSafeHtml(markdown) {
73
+ if (!markdown?.trim()) return "";
74
+ const rawHtml = marked.parse(markdown, { async: false });
75
+ return sanitizeHtml(rawHtml);
76
+ }
77
+
78
+ // src/components/markdown-renderer.tsx
79
+ import { cn } from "@reblu/site-ui";
80
+ import parse, { domToReact, Element } from "html-react-parser";
81
+ import { Fragment, jsx } from "react/jsx-runtime";
82
+ var HEADING_CLASSES = {
83
+ 1: "text-3xl font-bold mt-8 mb-4 first:mt-0",
84
+ 2: "text-2xl font-bold mt-7 mb-3 first:mt-0",
85
+ 3: "text-xl font-bold mt-6 mb-2 first:mt-0",
86
+ 4: "text-lg font-bold mt-5 mb-2 first:mt-0",
87
+ 5: "text-base font-bold mt-4 mb-2 first:mt-0",
88
+ 6: "text-sm font-bold mt-4 mb-2 first:mt-0"
89
+ };
90
+ function hasClass(el, name) {
91
+ return typeof el.attribs.class === "string" && el.attribs.class.split(/\s+/).includes(name);
92
+ }
93
+ function isExternalHref(href) {
94
+ return !!href && /^(https?:)?\/\//.test(href);
95
+ }
96
+ function buildOptions(headingOffset) {
97
+ const options = {
98
+ replace: (node) => {
99
+ if (!(node instanceof Element)) return void 0;
100
+ const children = () => domToReact(node.children, options);
101
+ const { name, attribs } = node;
102
+ const headingMatch = /^h([1-6])$/.exec(name);
103
+ if (headingMatch) {
104
+ const rendered = Math.min(6, Number(headingMatch[1]) + headingOffset);
105
+ const Tag = `h${rendered}`;
106
+ return /* @__PURE__ */ jsx(Tag, { className: HEADING_CLASSES[rendered], children: children() });
107
+ }
108
+ switch (name) {
109
+ case "p":
110
+ return /* @__PURE__ */ jsx("p", { className: "my-4 leading-relaxed text-foreground/90", children: children() });
111
+ case "ul":
112
+ return /* @__PURE__ */ jsx("ul", { className: hasClass(node, "contains-task-list") ? "list-none pl-0 my-4 space-y-1" : "list-disc ml-6 my-4", children: children() });
113
+ case "ol":
114
+ return /* @__PURE__ */ jsx("ol", { className: "list-decimal ml-6 my-4", children: children() });
115
+ case "li":
116
+ return /* @__PURE__ */ jsx("li", { className: hasClass(node, "task-list-item") ? "flex items-start gap-2 my-1" : "my-1", children: children() });
117
+ case "blockquote":
118
+ return /* @__PURE__ */ jsx("blockquote", { className: "border-l-4 border-primary/50 pl-6 italic my-6 text-muted-foreground", children: children() });
119
+ case "pre":
120
+ return /* @__PURE__ */ jsx("pre", { className: "rounded-lg border border-input bg-muted p-4 my-6 overflow-x-auto font-mono text-sm", children: children() });
121
+ case "code": {
122
+ const isBlock = node.parent instanceof Element && node.parent.name === "pre";
123
+ if (isBlock) return /* @__PURE__ */ jsx("code", { className: "block", children: children() });
124
+ return /* @__PURE__ */ jsx("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono text-sm", children: children() });
125
+ }
126
+ case "a": {
127
+ const href = attribs.href;
128
+ const external = isExternalHref(href);
129
+ return /* @__PURE__ */ jsx(
130
+ "a",
131
+ {
132
+ href,
133
+ className: "text-primary underline underline-offset-4 hover:text-primary/80",
134
+ ...external ? { target: "_blank", rel: "noopener noreferrer" } : {},
135
+ children: children()
136
+ }
137
+ );
138
+ }
139
+ case "img":
140
+ if (!attribs.src) return /* @__PURE__ */ jsx(Fragment, {});
141
+ return (
142
+ // Plain <img> keeps the block portable — a distributable can't assume a
143
+ // tenant's next/image remotePatterns. Consumers can post-process if they
144
+ // want optimisation.
145
+ /* @__PURE__ */ jsx(
146
+ "img",
147
+ {
148
+ src: attribs.src,
149
+ alt: attribs.alt ?? "",
150
+ loading: "lazy",
151
+ className: "rounded-lg w-full h-auto my-6"
152
+ }
153
+ )
154
+ );
155
+ case "iframe":
156
+ return /* @__PURE__ */ jsx("div", { className: "relative my-6 aspect-video w-full overflow-hidden rounded-lg", children: /* @__PURE__ */ jsx(
157
+ "iframe",
158
+ {
159
+ src: attribs.src,
160
+ title: attribs.title ?? "Embedded video",
161
+ allow: attribs.allow ?? "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",
162
+ allowFullScreen: true,
163
+ className: "absolute inset-0 h-full w-full border-0"
164
+ }
165
+ ) });
166
+ case "hr":
167
+ return /* @__PURE__ */ jsx("hr", { className: "my-8 border-muted-foreground/30" });
168
+ case "strong":
169
+ return /* @__PURE__ */ jsx("strong", { className: "font-bold text-foreground", children: children() });
170
+ case "em":
171
+ return /* @__PURE__ */ jsx("em", { className: "italic", children: children() });
172
+ case "del":
173
+ return /* @__PURE__ */ jsx("del", { className: "line-through", children: children() });
174
+ default:
175
+ return void 0;
176
+ }
177
+ }
178
+ };
179
+ return options;
180
+ }
181
+ function MarkdownRenderer({ content, className, headingOffset = 0 }) {
182
+ const html = markdownToSafeHtml(content);
183
+ if (!html) return null;
184
+ return /* @__PURE__ */ jsx("div", { className: cn("max-w-none", className), "data-slot": "markdown", children: parse(html, buildOptions(headingOffset)) });
185
+ }
186
+ function SafeHtml({ html, className, headingOffset = 0 }) {
187
+ if (!html) return null;
188
+ return /* @__PURE__ */ jsx("div", { className: cn("max-w-none", className), "data-slot": "safe-html", children: parse(html, buildOptions(headingOffset)) });
189
+ }
190
+
191
+ // src/components/treatment.tsx
192
+ import { cn as cn3 } from "@reblu/site-ui";
193
+
194
+ // src/components/_page-parts.tsx
195
+ import { Button, cn as cn2 } from "@reblu/site-ui";
196
+ import { Fragment as Fragment2, jsx as jsx2, jsxs } from "react/jsx-runtime";
197
+ var KNOWN_BUTTON_VARIANTS = /* @__PURE__ */ new Set([
198
+ "default",
199
+ "destructive",
200
+ "outline",
201
+ "secondary",
202
+ "ghost",
203
+ "link"
204
+ ]);
205
+ function toButtonVariant(variant) {
206
+ return variant && KNOWN_BUTTON_VARIANTS.has(variant) ? variant : "default";
207
+ }
208
+ function PageHero({ page }) {
209
+ return /* @__PURE__ */ jsxs("header", { className: "space-y-4", children: [
210
+ page.category && /* @__PURE__ */ jsx2("span", { className: "inline-flex items-center rounded-full bg-secondary px-3 py-1 text-sm font-medium text-secondary-foreground", children: page.category.name }),
211
+ /* @__PURE__ */ jsx2("h1", { className: "text-4xl font-bold tracking-tight", children: page.title }),
212
+ page.subtitle && /* @__PURE__ */ jsx2("p", { className: "text-lg text-muted-foreground", children: page.subtitle }),
213
+ page.coverImageUrl && /* @__PURE__ */ jsx2("div", { className: "relative aspect-[21/9] w-full overflow-hidden rounded-xl bg-muted", children: /* @__PURE__ */ jsx2(
214
+ "img",
215
+ {
216
+ src: page.coverImageUrl,
217
+ alt: page.title,
218
+ loading: "lazy",
219
+ className: "h-full w-full object-cover"
220
+ }
221
+ ) })
222
+ ] });
223
+ }
224
+ function SectionCta({ section }) {
225
+ if (!section.ctaLabel || !section.ctaUrl) return null;
226
+ return /* @__PURE__ */ jsx2("div", { className: "pt-2", children: /* @__PURE__ */ jsx2(Button, { asChild: true, variant: toButtonVariant(section.ctaVariant), children: /* @__PURE__ */ jsx2(
227
+ "a",
228
+ {
229
+ href: section.ctaUrl,
230
+ ...section.ctaIsExternal ? { target: "_blank", rel: "noopener noreferrer" } : {},
231
+ children: section.ctaLabel
232
+ }
233
+ ) }) });
234
+ }
235
+ function SectionImages({ section }) {
236
+ if (section.images.length === 0) return null;
237
+ return /* @__PURE__ */ jsx2("div", { className: cn2("grid gap-4", section.images.length > 1 ? "sm:grid-cols-2" : "grid-cols-1"), children: section.images.map((img) => /* @__PURE__ */ jsxs("figure", { className: "space-y-1", children: [
238
+ /* @__PURE__ */ jsx2("div", { className: "relative aspect-video w-full overflow-hidden rounded-lg bg-muted", children: /* @__PURE__ */ jsx2("img", { src: img.url, alt: img.alt, loading: "lazy", className: "h-full w-full object-cover" }) }),
239
+ img.caption && /* @__PURE__ */ jsx2("figcaption", { className: "text-sm text-muted-foreground", children: img.caption })
240
+ ] }, `${img.url}-${img.order}`)) });
241
+ }
242
+ function PageSectionBlock({ section }) {
243
+ const imageFirst = section.layout === "IMAGE_TEXT";
244
+ const sideBySide = section.layout === "TEXT_IMAGE" || section.layout === "IMAGE_TEXT";
245
+ const hasImages = section.images.length > 0;
246
+ const body = /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
247
+ section.heading && /* @__PURE__ */ jsx2("h2", { className: "text-2xl font-bold", children: section.heading }),
248
+ /* @__PURE__ */ jsx2(SafeHtml, { html: sanitizeHtml(section.contentHtml), headingOffset: 2 }),
249
+ /* @__PURE__ */ jsx2(SectionCta, { section })
250
+ ] });
251
+ if (sideBySide && hasImages) {
252
+ return /* @__PURE__ */ jsx2("section", { className: "grid items-center gap-8 md:grid-cols-2", children: imageFirst ? /* @__PURE__ */ jsxs(Fragment2, { children: [
253
+ /* @__PURE__ */ jsx2(SectionImages, { section }),
254
+ body
255
+ ] }) : /* @__PURE__ */ jsxs(Fragment2, { children: [
256
+ body,
257
+ /* @__PURE__ */ jsx2(SectionImages, { section })
258
+ ] }) });
259
+ }
260
+ return /* @__PURE__ */ jsxs("section", { className: "space-y-4", children: [
261
+ body,
262
+ /* @__PURE__ */ jsx2(SectionImages, { section })
263
+ ] });
264
+ }
265
+ function PageSectionList({ sections }) {
266
+ if (sections.length === 0) return null;
267
+ const ordered = sections.slice().sort((a, b) => a.order - b.order);
268
+ return /* @__PURE__ */ jsx2("div", { className: "space-y-12", children: ordered.map((section) => /* @__PURE__ */ jsx2(PageSectionBlock, { section }, section.id)) });
269
+ }
270
+
271
+ // src/components/treatment.tsx
272
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
273
+ function Treatment({ page, className }) {
274
+ return /* @__PURE__ */ jsxs2("article", { className: cn3("space-y-12", className), "data-slot": "treatment", children: [
275
+ /* @__PURE__ */ jsx3(PageHero, { page }),
276
+ /* @__PURE__ */ jsx3(PageSectionList, { sections: page.sections })
277
+ ] });
278
+ }
279
+
280
+ // src/components/specialty.tsx
281
+ import { cn as cn4 } from "@reblu/site-ui";
282
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
283
+ function Specialty({
284
+ page,
285
+ treatments = [],
286
+ treatmentHref = (slug) => `/${slug}`,
287
+ treatmentsLabel = "Treatments",
288
+ className
289
+ }) {
290
+ return /* @__PURE__ */ jsxs3("article", { className: cn4("space-y-12", className), "data-slot": "specialty", children: [
291
+ /* @__PURE__ */ jsx4(PageHero, { page }),
292
+ /* @__PURE__ */ jsx4(PageSectionList, { sections: page.sections }),
293
+ treatments.length > 0 && /* @__PURE__ */ jsxs3("section", { className: "space-y-6", children: [
294
+ /* @__PURE__ */ jsx4("h2", { className: "text-2xl font-bold", children: treatmentsLabel }),
295
+ /* @__PURE__ */ jsx4("ul", { className: "grid list-none grid-cols-1 gap-6 p-0 sm:grid-cols-2 lg:grid-cols-3", children: treatments.map((t) => /* @__PURE__ */ jsx4("li", { children: /* @__PURE__ */ jsxs3(
296
+ "a",
297
+ {
298
+ href: treatmentHref(t.slug),
299
+ className: "group flex h-full flex-col overflow-hidden rounded-lg border bg-card transition-shadow hover:shadow-lg",
300
+ children: [
301
+ t.coverImageUrl && /* @__PURE__ */ jsx4("div", { className: "relative aspect-video overflow-hidden bg-muted", children: /* @__PURE__ */ jsx4(
302
+ "img",
303
+ {
304
+ src: t.coverImageUrl,
305
+ alt: "",
306
+ loading: "lazy",
307
+ className: "h-full w-full object-cover transition-transform group-hover:scale-105"
308
+ }
309
+ ) }),
310
+ /* @__PURE__ */ jsxs3("div", { className: "flex flex-1 flex-col gap-2 p-4", children: [
311
+ /* @__PURE__ */ jsx4("h3", { className: "font-semibold leading-tight transition-colors group-hover:text-primary", children: t.title }),
312
+ t.excerpt && /* @__PURE__ */ jsx4("p", { className: "line-clamp-2 text-sm text-muted-foreground", children: t.excerpt })
313
+ ] })
314
+ ]
315
+ }
316
+ ) }, t.slug)) })
317
+ ] })
318
+ ] });
319
+ }
320
+
321
+ // src/components/profile-card.tsx
322
+ import { cn as cn5 } from "@reblu/site-ui";
323
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
324
+ function initials(name) {
325
+ return name.split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]?.toUpperCase() ?? "").join("");
326
+ }
327
+ function ProfileCard({ name, role, imageUrl, specialties = [], bio, href, className }) {
328
+ const NameTag = href ? "a" : "span";
329
+ return /* @__PURE__ */ jsxs4(
330
+ "article",
331
+ {
332
+ className: cn5(
333
+ "flex flex-col items-center gap-3 rounded-lg border bg-card p-6 text-center",
334
+ className
335
+ ),
336
+ "data-slot": "profile-card",
337
+ children: [
338
+ /* @__PURE__ */ jsx5("div", { className: "relative h-24 w-24 overflow-hidden rounded-full bg-muted", children: imageUrl ? /* @__PURE__ */ jsx5("img", { src: imageUrl, alt: name, loading: "lazy", className: "h-full w-full object-cover" }) : /* @__PURE__ */ jsx5("span", { className: "flex h-full w-full items-center justify-center text-2xl font-semibold text-muted-foreground", "aria-hidden": "true", children: initials(name) }) }),
339
+ /* @__PURE__ */ jsxs4("div", { className: "space-y-1", children: [
340
+ /* @__PURE__ */ jsx5("h3", { className: "text-lg font-semibold leading-tight", children: /* @__PURE__ */ jsx5(
341
+ NameTag,
342
+ {
343
+ ...href ? { href, className: "transition-colors hover:text-primary" } : {},
344
+ children: name
345
+ }
346
+ ) }),
347
+ role && /* @__PURE__ */ jsx5("p", { className: "text-sm text-muted-foreground", children: role })
348
+ ] }),
349
+ specialties.length > 0 && /* @__PURE__ */ jsx5("ul", { className: "flex flex-wrap justify-center gap-1.5 p-0", children: specialties.map((s) => /* @__PURE__ */ jsx5(
350
+ "li",
351
+ {
352
+ className: "inline-flex items-center rounded-full bg-secondary px-2.5 py-0.5 text-xs font-medium text-secondary-foreground",
353
+ children: s
354
+ },
355
+ s
356
+ )) }),
357
+ bio && /* @__PURE__ */ jsx5("p", { className: "text-sm text-muted-foreground", children: bio })
358
+ ]
359
+ }
360
+ );
361
+ }
362
+
363
+ // src/components/product-card.tsx
364
+ import { cn as cn6 } from "@reblu/site-ui";
365
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
366
+ function ProductCard({ product, href, currencyCode = "EUR", locale, className }) {
367
+ const image = product.images.find((i) => i.isPrimary) ?? product.images[0];
368
+ const hasDiscount = product.comparePrice != null && product.comparePrice > product.price;
369
+ return /* @__PURE__ */ jsxs5(
370
+ "article",
371
+ {
372
+ className: cn6(
373
+ "group flex flex-col overflow-hidden rounded-lg border bg-card transition-shadow hover:shadow-lg",
374
+ className
375
+ ),
376
+ "data-slot": "product-card",
377
+ children: [
378
+ /* @__PURE__ */ jsx6("div", { className: "relative aspect-square overflow-hidden bg-muted", children: image ? /* @__PURE__ */ jsx6(
379
+ "img",
380
+ {
381
+ src: image.url,
382
+ alt: image.alt ?? product.name,
383
+ loading: "lazy",
384
+ className: "h-full w-full object-cover transition-transform group-hover:scale-105"
385
+ }
386
+ ) : /* @__PURE__ */ jsx6("div", { className: "flex h-full items-center justify-center text-4xl text-muted-foreground/30", "aria-hidden": "true", children: "\u{1F6CD}\uFE0F" }) }),
387
+ /* @__PURE__ */ jsxs5("div", { className: "flex flex-1 flex-col gap-2 p-4", children: [
388
+ product.category && /* @__PURE__ */ jsx6("span", { className: "inline-flex w-fit items-center rounded-full bg-secondary px-2.5 py-0.5 text-xs font-medium text-secondary-foreground", children: product.category.name }),
389
+ /* @__PURE__ */ jsx6("h3", { className: "text-base font-semibold leading-tight", children: href ? /* @__PURE__ */ jsx6("a", { href, className: "transition-colors hover:text-primary", children: product.name }) : product.name }),
390
+ product.shortDescription && /* @__PURE__ */ jsx6("p", { className: "line-clamp-2 text-sm text-muted-foreground", children: product.shortDescription }),
391
+ /* @__PURE__ */ jsxs5("div", { className: "mt-auto flex items-baseline gap-2 pt-2", children: [
392
+ /* @__PURE__ */ jsx6("span", { className: "text-lg font-bold text-foreground", children: formatPrice(product.price, currencyCode, locale) }),
393
+ hasDiscount && /* @__PURE__ */ jsx6("span", { className: "text-sm text-muted-foreground line-through", children: formatPrice(product.comparePrice, currencyCode, locale) })
394
+ ] })
395
+ ] })
396
+ ]
397
+ }
398
+ );
399
+ }
400
+ export {
401
+ ALLOWED_YOUTUBE_HOSTS,
402
+ MarkdownRenderer,
403
+ ProductCard,
404
+ ProfileCard,
405
+ SafeHtml,
406
+ Specialty,
407
+ Treatment,
408
+ buildShareUrls,
409
+ formatPrice,
410
+ isAllowedYouTubeHost,
411
+ markdownToSafeHtml,
412
+ readingTime,
413
+ sanitizeHtml
414
+ };
415
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/reading-time.ts","../src/lib/format-price.ts","../src/lib/share-urls.ts","../src/lib/markdown.ts","../src/components/markdown-renderer.tsx","../src/components/treatment.tsx","../src/components/_page-parts.tsx","../src/components/specialty.tsx","../src/components/profile-card.tsx","../src/components/product-card.tsx"],"sourcesContent":["/**\n * Estimate a post's reading time in minutes from its content.\n *\n * The canonical helper for the Reblu SITE SDK: base-api does not persist reading\n * time on the blog contracts, so the SITE computes it wherever `content` is\n * present (the post detail and preview). The list contract omits `content`, so\n * cards show no reading time until the SDK surfaces the field.\n */\nconst WORDS_PER_MINUTE = 200\n\nexport function readingTime(content: string | null | undefined): number | null {\n if (!content?.trim()) return null\n // Strip HTML tags so markup does not inflate the word count.\n const words = content\n .replace(/<[^>]*>/g, ' ')\n .split(/\\s+/)\n .filter(Boolean).length\n if (words === 0) return null\n return Math.max(1, Math.round(words / WORDS_PER_MINUTE))\n}\n","/**\n * Format an integer-cents amount as a localized currency string.\n *\n * All Reblu shop monetary values travel as integer cents (Prisma `Int`, never\n * Decimal), so the SITE never sees floats. This helper is the single place that\n * converts cents → major units and applies `Intl.NumberFormat`, keeping price\n * rendering consistent across `ProductCard` and any future shop organism.\n */\nexport function formatPrice(cents: number, currency = 'EUR', locale = 'es-ES'): string {\n return new Intl.NumberFormat(locale, {\n style: 'currency',\n currency,\n }).format(cents / 100)\n}\n","/**\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","import DOMPurify from 'isomorphic-dompurify'\nimport { Marked } from 'marked'\n\n/**\n * Canonical Blog markdown → **safe HTML** pipeline for the Reblu SITE SDK.\n *\n * Blog `content` arrives as raw Markdown (tiptap-markdown). This module owns the\n * single, hardened conversion every SITE reuses instead of reinventing it:\n *\n * Markdown ──(marked, GFM)──▶ HTML ──(DOMPurify)──▶ safe HTML\n *\n * DOMPurify is MANDATORY (XSS): the output is later parsed into React nodes by\n * `MarkdownRenderer` (no `dangerouslySetInnerHTML`), but sanitising here means the\n * string is safe regardless of how a consumer chooses to render it.\n *\n * `<iframe>` is the one non-default tag we allow, and ONLY when its `src` host is\n * on the YouTube allowlist — every other embed (arbitrary iframes) is dropped.\n */\n\n/** Hosts permitted for `<iframe>` embeds. Exact host match — no suffix tricks. */\nexport const ALLOWED_YOUTUBE_HOSTS: readonly string[] = [\n 'youtube.com',\n 'www.youtube.com',\n 'youtube-nocookie.com',\n 'www.youtube-nocookie.com',\n]\n\n/** True when `src` is an absolute URL whose host is exactly a YouTube host. */\nexport function isAllowedYouTubeHost(src: string | null | undefined): boolean {\n if (!src) return false\n try {\n const { host } = new URL(src)\n return ALLOWED_YOUTUBE_HOSTS.includes(host)\n } catch {\n // Relative or malformed URLs can't be host-verified → reject.\n return false\n }\n}\n\n// Local marked instance — avoids mutating the global `marked` singleton another\n// package (base-api) may also configure. GFM on; no single-newline <br> (tiptap\n// does not emit hard breaks on a lone newline).\nconst marked = new Marked({ gfm: true, breaks: false })\n\n/**\n * DOMPurify config: default-safe HTML plus `<iframe>` (host-gated by the hook\n * below) and a handful of attributes the YouTube embed and images need.\n */\nconst PURIFY_CONFIG = {\n ADD_TAGS: ['iframe'],\n ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling', 'title', 'loading', 'target'],\n}\n\n/**\n * Enforce the YouTube host allowlist. Runs as a scoped hook (added/removed around\n * each sanitise call) so we never leave a global DOMPurify hook installed for\n * other consumers of the shared singleton.\n */\nfunction dropDisallowedIframes(node: Element): void {\n if (node.nodeName?.toLowerCase() !== 'iframe') return\n const src = node.getAttribute('src')\n if (!isAllowedYouTubeHost(src)) {\n node.remove()\n }\n}\n\n/**\n * Sanitise an already-HTML string with the same policy (default-safe tags plus a\n * host-gated `<iframe>`). Use for content that arrives as HTML — e.g. page\n * `section.contentHtml` from base-api, which is `marked` output that has NOT been\n * sanitised server-side.\n */\nexport function sanitizeHtml(html: string | null | undefined): string {\n if (!html?.trim()) return ''\n\n DOMPurify.addHook('uponSanitizeElement', dropDisallowedIframes as never)\n try {\n // sanitize() is synchronous, so the scoped hook cannot leak into another call.\n return DOMPurify.sanitize(html, PURIFY_CONFIG)\n } finally {\n DOMPurify.removeHook('uponSanitizeElement')\n }\n}\n\n/** Convert raw Markdown to sanitised, XSS-safe HTML. */\nexport function markdownToSafeHtml(markdown: string | null | undefined): string {\n if (!markdown?.trim()) return ''\n const rawHtml = marked.parse(markdown, { async: false }) as string\n return sanitizeHtml(rawHtml)\n}\n","import * as React from 'react'\nimport { cn } from '@reblu/site-ui'\nimport parse, { domToReact, Element, type DOMNode, type HTMLReactParserOptions } from 'html-react-parser'\n\nimport { markdownToSafeHtml } from '../lib/markdown'\n\n/**\n * `MarkdownRenderer` — the canonical Blog body renderer.\n *\n * Pipeline: raw Markdown → `markdownToSafeHtml` (marked → DOMPurify, XSS-safe) →\n * `html-react-parser` into a token-styled React tree. We parse the sanitised HTML\n * into real React nodes (a component map) rather than using\n * `dangerouslySetInnerHTML`, so images lazy-load, external links are hardened, and\n * YouTube embeds get a responsive aspect-ratio wrapper.\n *\n * RSC-safe: no hooks, no browser APIs — renders inside a Server Component.\n * Styling is entirely token-driven (shadcn/OKLCH tokens); the block carries no\n * brand aesthetic of its own.\n */\nexport interface MarkdownRendererProps {\n /** Raw Markdown (blog `content`). */\n content: string | null | undefined\n className?: string\n /**\n * Shift every heading down by this many levels (clamped to h6). Use `1` on a\n * page that already renders its own `<h1>` so markdown `#` becomes `<h2>` and\n * the page keeps a single h1.\n */\n headingOffset?: number\n}\n\nconst HEADING_CLASSES: Record<number, string> = {\n 1: 'text-3xl font-bold mt-8 mb-4 first:mt-0',\n 2: 'text-2xl font-bold mt-7 mb-3 first:mt-0',\n 3: 'text-xl font-bold mt-6 mb-2 first:mt-0',\n 4: 'text-lg font-bold mt-5 mb-2 first:mt-0',\n 5: 'text-base font-bold mt-4 mb-2 first:mt-0',\n 6: 'text-sm font-bold mt-4 mb-2 first:mt-0',\n}\n\nfunction hasClass(el: Element, name: string): boolean {\n return typeof el.attribs.class === 'string' && el.attribs.class.split(/\\s+/).includes(name)\n}\n\nfunction isExternalHref(href: string | undefined): boolean {\n return !!href && /^(https?:)?\\/\\//.test(href)\n}\n\nfunction buildOptions(headingOffset: number): HTMLReactParserOptions {\n const options: HTMLReactParserOptions = {\n replace: (node) => {\n if (!(node instanceof Element)) return undefined\n const children = () => domToReact(node.children as DOMNode[], options)\n const { name, attribs } = node\n\n // Headings — shift level, keep the rendered level's visual weight.\n const headingMatch = /^h([1-6])$/.exec(name)\n if (headingMatch) {\n const rendered = Math.min(6, Number(headingMatch[1]) + headingOffset)\n const Tag = `h${rendered}` as keyof React.JSX.IntrinsicElements\n return <Tag className={HEADING_CLASSES[rendered]}>{children()}</Tag>\n }\n\n switch (name) {\n case 'p':\n return <p className='my-4 leading-relaxed text-foreground/90'>{children()}</p>\n case 'ul':\n return (\n <ul className={hasClass(node, 'contains-task-list') ? 'list-none pl-0 my-4 space-y-1' : 'list-disc ml-6 my-4'}>\n {children()}\n </ul>\n )\n case 'ol':\n return <ol className='list-decimal ml-6 my-4'>{children()}</ol>\n case 'li':\n return (\n <li className={hasClass(node, 'task-list-item') ? 'flex items-start gap-2 my-1' : 'my-1'}>\n {children()}\n </li>\n )\n case 'blockquote':\n return (\n <blockquote className='border-l-4 border-primary/50 pl-6 italic my-6 text-muted-foreground'>\n {children()}\n </blockquote>\n )\n case 'pre':\n return (\n <pre className='rounded-lg border border-input bg-muted p-4 my-6 overflow-x-auto font-mono text-sm'>\n {children()}\n </pre>\n )\n case 'code': {\n // Block code (inside <pre>) inherits the <pre> chrome; only inline code\n // gets the chip treatment.\n const isBlock = node.parent instanceof Element && node.parent.name === 'pre'\n if (isBlock) return <code className='block'>{children()}</code>\n return <code className='rounded bg-muted px-1.5 py-0.5 font-mono text-sm'>{children()}</code>\n }\n case 'a': {\n const href = attribs.href\n const external = isExternalHref(href)\n return (\n <a\n href={href}\n className='text-primary underline underline-offset-4 hover:text-primary/80'\n {...(external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}\n >\n {children()}\n </a>\n )\n }\n case 'img':\n if (!attribs.src) return <></>\n return (\n // Plain <img> keeps the block portable — a distributable can't assume a\n // tenant's next/image remotePatterns. Consumers can post-process if they\n // want optimisation.\n <img\n src={attribs.src}\n alt={attribs.alt ?? ''}\n loading='lazy'\n className='rounded-lg w-full h-auto my-6'\n />\n )\n case 'iframe':\n // Only allowlisted YouTube iframes reach here (sanitiser drops the rest).\n return (\n <div className='relative my-6 aspect-video w-full overflow-hidden rounded-lg'>\n <iframe\n src={attribs.src}\n title={attribs.title ?? 'Embedded video'}\n allow={attribs.allow ?? 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'}\n allowFullScreen\n className='absolute inset-0 h-full w-full border-0'\n />\n </div>\n )\n case 'hr':\n return <hr className='my-8 border-muted-foreground/30' />\n case 'strong':\n return <strong className='font-bold text-foreground'>{children()}</strong>\n case 'em':\n return <em className='italic'>{children()}</em>\n case 'del':\n return <del className='line-through'>{children()}</del>\n default:\n return undefined\n }\n },\n }\n return options\n}\n\nexport function MarkdownRenderer({ content, className, headingOffset = 0 }: MarkdownRendererProps) {\n const html = markdownToSafeHtml(content)\n if (!html) return null\n\n return (\n <div className={cn('max-w-none', className)} data-slot='markdown'>\n {parse(html, buildOptions(headingOffset))}\n </div>\n )\n}\n\n/**\n * Render an **already-sanitised** HTML string through the same token-driven\n * component map as {@link MarkdownRenderer}. Consumers pass HTML that has been run\n * through `sanitizeHtml`/`markdownToSafeHtml`; this component does NOT sanitise, so\n * never hand it untrusted HTML.\n */\nexport interface SafeHtmlProps {\n html: string | null | undefined\n className?: string\n headingOffset?: number\n}\n\nexport function SafeHtml({ html, className, headingOffset = 0 }: SafeHtmlProps) {\n if (!html) return null\n return (\n <div className={cn('max-w-none', className)} data-slot='safe-html'>\n {parse(html, buildOptions(headingOffset))}\n </div>\n )\n}\n","import { cn } from '@reblu/site-ui'\nimport type { PageDetail } from '@reblu/site-contracts'\n\nimport { PageHero, PageSectionList } from './_page-parts'\n\n/**\n * `Treatment` — full treatment page organism over a `PageDetail` (a SERVICE-type\n * page). Renders the hero band followed by the ordered content sections.\n * Token-driven, RSC-safe.\n */\nexport interface TreatmentProps {\n page: PageDetail\n className?: string\n}\n\nexport function Treatment({ page, className }: TreatmentProps) {\n return (\n <article className={cn('space-y-12', className)} data-slot='treatment'>\n <PageHero page={page} />\n <PageSectionList sections={page.sections} />\n </article>\n )\n}\n","import { Button, cn } from '@reblu/site-ui'\nimport type { PageDetail, PageSection } from '@reblu/site-contracts'\n\nimport { sanitizeHtml } from '../lib/markdown'\nimport { SafeHtml } from './markdown-renderer'\n\n/**\n * Internal building blocks shared by the `Treatment` and `Specialty` organisms.\n * Not part of the public API — imported directly by those two files.\n */\n\nconst KNOWN_BUTTON_VARIANTS = new Set([\n 'default',\n 'destructive',\n 'outline',\n 'secondary',\n 'ghost',\n 'link',\n])\n\ntype ButtonVariant = 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'\n\nfunction toButtonVariant(variant: string | null): ButtonVariant {\n return variant && KNOWN_BUTTON_VARIANTS.has(variant) ? (variant as ButtonVariant) : 'default'\n}\n\n/** Hero band for a page: category chip, title, subtitle and cover image. */\nexport function PageHero({ page }: { page: PageDetail }) {\n return (\n <header className='space-y-4'>\n {page.category && (\n <span className='inline-flex items-center rounded-full bg-secondary px-3 py-1 text-sm font-medium text-secondary-foreground'>\n {page.category.name}\n </span>\n )}\n <h1 className='text-4xl font-bold tracking-tight'>{page.title}</h1>\n {page.subtitle && <p className='text-lg text-muted-foreground'>{page.subtitle}</p>}\n {page.coverImageUrl && (\n <div className='relative aspect-[21/9] w-full overflow-hidden rounded-xl bg-muted'>\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img\n src={page.coverImageUrl}\n alt={page.title}\n loading='lazy'\n className='h-full w-full object-cover'\n />\n </div>\n )}\n </header>\n )\n}\n\nfunction SectionCta({ section }: { section: PageSection }) {\n if (!section.ctaLabel || !section.ctaUrl) return null\n return (\n <div className='pt-2'>\n <Button asChild variant={toButtonVariant(section.ctaVariant)}>\n <a\n href={section.ctaUrl}\n {...(section.ctaIsExternal ? { target: '_blank', rel: 'noopener noreferrer' } : {})}\n >\n {section.ctaLabel}\n </a>\n </Button>\n </div>\n )\n}\n\nfunction SectionImages({ section }: { section: PageSection }) {\n if (section.images.length === 0) return null\n return (\n <div className={cn('grid gap-4', section.images.length > 1 ? 'sm:grid-cols-2' : 'grid-cols-1')}>\n {section.images.map((img) => (\n <figure key={`${img.url}-${img.order}`} className='space-y-1'>\n <div className='relative aspect-video w-full overflow-hidden rounded-lg bg-muted'>\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img src={img.url} alt={img.alt} loading='lazy' className='h-full w-full object-cover' />\n </div>\n {img.caption && (\n <figcaption className='text-sm text-muted-foreground'>{img.caption}</figcaption>\n )}\n </figure>\n ))}\n </div>\n )\n}\n\n/** One page section — heading, sanitised body, images and CTA. */\nexport function PageSectionBlock({ section }: { section: PageSection }) {\n const imageFirst = section.layout === 'IMAGE_TEXT'\n const sideBySide = section.layout === 'TEXT_IMAGE' || section.layout === 'IMAGE_TEXT'\n const hasImages = section.images.length > 0\n\n const body = (\n <div className='space-y-4'>\n {section.heading && <h2 className='text-2xl font-bold'>{section.heading}</h2>}\n {/* contentHtml from base-api is marked output that is NOT sanitised server-side. */}\n <SafeHtml html={sanitizeHtml(section.contentHtml)} headingOffset={2} />\n <SectionCta section={section} />\n </div>\n )\n\n if (sideBySide && hasImages) {\n return (\n <section className='grid items-center gap-8 md:grid-cols-2'>\n {imageFirst ? (\n <>\n <SectionImages section={section} />\n {body}\n </>\n ) : (\n <>\n {body}\n <SectionImages section={section} />\n </>\n )}\n </section>\n )\n }\n\n return (\n <section className='space-y-4'>\n {body}\n <SectionImages section={section} />\n </section>\n )\n}\n\n/** Ordered list of a page's sections. */\nexport function PageSectionList({ sections }: { sections: PageSection[] }) {\n if (sections.length === 0) return null\n const ordered = sections.slice().sort((a, b) => a.order - b.order)\n return (\n <div className='space-y-12'>\n {ordered.map((section) => (\n <PageSectionBlock key={section.id} section={section} />\n ))}\n </div>\n )\n}\n","import { cn } from '@reblu/site-ui'\nimport type { PageDetail, PageListItem } from '@reblu/site-contracts'\n\nimport { PageHero, PageSectionList } from './_page-parts'\n\n/**\n * `Specialty` — specialty page organism over a `PageDetail`. Like `Treatment` it\n * renders a hero and content sections, but a specialty groups treatments, so it\n * also renders an optional grid of the treatment pages under it (links to each\n * treatment). Token-driven, RSC-safe.\n */\nexport interface SpecialtyProps {\n page: PageDetail\n /** Treatment pages under this specialty. Rendered as a linked grid when present. */\n treatments?: PageListItem[]\n /** Builds the href for a treatment page from its slug. Defaults to `/{slug}`. */\n treatmentHref?: (slug: string) => string\n /** Heading for the treatments grid. Defaults to \"Treatments\". */\n treatmentsLabel?: string\n className?: string\n}\n\nexport function Specialty({\n page,\n treatments = [],\n treatmentHref = (slug) => `/${slug}`,\n treatmentsLabel = 'Treatments',\n className,\n}: SpecialtyProps) {\n return (\n <article className={cn('space-y-12', className)} data-slot='specialty'>\n <PageHero page={page} />\n <PageSectionList sections={page.sections} />\n\n {treatments.length > 0 && (\n <section className='space-y-6'>\n <h2 className='text-2xl font-bold'>{treatmentsLabel}</h2>\n <ul className='grid list-none grid-cols-1 gap-6 p-0 sm:grid-cols-2 lg:grid-cols-3'>\n {treatments.map((t) => (\n <li key={t.slug}>\n <a\n href={treatmentHref(t.slug)}\n className='group flex h-full flex-col overflow-hidden rounded-lg border bg-card transition-shadow hover:shadow-lg'\n >\n {t.coverImageUrl && (\n <div className='relative aspect-video overflow-hidden bg-muted'>\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img\n src={t.coverImageUrl}\n alt=''\n loading='lazy'\n className='h-full w-full object-cover transition-transform group-hover:scale-105'\n />\n </div>\n )}\n <div className='flex flex-1 flex-col gap-2 p-4'>\n <h3 className='font-semibold leading-tight transition-colors group-hover:text-primary'>\n {t.title}\n </h3>\n {t.excerpt && (\n <p className='line-clamp-2 text-sm text-muted-foreground'>{t.excerpt}</p>\n )}\n </div>\n </a>\n </li>\n ))}\n </ul>\n </section>\n )}\n </article>\n )\n}\n","import { cn } from '@reblu/site-ui'\n\n/**\n * `ProfileCard` — presentational professional/team-member card (the clinic\n * \"doctor card\" from the mockups). There is no SITE API contract for staff yet,\n * so the prop shape is local and intentionally minimal. Token-driven, RSC-safe.\n */\nexport interface ProfileCardProps {\n name: string\n role?: string\n imageUrl?: string\n /** Short list of specialties / tags shown as chips. */\n specialties?: string[]\n bio?: string\n /** Link target for the full profile. Omit to render non-linking. */\n href?: string\n className?: string\n}\n\n/** Up to two initials from the name, for the avatar fallback. */\nfunction initials(name: string): string {\n return name\n .split(/\\s+/)\n .filter(Boolean)\n .slice(0, 2)\n .map((part) => part[0]?.toUpperCase() ?? '')\n .join('')\n}\n\nexport function ProfileCard({ name, role, imageUrl, specialties = [], bio, href, className }: ProfileCardProps) {\n const NameTag = href ? 'a' : 'span'\n\n return (\n <article\n className={cn(\n 'flex flex-col items-center gap-3 rounded-lg border bg-card p-6 text-center',\n className,\n )}\n data-slot='profile-card'\n >\n <div className='relative h-24 w-24 overflow-hidden rounded-full bg-muted'>\n {imageUrl ? (\n <img src={imageUrl} alt={name} loading='lazy' className='h-full w-full object-cover' />\n ) : (\n <span className='flex h-full w-full items-center justify-center text-2xl font-semibold text-muted-foreground' aria-hidden='true'>\n {initials(name)}\n </span>\n )}\n </div>\n\n <div className='space-y-1'>\n <h3 className='text-lg font-semibold leading-tight'>\n <NameTag\n {...(href ? { href, className: 'transition-colors hover:text-primary' } : {})}\n >\n {name}\n </NameTag>\n </h3>\n {role && <p className='text-sm text-muted-foreground'>{role}</p>}\n </div>\n\n {specialties.length > 0 && (\n <ul className='flex flex-wrap justify-center gap-1.5 p-0'>\n {specialties.map((s) => (\n <li\n key={s}\n className='inline-flex items-center rounded-full bg-secondary px-2.5 py-0.5 text-xs font-medium text-secondary-foreground'\n >\n {s}\n </li>\n ))}\n </ul>\n )}\n\n {bio && <p className='text-sm text-muted-foreground'>{bio}</p>}\n </article>\n )\n}\n","import { cn } from '@reblu/site-ui'\nimport type { ShopProductListItem } from '@reblu/site-contracts'\n\nimport { formatPrice } from '../lib/format-price'\n\n/**\n * `ProductCard` — presentational shop product tile over `ShopProductListItem`.\n *\n * Token-driven, RSC-safe (no hooks). Prices arrive as integer cents and are\n * formatted with {@link formatPrice}. A `comparePrice` higher than `price` renders\n * as a struck-through \"was\" amount.\n */\nexport interface ProductCardProps {\n product: ShopProductListItem\n /** Link target for the product (e.g. `/shop/{slug}`). Omit to render non-linking. */\n href?: string\n currencyCode?: string\n locale?: string\n className?: string\n}\n\nexport function ProductCard({ product, href, currencyCode = 'EUR', locale, className }: ProductCardProps) {\n const image = product.images.find((i) => i.isPrimary) ?? product.images[0]\n const hasDiscount = product.comparePrice != null && product.comparePrice > product.price\n\n return (\n <article\n className={cn(\n 'group flex flex-col overflow-hidden rounded-lg border bg-card transition-shadow hover:shadow-lg',\n className,\n )}\n data-slot='product-card'\n >\n <div className='relative aspect-square overflow-hidden bg-muted'>\n {image ? (\n <img\n src={image.url}\n alt={image.alt ?? product.name}\n loading='lazy'\n className='h-full w-full object-cover transition-transform group-hover:scale-105'\n />\n ) : (\n <div className='flex h-full items-center justify-center text-4xl text-muted-foreground/30' aria-hidden='true'>\n 🛍️\n </div>\n )}\n </div>\n\n <div className='flex flex-1 flex-col gap-2 p-4'>\n {product.category && (\n <span className='inline-flex w-fit items-center rounded-full bg-secondary px-2.5 py-0.5 text-xs font-medium text-secondary-foreground'>\n {product.category.name}\n </span>\n )}\n\n <h3 className='text-base font-semibold leading-tight'>\n {href ? (\n <a href={href} className='transition-colors hover:text-primary'>\n {product.name}\n </a>\n ) : (\n product.name\n )}\n </h3>\n\n {product.shortDescription && (\n <p className='line-clamp-2 text-sm text-muted-foreground'>{product.shortDescription}</p>\n )}\n\n <div className='mt-auto flex items-baseline gap-2 pt-2'>\n <span className='text-lg font-bold text-foreground'>\n {formatPrice(product.price, currencyCode, locale)}\n </span>\n {hasDiscount && (\n <span className='text-sm text-muted-foreground line-through'>\n {formatPrice(product.comparePrice as number, currencyCode, locale)}\n </span>\n )}\n </div>\n </div>\n </article>\n )\n}\n"],"mappings":"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","names":["cn","cn","Fragment","jsx","cn","jsx","jsxs","cn","cn","jsx","jsxs","cn","cn","jsx","jsxs","cn","jsx","jsxs","cn"]}
package/package.json ADDED
@@ -0,0 +1,96 @@
1
+ {
2
+ "name": "@reblu/site-blocks",
3
+ "version": "0.2.1",
4
+ "description": "Data-driven presentational organisms for Reblu SITEs (Collection, MarkdownRenderer, Treatment, Specialty, ProfileCard, ProductCard) — clean, token-driven, white-label.",
5
+ "license": "Apache-2.0",
6
+ "author": "Reblu — Adolfo Unturbe",
7
+ "homepage": "https://reblu.app",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/apu314/reblu-platform.git",
11
+ "directory": "packages/site-blocks"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/apu314/reblu-platform/issues"
15
+ },
16
+ "keywords": [
17
+ "reblu",
18
+ "sdk",
19
+ "react",
20
+ "blocks",
21
+ "organisms",
22
+ "markdown",
23
+ "collection"
24
+ ],
25
+ "private": false,
26
+ "type": "module",
27
+ "sideEffects": false,
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "main": "./dist/index.cjs",
32
+ "module": "./dist/index.js",
33
+ "types": "./dist/index.d.ts",
34
+ "exports": {
35
+ ".": {
36
+ "import": {
37
+ "types": "./dist/index.d.ts",
38
+ "default": "./dist/index.js"
39
+ },
40
+ "require": {
41
+ "types": "./dist/index.d.cts",
42
+ "default": "./dist/index.cjs"
43
+ }
44
+ },
45
+ "./client": {
46
+ "import": {
47
+ "types": "./dist/client.d.ts",
48
+ "default": "./dist/client.js"
49
+ },
50
+ "require": {
51
+ "types": "./dist/client.d.cts",
52
+ "default": "./dist/client.cjs"
53
+ }
54
+ }
55
+ },
56
+ "publishConfig": {
57
+ "access": "public"
58
+ },
59
+ "dependencies": {
60
+ "class-variance-authority": "^0.7.1",
61
+ "html-react-parser": "^6.1.5",
62
+ "isomorphic-dompurify": "^3.21.0",
63
+ "marked": "^16.4.2",
64
+ "@reblu/site-contracts": "0.1.2",
65
+ "@reblu/site-ui": "0.3.0"
66
+ },
67
+ "peerDependencies": {
68
+ "react": ">=19",
69
+ "react-dom": ">=19"
70
+ },
71
+ "devDependencies": {
72
+ "@testing-library/jest-dom": "^6.9.1",
73
+ "@testing-library/react": "^16.3.1",
74
+ "@testing-library/user-event": "^14.6.1",
75
+ "@types/jest-axe": "^3.5.9",
76
+ "@types/react": "^19.1.2",
77
+ "@vitejs/plugin-react": "^4.4.1",
78
+ "@vitest/coverage-v8": "^4.0.15",
79
+ "jest-axe": "^10.0.0",
80
+ "jsdom": "^27.3.0",
81
+ "react": "19.2.4",
82
+ "react-dom": "19.2.4",
83
+ "tsup": "^8.5.0",
84
+ "typescript": "^5.8.3",
85
+ "vitest": "^4.0.15",
86
+ "@reblu/config": "0.0.1"
87
+ },
88
+ "scripts": {
89
+ "build": "tsup",
90
+ "dev": "tsup --watch",
91
+ "test": "vitest run",
92
+ "test:unit": "vitest run",
93
+ "test:coverage": "vitest run --coverage",
94
+ "type-check": "tsc --noEmit"
95
+ }
96
+ }