sfc-utils 1.4.55 → 1.4.57

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.
@@ -0,0 +1,31 @@
1
+ import React from 'react'
2
+ import { Helmet } from "react-helmet"
3
+ import { getFigureWidth } from './helpers/utilfunctions.mjs'
4
+
5
+ const Datawrapper = ({ altText, id, height, figureWidth = 'text-width' }) => {
6
+
7
+ const url = `https://datawrapper.dwcdn.net/${id}`
8
+ const nu_id = `datawrapper-chart-${id}`
9
+
10
+ // width options: float-left, float-right, text-width, large, wide, full
11
+ const width = getFigureWidth(figureWidth);
12
+
13
+ return (
14
+ <>
15
+ {/* responsive DW script */}
16
+ <Helmet>
17
+ <script
18
+ type="text/javascript"
19
+ src="https://datawrapper.dwcdn.net/lib/embed.min.js"
20
+ ></script>
21
+ </Helmet>
22
+ <div className={`iframe_Container graphic-wrapper inline-figure ${width}`}>
23
+ <figure>
24
+ <iframe title={altText} aria-label={altText} id={nu_id} src={url} scrolling="no" frameborder="0" style={{ width: 0, minWidth: "100%", border: "none" }} height={height}></iframe>
25
+ </figure>
26
+ </div>
27
+ </>
28
+ )
29
+ }
30
+
31
+ export default Datawrapper
@@ -1,179 +1,179 @@
1
- import React, { useRef, useState } from 'react'
2
- import * as geocoderStyles from '../styles/modules/geocoder.module.less'
1
+ import React, { useRef, useState } from "react";
2
+ import * as geocoderStyles from "../styles/modules/geocoder.module.less";
3
3
 
4
4
  // This is a singleton event listener that we can use to add/remove event listeners
5
5
  // Don't call it during SSR
6
6
  var setSingletonEventListener = (function (element) {
7
- var handlers = {}
7
+ var handlers = {};
8
8
  return function (evtName, func) {
9
9
  handlers.hasOwnProperty(evtName) &&
10
- element.removeEventListener(evtName, handlers[evtName])
10
+ element.removeEventListener(evtName, handlers[evtName]);
11
11
  if (func) {
12
- handlers[evtName] = func
13
- element.addEventListener(evtName, func)
12
+ handlers[evtName] = func;
13
+ element.addEventListener(evtName, func);
14
14
  } else {
15
- delete handlers[evtName]
15
+ delete handlers[evtName];
16
16
  }
17
- }
18
- })(typeof document !== 'undefined' && document)
17
+ };
18
+ })(typeof document !== "undefined" && document);
19
19
 
20
20
  // Our new and improved geocoder! Hits PositionStack first and then falls back to classic geocoder
21
21
  // Accepts a region to filter results by -- this can be custom but if it's not passed in, it will default to the state where the market resides
