@bapla/site-theme 0.0.0-stage → 0.2.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/index.js ADDED
@@ -0,0 +1,904 @@
1
+ import {
2
+ MEDIA_WIDTHS,
3
+ imgproxyPath,
4
+ isTransformable,
5
+ mediaUrl,
6
+ objectPosition,
7
+ readMedia
8
+ } from "./chunk-RYCW4YWV.js";
9
+
10
+ // src/theme/themeCss.ts
11
+ var SHADES = ["50", "100", "200", "300", "400", "500", "600", "700", "800", "900", "950"];
12
+ var RAMPS = ["primary", "secondary", "accent", "neutral"];
13
+ var LEVELS = ["h1", "h2", "h3", "h4", "h5", "h6", "p"];
14
+ var HEX = /^#[0-9a-f]{6}$/i;
15
+ var LENGTH = /^-?(\d+(\.\d+)?|\.\d+)(rem|em|px|%)?$/;
16
+ var FONT_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
17
+ function hex(value) {
18
+ return typeof value === "string" && HEX.test(value) ? value.toLowerCase() : null;
19
+ }
20
+ function length(value) {
21
+ return typeof value === "string" && LENGTH.test(value.trim()) ? value.trim() : null;
22
+ }
23
+ function number(value, min, max) {
24
+ return typeof value === "number" && Number.isFinite(value) && value >= min && value <= max ? value : null;
25
+ }
26
+ function familyName(name) {
27
+ return name.replace(/[^\p{L}\p{N} _.-]/gu, "").replace(/\s+/g, " ").trim();
28
+ }
29
+ function fontStack(font) {
30
+ const fallback = ["sans-serif", "serif", "monospace"].includes(font.fallback) ? font.fallback : "sans-serif";
31
+ const name = familyName(font.family);
32
+ return name === "" ? fallback : `"${name} Variable", "${name}", ${fallback}`;
33
+ }
34
+ function declarations(entries) {
35
+ return entries.filter((entry) => entry[1] !== null).map(([name, value]) => ` ${name}: ${value};`).join("\n");
36
+ }
37
+ function rampVars(name, ramp) {
38
+ return SHADES.map((shade) => [`--color-${name}-${shade}`, hex(ramp?.[shade])]);
39
+ }
40
+ function themeCss(appearance) {
41
+ const { colors, fonts, type } = appearance;
42
+ const theme = [
43
+ ...RAMPS.flatMap((name) => rampVars(name, colors[name])),
44
+ ["--color-background", hex(colors.background)],
45
+ ["--color-text", hex(colors.text)],
46
+ ["--font-body", fontStack(fonts.body)],
47
+ ["--font-heading", fontStack(fonts.heading)]
48
+ ];
49
+ for (const level of LEVELS) {
50
+ const style = type[level];
51
+ theme.push(
52
+ [`--text-${level}`, length(style?.size)],
53
+ [`--text-${level}--line-height`, number(style?.line_height, 0.5, 4)],
54
+ [`--text-${level}--letter-spacing`, length(style?.letter_spacing)],
55
+ [`--text-${level}--font-weight`, number(style?.weight, 100, 1e3)]
56
+ );
57
+ }
58
+ theme.push(["--radius-base", length(appearance.radius)]);
59
+ const headings = LEVELS.filter((level) => level !== "p").map(
60
+ (level) => ` ${level} {
61
+ font-family: var(--font-heading);
62
+ font-size: var(--text-${level});
63
+ line-height: var(--text-${level}--line-height);
64
+ letter-spacing: var(--text-${level}--letter-spacing);
65
+ font-weight: var(--text-${level}--font-weight);
66
+ }`
67
+ ).join("\n");
68
+ const parts = [
69
+ `@theme {
70
+ ${declarations(theme)}
71
+ }`,
72
+ `@layer base {
73
+ :root {
74
+ color-scheme: ${appearance.mode === "both" ? "light dark" : appearance.mode};
75
+ }
76
+ body {
77
+ background-color: var(--color-background);
78
+ color: var(--color-text);
79
+ font-family: var(--font-body);
80
+ font-size: var(--text-p);
81
+ line-height: var(--text-p--line-height);
82
+ letter-spacing: var(--text-p--letter-spacing);
83
+ font-weight: var(--text-p--font-weight);
84
+ }
85
+ ${headings}
86
+ p {
87
+ font-size: var(--text-p);
88
+ line-height: var(--text-p--line-height);
89
+ }
90
+ }`
91
+ ];
92
+ if (appearance.mode === "both") {
93
+ parts.push(`@media (prefers-color-scheme: dark) {
94
+ :root {
95
+ ${declarations([
96
+ ["--color-background", hex(colors.neutral?.["950"])],
97
+ ["--color-text", hex(colors.neutral?.["50"])]
98
+ ]).replace(/^ {2}/gm, " ")}
99
+ }
100
+ }`);
101
+ }
102
+ return parts.join("\n\n") + "\n";
103
+ }
104
+ function fontImports(appearance, options = {}) {
105
+ const variable = options.variable ?? true;
106
+ const weights = /* @__PURE__ */ new Map();
107
+ const add = (font, levels, extra = []) => {
108
+ if (!FONT_ID.test(font.id)) return;
109
+ const set = weights.get(font.id) ?? /* @__PURE__ */ new Set();
110
+ for (const level of levels) {
111
+ const weight = number(appearance.type[level]?.weight, 100, 900);
112
+ if (weight !== null) set.add(Math.round(weight / 100) * 100);
113
+ }
114
+ extra.forEach((weight) => set.add(weight));
115
+ weights.set(font.id, set);
116
+ };
117
+ add(appearance.fonts.body, ["p"], [700]);
118
+ add(appearance.fonts.heading, ["h1", "h2", "h3", "h4", "h5", "h6"]);
119
+ if (variable) {
120
+ return [...weights.keys()].map((id) => `@fontsource-variable/${id}`);
121
+ }
122
+ return [...weights.entries()].flatMap(
123
+ ([id, set]) => [...set].sort((a, b) => a - b).map((weight) => `@fontsource/${id}/${weight}.css`)
124
+ );
125
+ }
126
+
127
+ // src/context.tsx
128
+ import { createContext, useContext, useMemo } from "react";
129
+ import { jsx } from "react/jsx-runtime";
130
+ var frenchLabels = {
131
+ mainNavigation: "Navigation principale",
132
+ menu: "Menu",
133
+ submenu: (label) => `Sous-menu ${label}`,
134
+ breadcrumb: "Fil d'Ariane",
135
+ carousel: "Carrousel",
136
+ previous: "Pr\xE9c\xE9dent",
137
+ next: "Suivant",
138
+ slide: (index, count) => `${index} sur ${count}`,
139
+ goToSlide: (index) => `Aller \xE0 la diapositive ${index}`,
140
+ pause: "Mettre en pause",
141
+ play: "Lire",
142
+ embed: "Contenu int\xE9gr\xE9"
143
+ };
144
+ var defaultImageSizes = {
145
+ thumbnail: { w: 150, h: 150, fit: "cover" },
146
+ card: { w: 300, h: 300, fit: "cover" },
147
+ medium: { w: 768 },
148
+ large: { w: 1280 }
149
+ };
150
+ var defaults = {
151
+ components: {},
152
+ resolveLink: () => null,
153
+ resolveMedia: () => null,
154
+ imageUrl: (media, transform2) => mediaUrl(media, transform2),
155
+ imageSizes: defaultImageSizes,
156
+ labels: frenchLabels
157
+ };
158
+ var SiteThemeContext = createContext(defaults);
159
+ function BlocksProvider({ components, resolveLink, resolveMedia, imageUrl, imageSizes, labels, children }) {
160
+ const parent = useContext(SiteThemeContext);
161
+ const value = useMemo(
162
+ () => ({
163
+ components: { ...parent.components, ...components },
164
+ resolveLink: resolveLink ?? parent.resolveLink,
165
+ resolveMedia: resolveMedia ?? parent.resolveMedia,
166
+ imageUrl: imageUrl ?? parent.imageUrl,
167
+ imageSizes: { ...parent.imageSizes, ...imageSizes },
168
+ labels: { ...parent.labels, ...labels }
169
+ }),
170
+ [parent, components, resolveLink, resolveMedia, imageUrl, imageSizes, labels]
171
+ );
172
+ return /* @__PURE__ */ jsx(SiteThemeContext.Provider, { value, children });
173
+ }
174
+ function useSiteTheme() {
175
+ return useContext(SiteThemeContext);
176
+ }
177
+
178
+ // src/blocks/Blocks.tsx
179
+ import { Fragment as Fragment4 } from "react";
180
+
181
+ // src/links.ts
182
+ var NODE_ID = /^web_nod_[0-9a-f-]{36}$/;
183
+ function safeHref(url, resolve = () => null) {
184
+ if (typeof url !== "string" || url === "" || url.length > 2048 || /[\s<>"]/.test(url)) return null;
185
+ if (NODE_ID.test(url)) {
186
+ const resolved = resolve(url);
187
+ return resolved !== null && !NODE_ID.test(resolved) ? safeHref(resolved) : null;
188
+ }
189
+ if (url.startsWith("/") && !url.startsWith("//")) return url;
190
+ if (url.startsWith("#")) return url.length > 1 ? url : null;
191
+ if (/^mailto:[^@\s]+@[^@\s]+$/i.test(url) || /^tel:\+?[0-9 ().-]{3,30}$/i.test(url)) return url;
192
+ return /^https?:\/\/[^/]/i.test(url) ? url : null;
193
+ }
194
+ function readLink(value) {
195
+ if (typeof value === "string") return { url: value, label: null, new_tab: false };
196
+ if (value === null || typeof value !== "object") return null;
197
+ const link = value;
198
+ if (typeof link.url !== "string") return null;
199
+ return { url: link.url, label: typeof link.label === "string" ? link.label : null, new_tab: link.new_tab === true };
200
+ }
201
+ function targetProps(newTab) {
202
+ return newTab ? { target: "_blank", rel: "noopener noreferrer" } : {};
203
+ }
204
+
205
+ // src/menu/context.ts
206
+ import { createContext as createContext2, useContext as useContext2 } from "react";
207
+ var MenuItemContext = createContext2(null);
208
+ function useMenuItem() {
209
+ return useContext2(MenuItemContext);
210
+ }
211
+ function findItem(items, id) {
212
+ for (const item of items) {
213
+ if (item.id === id) return item;
214
+ const found = findItem(item.children, id);
215
+ if (found) return found;
216
+ }
217
+ return null;
218
+ }
219
+
220
+ // src/menu/MenuLinks.tsx
221
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
222
+ function MenuItemLink({ item, className }) {
223
+ const { resolveLink } = useSiteTheme();
224
+ const href = item.kind === "link" ? safeHref(item.url, resolveLink) : null;
225
+ if (!href) return /* @__PURE__ */ jsx2("span", { className, children: item.label });
226
+ return /* @__PURE__ */ jsx2("a", { className, href, "aria-current": item.active && item.children.length === 0 ? "page" : void 0, ...targetProps(item.new_tab), children: item.label });
227
+ }
228
+ function MenuLinks({ props }) {
229
+ const current = useMenuItem();
230
+ if (!current) return null;
231
+ const root = typeof props.item === "string" ? findItem([current], props.item) : current;
232
+ if (!root || root.children.length === 0) return null;
233
+ return /* @__PURE__ */ jsx2("ul", { className: "bapla-menu-links", children: root.children.map((child) => /* @__PURE__ */ jsxs("li", { "data-kind": child.kind, children: [
234
+ /* @__PURE__ */ jsx2(MenuItemLink, { item: child, className: child.kind === "heading" ? "bapla-menu-heading" : void 0 }),
235
+ child.children.length > 0 && /* @__PURE__ */ jsx2("ul", { children: child.children.map((grandchild) => /* @__PURE__ */ jsx2("li", { children: /* @__PURE__ */ jsx2(MenuItemLink, { item: grandchild }) }, grandchild.id)) })
236
+ ] }, child.id)) });
237
+ }
238
+
239
+ // src/media/Media.tsx
240
+ import { jsx as jsx3 } from "react/jsx-runtime";
241
+ function Media({ media, alt, sizes = "100vw", cover = false, priority = false, className, style }) {
242
+ const { imageUrl } = useSiteTheme();
243
+ const text2 = alt ?? media.alt ?? "";
244
+ const classes = ["bapla-media", className].filter(Boolean).join(" ");
245
+ if (media.mime.startsWith("video/")) {
246
+ return /* @__PURE__ */ jsx3("video", { className: classes, style, src: mediaUrl(media), controls: true, preload: "metadata", width: media.width ?? void 0, height: media.height ?? void 0, "aria-label": text2 || void 0 });
247
+ }
248
+ const scalable = media.mime !== "image/svg+xml" && media.mime !== "image/gif";
249
+ const widths = scalable ? MEDIA_WIDTHS.filter((w) => media.width === null || w <= media.width) : [];
250
+ if (scalable && media.width !== null && !widths.includes(media.width) && media.width < MEDIA_WIDTHS[MEDIA_WIDTHS.length - 1]) {
251
+ widths.push(media.width);
252
+ }
253
+ const largest = widths.length > 0 ? Math.min(widths[widths.length - 1], 1280) : void 0;
254
+ return /* @__PURE__ */ jsx3(
255
+ "img",
256
+ {
257
+ className: classes,
258
+ src: largest !== void 0 ? imageUrl(media, { w: largest }) : mediaUrl(media),
259
+ srcSet: widths.length > 1 ? widths.map((w) => `${imageUrl(media, { w })} ${w}w`).join(", ") : void 0,
260
+ sizes: widths.length > 1 ? sizes : void 0,
261
+ alt: text2,
262
+ width: media.width ?? void 0,
263
+ height: media.height ?? void 0,
264
+ loading: priority ? "eager" : "lazy",
265
+ decoding: "async",
266
+ style: { ...cover ? { objectFit: "cover", objectPosition: objectPosition(media.focal_point) } : {}, ...style }
267
+ }
268
+ );
269
+ }
270
+
271
+ // src/richtext/RichText.tsx
272
+ import { Fragment } from "react";
273
+ import { Fragment as Fragment2, jsx as jsx4 } from "react/jsx-runtime";
274
+ var ALIGN = ["left", "center", "right", "justify"];
275
+ function align(node) {
276
+ const value = node.attrs?.textAlign;
277
+ return typeof value === "string" && ALIGN.includes(value) && value !== "left" ? { textAlign: value } : void 0;
278
+ }
279
+ function span(value) {
280
+ return typeof value === "number" && Number.isInteger(value) && value > 1 && value < 100 ? value : void 0;
281
+ }
282
+ function RichText({ doc, className }) {
283
+ const theme = useSiteTheme();
284
+ if (doc === null || typeof doc !== "object" || doc.type !== "doc") return null;
285
+ const marks = (node, text2) => (node.marks ?? []).reduceRight((inner, mark) => {
286
+ switch (mark.type) {
287
+ case "bold":
288
+ return /* @__PURE__ */ jsx4("strong", { children: inner });
289
+ case "italic":
290
+ return /* @__PURE__ */ jsx4("em", { children: inner });
291
+ case "underline":
292
+ return /* @__PURE__ */ jsx4("u", { children: inner });
293
+ case "strike":
294
+ return /* @__PURE__ */ jsx4("s", { children: inner });
295
+ case "code":
296
+ return /* @__PURE__ */ jsx4("code", { children: inner });
297
+ case "link": {
298
+ const href = safeHref(mark.attrs?.href, theme.resolveLink);
299
+ if (href === null) return inner;
300
+ const blank = mark.attrs?.target === "_blank";
301
+ return /* @__PURE__ */ jsx4("a", { href, ...blank ? { target: "_blank", rel: "noopener noreferrer" } : {}, children: inner });
302
+ }
303
+ default:
304
+ return inner;
305
+ }
306
+ }, text2);
307
+ const children = (node) => (node.content ?? []).map((child, index) => /* @__PURE__ */ jsx4(Fragment, { children: render(child) }, index));
308
+ function render(node) {
309
+ switch (node.type) {
310
+ case "text":
311
+ return typeof node.text === "string" ? marks(node, node.text) : null;
312
+ case "paragraph":
313
+ return /* @__PURE__ */ jsx4("p", { style: align(node), children: children(node) });
314
+ case "hardBreak":
315
+ return /* @__PURE__ */ jsx4("br", {});
316
+ case "heading": {
317
+ const level = node.attrs?.level;
318
+ const Tag = level === 3 ? "h3" : level === 4 ? "h4" : "h2";
319
+ return /* @__PURE__ */ jsx4(Tag, { style: align(node), children: children(node) });
320
+ }
321
+ case "bulletList":
322
+ return /* @__PURE__ */ jsx4("ul", { children: children(node) });
323
+ case "orderedList": {
324
+ const start = typeof node.attrs?.start === "number" && node.attrs.start !== 1 ? node.attrs.start : void 0;
325
+ return /* @__PURE__ */ jsx4("ol", { start, children: children(node) });
326
+ }
327
+ case "listItem":
328
+ return /* @__PURE__ */ jsx4("li", { children: children(node) });
329
+ case "blockquote":
330
+ return /* @__PURE__ */ jsx4("blockquote", { children: children(node) });
331
+ case "codeBlock": {
332
+ const language = typeof node.attrs?.language === "string" && /^[a-z0-9+#-]{1,30}$/i.test(node.attrs.language) ? `language-${node.attrs.language}` : void 0;
333
+ return /* @__PURE__ */ jsx4("pre", { children: /* @__PURE__ */ jsx4("code", { className: language, children: (node.content ?? []).map((c) => c.text ?? "").join("") }) });
334
+ }
335
+ case "horizontalRule":
336
+ return /* @__PURE__ */ jsx4("hr", {});
337
+ case "image": {
338
+ const src = node.attrs?.src;
339
+ const alt = typeof node.attrs?.alt === "string" ? node.attrs.alt : "";
340
+ if (typeof src === "string" && src.startsWith("web_med_")) {
341
+ const media = readMedia(src, theme.resolveMedia);
342
+ return media ? /* @__PURE__ */ jsx4(Media, { media, alt: alt || null }) : null;
343
+ }
344
+ return typeof src === "string" && /^https:\/\/[^/]/.test(src) ? /* @__PURE__ */ jsx4("img", { src, alt, loading: "lazy", decoding: "async" }) : null;
345
+ }
346
+ case "table":
347
+ return /* @__PURE__ */ jsx4("div", { className: "bapla-table", children: /* @__PURE__ */ jsx4("table", { children: /* @__PURE__ */ jsx4("tbody", { children: children(node) }) }) });
348
+ case "tableRow":
349
+ return /* @__PURE__ */ jsx4("tr", { children: children(node) });
350
+ case "tableHeader":
351
+ return /* @__PURE__ */ jsx4("th", { colSpan: span(node.attrs?.colspan), rowSpan: span(node.attrs?.rowspan), scope: "col", children: children(node) });
352
+ case "tableCell":
353
+ return /* @__PURE__ */ jsx4("td", { colSpan: span(node.attrs?.colspan), rowSpan: span(node.attrs?.rowspan), children: children(node) });
354
+ default:
355
+ return node.content ? /* @__PURE__ */ jsx4(Fragment2, { children: children(node) }) : null;
356
+ }
357
+ }
358
+ return /* @__PURE__ */ jsx4("div", { className: ["bapla-richtext", className].filter(Boolean).join(" "), children: children(doc) });
359
+ }
360
+
361
+ // src/blocks/props.ts
362
+ function choice(value, options, fallback) {
363
+ return typeof value === "string" && options.includes(value) ? value : fallback;
364
+ }
365
+ function integer(value, min, max) {
366
+ return typeof value === "number" && Number.isInteger(value) && value >= min && value <= max ? value : null;
367
+ }
368
+ function text(value) {
369
+ return typeof value === "string" && value.trim() !== "" ? value : null;
370
+ }
371
+ function flag(value) {
372
+ return value === true;
373
+ }
374
+
375
+ // src/blocks/content.tsx
376
+ import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
377
+ function Text({ props }) {
378
+ return /* @__PURE__ */ jsx5(RichText, { doc: props.content, className: "bapla-text" });
379
+ }
380
+ function MediaBlock({ props }) {
381
+ const { resolveMedia, resolveLink } = useSiteTheme();
382
+ const media = readMedia(props.media, resolveMedia);
383
+ if (!media) return null;
384
+ const aspect = choice(props.aspect, ["auto", "16:9", "4:3", "1:1", "3:4"], "auto");
385
+ const caption = text(props.caption);
386
+ const link = readLink(props.link);
387
+ const href = link ? safeHref(link.url, resolveLink) : null;
388
+ const image = /* @__PURE__ */ jsx5(Media, { media, alt: text(props.alt), cover: aspect !== "auto" });
389
+ return /* @__PURE__ */ jsxs2("figure", { className: "bapla-figure", "data-aspect": aspect, children: [
390
+ href ? /* @__PURE__ */ jsx5("a", { href, ...targetProps(link?.new_tab ?? false), children: image }) : image,
391
+ caption && /* @__PURE__ */ jsx5("figcaption", { children: caption })
392
+ ] });
393
+ }
394
+ function Button({ props }) {
395
+ const { resolveLink } = useSiteTheme();
396
+ const label = text(props.label);
397
+ const link = readLink(props.link);
398
+ if (!label) return null;
399
+ const href = link ? safeHref(link.url, resolveLink) : null;
400
+ const attributes = {
401
+ className: "bapla-button",
402
+ "data-variant": choice(props.variant, ["primary", "secondary", "outline", "link"], "primary"),
403
+ "data-size": choice(props.size, ["sm", "md", "lg"], "md")
404
+ };
405
+ return href ? /* @__PURE__ */ jsx5("a", { href, ...attributes, ...targetProps(link?.new_tab ?? false), children: label }) : /* @__PURE__ */ jsx5("span", { ...attributes, "aria-disabled": "true", children: label });
406
+ }
407
+ function embedUrl(address) {
408
+ let url;
409
+ try {
410
+ url = new URL(address);
411
+ } catch {
412
+ return null;
413
+ }
414
+ if (url.protocol !== "https:" && url.protocol !== "http:") return null;
415
+ const host = url.hostname.replace(/^www\./, "");
416
+ const id = (value) => value && /^[\w-]{1,64}$/.test(value) ? value : null;
417
+ if (host === "youtube.com" || host === "m.youtube.com" || host === "youtube-nocookie.com") {
418
+ const video = url.pathname === "/watch" ? id(url.searchParams.get("v")) : id(url.pathname.match(/^\/(?:embed|shorts)\/([^/]+)/)?.[1]);
419
+ return video ? `https://www.youtube-nocookie.com/embed/${video}` : null;
420
+ }
421
+ if (host === "youtu.be") {
422
+ const video = id(url.pathname.slice(1));
423
+ return video ? `https://www.youtube-nocookie.com/embed/${video}` : null;
424
+ }
425
+ if (host === "vimeo.com" || host === "player.vimeo.com") {
426
+ const video = url.pathname.match(/(\d{3,12})/)?.[1];
427
+ return video ? `https://player.vimeo.com/video/${video}` : null;
428
+ }
429
+ if (host === "dailymotion.com") {
430
+ const video = id(url.pathname.match(/^\/(?:embed\/)?video\/([^/]+)/)?.[1]);
431
+ return video ? `https://www.dailymotion.com/embed/video/${video}` : null;
432
+ }
433
+ if (host === "google.com" && url.pathname.startsWith("/maps/embed")) return `https://www.google.com${url.pathname}${url.search}`;
434
+ if (host === "openstreetmap.org" && url.pathname === "/export/embed.html") return `https://www.openstreetmap.org${url.pathname}${url.search}`;
435
+ return null;
436
+ }
437
+ function Embed({ props }) {
438
+ const { labels } = useSiteTheme();
439
+ const link = readLink(props.url);
440
+ if (!link) return null;
441
+ const src = embedUrl(link.url);
442
+ if (!src) {
443
+ const href = safeHref(link.url);
444
+ return href ? /* @__PURE__ */ jsx5("p", { className: "bapla-embed-link", children: /* @__PURE__ */ jsx5("a", { href, ...targetProps(true), children: link.label ?? link.url }) }) : null;
445
+ }
446
+ return /* @__PURE__ */ jsx5("div", { className: "bapla-embed", "data-aspect": choice(props.aspect, ["16:9", "4:3", "1:1"], "16:9"), children: /* @__PURE__ */ jsx5("iframe", { src, title: link.label ?? labels.embed, loading: "lazy", allowFullScreen: true, referrerPolicy: "strict-origin-when-cross-origin", allow: "fullscreen; picture-in-picture" }) });
447
+ }
448
+
449
+ // src/blocks/interactive.tsx
450
+ import { useEffect, useId, useRef, useState } from "react";
451
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
452
+ function useReducedMotion() {
453
+ const [reduced, setReduced] = useState(false);
454
+ useEffect(() => {
455
+ if (typeof window === "undefined" || !window.matchMedia) return;
456
+ const query = window.matchMedia("(prefers-reduced-motion: reduce)");
457
+ setReduced(query.matches);
458
+ const change = () => setReduced(query.matches);
459
+ query.addEventListener?.("change", change);
460
+ return () => query.removeEventListener?.("change", change);
461
+ }, []);
462
+ return reduced;
463
+ }
464
+ function Carousel({ block, props, render }) {
465
+ const { labels } = useSiteTheme();
466
+ const slides = block.children ?? [];
467
+ const [index, setIndex] = useState(0);
468
+ const [paused, setPaused] = useState(false);
469
+ const [hovered, setHovered] = useState(false);
470
+ const reduced = useReducedMotion();
471
+ const autoplay = flag(props.autoplay) && !reduced && slides.length > 1;
472
+ const interval = (integer(props.interval, 2, 30) ?? 5) * 1e3;
473
+ const playing = autoplay && !paused && !hovered;
474
+ const arrows = props.arrows === void 0 ? true : flag(props.arrows);
475
+ const dots = props.dots === void 0 ? true : flag(props.dots);
476
+ const count = slides.length;
477
+ useEffect(() => {
478
+ if (!playing) return;
479
+ const timer = window.setInterval(() => setIndex((i) => (i + 1) % count), interval);
480
+ return () => window.clearInterval(timer);
481
+ }, [playing, interval, count]);
482
+ if (count === 0) return null;
483
+ const go = (next) => setIndex((next + count) % count);
484
+ const onKeyDown = (event) => {
485
+ if (event.key === "ArrowLeft") go(index - 1);
486
+ else if (event.key === "ArrowRight") go(index + 1);
487
+ else return;
488
+ event.preventDefault();
489
+ };
490
+ return /* @__PURE__ */ jsxs3(
491
+ "section",
492
+ {
493
+ className: "bapla-carousel",
494
+ "aria-roledescription": "carousel",
495
+ "aria-label": labels.carousel,
496
+ onKeyDown,
497
+ onMouseEnter: () => setHovered(true),
498
+ onMouseLeave: () => setHovered(false),
499
+ onFocus: () => setHovered(true),
500
+ onBlur: () => setHovered(false),
501
+ children: [
502
+ /* @__PURE__ */ jsxs3("div", { className: "bapla-carousel-controls", children: [
503
+ autoplay && /* @__PURE__ */ jsx6("button", { type: "button", className: "bapla-carousel-play", onClick: () => setPaused((p) => !p), children: paused ? labels.play : labels.pause }),
504
+ arrows && count > 1 && /* @__PURE__ */ jsxs3(Fragment3, { children: [
505
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "bapla-carousel-prev", onClick: () => go(index - 1), children: labels.previous }),
506
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "bapla-carousel-next", onClick: () => go(index + 1), children: labels.next })
507
+ ] })
508
+ ] }),
509
+ /* @__PURE__ */ jsx6("div", { className: "bapla-carousel-slides", "aria-live": playing ? "off" : "polite", children: slides.map((slide, i) => /* @__PURE__ */ jsx6("div", { role: "group", "aria-roledescription": "slide", "aria-label": labels.slide(i + 1, count), className: "bapla-carousel-slide", hidden: i !== index, children: render([slide]) }, slide.id)) }),
510
+ dots && count > 1 && /* @__PURE__ */ jsx6("div", { className: "bapla-carousel-dots", children: slides.map((slide, i) => /* @__PURE__ */ jsx6("button", { type: "button", "aria-label": labels.goToSlide(i + 1), "aria-current": i === index ? "true" : void 0, onClick: () => go(i) }, slide.id)) })
511
+ ]
512
+ }
513
+ );
514
+ }
515
+ function Tabs({ block, props, render }) {
516
+ const tabs = block.children ?? [];
517
+ const [selected, setSelected] = useState(0);
518
+ const refs = useRef([]);
519
+ const base = useId();
520
+ if (tabs.length === 0) return null;
521
+ const select = (i) => {
522
+ const next = (i + tabs.length) % tabs.length;
523
+ setSelected(next);
524
+ refs.current[next]?.focus();
525
+ };
526
+ const onKeyDown = (event) => {
527
+ const keys = { ArrowRight: selected + 1, ArrowLeft: selected - 1, Home: 0, End: tabs.length - 1 };
528
+ if (!(event.key in keys)) return;
529
+ event.preventDefault();
530
+ select(keys[event.key]);
531
+ };
532
+ return /* @__PURE__ */ jsxs3("div", { className: "bapla-tabs", "data-alignment": choice(props.alignment, ["start", "center", "stretch"], "start"), children: [
533
+ /* @__PURE__ */ jsx6("div", { role: "tablist", className: "bapla-tablist", onKeyDown, children: tabs.map((tab, i) => /* @__PURE__ */ jsx6(
534
+ "button",
535
+ {
536
+ ref: (el) => {
537
+ refs.current[i] = el;
538
+ },
539
+ type: "button",
540
+ role: "tab",
541
+ id: `${base}-tab-${i}`,
542
+ "aria-selected": i === selected,
543
+ "aria-controls": `${base}-panel-${i}`,
544
+ tabIndex: i === selected ? 0 : -1,
545
+ onClick: () => setSelected(i),
546
+ children: text(tab.props?.label) ?? String(i + 1)
547
+ },
548
+ tab.id
549
+ )) }),
550
+ tabs.map((tab, i) => /* @__PURE__ */ jsx6("div", { role: "tabpanel", id: `${base}-panel-${i}`, "aria-labelledby": `${base}-tab-${i}`, tabIndex: 0, hidden: i !== selected, className: "bapla-tabpanel", children: render(tab.children) }, tab.id))
551
+ ] });
552
+ }
553
+ function Accordion({ block, props, render }) {
554
+ const items = block.children ?? [];
555
+ const multiple = flag(props.multiple);
556
+ const base = useId();
557
+ const [open, setOpen] = useState(() => {
558
+ const initial = items.filter((item) => flag(item.props?.open)).map((item) => item.id);
559
+ return new Set(multiple ? initial : initial.slice(0, 1));
560
+ });
561
+ const toggle = (id) => setOpen((current) => {
562
+ const next = new Set(multiple ? current : [...current].filter((x) => x === id));
563
+ if (next.has(id)) next.delete(id);
564
+ else next.add(id);
565
+ return next;
566
+ });
567
+ return /* @__PURE__ */ jsx6("div", { className: "bapla-accordion", children: items.map((item, i) => {
568
+ const expanded = open.has(item.id);
569
+ return /* @__PURE__ */ jsxs3("div", { className: "bapla-accordion-item", children: [
570
+ /* @__PURE__ */ jsx6("h3", { children: /* @__PURE__ */ jsx6("button", { type: "button", id: `${base}-header-${i}`, "aria-expanded": expanded, "aria-controls": `${base}-panel-${i}`, onClick: () => toggle(item.id), children: text(item.props?.label) ?? String(i + 1) }) }),
571
+ /* @__PURE__ */ jsx6("div", { role: "region", id: `${base}-panel-${i}`, "aria-labelledby": `${base}-header-${i}`, hidden: !expanded, className: "bapla-accordion-panel", children: render(item.children) })
572
+ ] }, item.id);
573
+ }) });
574
+ }
575
+ function Passthrough({ children }) {
576
+ return /* @__PURE__ */ jsx6(Fragment3, { children });
577
+ }
578
+
579
+ // src/blocks/layout.tsx
580
+ import { createContext as createContext3, useContext as useContext3 } from "react";
581
+ import { jsx as jsx7 } from "react/jsx-runtime";
582
+ var SIZES = ["none", "sm", "md", "lg"];
583
+ function Section({ props, children }) {
584
+ const { resolveMedia } = useSiteTheme();
585
+ const background = choice(props.background, ["none", "muted", "accent", "dark", "image"], "none");
586
+ const image = background === "image" ? readMedia(props.background_image, resolveMedia) : null;
587
+ const anchor = text(props.anchor);
588
+ return /* @__PURE__ */ jsx7(
589
+ "section",
590
+ {
591
+ className: "bapla-section",
592
+ id: anchor && /^[A-Za-z][\w-]*$/.test(anchor) ? anchor : void 0,
593
+ "data-width": choice(props.width, ["narrow", "default", "wide", "full"], "default"),
594
+ "data-background": background,
595
+ "data-padding": choice(props.padding, SIZES, "md"),
596
+ style: image ? { backgroundImage: `url("${mediaUrl(image, { w: 1920 }).replace(/["\\()\s]/g, (c) => encodeURIComponent(c))}")` } : void 0,
597
+ children: /* @__PURE__ */ jsx7("div", { className: "bapla-container", children })
598
+ }
599
+ );
600
+ }
601
+ var GridContext = createContext3({ mode: "equal", unit: "twelfths" });
602
+ function Grid({ block, props, children }) {
603
+ const mode = choice(props.mode, ["equal", "auto", "custom"], "equal");
604
+ const unit = choice(props.unit, ["twelfths", "percent"], "twelfths");
605
+ const columns = block.children ?? [];
606
+ let template;
607
+ if (mode === "auto") {
608
+ const min = integer(props.min_width, 120, 800) ?? 240;
609
+ template = `repeat(auto-fit, minmax(min(100%, ${min}px), 1fr))`;
610
+ } else if (mode === "custom" && unit === "percent") {
611
+ const widths = columns.map((column) => integer(column.props?.width, 1, 100) ?? Math.floor(100 / Math.max(columns.length, 1)));
612
+ const total = widths.reduce((sum, w) => sum + w, 0);
613
+ template = [...widths, ...total < 100 ? [100 - total] : []].map((w) => `minmax(0, ${w}fr)`).join(" ") || "minmax(0, 1fr)";
614
+ } else if (mode === "custom") {
615
+ template = "repeat(12, minmax(0, 1fr))";
616
+ } else {
617
+ const count = integer(props.columns, 1, 12) ?? Math.max(columns.length, 1);
618
+ template = `repeat(${count}, minmax(0, 1fr))`;
619
+ }
620
+ return /* @__PURE__ */ jsx7(GridContext.Provider, { value: { mode, unit }, children: /* @__PURE__ */ jsx7(
621
+ "div",
622
+ {
623
+ className: "bapla-grid",
624
+ "data-mode": mode,
625
+ "data-unit": mode === "custom" ? unit : void 0,
626
+ "data-gap": choice(props.gap, SIZES, "md"),
627
+ "data-align": choice(props.align, ["start", "center", "end", "stretch"], "stretch"),
628
+ "data-stack": choice(props.stack_below, ["sm", "md", "lg", "never"], "md"),
629
+ style: { "--bapla-grid-cols": template },
630
+ children
631
+ }
632
+ ) });
633
+ }
634
+ function Column({ props, children }) {
635
+ const { mode, unit } = useContext3(GridContext);
636
+ const style = {};
637
+ if (mode === "custom" && unit === "twelfths") {
638
+ style["--bapla-col-span"] = `span ${integer(props.width, 1, 12) ?? 12}`;
639
+ }
640
+ const align2 = typeof props.align === "string" ? choice(props.align, ["start", "center", "end"], "start") : void 0;
641
+ return /* @__PURE__ */ jsx7("div", { className: "bapla-column", "data-align": align2, style, children });
642
+ }
643
+ function Divider({ props }) {
644
+ return /* @__PURE__ */ jsx7("hr", { className: "bapla-divider", "data-size": choice(props.size, SIZES, "md") });
645
+ }
646
+ function Spacer({ props }) {
647
+ return /* @__PURE__ */ jsx7("div", { className: "bapla-spacer", "aria-hidden": "true", "data-size": choice(props.size, ["sm", "md", "lg", "xl"], "md") });
648
+ }
649
+
650
+ // src/blocks/registry.ts
651
+ var standardBlocks = {
652
+ section: Section,
653
+ grid: Grid,
654
+ column: Column,
655
+ text: Text,
656
+ media: MediaBlock,
657
+ carousel: Carousel,
658
+ tabs: Tabs,
659
+ tab: Passthrough,
660
+ accordion: Accordion,
661
+ accordion_item: Passthrough,
662
+ button: Button,
663
+ embed: Embed,
664
+ divider: Divider,
665
+ spacer: Spacer,
666
+ menu_links: MenuLinks
667
+ };
668
+
669
+ // src/blocks/Blocks.tsx
670
+ import { Fragment as Fragment5, jsx as jsx8 } from "react/jsx-runtime";
671
+ function Blocks({ blocks }) {
672
+ const { components } = useSiteTheme();
673
+ const registry = { ...standardBlocks, ...components };
674
+ const render = (list) => (Array.isArray(list) ? list : []).map((block, index) => {
675
+ const Component = block && typeof block.type === "string" ? registry[block.type] : void 0;
676
+ if (!Component) return null;
677
+ const props = block.props && typeof block.props === "object" && !Array.isArray(block.props) ? block.props : {};
678
+ return /* @__PURE__ */ jsx8(Fragment4, { children: /* @__PURE__ */ jsx8(Component, { block, props, render, children: block.children && block.children.length > 0 ? render(block.children) : null }) }, typeof block.id === "string" ? block.id : index);
679
+ });
680
+ return /* @__PURE__ */ jsx8(Fragment5, { children: render(blocks) });
681
+ }
682
+
683
+ // src/media/Image.tsx
684
+ import { jsx as jsx9, jsxs as jsxs4 } from "react/jsx-runtime";
685
+ function Image({ media, size, w, h, fit, q, alt, widths, sizes, formats, priority = false, className, style }) {
686
+ const { imageUrl, imageSizes } = useSiteTheme();
687
+ const named = size !== void 0 ? imageSizes[size] ?? {} : {};
688
+ const box = { ...named, ...defined({ w, h, fit, q }) };
689
+ const text2 = alt ?? media.alt ?? "";
690
+ const classes = ["bapla-image", className].filter(Boolean).join(" ");
691
+ const common = {
692
+ alt: text2,
693
+ className: classes,
694
+ loading: priority ? "eager" : "lazy",
695
+ decoding: "async",
696
+ ...priority ? { fetchPriority: "high" } : {},
697
+ style: { ...box.fit === "cover" ? { objectFit: "cover", objectPosition: objectPosition(media.focal_point) } : {}, ...style }
698
+ };
699
+ if (!isTransformable(media)) {
700
+ return /* @__PURE__ */ jsx9("img", { ...common, src: mediaUrl(media), width: media.width ?? void 0, height: media.height ?? void 0 });
701
+ }
702
+ const [outW, outH] = output(media, box);
703
+ const candidates = (fm) => {
704
+ if (widths && widths.length > 0) {
705
+ const list = responsive(media, box, widths);
706
+ return list.map((width) => `${imageUrl(media, scaled(box, width, fm))} ${width}w`).join(", ");
707
+ }
708
+ const two = outW !== void 0 && media.width !== null && outW * 2 <= media.width;
709
+ return [`${imageUrl(media, transform(box, 1, fm))} 1x`, ...two ? [`${imageUrl(media, transform(box, 2, fm))} 2x`] : []].join(", ");
710
+ };
711
+ const responsiveSizes = widths && widths.length > 0 ? sizes ?? "100vw" : void 0;
712
+ const img = /* @__PURE__ */ jsx9(
713
+ "img",
714
+ {
715
+ ...common,
716
+ src: imageUrl(media, widths && widths.length > 0 ? scaled(box, Math.min(Math.max(...responsive(media, box, widths)), 1280)) : transform(box, 1)),
717
+ srcSet: candidates(),
718
+ sizes: responsiveSizes,
719
+ width: outW,
720
+ height: outH
721
+ }
722
+ );
723
+ if (!formats || formats.length === 0) return img;
724
+ return /* @__PURE__ */ jsxs4("picture", { children: [
725
+ formats.map((fm) => /* @__PURE__ */ jsx9("source", { type: `image/${fm}`, srcSet: candidates(fm), sizes: responsiveSizes }, fm)),
726
+ img
727
+ ] });
728
+ }
729
+ function output(media, box) {
730
+ const W = media.width;
731
+ const H = media.height;
732
+ if (W === null || H === null) return [box.w, box.h];
733
+ if (box.w === void 0 && box.h === void 0) return [W, H];
734
+ if (box.fit === "cover" && box.w !== void 0 && box.h !== void 0) {
735
+ const shrink = Math.min(1, W / box.w, H / box.h);
736
+ return [Math.round(box.w * shrink), Math.round(box.h * shrink)];
737
+ }
738
+ const scale = Math.min(1, box.w === void 0 ? 1 : box.w / W, box.h === void 0 ? 1 : box.h / H);
739
+ return [Math.round(W * scale), Math.round(H * scale)];
740
+ }
741
+ function transform(box, density, fm) {
742
+ return defined({
743
+ w: box.w === void 0 ? void 0 : box.w * density,
744
+ h: box.h === void 0 ? void 0 : box.h * density,
745
+ fit: box.fit,
746
+ q: box.q,
747
+ fm
748
+ });
749
+ }
750
+ function scaled(box, width, fm) {
751
+ const h = box.fit === "cover" && box.w !== void 0 && box.h !== void 0 ? Math.round(width * box.h / box.w) : void 0;
752
+ return defined({ w: width, h, fit: box.fit, q: box.q, fm });
753
+ }
754
+ function responsive(media, box, widths) {
755
+ const sorted = [...new Set(widths)].sort((a, b) => a - b);
756
+ if (media.width === null) return sorted;
757
+ const limit = box.fit === "cover" && box.w !== void 0 && box.h !== void 0 && media.height !== null ? Math.min(media.width, Math.floor(media.height * box.w / box.h)) : media.width;
758
+ const kept = sorted.filter((width) => width <= limit);
759
+ if (kept.length < sorted.length && !kept.includes(limit)) kept.push(limit);
760
+ return kept;
761
+ }
762
+ function defined(value) {
763
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
764
+ }
765
+
766
+ // src/menu/NavBar.tsx
767
+ import { useEffect as useEffect2, useId as useId2, useRef as useRef2, useState as useState2 } from "react";
768
+ import { jsx as jsx10, jsxs as jsxs5 } from "react/jsx-runtime";
769
+ function NavBar({ items, label, brand, className }) {
770
+ const { labels } = useSiteTheme();
771
+ const [open, setOpen] = useState2(false);
772
+ const [openItem, setOpenItem] = useState2(null);
773
+ const nav = useRef2(null);
774
+ const listId = useId2();
775
+ useEffect2(() => {
776
+ const close = (event) => {
777
+ if (nav.current && !nav.current.contains(event.target)) setOpenItem(null);
778
+ };
779
+ document.addEventListener("pointerdown", close);
780
+ return () => document.removeEventListener("pointerdown", close);
781
+ }, []);
782
+ const onKeyDown = (event) => {
783
+ if (event.key === "Escape" && open && openItem === null) {
784
+ setOpen(false);
785
+ nav.current?.querySelector(".bapla-nav-toggle")?.focus();
786
+ }
787
+ };
788
+ return /* @__PURE__ */ jsxs5(
789
+ "nav",
790
+ {
791
+ ref: nav,
792
+ className: ["bapla-nav", className].filter(Boolean).join(" "),
793
+ "aria-label": label ?? labels.mainNavigation,
794
+ onKeyDown,
795
+ onBlur: (event) => {
796
+ if (!event.currentTarget.contains(event.relatedTarget)) setOpenItem(null);
797
+ },
798
+ children: [
799
+ brand && /* @__PURE__ */ jsx10("div", { className: "bapla-nav-brand", children: brand }),
800
+ /* @__PURE__ */ jsx10("button", { type: "button", className: "bapla-nav-toggle", "aria-expanded": open, "aria-controls": listId, onClick: () => setOpen((o) => !o), children: labels.menu }),
801
+ /* @__PURE__ */ jsx10("ul", { id: listId, className: "bapla-nav-list", "data-open": open ? "true" : "false", children: items.map((item) => /* @__PURE__ */ jsx10(NavItem, { item, level: 0, open: openItem === item.id, onToggle: (o) => setOpenItem(o ? item.id : null) }, item.id)) })
802
+ ]
803
+ }
804
+ );
805
+ }
806
+ function NavItem({ item, level, open, onToggle }) {
807
+ const { labels } = useSiteTheme();
808
+ const [openChild, setOpenChild] = useState2(null);
809
+ const button = useRef2(null);
810
+ const id = useId2();
811
+ const hasPanel = item.kind === "panel" && item.panel !== null && item.panel.length > 0;
812
+ const hasChildren = item.children.length > 0;
813
+ if (level > 0 && item.kind === "heading") {
814
+ return /* @__PURE__ */ jsxs5("li", { className: "bapla-nav-item", "data-kind": "heading", children: [
815
+ /* @__PURE__ */ jsx10("span", { className: "bapla-nav-heading", children: item.label }),
816
+ hasChildren && /* @__PURE__ */ jsx10(SubList, { items: item.children, level: level + 1, openChild, setOpenChild })
817
+ ] });
818
+ }
819
+ if (!hasPanel && !hasChildren) {
820
+ return /* @__PURE__ */ jsx10("li", { className: "bapla-nav-item", "data-kind": item.kind, "data-active": item.active ? "true" : void 0, children: /* @__PURE__ */ jsx10(MenuItemLink, { item, className: "bapla-nav-link" }) });
821
+ }
822
+ const onKeyDown = (event) => {
823
+ if (event.key === "Escape" && open) {
824
+ event.stopPropagation();
825
+ onToggle(false);
826
+ button.current?.focus();
827
+ }
828
+ };
829
+ const linked = item.kind === "link" && item.url !== null;
830
+ return /* @__PURE__ */ jsxs5("li", { className: "bapla-nav-item", "data-kind": item.kind, "data-active": item.active ? "true" : void 0, "data-open": open ? "true" : "false", onKeyDown, children: [
831
+ linked && /* @__PURE__ */ jsx10(MenuItemLink, { item, className: "bapla-nav-link" }),
832
+ /* @__PURE__ */ jsx10(
833
+ "button",
834
+ {
835
+ ref: button,
836
+ type: "button",
837
+ className: "bapla-nav-disclosure",
838
+ "aria-expanded": open,
839
+ "aria-controls": id,
840
+ "aria-label": linked ? labels.submenu(item.label) : void 0,
841
+ onClick: () => onToggle(!open),
842
+ children: linked ? /* @__PURE__ */ jsx10("span", { "aria-hidden": "true", className: "bapla-nav-caret" }) : item.label
843
+ }
844
+ ),
845
+ hasPanel ? /* @__PURE__ */ jsx10("div", { id, className: "bapla-mega", hidden: !open, children: /* @__PURE__ */ jsx10(MenuItemContext.Provider, { value: item, children: /* @__PURE__ */ jsx10(Blocks, { blocks: item.panel }) }) }) : /* @__PURE__ */ jsx10(SubList, { id, hidden: !open, items: item.children, level: level + 1, openChild, setOpenChild })
846
+ ] });
847
+ }
848
+ function SubList({ id, hidden, items, level, openChild, setOpenChild }) {
849
+ return /* @__PURE__ */ jsx10("ul", { id, className: "bapla-nav-submenu", "data-level": level, hidden, children: items.map((child) => /* @__PURE__ */ jsx10(NavItem, { item: child, level, open: openChild === child.id, onToggle: (o) => setOpenChild(o ? child.id : null) }, child.id)) });
850
+ }
851
+
852
+ // src/menu/Breadcrumb.tsx
853
+ import { jsx as jsx11 } from "react/jsx-runtime";
854
+ function Breadcrumb({ items, className }) {
855
+ const { labels, resolveLink } = useSiteTheme();
856
+ if (items.length === 0) return null;
857
+ return /* @__PURE__ */ jsx11("nav", { className: ["bapla-breadcrumb", className].filter(Boolean).join(" "), "aria-label": labels.breadcrumb, children: /* @__PURE__ */ jsx11("ol", { children: items.map((item, i) => {
858
+ const last = i === items.length - 1;
859
+ const href = last ? null : safeHref(item.url, resolveLink);
860
+ return /* @__PURE__ */ jsx11("li", { children: last ? /* @__PURE__ */ jsx11("span", { "aria-current": "page", children: item.label }) : href ? /* @__PURE__ */ jsx11("a", { href, children: item.label }) : /* @__PURE__ */ jsx11("span", { children: item.label }) }, i);
861
+ }) }) });
862
+ }
863
+ export {
864
+ Accordion,
865
+ Blocks,
866
+ BlocksProvider,
867
+ Breadcrumb,
868
+ Button,
869
+ Carousel,
870
+ Column,
871
+ Divider,
872
+ Embed,
873
+ Grid,
874
+ Image,
875
+ MEDIA_WIDTHS,
876
+ Media,
877
+ MediaBlock,
878
+ MenuItemContext,
879
+ MenuItemLink,
880
+ MenuLinks,
881
+ NavBar,
882
+ RichText,
883
+ Section,
884
+ Spacer,
885
+ Tabs,
886
+ Text,
887
+ defaultImageSizes,
888
+ embedUrl,
889
+ fontImports,
890
+ fontStack,
891
+ frenchLabels,
892
+ imgproxyPath,
893
+ isTransformable,
894
+ mediaUrl,
895
+ objectPosition,
896
+ readLink,
897
+ readMedia,
898
+ safeHref,
899
+ standardBlocks,
900
+ targetProps,
901
+ themeCss,
902
+ useMenuItem,
903
+ useSiteTheme
904
+ };