@styleflow.app/flowpress-astro-core 1.0.0-beta.0 → 1.0.0-beta.10

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.
Files changed (74) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +94 -3
  3. package/dist/astro/BaseLayout.astro +5 -1
  4. package/dist/astro/CompositionRenderer.astro +127 -0
  5. package/dist/astro/DefaultBlockAdapter.astro +35 -0
  6. package/dist/astro/DefaultTemplate.astro +6 -1
  7. package/dist/astro/GutenbergRenderer.astro +215 -55
  8. package/dist/astro/ThemeRuntime.astro +24 -0
  9. package/dist/astro/blocks/CarouselBlock.astro +198 -0
  10. package/dist/astro/blocks/CarouselItemBlock.astro +26 -0
  11. package/dist/astro/blocks/ColumnBlock.astro +2 -3
  12. package/dist/astro/blocks/FormBlock.astro +5 -5
  13. package/dist/astro/blocks/GridBlock.astro +2 -2
  14. package/dist/astro/blocks/HeroBlock.astro +2 -3
  15. package/dist/astro/blocks/ImageBlock.astro +2 -3
  16. package/dist/astro/blocks/ListItemFallback.astro +7 -5
  17. package/dist/astro/blocks/NumberSpinnerBlock.astro +24 -18
  18. package/dist/astro/blocks/SemanticElement.astro +35 -0
  19. package/dist/astro/blocks/SiteLogoBlock.astro +53 -0
  20. package/dist/astro/blocks/TextBlock.astro +27 -23
  21. package/dist/astro/blocks/ThemeToggleBlock.astro +23 -0
  22. package/dist/astro/blocks/helpers.d.ts +7 -0
  23. package/dist/astro/blocks/helpers.d.ts.map +1 -1
  24. package/dist/astro/blocks/helpers.js +51 -41
  25. package/dist/astro/blocks/helpers.js.map +1 -1
  26. package/dist/astro/blocks/helpers.ts +63 -45
  27. package/dist/block-renderers.d.ts +19 -2
  28. package/dist/block-renderers.d.ts.map +1 -1
  29. package/dist/block-renderers.js.map +1 -1
  30. package/dist/carousel.d.ts +22 -0
  31. package/dist/carousel.d.ts.map +1 -0
  32. package/dist/carousel.js +102 -0
  33. package/dist/carousel.js.map +1 -0
  34. package/dist/client.d.ts +2 -1
  35. package/dist/client.d.ts.map +1 -1
  36. package/dist/client.js +31 -0
  37. package/dist/client.js.map +1 -1
  38. package/dist/composition.d.ts +14 -0
  39. package/dist/composition.d.ts.map +1 -0
  40. package/dist/composition.js +124 -0
  41. package/dist/composition.js.map +1 -0
  42. package/dist/default-blocks.d.ts +9 -0
  43. package/dist/default-blocks.d.ts.map +1 -0
  44. package/dist/default-blocks.js +54 -0
  45. package/dist/default-blocks.js.map +1 -0
  46. package/dist/env.d.ts +9 -0
  47. package/dist/env.d.ts.map +1 -1
  48. package/dist/env.js +20 -0
  49. package/dist/env.js.map +1 -1
  50. package/dist/index.d.ts +6 -0
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +6 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/links.d.ts +5 -0
  55. package/dist/links.d.ts.map +1 -0
  56. package/dist/links.js +32 -0
  57. package/dist/links.js.map +1 -0
  58. package/dist/media.d.ts +8 -0
  59. package/dist/media.d.ts.map +1 -0
  60. package/dist/media.js +121 -0
  61. package/dist/media.js.map +1 -0
  62. package/dist/styleflow.d.ts +2 -0
  63. package/dist/styleflow.d.ts.map +1 -1
  64. package/dist/styleflow.js +47 -5
  65. package/dist/styleflow.js.map +1 -1
  66. package/dist/theme-runtime.d.ts +16 -0
  67. package/dist/theme-runtime.d.ts.map +1 -0
  68. package/dist/theme-runtime.js +77 -0
  69. package/dist/theme-runtime.js.map +1 -0
  70. package/dist/tsconfig.tsbuildinfo +1 -1
  71. package/dist/types.d.ts +84 -1
  72. package/dist/types.d.ts.map +1 -1
  73. package/dist/types.js.map +1 -1
  74. package/package.json +9 -3