22
22
  const Geocoder = ({
23
23
  filterRegion, // You need to test results, but could also pass in a neighbourhood, district, city, county, state or administrative area
24
- market, // Will filter by the market's state if no filterRegion provided
24
+ market, // Will filter by the market's state if no filterRegion provided
25
25
  resultFunc,
26
26
  buttonTrackingId,
27
- placeholder
27
+ placeholder,
28
28
  }) => {
29
29
  // Show a loader when we're requesting
30
- const [loading, setLoading] = useState(false)
31
- const [locData, setLocData] = useState(null)
32
- const [inputValue, setInputValue] = useState('')
33
- const [activeKeyboardIndex, setActiveKeyboardIndex] = useState(null)
34
- const resultsRef = useRef(null)
35
- const geocoderInputRef = useRef(null)
36
- const latestFetchRef = useRef(null)
30
+ const [loading, setLoading] = useState(false);
31
+ const [locData, setLocData] = useState(null);
32
+ const [inputValue, setInputValue] = useState("");
33
+ const [activeKeyboardIndex, setActiveKeyboardIndex] = useState(null);
34
+ const resultsRef = useRef(null);
35
+ const geocoderInputRef = useRef(null);
36
+ const latestFetchRef = useRef(null);
37
37
 
38
38
  if (!filterRegion) {
39
39
  switch (market) {
40
- case 'SFC':
41
- filterRegion = 'California'
42
- break
43
- case 'Houston':
44
- case 'San Antonio':
45
- filterRegion = 'Texas'
46
- break
47
- case 'Albany':
48
- filterRegion = 'New York'
49
- break
50
- case 'CT':
51
- filterRegion = 'Connecticut'
40
+ case "SFC":
41
+ filterRegion = "California";
42
+ break;
43
+ case "Houston":
44
+ case "San Antonio":
45
+ filterRegion = "Texas";
46
+ break;
47
+ case "Albany":
48
+ filterRegion = "New York";
49
+ break;
50
+ case "CT":
51
+ filterRegion = "Connecticut";
52
52
  default:
53
- filterRegion = 'United States'
53
+ filterRegion = "United States";
54
54
  }
55
55
  }
56
56
 
57
57
  // We don't want this CONSTANTLY firing, so we debounce it
58
58
  const search = (query) => {
59
59
  // Save value of query so we can check if it's the last one
60
- latestFetchRef.current = query
60
+ latestFetchRef.current = query;
61
61
  // If we're already loading, don't fire another request, the request will be re-requested after the first one finishes
62
62
  if (loading) {
63
- return false
63
+ return false;
64
64
  }
65
65
  // POST as form url encoded data
66
- let formData = new FormData()
67
- formData.append('query', query)
68
- formData.append('region', filterRegion)
66
+ let formData = new FormData();
67
+ formData.append("query", query);
68
+ formData.append("region", filterRegion);
69
69
  // Remove any existing event listeners
70
- setSingletonEventListener('keydown')
70
+ setSingletonEventListener("keydown");
71
71
  // Make req
72
- fetch('https://projects.sfchronicle.com/feeds/geocode/v2.php', {
73
- method: 'POST',
72
+ fetch("https://projects.sfchronicle.com/feeds/geocode/v2.php", {
73
+ method: "POST",
74
74
  body: formData,
75
75
  })
76
76
  .then((resp) => {
77
77
  // Sometimes, there's a junk response, so let's handle that gracefully
78
78
  if (!resp || !resp.ok) {
79
- return null
79
+ return null;
80
80
  }
81
- return resp.json()
81
+ return resp.json();
82
82
  })
83
83
  .then((output) => {
84
84
  // If this is not the latest fetch, bail and fetch latest
85
85
  if (latestFetchRef.current !== query) {
86
86
  setTimeout(() => {
87
87
  // Delay a bit because it seems like we're still typing
88
- search(latestFetchRef.current)
89
- }, 1000)
90
- return
88
+ search(latestFetchRef.current);
89
+ }, 1000);
90
+ return;
91
91
  } else {
92
92
  // Uncomment this to see what request was actually honored
93
93
  //console.log('OK VALID RESULTS FOR', latestFetchRef.current)
94
94
  }
95
95
  // Unset loading
96
- setLoading(false)
96
+ setLoading(false);
97
97
  // Remove any existing event listeners
98
- setSingletonEventListener('keydown')
98
+ setSingletonEventListener("keydown");
99
99
  // Show results
100
- setLocData(output.data)
100
+ setLocData(output.data);
101
101
  // Bail early if there's no data
102
102
  if (!output) {
103
- return false
103
+ return false;
104
104
  }
105
105
  // Handle result
106
106
  if (output.data.length === 0) {
107
107
  // We could show something saying "No results" ... or we could not
108
108
  } else {
109
109
  // Create a keydown event listener
110
- setSingletonEventListener('keydown', function resultsKeyHandler(e) {
110
+ setSingletonEventListener("keydown", function resultsKeyHandler(e) {
111
111
  setActiveKeyboardIndex((prevIndex) => {
112
- let newIndex = prevIndex
112
+ let newIndex = prevIndex;
113
113
  switch (e.key) {
114
- case 'ArrowDown':
114
+ case "ArrowDown":
115
115
  // Handle the index
116
116
  if (prevIndex === null) {
117
- newIndex = 0
117
+ newIndex = 0;
118
118
  } else if (prevIndex < output.data.length - 1) {
119
- newIndex = prevIndex + 1
119
+ newIndex = prevIndex + 1;
120
120
  } else {
121
- newIndex = 0
121
+ newIndex = 0;
122
122
  }
123
- break
124
- case 'ArrowUp':
123
+ break;
124
+ case "ArrowUp":
125
125
  // Handle the index
126
126
  if (prevIndex === null) {
127
- newIndex = output.data.length - 1
127
+ newIndex = output.data.length - 1;
128
128
  } else if (prevIndex > 0) {
129
- newIndex = prevIndex - 1
129
+ newIndex = prevIndex - 1;
130
130
  } else {
131
- newIndex = output.data.length - 1
131
+ newIndex = output.data.length - 1;
132
132
  }
133
- break
134
- case 'Enter':
133
+ break;
134
+ case "Enter":
135
135
  // Do something with the data
136
- const selectedLocation = output.data[prevIndex]
136
+ const selectedLocation = output.data[prevIndex];
137
137
  // Update the input value with the choice
138
138
  if (selectedLocation) {
139
- setInputValue(selectedLocation.name)
139
+ setInputValue(selectedLocation.name);
140
140
  // Call function if it exists
141
141
  if (resultFunc) {
142
- resultFunc(selectedLocation)
142
+ resultFunc(selectedLocation);
143
143
  }
144
144
  }
145
145
  // Hide the list now
146
- setLocData(null)
146
+ setLocData(null);
147
147
  }
148
- return newIndex
149
- })
150
- })
148
+ return newIndex;
149
+ });
150
+ });
151
151
 
152
152
  // Start listening for a click outside the div
153
- document.addEventListener('click', function resultsClickHandler(e) {
153
+ document.addEventListener("click", function resultsClickHandler(e) {
154
154
  // Whether we clicked inside or outside, we hide the list
155
- setLocData(null)
155
+ setLocData(null);
156
156
  // Also cancel the keydown listener
157
- setSingletonEventListener('keydown')
157
+ setSingletonEventListener("keydown");
158
158
  // Received click, cancel this listener
159
- this.removeEventListener('click', resultsClickHandler)
160
- })
159
+ this.removeEventListener("click", resultsClickHandler);
160
+ });
161
161
  }
162
- })
163
- }
162
+ });
163
+ };
164
164
 
