@eeacms/volto-eea-design-system 1.15.0 → 1.16.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/DEVELOP.md +56 -0
  3. package/README.md +18 -0
  4. package/docker-compose.yml +28 -0
  5. package/locales/de/LC_MESSAGES/volto.po +14 -0
  6. package/locales/it/LC_MESSAGES/volto.po +14 -0
  7. package/locales/ro/LC_MESSAGES/volto.po +14 -0
  8. package/locales/volto.pot +16 -0
  9. package/package.json +1 -1
  10. package/src/ui/Accordion/Accordion.stories.js +70 -25
  11. package/src/ui/Banner/Banner.jsx +9 -2
  12. package/src/ui/Card/Card.stories.jsx +4 -1
  13. package/src/ui/Header/Header.jsx +4 -0
  14. package/src/ui/Header/Header.stories.js +26 -4
  15. package/src/ui/Header/HeaderMenuPopUp.js +116 -103
  16. package/src/ui/Popup/Popup.jsx +84 -95
  17. package/src/ui/Popup/Popup.stories.jsx +1 -1
  18. package/theme/plugins.js +182 -0
  19. package/theme/themes/eea/elements/input.overrides +1 -1
  20. package/theme/themes/eea/extras/banner.less +0 -1
  21. package/theme/themes/eea/extras/custom.overrides +5 -0
  22. package/theme/themes/eea/extras/header.less +3 -22
  23. package/theme/themes/eea/extras/header.variables +1 -1
  24. package/theme/themes/eea/extras/tagList.less +1 -1
  25. package/theme/themes/eea/extras/tagList.variables +1 -1
  26. package/theme/themes/eea/globals/site.variables +3 -3
  27. package/theme/themes/eea/globals/utilities.less +9 -0
  28. package/theme/themes/eea/modules/accordion.overrides +44 -42
  29. package/theme/themes/eea/modules/accordion.variables +4 -4
  30. package/theme/themes/eea/modules/popup.overrides +4 -0
  31. package/theme/themes/eea/views/card.overrides +5 -1
  32. package/theme/themes/eea/views/card.variables +1 -0
  33. package/.i18n.babel.config.js +0 -1
@@ -1,13 +1,19 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { Transition } from 'semantic-ui-react';
3
- import { Container, Grid, List, Icon, Accordion } from 'semantic-ui-react';
1
+ import React, { useEffect, useState } from 'react';
2
+ import {
3
+ Accordion,
4
+ Container,
5
+ Grid,
6
+ Icon,
7
+ List,
8
+ Transition,
9
+ } from 'semantic-ui-react';
4
10
 
5
11
  import { cloneDeep } from 'lodash';
6
12
 
7
13
  import { useClickOutside } from '@eeacms/volto-eea-design-system/helpers';
8
14
 