@@ -1,12 +1,24 @@
1
1
  ---
2
2
  import S from "@styleflow.app/astro/S.astro";
3
3
  import type {
4
+ FlowpressBlockAdapters,
4
5
  FlowpressBlockRenderers,
5
6
  FlowpressIconRenderer
6
7
  } from "../block-renderers.js";
8
+ import {
9
+ flowpressAdapterCompatibilityIssue,
10
+ flowpressBlockTypeDescriptors
11
+ } from "../composition.js";
7
12
  import type { StyleflowContract } from "../styleflow.js";
8
- import type { FlowpressBlock } from "../types.js";
13
+ import type {
14
+ FlowpressBlock,
15
+ FlowpressBlockHtmlProps,
16
+ FlowpressCompositionV1
17
+ } from "../types.js";
18
+ import type { DynamicStyleflowProps } from "../styleflow.js";
9
19
  import ColumnBlock from "./blocks/ColumnBlock.astro";
20
+ import CarouselBlock from "./blocks/CarouselBlock.astro";
21
+ import CarouselItemBlock from "./blocks/CarouselItemBlock.astro";
10
22
  import FormBlock from "./blocks/FormBlock.astro";
11
23
  import GridBlock from "./blocks/GridBlock.astro";
12
24
  import HeroBlock from "./blocks/HeroBlock.astro";
@@ -27,9 +39,33 @@ interface Props {
27
39
  contract?: StyleflowContract;
28
40
  blockRenderers?: FlowpressBlockRenderers;
29
41
  iconRenderer?: FlowpressIconRenderer;
42
+ rootHtmlProps?: FlowpressBlockHtmlProps;
43
+ rootStyleflowProps?: DynamicStyleflowProps;
44
+ adapterDefinitions?: FlowpressBlockAdapters;
45
+ composition?: FlowpressCompositionV1;
46
+ parentBlockName?: string;
47
+ production?: boolean;
48
+ depth?: number;
49
+ skipCustomRendererFor?: string;
50
+ requireRegisteredAdapters?: boolean;
30
51
  }
31
52
 
32
- const { blocks, contract, blockRenderers = {}, iconRenderer: IconRenderer } = Astro.props;
53
+ const {
54
+ blocks,
55
+ contract,
56
+ blockRenderers = {},
57
+ iconRenderer: IconRenderer,
58
+ rootHtmlProps,
59
+ rootStyleflowProps,
60
+ adapterDefinitions,
61
+ composition,
62
+ parentBlockName,
63
+ production = import.meta.env.PROD,
64
+ depth = 0,
65
+ skipCustomRendererFor,
66
+ requireRegisteredAdapters = false
67
+ } = Astro.props;
68
+ const descriptors = composition ? flowpressBlockTypeDescriptors(composition) : undefined;
33
69
 