165
165
  // Handle the change event
166
166
  const handleChange = (event) => {
167
167
  // Set the input value
168
- const inputValue = event.target.value
169
- setInputValue(inputValue)
170
- setActiveKeyboardIndex(null)
168
+ const inputValue = event.target.value;
169
+ setInputValue(inputValue);
170
+ setActiveKeyboardIndex(null);
171
171
  // Only query if the value is not empty
172
172
  if (inputValue) {
173
- setLoading(true)
174
- search(inputValue)
173
+ setLoading(true);
174
+ search(inputValue);
175
175
  }
176
- }
176
+ };
177
177
 
178
178
  return (
179
179
  <div className={geocoderStyles.wrapper}>
@@ -192,44 +192,43 @@ const Geocoder = ({
192
192
  {locData && (
193
193
  <ul className={geocoderStyles.resultsWrapper} ref={resultsRef}>
194
194
  {locData.map((loc, i) => {
195
- let thisClass = geocoderStyles.result
195
+ let thisClass = geocoderStyles.result;
196
196
  if (activeKeyboardIndex === i) {
197
- thisClass += ' ' + geocoderStyles.active
197
+ thisClass += " " + geocoderStyles.active;
198
198
  }
199
199
  return (
200
200
  <li
201
201
  className={thisClass}
202
202
  key={i}
203
203
  onClick={() => {
204
- setInputValue(locData[i].name)
204
+ setInputValue(locData[i].name);
205
205
  // Call function if it exists
206
206
  if (resultFunc) {
207
- resultFunc(locData[i])
207
+ resultFunc(locData[i]);
208
208
  }
209
209
  }}
210
210
  >
211
211
  <button
212
- id={buttonTrackingId ? buttonTrackingId : ""}
213
- className={buttonTrackingId ? geocoderStyles.button + " devhub-ga-tracking" : geocoderStyles.button}
212
+ className={geocoderStyles.button}
214
213
  onFocus={(e) => {
215
214
  // Set index
216
- setActiveKeyboardIndex(i)
215
+ setActiveKeyboardIndex(i);
217
216
  }}
218
217
  >
219
218
  <div className={geocoderStyles.name}>{loc.name}</div>
220
219
  <div className={geocoderStyles.details}>
221
220
  {loc.locality}
222
- {loc.locality && loc.region && ', '}
221
+ {loc.locality && loc.region && ", "}
223
222
  {loc.region}
224
223
  </div>
225
224
  </button>
226
225
  </li>
227
- )
226
+ );
228
227
  })}
229
228
  </ul>
230
229
  )}
231
230
  </div>
232
- )
233
- }
231
+ );
232
+ };
234
233
 
