@levo-so/blocks 0.1.112 → 0.1.113

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@levo-so/blocks",
3
3
  "description": "Set of blocks for Levo studio",
4
- "version": "0.1.112",
4
+ "version": "0.1.113",
5
5
  "author": "Levo Engineering <devs@theinternetfolks.com>",
6
6
  "dependencies": {
7
7
  "dayjs": "1.11.13",
@@ -14,10 +14,10 @@
14
14
  "@types/react": "19.2.14",
15
15
  "@types/react-dom": "19.2.3",
16
16
  "typescript": "5.9.3",
17
- "@levo-so/core": "0.1.111",
18
- "@levo-so/studio": "0.1.112",
19
- "@levo/ts-config": "0.0.0",
20
- "@levo-so/react": "0.1.111"
17
+ "@levo-so/core": "0.1.113",
18
+ "@levo-so/studio": "0.1.113",
19
+ "@levo-so/react": "0.1.113",
20
+ "@levo/ts-config": "0.0.0"
21
21
  },
22
22
  "exports": {
23
23
  ".": "./src/index.ts"
@@ -30,9 +30,9 @@
30
30
  "peerDependencies": {
31
31
  "react": ">=18",
32
32
  "react-dom": ">=18",
33
- "@levo-so/react": "0.1.111",
34
- "@levo-so/core": "0.1.111",
35
- "@levo-so/studio": "0.1.112"
33
+ "@levo-so/core": "0.1.113",
34
+ "@levo-so/studio": "0.1.113",
35
+ "@levo-so/react": "0.1.113"
36
36
  },
37
37
  "publishConfig": {
38
38
  "access": "public"
@@ -33,6 +33,9 @@ interface IBlogMeta {
33
33
  }
34
34
 
35
35
  const getAuthorsText = (authors: any[]): string => {
36
+ // `authors` is variable-bound and can resolve to null / a non-array at runtime —
37
+ // an unguarded `.map` here throws and blanks the whole studio canvas.
38
+ if (!Array.isArray(authors) || authors.length === 0) return "Anonymous";
36
39
  const names = authors.map((a) => [a?.first_name].filter(Boolean).join(" ") || "Anonymous");
37
40
 
38
41
  if (names.length === 2) return `${names[0]} and ${names[1]}`;
@@ -24,6 +24,9 @@ import { ClientOnly } from "../event/ClientOnly";
24
24
  import type { IBlogListing2Content } from "./blog-listing-2.schema";
25
25
 
26
26
  const getAuthorsText = (authors: any[]): string => {
27
+ // `authors` is variable-bound and can resolve to null / a non-array at runtime —
28
+ // an unguarded `.map` here throws and blanks the whole studio canvas.
29
+ if (!Array.isArray(authors) || authors.length === 0) return "Anonymous";
27
30
  const names = authors.map((a) => [a?.first_name].filter(Boolean).join(" ") || "Anonymous");
28
31
 
29
32
  if (names.length === 2) return `${names[0]} and ${names[1]}`;
@@ -33,6 +33,9 @@ interface IBlogMeta {
33
33
  }
34
34
 
35
35
  const getAuthorsText = (authors: any[]): string => {
36
+ // `authors` is variable-bound and can resolve to null / a non-array at runtime —
37
+ // an unguarded `.map` here throws and blanks the whole studio canvas.
38
+ if (!Array.isArray(authors) || authors.length === 0) return "Anonymous";
36
39
  const names = authors.map((a) => [a?.first_name].filter(Boolean).join(" ") || "Anonymous");
37
40
 
38
41
  if (names.length === 2) return `${names[0]} and ${names[1]}`;
@@ -9,6 +9,7 @@ import {
9
9
  Typography,
10
10
  } from "@levo-so/studio";
11
11
 
12
+ import { toArray } from "../../utils/array";
12
13
  import type { ICards9Content } from "./cards-9.schema";
13
14
 
14
15
  const Cards9: React.FC<ILevoBlockBaseProps<ICards9Content>> = ({ content }) => (
@@ -20,7 +21,7 @@ const Cards9: React.FC<ILevoBlockBaseProps<ICards9Content>> = ({ content }) => (
20
21
  <Typography elementKey="description" />
21
22
  </Box>
22
23
  <Box elementKey="header_ctas_levoGroup" data-levo_group>
23
- {(content?.header_ctas ?? []).map((_: any, index: number) => (
24
+ {toArray(content?.header_ctas).map((_: any, index: number) => (
24
25
  <Button
25
26
  key={`header_ctas.${index}.header_cta`}
26
27
  elementKey={`header_ctas.${index}.header_cta`}
@@ -31,7 +32,7 @@ const Cards9: React.FC<ILevoBlockBaseProps<ICards9Content>> = ({ content }) => (
31
32
  </Box>
32
33
 
33
34
  <Box elementKey="cards_levoGroup" data-levo_group>
34
- {(content.cards ?? []).map((_, index) => (
35
+ {toArray(content?.cards).map((_, index) => (
35
36
  <Box
36
37
  elementKey={`cards.${index}.card_wrapper`}
37
38
  key={`cards-9-card-${index}`}
@@ -50,7 +51,7 @@ const Cards9: React.FC<ILevoBlockBaseProps<ICards9Content>> = ({ content }) => (
50
51
  <Box elementKey={`cards.${index}.text_container`}>
51
52
  <Typography elementKey={`cards.${index}.card_title`} />
52
53
  <Box elementKey={`cards.${index}.ctas_levoGroup`} data-levo_group>
53
- {(content?.cards?.[index]?.ctas ?? []).map((_: any, ctaIndex: number) => (
54
+ {toArray(content?.cards?.[index]?.ctas).map((_: any, ctaIndex: number) => (
54
55
  <Button
55
56
  key={`cards.${index}.ctas.${ctaIndex}.cta`}
56
57
  elementKey={`cards.${index}.ctas.${ctaIndex}.cta`}
@@ -9,6 +9,7 @@ import {
9
9
  Typography,
10
10
  } from "@levo-so/studio";
11
11
 
12
+ import { toArray } from "../../utils/array";
12
13
  import type { ICta2Content } from "./cta-2.schema";
13
14
 
14
15
  const Cta2: React.FC<ILevoBlockBaseProps<ICta2Content>> = ({ content }) => (
@@ -19,7 +20,7 @@ const Cta2: React.FC<ILevoBlockBaseProps<ICta2Content>> = ({ content }) => (
19
20
  <Heading elementKey="title" />
20
21
  <Typography elementKey="description" />
21
22
  <Box elementKey="buttons_levoGroup" data-levo_group>
22
- {(content?.buttons ?? []).map((_: any, index: number) => (
23
+ {toArray(content?.buttons).map((_: any, index: number) => (
23
24
  <Button key={index} data-levo_group_item elementKey={`buttons.${index}.text`} />
24
25
  ))}
25
26
  </Box>
@@ -18,6 +18,7 @@ import {
18
18
  import type React from "react";
19
19
  import { useState } from "react";
20
20
 
21
+ import { toArray } from "../../utils/array";
21
22
  import type { IFaq2Content } from "./faq-2.schema";
22
23
 
23
24
  const Faq2: React.FC<ILevoBlockBaseProps<IFaq2Content>> = ({ content }) => {
@@ -52,7 +53,7 @@ const Faq2: React.FC<ILevoBlockBaseProps<IFaq2Content>> = ({ content }) => {
52
53
  defaultValue={`item-0`}
53
54
  onValueChange={handleValueChange}
54
55
  >
55
- {(content.faqs ?? []).map((_, index) => (
56
+ {toArray(content?.faqs).map((_, index) => (
56
57
  <AccordionItem
57
58
  value={`item-${index}`}
58
59
  key={index}
@@ -72,7 +73,7 @@ const Faq2: React.FC<ILevoBlockBaseProps<IFaq2Content>> = ({ content }) => {
72
73
  ))}
73
74
  </AccordionRoot>
74
75
  <Box elementKey="ctas_levoGroup" data-levo_group>
75
- {(content?.ctas ?? []).map((_: any, index: number) => (
76
+ {toArray(content?.ctas).map((_: any, index: number) => (
76
77
  <Button
77
78
  key={`ctas.${index}.cta`}
78
79
  elementKey={`ctas.${index}.cta`}
@@ -9,6 +9,7 @@ import {
9
9
  Typography,
10
10
  } from "@levo-so/studio";
11
11
 
12
+ import { toArray } from "../../utils/array";
12
13
  import type { IFeatures3Content } from "./features-3.schema";
13
14
 
14
15
  const Features3: React.FC<ILevoBlockBaseProps<IFeatures3Content>> = ({ content }) => (
@@ -21,7 +22,7 @@ const Features3: React.FC<ILevoBlockBaseProps<IFeatures3Content>> = ({ content }
21
22
  <Typography elementKey="description" />
22
23
  <Box elementKey="cta_wrapper">
23
24
  <Box elementKey="cta_buttons_levoGroup" data-levo_group>
24
- {(content?.cta_buttons ?? []).map((_: any, index: number) => (
25
+ {toArray(content?.cta_buttons).map((_: any, index: number) => (
25
26
  <Button
26
27
  key={`cta_buttons.${index}.cta_button`}
27
28
  elementKey={`cta_buttons.${index}.cta_button`}
@@ -32,7 +33,7 @@ const Features3: React.FC<ILevoBlockBaseProps<IFeatures3Content>> = ({ content }
32
33
  </Box>
33
34
  </Box>
34
35
  <Box data-levo_group elementKey="services_levoGroup">
35
- {(content.services ?? []).map((_, index) => (
36
+ {toArray(content?.services).map((_, index) => (
36
37
  <Box data-levo_group_item elementKey={`services.${index}.card`} key={index}>
37
38
  <Image
38
39
  elementKey={`services.${index}.image`}
@@ -46,7 +47,7 @@ const Features3: React.FC<ILevoBlockBaseProps<IFeatures3Content>> = ({ content }
46
47
  <Heading elementKey={`services.${index}.title`} />
47
48
  <Typography elementKey={`services.${index}.description`} />
48
49
  <Box elementKey={`services.${index}.ctas_levoGroup`} data-levo_group>
49
- {(content?.services?.[index]?.ctas ?? []).map((_, ctaIndex) => (
50
+ {toArray(content?.services?.[index]?.ctas).map((_, ctaIndex) => (
50
51
  <Button
51
52
  key={`services.${index}.ctas.${ctaIndex}.cta`}
52
53
  elementKey={`services.${index}.ctas.${ctaIndex}.cta`}
@@ -295,6 +295,8 @@ export const FilterListing1: IBlock = {
295
295
  max_characters: 30,
296
296
  },
297
297
  },
298
+ { key: "search-container", label: "Search Container", field_interface: "BoxWidget" },
299
+ { key: "search-input-box", label: "Search Input Box", field_interface: "BoxWidget" },
298
300
  {
299
301
  key: "listing_wrapper",
300
302
  label: "Listing Wrapper",
@@ -520,6 +522,7 @@ export const FilterListing1: IBlock = {
520
522
  },
521
523
  },
522
524
  header: { flex: "1" },
525
+ "search-container": { width: "full" },
523
526
  listing_wrapper: {
524
527
  display: "flex",
525
528
  "column-gap": "2xl",
@@ -727,6 +730,7 @@ export const FilterListing1: IBlock = {
727
730
  "align-items": "center",
728
731
  },
729
732
  header: { flex: "1" },
733
+ "search-container": { width: "full" },
730
734
  listing_wrapper: {
731
735
  display: "flex",
732
736
  "flex-direction": "column",
@@ -854,12 +858,30 @@ export const FilterListing1: IBlock = {
854
858
  selectedVariants: { Heading_Level: "H2" },
855
859
  },
856
860
  listing: [
857
- { title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } }, cta: { selectedVariants: { Button_Variants: "Link" } } },
858
- { title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } }, cta: { selectedVariants: { Button_Variants: "Link" } } },
859
- { title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } }, cta: { selectedVariants: { Button_Variants: "Link" } } },
860
- { title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } }, cta: { selectedVariants: { Button_Variants: "Link" } } },
861
- { title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } }, cta: { selectedVariants: { Button_Variants: "Link" } } },
862
- { title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } }, cta: { selectedVariants: { Button_Variants: "Link" } } },
861
+ {
862
+ title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
863
+ cta: { selectedVariants: { Button_Variants: "Link" } },
864
+ },
865
+ {
866
+ title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
867
+ cta: { selectedVariants: { Button_Variants: "Link" } },
868
+ },
869
+ {
870
+ title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
871
+ cta: { selectedVariants: { Button_Variants: "Link" } },
872
+ },
873
+ {
874
+ title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
875
+ cta: { selectedVariants: { Button_Variants: "Link" } },
876
+ },
877
+ {
878
+ title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
879
+ cta: { selectedVariants: { Button_Variants: "Link" } },
880
+ },
881
+ {
882
+ title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
883
+ cta: { selectedVariants: { Button_Variants: "Link" } },
884
+ },
863
885
  ],
864
886
  },
865
887
  },
@@ -887,6 +909,7 @@ export const FilterListing1: IBlock = {
887
909
  "text-align": "center",
888
910
  },
889
911
  header: {},
912
+ "search-container": { width: "full" },
890
913
  listing_wrapper: {
891
914
  display: "flex",
892
915
  "flex-direction": "column",
@@ -987,12 +1010,30 @@ export const FilterListing1: IBlock = {
987
1010
  selectedVariants: { Heading_Level: "H2" },
988
1011
  },
989
1012
  listing: [
990
- { title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } }, cta: { selectedVariants: { Button_Variants: "Link" } } },
991
- { title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } }, cta: { selectedVariants: { Button_Variants: "Link" } } },
992
- { title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } }, cta: { selectedVariants: { Button_Variants: "Link" } } },
993
- { title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } }, cta: { selectedVariants: { Button_Variants: "Link" } } },
994
- { title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } }, cta: { selectedVariants: { Button_Variants: "Link" } } },
995
- { title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } }, cta: { selectedVariants: { Button_Variants: "Link" } } },
1013
+ {
1014
+ title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
1015
+ cta: { selectedVariants: { Button_Variants: "Link" } },
1016
+ },
1017
+ {
1018
+ title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
1019
+ cta: { selectedVariants: { Button_Variants: "Link" } },
1020
+ },
1021
+ {
1022
+ title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
1023
+ cta: { selectedVariants: { Button_Variants: "Link" } },
1024
+ },
1025
+ {
1026
+ title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
1027
+ cta: { selectedVariants: { Button_Variants: "Link" } },
1028
+ },
1029
+ {
1030
+ title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
1031
+ cta: { selectedVariants: { Button_Variants: "Link" } },
1032
+ },
1033
+ {
1034
+ title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
1035
+ cta: { selectedVariants: { Button_Variants: "Link" } },
1036
+ },
996
1037
  ],
997
1038
  },
998
1039
  },
@@ -1022,6 +1063,7 @@ export const FilterListing1: IBlock = {
1022
1063
  "align-items": "center",
1023
1064
  },
1024
1065
  header: {},
1066
+ "search-container": { width: "full" },
1025
1067
  listing_wrapper: {
1026
1068
  display: "flex",
1027
1069
  "flex-direction": "column",
@@ -1123,12 +1165,30 @@ export const FilterListing1: IBlock = {
1123
1165
  selectedVariants: { Heading_Level: "H2" },
1124
1166
  },
1125
1167
  listing: [
1126
- { title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } }, cta: { selectedVariants: { Button_Variants: "Link" } } },
1127
- { title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } }, cta: { selectedVariants: { Button_Variants: "Link" } } },
1128
- { title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } }, cta: { selectedVariants: { Button_Variants: "Link" } } },
1129
- { title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } }, cta: { selectedVariants: { Button_Variants: "Link" } } },
1130
- { title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } }, cta: { selectedVariants: { Button_Variants: "Link" } } },
1131
- { title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } }, cta: { selectedVariants: { Button_Variants: "Link" } } },
1168
+ {
1169
+ title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
1170
+ cta: { selectedVariants: { Button_Variants: "Link" } },
1171
+ },
1172
+ {
1173
+ title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
1174
+ cta: { selectedVariants: { Button_Variants: "Link" } },
1175
+ },
1176
+ {
1177
+ title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
1178
+ cta: { selectedVariants: { Button_Variants: "Link" } },
1179
+ },
1180
+ {
1181
+ title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
1182
+ cta: { selectedVariants: { Button_Variants: "Link" } },
1183
+ },
1184
+ {
1185
+ title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
1186
+ cta: { selectedVariants: { Button_Variants: "Link" } },
1187
+ },
1188
+ {
1189
+ title: { selectedVariants: { Heading_Level: "H5" }, heading: { level: 5 } },
1190
+ cta: { selectedVariants: { Button_Variants: "Link" } },
1191
+ },
1132
1192
  ],
1133
1193
  },
1134
1194
  },
@@ -1157,6 +1217,7 @@ export const FilterListing1: IBlock = {
1157
1217
  "align-items": "center",
1158
1218
  },
1159
1219
  header: {},
1220
+ "search-container": { width: "full" },
1160
1221
  listing_wrapper: {
1161
1222
  display: "flex",
1162
1223
  "flex-direction": "column",
@@ -6,6 +6,7 @@ import {
6
6
  Box,
7
7
  Button,
8
8
  Container,
9
+ FormInput,
9
10
  Heading,
10
11
  Icon,
11
12
  type ILevoBlockBaseProps,
@@ -15,12 +16,23 @@ import {
15
16
  Typography,
16
17
  useContentEngine,
17
18
  } from "@levo-so/studio";
19
+ import { Search } from "lucide-react";
20
+ import { useState } from "react";
21
+ import { useDebouncedCallback } from "use-debounce";
18
22
 
19
23
  import type { IFilterListing1Content } from "./filter-listing-1.schema";
20
24
 
21
25
  const FilterListing1: React.FC<ILevoBlockBaseProps<IFilterListing1Content>> = ({ config }) => {
22
26
  const { data, query, setQuery, meta } = useContentEngine("listing");
23
27
 
28
+ const [search, setSearch] = useState("");
29
+
30
+ // The "listing" collection endpoint full-text searches on the top-level `search`
31
+ // key (see IntegrationFilters); resetting page keeps results on the first page.
32
+ const setDebouncedSearchKey = useDebouncedCallback((v: string) => {
33
+ setQuery({ search: v || undefined, page: 1 });
34
+ }, 1000);
35
+
24
36
  const handleNext = () => setQuery({ page: (query?.page || 0) + 1 });
25
37
  const handlePrevious = () => setQuery({ page: (query?.page || 0) - 1 });
26
38
  return (
@@ -29,6 +41,37 @@ const FilterListing1: React.FC<ILevoBlockBaseProps<IFilterListing1Content>> = ({
29
41
  <Box elementKey="header_wrapper">
30
42
  <Heading elementKey="header" />
31
43
  </Box>
44
+ <Box elementKey="search-container">
45
+ <Box elementKey="search-input-box">
46
+ <FormInput
47
+ name="search"
48
+ placeholder="Search..."
49
+ value={search}
50
+ onChange={(e) => {
51
+ setSearch(e?.target?.value || "");
52
+ setDebouncedSearchKey(e?.target?.value || "");
53
+ }}
54
+ leftIcon={<Search size={22} />}
55
+ inputProps={{
56
+ style: {
57
+ padding: "1rem",
58
+ paddingLeft: "3rem",
59
+ height: "unset",
60
+ fontSize: "1.125rem",
61
+ color: "var(--color-text-1)",
62
+ borderColor: "var(--color-border)",
63
+ borderRadius: "99px",
64
+ },
65
+ }}
66
+ leftIconBoxProps={{
67
+ style: {
68
+ width: "unset",
69
+ paddingLeft: "1.25rem",
70
+ },
71
+ }}
72
+ />
73
+ </Box>
74
+ </Box>
32
75
  <Box elementKey="listing_wrapper">
33
76
  {config?.listing_levoGroup?.filters?.schema && (
34
77
  <AccordionRoot type="single" elementKey="filters_accordion" defaultValue="filters">
@@ -10,6 +10,7 @@ import {
10
10
  } from "@levo-so/studio";
11
11
  import React from "react";
12
12
 
13
+ import { toArray } from "../../utils/array";
13
14
  import type { IFooter5Content } from "./footer-5.schema";
14
15
 
15
16
  const Footer5: React.FC<ILevoBlockBaseProps<IFooter5Content>> = ({ content }) => (
@@ -24,7 +25,7 @@ const Footer5: React.FC<ILevoBlockBaseProps<IFooter5Content>> = ({ content }) =>
24
25
 
25
26
  {/* Links */}
26
27
  <Box data-levo_group elementKey="links_levoGroup">
27
- {(content?.links ?? []).map((_, index) => (
28
+ {toArray(content?.links).map((_, index) => (
28
29
  <Box
29
30
  elementKey={`links.${index}.links`}
30
31
  key={`footer-5-links-${index}`}
@@ -32,7 +33,7 @@ const Footer5: React.FC<ILevoBlockBaseProps<IFooter5Content>> = ({ content }) =>
32
33
  >
33
34
  <Heading elementKey={`links.${index}.category`} />
34
35
  <Box elementKey={`links.${index}.links_levoGroup`} data-levo_group>
35
- {(content?.links?.[index]?.links ?? []).map((_, subIndex) => (
36
+ {toArray(content?.links?.[index]?.links).map((_, subIndex) => (
36
37
  <Box
37
38
  elementKey={`links.${index}.links.${subIndex}.iconWrapper`}
38
39
  key={`footer-5-links-${index}-${subIndex}`}
@@ -57,7 +58,7 @@ const Footer5: React.FC<ILevoBlockBaseProps<IFooter5Content>> = ({ content }) =>
57
58
 
58
59
  {/* Right Area */}
59
60
  <Box elementKey="footerBottomRightArea_levoGroup" data-levo_group>
60
- {(content?.footerBottomRightArea ?? [])?.map((_: any, index: number) => (
61
+ {toArray(content?.footerBottomRightArea).map((_: any, index: number) => (
61
62
  <React.Fragment key={`footer-4-right-area-${index}`}>
62
63
  <Typography data-levo_group_item elementKey={`footerBottomRightArea.${index}.cta`} />
63
64
  {index < (content?.footerBottomRightArea?.length ?? 0) - 1 && (
@@ -69,7 +70,7 @@ const Footer5: React.FC<ILevoBlockBaseProps<IFooter5Content>> = ({ content }) =>
69
70
 
70
71
  {/* Social Icons */}
71
72
  <Box elementKey="socialIcons_levoGroup" data-levo_group>
72
- {(content?.socialIcons ?? []).map((_, index) => (
73
+ {toArray(content?.socialIcons).map((_, index) => (
73
74
  <Icon
74
75
  key={`footer-5-social-${index}`}
75
76
  data-levo_group_item
@@ -9,6 +9,7 @@ import {
9
9
  Typography,
10
10
  } from "@levo-so/studio";
11
11
 
12
+ import { toArray } from "../../utils/array";
12
13
  import type { IHero1Content } from "./hero-1.schema";
13
14
 
14
15
  const Hero1: React.FC<ILevoBlockBaseProps<IHero1Content>> = ({ content }) => (
@@ -25,7 +26,7 @@ const Hero1: React.FC<ILevoBlockBaseProps<IHero1Content>> = ({ content }) => (
25
26
  <Typography elementKey="description" />
26
27
  </Box>
27
28
  <Box elementKey="hero_ctas_levoGroup" data-levo_group>
28
- {(content?.hero_ctas || [])?.map((_, index) => (
29
+ {toArray(content?.hero_ctas).map((_, index) => (
29
30
  <Button
30
31
  elementKey={`hero_ctas.${index}.button`}
31
32
  key={`hero-1-cta-${index}`}
@@ -35,7 +36,7 @@ const Hero1: React.FC<ILevoBlockBaseProps<IHero1Content>> = ({ content }) => (
35
36
  </Box>
36
37
  </Box>
37
38
  <Box elementKey="hero_images_levoGroup" data-levo_group>
38
- {(content?.hero_images || [])?.map((_, index) => (
39
+ {toArray(content?.hero_images).map((_, index) => (
39
40
  <Media
40
41
  elementKey={`hero_images.${index}.image`}
41
42
  key={`hero-1-image-${index}`}
@@ -17,6 +17,7 @@ import {
17
17
  import { ChevronDown, ChevronRight, Menu, X } from "lucide-react";
18
18
  import React, { useEffect, useState } from "react";
19
19
 
20
+ import { toArray } from "../../utils/array";
20
21
  import type { INavbar3Content } from "./navbar-3.schema";
21
22
 
22
23
  const NestedDropdownItem: React.FC<{
@@ -439,9 +440,9 @@ const MobileNavigationSheet: React.FC<{
439
440
  })
440
441
  : null}
441
442
  </Box>
442
- {content?.["mobile-cta-group"]?.length > 0 && (
443
+ {toArray(content?.["mobile-cta-group"]).length > 0 && (
443
444
  <Box elementKey="mobile-cta-group_levoGroup" data-levo_group>
444
- {content?.["mobile-cta-group"]?.map((_: any, index: number) => (
445
+ {toArray(content?.["mobile-cta-group"]).map((_: any, index: number) => (
445
446
  <Button
446
447
  key={`mobile-cta-group.${index}.cta`}
447
448
  elementKey={`mobile-cta-group.${index}.cta`}
@@ -454,7 +455,7 @@ const MobileNavigationSheet: React.FC<{
454
455
  </Box>
455
456
  )}
456
457
  <Box elementKey="icon_links_levoGroup" data-levo_group>
457
- {((content as any)?.icon_links ?? [])?.map((_: any, index: number) => (
458
+ {toArray((content as any)?.icon_links).map((_: any, index: number) => (
458
459
  <Icon
459
460
  key={`footer-1-icon-${index}`}
460
461
  data-levo_group_item
@@ -517,8 +518,8 @@ const Navbar3: React.FC<ILevoBlockBaseProps<INavbar3Content>> = ({ content }) =>
517
518
  <Box elementKey="desktop-search-container">
518
519
  <GlobalSearch variant="icon" />
519
520
  </Box>
520
- {content?.["desktop-cta-group"]?.length > 0 &&
521
- content?.["desktop-cta-group"]?.map((_: any, index: number) => (
521
+ {toArray(content?.["desktop-cta-group"]).length > 0 &&
522
+ toArray(content?.["desktop-cta-group"]).map((_: any, index: number) => (
522
523
  <Button
523
524
  key={`desktop-cta-group.${index}.cta`}
524
525
  elementKey={`desktop-cta-group.${index}.cta`}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Coerces an unknown value to an array, returning `[]` for anything that isn't one.
3
+ *
4
+ * Block content fields are bound to studio variables that can resolve to a non-array
5
+ * (a string, an object, null) at runtime. `value ?? []` only guards null/undefined, so a
6
+ * truthy non-array still reaches `.map`/`.filter` and throws — blanking the whole studio
7
+ * canvas. Use this anywhere a content array is iterated.
8
+ */
9
+ export const toArray = <T>(value: unknown): T[] => (Array.isArray(value) ? (value as T[]) : []);