@griddo/ax 1.55.13 → 1.56.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.
Files changed (29) hide show
  1. package/package.json +2 -2
  2. package/src/GlobalStore.tsx +3 -0
  3. package/src/components/ConfigPanel/Form/ConnectedField/PageConnectedField/index.tsx +6 -0
  4. package/src/components/Fields/Wysiwyg/config.tsx +0 -1
  5. package/src/components/Fields/Wysiwyg/index.tsx +16 -5
  6. package/src/components/Gallery/GalleryPanel/DetailPanel/index.tsx +110 -99
  7. package/src/components/Gallery/GalleryPanel/GalleryDragAndDrop/index.tsx +75 -55
  8. package/src/components/Gallery/GalleryPanel/index.tsx +14 -8
  9. package/src/components/Gallery/index.tsx +113 -151
  10. package/src/components/Gallery/style.tsx +40 -10
  11. package/src/components/MainWrapper/AppBar/index.tsx +1 -0
  12. package/src/components/Toast/index.tsx +15 -9
  13. package/src/components/Toast/style.tsx +2 -2
  14. package/src/containers/Gallery/actions.tsx +171 -0
  15. package/src/containers/Gallery/constants.tsx +18 -0
  16. package/src/containers/Gallery/index.tsx +7 -0
  17. package/src/containers/Gallery/interfaces.tsx +41 -0
  18. package/src/containers/Gallery/reducer.tsx +78 -0
  19. package/src/containers/PageEditor/actions.tsx +15 -5
  20. package/src/containers/StructuredData/actions.tsx +4 -21
  21. package/src/forms/fields.tsx +19 -6
  22. package/src/guards/error/index.tsx +3 -1
  23. package/src/modules/Content/HeaderMenus/Live/index.tsx +6 -5
  24. package/src/modules/Sites/SitesList/index.tsx +1 -1
  25. package/src/modules/StructuredData/Form/ConnectedField/index.tsx +1 -1
  26. package/src/modules/StructuredData/Form/index.tsx +10 -4
  27. package/src/modules/StructuredData/StructuredDataList/HeaderMenus/Live/index.tsx +1 -1
  28. package/src/types/index.tsx +20 -0
  29. package/src/components/Gallery/store.tsx +0 -186
@@ -1,36 +1,31 @@
1
1
  import React, { useEffect, useState, memo, useRef } from "react";
2
+ import { connect } from "react-redux";
2
3
 
3
- import { getSiteImages, ITEMS_PER_PAGE } from "./store";
4
- import { IImage, ISite } from "@ax/types";
5
- import { Icon, Loader, Tabs, SearchField, EmptyState } from "@ax/components";
4
+ import { galleryActions } from "@ax/containers/Gallery";
5
+ import { IData, IIsLoading } from "@ax/containers/Gallery/reducer";
6
+ import { IGetSiteImages, IImage, IRootState, ISite } from "@ax/types";
7
+ import { Icon, Loader, Tabs, SearchField, EmptyState, ErrorToast, Notification } from "@ax/components";
6
8
 
7
- import Orientation from "./GalleryFilters/Orientation"
8
- import SortBy from "./GalleryFilters/SortBy"
9
+ import Orientation from "./GalleryFilters/Orientation";
10
+ import SortBy from "./GalleryFilters/SortBy";
9
11
  import GalleryPanel from "./GalleryPanel";
10
12
  import * as S from "./style";
11
13
  import { useFilterQuery, useSortedListStatus } from "./hooks";
12
- import { getSortedListStatus } from "./utils"
14
+ import { getSortedListStatus } from "./utils";
13
15
 