235
- export default Geocoder
234
+ export default Geocoder;
@@ -50,7 +50,7 @@ function appendLayoutScripts(isEmbedded, isAdRemoved, marketKey) {
50
50
  document.body.appendChild(script)
51
51
 
52
52
  // Init sailthru
53
- if (window && window.Sailthru && marketKey){
53
+ if (window && window.Sailthru && marketKey) {
54
54
  window.Sailthru.init({ customerId: getBrands2(marketKey).attributes.sailCustomer })
55
55
  }
56
56
  }
@@ -81,4 +81,25 @@ function formatHDN(isEmbedded, url_add, meta) {
81
81
  return stringHDN;
82
82
  }
83
83
 
84
- export { debounce, appendLayoutScripts, formatHDN }
84
+ function getFigureWidth(maxWidth) {
85
+ switch (maxWidth) {
86
+ case "text-width":
87
+ return "text-width";
88
+ case "large":
89
+ return "large mw-lg mt-md mb-md ml-auto mr-auto";
90
+ case "wide":
91
+ return "wide mw-xl mt-md mb-md ml-auto mr-auto";
92
+ case "full":
93
+ return "full mw-100 mt-md mb-md ml-auto mr-auto";
94
+ case "embed":
95
+ return "mw-xl ml-auto mr-auto";
96
+ case "float-right":
97
+ return "float-right";
98
+ case "float-left":
99
+ return "float-left";
100
+ default:
101
+ return "text-width";
102
+ }
103
+ }
104
+
105
+ export { debounce, appendLayoutScripts, formatHDN, getFigureWidth }
@@ -28,6 +28,22 @@ const ImageSlideshow = ({ wcmData, imageList, altList, topperStyle, isLayoutInve
28
28
  };
29
29
  }, [index]);
30
30
 
