@skyscanner/backpack-web 3.3.0 → 4.0.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 (79) hide show
  1. package/bpk-animate-height/package.json +2 -2
  2. package/bpk-component-accordion/package.json +6 -6
  3. package/bpk-component-aria-live/package.json +7 -7
  4. package/bpk-component-autosuggest/package.json +4 -4
  5. package/bpk-component-badge/package.json +4 -4
  6. package/bpk-component-banner-alert/package.json +8 -8
  7. package/bpk-component-barchart/package.json +7 -7
  8. package/bpk-component-blockquote/package.json +2 -2
  9. package/bpk-component-breadcrumb/package.json +5 -5
  10. package/bpk-component-breakpoint/package.json +2 -2
  11. package/bpk-component-button/package.json +2 -2
  12. package/bpk-component-calendar/package.json +8 -8
  13. package/bpk-component-card/package.json +4 -4
  14. package/bpk-component-checkbox/package.json +4 -4
  15. package/bpk-component-chip/package.json +5 -5
  16. package/bpk-component-close-button/package.json +4 -4
  17. package/bpk-component-code/package.json +2 -2
  18. package/bpk-component-content-container/package.json +2 -2
  19. package/bpk-component-datatable/package.json +3 -3
  20. package/bpk-component-datepicker/package.json +8 -8
  21. package/bpk-component-description-list/package.json +2 -2
  22. package/bpk-component-dialog/package.json +8 -8
  23. package/bpk-component-drawer/package.json +8 -8
  24. package/bpk-component-fieldset/package.json +12 -12
  25. package/bpk-component-flare/package.json +4 -4
  26. package/bpk-component-floating-notification/package.json +4 -4
  27. package/bpk-component-form-validation/package.json +9 -9
  28. package/bpk-component-graphic-promotion/package.json +4 -5
  29. package/bpk-component-graphic-promotion/src/BpkGraphicPromo.js +22 -6
  30. package/bpk-component-graphic-promotion/src/BpkGraphicPromo.module.scss +4 -0
  31. package/bpk-component-grid/package.json +2 -2
  32. package/bpk-component-grid-toggle/package.json +3 -3
  33. package/bpk-component-horizontal-nav/package.json +5 -5
  34. package/bpk-component-icon/package.json +5 -5
  35. package/bpk-component-image/package.json +5 -5
  36. package/bpk-component-infinite-scroll/package.json +6 -6
  37. package/bpk-component-input/package.json +6 -6
  38. package/bpk-component-label/package.json +2 -2
  39. package/bpk-component-link/package.json +3 -3
  40. package/bpk-component-list/package.json +2 -2
  41. package/bpk-component-loading-button/package.json +6 -6
  42. package/bpk-component-map/package.json +6 -6
  43. package/bpk-component-mobile-scroll-container/package.json +2 -2
  44. package/bpk-component-modal/package.json +9 -9
  45. package/bpk-component-navigation-bar/package.json +7 -7
  46. package/bpk-component-navigation-stack/package.json +6 -6
  47. package/bpk-component-nudger/package.json +6 -6
  48. package/bpk-component-overlay/package.json +4 -4
  49. package/bpk-component-pagination/package.json +4 -4
  50. package/bpk-component-panel/package.json +2 -2
  51. package/bpk-component-phone-input/package.json +8 -8
  52. package/bpk-component-popover/package.json +8 -8
  53. package/bpk-component-popover/src/BpkPopoverPortal.js +3 -3
  54. package/bpk-component-price/package.json +3 -3
  55. package/bpk-component-progress/package.json +4 -4
  56. package/bpk-component-radio/package.json +3 -3
  57. package/bpk-component-rating/package.json +3 -3
  58. package/bpk-component-rtl-toggle/package.json +3 -3
  59. package/bpk-component-scrollable-calendar/package.json +5 -5
  60. package/bpk-component-section-list/package.json +5 -5
  61. package/bpk-component-select/package.json +3 -3
  62. package/bpk-component-skip-link/package.json +2 -2
  63. package/bpk-component-slider/package.json +3 -3
  64. package/bpk-component-spinner/package.json +2 -2
  65. package/bpk-component-star-rating/package.json +5 -5
  66. package/bpk-component-switch/package.json +2 -2
  67. package/bpk-component-table/package.json +2 -2
  68. package/bpk-component-text/package.json +2 -2
  69. package/bpk-component-textarea/package.json +4 -4
  70. package/bpk-component-theme-toggle/package.json +4 -4
  71. package/bpk-component-ticket/package.json +4 -4
  72. package/bpk-component-tooltip/package.json +3 -3
  73. package/bpk-component-tooltip/src/BpkTooltipPortal.js +3 -3
  74. package/bpk-react-utils/index.js +3 -0
  75. package/bpk-react-utils/package.json +1 -1
  76. package/bpk-react-utils/src/Portal-v1.js +310 -0
  77. package/bpk-react-utils/src/Portal.js +42 -54
  78. package/bpk-scrim-utils/package.json +2 -2
  79. package/package.json +1 -1
