@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.
@@ -0,0 +1,162 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { PageDetail, PageListItem, ShopProductListItem } from '@reblu/site-contracts';
3
+
4
+ declare function readingTime(content: string | null | undefined): number | null;
5
+
6
+ /**
7
+ * Format an integer-cents amount as a localized currency string.
8
+ *
9
+ * All Reblu shop monetary values travel as integer cents (Prisma `Int`, never
10
+ * Decimal), so the SITE never sees floats. This helper is the single place that
11
+ * converts cents → major units and applies `Intl.NumberFormat`, keeping price
12
+ * rendering consistent across `ProductCard` and any future shop organism.
13
+ */
14
+ declare function formatPrice(cents: number, currency?: string, locale?: string): string;
15
+
16
+ /**
17
+ * Pure builders for the social-share targets used by `ShareButtons`.
18
+ *
19
+ * Framework-free so the URL construction is unit-testable without rendering.
20
+ * Every provider gets the canonical post URL; providers that support custom copy
21
+ * (X, WhatsApp) also receive the post title.
22
+ */
23
+ interface ShareUrls {
24
+ linkedin: string;
25
+ x: string;
26
+ facebook: string;
27
+ whatsapp: string;
28
+ }
29
+ declare function buildShareUrls(url: string, title: string): ShareUrls;
30
+
31
+ /**
32
+ * Canonical Blog markdown → **safe HTML** pipeline for the Reblu SITE SDK.
33
+ *
34
+ * Blog `content` arrives as raw Markdown (tiptap-markdown). This module owns the
35
+ * single, hardened conversion every SITE reuses instead of reinventing it:
36
+ *
37
+ * Markdown ──(marked, GFM)──▶ HTML ──(DOMPurify)──▶ safe HTML
38
+ *
39
+ * DOMPurify is MANDATORY (XSS): the output is later parsed into React nodes by
40
+ * `MarkdownRenderer` (no `dangerouslySetInnerHTML`), but sanitising here means the
41
+ * string is safe regardless of how a consumer chooses to render it.
42
+ *
43
+ * `<iframe>` is the one non-default tag we allow, and ONLY when its `src` host is
44
+ * on the YouTube allowlist — every other embed (arbitrary iframes) is dropped.
45
+ */
46
+ /** Hosts permitted for `<iframe>` embeds. Exact host match — no suffix tricks. */
47
+ declare const ALLOWED_YOUTUBE_HOSTS: readonly string[];
48
+ /** True when `src` is an absolute URL whose host is exactly a YouTube host. */
49
+ declare function isAllowedYouTubeHost(src: string | null | undefined): boolean;
50
+ /**
51
+ * Sanitise an already-HTML string with the same policy (default-safe tags plus a
52
+ * host-gated `<iframe>`). Use for content that arrives as HTML — e.g. page
53
+ * `section.contentHtml` from base-api, which is `marked` output that has NOT been
54
+ * sanitised server-side.
55
+ */
56
+ declare function sanitizeHtml(html: string | null | undefined): string;
57
+ /** Convert raw Markdown to sanitised, XSS-safe HTML. */
58
+ declare function markdownToSafeHtml(markdown: string | null | undefined): string;
59
+
60
+ /**
61
+ * `MarkdownRenderer` — the canonical Blog body renderer.
62
+ *
63
+ * Pipeline: raw Markdown → `markdownToSafeHtml` (marked → DOMPurify, XSS-safe) →
64
+ * `html-react-parser` into a token-styled React tree. We parse the sanitised HTML
65
+ * into real React nodes (a component map) rather than using
66
+ * `dangerouslySetInnerHTML`, so images lazy-load, external links are hardened, and
67
+ * YouTube embeds get a responsive aspect-ratio wrapper.
68
+ *
69
+ * RSC-safe: no hooks, no browser APIs — renders inside a Server Component.
70
+ * Styling is entirely token-driven (shadcn/OKLCH tokens); the block carries no
71
+ * brand aesthetic of its own.
72
+ */
73
+ interface MarkdownRendererProps {
74
+ /** Raw Markdown (blog `content`). */
75
+ content: string | null | undefined;
76
+ className?: string;
77
+ /**
78
+ * Shift every heading down by this many levels (clamped to h6). Use `1` on a
79
+ * page that already renders its own `<h1>` so markdown `#` becomes `<h2>` and
80
+ * the page keeps a single h1.
81
+ */
82
+ headingOffset?: number;
83
+ }
84
+ declare function MarkdownRenderer({ content, className, headingOffset }: MarkdownRendererProps): react_jsx_runtime.JSX.Element | null;
85
+ /**
86
+ * Render an **already-sanitised** HTML string through the same token-driven
87
+ * component map as {@link MarkdownRenderer}. Consumers pass HTML that has been run
88
+ * through `sanitizeHtml`/`markdownToSafeHtml`; this component does NOT sanitise, so
89
+ * never hand it untrusted HTML.
90
+ */
91
+ interface SafeHtmlProps {
92
+ html: string | null | undefined;
93
+ className?: string;
94
+ headingOffset?: number;
95
+ }
96
+ declare function SafeHtml({ html, className, headingOffset }: SafeHtmlProps): react_jsx_runtime.JSX.Element | null;
97
+
98
+ /**
99
+ * `Treatment` — full treatment page organism over a `PageDetail` (a SERVICE-type
100
+ * page). Renders the hero band followed by the ordered content sections.
101
+ * Token-driven, RSC-safe.
102
+ */
103
+ interface TreatmentProps {
104
+ page: PageDetail;
105
+ className?: string;
106
+ }
107
+ declare function Treatment({ page, className }: TreatmentProps): react_jsx_runtime.JSX.Element;
108
+
109
+ /**
110
+ * `Specialty` — specialty page organism over a `PageDetail`. Like `Treatment` it
111
+ * renders a hero and content sections, but a specialty groups treatments, so it
112
+ * also renders an optional grid of the treatment pages under it (links to each
113
+ * treatment). Token-driven, RSC-safe.
114
+ */
115
+ interface SpecialtyProps {
116
+ page: PageDetail;
117
+ /** Treatment pages under this specialty. Rendered as a linked grid when present. */
118
+ treatments?: PageListItem[];
119
+ /** Builds the href for a treatment page from its slug. Defaults to `/{slug}`. */
120
+ treatmentHref?: (slug: string) => string;
121
+ /** Heading for the treatments grid. Defaults to "Treatments". */
122
+ treatmentsLabel?: string;
123
+ className?: string;
124
+ }
125
+ declare function Specialty({ page, treatments, treatmentHref, treatmentsLabel, className, }: SpecialtyProps): react_jsx_runtime.JSX.Element;
126
+
127
+ /**
128
+ * `ProfileCard` — presentational professional/team-member card (the clinic
129
+ * "doctor card" from the mockups). There is no SITE API contract for staff yet,
130
+ * so the prop shape is local and intentionally minimal. Token-driven, RSC-safe.
131
+ */
132
+ interface ProfileCardProps {
133
+ name: string;
134
+ role?: string;
135
+ imageUrl?: string;
136
+ /** Short list of specialties / tags shown as chips. */
137
+ specialties?: string[];
138
+ bio?: string;
139
+ /** Link target for the full profile. Omit to render non-linking. */
140
+ href?: string;
141
+ className?: string;
142
+ }
143
+ declare function ProfileCard({ name, role, imageUrl, specialties, bio, href, className }: ProfileCardProps): react_jsx_runtime.JSX.Element;
144
+
145
+ /**
146
+ * `ProductCard` — presentational shop product tile over `ShopProductListItem`.
147
+ *
148
+ * Token-driven, RSC-safe (no hooks). Prices arrive as integer cents and are
149
+ * formatted with {@link formatPrice}. A `comparePrice` higher than `price` renders
150
+ * as a struck-through "was" amount.
151
+ */
152
+ interface ProductCardProps {
153
+ product: ShopProductListItem;
154
+ /** Link target for the product (e.g. `/shop/{slug}`). Omit to render non-linking. */
155
+ href?: string;
156
+ currencyCode?: string;
157
+ locale?: string;
158
+ className?: string;
159
+ }
160
+ declare function ProductCard({ product, href, currencyCode, locale, className }: ProductCardProps): react_jsx_runtime.JSX.Element;
161
+
162
+ export { ALLOWED_YOUTUBE_HOSTS, MarkdownRenderer, type MarkdownRendererProps, ProductCard, type ProductCardProps, ProfileCard, type ProfileCardProps, SafeHtml, type SafeHtmlProps, type ShareUrls, Specialty, type SpecialtyProps, Treatment, type TreatmentProps, buildShareUrls, formatPrice, isAllowedYouTubeHost, markdownToSafeHtml, readingTime, sanitizeHtml };
@@ -0,0 +1,162 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { PageDetail, PageListItem, ShopProductListItem } from '@reblu/site-contracts';
3
+
4
+ declare function readingTime(content: string | null | undefined): number | null;
5
+
6
+ /**
7
+ * Format an integer-cents amount as a localized currency string.
8
+ *
9
+ * All Reblu shop monetary values travel as integer cents (Prisma `Int`, never
10
+ * Decimal), so the SITE never sees floats. This helper is the single place that
11
+ * converts cents → major units and applies `Intl.NumberFormat`, keeping price
12
+ * rendering consistent across `ProductCard` and any future shop organism.
13
+ */
14
+ declare function formatPrice(cents: number, currency?: string, locale?: string): string;
15
+
16
+ /**
17
+ * Pure builders for the social-share targets used by `ShareButtons`.
18
+ *
19
+ * Framework-free so the URL construction is unit-testable without rendering.
20
+ * Every provider gets the canonical post URL; providers that support custom copy
21
+ * (X, WhatsApp) also receive the post title.
22
+ */
23
+ interface ShareUrls {
24
+ linkedin: string;
25
+ x: string;
26
+ facebook: string;
27
+ whatsapp: string;
28
+ }
29
+ declare function buildShareUrls(url: string, title: string): ShareUrls;
30
+
31
+ /**
32
+ * Canonical Blog markdown → **safe HTML** pipeline for the Reblu SITE SDK.
33
+ *
34
+ * Blog `content` arrives as raw Markdown (tiptap-markdown). This module owns the
35
+ * single, hardened conversion every SITE reuses instead of reinventing it:
36
+ *
37
+ * Markdown ──(marked, GFM)──▶ HTML ──(DOMPurify)──▶ safe HTML
38
+ *
39
+ * DOMPurify is MANDATORY (XSS): the output is later parsed into React nodes by
40
+ * `MarkdownRenderer` (no `dangerouslySetInnerHTML`), but sanitising here means the
41
+ * string is safe regardless of how a consumer chooses to render it.
42
+ *
43
+ * `<iframe>` is the one non-default tag we allow, and ONLY when its `src` host is
44
+ * on the YouTube allowlist — every other embed (arbitrary iframes) is dropped.
45
+ */
46
+ /** Hosts permitted for `<iframe>` embeds. Exact host match — no suffix tricks. */
47
+ declare const ALLOWED_YOUTUBE_HOSTS: readonly string[];
48
+ /** True when `src` is an absolute URL whose host is exactly a YouTube host. */
49
+ declare function isAllowedYouTubeHost(src: string | null | undefined): boolean;
50
+ /**
51
+ * Sanitise an already-HTML string with the same policy (default-safe tags plus a
52
+ * host-gated `<iframe>`). Use for content that arrives as HTML — e.g. page
53
+ * `section.contentHtml` from base-api, which is `marked` output that has NOT been
54
+ * sanitised server-side.
55
+ */
56
+ declare function sanitizeHtml(html: string | null | undefined): string;
57
+ /** Convert raw Markdown to sanitised, XSS-safe HTML. */
58
+ declare function markdownToSafeHtml(markdown: string | null | undefined): string;
59
+
60
+ /**
61
+ * `MarkdownRenderer` — the canonical Blog body renderer.
62
+ *
63
+ * Pipeline: raw Markdown → `markdownToSafeHtml` (marked → DOMPurify, XSS-safe) →
64
+ * `html-react-parser` into a token-styled React tree. We parse the sanitised HTML
65
+ * into real React nodes (a component map) rather than using
66
+ * `dangerouslySetInnerHTML`, so images lazy-load, external links are hardened, and
67
+ * YouTube embeds get a responsive aspect-ratio wrapper.
68
+ *
69
+ * RSC-safe: no hooks, no browser APIs — renders inside a Server Component.
70
+ * Styling is entirely token-driven (shadcn/OKLCH tokens); the block carries no
71
+ * brand aesthetic of its own.
72
+ */
73
+ interface MarkdownRendererProps {
74
+ /** Raw Markdown (blog `content`). */
75
+ content: string | null | undefined;
76
+ className?: string;
77
+ /**
78
+ * Shift every heading down by this many levels (clamped to h6). Use `1` on a
79
+ * page that already renders its own `<h1>` so markdown `#` becomes `<h2>` and
80
+ * the page keeps a single h1.
81
+ */
82
+ headingOffset?: number;
83
+ }
84
+ declare function MarkdownRenderer({ content, className, headingOffset }: MarkdownRendererProps): react_jsx_runtime.JSX.Element | null;
85
+ /**
86
+ * Render an **already-sanitised** HTML string through the same token-driven
87
+ * component map as {@link MarkdownRenderer}. Consumers pass HTML that has been run
88
+ * through `sanitizeHtml`/`markdownToSafeHtml`; this component does NOT sanitise, so
89
+ * never hand it untrusted HTML.
90
+ */
91
+ interface SafeHtmlProps {
92
+ html: string | null | undefined;
93
+ className?: string;
94
+ headingOffset?: number;
95
+ }
96
+ declare function SafeHtml({ html, className, headingOffset }: SafeHtmlProps): react_jsx_runtime.JSX.Element | null;
97
+
98
+ /**
99
+ * `Treatment` — full treatment page organism over a `PageDetail` (a SERVICE-type
100
+ * page). Renders the hero band followed by the ordered content sections.
101
+ * Token-driven, RSC-safe.
102
+ */
103
+ interface TreatmentProps {
104
+ page: PageDetail;
105
+ className?: string;
106
+ }
107
+ declare function Treatment({ page, className }: TreatmentProps): react_jsx_runtime.JSX.Element;
108
+
109
+ /**
110
+ * `Specialty` — specialty page organism over a `PageDetail`. Like `Treatment` it
111
+ * renders a hero and content sections, but a specialty groups treatments, so it
112
+ * also renders an optional grid of the treatment pages under it (links to each
113
+ * treatment). Token-driven, RSC-safe.
114
+ */
115
+ interface SpecialtyProps {
116
+ page: PageDetail;
117
+ /** Treatment pages under this specialty. Rendered as a linked grid when present. */
118
+ treatments?: PageListItem[];
119
+ /** Builds the href for a treatment page from its slug. Defaults to `/{slug}`. */
120
+ treatmentHref?: (slug: string) => string;
121
+ /** Heading for the treatments grid. Defaults to "Treatments". */
122
+ treatmentsLabel?: string;
123
+ className?: string;
124
+ }
125
+ declare function Specialty({ page, treatments, treatmentHref, treatmentsLabel, className, }: SpecialtyProps): react_jsx_runtime.JSX.Element;
126
+
127
+ /**
128
+ * `ProfileCard` — presentational professional/team-member card (the clinic
129
+ * "doctor card" from the mockups). There is no SITE API contract for staff yet,
130
+ * so the prop shape is local and intentionally minimal. Token-driven, RSC-safe.
131
+ */
132
+ interface ProfileCardProps {
133
+ name: string;
134
+ role?: string;
135
+ imageUrl?: string;
136
+ /** Short list of specialties / tags shown as chips. */
137
+ specialties?: string[];
138
+ bio?: string;
139
+ /** Link target for the full profile. Omit to render non-linking. */
140
+ href?: string;
141
+ className?: string;
142
+ }
143
+ declare function ProfileCard({ name, role, imageUrl, specialties, bio, href, className }: ProfileCardProps): react_jsx_runtime.JSX.Element;
144
+
145
+ /**
146
+ * `ProductCard` — presentational shop product tile over `ShopProductListItem`.
147
+ *
148
+ * Token-driven, RSC-safe (no hooks). Prices arrive as integer cents and are
149
+ * formatted with {@link formatPrice}. A `comparePrice` higher than `price` renders
150
+ * as a struck-through "was" amount.
151
+ */
152
+ interface ProductCardProps {
153
+ product: ShopProductListItem;
154
+ /** Link target for the product (e.g. `/shop/{slug}`). Omit to render non-linking. */
155
+ href?: string;
156
+ currencyCode?: string;
157
+ locale?: string;
158
+ className?: string;
159
+ }
160
+ declare function ProductCard({ product, href, currencyCode, locale, className }: ProductCardProps): react_jsx_runtime.JSX.Element;
161
+
162
+ export { ALLOWED_YOUTUBE_HOSTS, MarkdownRenderer, type MarkdownRendererProps, ProductCard, type ProductCardProps, ProfileCard, type ProfileCardProps, SafeHtml, type SafeHtmlProps, type ShareUrls, Specialty, type SpecialtyProps, Treatment, type TreatmentProps, buildShareUrls, formatPrice, isAllowedYouTubeHost, markdownToSafeHtml, readingTime, sanitizeHtml };