@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/LICENSE +201 -0
- package/README.md +29 -0
- package/dist/client.cjs +335 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +134 -0
- package/dist/client.d.ts +134 -0
- package/dist/client.js +297 -0
- package/dist/client.js.map +1 -0
- package/dist/index.cjs +464 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +162 -0
- package/dist/index.d.ts +162 -0
- package/dist/index.js +415 -0
- package/dist/index.js.map +1 -0
- package/package.json +96 -0
package/dist/index.d.cts
ADDED
|
@@ -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 };
|
package/dist/index.d.ts
ADDED
|
@@ -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 };
|