31
+ useEffect(() => {
32
+ // Find the max height ratio within an wcm image list, this is only used for the small visual slideshow
33
+ if (wcmData.nodes) {
34
+ let ratioList = wcmData.nodes.map((d) => d.photo.ratio)
35
+ console.log(ratioList)
36
+ let minRatio = Math.max(...ratioList)
37
+
38
+ // During server-side rendering, access to ":root" is unavailable. This is okay; we just need
39
+ // to make sure that the site does not crash during SSR
40
+ let r = (typeof window != "undefined") ? document.querySelector(':root') : null;
41
+ if (r) {
42
+ r.style.setProperty('--small-visual-height-ratio', minRatio);
43
+ }
44
+ }
45
+ }, [])
46
+
31
47
  const getContainerClass = () => {
32
48
  switch (topperStyle) {
33
49
  case "stacked":
@@ -6,13 +6,14 @@ import CaptionCreditSlideshow from "./slideshow/captioncreditslideshow.mjs"
6
6
  import ImageSlideshow from "./slideshow/imageslideshow.mjs"
7
7
  import * as topperStyles from "../styles/modules/topper2.module.less"
8
8
  import * as imageStyles from "../styles/modules/topperimage.module.less"
9
+ import Datawrapper from "./datawrapper.mjs"
9
10
 
10
11
  const Topper2 = ({ settings, wcmData, mods }) => {
11
12
  let {
12
- Topper_Style, Title, Title_Style, Deck, Image, Image_Alt, Kicker, Video_Mp4, Image_Caption, Image_Credits,
13
- HeaderDek_Vertical_Position, HeaderDek_Vertical_Offset, HeaderDek_Horizontal_Offset,
14
- HeaderDek_Horizontal_Position, Inverted_Colors, Inverted_Layout, Inverted_Text_Color,
15
- Topper_Background_Color, Small_Visual_Max_Width, Small_Visual_Topper_Url
13
+ Topper_Style, Title, Title_Style, Deck, Image, Image_Alt, Kicker, Video_Mp4, Datawrapper_Id,
14
+ Image_Caption, Image_Credits, HeaderDek_Vertical_Position, HeaderDek_Vertical_Offset,
15
+ HeaderDek_Horizontal_Offset, HeaderDek_Horizontal_Position, Inverted_Colors, Inverted_Layout,
16
+ Inverted_Text_Color, Topper_Background_Color, Small_Visual_Max_Width, Small_Visual_Topper_Url
16
17
  } = settings
17
18
 
18
19
  // During server-side rendering, access to ":root" is unavailable. This is okay; we just need
@@ -216,6 +217,11 @@ const Topper2 = ({ settings, wcmData, mods }) => {
216
217
  return (wcmIdList.length > 1);
217
218
  }
218
219
 
220
+ /** Checks if the media type is a datawrapper chart */
221
+ const isDatawrapper = () => {
222
+ return Datawrapper_Id && Datawrapper_Id !== 0;
223
+ }
224
+
219
225
  /** Converts string from spreadsheet into a list and pads the list if the length is incorrect */
220
226
  const convertStringToList = (str, size) => {
221
227
  // If the string is empty (ie caption/credit is not filled out), return early
@@ -262,8 +268,18 @@ const Topper2 = ({ settings, wcmData, mods }) => {
262
268
  )
263
269
  }
264
270
 
271
+ const getDatawrapperHtml = () => {
272
+ let datawrapperCss = ""
273
+
274
+ return (
275
+ <Datawrapper altText={Image_Alt} id={Datawrapper_Id} />
276
+ )
277
+ }
278
+
265
279
  /* Returns the corresponding image HTML for each topper style and slideshow status */
266
280
  const getMediaHTML = (isSlideshow) => {
281
+ if (isDatawrapper()) return getDatawrapperHtml();
282
+
267
283
  if (Video_Mp4) return getVideoHtml();
268
284
 
269
285
  if (isSlideshow) return (
@@ -426,17 +442,18 @@ const Topper2 = ({ settings, wcmData, mods }) => {
426
442
  extraStyles={[topperStyles.smallPaddingLeftWhenTablet]}
427
443
  />
428
444
  }
429
- {!isSlideshow(wcmIdList) && <CaptionCredit caption={Image_Caption} credit={Image_Credits} extraStyles={[topperStyles.smallPaddingLeftWhenTablet]} />}
445
+ {!isSlideshow(wcmIdList) && (!isDatawrapper()) && <CaptionCredit caption={Image_Caption} credit={Image_Credits} extraStyles={[topperStyles.smallPaddingLeftWhenTablet]} />}
430
446
  </figure>
431
447
  </div>
432
448
  </>
433
449
  );
434
450
 
435
451
  case "small-visual":
452
+ let smallVisualCss = (isDatawrapper()) ? `${topperStyles.imageSmallVisualDatawrapper}` : `${topperStyles.imageSmallVisual}`;
436
453
  return (
437
454
  <>
438
455
  <div>
439
- <figure className={`mw-xl ml-auto mr-auto ${topperStyles.imageSmallVisual}`}>
456
+ <figure className={`mw-xl ml-auto mr-auto ${smallVisualCss}`}>
440
457
  {getMediaHTML(isSlideshow(wcmIdList))}
441
458
  </figure>
442
459
  {Kicker && <Heading level={6} text={Kicker} className={kickerStyleList().join(' ')} />}
@@ -478,7 +495,15 @@ const Topper2 = ({ settings, wcmData, mods }) => {
478
495
  );
479
496
 
480
497
  case "side-by-side":
481
- let figureCss = isSlideshow(wcmIdList) ? `${topperStyles.imageSideBySideSlideshow}` : `${topperStyles.imageSideBySide}`;
498
+ let figureCss = "";
499
+ if (isSlideshow(wcmIdList)) {
500
+ figureCss = `${topperStyles.imageSideBySideSlideshow}`
501
+ } else if (isDatawrapper()) {
502
+ figureCss = `${topperStyles.imageSideBySideDatawrapper}`
503
+ } else {
504
+ figureCss = `${topperStyles.imageSideBySide}`
505
+ }
506
+
482
507
  let sideBySideContainerCss = (Inverted_Layout === "headerdek-right-image-left") ? `${topperStyles.topperContainerSideBySide} ${topperStyles.reverseFlexbox}` : `${topperStyles.topperContainerSideBySide}`;
483
508
  setBackgroundAndTextColor();
484
509
  return (
@@ -508,13 +533,20 @@ const Topper2 = ({ settings, wcmData, mods }) => {
508
533
  creditStyles={[sideBySideCapCredColorCss()]}
509
534
  />
510
535
  }
511
- {!isSlideshow(wcmIdList) && <CaptionCredit caption={Image_Caption} credit={Image_Credits} extraStyles={[topperStyles.captionSideBySide, sideBySideCapCredColorCss()]} creditStyles={[sideBySideCapCredColorCss()]} />}
536
+ {!isSlideshow(wcmIdList) && (!isDatawrapper()) && <CaptionCredit caption={Image_Caption} credit={Image_Credits} extraStyles={[topperStyles.captionSideBySide, sideBySideCapCredColorCss()]} creditStyles={[sideBySideCapCredColorCss()]} />}
512
537
  </figure>
513
538
  </div>
514
539
  );
515
540
 
516
541
  case "side-by-side-portrait":
517
- let portraitFigureCss = isSlideshow(wcmIdList) ? `${topperStyles.imageSideBySidePortraitSlideshow}` : `${topperStyles.imageSideBySidePortrait}`;
542
+ let portraitFigureCss = "";
543
+ if (isSlideshow(wcmIdList)) {
544
+ portraitFigureCss = `${topperStyles.imageSideBySidePortraitSlideshow}`
545
+ } else if (isDatawrapper()) {
546
+ portraitFigureCss = `${topperStyles.imageSideBySidePortraitDatawrapper}`
547
+ } else {
548
+ portraitFigureCss = `${topperStyles.imageSideBySidePortrait}`
549
+ }
518
550
  let sideBySidePortraitContainerCss = (Inverted_Layout === "headerdek-right-image-left") ? `${topperStyles.topperContainerSideBySidePortrait} ${topperStyles.reverseFlexbox}` : `${topperStyles.topperContainerSideBySidePortrait}`;
519
551
  setBackgroundAndTextColor();
520
552
  return (
@@ -546,7 +578,7 @@ const Topper2 = ({ settings, wcmData, mods }) => {
546
578
  creditStyles={[sideBySideCapCredColorCss()]}
547
579
  />
548
580
  }
549
- {!isSlideshow(wcmIdList) && <CaptionCredit caption={Image_Caption} credit={Image_Credits} extraStyles={[topperStyles.captionSideBySidePortrait, sideBySideCapCredColorCss(), sideBySidePortraitFloatCss()]} creditStyles={[sideBySideCapCredColorCss()]} />}
581
+ {!isSlideshow(wcmIdList) && (!isDatawrapper()) && <CaptionCredit caption={Image_Caption} credit={Image_Credits} extraStyles={[topperStyles.captionSideBySidePortrait, sideBySideCapCredColorCss(), sideBySidePortraitFloatCss()]} creditStyles={[sideBySideCapCredColorCss()]} />}
550
582
  </figure>
551
583
  </div>
552
584
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sfc-utils",
3
- "version": "1.4.55",
3
+ "version": "1.4.57",
4
4
  "author": "ewagstaff <evanjwagstaff@gmail.com>",
5
5
  "dependencies": {
6
6
  "archieml": "^0.4.2",
@@ -14,4 +14,4 @@
14
14
  "react-transition-group": "^4.4.5",
15
15
  "write": "^2.0.0"
16
16
  }
17
- }
17
+ }
@@ -1,5 +1,9 @@
1
1
  @import (less) '../values';
2
2
 
3
+ :root {
4
+ --small-visual-height-ratio: 2/3;
5
+ }
6
+
3
7
  .container-stacked {
4
8
  margin: 0 auto;
5
9
  white-space: nowrap;
@@ -36,7 +40,7 @@
36
40
 
37
41
  .container-small-visual {
38
42
  width: 150px;
39
- height: calc(150px*2/3);
43
+ height: calc(150px*var(--small-visual-height-ratio))
40
44
  }
41
45
 
42
46
  .image-wrapper-stacked {
@@ -69,7 +73,10 @@
69
73
  .image-wrapper-small-visual {
70
74
  position: absolute;
71
75
  width: 150px;
72
- height: calc(150px*2/3)
76
+ right: 0;
77
+ left: 0;
78
+ bottom: 0;
79
+ margin: auto;
73
80
  }
74
81
 
75
82
  .image-wrapper-side-by-side-portrait {
@@ -98,6 +98,20 @@
98
98
  }
99
99
  }
100
100
 
101
+ .imageSmallVisualDatawrapper {
102
+ position: relative;
103
+ height: auto;
104
+ width: 100%;
105
+ margin: @s24 auto 0 auto;
106
+
107
+ @media (max-width: 960px) {
108
+ margin-left: @s16;
109
+ margin-right: @s16;
110
+ width: auto;
111
+ height: auto;
112
+ }
113
+ }
114
+
101
115
  .imageSideBySide {
102
116
  width: 50%;
103
117
  float: left;
@@ -110,6 +124,18 @@
110
124
  }
111
125
  }
112
126
 
127
+ .imageSideBySideDatawrapper {
128
+ width: 50%;
129
+ float: left;
130
+ padding: @s16;
131
+
132
+ @media (max-width: @lg) {
133
+ width: 100%;
134
+ float: none;
135
+ padding-bottom: 0;
136
+ }
137
+ }
138
+
113
139
  .imageSideBySideSlideshow {
114
140
  width: 50%;
115
141
  height: calc(50vw*2/3 + 10px);
@@ -136,6 +162,22 @@
136
162
  }
137
163
  }
138
164
 
165
+ .imageSideBySidePortraitDatawrapper {
166
+ width: 50%;
167
+ height: 90vh;
168
+ min-height: calc(600px + 32px);
169
+ float: left;
170
+ padding: @s16;
171
+
172
+ @media (max-width: @lg) {
173
+ width: 100%;
174
+ float: none;
175
+ height: auto;
176
+ padding-bottom: 0;
177
+ min-height: unset;
178
+ }
179
+ }
180
+
139
181
  .imageSideBySidePortraitSlideshow {
140
182
  width: 50%;
141
183
  height: 90vh;