@@ -20,6 +20,7 @@
20
20
 
21
21
  import wrapDisplayName from './src/wrapDisplayName';
22
22
  import Portal from './src/Portal';
23
+ import PortalV1 from './src/Portal-v1';
23
24
  import TransitionInitialMount from './src/TransitionInitialMount';
24
25
  import cssModules from './src/cssModules';
25
26
  import deprecated from './src/deprecated';
@@ -33,6 +34,7 @@ import isRTL from './src/isRTL';
33
34
 
34
35
  export {
35
36
  Portal,
37
+ PortalV1,
36
38
  TransitionInitialMount,
37
39
  cssModules,
38
40
  deprecated,
@@ -45,6 +47,7 @@ export {
45
47
  };
46
48
  export default {
47
49
  Portal,
50
+ PortalV1,
48
51
  TransitionInitialMount,
49
52
  cssModules,
50
53
  deprecated,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bpk-react-utils",
3
- "version": "4.1.2",
3
+ "version": "5.0.0",
4
4
  "description": "Utilities for Backpack's React components.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -0,0 +1,310 @@
1
+ /*
2
+ * Backpack - Skyscanner's Design System
3
+ *
4
+ * Copyright 2016-2021 Skyscanner Ltd
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */
18
+
19
+ import {
20
+ unstable_renderSubtreeIntoContainer, // eslint-disable-line camelcase
21
+ unmountComponentAtNode,
22
+ findDOMNode,
23
+ } from 'react-dom';
24
+ import { Component } from 'react';
25
+ import assign from 'object-assign';
26
+ import PropTypes from 'prop-types';
27
+
28
+ const KEYCODES = {
29
+ ESCAPE: 'Escape',
30
+ };
31
+
32
+ class PortalV1 extends Component {
33
+ constructor() {
34
+ super();
35
+
36
+ this.portalElement = null;
37
+
38
+ // shouldClose is used to keep track of the user's mouse-down events in order to
39
+ // prevent the dialog closing if the mouse leaves / enters the portal during the click
40
+ this.shouldClose = false;
41
+
42
+ this.close = this.close.bind(this);
43
+ this.onDocumentMouseUp = this.onDocumentMouseUp.bind(this);
44
+ this.onDocumentMouseDown = this.onDocumentMouseDown.bind(this);
45
+ this.onDocumentKeyDown = this.onDocumentKeyDown.bind(this);
46
+ this.onDocumentMouseMove = this.onDocumentMouseMove.bind(this);
47
+ this.getClickEventProperties = this.getClickEventProperties.bind(this);
48
+ this.supportsPassiveEvents = this.supportsPassiveEvents.bind(this);
49
+ }
50
+
51
+ componentDidMount() {
52
+ if (this.props.isOpen) {
53
+ this.open();
54
+ }
55
+
56
+ if (this.props.targetRef) {
57
+ this.props.targetRef(this.getTargetElement());
58
+ }
59
+ }
60
+
61
+ // When the consumer updates `isOpen` to be `false`, we
62
+ // call their beforeClose function so that they can trigger the close.
63
+ // If they don't provide `beforeClose` we just call `close` directly.
64
+ componentDidUpdate(prevProps) {
65
+ if (this.props.isOpen) {
66
+ if (!prevProps.isOpen) {
67
+ this.open();
68
+ return;
69
+ }
70
+ } else if (prevProps.isOpen) {
71
+ if (this.props.beforeClose) {
72
+ this.props.beforeClose(this.close);
73
+ } else {
74
+ this.close();
75
+ }
76
+ return;
77
+ }
78
+
79
+ this.renderPortal();
80
+ }
81
+
82
+ componentWillUnmount() {
83
+ this.close();
84
+ }
85
+
86
+ onDocumentMouseDown(event) {
87
+ const clickEventProperties = this.getClickEventProperties(event);
88
+ if (
89
+ clickEventProperties.isNotLeftClick ||
90
+ clickEventProperties.isTargetClick ||
91
+ clickEventProperties.isPortalClick
92
+ ) {
93
+ this.shouldClose = false;
94
+ return;
95
+ }
96
+
97
+ this.shouldClose = true;
98
+ }
99
+
100
+ onDocumentMouseUp(event) {
101
+ const clickEventProperties = this.getClickEventProperties(event);
102
+
103
+ if (
104
+ clickEventProperties.isNotLeftClick ||
105
+ clickEventProperties.isTargetClick ||
106
+ clickEventProperties.isPortalClick
107
+ ) {
108
+ this.shouldClose = false;
109
+ return;
110
+ }
111
+
112
+ if (this.shouldClose) {
113
+ // `onClose` tells the consumer that they should change `isOpen` to false.
114
+ // Once the consumer has responded to `onClose`, `beforeClose` and `close` will be called.
115
+ this.props.onClose(event, { source: 'DOCUMENT_CLICK' });
116
+ }
117
+ }
118
+
119
+ onDocumentKeyDown(event) {
120
+ if (
121
+ event.key === KEYCODES.ESCAPE &&
122
+ this.props.isOpen &&
123
+ this.props.closeOnEscPressed
124
+ ) {
125
+ // `onClose` tells the consumer that they should change `isOpen` to false.
126
+ // Once the consumer has responded to `onClose`, `beforeClose` and `close` will be called.
127
+ this.props.onClose(event, { source: 'ESCAPE' });
128
+ }
129
+ }
130
+
131
+ onDocumentMouseMove() {
132
+ this.shouldClose = false;
133
+ }
134
+
135
+ getClickEventProperties(event) {
136
+ const isNotLeftClick = event.button && event.button !== 0;
137
+
138
+ const targetElement = this.getTargetElement();
139
+ const isTargetClick =
140
+ targetElement &&
141
+ (event.target === targetElement || targetElement.contains(event.target));
142
+
143
+ const isPortalClick =
144
+ this.portalElement &&
145
+ (event.target === this.portalElement ||
146
+ this.portalElement.contains(event.target));
147
+
148
+ return {
149
+ isNotLeftClick,
150
+ isTargetClick,
151
+ isPortalClick,
152
+ };
153
+ }
154
+
155
+ getTargetElement() {
156
+ if (typeof this.props.target === 'function') {
157
+ return this.props.target();
158
+ }
159
+
160
+ // Whilst findDOMNode is planned for deprecation in a future implementation of react, since this is the only usage
161
+ // in backpack, we have decided to ignore this instance as it'll be deleted in favour of React 16 first class portal
162
+ // implementation (see https://reactjs.org/docs/portals.html).
163
+ // eslint-disable-next-line react/no-find-dom-node
164
+ return this.props.target && findDOMNode(this);
165
+ }
166
+
167
+ getRenderTarget() {
168
+ if (typeof this.props.renderTarget === 'function') {
169
+ return this.props.renderTarget();
170
+ }
171
+
172
+ return document.body;
173
+ }
174
+
175
+ open() {
176
+ if (this.portalElement) {
177
+ return;
178
+ }
179
+
180
+ this.portalElement = document.createElement('div');
181
+ this.getRenderTarget().appendChild(this.portalElement);
182
+
183
+ const passiveArgs = this.supportsPassiveEvents()
184
+ ? { passive: true }
185
+ : false;
186
+ document.addEventListener(
187
+ 'touchstart',
188
+ this.onDocumentMouseDown,
189
+ passiveArgs,
190
+ );
191
+ document.addEventListener(
192
+ 'touchmove',
193
+ this.onDocumentMouseMove,
194
+ passiveArgs,
195
+ );
196
+ document.addEventListener('touchend', this.onDocumentMouseUp, passiveArgs);
197
+ document.addEventListener('mousedown', this.onDocumentMouseDown, false);
198
+ document.addEventListener('mouseup', this.onDocumentMouseUp, false);
199
+ document.addEventListener('keydown', this.onDocumentKeyDown, false);
200
+
201
+ if (this.props.style) {
202
+ assign(this.portalElement.style, this.props.style);
203
+ }
204
+
205
+ if (this.props.className) {
206
+ this.portalElement.className = this.props.className;
207
+ }
208
+
209
+ this.renderPortal(() => {
210
+ this.props.onOpen(this.portalElement, this.getTargetElement());
211
+ });
212
+ }
213
+
214
+ close() {
215
+ if (!this.portalElement) {
216
+ return;
217
+ }
218
+
219
+ unmountComponentAtNode(this.portalElement);
220
+
221
+ const renderTarget = this.getRenderTarget();
222
+ if (renderTarget) {
223
+ renderTarget.removeChild(this.portalElement);
224
+ }
225
+
226
+ document.removeEventListener('touchstart', this.onDocumentMouseDown);
227
+ document.removeEventListener('touchmove', this.onDocumentMouseMove);
228
+ document.removeEventListener('touchend', this.onDocumentMouseUp);
229
+ document.removeEventListener('mousedown', this.onDocumentMouseDown);
230
+ document.removeEventListener('mouseup', this.onDocumentMouseUp);
231
+ document.removeEventListener('keydown', this.onDocumentKeyDown);
232
+
233
+ this.portalElement = null;
234
+ }
235
+
236
+ // This function is taken from modernizr
237
+ // See https://github.com/modernizr/modernizr
238
+ // eslint-disable-next-line class-methods-use-this
239
+ supportsPassiveEvents() {
240
+ let supportsPassiveOption = false;
241
+ try {
242
+ const opts = Object.defineProperty({}, 'passive', {
243
+ // eslint-disable-next-line getter-return
244
+ get() {
245
+ supportsPassiveOption = true;
246
+ },
247
+ });
248
+ window.addEventListener('test', null, opts);
249
+ } catch (e) {
250
+ return false;
251
+ }
252
+ return supportsPassiveOption;
253
+ }
254
+
255
+ renderPortal(cb = () => {}) {
256
+ // If the `target` prop is null, it's fine that there is no targetElement
257
+ // Otherwise, if a `target` is provided, we don't render if we cannot find the respective element
258
+ const missesExpectedTarget = this.props.target && !this.getTargetElement();
259
+
260
+ if (this.portalElement && !missesExpectedTarget) {
261
+ unstable_renderSubtreeIntoContainer(
262
+ this,
263
+ this.props.children,
264
+ this.portalElement,
265
+ () => {
266
+ if (this.props.isOpen) {
267
+ this.props.onRender(this.portalElement, this.getTargetElement());
268
+ }
269
+ cb();
270
+ },
271
+ );
272
+ } else {
273
+ setImmediate(cb);
274
+ }
275
+ }
276
+
277
+ render() {
278
+ return typeof this.props.target === 'function' ? null : this.props.target;
279
+ }
280
+ }
281
+
282
+ PortalV1.propTypes = {
283
+ children: PropTypes.node.isRequired,
284
+ isOpen: PropTypes.bool.isRequired,
285
+ beforeClose: PropTypes.func,
286
+ className: PropTypes.string,
287
+ onClose: PropTypes.func,
288
+ onOpen: PropTypes.func,
289
+ onRender: PropTypes.func,
290
+ style: PropTypes.object, // eslint-disable-line react/forbid-prop-types
291
+ renderTarget: PropTypes.func,
292
+ target: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
293
+ targetRef: PropTypes.func,
294
+ closeOnEscPressed: PropTypes.bool,
295
+ };
296
+
297
+ PortalV1.defaultProps = {
298
+ beforeClose: null,
299
+ className: null,
300
+ onClose: () => null,
301
+ onOpen: () => null,
302
+ onRender: () => null,
303
+ style: null,
304
+ renderTarget: null,
305
+ target: null,
306
+ targetRef: null,
307
+ closeOnEscPressed: true,
308
+ };
309
+
310
+ export default PortalV1;
@@ -16,17 +16,13 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
- import {
20
- unstable_renderSubtreeIntoContainer, // eslint-disable-line camelcase
21
- unmountComponentAtNode,
22
- findDOMNode,
23
- } from 'react-dom';
24
19
  import { Component } from 'react';
20
+ import { createPortal } from 'react-dom';
25
21
  import assign from 'object-assign';
26
22
  import PropTypes from 'prop-types';
27
23
 
28
24
  const KEYCODES = {
29
- ESCAPE: 27,
25
+ ESCAPE: 'Escape',
30
26
  };
31
27
 
32
28
  class Portal extends Component {
@@ -35,6 +31,10 @@ class Portal extends Component {
35
31
 
36
32
  this.portalElement = null;
37
33
 
34
+ this.state = {
35
+ isVisible: false,
36
+ };
37
+
38
38
  // shouldClose is used to keep track of the user's mouse-down events in order to
39
39
  // prevent the dialog closing if the mouse leaves / enters the portal during the click
40
40
  this.shouldClose = false;
@@ -65,7 +65,8 @@ class Portal extends Component {
65
65
  if (this.props.isOpen) {
66
66
  if (!prevProps.isOpen) {
67
67
  this.open();
68
- return;
68
+ } else {
69
+ this.props.onRender(this.portalElement, this.getTargetElement());
69
70
  }
70
71
  } else if (prevProps.isOpen) {
71
72
  if (this.props.beforeClose) {
@@ -73,10 +74,7 @@ class Portal extends Component {
73
74
  } else {
74
75
  this.close();
75
76
  }
76
- return;
77
77
  }
78
-
79
- this.renderPortal();
80
78
  }
81
79
 
82
80
  componentWillUnmount() {
@@ -118,7 +116,7 @@ class Portal extends Component {
118
116
 
119
117
  onDocumentKeyDown(event) {
120
118
  if (
121
- event.keyCode === KEYCODES.ESCAPE &&
119
+ event.key === KEYCODES.ESCAPE &&
122
120
  this.props.isOpen &&
123
121
  this.props.closeOnEscPressed
124
122
  ) {
@@ -138,38 +136,42 @@ class Portal extends Component {
138
136
  const targetElement = this.getTargetElement();
139
137
  const isTargetClick =
140
138
  targetElement &&
139
+ event.target instanceof Node &&
141
140
  (event.target === targetElement || targetElement.contains(event.target));
142
141
 
143
142
  const isPortalClick =
144
143
  this.portalElement &&
144
+ event.target instanceof Node &&
145
145
  (event.target === this.portalElement ||
146
146
  this.portalElement.contains(event.target));
147
147
 
148
148
  return {
149
- isNotLeftClick,
150
- isTargetClick,
151
- isPortalClick,
149
+ isNotLeftClick: !!isNotLeftClick,
150
+ isTargetClick: !!isTargetClick,
151
+ isPortalClick: !!isPortalClick,
152
152
  };
153
153
  }
154
154
 
155
155
  getTargetElement() {
156
- if (typeof this.props.target === 'function') {
157
- return this.props.target();
158
- }
159
-
160
- // Whilst findDOMNode is planned for deprecation in a future implementation of react, since this is the only usage
161
- // in backpack, we have decided to ignore this instance as it'll be deleted in favour of React 16 first class portal
162
- // implementation (see https://reactjs.org/docs/portals.html).
163
- // eslint-disable-next-line react/no-find-dom-node
164
- return this.props.target && findDOMNode(this);
156
+ return typeof this.props.target === 'function'
157
+ ? this.props.target()
158
+ : this.props.target;
165
159
  }
166
160
 
167
161
  getRenderTarget() {
168
- if (typeof this.props.renderTarget === 'function') {
169
- return this.props.renderTarget();
162
+ const target =
163
+ typeof this.props.renderTarget === 'function'
164
+ ? this.props.renderTarget()
165
+ : this.props.renderTarget;
166
+
167
+ if (target) {
168
+ return target;
169
+ }
170
+ if (document.body) {
171
+ return document.body;
170
172
  }
171
173
 
172
- return document.body;
174
+ throw new Error('Render target and fallback unavailable');
173
175
  }
174
176
 
175
177
  open() {
@@ -205,10 +207,9 @@ class Portal extends Component {
205
207
  if (this.props.className) {
206
208
  this.portalElement.className = this.props.className;
207
209
  }
208
-
209
- this.renderPortal(() => {
210
- this.props.onOpen(this.portalElement, this.getTargetElement());
211
- });
210
+ this.setState({ isVisible: true }, () =>
211
+ this.props.onOpen(this.portalElement),
212
+ );
212
213
  }
213
214
 
214
215
  close() {
@@ -216,8 +217,6 @@ class Portal extends Component {
216
217
  return;
217
218
  }
218
219
 
219
- unmountComponentAtNode(this.portalElement);
220
-
221
220
  const renderTarget = this.getRenderTarget();
222
221
  if (renderTarget) {
223
222
  renderTarget.removeChild(this.portalElement);
@@ -231,6 +230,7 @@ class Portal extends Component {
231
230
  document.removeEventListener('keydown', this.onDocumentKeyDown);
232
231
 
233
232
  this.portalElement = null;
233
+ this.setState({ isVisible: false });
234
234
  }
235
235
 
236
236
  // This function is taken from modernizr
@@ -252,30 +252,18 @@ class Portal extends Component {
252
252
  return supportsPassiveOption;
253
253
  }
254
254
 
255
- renderPortal(cb = () => {}) {
256
- // If the `target` prop is null, it's fine that there is no targetElement
257
- // Otherwise, if a `target` is provided, we don't render if we cannot find the respective element
258
- const missesExpectedTarget = this.props.target && !this.getTargetElement();
259
-
260
- if (this.portalElement && !missesExpectedTarget) {
261
- unstable_renderSubtreeIntoContainer(
262
- this,
263
- this.props.children,
264
- this.portalElement,
265
- () => {
266
- if (this.props.isOpen) {
267
- this.props.onRender(this.portalElement, this.getTargetElement());
268
- }
269
- cb();
270
- },
271
- );
272
- } else {
273
- setImmediate(cb);
255
+ render() {
256
+ const {
257
+ portalElement,
258
+ props: { children },
259
+ state: { isVisible },
260
+ } = this;
261
+
262
+ if (!isVisible || !portalElement) {
263
+ return null;
274
264
  }
275
- }
276
265
 
277
- render() {
278
- return typeof this.props.target === 'function' ? null : this.props.target;
266
+ return createPortal(children, portalElement);
279
267
  }
280
268
  }
281
269
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bpk-scrim-utils",
3
- "version": "5.1.9",
3
+ "version": "5.1.10",
4
4
  "description": "Higher order component that adds an overlay behind components and manages scroll states.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -17,7 +17,7 @@
17
17
  "a11y-focus-scope": "^1.1.3",
18
18
  "a11y-focus-store": "^1.0.0",
19
19
  "bpk-mixins": "^31.1.1",
20
- "bpk-react-utils": "^4.1.2"
20
+ "bpk-react-utils": "^5.0.0"
21
21
  },
22
22
  "peerDependencies": {
23
23
  "react": "^16.3.0 || ^17.0.0"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyscanner/backpack-web",
3
- "version": "3.3.0",
3
+ "version": "4.0.0",
4
4
  "description": "Backpack Design System web library",
5
5
  "repository": {
6
6
  "type": "git",