14
- const Gallery = (props: IGalleryProps): JSX.Element => {
15
- const { getImageSelected, toggleModal, site } = props;
16
+ const itemsPerPage = 50;
17
+ const firstPage = 1;
18
+
19
+ const Gallery = (props: IProps): JSX.Element => {
20
+ const { data, isLoading, getSiteImages, selectImage, getImageSelected, toggleModal, site } = props;
16
21
 
17
22
  const tabs = [];
18
23
  if (site) tabs.unshift(...["Local", "Global"]);
19
24
  const [selectedTab, setSelectedTab] = useState(tabs[0]);
20
25
  const isLocalTab = selectedTab === "Local";
26
+ const isGlobalTab = selectedTab === "Global";
21
27
  const galleryScope = isLocalTab ? site.id : "global";
22
28
 
23
- const initialState: IGalleryState = {
24
- items: [],
25
- isImageSelected: false,
26
- imageSelected: null,
27
- page: 1,
28
- isFinished: false,
29
- };
30
-
31
- const [data, setData] = useState(initialState);
32
- const [isLoading, setIsLoading] = useState({ init: false, more: false });
33
-
34
29
  const validFormats = ["jpeg", "jpg", "png", "svg"];
35
30
 
36
31
  const galleryRef = useRef<HTMLDivElement>(null);
@@ -40,125 +35,67 @@ const Gallery = (props: IGalleryProps): JSX.Element => {
40
35
  const { sortedListStatus, setSortedListStatus } = useSortedListStatus();
41
36
  const [searchQuery, setSearchQuery] = useState<string>("");
42
37
  const isSearching = searchQuery.length > 0;
38
+ const pageRef = useRef(firstPage);
43
39
 
44
40
  const getParams = () => ({
45
41
  site: galleryScope,
46
- page: data.page,
47
- items: ITEMS_PER_PAGE,
42
+ page: data?.page,
43
+ items: itemsPerPage,
48
44
  query: currentFilterQuery,
49
- search: searchQuery
45
+ search: searchQuery,
50
46
  });
51
47
 
48
+ pageRef.current = getParams().page;
49
+
52
50
  useEffect(() => {
53
51
  const params = getParams();
54
- const getAndSetImages = () => {
55
- getSiteImages({ ...params, page: 1 })
56
- .then((result) => {
57
- setData({ items: result, page: 1, isImageSelected: false, imageSelected: null, isFinished: false });
58
- setIsLoading({ init: false, more: false });
59
- })
60
- .catch((error) => console.log(error));
61
- };
62
- setIsLoading({ init: true, more: false });
63
- getAndSetImages();
64
- // eslint-disable-next-line react-hooks/exhaustive-deps
52
+ getSiteImages({ ...params, page: firstPage });
53
+ // eslint-disable-next-line react-hooks/exhaustive-deps
65
54
  }, [galleryScope]);
66
55
 
67
56
  useEffect(() => {
68
57
  const handleScroll = () => {
69
- if (
70
- galleryRef.current && galleryRef.current.getBoundingClientRect().top + galleryRef.current.scrollTop + 60 <=
71
- galleryRef.current.getBoundingClientRect().height
72
- ) {
73
- return;
74
- }
58
+ const loadMore =
59
+ galleryRef.current &&
60
+ galleryRef.current.scrollHeight -
61
+ (galleryRef.current.scrollTop + galleryRef.current.getBoundingClientRect().height) <
62
+ 60 &&
63
+ galleryRef.current.scrollTop > 0;
64
+ if (!loadMore) return;
75
65
  if (!data.isFinished) {
76
- setIsLoading({ init: false, more: true });
66
+ const params = getParams();
67
+ getSiteImages({ ...params, page: pageRef.current + 1, more: true });
77
68
  }
78
69
  };
79
70
  window.addEventListener("scroll", handleScroll, true);
80
71
  return () => window.removeEventListener("scroll", handleScroll, true);
81
- }, [data.isFinished]);
82
-
83
- const getAndSetMoreImages = async () => {
84
- const params = getParams();
85
- const result = await getSiteImages({ ...params, page: params.page + 1 });
86
- try {
87
- setData({
88
- ...data,
89
- items: [...data.items, ...result],
90
- page: data.page + 1,
91
- isFinished: result.length < ITEMS_PER_PAGE,
92
- });
93
- setIsLoading({ init: false, more: false });
94
- } catch (error) {
95
- console.log(error);
96
- }
97
- };
72
+ // eslint-disable-next-line react-hooks/exhaustive-deps
73
+ }, [data?.isFinished]);
98
74
 
99
- const getAndSetImages = async () => {
75
+ const refreshImages = async (page = firstPage) => {
100
76
  const params = getParams();
101
- const result = await getSiteImages(params);
102
- try {
103
- setData({ ...data, items: result, isFinished: result.length < ITEMS_PER_PAGE });
104
- setIsLoading({ init: false, more: false });
105
- galleryRef && galleryRef.current && galleryRef.current.scrollTo(0, 0);
106
- } catch (error) {
107
- console.log(error);
108
- }
77
+ const more = page !== firstPage;
78
+ await getSiteImages({ ...params, page, more });
79
+ if (page < params.page) refreshImages(page + 1);
109
80
  };
110
81
 
111
82
  useEffect(() => {
112
- if (!isLoading.more) return;
113
- setTimeout(() => {
114
- getAndSetMoreImages();
115
- }, 2000);
116
- // eslint-disable-next-line react-hooks/exhaustive-deps
117
- }, [isLoading.more]);
118
-
119
- useEffect(() => {
120
- setIsLoading({ init: true, more: false });
121
- getAndSetImages();
83
+ const params = getParams();
84
+ getSiteImages({ ...params, page: firstPage });
122
85
  // eslint-disable-next-line react-hooks/exhaustive-deps
123
86
  }, [currentFilterQuery, searchQuery]);
124
87
 
125
- const _handleClick = (item: IImage) => (e: React.MouseEvent<HTMLDivElement>) => {
126
- if (data.imageSelected === item) {
127
- setData({ ...data, imageSelected: null, isImageSelected: false });
128
- } else {
129
- setData({ ...data, imageSelected: item, isImageSelected: true });
130
- }
88
+ const handleClick = (item: IImage) => (e: React.MouseEvent<HTMLDivElement>) => {
89
+ selectImage(item);
131
90
  };
132
91
 
133
- const setImage = () => {
134
- getImageSelected(data.imageSelected);
92
+ const setImage = (imageData: any) => {
93
+ const updatedImage = { ...data.imageSelected, ...imageData };
94
+ getImageSelected(updatedImage);
135
95
  toggleModal();
136
96
  };
137
97
 
138
- const updateItems = () => {
139
- setIsLoading({ init: true, more: false });
140
- const params = getParams();
141
- getSiteImages({ ...params, page: 1 })
142
- .then((result) => {
143
- setData({ items: result, isImageSelected: false, imageSelected: null, page: 1, isFinished: false });
144
- setIsLoading({ init: false, more: false });
145
- })
146
- .catch((error) => console.log(error));
147
- };
148
-
149
- const addImage = (item: IImage) => {
150
- setIsLoading({ init: true, more: false });
151
- const params = getParams();
152
- getSiteImages({ ...params, page: 1 })
153
- .then((result) => {
154
- setData({ items: result, isImageSelected: true, imageSelected: item, page: 1, isFinished: false });
155
- setIsLoading({ init: false, more: false });
156
- })
157
- .catch((error) => console.log(error));
158
- };
159
-
160
98
  const sortItems = async (orderPointer: string, isAscending: boolean) => {
161
- setData((data) => ({ ...data, page: 1 }));
162
99
  const sortedState = getSortedListStatus(orderPointer, isAscending);
163
100
  setSortedListStatus(sortedState);
164
101
 
@@ -168,7 +105,6 @@ const Gallery = (props: IGalleryProps): JSX.Element => {
168
105
  };
169
106
 
170
107
  const filterItems = async (filterPointer: string, filtersSelected: string) => {
171
- setData((data) => ({ ...data, page: 1 }));
172
108
  const filtersSelection = setFiltersSelection(filterPointer, filtersSelected);
173
109
  const filterQuery = setFilterQuery(filtersSelection);
174
110
  setCurrentFilterQuery(filterQuery);
@@ -178,9 +114,9 @@ const Gallery = (props: IGalleryProps): JSX.Element => {
178
114
  icon: "search",
179
115
  title: "Oh! No Results Found",
180
116
  message: "We couldn’t find what you are looking for. Please, try another search.",
181
- }
117
+ };
182
118
 
183
- const noSearchResults = !data.items.length && isSearching;
119
+ const noSearchResults = !data?.items?.length && isSearching;
184
120
 
185
121
  return (
186
122
  <S.Wrapper>
@@ -195,76 +131,102 @@ const Gallery = (props: IGalleryProps): JSX.Element => {
195
131
  <Orientation filterItems={filterItems} />
196
132
  <SortBy sortItems={sortItems} sortedState={sortedListStatus} />
197
133
  </S.Filters>
198
- </ S.Header>
134
+ </S.Header>
199
135
  <S.Search>
200
136
  <SearchField onChange={setSearchQuery} placeholder="Type an image’s name, title, or #tag" />
201
- </ S.Search>
137
+ </S.Search>
202
138
  <S.GalleryWrapper ref={galleryRef}>
139
+ {isGlobalTab && (
140
+ <S.NotificationWrapper>
141
+ <Notification
142
+ type="info"
143
+ text="This is a global Library. All the changes you make will be applied to all the sites."
144
+ />
145
+ </S.NotificationWrapper>
146
+ )}
147
+ <ErrorToast size="l" />
203
148
  {isLoading.init ? (
204
149
  <S.LoadingWrapper>
205
150
  <Loader name="circle" />
206
151
  </S.LoadingWrapper>
207
152
  ) : (
208
- <S.Grid>
209
- {data.items &&
210
- data.items.map((item: IImage, index: number) => {
211
- const isSelected = data.imageSelected ? item.id === data.imageSelected.id : false;
212
- return (
213
- <S.GridItem key={item.name + index}>
214
- <S.ImageWrapper onClick={_handleClick(item)} selected={isSelected}>
215
- <img src={item.thumb} alt={item.alt} />
216
- <S.IconUnchecked>
217
- <Icon name="emptyCheck" size="24" />
218
- </S.IconUnchecked>
219
- <S.IconChecked>
220
- <Icon name="success" size="24" />
221
- </S.IconChecked>
222
- </S.ImageWrapper>
223
- </S.GridItem>
224
- );
225
- })
226
- }
153
+ <>
154
+ {noSearchResults ? (
155
+ <S.EmptyWrapper>
156
+ <EmptyState {...emptyStateProps} />
157
+ </S.EmptyWrapper>
158
+ ) : (
159
+ <S.Grid>
160
+ {data &&
161
+ data.items &&
162
+ data.items.map((item: IImage, index: number) => {
163
+ const isSelected = data.imageSelected ? item.id === data.imageSelected.id : false;
164
+ return (
165
+ <S.GridItem key={item.name + index} orientation={item.orientation}>
166
+ <S.ImageWrapper
167
+ onClick={handleClick(item)}
168
+ selected={isSelected}
169
+ orientation={item.orientation}
170
+ >
171
+ <img src={item.thumb} alt={item.alt} />
172
+ <S.IconUnchecked>
173
+ <Icon name="emptyCheck" size="24" />
174
+ </S.IconUnchecked>
175
+ <S.IconChecked>
176
+ <Icon name="success" size="24" />
177
+ </S.IconChecked>
178
+ </S.ImageWrapper>
179
+ </S.GridItem>
180
+ );
181
+ })}
182
+ </S.Grid>
183
+ )}
227
184
  {isLoading.more && (
228
185
  <S.LoaderWrapper>
229
186
  <Loader name="dots" />
230
187
  </S.LoaderWrapper>
231
188
  )}
232
- {noSearchResults && (
233
- <S.EmptyWrapper>
234
- <EmptyState {...emptyStateProps} />
235
- </S.EmptyWrapper>)
236
- }
237
- </S.Grid>
189
+ </>
238
190
  )}
239
191
  </S.GalleryWrapper>
240
- </ S.GalleryTabs>
192
+ </S.GalleryTabs>
241
193
  <GalleryPanel
242
- isImageSelected={data.isImageSelected}
243
- imageSelected={data.imageSelected}
194
+ isImageSelected={data?.isImageSelected}
195
+ imageSelected={data?.imageSelected}
244
196
  validFormats={validFormats}
245
- addImage={addImage}
246
- updateItems={updateItems}
247
197
  setImage={setImage}
248
198
  isGlobalTab={!isLocalTab}
249
199
  site={galleryScope}
250
200
  selectedTab={selectedTab}
201
+ refreshImages={refreshImages}
251
202
  />
252
203
  </S.Wrapper>
253
204
  );
254
205
  };
255
206
 
256
- interface IGalleryState {
257
- items: IImage[];
258
- isImageSelected: boolean;
259
- imageSelected: IImage | null;
260
- page: number;
261
- isFinished: boolean;
262
- }
263
-
264
207
  interface IGalleryProps {
265
208
  getImageSelected: (img: IImage | null) => void;
266
209
  toggleModal: () => void;
267
210
  site: ISite;
211
+ data: IData;
212
+ isLoading: IIsLoading;
213
+ }
214
+
215
+ const mapStateToProps = (state: IRootState) => ({
216
+ data: state.gallery.data,
217
+ isLoading: state.gallery.isLoading,
218
+ });
219
+
220
+ interface IDispatchProps {
221
+ getSiteImages(params: IGetSiteImages): Promise<void>;
222
+ selectImage(item: IImage): void;
268
223
  }
269
224
 
270
- export default memo(Gallery);
225
+ const mapDispatchToProps = {
226
+ getSiteImages: galleryActions.getSiteImages,
227
+ selectImage: galleryActions.selectImage,
228
+ };
229
+
230
+ type IProps = IGalleryProps & IDispatchProps;
231
+
232
+ export default connect(mapStateToProps, mapDispatchToProps)(memo(Gallery));
@@ -1,7 +1,11 @@
1
1
  import styled from "styled-components";
2
2
 
3
- const gutter = "16px";
4
- const columns = "5";
3
+ const gutter = "6px";
4
+ const imageSizes: Record<string, number> = {
5
+ "S": 2.5,
6
+ "L": 4,
7
+ "P": 1.75
8
+ }
5
9
 
6
10
  export const Wrapper = styled.div`
7
11
  display: flex;
@@ -53,6 +57,7 @@ export const GalleryWrapper = styled.div`
53
57
  overflow: auto;
54
58
  padding-right: ${gutter};
55
59
  min-height: calc(100% - ${p => p.theme.spacing.xl} * 2);
60
+ position: relative;
56
61
  `;
57
62
 
58
63
  export const IconChecked = styled.div`
@@ -86,26 +91,37 @@ export const IconUnchecked = styled.div`
86
91
  export const Grid = styled.div`
87
92
  display: flex;
88
93
  flex-flow: row wrap;
94
+ align-content: flex-start;
95
+ align-items: stretch;
89
96
  padding: ${p => p.theme.spacing.m};
90
97
  margin: 0 -${gutter};
91
98
  margin-top: -${gutter};
92
99
  border-right: 1px solid ${p => p.theme.color.uiLine};
93
100
  min-height: calc(100% + ${gutter});
101
+
102
+ &::after {
103
+ content: "";
104
+ flex: auto;
105
+ }
94
106
  `;
95
107
 
96
- export const GridItem = styled.div`
97
- width: calc(100% / ${columns});
108
+ export const GridItem = styled.div<{ orientation: string }>`
98
109
  padding-left: ${gutter};
99
110
  padding-top: ${gutter};
111
+ flex: ${p => p.orientation === "P" ? 0 : 1};
112
+
113
+ &:last-child {
114
+ flex: 0;
115
+ }
100
116
  `;
101
117
 
102
- export const ImageWrapper = styled.div<{ selected: boolean }>`
118
+ export const ImageWrapper = styled.div<{ selected: boolean, orientation: string }>`
103
119
  position: relative;
104
- width: ${p => `calc(${p.theme.spacing.xl} * 2)`};
105
- height: ${p => `calc(${p.theme.spacing.l} * 2)`};
120
+ min-width: ${p => `calc(${p.theme.spacing.l} * ${imageSizes[p.orientation]})`};
121
+ width: ${p => p.orientation === "P" ? `calc(${p.theme.spacing.l} * ${imageSizes["P"]})` : "auto"};
122
+ height: ${p => `calc(${p.theme.spacing.l} * ${imageSizes["S"]})`};
106
123
  border: 1px solid;
107
124
  border-color: ${p => (p.selected === true ? p.theme.color.interactive02 : p.theme.color.uiLine)};
108
- border-radius: 4px;
109
125
  :before {
110
126
  content: "";
111
127
  position: absolute;
@@ -116,7 +132,6 @@ export const ImageWrapper = styled.div<{ selected: boolean }>`
116
132
  opacity: ${p => (p.selected === true ? 1 : 0)};
117
133
  transition: opacity 0.1s;
118
134
  background-color: ${p => (p.selected === true ? p.theme.color.overlay : `transparent`)};
119
- border-radius: 4px;
120
135
  }
121
136
  :hover {
122
137
  cursor: pointer;
@@ -133,7 +148,6 @@ export const ImageWrapper = styled.div<{ selected: boolean }>`
133
148
  width: 100%;
134
149
  height: 100%;
135
150
  object-fit: cover;
136
- border-radius: 4px;
137
151
  }
138
152
  ${IconChecked} {
139
153
  display: ${p => (p.selected === true ? `block` : 0)};
@@ -156,10 +170,26 @@ export const LoadingWrapper = styled.div`
156
170
  export const LoaderWrapper = styled.div`
157
171
  width: 100%;
158
172
  text-align: center;
173
+ margin-bottom: ${p => p.theme.spacing.s};
159
174
  `;
160
175
 
161
176
  export const EmptyWrapper = styled.div`
162
177
  display: flex;
163
178
  align-items: center;
164
179
  margin: auto;
180
+ align-content: center;
181
+ padding: ${p => p.theme.spacing.m};
182
+ margin: 0 -${gutter};
183
+ margin-top: -${gutter};
184
+ border-right: 1px solid ${p => p.theme.color.uiLine};
185
+ min-height: calc(100% + ${gutter});
186
+ `;
187
+
188
+ export const NotificationWrapper = styled.div`
189
+ width: 100%;
190
+ position: absolute;
191
+ top: 0;
192
+ left: 0;
193
+ right: 0;
194
+ z-index: 2;
165
195
  `;
@@ -84,6 +84,7 @@ const AppBar = (props: IProps): JSX.Element => {
84
84
  if (pageLanguages && !getCurrentLanguage(item.locale)) {
85
85
  setNewTranslation(true);
86
86
  languageActions.getContent && currentPageID && languageActions.getContent(currentPageID);
87
+ languageActions.getDataContent && currentPageID && languageActions.getDataContent(currentPageID);
87
88
  }
88
89
  };
89
90
 
@@ -1,4 +1,4 @@
1
- import React, { useRef } from "react";
1
+ import React, { useRef, useEffect } from "react";
2
2
 
3
3
  import { createPortal } from "react-dom";
4
4
  import { Button, IconAction } from "@ax/components";
@@ -10,22 +10,28 @@ const Toast = (props: IProps) => {
10
10
  const toast = useRef<any>(null);
11
11
 
12
12
  let temp: any;
13
- const setTemp = () => (temp = setTimeout(() => setIsVisible(false), 2000));
14
- const stopTemp = () => clearTimeout(temp);
13
+ const setTemp = (time: number) => (temp = setTimeout(() => setIsVisible(false), time));
15
14
 
16
15
  const close = () => {
17
16
  toast.current.classList.add("close-animation");
18
- setTemp();
19
- stopTemp();
17
+ setTemp(1000);
20
18
  };
21
19
 
20
+ useEffect(() => {
21
+ setTemp(5000);
22
+ return () => clearTimeout(temp);
23
+ // eslint-disable-next-line react-hooks/exhaustive-deps
24
+ }, []);
25
+
22
26
  return createPortal(
23
27
  <S.Wrapper ref={toast}>
24
28
  <S.Text>{message}</S.Text>
25
29
  <S.Buttons>
26
- <Button type="button" buttonStyle="lineInverse" onClick={action}>
27
- Undo
28
- </Button>
30
+ {action &&
31
+ <Button type="button" buttonStyle="lineInverse" onClick={action}>
32
+ Undo
33
+ </Button>
34
+ }
29
35
  <IconAction icon="close" onClick={close} inversed={true} />
30
36
  </S.Buttons>
31
37
  </S.Wrapper>,
@@ -34,7 +40,7 @@ const Toast = (props: IProps) => {
34
40
  };
35
41
 
36
42
  interface IProps {
37
- action: () => void;
43
+ action?: () => void;
38
44
  message: string;
39
45
  setIsVisible: (visibility: boolean) => void;
40
46
  }
@@ -8,8 +8,8 @@ const Wrapper = styled.div`
8
8
  justify-content: space-between;
9
9
  align-items: center;
10
10
  border-radius: 4px;
11
- position: absolute;
12
- z-index: 1;
11
+ position: fixed;
12
+ z-index: 1100;
13
13
  bottom: ${p => p.theme.spacing.m};
14
14
  left: ${p => `calc(${p.theme.spacing.m} * 4)`};
15
15
  padding-left: ${p => p.theme.spacing.m};