34
70
  function youtubeEmbedUrl(value: unknown): string | null {
35
71
  try {
@@ -47,25 +83,92 @@ function youtubeEmbedUrl(value: unknown): string | null {
47
83
  return null;
48
84
  }
49
85
  }
86
+
87
+ function nestedRendererProps(nextParentBlockName: string) {
88
+ return {
89
+ adapterDefinitions,
90
+ composition,
91
+ parentBlockName: nextParentBlockName,
92
+ production,
93
+ depth: depth + 1,
94
+ requireRegisteredAdapters
95
+ };
96
+ }
50
97
  ---
51
98
 
52
- {blocks.map((block) => {
99
+ {blocks.map((block, index) => {
53
100
  const name = block.name.replace(/^sf\//, "");
54
101
  const children = block.innerBlocks ?? [];
55
- const semantic = semanticProps(block.attributes, contract);
56
- const htmlAttributes = blockHtmlAttributes(block.attributes);
57
- const CustomBlockRenderer = blockRenderers[block.name];
102
+ const semantic = index === 0 && blocks.length === 1 && rootStyleflowProps
103
+ ? rootStyleflowProps
104
+ : semanticProps(block.attributes, contract);
105
+ const htmlAttributes = index === 0 && blocks.length === 1 && rootHtmlProps
106
+ ? rootHtmlProps
107
+ : blockHtmlAttributes(block.attributes);
108
+ const CustomBlockRenderer = block.name === skipCustomRendererFor
109
+ ? undefined
110
+ : blockRenderers[block.name];
111
+ const compatibilityIssue = descriptors && adapterDefinitions
112
+ ? flowpressAdapterCompatibilityIssue(block.name, descriptors, adapterDefinitions)
113
+ : null;
114
+
115
+ if (compatibilityIssue && production && block.name !== skipCustomRendererFor) {
116
+ throw new Error(compatibilityIssue.message);
117
+ }
118
+
119
+ if (
120
+ requireRegisteredAdapters
121
+ && block.name !== skipCustomRendererFor
122
+ && !CustomBlockRenderer
123
+ ) {
124
+ const message = compatibilityIssue?.message
125
+ ?? `Missing FlowPress block adapter for "${block.name}".`;
126
+ if (production) throw new Error(message);
127
+ return (
128
+ <aside data-flowpress-compatibility="missing-adapter" data-block-name={block.name} role="status">
129
+ <strong>{message}</strong>
130
+ <code>{compatibilityIssue?.command ?? `pnpm cms:block-adapter ${block.name}`}</code>
131
+ </aside>
132
+ );
133
+ }
58
134
 
59
135
  if (CustomBlockRenderer) {
60
136
  return (
61
- <CustomBlockRenderer block={block} contract={contract}>
137
+ <>
138
+ {compatibilityIssue ? (
139
+ <aside data-flowpress-compatibility={compatibilityIssue.code} data-block-name={block.name} role="status">
140
+ <strong>{compatibilityIssue.message}</strong>
141
+ <code>{compatibilityIssue.command}</code>
142
+ </aside>
143
+ ) : null}
144
+ <CustomBlockRenderer
145
+ block={block}
146
+ contract={contract}
147
+ composition={composition}
148
+ adapters={adapterDefinitions ?? {}}
149
+ htmlProps={htmlAttributes}
150
+ styleflowProps={semantic}
151
+ parentBlockName={parentBlockName}
152
+ context={{
153
+ ...(parentBlockName ? { parentBlockName } : {}),
154
+ depth,
155
+ production
156
+ }}
157
+ >
62
158
  <Astro.self
63
159
  blocks={children}
64
160
  contract={contract}
65
161
  blockRenderers={blockRenderers}
66
162
  iconRenderer={IconRenderer}
163
+ adapterDefinitions={adapterDefinitions}
164
+ composition={composition}
165
+ parentBlockName={block.name}
166
+ production={production}
167
+ depth={depth + 1}
168
+ requireRegisteredAdapters={requireRegisteredAdapters}
67
169
  />
68
170
  </CustomBlockRenderer>
171
+ </>
69
172
  );
70
173
  }
71
174
 
@@ -77,6 +180,7 @@ function youtubeEmbedUrl(value: unknown): string | null {
77
180
  contract={contract}
78
181
  blockRenderers={blockRenderers}
79
182
  iconRenderer={IconRenderer}
183
+ {...nestedRendererProps(block.name)}
80
184
  />
81
185
  </HeroBlock>
82
186
  );
@@ -90,6 +194,7 @@ function youtubeEmbedUrl(value: unknown): string | null {
90
194
  contract={contract}
91
195
  blockRenderers={blockRenderers}
92
196
  iconRenderer={IconRenderer}
197
+ {...nestedRendererProps(block.name)}
93
198
  />
94
199
  </GridBlock>
95
200
  );
@@ -103,11 +208,48 @@ function youtubeEmbedUrl(value: unknown): string | null {
103
208
  contract={contract}
104
209
  blockRenderers={blockRenderers}
105
210
  iconRenderer={IconRenderer}
211
+ {...nestedRendererProps(block.name)}
106
212
  />
107
213
  </ColumnBlock>
108
214
  );
109
215
  }
110
216
 
217
+ if (name === "carousel") {
218
+ const invalidChildNames = children
219
+ .filter((child) => child.name !== "sf/carousel-item")
220
+ .map((child) => child.name);
221
+ return (
222
+ <CarouselBlock
223
+ block={block}
224
+ contract={contract}
225
+ validChildren={invalidChildNames.length === 0}
226
+ invalidChildNames={invalidChildNames}
227
+ >
228
+ <Astro.self
229
+ blocks={children}
230
+ contract={contract}
231
+ blockRenderers={blockRenderers}
232
+ iconRenderer={IconRenderer}
233
+ {...nestedRendererProps(block.name)}
234
+ />
235
+ </CarouselBlock>
236
+ );
237
+ }
238
+
239
+ if (name === "carousel-item") {
240
+ return (
241
+ <CarouselItemBlock block={block} contract={contract}>
242
+ <Astro.self
243
+ blocks={children}
244
+ contract={contract}
245
+ blockRenderers={blockRenderers}
246
+ iconRenderer={IconRenderer}
247
+ {...nestedRendererProps(block.name)}
248
+ />
249
+ </CarouselItemBlock>
250
+ );
251
+ }
252
+
111
253
  if (name === "number-spinner") {
112
254
  return <NumberSpinnerBlock block={block} contract={contract} />;
113
255
  }
@@ -129,13 +271,14 @@ function youtubeEmbedUrl(value: unknown): string | null {
129
271
  contract={contract}
130
272
  blockRenderers={blockRenderers}
131
273
  iconRenderer={IconRenderer}
274
+ {...nestedRendererProps(block.name)}
132
275
  />
133
276
  </TextBlock>
134
277
  );
135
278
  }
136
279
 
137
280
  if (name === "button") {
138
- const href = linkValue(block, "href") || "#";
281
+ const href = linkValue(block, "href");
139
282
  const link = objectValue(block, "href");
140
283
  const opensInNewTab = Boolean(link.opensInNewTab);
141
284
  const icon = String(block.attributes.icon ?? "").trim();
@@ -149,19 +292,32 @@ function youtubeEmbedUrl(value: unknown): string | null {
149
292
  const buttonIcon = IconRenderer && icon ? (
150
293
  <IconRenderer name={icon} display="inline" size={4} />
151
294
  ) : null;
152
- return (
153
- <S
154
- as="a"
295
+ const buttonContent = (
296
+ <>
297
+ {iconPosition === "before" ? buttonIcon : null}
298
+ {textValue(block, "label")}
299
+ {iconPosition === "after" ? buttonIcon : null}
300
+ </>
301
+ );
302
+ return href ? (
303
+ <S.a
155
304
  href={href}
156
305
  target={opensInNewTab ? "_blank" : undefined}
157
306
  rel={opensInNewTab ? "noopener noreferrer" : undefined}
158
307
  {...buttonSemantic}
159
308
  {...htmlAttributes}
160
309
  >
161
- {iconPosition === "before" ? buttonIcon : null}
162
- {textValue(block, "label")}
163
- {iconPosition === "after" ? buttonIcon : null}
164
- </S>
310
+ {buttonContent}
311
+ </S.a>
312
+ ) : (
313
+ <S.span
314
+ aria-disabled="true"
315
+ data-flowpress-link-unresolved
316
+ {...buttonSemantic}
317
+ {...htmlAttributes}
318
+ >
319
+ {buttonContent}
320
+ </S.span>
165
321
  );
166
322
  }
167
323
 
@@ -208,7 +364,7 @@ function youtubeEmbedUrl(value: unknown): string | null {
208
364
  ? block.attributes.images as Array<Record<string, unknown>>
209
365
  : [];
210
366
  return (
211
- <S as="div" {...semantic} {...htmlAttributes}>
367
+ <S.div {...semantic} {...htmlAttributes}>
212
368
  {images.map((image) => image.url ? (
213
369
  <img
214
370
  src={String(image.url)}
@@ -217,7 +373,7 @@ function youtubeEmbedUrl(value: unknown): string | null {
217
373
  decoding="async"
218
374
  />
219
375
  ) : null)}
220
- </S>
376
+ </S.div>
221
377
  );
222
378
  }
223
379
 
@@ -225,7 +381,7 @@ function youtubeEmbedUrl(value: unknown): string | null {
225
381
  const video = objectValue(block, "video");
226
382
  const poster = objectValue(block, "poster");
227
383
  return video.url ? (
228
- <S as="div" {...semantic} {...htmlAttributes}>
384
+ <S.div {...semantic} {...htmlAttributes}>
229
385
  <video
230
386
  src={String(video.url)}
231
387
  poster={poster.url ? String(poster.url) : undefined}
@@ -234,14 +390,14 @@ function youtubeEmbedUrl(value: unknown): string | null {
234
390
  muted={Boolean(block.attributes.autoplay)}
235
391
  playsinline
236
392
  />
237
- </S>
393
+ </S.div>
238
394
  ) : null;
239
395
  }
240
396
 
241
397
  if (name === "video-youtube") {
242
398
  const embed = youtubeEmbedUrl(block.attributes.url);
243
399
  return embed ? (
244
- <S as="div" {...semantic} {...htmlAttributes}>
400
+ <S.div {...semantic} {...htmlAttributes}>
245
401
  <iframe
246
402
  src={embed}
247
403
  title={String(block.attributes.title ?? "YouTube video")}
@@ -249,15 +405,15 @@ function youtubeEmbedUrl(value: unknown): string | null {
249
405
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
250
406
  allowfullscreen
251
407
  />
252
- </S>
408
+ </S.div>
253
409
  ) : null;
254
410
  }
255
411
 
256
412
  if (name === "chip") {
257
413
  return (
258
- <S as="span" {...semantic} {...htmlAttributes}>
414
+ <S.span {...semantic} {...htmlAttributes}>
259
415
  {textValue(block, "label")}
260
- </S>
416
+ </S.span>
261
417
  );
262
418
  }
263
419
 
@@ -265,14 +421,14 @@ function youtubeEmbedUrl(value: unknown): string | null {
265
421
  const media = objectValue(block, "image");
266
422
  const href = linkValue(block);
267
423
  return (
268
- <S as="article" {...semantic} {...htmlAttributes}>
424
+ <S.article {...semantic} {...htmlAttributes}>
269
425
  {media.url && (
270
426
  <img src={String(media.url)} alt={String(media.alt ?? "")} loading="lazy" />
271
427
  )}
272
- {block.attributes.title && <S as="h3">{String(block.attributes.title)}</S>}
273
- {block.attributes.text && <S as="p">{String(block.attributes.text)}</S>}
274
- {href && <S as="a" href={href}>Scopri</S>}
275
- </S>
428
+ {block.attributes.title && <S.h3>{String(block.attributes.title)}</S.h3>}
429
+ {block.attributes.text && <S.p>{String(block.attributes.text)}</S.p>}
430
+ {href && <S.a href={href}>Scopri</S.a>}
431
+ </S.article>
276
432
  );
277
433
  }
278
434
 
@@ -284,22 +440,23 @@ function youtubeEmbedUrl(value: unknown): string | null {
284
440
  ? block.attributes.resolvedBlocks as FlowpressBlock[][]
285
441
  : [];
286
442
  return (
287
- <S as="ul" {...semantic} {...htmlAttributes}>
443
+ <S.ul {...semantic} {...htmlAttributes}>
288
444
  {items.map((item, index) => (
289
- <S as="li">
445
+ <S.li>
290
446
  {resolvedBlocks[index]?.length ? (
291
447
  <Astro.self
292
448
  blocks={resolvedBlocks[index]}
293
449
  contract={contract}
294
450
  blockRenderers={blockRenderers}
295
451
  iconRenderer={IconRenderer}
452
+ {...nestedRendererProps(block.name)}
296
453
  />
297
454
  ) : (
298
455
  <ListItemFallback item={item} />
299
456
  )}
300
- </S>
457
+ </S.li>
301
458
  ))}
302
- </S>
459
+ </S.ul>
303
460
  );
304
461
  }
305
462
 
@@ -311,6 +468,7 @@ function youtubeEmbedUrl(value: unknown): string | null {
311
468
  contract={contract}
312
469
  blockRenderers={blockRenderers}
313
470
  iconRenderer={IconRenderer}
471
+ {...nestedRendererProps(block.name)}
314
472
  />
315
473
  </div>
316
474
  ) : (
@@ -319,6 +477,7 @@ function youtubeEmbedUrl(value: unknown): string | null {
319
477
  contract={contract}
320
478
  blockRenderers={blockRenderers}
321
479
  iconRenderer={IconRenderer}
480
+ {...nestedRendererProps(block.name)}
322
481
  />
323
482
  );
324
483
  }
@@ -329,7 +488,7 @@ function youtubeEmbedUrl(value: unknown): string | null {
329
488
 
330
489
  if (name === "accordion-item") {
331
490
  return (
332
- <S as="div" {...semantic} {...htmlAttributes}>
491
+ <S.div {...semantic} {...htmlAttributes}>
333
492
  <details>
334
493
  <summary>{String(block.attributes.title ?? "")}</summary>
335
494
  <Astro.self
@@ -337,9 +496,10 @@ function youtubeEmbedUrl(value: unknown): string | null {
337
496
  contract={contract}
338
497
  blockRenderers={blockRenderers}
339
498
  iconRenderer={IconRenderer}
499
+ {...nestedRendererProps(block.name)}
340
500
  />
341
501
  </details>
342
- </S>
502
+ </S.div>
343
503
  );
344
504
  }
345
505
 
@@ -360,42 +520,42 @@ function youtubeEmbedUrl(value: unknown): string | null {
360
520
  + `?key=${encodeURIComponent(googleMapsApiKey)}`
361
521
  + `&q=${encodeURIComponent(query)}`;
362
522
  return (
363
- <S as="div" {...semantic} {...htmlAttributes}>
523
+ <S.div {...semantic} {...htmlAttributes}>
364
524
  <iframe
365
525
  src={source}
366
526
  title={address || "Map"}
367
527
  loading="lazy"
368
528
  referrerpolicy="no-referrer-when-downgrade"
369
529
  />
370
- </S>
530
+ </S.div>
371
531
  );
372
532
  }
373
533
  return query ? (
374
- <S as="div" {...semantic} {...htmlAttributes}>
375
- <S
376
- as="a"
534
+ <S.div {...semantic} {...htmlAttributes}>
535
+ <S.a
377
536
  href={`https://www.openstreetmap.org/search?query=${encodeURIComponent(query)}`}
378
537
  rel="noreferrer"
379
538
  >
380
539
  {address || query}
381
- </S>
382
- </S>
540
+ </S.a>
541
+ </S.div>
383
542
  ) : null;
384
543
  }
385
544
 
386
- const layoutTag = name === "section" ? "section" : "div";
387
- return (
388
- <S as={layoutTag} {...semantic} {...htmlAttributes}>
389
- {block.innerHtml && children.length === 0
390
- ? <S as="div" set:html={block.innerHtml} />
391
- : (
392
- <Astro.self
393
- blocks={children}
394
- contract={contract}
395
- blockRenderers={blockRenderers}
396
- iconRenderer={IconRenderer}
397
- />
398
- )}
399
- </S>
545
+ const fallbackContent = block.innerHtml && children.length === 0
546
+ ? <S.div set:html={block.innerHtml} />
547
+ : (
548
+ <Astro.self
549
+ blocks={children}
550
+ contract={contract}
551
+ blockRenderers={blockRenderers}
552
+ iconRenderer={IconRenderer}
553
+ {...nestedRendererProps(block.name)}
554
+ />
555
+ );
556
+ return name === "section" ? (
557
+ <S.section {...semantic} {...htmlAttributes}>{fallbackContent}</S.section>
558
+ ) : (
559
+ <S.div {...semantic} {...htmlAttributes}>{fallbackContent}</S.div>
400
560
  );
401
561
  })}
@@ -0,0 +1,24 @@
1
+ ---
2
+ import { styleflowThemeBootstrapScript } from "../theme-runtime.js";
3
+
4
+ interface Props {
5
+ themes: string[];
6
+ defaultTheme: string;
7
+ }
8
+
9
+ const { themes, defaultTheme } = Astro.props;
10
+ const bootstrap = styleflowThemeBootstrapScript(themes, defaultTheme);
11
+ ---
12
+
13
+ <script is:inline set:html={bootstrap}></script>
14
+ <script>
15
+ import { initializeStyleflowThemeRuntime } from "../theme-runtime.js";
16
+
17
+ const initialize = () => initializeStyleflowThemeRuntime();
18
+ if (document.readyState === "loading") {
19
+ document.addEventListener("DOMContentLoaded", initialize, { once: true });
20
+ } else {
21
+ initialize();
22
+ }
23
+ document.addEventListener("astro:page-load", initialize);
24
+ </script>