9
15
  const createColumns = (item, renderMenuItem, item_id) => {
10
- const itemList = item.items.map((item, index) => (
16
+ return item.items.map((item, index) => (
11
17
  <React.Fragment key={index}>
12
18
  {renderMenuItem(item, {
13
19
  className: 'item',
@@ -16,16 +22,23 @@ const createColumns = (item, renderMenuItem, item_id) => {
16
22
  })}
17
23
  </React.Fragment>
18
24
  ));
19
- return itemList;
20
25
  };
21
26
 
22
- const ItemGrid = ({ sectionTitle, item, columns, renderMenuItem }) => {
27
+ const ItemGrid = ({
28
+ item,
29
+ columns,
30
+ renderMenuItem,
31
+ hideChildrenFromNavigation,
32
+ }) => {
23
33
  const item_id = item.title.toLowerCase().replaceAll(' ', '-') + '-sub-title';
24
34
  return (
25
35
  <>
26
36
  {renderMenuItem(item, { className: 'sub-title', id: item_id })}
27
- {item.items.length ? (
28
- <List aria-labelledby={item_id} style={{ columns: `${columns}` }}>
37
+ {item.items.length && !hideChildrenFromNavigation ? (
38
+ <List
39
+ aria-labelledby={item_id}
40
+ className={columns && columns > 1 ? `has--${columns}--columns` : ''}
41
+ >
29
42
  {createColumns(item, renderMenuItem, item_id)}
30
43
  </List>
31
44
  ) : null}
@@ -33,7 +46,13 @@ const ItemGrid = ({ sectionTitle, item, columns, renderMenuItem }) => {
33
46
  );
34
47
  };
35
48
 
36
- const Item = ({ item, icon = false, iconName, renderMenuItem }) => {
49
+ const Item = ({
50
+ item,
51
+ icon = false,
52
+ iconName,
53
+ renderMenuItem,
54
+ hideChildrenFromNavigation,
55
+ }) => {
37
56
  const item_id = item.title.toLowerCase().replaceAll(' ', '-') + '-sub-title';
38
57
  return (
39
58
  <>
@@ -41,97 +60,93 @@ const Item = ({ item, icon = false, iconName, renderMenuItem }) => {
41
60
  className: 'sub-title',
42
61
  id: item_id,
43
62
  })}
44
- <List className="menu-list" aria-labelledby={item_id}>
45
- {item.items.map((listItem, index) => (
46
- <React.Fragment key={index}>
47
- {renderMenuItem(
48
- listItem,
49
- {
50
- className: 'item',
51
- key: index,
52
- },
53
- { children: icon && <Icon className={iconName} /> },
54
- )}
55
- </React.Fragment>
56
- ))}
57
- </List>
63
+ {!hideChildrenFromNavigation && (
64
+ <List className="menu-list" aria-labelledby={item_id}>
65
+ {item.items.map((listItem, index) => (
66
+ <React.Fragment key={index}>
67
+ {renderMenuItem(
68
+ listItem,
69
+ {
70
+ className: 'item',
71
+ key: index,
72
+ },
73
+ { children: icon && <Icon className={iconName} /> },
74
+ )}
75
+ </React.Fragment>
76
+ ))}
77
+ </List>
78
+ )}
58
79
  </>
59
80
  );
60
81
  };
61
82
 
62
- const Topics = ({ menuItem, renderMenuItem }) => (
63
- <Grid>
64
- {menuItem.items.map((section, index) => (
65
- <React.Fragment key={index}>
66
- {section.title === 'At a glance' ? (
67
- <Grid.Column width={3} id="at-a-glance">
68
- <Item item={section} key={index} renderMenuItem={renderMenuItem} />
69
- </Grid.Column>
70
- ) : (
71
- <Grid.Column width={9} key={index} id="topics-right-column">
72
- <ItemGrid
73
- sectionTitle={section.title}
74
- item={section}
75
- columns={4}
76
- key={index}
77
- renderMenuItem={renderMenuItem}
78
- />
79
- </Grid.Column>
80
- )}
81
- </React.Fragment>
82
- ))}
83
- </Grid>
84
- );
83
+ const RenderItem = ({ layout, section, renderMenuItem, index }) => {
84
+ const hideChildrenFromNavigation =
85
+ layout.hideChildrenFromNavigation === undefined
86
+ ? true
87
+ : layout.hideChildrenFromNavigation;
88
+ return !layout.menuItemChildrenListColumns ||
89
+ layout.menuItemChildrenListColumns[index] === 1 ? (
90
+ <Item
91
+ item={section}
92
+ renderMenuItem={renderMenuItem}
93
+ hideChildrenFromNavigation={hideChildrenFromNavigation}
94
+ />
95
+ ) : (
96
+ <ItemGrid
97
+ item={section}
98
+ columns={layout.menuItemChildrenListColumns[index]}
99
+ renderMenuItem={renderMenuItem}
100
+ hideChildrenFromNavigation={hideChildrenFromNavigation}
101
+ />
102
+ );
103
+ };
104
+
105
+ export const StandardMegaMenuGrid = ({ menuItem, renderMenuItem, layout }) => {
106
+ const menuItemColumns = layout && layout.menuItemColumns;
107
+ const menuItemColumnsLength =
108
+ (menuItemColumns && menuItemColumns.length - 1) || 0;
109
+
110
+ const renderColumnContent = (section, columnIndex) => (
111
+ <RenderItem
112
+ layout={layout}
113
+ section={section}
114
+ renderMenuItem={renderMenuItem}
115
+ index={columnIndex}
116
+ />
117
+ );
85
118
 
86
- const Countries = ({ menuItem, renderMenuItem }) => (
87
- <Grid>
88
- <Grid.Column width={8}>
119
+ const renderColumns = () => (
120
+ <Grid>
121
+ {menuItemColumns.map((section, columnIndex) => (
122
+ <div className={layout.menuItemColumns[columnIndex]} key={columnIndex}>
123
+ {columnIndex !== menuItemColumnsLength
124
+ ? renderColumnContent(menuItem.items[columnIndex], columnIndex)
125
+ : menuItem.items
126
+ .slice(menuItemColumnsLength)
127
+ .map((section, _idx) =>
128
+ renderColumnContent(section, columnIndex),
129
+ )}
130
+ </div>
131
+ ))}
132
+ </Grid>
133
+ );
134
+
135
+ const renderDefaultColumns = () => (
136
+ <div className={layout?.gridContainerClass || 'ui four column grid'}>
89
137
  {menuItem.items.map((section, index) => (
90
- <React.Fragment key={index}>
91
- {index === 0 && (
92
- <ItemGrid
93
- sectionTitle={section.title}
94
- item={section}
95
- columns={5}
96
- renderMenuItem={renderMenuItem}
97
- />
98
- )}
99
- </React.Fragment>
138
+ <Grid.Column key={index}>
139
+ {renderColumnContent(section, index)}
140
+ </Grid.Column>
100
141
  ))}
101
- </Grid.Column>
102
- <Grid.Column width={4}>
103
- <Grid columns={1} className="nested-grid">
104
- {menuItem.items.map((section, index) => (
105
- <React.Fragment key={index}>
106
- {index !== 0 && (
107
- <Grid.Column>
108
- <ItemGrid
109
- sectionTitle={section.title}
110
- item={section}
111
- columns={2}
112
- renderMenuItem={renderMenuItem}
113
- />
114
- </Grid.Column>
115
- )}
116
- </React.Fragment>
117
- ))}
118
- </Grid>
119
- </Grid.Column>
120
- </Grid>
121
- );
142
+ </div>
143
+ );
122
144
 
123
- const StandardMegaMenuGrid = ({ menuItem, renderMenuItem }) => (
124
- <Grid columns={4}>
125
- {menuItem.items.map((section, index) => (
126
- <Grid.Column key={index}>
127
- <Item item={section} renderMenuItem={renderMenuItem} />
128
- </Grid.Column>
129
- ))}
130
- </Grid>
131
- );
145
+ return menuItemColumns ? renderColumns() : renderDefaultColumns();
146
+ };
132
147
 
133
148
  const FirstLevelContent = ({ element, renderMenuItem, pathName }) => {
134
- const topics = element.title === 'Topics' ? true : false;
149
+ const topics = element.title === 'Topics';
135
150
  let defaultIndex = -1;
136
151
 
137
152
  return (
@@ -308,6 +323,7 @@ const NestedAccordion = ({ menuItems, renderMenuItem, pathName }) => {
308
323
 
309
324
  function HeaderMenuPopUp({
310
325
  menuItems,
326
+ menuItemsLayouts,
311
327
  renderMenuItem,
312
328
  pathName,
313
329
  onClose,
@@ -322,6 +338,11 @@ function HeaderMenuPopUp({
322
338
  (current) => current.url === activeItem || current['@id'] === activeItem,
323
339
  );
324
340
 
341
+ const layout =
342
+ !!menuItemsLayouts &&
343
+ Object.keys(menuItemsLayouts).includes(menuItem?.url) &&
344
+ menuItemsLayouts[menuItem.url];
345
+
325
346
  return (
326
347
  <Transition visible={visible} animation="slide down" duration={300}>
327
348
  <div id="mega-menu" ref={nodeRef}>
@@ -351,19 +372,11 @@ function HeaderMenuPopUp({
351
372
  )}
352
373
  </div>
353
374
  )}
354
- {menuItem.title === 'Topics' ? (
355
- <Topics menuItem={menuItem} renderMenuItem={renderMenuItem} />
356
- ) : menuItem.title === 'Countries' ? (
357
- <Countries
358
- menuItem={menuItem}
359
- renderMenuItem={renderMenuItem}
360
- />
361
- ) : (
362
- <StandardMegaMenuGrid
363
- menuItem={menuItem}
364
- renderMenuItem={renderMenuItem}
365
- />
366
- )}
375
+ <StandardMegaMenuGrid
376
+ menuItem={menuItem}
377
+ renderMenuItem={renderMenuItem}
378
+ layout={layout}
379
+ />
367
380
  </div>
368
381
  )}
369
382
  <div className="tablet only mobile only">
@@ -1,8 +1,5 @@
1
- /* Simplified popup with several options found in the semantic ui implementation
2
- * https://github.com/Semantic-Org/Semantic-UI-React/blob/master/src/modules/Popup/Popup.js
3
- * */
4
- import React from 'react';
5
- import { createPopper } from '@popperjs/core';
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { usePopper } from 'react-popper';
6
3
  import EventStack from '@semantic-ui-react/event-stack';
7
4
  import cx from 'classnames';
8
5
 
@@ -10,106 +7,106 @@ export const positionsMapping = {
10
7
  'top center': 'top',
11
8
  'top left': 'top-start',
12
9
  'top right': 'top-end',
13
-
14
10
  'bottom center': 'bottom',
15
11
  'bottom left': 'bottom-start',
16
12
  'bottom right': 'bottom-end',
17
-
18
13
  'right center': 'right',
19
14
  'left center': 'left',
20
15
  };
21
16
 
22
- class Popup extends React.Component {
23
- constructor(props) {
24
- super(props);
17
+ function Popup(props) {
18
+ const triggerRef = useRef(null);
19
+ const popupRef = useRef(null);
20
+ const [isOpen, setIsOpen] = useState(false);
25
21
 
26
- this.triggerRef = React.createRef();
27
- this.popupRef = React.createRef();
22
+ const options = {
23
+ placement: positionsMapping[props.position] || 'bottom-end',
24
+ strategy: props.positionFixed || 'absolute',
25
+ modifiers: [
26
+ {
27
+ name: 'offset',
28
+ options: {
29
+ offset: props.offset,
30
+ },
31
+ },
32
+ ...props.popperModifiers,
33
+ ],
34
+ };
28
35
 
29
- this.state = {
30
- isOpen: false,
31
- };
36
+ const { styles, attributes } = usePopper(
37
+ triggerRef.current,
38
+ popupRef.current,
39
+ options,
40
+ );
32
41
 
33
- this.togglePopup = this.togglePopup.bind(this);
34
- this.closeOnEscape = this.closeOnEscape.bind(this);
35
- }
42
+ const handleClickOutside = (event) => {
43
+ if (
44
+ popupRef.current &&
45
+ !popupRef.current.contains(event.target) &&
46
+ !triggerRef.current.contains(event.target)
47
+ ) {
48
+ if (isOpen) {
49
+ setIsOpen(false);
50
+ }
51
+ }
52
+ };
36
53
 
37
- componentDidMount() {
38
- this.popper = createPopper(this.triggerRef.current, this.popupRef.current, {
39
- placement: positionsMapping[this.props.position] || 'bottom-end',
40
- strategy: this.props.positionFixed || 'absolute',
41
- modifiers: [
42
- {
43
- name: 'offset',
44
- options: {
45
- offset: this.props.offset,
46
- },
47
- },
48
- ...this.props.popperModifiers,
49
- ],
50
- });
51
- }
54
+ useEffect(() => {
55
+ document.addEventListener('mousedown', handleClickOutside);
56
+ return () => {
57
+ document.removeEventListener('mousedown', handleClickOutside);
58
+ };
59
+ // eslint-disable-next-line react-hooks/exhaustive-deps
60
+ }, [isOpen]);
52
61
 
53
- componentWillUnmount() {
54
- this.popper && this.popper.destroy();
55
- }
62
+ const togglePopup = () => {
63
+ setIsOpen((prevIsOpen) => !prevIsOpen);
64
+ };
56
65
 
57
- togglePopup() {
58
- this.setState(
59
- (state) => {
60
- return {
61
- isOpen: !state.isOpen,
62
- };
63
- },
64
- () => {
65
- this.popper.forceUpdate();
66
- },
67
- );
68
- }
69
- closeOnEscape(e) {
66
+ const closeOnEscape = (e) => {
70
67
  if (e.key === 'Escape') {
71
- this.setState((state) => {
72
- return {
73
- isOpen: !state.isOpen,
74
- };
75
- });
68
+ setIsOpen((prevIsOpen) => !prevIsOpen);
76
69
  }
77
- }
70
+ };
78
71
 
79
- render() {
80
- const { trigger, className, size, position, basic, content } = this.props;
81
- const event = this.props.on;
82
- const onEvent = 'on' + event.charAt(0).toUpperCase() + event.slice(1);
83
- return (
84
- <React.Fragment>
85
- {trigger &&
86
- React.cloneElement(trigger, {
87
- [onEvent]: this.togglePopup,
88
- ref: this.triggerRef,
89
- })}
72
+ const { trigger, className, size, position, basic, content, on } = props;
90
73
 
91
- <div className="popup-container" ref={this.popupRef}>
92
- {this.state.isOpen && (
93
- <EventStack name="keydown" on={this.closeOnEscape} />
94
- )}
95
- <React.Fragment>
96
- <div
97
- className={cx(
98
- 'ui popup transition',
99
- className,
100
- size,
101
- position,
102
- basic ? 'basic' : '',
103
- this.state.isOpen ? 'visible' : '',
104
- )}
105
- >
106
- {content}
107
- </div>
108
- </React.Fragment>
74
+ const onEvent = 'on' + on.charAt(0).toUpperCase() + on.slice(1);
75
+
76
+ return (
77
+ <React.Fragment>
78
+ {trigger && (
79
+ <div className="popup-trigger" ref={triggerRef}>
80
+ {React.cloneElement(trigger, {
81
+ [onEvent]: togglePopup,
82
+ })}
109
83
  </div>
110
- </React.Fragment>
111
- );
112
- }
84
+ )}
85
+
86
+ <div
87
+ className="popup-container"
88
+ ref={popupRef}
89
+ style={styles.popper}
90
+ {...attributes.popper}
91
+ >
92
+ {isOpen && <EventStack name="keydown" on={closeOnEscape} />}
93
+ <React.Fragment>
94
+ <div
95
+ className={cx(
96
+ 'ui popup transition',
97
+ className,
98
+ size,
99
+ position,
100
+ basic ? 'basic' : '',
101
+ isOpen ? 'visible' : '',
102
+ )}
103
+ >
104
+ {content}
105
+ </div>
106
+ </React.Fragment>
107
+ </div>
108
+ </React.Fragment>
109
+ );
113
110
  }
114
111
 
115
112
  Popup.defaultProps = {
@@ -122,14 +119,6 @@ Popup.defaultProps = {
122
119
  wide: false,
123
120
  on: 'click',
124
121
  popperModifiers: [],
125
- // disabled,
126
- // flowing,
127
- // header,
128
- // inverted,
129
- // pinned,
130
- // popper,
131
- // popperDependencies,
132
- // style,
133
122
  };
134
123
 
135
124
  export default Popup;
@@ -59,7 +59,7 @@ export default {
59
59
  };
60
60
 
61
61
  export const ButtonPopup = (args) => (
62
- <Segment placeholder>
62
+ <Segment placeholder className={'flex-row flex-items-center'}>
63
63
  <Popup
64
64
  {...args}
65
65
  trigger={
package/theme/plugins.js CHANGED
@@ -1,5 +1,187 @@
1
+ function toHSL(color) {
2
+ if (color.toHSL) {
3
+ return color.toHSL();
4
+ } else {
5
+ throw new Error('Argument cannot be evaluated to a color');
6
+ }
7
+ }
8
+
1
9
  module.exports = {
2
10
  install(less, pluginManager, functions) {
11
+ const darken = less.functions.functionRegistry.get('darken');
12
+ const lighten = less.functions.functionRegistry.get('lighten');
13
+ const saturate = less.functions.functionRegistry.get('saturate');
14
+ const desaturate = less.functions.functionRegistry.get('desaturate');
15
+
16
+ functions.add('darken', function (
17
+ color,
18
+ amount = new less.tree.Dimension(0),
19
+ method,
20
+ ) {
21
+ try {
22
+ const hsl = toHSL(color);
23
+
24
+ if (hsl) {
25
+ return darken(color, amount, method);
26
+ }
27
+ } catch (err) {
28
+ if (['hsl', 'hsla'].includes(color.name)) {
29
+ if (amount?.value) {
30
+ color.args[2] = new less.tree.Call('calc', [
31
+ new less.tree.Operation(
32
+ '-',
33
+ [
34
+ color.args[2],
35
+ new less.tree.Dimension(
36
+ amount.value,
37
+ new less.tree.Unit(null, null, '%'),
38
+ ),
39
+ ],
40
+ true,
41
+ ),
42
+ ]);
43
+ }
44
+
45
+ return color;
46
+ } else if (color.name === 'var') {
47
+ if (color.args[1]) {
48
+ color.args[1] = functions.get('darken')(
49
+ color.args[1],
50
+ amount,
51
+ method,
52
+ );
53
+ }
54
+ return color;
55
+ } else {
56
+ throw err;
57
+ }
58
+ }
59
+ });
60
+
61
+ functions.add('lighten', function (color, amount, method) {
62
+ try {
63
+ const hsl = toHSL(color);
64
+
65
+ if (hsl) {
66
+ return lighten(color, amount, method);
67
+ }
68
+ } catch (err) {
69
+ if (['hsl', 'hsla'].includes(color.name)) {
70
+ if (amount?.value) {
71
+ color.args[2] = new less.tree.Call('calc', [
72
+ new less.tree.Operation(
73
+ '+',
74
+ [
75
+ color.args[2],
76
+ new less.tree.Dimension(
77
+ amount.value,
78
+ new less.tree.Unit(null, null, '%'),
79
+ ),
80
+ ],
81
+ true,
82
+ ),
83
+ ]);
84
+ }
85
+
86
+ return color;
87
+ } else if (color.name === 'var') {
88
+ if (color.args[1]) {
89
+ color.args[1] = functions.get('lighten')(
90
+ color.args[1],
91
+ amount,
92
+ method,
93
+ );
94
+ }
95
+ return color;
96
+ } else {
97
+ throw err;
98
+ }
99
+ }
100
+ });
101
+
102
+ functions.add('saturate', function (color, amount, method) {
103
+ try {
104
+ const hsl = toHSL(color);
105
+
106
+ if (hsl) {
107
+ return saturate(color, amount, method);
108
+ }
109
+ } catch (err) {
110
+ if (['hsl', 'hsla'].includes(color.name)) {
111
+ if (amount?.value) {
112
+ color.args[1] = new less.tree.Call('calc', [
113
+ new less.tree.Operation(
114
+ '+',
115
+ [
116
+ color.args[1],
117
+ new less.tree.Dimension(
118
+ amount.value,
119
+ new less.tree.Unit(null, null, '%'),
120
+ ),
121
+ ],
122
+ true,
123
+ ),
124
+ ]);
125
+ }
126
+
127
+ return color;
128
+ } else if (!color.rgb || color.name === 'var') {
129
+ if (color.name === 'var' && color.args[1]) {
130
+ color.args[1] = functions.get('saturate')(
131
+ color.args[1],
132
+ amount,
133
+ method,
134
+ );
135
+ return color;
136
+ }
137
+ return null;
138
+ } else {
139
+ throw err;
140
+ }
141
+ }
142
+ });
143
+
144
+ functions.add('desaturate', function (color, amount, method) {
145
+ try {
146
+ const hsl = toHSL(color);
147
+
148
+ if (hsl) {
149
+ return desaturate(color, amount, method);
150
+ }
151
+ } catch (err) {
152
+ if (['hsl', 'hsla'].includes(color.name)) {
153
+ if (amount?.value) {
154
+ color.args[1] = new less.tree.Call('calc', [
155
+ new less.tree.Operation(
156
+ '-',
157
+ [
158
+ color.args[1],
159
+ new less.tree.Dimension(
160
+ amount.value,
161
+ new less.tree.Unit(null, null, '%'),
162
+ ),
163
+ ],
164
+ true,
165
+ ),
166
+ ]);
167
+ }
168
+
169
+ return color;
170
+ } else if (color.name === 'var') {
171
+ if (color.args[1]) {
172
+ color.args[1] = functions.get('desaturate')(
173
+ color.args[1],
174
+ amount,
175
+ method,
176
+ );
177
+ }
178
+ return color;
179
+ } else {
180
+ throw err;
181
+ }
182
+ }
183
+ });
184
+
3
185
  functions.add('vw', function (percentage) {
4
186
  return new less.tree.Call('calc', [
5
187
  new less.tree.Operation(